"""``FrameSource`` Protocol — public Layer 1 cross-cutting interface (AZ-398 v1.0.0).

Frozen per ``_docs/02_document/contracts/replay/replay_protocol.md``.

Two strategies implement this Protocol:

- :class:`LiveCameraFrameSource` — the formalised live camera ingest
  path (gated ``BUILD_LIVE_CAMERA_FRAME_SOURCE``).
- :class:`VideoFileFrameSource` — the replay-only file decoder (gated
  ``BUILD_VIDEO_FILE_FRAME_SOURCE``).

Consumers (C1 :class:`VioStrategy`) accept a :class:`FrameSource` via
constructor injection so production code stays mode-agnostic
(Invariant 1).
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

if TYPE_CHECKING:
    from gps_denied_onboard._types.nav import NavCameraFrame


@runtime_checkable
class FrameSource(Protocol):
    """A pluggable camera-frame producer.

    The Protocol exposes two methods and one ordering invariant:

    - :meth:`next_frame` returns the next :class:`NavCameraFrame` (with
      ``metadata["monotonic_ns"]`` set by the strategy from its
      injected :class:`Clock`) or ``None`` ONLY when the stream is
      permanently exhausted (Invariant 4).
    - Consecutive ``next_frame()`` returns MUST have non-decreasing
      ``metadata["monotonic_ns"]`` (Invariant 3); out-of-order frames
      raise :class:`FrameSourceError`.
    - :meth:`close` releases the underlying capture handle and is
      idempotent (AC-10).
    """

    def next_frame(self) -> "NavCameraFrame | None":
        """Return the next frame, ``None`` on end-of-stream.

        Transient I/O failures (decode error, disconnect) MUST raise
        :class:`FrameSourceError` — never return ``None`` silently
        (Invariant 4). After ``None`` has been returned once, every
        subsequent call MUST also return ``None`` (idempotent EOS).
        """
        ...

    def close(self) -> None:
        """Release the underlying capture handle.

        Idempotent: a second call is a no-op (AC-10); the strategy
        SHOULD log a DEBUG line on the second call so a debug trace
        can prove no double-free occurred.
        """
        ...


__all__ = ["FrameSource"]
