"""Background trace submitter — batches and POSTs traces to the Carrot API."""

from __future__ import annotations

import atexit
import json
import logging
import queue
import threading
import time
from typing import Any
from urllib.error import URLError
from urllib.request import Request, urlopen

logger = logging.getLogger("carrot_ai")

_SENTINEL = object()


class CarrotClient:
    """Async-batched HTTP client for submitting traces to the Carrot API.

    Traces are queued in-memory and flushed to the server by a background
    daemon thread.  ``flush()`` stops the worker, drains any remaining items,
    sends them synchronously, and restarts the worker — guaranteeing nothing
    is lost to a daemon-thread kill on interpreter exit.
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = "https://api.carrotlabs.ai",
        flush_interval: float = 5.0,
        batch_size: int = 50,
        max_queue: int = 1000,
    ) -> None:
        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._queue: queue.Queue[dict[str, Any] | object] = queue.Queue(
            maxsize=max_queue
        )
        self._flush_interval = flush_interval
        self._batch_size = batch_size
        self._shutdown = threading.Event()
        self._lifecycle_lock = threading.Lock()
        self._thread = self._start_worker()
        atexit.register(self.shutdown)

    # -- public ---------------------------------------------------------------

    def submit(self, trace: dict[str, Any]) -> None:
        """Enqueue a trace dict for background submission.  Non-blocking."""
        try:
            self._queue.put_nowait(trace)
        except queue.Full:
            logger.warning("carrot_ai: trace queue full, dropping trace")

    def flush(self) -> None:
        """Guarantee all queued traces are sent.

        Stops the background worker so nothing is stranded in its local
        batch, drains the queue, sends everything, then restarts the worker.
        """
        with self._lifecycle_lock:
            self._stop_worker()
            self._drain_and_send()
            self._thread = self._start_worker()

    def shutdown(self) -> None:
        """Final flush — drains and sends, does not restart the worker."""
        with self._lifecycle_lock:
            self._stop_worker()
            self._drain_and_send()

    # -- internals ------------------------------------------------------------

    def _start_worker(self) -> threading.Thread:
        self._shutdown.clear()
        t = threading.Thread(target=self._run, daemon=True, name="carrot-flush")
        t.start()
        return t

    def _stop_worker(self) -> None:
        """Signal the worker to exit and wait for it (including any in-flight send)."""
        self._shutdown.set()
        try:
            self._queue.put_nowait(_SENTINEL)
        except queue.Full:
            pass
        self._thread.join(timeout=10.0)

    def _drain_and_send(self) -> None:
        batch: list[dict[str, Any]] = []
        while True:
            try:
                item = self._queue.get_nowait()
                if item is _SENTINEL:
                    continue
                batch.append(item)  # type: ignore[arg-type]
            except queue.Empty:
                break
        if batch:
            self._send(batch)

    def _run(self) -> None:
        while not self._shutdown.is_set():
            batch: list[dict[str, Any]] = []
            deadline = time.monotonic() + self._flush_interval
            while time.monotonic() < deadline and len(batch) < self._batch_size:
                if self._shutdown.is_set():
                    break
                timeout = min(0.5, max(0.05, deadline - time.monotonic()))
                try:
                    item = self._queue.get(timeout=timeout)
                    if item is _SENTINEL:
                        break
                    batch.append(item)  # type: ignore[arg-type]
                except queue.Empty:
                    continue
            if batch:
                self._send(batch)

    def _send(self, traces: list[dict[str, Any]], *, _retries: int = 2) -> None:
        url = f"{self._base_url}/v1/traces/ingest"
        body = json.dumps({"traces": traces}).encode()

        for attempt in range(_retries + 1):
            req = Request(url, data=body, method="POST")
            req.add_header("Content-Type", "application/json")
            req.add_header("Authorization", f"Bearer {self._api_key}")
            try:
                with urlopen(req, timeout=10) as resp:
                    resp.read()
                logger.debug("carrot_ai: sent %d trace(s)", len(traces))
                return
            except (URLError, OSError) as exc:
                if attempt < _retries:
                    time.sleep(0.3 * (attempt + 1))
                    continue
                logger.warning(
                    "carrot_ai: failed to send %d trace(s) after %d attempt(s): %s",
                    len(traces),
                    _retries + 1,
                    exc,
                )
            except Exception:
                logger.warning(
                    "carrot_ai: unexpected error sending traces", exc_info=True
                )
                return
