"""C1 VIO strategy config block (AZ-331 + AZ-332).

Registered into ``config.components['c1_vio']`` by the package
``__init__.py``. The composition-root factory
:func:`gps_denied_onboard.runtime_root.vio_factory.build_vio_strategy`
reads this block to select the strategy and configure the LOST->FATAL
transition + warm-start convergence budget.

AZ-332 extends this with a nested :class:`Okvis2Config` sub-block
carrying OKVIS2-specific knobs (sliding-window size, parallax-driven
keyframe threshold, RANSAC inlier ratio, max optimisation iterations,
degraded-feature threshold, per-frame debug log). Only consulted when
``strategy == "okvis2"``.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Final

from gps_denied_onboard.config.schema import ConfigError

__all__ = [
    "KNOWN_STRATEGIES",
    "C1VioConfig",
    "Okvis2Config",
]

KNOWN_STRATEGIES: Final[frozenset[str]] = frozenset({"okvis2", "vins_mono", "klt_ransac"})


@dataclass(frozen=True)
class Okvis2Config:
    """OKVIS2-specific knobs (AZ-332).

    ``keyframe_window_size`` is the sliding-window keyframe count K
    per D-C5-3 — must be in [10, 20]. Lower values lose accuracy;
    higher values exceed the C1-PT-01 per-frame budget on Tier-2.

    ``keyframe_parallax_threshold_px`` is the parallax-driven keyframe
    decision; default 3.0 px (OKVIS2 upstream default).

    ``ransac_inlier_ratio`` is the RANSAC inlier-ratio threshold below
    which the frontend declares the frame untrackable; default 0.5.

    ``max_optimization_iters`` caps the per-frame Levenberg-Marquardt
    iterations to bound worst-case latency; default 4 (OKVIS2 default).

    ``degraded_feature_threshold`` is the per-frame tracked-feature
    count below which ``health_snapshot`` reports DEGRADED; default 30.

    ``per_frame_debug_log`` enables a DEBUG log line per ``process_frame``
    — OFF by default (would flood at 3 Hz steady-state).
    """

    keyframe_window_size: int = 15
    keyframe_parallax_threshold_px: float = 3.0
    ransac_inlier_ratio: float = 0.5
    max_optimization_iters: int = 4
    degraded_feature_threshold: int = 30
    per_frame_debug_log: bool = False

    def __post_init__(self) -> None:
        if not (10 <= self.keyframe_window_size <= 20):
            raise ConfigError(
                "Okvis2Config.keyframe_window_size must be in [10, 20] "
                f"(D-C5-3 budget); got {self.keyframe_window_size}"
            )
        if self.keyframe_parallax_threshold_px <= 0.0:
            raise ConfigError(
                "Okvis2Config.keyframe_parallax_threshold_px must be > 0; "
                f"got {self.keyframe_parallax_threshold_px}"
            )
        if not (0.0 < self.ransac_inlier_ratio <= 1.0):
            raise ConfigError(
                "Okvis2Config.ransac_inlier_ratio must be in (0.0, 1.0]; "
                f"got {self.ransac_inlier_ratio}"
            )
        if self.max_optimization_iters < 1:
            raise ConfigError(
                "Okvis2Config.max_optimization_iters must be >= 1; "
                f"got {self.max_optimization_iters}"
            )
        if self.degraded_feature_threshold < 1:
            raise ConfigError(
                "Okvis2Config.degraded_feature_threshold must be >= 1; "
                f"got {self.degraded_feature_threshold}"
            )


@dataclass(frozen=True)
class C1VioConfig:
    """Per-component config for C1 VIO.

    ``strategy`` selects exactly one of the three backends
    (``okvis2`` / ``vins_mono`` / ``klt_ransac``); the
    composition-root factory respects compile-time ``BUILD_*`` gating
    on top of this label.

    ``lost_frame_threshold`` is the number of consecutive ``LOST``
    frames before the strategy raises :class:`VioFatalError`;
    default 9 per ``vio_strategy_protocol.md`` v1.0.0.

    ``warm_start_max_frames`` is the convergence budget after
    :meth:`VioStrategy.reset_to_warm_start`; default 5.

    ``okvis2`` carries OKVIS2-specific knobs (AZ-332); consulted only
    when ``strategy == "okvis2"``.
    """

    strategy: str = "klt_ransac"
    lost_frame_threshold: int = 9
    warm_start_max_frames: int = 5
    okvis2: Okvis2Config = field(default_factory=Okvis2Config)

    def __post_init__(self) -> None:
        if self.strategy not in KNOWN_STRATEGIES:
            raise ConfigError(
                f"C1VioConfig.strategy={self.strategy!r} not in {sorted(KNOWN_STRATEGIES)}"
            )
        if self.lost_frame_threshold < 1:
            raise ConfigError(
                f"C1VioConfig.lost_frame_threshold must be >= 1; got {self.lost_frame_threshold}"
            )
        if self.warm_start_max_frames < 1:
            raise ConfigError(
                f"C1VioConfig.warm_start_max_frames must be >= 1; got {self.warm_start_max_frames}"
            )
