"""Async batch transport — daemon thread flushes events to the ingestion API."""

from __future__ import annotations

import atexit
import gzip
import json
import logging
import queue
import threading
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from typing import Any

import requests

from llm_observatory.config import get_config

logger = logging.getLogger("llm_observatory")

_transport: Transport | None = None
_lock = threading.Lock()
_missing_config_warned = False
_dropped_count: int = 0


def get_transport() -> Transport:
    global _transport
    if _transport is None:
        with _lock:
            if _transport is None:
                _transport = Transport()
                _transport.start()
    return _transport


def shutdown_transport() -> None:
    global _transport, _missing_config_warned, _dropped_count
    with _lock:
        if _transport is not None:
            _transport.shutdown()
            _transport = None
        _missing_config_warned = False
        _dropped_count = 0


class Transport:
    def __init__(self) -> None:
        cfg = get_config()
        self._queue: queue.Queue[dict[str, Any]] = queue.Queue(
            maxsize=cfg["max_queue_size"]
        )
        self._flush_interval: float = cfg["flush_interval"]
        self._flush_batch_size: int = cfg["flush_batch_size"]
        self._running = False
        self._thread: threading.Thread | None = None
        self._executor: ThreadPoolExecutor | None = None

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._executor = ThreadPoolExecutor(max_workers=2)
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()
        atexit.register(self.shutdown)

    def enqueue(self, event: dict[str, Any]) -> None:
        global _dropped_count
        try:
            self._queue.put_nowait(event)
        except queue.Full:
            _dropped_count += 1

    def flush(self) -> None:
        """Explicit flush — synchronous so the caller knows it completed."""
        events = self._drain()
        if events:
            self._send_in_batches(events)

    def shutdown(self) -> None:
        self._running = False
        self.flush()
        if self._executor is not None:
            self._executor.shutdown(wait=True)
            self._executor = None
        if self._thread is not None and self._thread.is_alive():
            self._thread.join(timeout=5.0)

    def _run(self) -> None:
        while self._running:
            deadline = time.monotonic() + self._flush_interval
            events: list[dict[str, Any]] = []

            while time.monotonic() < deadline and self._running:
                try:
                    remaining = max(0.1, deadline - time.monotonic())
                    event = self._queue.get(timeout=remaining)
                    events.append(event)
                    if len(events) >= self._flush_batch_size:
                        break
                except queue.Empty:
                    continue

            if events and self._executor is not None:
                batch = list(events)
                self._executor.submit(self._send, batch)

        # Final drain on exit — send synchronously to ensure delivery
        remaining = self._drain()
        if remaining:
            self._send_in_batches(remaining)

    def _drain(self) -> list[dict[str, Any]]:
        events: list[dict[str, Any]] = []
        while True:
            try:
                events.append(self._queue.get_nowait())
            except queue.Empty:
                break
        return events

    def _send_in_batches(self, events: list[dict[str, Any]]) -> None:
        """Send events in chunks of flush_batch_size."""
        for i in range(0, len(events), self._flush_batch_size):
            batch = events[i : i + self._flush_batch_size]
            self._send(batch)

    def _send(self, events: list[dict[str, Any]]) -> None:
        global _missing_config_warned, _dropped_count

        # Log and reset dropped count
        if _dropped_count > 0:
            count = _dropped_count
            _dropped_count = 0
            logger.warning(
                "llm_observatory: %d event(s) dropped due to full queue", count,
                extra={"event_count": count, "queue_size": self._queue.maxsize},
            )

        cfg = get_config()
        base_url = cfg.get("base_url")
        api_key = cfg.get("api_key")
        if not base_url or not api_key:
            if not _missing_config_warned:
                _missing_config_warned = True
                missing = []
                if not base_url:
                    missing.append("base_url")
                if not api_key:
                    missing.append("api_key")
                logger.warning(
                    "llm_observatory: dropping %d event(s) — %s not configured. "
                    "Call configure() or set LLM_OBSERVATORY_BASE_URL / LLM_OBSERVATORY_API_KEY.",
                    len(events),
                    " and ".join(missing),
                )
            return

        url = f"{base_url.rstrip('/')}/v1/ingest"
        body = gzip.compress(json.dumps(events).encode())
        batch_id = str(uuid.uuid4())
        headers = {
            "Content-Type": "application/json",
            "Content-Encoding": "gzip",
            "Authorization": f"Bearer {api_key}",
            "X-Request-ID": batch_id,
        }

        delays = [0, 1.0, 2.0]
        for attempt, delay in enumerate(delays):
            if delay > 0:
                time.sleep(delay)
            try:
                resp = requests.post(url, data=body, headers=headers, timeout=10)
                if resp.status_code < 500:
                    return  # Success or client error — don't retry
            except (requests.ConnectionError, requests.Timeout) as exc:
                logger.debug(
                    "llm_observatory: send attempt %d failed: %s", attempt + 1, exc
                )
                continue
            except Exception as exc:
                logger.debug("llm_observatory: send failed: %s", exc)
                return

        logger.warning("llm_observatory: failed to send %d events after 3 attempts", len(events))
