"""AIS (Automatic Identification System) message decoder."""

from dataclasses import dataclass
from enum import Enum

from fishinfo.exceptions import ValidationError

from .checksum import validate_checksum


class AISMessageType(Enum):
    """AIS message types."""

    POSITION_REPORT_CLASS_A = 1
    POSITION_REPORT_CLASS_A_ASSIGNED = 2
    POSITION_REPORT_CLASS_A_RESPONSE = 3
    BASE_STATION_REPORT = 4
    STATIC_AND_VOYAGE = 5
    BINARY_ADDRESSED = 6
    BINARY_ACKNOWLEDGE = 7
    BINARY_BROADCAST = 8
    SAR_AIRCRAFT_POSITION = 9
    UTC_AND_DATE_INQUIRY = 10
    UTC_AND_DATE_RESPONSE = 11
    ADDRESSED_SAFETY_MESSAGE = 12
    SAFETY_ACKNOWLEDGE = 13
    SAFETY_BROADCAST = 14
    INTERROGATION = 15
    ASSIGNMENT_MODE_COMMAND = 16
    DGNSS_BROADCAST = 17
    STANDARD_CLASS_B_POSITION = 18
    EXTENDED_CLASS_B_POSITION = 19
    DATA_LINK_MANAGEMENT = 20
    AID_TO_NAVIGATION = 21
    CHANNEL_MANAGEMENT = 22
    GROUP_ASSIGNMENT_COMMAND = 23
    STATIC_DATA_REPORT = 24
    SINGLE_SLOT_BINARY = 25
    MULTIPLE_SLOT_BINARY = 26
    POSITION_REPORT_LONG_RANGE = 27


class NavigationStatus(Enum):
    """AIS navigation status."""

    UNDER_WAY_USING_ENGINE = 0
    AT_ANCHOR = 1
    NOT_UNDER_COMMAND = 2
    RESTRICTED_MANOEUVRABILITY = 3
    CONSTRAINED_BY_DRAUGHT = 4
    MOORED = 5
    AGROUND = 6
    ENGAGED_IN_FISHING = 7
    UNDER_WAY_SAILING = 8
    RESERVED_HSC = 9
    RESERVED_WIG = 10
    RESERVED_11 = 11
    RESERVED_12 = 12
    RESERVED_13 = 13
    AIS_SART = 14
    NOT_DEFINED = 15


@dataclass
class AISPositionReport:
    """
    AIS Position Report (Message Types 1, 2, 3).

    Class A position reports with navigation status.
    """

    message_type: int
    mmsi: int
    navigation_status: NavigationStatus
    rate_of_turn: float | None  # degrees per minute
    speed_over_ground: float | None  # knots
    position_accuracy: bool  # True = high (DGPS), False = low (GPS)
    latitude: float | None  # degrees
    longitude: float | None  # degrees
    course_over_ground: float | None  # degrees
    true_heading: int | None  # degrees
    timestamp: int  # UTC seconds
    raw: str


@dataclass
class AISStaticData:
    """
    AIS Static and Voyage Related Data (Message Type 5).

    Ship static and voyage information.
    """

    message_type: int
    mmsi: int
    ais_version: int
    imo_number: int | None
    call_sign: str
    vessel_name: str
    ship_type: int
    dimension_to_bow: int  # meters
    dimension_to_stern: int  # meters
    dimension_to_port: int  # meters
    dimension_to_starboard: int  # meters
    position_fix_type: int
    eta_month: int | None
    eta_day: int | None
    eta_hour: int | None
    eta_minute: int | None
    draught: float | None  # meters
    destination: str
    raw: str


# Type alias for any AIS message
AISMessage = AISPositionReport | AISStaticData


# AIS payload encoding (6-bit ASCII)
AIS_PAYLOAD_ARMOR = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&'()*+,-./0123456789:;<=>?"


def decode_payload(payload: str) -> list[int]:
    """
    Decode AIS 6-bit ASCII armored payload to bit array.

    Args:
        payload: AIS payload string

    Returns:
        List of bits (0 or 1)
    """
    bits = []
    for char in payload:
        # Map character to 6-bit value
        value = AIS_PAYLOAD_ARMOR.index(char)
        # Convert to 6 bits
        for i in range(5, -1, -1):
            bits.append((value >> i) & 1)
    return bits


def extract_bits(bits: list[int], start: int, length: int) -> int:
    """
    Extract integer value from bit array.

    Args:
        bits: List of bits
        start: Start bit index
        length: Number of bits to extract

    Returns:
        Integer value
    """
    value = 0
    for i in range(length):
        value = (value << 1) | bits[start + i]
    return value


