"""C2.5 ``ReRankStrategy`` Protocol (AZ-342).

PEP 544 ``typing.Protocol`` with ``runtime_checkable=True``; a single
``rerank`` method that consumes a C2 :class:`VprResult` and produces
a :class:`RerankResult` ranked by single-pair LightGlue inlier count.

Concrete impl — :class:`InlierCountReRanker` (AZ-343) — lives in a
sibling module and is imported lazily by
:mod:`gps_denied_onboard.runtime_root.rerank_factory`.

The contract at
``_docs/02_document/contracts/c2_5_rerank/rerank_strategy_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.nav import NavCameraFrame
    from gps_denied_onboard._types.rerank import RerankResult
    from gps_denied_onboard._types.vpr import VprResult

__all__ = ["ReRankStrategy"]


@runtime_checkable
class ReRankStrategy(Protocol):
    """Single-camera re-rank strategy.

    Stateless per-frame; the only persistent state is the
    constructor-injected
    :class:`gps_denied_onboard.helpers.lightglue_runtime.LightGlueRuntime`
    helper handle and the :class:`TileStore` Public API reference.

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

    - **INV-1 single-threaded** — each instance is bound to one
      ingest thread; the shared ``LightGlueRuntime`` requires serial
      access. Concurrent :meth:`rerank` calls on a single instance
      race the GPU stream.
    - **INV-2 stateless per-frame** — same inputs → same surviving
      candidates in same order.
    - **INV-3 top-N descending by inlier_count** — ties broken
      deterministically by ``descriptor_distance`` ascending (the
      C2-stage value carried forward).
    - **INV-4 candidates length bounded** — ``0 < len <= n`` when
      returned (zero raises :class:`RerankAllCandidatesFailedError`);
      never exceeds ``n``; never exceeds
      ``len(vpr_result.candidates)``.
    - **INV-5 descriptor_distance carried forward unchanged** — the
      C2-stage value is preserved on every survivor for FDR
      provenance.
    - **INV-6 tile_pixels_handle is a reference, NOT a copy** —
      ``RerankCandidate.tile_pixels_handle`` is the same handle
      returned by ``TileStore.read_tile_pixels`` (page-cache
      backed).
    - **INV-7 deterministic per tuple** — same ``(frame,
      vpr_result, corpus, helper)`` → bit-identical
      :class:`RerankResult`.
    - **INV-8 drop-and-continue** — a per-candidate exception
      NEVER propagates out of :meth:`rerank` unless EVERY candidate
      fails. C3 relies on this partial-input tolerance.

    Error envelope: only :class:`RerankAllCandidatesFailedError`
    escapes :meth:`rerank`; per-candidate
    :class:`RerankBackboneError` / ``TileFetchError`` from C6 are
    caught inside the loop and turned into dropped candidates +
    ERROR logs + per-occurrence FDR records.
    """

    def rerank(
        self,
        frame: "NavCameraFrame",
        vpr_result: "VprResult",
        n: int,
        calibration: "CameraCalibration",
    ) -> "RerankResult":
        """Re-rank the top-K candidates down to top-N by inlier count.

        For each ``candidate`` in ``vpr_result.candidates``:

        1. Fetch tile pixels via ``TileStore.read_tile_pixels(candidate.tile_id)``.
        2. Run a single-pair LightGlue forward via the shared
           :class:`LightGlueRuntime` (frame ↔ tile).
        3. Record the inlier count.

        Sort candidates descending by inlier count; return the top-N
        as a :class:`RerankResult`. Drop-and-continue semantics
        apply per INV-8.

        Raises:
            RerankAllCandidatesFailedError: zero survivors after
                the per-candidate loop.
        """
        ...
