"""C3 ``CrossDomainMatcher`` Protocol (AZ-344).

PEP 544 ``typing.Protocol`` with ``runtime_checkable=True``; a
two-method surface: :meth:`match` and :meth:`health_snapshot`.

Concrete impls — DISK+LightGlue (AZ-345), ALIKED+LightGlue
(AZ-346), XFeat (AZ-347) — live in sibling modules and are imported
lazily by
:mod:`gps_denied_onboard.runtime_root.matcher_factory`.

The contract at
``_docs/02_document/contracts/c3_matcher/cross_domain_matcher_protocol.md``
v1.0.0 is the authoritative shape; this module mirrors it 1:1.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

if TYPE_CHECKING:
    from gps_denied_onboard._types.calibration import CameraCalibration
    from gps_denied_onboard._types.matcher import MatchResult, MatcherHealth
    from gps_denied_onboard._types.nav import NavCameraFrame
    from gps_denied_onboard._types.rerank import RerankResult

__all__ = ["CrossDomainMatcher"]


@runtime_checkable
class CrossDomainMatcher(Protocol):
    """Cross-domain (nav-camera ↔ satellite-imagery) matcher strategy.

    Stateless per-frame; the only persistent state is the
    constructor-injected backbone runtime handles
    (``InferenceRuntime`` for feature extraction,
    :class:`gps_denied_onboard.helpers.lightglue_runtime.LightGlueRuntime`
    for matching,
    :class:`gps_denied_onboard.helpers.ransac_filter.RansacFilter`
    for inlier filtering) and the rolling
    :class:`gps_denied_onboard.components.c3_matcher._health_window.RollingHealthWindow`.

    Invariants (see ``cross_domain_matcher_protocol.md`` v1.0.0):

    - **INV-1 single-threaded** — each instance is bound to one
      ingest thread by the composition root (same thread as C2.5
      because they share the ``LightGlueRuntime``).
    - **INV-2 stateless per-frame for `match`** — except for the
      rolling health window, no implicit dependency on prior
      frames; reordering ``match`` calls (tests only) MUST yield
      identical ``MatchResult`` content.
    - **INV-3 best-candidate selection is deterministic** —
      ``MatchResult.best_candidate_idx == 0`` and
      ``per_candidate`` is sorted by ``inlier_count`` descending
      with ties broken by ``per_candidate_residual_px`` ascending
      (lower residual wins).
    - **INV-4 drop-and-continue per candidate** — per-candidate
      exceptions never propagate out of ``match`` unless every
      candidate fails. Mirrors C2.5 INV-8.
    - **INV-5 ``per_candidate`` length is bounded** —
      ``0 < len <= len(rerank_result.candidates)``; zero raises
      :class:`InsufficientInliersError`; never exceeds the input N.
    - **INV-6 ``matcher_label`` is non-empty** — every
      ``MatchResult`` carries the strategy's name
      (e.g., ``"disk_lightglue"``) for FDR provenance; MUST match
      ``BUILD_MATCHER_<variant>`` lowercase.
    - **INV-7 ``inlier_correspondences`` shape contract** —
      ``ndarray[I, 4, dtype=float32]``, columns
      ``(px_query, py_query, px_tile, py_tile)``; rows are RANSAC
      inliers only; ``I == inlier_count``.
    - **INV-8 ``reprojection_residual_px`` is the BEST candidate's
      median residual** — not the mean, not the max; downstream
      C3.5's threshold gate compares against this value.
    - **INV-9 ``health_snapshot()`` is cheap** — O(1); reads the
      rolling window's pre-computed accumulators. Never recomputes
      over the window contents.

    Error envelope: only :class:`InsufficientInliersError` escapes
    :meth:`match`; per-candidate
    :class:`MatcherBackboneError` and C6 ``TileFetchError``
    instances are caught inside the loop and turned into dropped
    candidates + ERROR logs + per-occurrence FDR records.
    """

    def match(
        self,
        frame: "NavCameraFrame",
        rerank_result: "RerankResult",
        calibration: "CameraCalibration",
    ) -> "MatchResult":
        """Match a frame against every candidate in ``rerank_result``.

        For each candidate:

        1. Extract features from the nav frame and from the tile
           pixels (via the constructor-injected
           :class:`InferenceRuntime` handle).
        2. Run LightGlue forward via the shared
           :class:`LightGlueRuntime`.
        3. RANSAC-filter correspondences via the shared
           :class:`RansacFilter`; record inliers + median residual.

        Sort survivors by ``(inlier_count desc, residual asc)`` and
        return as :class:`MatchResult`. Drop-and-continue
        semantics apply per INV-4.

        Raises:
            InsufficientInliersError: zero survivors after the
                per-candidate loop, OR every candidate's
                ``inlier_count`` is below
                ``config.matcher.min_inliers_threshold``.
        """
        ...

    def health_snapshot(self) -> "MatcherHealth":
        """Return a rolling-window matcher health snapshot.

        O(1) per INV-9. Drives C5's spoof-promotion gate
        (AC-NEW-2 / AC-NEW-7) and post-flight forensics.
        """
        ...
