"""Service for PX4 health and calibration snapshot."""

from __future__ import annotations

import asyncio
from typing import Any, Dict, Optional, AsyncGenerator

from mavsdk import System

try:
    from grpc import RpcError
except ImportError:
    # Fallback if grpc is not directly available (shouldn't happen with mavsdk)
    RpcError = Exception

from ..config import settings
from ..px4_connection import (
    CachedTelemetry,
    get_cached_telemetry,
    get_system,
    mark_disconnected,
    _is_connection_loss,
)
from ..models import (
    Px4HealthResponse,
    CalibrationHealth,
    BatteryStatus,
)


async def read_px4_health(
    port: Optional[str] = None,
    baudrate: Optional[int] = None,
    timeout_s: float = 10.0,
) -> Px4HealthResponse:
    """Connect to PX4 via MAVSDK and return a health snapshot using telemetry health flags."""

    port = port or settings.default_port
    baudrate = baudrate or settings.default_baudrate
    try:
        # Ensure a shared MAVSDK System() exists for this port/baudrate so that
        # the connection is established and telemetry subscribers are running.
        drone = await get_system(port=port, baudrate=baudrate, timeout_s=timeout_s)

        # Prefer cached telemetry populated by background subscribers for
        # low-latency health snapshots.
        telemetry: CachedTelemetry = await get_cached_telemetry(port=port, baudrate=baudrate)

        # If we don't have any cached health yet (edge case: just connected),
        # fall back to a short direct telemetry read as a best effort.
        health: Optional[Any] = telemetry.health
        gps_result: Optional[Any] = telemetry.gps_info
        battery_result: Optional[Any] = telemetry.battery

        if health is None:
            try:
                # Give MAVSDK a brief window to deliver at least one health sample.
                async def _one_health_sample() -> Optional[Any]:
                    async for h in drone.telemetry.health():
                        return h
                    return None

                health = await asyncio.wait_for(_one_health_sample(), timeout=0.5)
            except Exception:
                health = None

        # Health is required to build the response; if it is still unavailable,
        # treat the PX4 as disconnected from the perspective of this API.
        if health is None:
            return Px4HealthResponse(
                status="disconnected",
                connection_port=port,
                connection_baudrate=baudrate,
                calibration=None,
                gps_fix_type=None,
                satellites_used=None,
                armable=False,
                raw_health=None,
            )

        # Use telemetry health flags directly
        calibration = CalibrationHealth(
            gyroscope_ok=health.is_gyrometer_calibration_ok,
            accelerometer_ok=health.is_accelerometer_calibration_ok,
            magnetometer_ok=health.is_magnetometer_calibration_ok,
        )

        # Determine status
        status = "ok" if (health.is_armable and not calibration.requires_calibration) else "degraded"

        # GPS snapshot for fix type and satellites used (optional)
        gps_info = gps_result

        gps_fix_type: Optional[str] = None
        satellites_used: Optional[int] = None

        if gps_info is not None:
            fix_type = getattr(gps_info, "fix_type", None)
            num_satellites = getattr(gps_info, "num_satellites", None)

            if fix_type is not None:
                raw_name = getattr(fix_type, "name", str(fix_type))
                key = raw_name.upper()
                # Map MAVSDK enum names to the strings documented in Px4HealthResponse
                gps_fix_type_map = {
                    "NO_FIX": "no_fix",
                    "FIX_2D": "2d_fix",
                    "FIX_3D": "3d_fix",
                    "RTK_FLOAT": "rtk_float",
                    "RTK_FIXED": "rtk_fix",
                }
                gps_fix_type = gps_fix_type_map.get(key, raw_name.lower())

            if isinstance(num_satellites, int):
                satellites_used = num_satellites

        # Battery snapshot for voltage / remaining percent / current (optional)
        battery_info = battery_result

        battery_status: Optional[BatteryStatus] = None

        if battery_info is not None:
            voltage_v = getattr(battery_info, "voltage_v", None)
            remaining_raw = getattr(battery_info, "remaining_percent", None)
            current_a = getattr(battery_info, "current_battery_a", None)

            remaining_percent: Optional[float] = None
            if isinstance(remaining_raw, (int, float)):
                value = float(remaining_raw)
                # MAVSDK typically reports 0.0–1.0; map to 0–100 for convenience.
                remaining_percent = value * 100.0 if value <= 1.0 else value

            battery_status = BatteryStatus(
                voltage_v=float(voltage_v) if isinstance(voltage_v, (int, float)) else None,
                remaining_percent=remaining_percent,
                current_a=float(current_a) if isinstance(current_a, (int, float)) else None,
            )

        # Minimal raw health + GPS + battery for debugging
        raw_health: Dict[str, Any] = {
            "raw_health_message": str(health),
            "is_gyrometer_calibration_ok": health.is_gyrometer_calibration_ok,
            "is_accelerometer_calibration_ok": health.is_accelerometer_calibration_ok,
            "is_magnetometer_calibration_ok": health.is_magnetometer_calibration_ok,
            "is_armable": health.is_armable,
            "gps_info": str(gps_info) if gps_info is not None else None,
            "battery_info": str(battery_info) if battery_info is not None else None,
        }

        return Px4HealthResponse(
            status=status,
            connection_port=port,
            connection_baudrate=baudrate,
            calibration=calibration,
            gps_fix_type=gps_fix_type,
            satellites_used=satellites_used,
            armable=health.is_armable,
            battery=battery_status,
            raw_health=raw_health,
        )

    except (RpcError, Exception, TimeoutError) as exc:
        # Return disconnected status on any connection/telemetry error
        if _is_connection_loss(exc):
            await mark_disconnected(port, baudrate)
        return Px4HealthResponse(
            status="disconnected",
            connection_port=port,
            connection_baudrate=baudrate,
            calibration=None,
            gps_fix_type=None,
            satellites_used=None,
            armable=False,
            raw_health=None,
        )