def extract_signed_bits(bits: list[int], start: int, length: int) -> int:
    """
    Extract signed integer value from bit array (two's complement).

    Args:
        bits: List of bits
        start: Start bit index
        length: Number of bits to extract

    Returns:
        Signed integer value
    """
    value = extract_bits(bits, start, length)
    # Check sign bit
    if value & (1 << (length - 1)):
        # Negative number - convert from two's complement
        value = value - (1 << length)
    return value


def extract_string(bits: list[int], start: int, length: int) -> str:
    """
    Extract ASCII string from bit array (6-bit encoding).

    Args:
        bits: List of bits
        start: Start bit index
        length: Number of bits to extract (must be multiple of 6)

    Returns:
        Decoded string
    """
    result = []
    for i in range(0, length, 6):
        char_bits = extract_bits(bits, start + i, 6)
        if char_bits == 0:
            break
        # AIS uses 6-bit ASCII: 0-31 map to @-_, 32-63 map to space and punctuation
        if char_bits < 32:
            char = chr(char_bits + 64)
        else:
            char = chr(char_bits)
        result.append(char)
    return "".join(result).strip()


class AISDecoder:
    """
    AIS message decoder.

    Decodes NMEA !AIVDM and !AIVDO sentences containing AIS messages.
    Supports message types 1-3 (position reports) and 5 (static data).
    """

    def __init__(self, validate_checksums: bool = True) -> None:
        """
        Initialize AIS decoder.

        Args:
            validate_checksums: Whether to validate sentence checksums
        """
        self.validate_checksums = validate_checksums
        self.message_fragments: dict[int, list[str]] = {}

    def decode(self, sentence: str) -> AISMessage | None:
        """
        Decode an AIS NMEA sentence.

        Args:
            sentence: Complete !AIVDM or !AIVDO sentence

        Returns:
            Decoded AIS message, or None if multipart message incomplete

        Raises:
            ValidationError: If sentence is invalid

        Examples:
            >>> decoder = AISDecoder()
            >>> msg = decoder.decode("!AIVDM,1,1,,B,13u@Dp0P00PlT>HMGN4?vhR00<02,0*66")
            >>> msg.mmsi
            265513580
        """
        # Strip whitespace
        sentence = sentence.strip()

        # Validate prefix
        if not sentence.startswith("!AIVDM") and not sentence.startswith("!AIVDO"):
            raise ValidationError(f"Invalid AIS sentence: must start with !AIVDM or !AIVDO: {sentence}")

        # Validate checksum if enabled
        if self.validate_checksums and "*" in sentence:
            if not validate_checksum(sentence):
                raise ValidationError(f"Invalid checksum: {sentence}")

        # Remove checksum for parsing
        if "*" in sentence:
            sentence = sentence.split("*")[0]

        # Parse AIVDM/AIVDO sentence format:
        # !AIVDM,frag_count,frag_num,message_id,channel,payload,fill_bits*checksum
        parts = sentence.split(",")
        if len(parts) < 7:
            raise ValidationError(f"Invalid AIS sentence format: {sentence}")

        frag_count = int(parts[1])
        frag_num = int(parts[2])
        message_id = int(parts[3]) if parts[3] else 0
        payload = parts[5]

        # Handle multi-part messages
        if frag_count > 1:
            # Store fragment
            if message_id not in self.message_fragments:
                self.message_fragments[message_id] = [""] * frag_count
            self.message_fragments[message_id][frag_num - 1] = payload

            # Check if complete
            if "" in self.message_fragments[message_id]:
                return None  # Still waiting for fragments

            # Reassemble payload
            payload = "".join(self.message_fragments[message_id])
            del self.message_fragments[message_id]

        # Decode payload
        bits = decode_payload(payload)

        if len(bits) < 8:
            raise ValidationError(f"AIS payload too short: {sentence}")

        # Extract message type
        msg_type = extract_bits(bits, 0, 6)

        # Decode based on message type
        if msg_type in (1, 2, 3):
            return self._decode_position_report(bits, msg_type, sentence)
        elif msg_type == 5:
            return self._decode_static_data(bits, sentence)
        else:
            raise ValidationError(f"Unsupported AIS message type: {msg_type}")

    def _decode_position_report(self, bits: list[int], msg_type: int, raw: str) -> AISPositionReport:
        """Decode AIS position report (types 1, 2, 3)."""
        if len(bits) < 168:
            raise ValidationError(f"AIS position report too short: {len(bits)} bits")

        # Extract fields
        mmsi = extract_bits(bits, 8, 30)
        nav_status = extract_bits(bits, 38, 4)
        rot_raw = extract_signed_bits(bits, 42, 8)
        sog_raw = extract_bits(bits, 50, 10)
        position_accuracy = extract_bits(bits, 60, 1) == 1
        lon_raw = extract_signed_bits(bits, 61, 28)
        lat_raw = extract_signed_bits(bits, 89, 27)
        cog_raw = extract_bits(bits, 116, 12)
        heading_raw = extract_bits(bits, 128, 9)
        timestamp = extract_bits(bits, 137, 6)

        # Convert rate of turn (ROT)
        rate_of_turn = None
        if rot_raw != -128:  # -128 = not available
            # ROT = (ROT_AIS/4.733)^2 degrees/minute
            rate_of_turn = (rot_raw / 4.733) ** 2
            if rot_raw < 0:
                rate_of_turn = -rate_of_turn

        # Convert speed over ground
        speed_over_ground = None
        if sog_raw != 1023:  # 1023 = not available
            speed_over_ground = sog_raw / 10.0

        # Convert longitude
        longitude = None
        if lon_raw != 0x6791AC0:  # 0x6791AC0 = not available
            longitude = lon_raw / 600000.0

        # Convert latitude
        latitude = None
        if lat_raw != 0x3412140:  # 0x3412140 = not available
            latitude = lat_raw / 600000.0

        # Convert course over ground
        course_over_ground = None
        if cog_raw != 3600:  # 3600 = not available
            course_over_ground = cog_raw / 10.0

        # Convert true heading
        true_heading = None
        if heading_raw != 511:  # 511 = not available
            true_heading = heading_raw

        return AISPositionReport(
            message_type=msg_type,
            mmsi=mmsi,
            navigation_status=NavigationStatus(nav_status),
            rate_of_turn=rate_of_turn,
            speed_over_ground=speed_over_ground,
            position_accuracy=position_accuracy,
            latitude=latitude,
            longitude=longitude,
            course_over_ground=course_over_ground,
            true_heading=true_heading,
            timestamp=timestamp,
            raw=raw,
        )

    def _decode_static_data(self, bits: list[int], raw: str) -> AISStaticData:
        """Decode AIS static and voyage data (type 5)."""
        if len(bits) < 424:
            raise ValidationError(f"AIS static data too short: {len(bits)} bits")

        # Extract fields
        mmsi = extract_bits(bits, 8, 30)
        ais_version = extract_bits(bits, 38, 2)
        imo = extract_bits(bits, 40, 30)
        call_sign = extract_string(bits, 70, 42)
        vessel_name = extract_string(bits, 112, 120)
        ship_type = extract_bits(bits, 232, 8)
        dim_bow = extract_bits(bits, 240, 9)
        dim_stern = extract_bits(bits, 249, 9)
        dim_port = extract_bits(bits, 258, 6)
        dim_starboard = extract_bits(bits, 264, 6)
        epfd = extract_bits(bits, 270, 4)
        eta_month = extract_bits(bits, 274, 4)
        eta_day = extract_bits(bits, 278, 5)
        eta_hour = extract_bits(bits, 283, 5)
        eta_minute = extract_bits(bits, 288, 6)
        draught_raw = extract_bits(bits, 294, 8)
        destination = extract_string(bits, 302, 120)

        # Convert IMO number (0 = not available)
        imo_number = imo if imo != 0 else None

        # Convert ETA fields (0 = not available)
        eta_month_val = eta_month if eta_month != 0 else None
        eta_day_val = eta_day if eta_day != 0 else None
        eta_hour_val = eta_hour if eta_hour < 24 else None
        eta_minute_val = eta_minute if eta_minute < 60 else None

        # Convert draught (0 = not available)
        draught = (draught_raw / 10.0) if draught_raw != 0 else None

        return AISStaticData(
            message_type=5,
            mmsi=mmsi,
            ais_version=ais_version,
            imo_number=imo_number,
            call_sign=call_sign,
            vessel_name=vessel_name,
            ship_type=ship_type,
            dimension_to_bow=dim_bow,
            dimension_to_stern=dim_stern,
            dimension_to_port=dim_port,
            dimension_to_starboard=dim_starboard,
            position_fix_type=epfd,
            eta_month=eta_month_val,
            eta_day=eta_day_val,
            eta_hour=eta_hour_val,
            eta_minute=eta_minute_val,
            draught=draught,
            destination=destination,
            raw=raw,
        )
