"""Bounded per-kind telemetry rings (AZ-391 / E-C8).

Single decode thread pushes; arbitrary number of consumer threads
``peek`` (lock-free read of the latest sample) and ``snapshot`` (copy
of the entire ring under the lock). Drop-oldest on overflow — emits a
single INFO log at the FIRST overflow per ring instance (subsequent
overflows are counted but not logged to avoid drowning the log).

Why a custom ring and not :class:`collections.deque(maxlen=N)`:

- We need a single INFO log on the FIRST overflow event, plus a
  monotonic dropped-counter that survives across overflows.
- ``deque`` push is GIL-fast but the dropped-counter would still need
  a separate lock; consolidating both behind a single ring is simpler
  and gives ``peek_latest`` for free as O(1).

The ring is thread-safe at the Python level (one lock guards both
``push`` and ``snapshot``). Decoder callbacks fire OUTSIDE the ring
lock per Invariant 8.
"""

from __future__ import annotations

import threading
from collections import deque
from collections.abc import Iterable
from typing import Final, Generic, TypeVar

from gps_denied_onboard.logging import get_logger

__all__ = ["TelemetryRing"]


T = TypeVar("T")

_OVERFLOW_KIND: Final[str] = "c8.inbound.ring_overflow"


class TelemetryRing(Generic[T]):
    """Bounded drop-oldest ring buffer for one telemetry kind."""

    def __init__(self, capacity: int, *, kind_name: str) -> None:
        if capacity <= 0:
            raise ValueError(f"TelemetryRing capacity must be > 0; got {capacity}")
        self._capacity = capacity
        self._kind_name = kind_name
        self._lock = threading.Lock()
        self._items: deque[T] = deque(maxlen=capacity)
        self._dropped_count = 0
        self._overflow_logged = False
        self._log = get_logger("c8_fc_adapter.telemetry_ring")

    @property
    def capacity(self) -> int:
        return self._capacity

    @property
    def kind_name(self) -> str:
        return self._kind_name

    @property
    def dropped_count(self) -> int:
        with self._lock:
            return self._dropped_count

    def push(self, item: T) -> bool:
        """Push ``item`` (drop-oldest on overflow). Returns ``False`` on drop."""
        with self._lock:
            if len(self._items) >= self._capacity:
                self._dropped_count += 1
                should_log_first = not self._overflow_logged
                self._items.append(item)
                if should_log_first:
                    self._overflow_logged = True
            else:
                self._items.append(item)
                should_log_first = False

        if should_log_first:
            self._log.info(
                f"c8.inbound.ring_overflow: kind={self._kind_name} capacity={self._capacity}",
                extra={
                    "kind": _OVERFLOW_KIND,
                    "kv": {
                        "ring_kind": self._kind_name,
                        "capacity": self._capacity,
                    },
                },
            )
            return False
        return True

    def peek_latest(self) -> T | None:
        """Return the most-recently-pushed item, or None if empty."""
        with self._lock:
            if not self._items:
                return None
            return self._items[-1]

    def snapshot(self) -> list[T]:
        """Return a shallow copy of the ring's contents (oldest -> newest)."""
        with self._lock:
            return list(self._items)

    def __len__(self) -> int:
        with self._lock:
            return len(self._items)

    def extend(self, items: Iterable[T]) -> None:
        """Push multiple items; per-item drop accounting still applies."""
        for item in items:
            self.push(item)
