"""BCM4775 binary packet framing and reliable transport.

Implements the packet-level protocol between host and BCM4775 GNSS SoC.
The protocol sits between the raw serial transport (bcm_transport.py)
and the RPC command layer (bcm_rpc.py).

Protocol layers:
    RPC Engine → Reliable Transport → Serial HAL

The reliable transport provides:
  - Packet framing with start-of-frame, type, sequence, length, checksum
  - Automatic sequence numbering
  - ACK/NAK handling with retry
  - Transport statistics tracking

Binary protocol values marked with # GHIDRA: are inferred from Broadcom
GNSS SoC precedent and need verification via Ghidra decompilation of
FlashUpdater4775_CL389797_r.exe transport layer functions.
"""

import asyncio
import logging
import struct
from dataclasses import dataclass

from .bcm_transport import BcmSerialTransport

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Protocol constants
# ---------------------------------------------------------------------------

# GHIDRA: Auto-baud byte sequence — BCM4775 ROM measures bit timing.
# Standard Broadcom GNSS pattern is 0x80 repeated. The ROM samples
# the start/stop bit edges to determine the host baud rate.
AUTOBAUD_BYTE = 0x80
AUTOBAUD_COUNT = 32  # Number of repetitions
AUTOBAUD_SEQUENCE = bytes([AUTOBAUD_BYTE] * AUTOBAUD_COUNT)

# GHIDRA: Sync request/response bytes for ROM and RAM bootloader.
# These are sent after auto-baud to establish protocol session.
SYNC_REQUEST_ROM = b"\x80"  # GHIDRA: needs verification
SYNC_RESPONSE_ROM = b"\xB5"  # GHIDRA: needs verification
SYNC_REQUEST_RAM = b"\x80"  # GHIDRA: needs verification
SYNC_RESPONSE_RAM = b"\xB5"  # GHIDRA: needs verification

# GHIDRA: Start-of-frame marker for binary packets
PACKET_SOF = 0x04  # GHIDRA: needs verification

# Packet type IDs (inferred from transport statistics and Broadcom precedent)
PACKET_TYPE_DATA = 0x01
PACKET_TYPE_ACK = 0x02
PACKET_TYPE_NAK = 0x03
PACKET_TYPE_SYNC = 0x04

# Protocol limits
MAX_PAYLOAD_SIZE = 4096  # Maximum payload per packet
MAX_RETRIES = 5  # From FlashUpdater MaxRetry counter
ACK_TIMEOUT = 2.0  # Seconds to wait for ACK
SYNC_TIMEOUT = 5.0  # Seconds to wait for sync response
SYNC_RETRIES = 10  # Number of sync attempts before giving up
VERSION_RETRIES = 5  # From FlashUpdater state 7 (5 retries)
DOWNLOAD_CHUNK_TIMEOUT = 0.5  # 500ms from FSM state 12

# Bootloader package constants
BOOTLOADER_MAGIC = b"\xf3\xf2\xf1\xf0"
BOOTLOADER_HEADER_SIZE = 32  # 0x20 bytes


# ---------------------------------------------------------------------------
# Checksum
# ---------------------------------------------------------------------------


def compute_bcm_checksum(data: bytes) -> int:
    """Compute checksum for a BCM4775 binary packet.

    GHIDRA: The exact algorithm needs verification. Using XOR of all
    bytes as the most common simple checksum in Broadcom GNSS protocols.
    Other candidates: CRC-8/MAXIM, CRC-8/CDMA2000.

    Args:
        data: Bytes to checksum (typically SOF through last payload byte).

    Returns:
        Single-byte checksum value (0x00-0xFF).
    """
    result = 0
    for b in data:
        result ^= b
    return result & 0xFF


# ---------------------------------------------------------------------------
# Packet dataclass
# ---------------------------------------------------------------------------


