"""High-throughput queue primitives for foundation/io data pipelines.

Profiling baseline (from #1467 hot-path report):
  - stdlib threading.Queue enqueue:  6.24 µs
  - stdlib threading.Queue dequeue:  7.02 µs
  - Target with FastQueue:           < 0.5 µs (single-thread) / < 2 µs (MT)

Two implementations are provided so callers can choose the right trade-off:

``SingleThreadQueue``
    Bare ``collections.deque``; no locking.  Use for single-thread pipelines
    (e.g. the common ``source → transform → sink`` chain running in one
    thread or coroutine).  Offers ~O(1) amortised push/pop and zero lock
    overhead.

``ThreadSafeFastQueue``
    ``collections.deque`` + one ``threading.Condition``.  Drop-in
    replacement for ``queue.Queue`` with substantially lower per-operation
    overhead: a single acquire/release per operation instead of two
    (``queue.Queue`` acquires both its mutex *and* one of the not-empty /
    not-full conditions on every put/get).

Both expose the same core API::

    q.put(item)                 # blocking put (no-op lock in single-thread)
    q.put_nowait(item)          # non-blocking put
    item = q.get()              # blocking get
    item = q.get_nowait()       # non-blocking get; raises QueueEmpty if empty
    n    = q.qsize()
    flag = q.empty()

Example::

    from sage.libs.foundation.io._fast_queue import FastQueue, SingleThreadQueue

    # single-threaded hot path
    q = SingleThreadQueue(maxsize=0)
    q.put_nowait({"payload": "data"})
    item = q.get_nowait()

    # multi-threaded pipeline (producer + consumer threads)
    q = FastQueue(maxsize=1024)
    q.put({"payload": "data"})   # blocks if full
    item = q.get()               # blocks until item available
"""

from __future__ import annotations

import collections
import threading
from typing import Generic, TypeVar

_T = TypeVar("_T")

# Sentinel for Empty exception
_EMPTY = object()


class QueueEmpty(Exception):
    """Raised by get_nowait() when the queue is empty."""


class QueueFull(Exception):
    """Raised by put_nowait() when the queue is full (maxsize > 0)."""


# ---------------------------------------------------------------------------
# SingleThreadQueue — bare deque, zero lock overhead
# ---------------------------------------------------------------------------


class SingleThreadQueue(Generic[_T]):
    """Lock-free FIFO queue backed by ``collections.deque``.

    Safe only within a single thread or coroutine.  Substantially faster than
    ``threading.Queue`` when concurrency is not required:
    ``deque.append`` / ``deque.popleft`` are O(1) and involve no Python-level
    locking.

    Args:
        maxsize: Maximum number of items (0 = unbounded).
    """

    __slots__ = ("_dq", "_maxsize")

    def __init__(self, maxsize: int = 0) -> None:
        self._maxsize = maxsize
        self._dq: collections.deque[_T] = collections.deque()

    # ------------------------------------------------------------------
    # Core interface
    # ------------------------------------------------------------------

    def put(self, item: _T) -> None:
        """Enqueue *item*.

        Raises:
            QueueFull: If *maxsize* > 0 and the queue is at capacity.
        """
        if self._maxsize > 0 and len(self._dq) >= self._maxsize:
            raise QueueFull("SingleThreadQueue is full")
        self._dq.append(item)

    def put_nowait(self, item: _T) -> None:
        """Non-blocking enqueue.

        Raises:
            QueueFull: If the queue is at capacity.
        """
        self.put(item)

    def get(self) -> _T:
        """Dequeue the oldest item.

        Raises:
            QueueEmpty: If the queue is empty.
        """
        if not self._dq:
            raise QueueEmpty("SingleThreadQueue is empty")
        return self._dq.popleft()

    def get_nowait(self) -> _T:
        """Non-blocking dequeue.

        Raises:
            QueueEmpty: If the queue is empty.
        """
        return self.get()

    def qsize(self) -> int:
        """Return number of items currently in the queue."""
        return len(self._dq)

    def empty(self) -> bool:
        """Return ``True`` if the queue contains no items."""
        return not self._dq

    def full(self) -> bool:
        """Return ``True`` if the queue is at its maxsize capacity."""
        if self._maxsize <= 0:
            return False
        return len(self._dq) >= self._maxsize


# ---------------------------------------------------------------------------
# ThreadSafeFastQueue — deque + single Condition, lower overhead than Queue
# ---------------------------------------------------------------------------


