"""``LiveCameraFrameSource`` — live nav-camera ingest (AZ-398).

Wraps :class:`cv2.VideoCapture` against an integer device index (the
USB / CSI camera bound at boot by the airborne / research / operator
binaries). The strategy is intentionally minimal: each
:meth:`next_frame` call performs one blocking ``capture.read()`` and
returns the freshest frame; no dedicated decode thread, no ring
buffer. C1 (the only consumer) drives the loop at its target
rate, and a blocking read is the simplest way to apply backpressure.

Gated by ``BUILD_LIVE_CAMERA_FRAME_SOURCE`` (Invariant 9). The flag is
``ON`` for live / research / operator / replay binaries and ``OFF``
only for unit tests that need to construct a substitute without
touching a real camera.
"""

from __future__ import annotations

import logging
import os
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any

from gps_denied_onboard.frame_source.errors import (
    FrameSourceConfigError,
    FrameSourceError,
)

if TYPE_CHECKING:
    from gps_denied_onboard._types.nav import NavCameraFrame
    from gps_denied_onboard.clock import Clock


_BUILD_FLAG = "BUILD_LIVE_CAMERA_FRAME_SOURCE"

_logger = logging.getLogger(__name__)


def _build_flag_on() -> bool:
    raw = os.environ.get(_BUILD_FLAG, "")
    return raw.strip().lower() in {"on", "1", "true", "yes"}


class LiveCameraFrameSource:
    """Live :class:`FrameSource` strategy backed by ``cv2.VideoCapture``.

    Constructor parameters:

    - ``device_index`` — integer index passed to ``cv2.VideoCapture``;
      typically ``0`` for the first attached camera.
    - ``camera_calibration_id`` — string identifier baked into every
      emitted frame (matches the intrinsics file shipped with the
      binary).
    - ``clock`` — injected :class:`Clock`; supplies the per-frame
      ``monotonic_ns`` ordering key and the wall-clock timestamp.
    """

    __slots__ = (
        "_device_index",
        "_camera_calibration_id",
        "_clock",
        "_capture",
        "_frame_counter",
        "_last_monotonic_ns",
        "_closed",
    )

    def __init__(
        self,
        *,
        device_index: int,
        camera_calibration_id: str,
        clock: "Clock",
    ) -> None:
        if not _build_flag_on():
            raise FrameSourceConfigError(
                f"{_BUILD_FLAG} is OFF in this binary; "
                "LiveCameraFrameSource is unavailable."
            )
        try:
            import cv2 as _cv2
        except ImportError as exc:
            raise FrameSourceConfigError(
                "LiveCameraFrameSource requires opencv-python; not "
                "importable in this binary."
            ) from exc
        capture = _cv2.VideoCapture(device_index)
        if not capture.isOpened():
            capture.release()
            raise FrameSourceConfigError(
                f"LiveCameraFrameSource: cv2.VideoCapture could not open "
                f"device index {device_index}"
            )
        self._device_index = device_index
        self._camera_calibration_id = camera_calibration_id
        self._clock = clock
        self._capture = capture
        self._frame_counter = 0
        self._last_monotonic_ns = -1
        self._closed = False

    def next_frame(self) -> "NavCameraFrame | None":
        from gps_denied_onboard._types.nav import NavCameraFrame

        if self._closed:
            return None
        ok, image = self._capture.read()
        if not ok or image is None:
            # Live camera: a failed read is a transient error (USB
            # glitch, driver hiccup). Invariant 4 requires raising,
            # not returning None — the only legitimate None is EOS,
            # and a live camera never EOSes.
            raise FrameSourceError(
                f"LiveCameraFrameSource: cv2.VideoCapture.read failed at "
                f"frame {self._frame_counter} (device "
                f"{self._device_index})"
            )
        monotonic_ns = self._clock.monotonic_ns()
        if monotonic_ns < self._last_monotonic_ns:
            raise FrameSourceError(
                f"LiveCameraFrameSource: clock went backwards at frame "
                f"{self._frame_counter}: {monotonic_ns} ns followed "
                f"{self._last_monotonic_ns} ns (Invariant 3)"
            )
        timestamp = datetime.fromtimestamp(
            self._clock.time_ns() / 1e9, tz=timezone.utc
        )
        metadata: dict[str, Any] = {
            "monotonic_ns": monotonic_ns,
            "source": "live_camera",
            "device_index": self._device_index,
        }
        frame = NavCameraFrame(
            frame_id=self._frame_counter,
            timestamp=timestamp,
            image=image,
            camera_calibration_id=self._camera_calibration_id,
            metadata=metadata,
        )
        self._frame_counter += 1
        self._last_monotonic_ns = monotonic_ns
        return frame

    def close(self) -> None:
        if self._closed:
            _logger.debug(
                "LiveCameraFrameSource(device=%s) close called twice; no-op",
                self._device_index,
            )
            return
        self._closed = True
        try:
            self._capture.release()
        except Exception:  # pragma: no cover — defensive.
            _logger.exception(
                "LiveCameraFrameSource(device=%s) cv2.release() raised",
                self._device_index,
            )


__all__ = ["LiveCameraFrameSource"]
