"""C8 flight-controller adapter DTOs + enums (AZ-390 / E-C8).

These are the shared types across the C8 component's public Protocol
surface (`FcAdapter`, `GcsAdapter`) — consumed by every downstream
consumer task (AZ-391 inbound decode, AZ-392 covariance projector,
AZ-393 AP outbound, AZ-394 iNav outbound, AZ-395 signing, AZ-396
source-set switch, AZ-397 GCS adapter).

Frozen + slotted per ADR-002 / module-layout.md so the wire encoders
and decoders cannot mutate observed telemetry. Enum integer values
mirror the per-FC wire constants where applicable
(``Severity`` follows MAVLink ``STATUSTEXT.severity`` semantics).
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum
from typing import Protocol, runtime_checkable

from gps_denied_onboard._types.geo import LatLonAlt

__all__ = [
    "AttitudeSample",
    "FcKind",
    "FcTelemetryFrame",
    "FlightState",
    "FlightStateSignal",
    "GpsHealth",
    "GpsSample",
    "GpsStatus",
    "ImuTelemetrySample",
    "OperatorCommand",
    "PortConfig",
    "Severity",
    "Subscription",
    "TelemetryKind",
]


class FcKind(Enum):
    """Concrete flight-controller variant (`config.fc.adapter`)."""

    ARDUPILOT_PLANE = "ardupilot_plane"
    INAV = "inav"
    # GCS link kind (C8 GcsAdapter open path). Not a true FC, but the
    # shared PortConfig DTO needs a marker for the GCS UART so tests
    # and the composition root use a single type-discriminator.
    GCS_QGC = "gcs_qgc"


class FlightState(Enum):
    """Coarse FC flight-state lattice consumed by C5 + C8 emit gate."""

    INIT = "init"
    ARMED = "armed"
    IN_FLIGHT = "in_flight"
    ON_GROUND = "on_ground"
    FAILED = "failed"


class GpsStatus(Enum):
    """FC-reported GPS health bucket consumed by C5's spoof-recovery gate."""

    NO_FIX = "no_fix"
    DEGRADED = "degraded"
    STABLE = "stable"
    STABLE_NON_SPOOFED = "stable_non_spoofed"
    SPOOFED = "spoofed"


class Severity(Enum):
    """STATUSTEXT severity; values mirror MAVLink ``MAV_SEVERITY``.

    Aligned with MAVLink's ``MAV_SEVERITY`` integer constants:
    ``EMERGENCY=0``, ``ALERT=1``, ``CRITICAL=2``, ``ERROR=3``,
    ``WARNING=4``, ``NOTICE=5``, ``INFO=6``, ``DEBUG=7``. AZ-388
    (AC-5.2 fallback) requires ``CRITICAL`` and ``NOTICE`` for
    engagement/recovery STATUSTEXT severities.
    """

    CRITICAL = 2
    ERROR = 3
    WARNING = 4
    NOTICE = 5
    INFO = 6


class TelemetryKind(Enum):
    """Discriminator for :class:`FcTelemetryFrame.payload`."""

    IMU_SAMPLE = "imu_sample"
    ATTITUDE = "attitude"
    GPS_HEALTH = "gps_health"
    MAV_STATE = "mav_state"


@dataclass(frozen=True, slots=True)
class PortConfig:
    """Serial-port descriptor for C8 ``open()`` (D-C8-1)."""

    device: str
    baud: int
    fc_kind: FcKind


@dataclass(frozen=True, slots=True)
class ImuTelemetrySample:
    """Single 6-axis IMU sample from the FC's onboard sensor stream.

    Distinct from `nav.ImuSample` (the C1/C5 preintegrator's input
    type) because C8 also carries `received_at` decode-side
    monotonic_ns from Invariant 7 (out-of-order drop). The C8 inbound
    path (AZ-391) wraps this in `FcTelemetryFrame`.
    """

    ts_ns: int
    accel_xyz: tuple[float, float, float]
    gyro_xyz: tuple[float, float, float]


@dataclass(frozen=True, slots=True)
class AttitudeSample:
    """Single FC-reported attitude (RPY + monotonic timestamp)."""

    ts_ns: int
    roll_rad: float
    pitch_rad: float
    yaw_rad: float


@dataclass(frozen=True, slots=True)
class GpsHealth:
    """FC-reported GPS health bundle (post-decode form).

    `captured_at` is monotonic_ns at the decode boundary
    (Invariant 7).
    """

    status: GpsStatus
    fix_age_ms: int
    captured_at: int


@dataclass(frozen=True, slots=True)
class GpsSample:
    """Single FC-reported GPS lat/lon/alt sample (post-decode form).

    Distinct from :class:`GpsHealth` (which carries only the health
    status enum) — this is the geographic position the FC reports
    alongside the health bucket. Consumed by C5's bounded-delta gate
    (AZ-490 / Principle #11 third clause) as the `vincenty(sample,
    smoother)` input. ``captured_at`` is monotonic_ns at the decode
    boundary, mirroring :class:`GpsHealth` for Invariant 7.
    """

    position_wgs84: LatLonAlt
    captured_at: int


@dataclass(frozen=True, slots=True)
class FlightStateSignal:
    """FC's high-level flight-state lattice + AC-5.1 warm-start hint."""

    state: FlightState
    last_valid_gps_hint_wgs84: LatLonAlt | None
    last_valid_gps_age_ms: int | None
    captured_at: int


@dataclass(frozen=True, slots=True)
class FcTelemetryFrame:
    """Unified inbound telemetry envelope (AZ-391 producer)."""

    kind: TelemetryKind
    payload: ImuTelemetrySample | AttitudeSample | GpsHealth | FlightStateSignal
    received_at: int
    signed: bool


@dataclass(frozen=True, slots=True)
class OperatorCommand:
    """Operator-issued command ingested via GcsAdapter (AZ-397 consumer)."""

    command: str
    payload: dict[str, str | int | float | bool]
    received_at: int


@runtime_checkable
class Subscription(Protocol):
    """Handle returned by `FcAdapter.subscribe_telemetry` /
    `GcsAdapter.subscribe_operator_commands`.

    Calling :meth:`cancel` removes the callback from the fan-out bus.
    Multiple cancels are no-ops. The handle is thread-safe (Invariant 8
    permits inbound callbacks on a different thread than the cancel
    site).
    """

    def cancel(self) -> None: ...


# Sentinel callback alias used by Protocol-side type annotations.
TelemetryCallback = Callable[["FcTelemetryFrame"], None]
OperatorCommandCallback = Callable[["OperatorCommand"], None]
