from __future__ import annotations

import sys
import threading
from collections import deque
from typing import Callable, List, Optional

from pulselog.record import LogRecord

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
QUEUE_MAX_SIZE = 100_000  # FIX #3: raised from 10_000 — at 38k/sec fills in 2.6s not 260ms

# How many drops before we emit a warning to stderr. After the first warning
# we emit again every DROP_WARN_INTERVAL drops so logs aren't flooded.
_DROP_WARN_EVERY = 1_000


class LogQueue:
    """Thread-safe, non-blocking bounded queue for LogRecord objects.

    Internally backed by a ``collections.deque`` with ``maxlen`` set, so when
    the queue is full the **oldest** item is automatically evicted — the caller
    never blocks.

    All public methods complete in O(1) amortised time.
    """

    def __init__(
        self,
        maxsize: int = QUEUE_MAX_SIZE,
        on_drop: Optional[Callable[[int], None]] = None,
    ) -> None:
        """
        Args:
            maxsize: Maximum number of records held before oldest are evicted.
            on_drop: Optional callback invoked with the total drop count each
                     time a record is evicted.  Defaults to a stderr warning
                     emitted once per ``_DROP_WARN_EVERY`` drops.
        """
        self._deque: deque[LogRecord] = deque(maxlen=maxsize)  # guarded by self._lock
        self._lock = threading.Lock()
        self._event = threading.Event()
        self._dropped = 0          # guarded by self._lock
        self._last_warned = 0      # guarded by self._lock — last _dropped value we warned at

        # FIX #1: on_drop callback — default emits a stderr warning
        self._on_drop: Callable[[int], None] = on_drop or self._default_drop_warning

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

    def put(self, record: LogRecord) -> None:
        """Enqueue a log record.  Never blocks, never raises.

        If the queue is at capacity the oldest record is silently evicted
        (deque maxlen behaviour).  The background worker is woken up.

        Args:
            record: The LogRecord to enqueue.
        """
        notify_drop: Optional[int] = None
        was_empty: bool

        with self._lock:
            was_empty = len(self._deque) == 0          # FIX #6: track empty→nonempty
            was_full = len(self._deque) == self._deque.maxlen
            self._deque.append(record)
            if was_full:
                self._dropped += 1
                # FIX #1: notify every _DROP_WARN_EVERY drops — outside the lock
                if self._dropped - self._last_warned >= _DROP_WARN_EVERY:
                    self._last_warned = self._dropped
                    notify_drop = self._dropped

        # FIX #6: only wake the worker when queue transitions empty → non-empty,
        # not on every single put(). Reduces threading.Event lock acquisitions
        # from 200k/sec to only the first put() of each batch.
        if was_empty:
            self._event.set()

        # Call drop callback OUTSIDE the lock — it may do I/O (stderr write)
        if notify_drop is not None:
            try:
                self._on_drop(notify_drop)
            except Exception:
                pass  # drop callbacks must never crash the caller

    def drain(self) -> List[LogRecord]:
        """Atomically remove and return all queued records as a list.

        The lock is held only long enough to snapshot and clear the deque.
        Any downstream processing (serialisation, I/O) happens outside the
        lock so callers are not delayed.

        Returns:
            List of records in FIFO order; empty list if queue was empty.
        """
        with self._lock:
            if not self._deque:      # FIX #25: early return avoids list() alloc
                self._event.clear()
                return []
            # Snapshot then clear — both O(1).
            batch = list(self._deque)
            self._deque.clear()
            self._event.clear()
        return batch

    def dropped_count(self) -> int:
        """Return the number of records silently evicted due to overflow.

        Returns:
            Non-negative integer; resets only on queue replacement.
        """
        with self._lock:
            return self._dropped

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    def __len__(self) -> int:
        """Current number of records waiting in the queue."""
        with self._lock:
            return len(self._deque)

    def is_empty(self) -> bool:
        """True when no records are waiting."""
        with self._lock:
            return len(self._deque) == 0

    # ------------------------------------------------------------------
    # Private
    # ------------------------------------------------------------------

    @staticmethod
    def _default_drop_warning(total_dropped: int) -> None:
        """Emit a one-time stderr warning when records start being dropped."""
        sys.stderr.write(
            f"[pulselog] WARNING: queue full — {total_dropped:,} records dropped so far. "
            f"Consider increasing queue_size or reducing worker_interval. "
            f"Call logger.stats() to monitor drop rate.\n"
        )