"""C5 state estimator config block — AZ-381.

The block is registered into the global config registry via
``register_component_block("c5_state", C5StateConfig)``; the runtime
root reads ``config.components["c5_state"]`` and dispatches by
``strategy``. ADR-002 build-time-exclusion gating happens in
:mod:`gps_denied_onboard.runtime_root.state_factory`, not here.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Final

from gps_denied_onboard.config.schema import ConfigError

__all__ = ["KNOWN_STATE_STRATEGIES", "C5StateConfig"]


KNOWN_STATE_STRATEGIES: Final[frozenset[str]] = frozenset({"gtsam_isam2", "eskf"})

_KEYFRAME_WINDOW_MIN: Final[int] = 10
_KEYFRAME_WINDOW_MAX: Final[int] = 20


@dataclass(frozen=True)
class C5StateConfig:
    """C5 state-estimator config block.

    Fields per the C5 contract §"Config schema additions":

    - ``strategy`` — selects between ``"gtsam_isam2"`` (production
      default) and ``"eskf"`` (mandatory simple baseline per IT-12).
    - ``keyframe_window_size`` — D-C5-3 K∈[10,20] for the
      ``IncrementalFixedLagSmoother`` window.
    - ``spoof_promotion_min_stable_s`` — AC-NEW-2 minimum dwell time
      in ``STABLE_NON_SPOOFED`` before the spoof-promotion gate opens.
    - ``spoof_promotion_visual_consistency_tol_m`` — AC-NEW-8 visual
      consistency tolerance on the next anchor.
    - ``spoof_promotion_bounded_delta_m`` — AZ-490 / Principle #11
      third clause: max horizontal distance (m) between an inbound FC
      GPS sample and the smoother's current estimate; samples within
      the ring are admitted as ``BOUNDED_DELTA_SOFT``, samples outside
      are rejected and counted against the dwell-time clause.
    - ``default_takeoff_origin_sigma_horiz_m`` — AZ-490 default
      horizontal sigma when ``set_takeoff_origin`` callers omit one.
    - ``default_takeoff_origin_sigma_vert_m`` — AZ-490 default
      vertical sigma when ``set_takeoff_origin`` callers omit one.
    - ``no_estimate_fallback_s`` — AC-5.2 timeout before the
      runtime root drops to FC-IMU-only mode.
    """

    strategy: str = "gtsam_isam2"
    keyframe_window_size: int = 15
    spoof_promotion_min_stable_s: float = 10.0
    spoof_promotion_visual_consistency_tol_m: float = 30.0
    spoof_promotion_bounded_delta_m: float = 200.0
    default_takeoff_origin_sigma_horiz_m: float = 5.0
    default_takeoff_origin_sigma_vert_m: float = 10.0
    no_estimate_fallback_s: float = 3.0

    def __post_init__(self) -> None:
        if self.strategy not in KNOWN_STATE_STRATEGIES:
            raise ConfigError(
                f"C5StateConfig.strategy={self.strategy!r} not in {sorted(KNOWN_STATE_STRATEGIES)}"
            )
        if not (_KEYFRAME_WINDOW_MIN <= self.keyframe_window_size <= _KEYFRAME_WINDOW_MAX):
            raise ConfigError(
                "C5StateConfig.keyframe_window_size must be in "
                f"[{_KEYFRAME_WINDOW_MIN}, {_KEYFRAME_WINDOW_MAX}] (D-C5-3); "
                f"got {self.keyframe_window_size}"
            )
        if self.spoof_promotion_min_stable_s <= 0.0:
            raise ConfigError(
                "C5StateConfig.spoof_promotion_min_stable_s must be > 0; "
                f"got {self.spoof_promotion_min_stable_s}"
            )
        if self.spoof_promotion_visual_consistency_tol_m <= 0.0:
            raise ConfigError(
                "C5StateConfig.spoof_promotion_visual_consistency_tol_m must be > 0; "
                f"got {self.spoof_promotion_visual_consistency_tol_m}"
            )
        if self.spoof_promotion_bounded_delta_m <= 0.0:
            raise ConfigError(
                "C5StateConfig.spoof_promotion_bounded_delta_m must be > 0; "
                f"got {self.spoof_promotion_bounded_delta_m}"
            )
        if self.default_takeoff_origin_sigma_horiz_m <= 0.0:
            raise ConfigError(
                "C5StateConfig.default_takeoff_origin_sigma_horiz_m must be > 0; "
                f"got {self.default_takeoff_origin_sigma_horiz_m}"
            )
        if self.default_takeoff_origin_sigma_vert_m <= 0.0:
            raise ConfigError(
                "C5StateConfig.default_takeoff_origin_sigma_vert_m must be > 0; "
                f"got {self.default_takeoff_origin_sigma_vert_m}"
            )
        if self.no_estimate_fallback_s <= 0.0:
            raise ConfigError(
                "C5StateConfig.no_estimate_fallback_s must be > 0; "
                f"got {self.no_estimate_fallback_s}"
            )
