"""Shared JSON-payload → :class:`FlightDto` parser (AZ-489).

Used by both the online HTTPS client and the offline file loader so they
satisfy FAC-INV-1 (same shape, same validation, same error types).
"""

from __future__ import annotations

import math
from typing import Any
from uuid import UUID

from gps_denied_onboard.components.c12_operator_tooling.flights_api.errors import (
    FlightsApiSchemaError,
    WaypointSchemaError,
)
from gps_denied_onboard.components.c12_operator_tooling.flights_api.interface import (
    FlightDto,
    WaypointDto,
    WaypointObjective,
    WaypointSource,
)

__all__ = ["parse_flight_payload"]


def parse_flight_payload(payload: Any, *, source_label: str) -> FlightDto:
    """Validate + normalise ``payload`` into a :class:`FlightDto`.

    ``source_label`` is folded into error messages so the operator can tell
    online failures (``"flights service"``) from offline failures
    (``"flight file <path>"``) without inspecting the exception type.

    Raises:
        FlightsApiSchemaError: top-level shape violation.
        WaypointSchemaError: any single waypoint is malformed, or the
            ordinal sequence is not a contiguous ``0..N-1`` run.
    """
    if not isinstance(payload, dict):
        raise FlightsApiSchemaError(
            f"{source_label}: expected JSON object at top level; got {type(payload).__name__}"
        )

    flight_id = _require_uuid(payload, "flight_id", source_label)
    name = _require_str(payload, "name", source_label)
    waypoints_raw = _require_list(payload, "waypoints", source_label)

    waypoints = tuple(
        sorted(
            (_parse_waypoint(item, index, source_label) for index, item in enumerate(waypoints_raw)),
            key=lambda wp: wp.ordinal,
        )
    )
    _enforce_contiguous_ordinals(waypoints, source_label)
    return FlightDto(flight_id=flight_id, name=name, waypoints=waypoints)


def _parse_waypoint(item: Any, source_index: int, source_label: str) -> WaypointDto:
    if not isinstance(item, dict):
        raise WaypointSchemaError(
            f"{source_label}: waypoint #{source_index} is not a JSON object; "
            f"got {type(item).__name__}"
        )
    ordinal = _require_int(item, "ordinal", f"{source_label} waypoint #{source_index}")
    if ordinal < 0:
        raise WaypointSchemaError(
            f"{source_label}: waypoint #{source_index} ordinal={ordinal} must be >= 0"
        )
    lat_deg = _require_finite_float(item, "lat_deg", f"{source_label} waypoint #{source_index}")
    if not -90.0 <= lat_deg <= 90.0:
        raise WaypointSchemaError(
            f"{source_label}: waypoint #{source_index} lat_deg={lat_deg} outside [-90, 90]"
        )
    lon_deg = _require_finite_float(item, "lon_deg", f"{source_label} waypoint #{source_index}")
    if not -180.0 <= lon_deg <= 180.0:
        raise WaypointSchemaError(
            f"{source_label}: waypoint #{source_index} lon_deg={lon_deg} outside [-180, 180]"
        )
    alt_m = _require_finite_float(item, "alt_m", f"{source_label} waypoint #{source_index}")
    objective = _parse_enum(
        item, "objective", WaypointObjective, f"{source_label} waypoint #{source_index}"
    )
    source = _parse_enum(
        item, "source", WaypointSource, f"{source_label} waypoint #{source_index}"
    )
    return WaypointDto(
        ordinal=ordinal,
        lat_deg=lat_deg,
        lon_deg=lon_deg,
        alt_m=alt_m,
        objective=objective,
        source=source,
    )


def _enforce_contiguous_ordinals(
    waypoints: tuple[WaypointDto, ...], source_label: str
) -> None:
    for expected, wp in enumerate(waypoints):
        if wp.ordinal != expected:
            raise WaypointSchemaError(
                f"{source_label}: waypoint ordinal sequence is not contiguous; "
                f"expected {expected} at position {expected}, got {wp.ordinal}"
            )


def _require_uuid(payload: dict[str, Any], field: str, source_label: str) -> UUID:
    raw = _require_str(payload, field, source_label)
    try:
        return UUID(raw)
    except (ValueError, AttributeError, TypeError) as exc:
        raise FlightsApiSchemaError(
            f"{source_label}: field {field!r} is not a valid UUID: {raw!r}"
        ) from exc


def _require_str(payload: dict[str, Any], field: str, source_label: str) -> str:
    if field not in payload:
        raise FlightsApiSchemaError(f"{source_label}: missing required field {field!r}")
    value = payload[field]
    if not isinstance(value, str) or not value:
        raise FlightsApiSchemaError(
            f"{source_label}: field {field!r} must be a non-empty string; "
            f"got {type(value).__name__}"
        )
    return value


def _require_list(payload: dict[str, Any], field: str, source_label: str) -> list[Any]:
    if field not in payload:
        raise FlightsApiSchemaError(f"{source_label}: missing required field {field!r}")
    value = payload[field]
    if not isinstance(value, list):
        raise FlightsApiSchemaError(
            f"{source_label}: field {field!r} must be a JSON array; got {type(value).__name__}"
        )
    return value


def _require_int(payload: dict[str, Any], field: str, source_label: str) -> int:
    if field not in payload:
        raise WaypointSchemaError(f"{source_label}: missing required field {field!r}")
    value = payload[field]
    if isinstance(value, bool) or not isinstance(value, int):
        raise WaypointSchemaError(
            f"{source_label}: field {field!r} must be an integer; "
            f"got {type(value).__name__}"
        )
    return value


def _require_finite_float(payload: dict[str, Any], field: str, source_label: str) -> float:
    if field not in payload:
        raise WaypointSchemaError(f"{source_label}: missing required field {field!r}")
    value = payload[field]
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise WaypointSchemaError(
            f"{source_label}: field {field!r} must be a number; got {type(value).__name__}"
        )
    fvalue = float(value)
    if not math.isfinite(fvalue):
        raise WaypointSchemaError(f"{source_label}: field {field!r} must be finite; got {value}")
    return fvalue


def _parse_enum(
    payload: dict[str, Any], field: str, enum_cls: type, source_label: str
) -> Any:
    if field not in payload:
        raise WaypointSchemaError(f"{source_label}: missing required field {field!r}")
    raw = payload[field]
    if not isinstance(raw, str):
        raise WaypointSchemaError(
            f"{source_label}: field {field!r} must be a string; got {type(raw).__name__}"
        )
    try:
        return enum_cls(raw)
    except ValueError as exc:
        valid = sorted(member.value for member in enum_cls)
        raise WaypointSchemaError(
            f"{source_label}: field {field!r}={raw!r} is not in {valid}"
        ) from exc
