"""NMEA 0183 sentence data classes and parsers."""

from dataclasses import dataclass
from datetime import datetime, timezone
from enum import Enum


class FixQuality(Enum):
    """GPS fix quality indicators."""

    INVALID = 0
    GPS_FIX = 1
    DGPS_FIX = 2
    PPS_FIX = 3
    RTK_FIX = 4
    RTK_FLOAT = 5
    ESTIMATED = 6
    MANUAL = 7
    SIMULATION = 8


@dataclass
class GGASentence:
    """
    GGA - Global Positioning System Fix Data.

    Time, position, and fix related data for a GPS receiver.
    """

    sentence_type: str  # "GGA"
    time: datetime | None
    latitude: float | None
    longitude: float | None
    fix_quality: FixQuality
    satellites: int | None
    hdop: float | None
    altitude: float | None  # meters above mean sea level
    geoid_separation: float | None  # meters
    raw: str


@dataclass
class RMCSentence:
    """
    RMC - Recommended Minimum Navigation Information.

    Time, date, position, course and speed data.
    """

    sentence_type: str  # "RMC"
    time: datetime | None
    status: str  # "A" = active/valid, "V" = void/invalid
    latitude: float | None
    longitude: float | None
    speed_knots: float | None
    course: float | None  # degrees true
    date: datetime | None
    magnetic_variation: float | None  # degrees (positive = East)
    raw: str


@dataclass
class VTGSentence:
    """
    VTG - Track Made Good and Ground Speed.

    Course and speed information.
    """

    sentence_type: str  # "VTG"
    course_true: float | None  # degrees
    course_magnetic: float | None  # degrees
    speed_knots: float | None
    speed_kmh: float | None
    raw: str


@dataclass
class HDTSentence:
    """
    HDT - Heading True.

    Vessel heading with respect to true north.
    """

    sentence_type: str  # "HDT"
    heading: float | None  # degrees true
    raw: str


@dataclass
class HDGSentence:
    """
    HDG - Heading, Deviation and Variation.

    Magnetic sensor heading, with deviation and variation.
    """

    sentence_type: str  # "HDG"
    heading: float | None  # degrees magnetic
    deviation: float | None  # degrees (positive = East)
    variation: float | None  # degrees (positive = East)
    raw: str


@dataclass
class DBTSentence:
    """
    DBT - Depth Below Transducer.

    Water depth below the transducer.
    """

    sentence_type: str  # "DBT"
    depth_feet: float | None
    depth_meters: float | None
    depth_fathoms: float | None
    raw: str


@dataclass
class DPTSentence:
    """
    DPT - Depth.

    Water depth relative to the transducer and offset.
    """

    sentence_type: str  # "DPT"
    depth: float | None  # meters
    offset: float | None  # meters (positive = distance from transducer to waterline)
    max_range: float | None  # meters
    raw: str


@dataclass
class MWVSentence:
    """
    MWV - Wind Speed and Angle.

    Wind angle and speed, either relative or true.
    """

    sentence_type: str  # "MWV"
    wind_angle: float | None  # degrees
    reference: str  # "R" = relative, "T" = true
    wind_speed: float | None
    speed_units: str  # "K" = km/h, "M" = m/s, "N" = knots
    status: str  # "A" = valid, "V" = invalid
    raw: str


@dataclass
class MTWSentence:
    """
    MTW - Water Temperature.

    Water temperature in degrees Celsius.
    """

    sentence_type: str  # "MTW"
    temperature: float | None  # degrees Celsius
    units: str  # "C" = Celsius
    raw: str


# Type alias for any sentence
NMEASentence = (
    GGASentence
    | RMCSentence
    | VTGSentence
    | HDTSentence
    | HDGSentence
    | DBTSentence
    | DPTSentence
    | MWVSentence
    | MTWSentence
)


def parse_latitude(lat_str: str, hemisphere: str) -> float | None:
    """
    Parse NMEA latitude format (DDMM.MMMM) to decimal degrees.

    Args:
        lat_str: Latitude in DDMM.MMMM format
        hemisphere: "N" or "S"

    Returns:
        Latitude in decimal degrees, or None if invalid

    Examples:
        >>> parse_latitude("4807.038", "N")
        48.1173
        >>> parse_latitude("4807.038", "S")
        -48.1173
    """
    if not lat_str or not hemisphere:
        return None

    try:
        # Split into degrees and minutes
        lat_float = float(lat_str)
        degrees = int(lat_float / 100)
        minutes = lat_float - (degrees * 100)

        decimal = degrees + (minutes / 60)

        if hemisphere == "S":
            decimal = -decimal

        return decimal
    except (ValueError, ZeroDivisionError):
        return None


def parse_longitude(lon_str: str, hemisphere: str) -> float | None:
    """
    Parse NMEA longitude format (DDDMM.MMMM) to decimal degrees.

    Args:
        lon_str: Longitude in DDDMM.MMMM format
        hemisphere: "E" or "W"

    Returns:
        Longitude in decimal degrees, or None if invalid

    Examples:
        >>> parse_longitude("01131.000", "E")
        11.5167
        >>> parse_longitude("01131.000", "W")
        -11.5167
    """
    if not lon_str or not hemisphere:
        return None

    try:
        # Split into degrees and minutes
        lon_float = float(lon_str)
        degrees = int(lon_float / 100)
        minutes = lon_float - (degrees * 100)

        decimal = degrees + (minutes / 60)

        if hemisphere == "W":
            decimal = -decimal

        return decimal
    except (ValueError, ZeroDivisionError):
        return None


def parse_time(time_str: str, date_str: str | None = None) -> datetime | None:
    """
    Parse NMEA time format (HHMMSS.SSS) to datetime.

    Args:
        time_str: Time in HHMMSS or HHMMSS.SSS format
        date_str: Optional date in DDMMYY format

    Returns:
        datetime in UTC, or None if invalid

    Examples:
        >>> parse_time("123519")
        datetime.datetime(1970, 1, 1, 12, 35, 19, tzinfo=datetime.timezone.utc)
        >>> parse_time("123519", "100423")
        datetime.datetime(2023, 4, 10, 12, 35, 19, tzinfo=datetime.timezone.utc)
    """
    if not time_str:
        return None

    try:
        # Parse time
        hours = int(time_str[0:2])
        minutes = int(time_str[2:4])
        seconds = int(time_str[4:6])
        microseconds = 0

        # Handle fractional seconds if present
        if "." in time_str:
            frac_str = time_str.split(".")[1]
            # Pad to 6 digits for microseconds
            frac_str = frac_str.ljust(6, "0")[:6]
            microseconds = int(frac_str)

        # Parse date if provided
        if date_str and len(date_str) >= 6:
            day = int(date_str[0:2])
            month = int(date_str[2:4])
            year = int(date_str[4:6])
            # Convert 2-digit year to 4-digit (assume 2000s)
            year = 2000 + year if year < 50 else 1900 + year
        else:
            # Default to epoch date
            day = 1
            month = 1
            year = 1970

        return datetime(
            year=year,
            month=month,
            day=day,
            hour=hours,
            minute=minutes,
            second=seconds,
            microsecond=microseconds,
            tzinfo=timezone.utc,
        )
    except (ValueError, IndexError):
        return None


def parse_float(value: str) -> float | None:
    """Parse float value, returning None if empty or invalid."""
    if not value or value == "":
        return None
    try:
        return float(value)
    except ValueError:
        return None


def parse_int(value: str) -> int | None:
    """Parse integer value, returning None if empty or invalid."""
    if not value or value == "":
        return None
    try:
        return int(value)
    except ValueError:
        return None
