from __future__ import annotations

import threading
from collections import deque
from typing import List

from pulselog.record import LogRecord

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
QUEUE_MAX_SIZE = 10_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) -> None:
        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 — tracks auto-evicted records

    # ------------------------------------------------------------------
    # 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.
        """
        with self._lock:
            was_full = len(self._deque) == self._deque.maxlen
            self._deque.append(record)
            if was_full:
                self._dropped += 1  # one old record was evicted
        # Wake the worker outside the lock so it can acquire it immediately.
        self._event.set()

    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:
                self._event.clear()
                return []
            # Snapshot then clear — both O(1).
            batch = list(self._deque)
            self._deque.clear()
            self._event.clear()
        # JSON serialisation / further processing happens here, outside lock.
        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
