"""C4 ``PoseEstimator`` Protocol (AZ-355).

Per ADR-009 (interface-first DI) consumers (C5, runtime root, tests)
hold a typed reference to ``PoseEstimator`` rather than the concrete
``OpenCVGtsamPoseEstimator`` (AZ-358) class. ADR-002 build-time
exclusion does NOT apply — exactly one concrete implementation
exists — but the Protocol + factory wiring matches the C2 / C2.5 /
C3 / C3.5 pattern for symmetry.

The Protocol surface is two methods:

* :meth:`PoseEstimator.estimate` — full per-frame pose estimation
  pipeline (PnP + factor add + covariance recovery). Raises
  :class:`PnpFailureError` on RANSAC failure.
* :meth:`PoseEstimator.current_covariance_mode` — exposes the
  per-frame decision (marginals / jacobian) for C5 FDR provenance
  and the C4-IT-03 mode-switch test.

The Protocol is ``@runtime_checkable`` so test fakes pass
``isinstance(fake, PoseEstimator)``.
"""

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
    from gps_denied_onboard._types.pose import CovarianceMode, PoseEstimate
    from gps_denied_onboard._types.thermal import ThermalState

__all__ = ["PoseEstimator"]


@runtime_checkable
class PoseEstimator(Protocol):
    """Single-pose estimator producing WGS84 + 6x6 covariance + provenance label.

    Stateless per-frame except for the constructor-injected shared
    GTSAM substrate (owned by C5). Per Invariant 1 the implementation
    is bound to the same ingest thread as C5 (composition root
    enforces).
    """

    def estimate(
        self,
        match_result: MatchResult,
        calibration: CameraCalibration,
        thermal_state: ThermalState,
    ) -> PoseEstimate:
        """Run PnP → factor add → covariance recovery.

        Per-frame thermal decision: ``thermal_state.thermal_throttle_active == True``
        engages the Jacobian path (cheap, ~5-10 % accuracy loss);
        ``False`` engages the Marginals path (production default).

        Raises:
            PnpFailureError: RANSAC convergence failure or degenerate
                match geometry. C5 owns the fallback decision; this
                method NEVER returns a fallback ``PoseEstimate``.
        """

    def current_covariance_mode(self) -> CovarianceMode:
        """Return the mode used for the LAST :meth:`estimate` call.

        Consumed by C5 for FDR provenance and by C4-IT-03 to verify
        the per-frame mode switch.
        """
