"""Composition-root :class:`FrameSource` factory (AZ-398).

Selects exactly one :class:`FrameSource` strategy per binary based on
the requested ``kind`` and the compile-time ``BUILD_*`` flags. The
concrete strategy modules are imported lazily so a Tier-0 build with
``BUILD_LIVE_CAMERA_FRAME_SOURCE=OFF`` and
``BUILD_VIDEO_FILE_FRAME_SOURCE=OFF`` never pulls OpenCV into
``sys.modules`` (Invariant 9 — verifiable via ``sys.modules``).

Build-flag gating happens INSIDE the strategy constructor (so unit
tests that monkey-patch the env still hit the gate); this factory
performs the strategy-name → module mapping only.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from gps_denied_onboard.frame_source.errors import FrameSourceConfigError

if TYPE_CHECKING:
    from gps_denied_onboard.clock import Clock
    from gps_denied_onboard.frame_source import FrameSource


__all__ = ["build_frame_source"]


def build_frame_source(
    *,
    kind: str,
    camera_calibration_id: str,
    clock: "Clock",
    device_index: int | None = None,
    video_path: Path | str | None = None,
) -> "FrameSource":
    """Construct the :class:`FrameSource` strategy.

    ``kind`` is one of ``"live"`` or ``"video_file"``:

    - ``"live"`` requires ``device_index`` (integer camera index) and
      forbids ``video_path``.
    - ``"video_file"`` requires ``video_path`` (filesystem path) and
      forbids ``device_index``.

    Build-flag gating is enforced by the strategy constructor; this
    factory raises :class:`FrameSourceConfigError` ONLY on argument-
    shape mistakes (missing or extra parameters for the chosen kind).
    """
    if kind == "live":
        if device_index is None:
            raise FrameSourceConfigError(
                "build_frame_source(kind='live'): device_index is required"
            )
        if video_path is not None:
            raise FrameSourceConfigError(
                "build_frame_source(kind='live'): video_path must be None"
            )
        from gps_denied_onboard.frame_source.live_camera import (
            LiveCameraFrameSource,
        )

        return LiveCameraFrameSource(
            device_index=device_index,
            camera_calibration_id=camera_calibration_id,
            clock=clock,
        )
    if kind == "video_file":
        if video_path is None:
            raise FrameSourceConfigError(
                "build_frame_source(kind='video_file'): video_path is required"
            )
        if device_index is not None:
            raise FrameSourceConfigError(
                "build_frame_source(kind='video_file'): "
                "device_index must be None"
            )
        from gps_denied_onboard.frame_source.video_file import (
            VideoFileFrameSource,
        )

        return VideoFileFrameSource(
            path=video_path,
            camera_calibration_id=camera_calibration_id,
            clock=clock,
        )
    raise FrameSourceConfigError(
        f"build_frame_source: unknown kind {kind!r}; "
        "expected 'live' or 'video_file'"
    )
