"""C5 ``StateEstimator`` Protocol — AZ-381.

The single typed handle C8 / C4 / runtime root hold for the state
estimator. Concrete implementations live in
``gps_denied_onboard.components.c5_state.gtsam_isam2_estimator`` (AZ-382
onward) and ``...eskf_baseline`` (AZ-386). Both are link-time exclusive
via the ``BUILD_STATE_<variant>`` flags per ADR-002.

The Protocol is ``runtime_checkable`` so test fakes pass
``isinstance(fake, StateEstimator)`` without depending on a concrete
parent class (ADR-009 — interface-first DI).

See the contract at
``_docs/02_document/contracts/c5_state/state_estimator_protocol.md``
for the complete invariant list (10 invariants total).
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

if TYPE_CHECKING:
    from gps_denied_onboard._types.nav import ImuWindow
    from gps_denied_onboard._types.pose import LatLonAlt, PoseEstimate
    from gps_denied_onboard._types.state import (
        EstimatorHealth,
        EstimatorOutput,
    )
    from gps_denied_onboard._types.nav import VioOutput

__all__ = ["StateEstimator"]


@runtime_checkable
class StateEstimator(Protocol):
    """Smoothed state estimator (fuses VIO + satellite anchors + IMU).

    All six methods run on the same ingest thread (Invariant 1 —
    GTSAM iSAM2 is not thread-safe; composition root enforces). The
    six methods correspond 1:1 to the contract surface; concrete
    impls must implement every method.
    """

    def set_takeoff_origin(
        self,
        origin: LatLonAlt,
        sigma_horiz_m: float,
        sigma_vert_m: float,
    ) -> None:
        """Seed the cold-start prior with an operator-supplied origin (AZ-490 / ADR-010).

        Pre-takeoff entrypoint. Called by the composition root during
        F2 (Takeoff load) when the C10 ManifestVerifier reports a
        valid ``flight.takeoff_origin``. With this prior set, the
        FC-EKF first-frame cold-start path becomes the secondary
        fallback (Invariant 11).

        Contract:

        * Idempotent if called twice with byte-identical args (no-op).
        * Raises :class:`StateEstimatorConfigError` if called twice
          with different args, on negative / non-finite sigmas, or on
          a ``LatLonAlt`` outside WGS-84 bounds.
        * Raises :class:`EstimatorAlreadyStartedError` if called
          AFTER the first ``add_vio`` / ``add_pose_anchor`` /
          ``add_fc_imu`` (cold-start window closed).
        * Emits one ``c5.cold_start_origin.set`` FDR record with
          ``source="manifest"``.
        """

    def add_vio(self, vio: VioOutput) -> None:
        """Add a VIO output as a relative-pose factor to the iSAM2 graph."""

    def add_pose_anchor(self, pose: PoseEstimate) -> None:
        """Add a C4 pose anchor.

        Invariant 3 — ``pose.covariance_mode == "jacobian"`` MUST NOT
        produce an iSAM2 factor; only the marginals path triggers the
        factor + update cycle.
        """

    def add_fc_imu(self, imu_window: ImuWindow) -> None:
        """Add an FC IMU window to the iSAM2 preintegrator (CombinedImuFactor).

        AZ-383 fix-up: AZ-381 incorrectly annotated this as
        ``ImuTelemetrySample`` (a single sample); the C5 contract has
        always specified ``ImuWindow`` because the shared
        ``ImuPreintegrator`` operates on batches.
        """

    def current_estimate(self) -> EstimatorOutput:
        """Return the latest (non-smoothed) estimate. Never returns ``None``."""

    def smoothed_history(self, n_keyframes: int) -> list[EstimatorOutput]:
        """Return up to ``n_keyframes`` recent smoothed estimates.

        Every entry has ``smoothed=True`` (Invariant 7); never emitted
        to the FC (Invariant 6); bounded by the keyframe window K
        (Invariant 6 of the contract; D-C5-3 K∈[10,20]).
        """

    def health_snapshot(self) -> EstimatorHealth:
        """Return the current iSAM2 health snapshot."""
