"""
EventBatcher — fire-and-forget event queue with periodic flushing.

Buffers tracking events (tool calls, decisions, errors, generic events)
and flushes them in batches to reduce HTTP overhead. Session lifecycle
calls (create_session, complete_session) bypass the batcher entirely
because they need synchronous responses.

Flush triggers:
  1. Timer — daemon thread flushes every ``flush_interval`` seconds (default 1.0)
  2. Queue size — when queue reaches ``flush_threshold`` (default 10)
  3. Manual — ``flush()`` or ``shutdown()``
  4. Process exit — ``atexit`` handler

Back-pressure: queue is capped at ``max_queue_size`` (default 1 000).
When full, oldest events are dropped and a warning is logged.
"""

from __future__ import annotations

import atexit
import logging
import queue
import threading
from dataclasses import dataclass, field
from typing import Any, Callable, Optional

_logger = logging.getLogger("sentrial")


@dataclass
class BatcherConfig:
    """Configuration for the EventBatcher."""

    enabled: bool = False
    """Whether batching is enabled (default False — all calls are immediate)."""

    flush_interval: float = 1.0
    """Seconds between automatic flushes (default 1.0)."""

    flush_threshold: int = 10
    """Flush when the queue reaches this many items (default 10)."""

    max_queue_size: int = 1_000
    """Maximum queue size before dropping oldest events (default 1 000)."""


@dataclass
class _QueuedEvent:
    method: str
    url: str
    kwargs: dict[str, Any] = field(default_factory=dict)


# Type alias for the send function the batcher delegates to.
SendFn = Callable[..., Optional[Any]]


class EventBatcher:
    """Background event batcher with a daemon flush thread."""

    def __init__(self, send_fn: SendFn, config: BatcherConfig | None = None) -> None:
        cfg = config or BatcherConfig()
        self._send_fn = send_fn
        self._flush_interval = cfg.flush_interval
        self._flush_threshold = cfg.flush_threshold
        self._max_queue_size = cfg.max_queue_size

        self._queue: queue.Queue[_QueuedEvent] = queue.Queue(maxsize=0)  # unbounded
        self._queue_size = 0  # atomic-ish counter for fast threshold checks
        self._lock = threading.Lock()
        self._shutdown_event = threading.Event()

        # Daemon thread — dies automatically when the main thread exits
        self._thread = threading.Thread(target=self._run, daemon=True, name="sentrial-batcher")
        self._thread.start()

        # Best-effort flush on interpreter shutdown
        atexit.register(self._atexit_flush)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def enqueue(self, method: str, url: str, **kwargs: Any) -> None:
        """Add an event to the queue for batched delivery."""
        if self._shutdown_event.is_set():
            return

        # Back-pressure: drop oldest when at capacity
        with self._lock:
            if self._queue_size >= self._max_queue_size:
                try:
                    self._queue.get_nowait()
                    _logger.warning(
                        "Sentrial: Event queue full (%d), dropping oldest event",
                        self._max_queue_size,
                    )
                except queue.Empty:
                    pass
                else:
                    self._queue_size -= 1

        self._queue.put(_QueuedEvent(method=method, url=url, kwargs=kwargs))
        with self._lock:
            self._queue_size += 1

        # Trigger immediate flush if we hit the threshold
        if self._queue_size >= self._flush_threshold:
            self.flush()

    def flush(self) -> None:
        """Flush all queued events synchronously (blocks until done)."""
        batch: list[_QueuedEvent] = []
        while True:
            try:
                batch.append(self._queue.get_nowait())
            except queue.Empty:
                break

        with self._lock:
            self._queue_size = 0

        for event in batch:
            try:
                self._send_fn(event.method, event.url, **event.kwargs)
            except Exception:
                _logger.warning("Sentrial: Batched event failed", exc_info=True)

    def shutdown(self) -> None:
        """Stop the background thread and perform a final flush."""
        if self._shutdown_event.is_set():
            return
        self._shutdown_event.set()
        self._thread.join(timeout=5.0)
        self.flush()  # drain anything remaining

    @property
    def size(self) -> int:
        """Number of events currently queued."""
        return self._queue_size

    # ------------------------------------------------------------------
    # Internal
    # ------------------------------------------------------------------

    def _run(self) -> None:
        """Daemon loop: periodically flush the queue."""
        while not self._shutdown_event.is_set():
            self._shutdown_event.wait(timeout=self._flush_interval)
            if not self._shutdown_event.is_set():
                self.flush()

    def _atexit_flush(self) -> None:
        """Best-effort flush at interpreter shutdown."""
        self._shutdown_event.set()
        self.flush()
