"""C3.5 ``ConditionalRefiner`` Protocol (AZ-348).

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

Concrete impls — :class:`PassthroughRefiner` (this task) and
:class:`AdHoPRefiner` (AZ-349) — live in sibling modules.
Both are linked into the production binary unconditionally per
ADR-001; runtime selection is via ``config.refiner.strategy``
(NO ``BUILD_REFINER_*`` flag — NOT ADR-002 territory because
both strategies are tiny and the AdHoP TRT engine is shared C7
infrastructure).

The contract at
``_docs/02_document/contracts/c3_5_adhop/conditional_refiner_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.matcher import MatchResult
    from gps_denied_onboard._types.nav import NavCameraFrame

__all__ = ["ConditionalRefiner"]


@runtime_checkable
class ConditionalRefiner(Protocol):
    """Conditional refinement strategy between C3 (matcher) and C4 (pose).

    Stateless per-frame; the only persistent state is the
    constructor-injected backbone runtime handle (when the strategy
    uses one) and the last-invocation flag.

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

    - **INV-1 single-threaded** — each instance is bound to one
      ingest thread; same thread as C3 (shared C-frame ingest
      path).
    - **INV-2 stateless per-frame** — except for the
      ``was_invoked`` flag, no implicit dependency on prior frames;
      reordering :meth:`refine_if_needed` calls (tests only) MUST
      yield identical output ``MatchResult`` content.
    - **INV-3 conditional gate is a pure comparison** —
      ``mr.reprojection_residual_px <= threshold`` → passthrough;
      ``>`` → invoke. No tolerance, no smoothing, no hysteresis.
    - **INV-4 passthrough fall-through on backbone error** —
      :class:`RefinerBackboneError` raised inside the invoked path
      is caught by the strategy and converted to passthrough output
      with ``refinement_label = "passthrough"``; the error is
      logged at ERROR + emitted to FDR; NEVER re-raised out of
      :meth:`refine_if_needed`.
    - **INV-5 bit-identical correspondences on passthrough** —
      when ``refinement_label == "passthrough"``, every
      ``inlier_correspondences`` ndarray in the output equals the
      input ndarray bit-for-bit (``np.array_equal`` AND same
      ``dtype``).
    - **INV-6 ``refinement_label`` is `"adhop"` OR
      `"passthrough"`** — exactly one of those two values; matches
      the strategy's selected variant. Readers check
      :meth:`was_invoked` to discriminate "AdHoP ran" from
      "AdHoP-fell-through-to-passthrough".
    - **INV-7 ``refinement_added_latency_ms`` is strategy-internal
      added latency** — always ``>= 0``; near-zero on passthrough;
      up to ~90 ms on AdHoP invoke per C3.5-PT-01.
    - **INV-8 ``was_invoked()`` semantics** — set to ``True`` iff
      the strategy entered the refinement procedure (post-gate,
      regardless of whether AdHoP succeeded or fell through). On
      pure passthrough strategy + every gate-decided-passthrough
      call: ``False``.
    - **INV-9 threshold validation** — the strategy MUST reject
      ``residual_threshold_px <= 0`` (raise :class:`ValueError`);
      the composition root validates the config-loaded threshold
      at startup so this in-method check is defensive.
    """

    def refine_if_needed(
        self,
        frame: "NavCameraFrame",
        mr: "MatchResult",
        residual_threshold_px: float,
    ) -> "MatchResult":
        """Either pass ``mr`` through unchanged or run refinement.

        If ``mr.reprojection_residual_px <= residual_threshold_px``
        (the steady-state path), return ``mr`` unchanged AND set
        :meth:`was_invoked` to ``False``. Otherwise run the
        strategy's refinement procedure and return an enriched
        :class:`MatchResult` (typically via
        :func:`dataclasses.replace`) with ``refinement_label``
        set, AND set :meth:`was_invoked` to ``True``.

        On :class:`RefinerBackboneError` (AdHoP backbone failure
        during the invoked path), the refiner MUST fall through to
        passthrough — return ``mr`` unchanged with
        ``refinement_label = "passthrough"`` AND :meth:`was_invoked`
        ``True`` (the attempt counts towards the invocation rate
        even on failure). The error is logged at ERROR + emitted
        to FDR; downstream pose estimation gets a usable
        :class:`MatchResult` and decides whether to trigger F6.

        Determinism: same inputs MUST produce the same output. No
        probabilistic gating; no time-based gating.

        Raises:
            ValueError: ``residual_threshold_px <= 0`` (defensive;
                composition root should have caught this already).
        """
        ...

    def was_invoked(self) -> bool:
        """Return ``True`` iff the last :meth:`refine_if_needed`
        call entered the refinement procedure.

        Set at the start of every :meth:`refine_if_needed` call.
        Used by FDR per-frame provenance and by
        NFT-PERF-01 / C3.5-IT-03 invocation-rate accounting.
        """
        ...