@dataclass
class BcmPacket:
    """A single BCM4775 binary protocol packet.

    Wire format (inferred):
        [SOF:1][Type:1][Seq:1][Length:2 LE][Payload:N][Checksum:1]

    Total overhead: 6 bytes per packet.
    """

    packet_type: int
    seq: int
    payload: bytes = b""

    def encode(self) -> bytes:
        """Serialize packet to wire format bytes."""
        length = len(self.payload)
        header = struct.pack("<BBBH", PACKET_SOF, self.packet_type, self.seq, length)
        frame = header + self.payload
        checksum = compute_bcm_checksum(frame)
        return frame + bytes([checksum])

    @classmethod
    def decode(cls, data: bytes) -> "BcmPacket":
        """Deserialize packet from wire format bytes.

        Args:
            data: Raw bytes including SOF, header, payload, and checksum.

        Returns:
            Decoded BcmPacket.

        Raises:
            ValueError: If SOF mismatch, length mismatch, or checksum error.
        """
        if len(data) < 6:
            raise ValueError(f"Packet too short: {len(data)} bytes (minimum 6)")

        sof, ptype, seq, length = struct.unpack_from("<BBBH", data, 0)

        if sof != PACKET_SOF:
            raise ValueError(f"SOF mismatch: expected 0x{PACKET_SOF:02X}, got 0x{sof:02X}")

        expected_total = 5 + length + 1  # header(5) + payload + checksum(1)
        if len(data) < expected_total:
            raise ValueError(
                f"Packet truncated: expected {expected_total} bytes, got {len(data)}"
            )

        payload = data[5 : 5 + length]
        received_checksum = data[5 + length]
        computed_checksum = compute_bcm_checksum(data[: 5 + length])

        if received_checksum != computed_checksum:
            raise ValueError(
                f"Checksum mismatch: expected 0x{computed_checksum:02X}, "
                f"got 0x{received_checksum:02X}"
            )

        return cls(packet_type=ptype, seq=seq, payload=payload)

    @classmethod
    def make_ack(cls, seq: int) -> "BcmPacket":
        """Create an ACK packet for the given sequence number."""
        return cls(packet_type=PACKET_TYPE_ACK, seq=seq)

    @classmethod
    def make_data(cls, seq: int, payload: bytes) -> "BcmPacket":
        """Create a DATA packet with payload."""
        return cls(packet_type=PACKET_TYPE_DATA, seq=seq, payload=payload)


# ---------------------------------------------------------------------------
# Transport statistics
# ---------------------------------------------------------------------------


@dataclass
class TransportStats:
    """Transport reliability statistics, mirroring FlashUpdater counters."""

    rx_packets: int = 0
    rx_garbage_bytes: int = 0
    rx_packets_lost: int = 0
    tx_packets: int = 0
    tx_reliable_packets: int = 0
    tx_retries: int = 0
    max_retry_reached: int = 0


# ---------------------------------------------------------------------------
# Reliable transport
# ---------------------------------------------------------------------------