async def stream_status_text(
    port: str,
    baudrate: int,
) -> AsyncGenerator[Dict[str, Any], None]:
    """
    Stream PX4 status_text messages over MAVSDK.

    This is useful on setups where the Events plugin is unavailable and
    `health_and_arming_checks_report` cannot be used to explain arming
    failures. The consumer can inspect the text messages to understand
    why the vehicle cannot arm (e.g. battery over-voltage, GPS issues).
    """

    serial = f"serial://{port}:{baudrate}"

    try:
        # Initial status messages
        yield {
            "type": "status",
            "message": f"Connecting to {port}...",
        }

        # Reuse the shared MAVSDK System() so that status_text comes from the
        # same underlying connection as other services.
        drone = await get_system(port=port, baudrate=baudrate)

        yield {
            "type": "status",
            "message": "Connected to device",
        }

        # Stream status_text messages indefinitely until the caller disconnects
        async for msg in drone.telemetry.status_text():
            severity = None
            msg_type = getattr(msg, "type", None)
            if msg_type is not None:
                # MAVSDK enums usually expose a .name attribute
                severity = getattr(msg_type, "name", str(msg_type))

            text = getattr(msg, "text", "")

            yield {
                "type": "status_text",
                "severity": severity,
                "text": text,
            }

    except (RpcError, Exception) as exc:
        # Handle connection loss ("Socket closed") as a normal end of stream to
        # avoid noisy errors when PX4 or the link resets. Other errors are still
        # surfaced as an error message.
        message = str(exc)
        if "Socket closed" in message or "StatusCode.UNAVAILABLE" in message:
            await mark_disconnected(port, baudrate)
            yield {
                "type": "status",
                "message": "Disconnected from device (socket closed).",
            }
            # Do not re-raise: treat as graceful disconnect
            return

        yield {
            "type": "error",
            "message": f"Status text stream error: {exc}",
        }
        raise

    finally:
        # Shared System is managed by the connection manager; no teardown here.
        pass