class ThreadSafeFastQueue(Generic[_T]):
    """Thread-safe FIFO queue using ``collections.deque`` + one
    ``threading.Condition``.

    Compared with the stdlib ``queue.Queue``:

    - ``queue.Queue`` acquires its mutex lock *and* notifies one of the
      ``not_empty`` / ``not_full`` ``Condition`` objects on every operation —
      effectively two lock operations per put/get.
    - ``ThreadSafeFastQueue`` uses a single ``Condition`` (which wraps one
      ``RLock``), halving the lock overhead for the common uncontended case.

    This queue is well-suited for single-producer / single-consumer (SPSC)
    hot paths.  For highly contended MPMC scenarios, consider a C++ lockfree
    queue via pybind11 (planned in a later iteration of #1469).

    Args:
        maxsize: Maximum number of items (0 = unbounded).
    """

    __slots__ = ("_dq", "_maxsize", "_cond")

    def __init__(self, maxsize: int = 0) -> None:
        self._maxsize = maxsize
        self._dq: collections.deque[_T] = collections.deque()
        self._cond: threading.Condition = threading.Condition(threading.Lock())

    # ------------------------------------------------------------------
    # Core interface
    # ------------------------------------------------------------------

    def put(self, item: _T, block: bool = True, timeout: float | None = None) -> None:
        """Enqueue *item*, optionally blocking until space is available.

        Args:
            item: Item to enqueue.
            block: If ``True`` (default) block until space is available.
            timeout: Maximum seconds to wait when *block* is ``True``.  ``None``
                means wait indefinitely.

        Raises:
            QueueFull: If *block* is ``False`` and queue is at capacity, or if
                the timeout expires before space becomes available.
        """
        with self._cond:
            if self._maxsize > 0:
                if not block:
                    if len(self._dq) >= self._maxsize:
                        raise QueueFull("ThreadSafeFastQueue is full")
                elif timeout is None:
                    while len(self._dq) >= self._maxsize:
                        self._cond.wait()
                else:
                    import time

                    deadline = time.monotonic() + timeout
                    while len(self._dq) >= self._maxsize:
                        remaining = deadline - time.monotonic()
                        if remaining <= 0:
                            raise QueueFull("ThreadSafeFastQueue put timed out")
                        self._cond.wait(remaining)
            self._dq.append(item)
            self._cond.notify()

    def put_nowait(self, item: _T) -> None:
        """Non-blocking enqueue.

        Raises:
            QueueFull: If the queue is at capacity.
        """
        self.put(item, block=False)

    def get(self, block: bool = True, timeout: float | None = None) -> _T:
        """Dequeue the oldest item, optionally blocking until one is available.

        Args:
            block: If ``True`` (default) block until an item is available.
            timeout: Maximum seconds to wait when *block* is ``True``.

        Raises:
            QueueEmpty: If *block* is ``False`` and queue is empty, or if the
                timeout expires before an item becomes available.
        """
        with self._cond:
            if not block:
                if not self._dq:
                    raise QueueEmpty("ThreadSafeFastQueue is empty")
            elif timeout is None:
                while not self._dq:
                    self._cond.wait()
            else:
                import time

                deadline = time.monotonic() + timeout
                while not self._dq:
                    remaining = deadline - time.monotonic()
                    if remaining <= 0:
                        raise QueueEmpty("ThreadSafeFastQueue get timed out")
                    self._cond.wait(remaining)
            item = self._dq.popleft()
            if self._maxsize > 0:
                self._cond.notify()  # wake potential producers
            return item

    def get_nowait(self) -> _T:
        """Non-blocking dequeue.

        Raises:
            QueueEmpty: If the queue is empty.
        """
        return self.get(block=False)

    def qsize(self) -> int:
        """Return approximate number of items in the queue."""
        return len(self._dq)

    def empty(self) -> bool:
        """Return ``True`` if the queue appears empty (approximate under MT)."""
        return not self._dq

    def full(self) -> bool:
        """Return ``True`` if the queue appears at capacity (approximate under MT)."""
        if self._maxsize <= 0:
            return False
        return len(self._dq) >= self._maxsize


# ---------------------------------------------------------------------------
# Public alias — ``FastQueue`` → ``ThreadSafeFastQueue`` by default
# ---------------------------------------------------------------------------

#: Drop-in replacement for ``queue.Queue`` with lower per-operation overhead.
#: Points to :class:`ThreadSafeFastQueue`.
FastQueue = ThreadSafeFastQueue