class BcmReliableTransport:
    """Reliable transport with sequence numbers, ACKs, and retry.

    Wraps BcmSerialTransport to provide reliable packet delivery.
    Each sent DATA packet gets a sequence number and waits for an ACK.
    On timeout, the packet is retried up to MAX_RETRIES times.

    This layer corresponds to the FlashUpdater transport layer that
    tracks RxPacket, TxPacketRetry, MaxRetry counters.
    """

    def __init__(self, transport: BcmSerialTransport):
        self._transport = transport
        self._tx_seq = 0
        self._rx_seq = 0
        self.stats = TransportStats()

    @property
    def raw_transport(self) -> BcmSerialTransport:
        """Access the underlying raw transport (for auto-baud, sync, etc.)."""
        return self._transport

    def _next_tx_seq(self) -> int:
        """Get next transmit sequence number (wraps at 256)."""
        seq = self._tx_seq
        self._tx_seq = (self._tx_seq + 1) & 0xFF
        return seq

    async def send_packet(self, payload: bytes, timeout: float = ACK_TIMEOUT) -> BcmPacket | None:
        """Send a reliable data packet and wait for ACK.

        Args:
            payload: Data to send.
            timeout: Time to wait for ACK per attempt.

        Returns:
            The ACK packet if received, None if all retries exhausted.
        """
        seq = self._next_tx_seq()
        packet = BcmPacket.make_data(seq, payload)
        encoded = packet.encode()

        for attempt in range(MAX_RETRIES):
            self.stats.tx_packets += 1
            self.stats.tx_reliable_packets += 1

            await self._transport.write_raw(encoded)
            logger.debug("TX packet seq=%d attempt=%d/%d", seq, attempt + 1, MAX_RETRIES)

            try:
                response = await self._read_packet(timeout)
                if response and response.packet_type == PACKET_TYPE_ACK and response.seq == seq:
                    self.stats.rx_packets += 1
                    return response
                elif response and response.packet_type == PACKET_TYPE_NAK:
                    logger.warning("NAK received for seq=%d, retrying", seq)
                    self.stats.tx_retries += 1
                    continue
                else:
                    logger.warning(
                        "Unexpected response for seq=%d: type=0x%02X",
                        seq,
                        response.packet_type if response else 0,
                    )
                    self.stats.tx_retries += 1
            except TimeoutError:
                logger.warning("ACK timeout for seq=%d attempt=%d", seq, attempt + 1)
                self.stats.tx_retries += 1

        self.stats.max_retry_reached += 1
        logger.error("Max retries reached for seq=%d", seq)
        return None

    async def receive_packet(self, timeout: float = ACK_TIMEOUT) -> BcmPacket | None:
        """Receive a data packet and send ACK.

        Args:
            timeout: Time to wait for incoming packet.

        Returns:
            The received data packet, or None on timeout.
        """
        try:
            packet = await self._read_packet(timeout)
        except TimeoutError:
            return None

        if packet and packet.packet_type == PACKET_TYPE_DATA:
            self.stats.rx_packets += 1
            # Send ACK
            ack = BcmPacket.make_ack(packet.seq)
            await self._transport.write_raw(ack.encode())
            self.stats.tx_packets += 1
            return packet

        return packet

    async def _read_packet(self, timeout: float) -> BcmPacket | None:
        """Read and decode a single packet from the transport.

        Scans the input stream for a valid SOF byte, then reads the
        header and payload. Discards garbage bytes (counted in stats).
        """
        deadline = asyncio.get_running_loop().time() + timeout

        while asyncio.get_running_loop().time() < deadline:
            remaining = deadline - asyncio.get_running_loop().time()
            if remaining <= 0:
                break

            try:
                # Read one byte, looking for SOF
                byte = await self._transport.read_raw(1, timeout=min(remaining, 0.5))
                if byte[0] != PACKET_SOF:
                    self.stats.rx_garbage_bytes += 1
                    continue

                # Found SOF — read rest of header (type, seq, length = 4 bytes)
                header_rest = await self._transport.read_raw(4, timeout=min(remaining, 1.0))
                ptype = header_rest[0]
                seq = header_rest[1]
                length = struct.unpack_from("<H", header_rest, 2)[0]

                if length > MAX_PAYLOAD_SIZE:
                    logger.warning("Oversized packet: length=%d, discarding", length)
                    self.stats.rx_garbage_bytes += 5
                    continue

                # Read payload + checksum
                rest = await self._transport.read_raw(length + 1, timeout=min(remaining, 2.0))
                payload = rest[:length]
                checksum = rest[length]

                # Validate checksum
                full_header = bytes([PACKET_SOF]) + header_rest
                computed = compute_bcm_checksum(full_header + payload)
                if checksum != computed:
                    logger.warning(
                        "Checksum mismatch: computed=0x%02X received=0x%02X",
                        computed,
                        checksum,
                    )
                    self.stats.rx_garbage_bytes += 5 + length + 1
                    continue

                return BcmPacket(packet_type=ptype, seq=seq, payload=payload)

            except TimeoutError:
                continue

        raise TimeoutError(f"No valid packet within {timeout}s")

    async def send_autobaud(self) -> None:
        """Send the auto-baud sequence to the BCM4775 ROM.

        The ROM bootloader samples these bytes to calibrate its UART
        receiver to match the host's baud rate.
        """
        logger.info(
            "Sending auto-baud sequence (%d bytes of 0x%02X)",
            AUTOBAUD_COUNT, AUTOBAUD_BYTE,
        )
        await self._transport.write_raw(AUTOBAUD_SEQUENCE)

    async def send_sync(self, target: str = "rom") -> bool:
        """Send sync request and wait for response.

        Args:
            target: "rom" for initial ROM bootloader sync, or
                    "ram" for loaded bootloader patch sync.

        Returns:
            True if sync response received, False on timeout.
        """
        if target == "rom":
            request = SYNC_REQUEST_ROM
            expected = SYNC_RESPONSE_ROM
        else:
            request = SYNC_REQUEST_RAM
            expected = SYNC_RESPONSE_RAM

        for attempt in range(SYNC_RETRIES):
            logger.debug("Sync attempt %d/%d (target=%s)", attempt + 1, SYNC_RETRIES, target)
            await self._transport.write_raw(request)

            try:
                response = await self._transport.read_raw(len(expected), timeout=SYNC_TIMEOUT)
                if response == expected:
                    logger.info("Sync achieved (target=%s)", target)
                    return True
                logger.debug(
                    "Sync response mismatch: expected %s, got %s",
                    expected.hex(),
                    response.hex(),
                )
            except TimeoutError:
                logger.debug("Sync timeout (attempt %d)", attempt + 1)

        logger.error("Sync failed after %d attempts (target=%s)", SYNC_RETRIES, target)
        return False

    def reset_stats(self) -> None:
        """Reset transport statistics counters."""
        self.stats = TransportStats()
