"""Binary serial transport for BCM4775 bootloader protocol.

Separate from SerialTransport (which handles NMEA text lines). This
transport deals with raw binary frames for the firmware update protocol.
Uses pyserial-asyncio for non-blocking I/O.

The BCM4775 firmware update protocol uses binary packets, not
newline-terminated text. This transport provides:
  - write_raw(): Send arbitrary bytes
  - read_raw(): Read exact number of bytes with timeout
  - read_until(): Read until a marker byte sequence appears
  - set_baud(): Change baud rate after auto-baud negotiation
  - flush_input(): Discard pending input before sync
"""

import asyncio
import logging

import serial_asyncio

logger = logging.getLogger(__name__)


class BcmSerialTransport:
    """Raw binary serial transport for BCM4775 bootloader communication.

    Unlike SerialTransport (NMEA text lines), this handles arbitrary
    binary data with explicit length reads and marker-based framing.
    """

    def __init__(self, port: str = "/dev/ttyS0", baud: int = 115200):
        self.port = port
        self.baud = baud
        self._reader: asyncio.StreamReader | None = None
        self._writer: asyncio.StreamWriter | None = None
        self._connected = False

    @property
    def connected(self) -> bool:
        return self._connected

    async def __aenter__(self) -> "BcmSerialTransport":
        await self.connect()
        return self

    async def __aexit__(self, *exc) -> None:
        await self.disconnect()

    async def connect(self) -> None:
        """Open serial connection for binary communication."""
        logger.info("BCM transport: connecting to %s at %d baud", self.port, self.baud)
        self._reader, self._writer = await serial_asyncio.open_serial_connection(
            url=self.port,
            baudrate=self.baud,
            bytesize=8,
            parity="N",
            stopbits=1,
            rtscts=False,
            xonxoff=False,
        )
        self._connected = True
        logger.info("BCM transport: connected to %s", self.port)

    async def disconnect(self) -> None:
        """Close serial connection."""
        if self._writer:
            self._writer.close()
            self._connected = False
            logger.info("BCM transport: disconnected from %s", self.port)

    async def write_raw(self, data: bytes) -> None:
        """Write raw bytes to the serial port."""
        if not self._writer:
            raise ConnectionError("BCM transport not connected")
        self._writer.write(data)
        await self._writer.drain()
        logger.debug("BCM TX: %s (%d bytes)", data[:16].hex(), len(data))

    async def read_raw(self, length: int, timeout: float = 2.0) -> bytes:
        """Read exactly `length` bytes from the serial port.

        Args:
            length: Number of bytes to read.
            timeout: Maximum wait time in seconds.

        Returns:
            Exactly `length` bytes.

        Raises:
            TimeoutError: If not enough bytes arrive within timeout.
            ConnectionError: If not connected.
        """
        if not self._reader:
            raise ConnectionError("BCM transport not connected")
        try:
            data = await asyncio.wait_for(self._reader.readexactly(length), timeout=timeout)
            logger.debug("BCM RX: %s (%d bytes)", data[:16].hex(), len(data))
            return data
        except asyncio.IncompleteReadError as e:
            raise TimeoutError(
                f"Incomplete read: got {len(e.partial)} of {length} bytes"
            ) from None
        except TimeoutError:
            raise TimeoutError(f"No response within {timeout}s (expected {length} bytes)") from None

    async def read_until(self, marker: bytes, timeout: float = 2.0, max_bytes: int = 4096) -> bytes:
        """Read bytes until a marker sequence is found.

        Returns all bytes up to and including the marker. Useful for
        finding sync responses or packet delimiters in the stream.

        Args:
            marker: Byte sequence to search for.
            timeout: Maximum wait time in seconds.
            max_bytes: Safety limit to prevent unbounded reads.

        Returns:
            All bytes read, including the marker.

        Raises:
            TimeoutError: If marker not found within timeout.
            ValueError: If max_bytes exceeded without finding marker.
        """
        if not self._reader:
            raise ConnectionError("BCM transport not connected")

        buf = bytearray()
        deadline = asyncio.get_running_loop().time() + timeout

        while True:
            remaining = deadline - asyncio.get_running_loop().time()
            if remaining <= 0:
                raise TimeoutError(
                    f"Marker {marker.hex()} not found within {timeout}s "
                    f"(read {len(buf)} bytes)"
                )

            try:
                byte = await asyncio.wait_for(
                    self._reader.read(1), timeout=min(remaining, 0.5)
                )
                if not byte:
                    continue
                buf.extend(byte)

                if buf.endswith(marker):
                    logger.debug(
                        "BCM RX (until %s): %d bytes", marker.hex(), len(buf)
                    )
                    return bytes(buf)

                if len(buf) > max_bytes:
                    raise ValueError(
                        f"Read {len(buf)} bytes without finding marker {marker.hex()}"
                    )
            except TimeoutError:
                if asyncio.get_running_loop().time() >= deadline:
                    raise TimeoutError(
                        f"Marker {marker.hex()} not found within {timeout}s"
                    ) from None

    async def set_baud(self, baud: int) -> None:
        """Change the serial baud rate.

        Called after auto-baud negotiation to switch to the agreed rate.
        Closes and reopens the connection at the new baud rate.
        """
        logger.info("BCM transport: switching baud %d → %d", self.baud, baud)
        await self.disconnect()
        self.baud = baud
        await self.connect()

    async def flush_input(self) -> int:
        """Discard all pending input bytes.

        Called before sync to clear any stale NMEA data or garbage
        from the serial buffer. Returns the number of bytes flushed.
        """
        if not self._reader:
            raise ConnectionError("BCM transport not connected")

        flushed = 0
        while True:
            try:
                data = await asyncio.wait_for(self._reader.read(1024), timeout=0.1)
                if data:
                    flushed += len(data)
                else:
                    break
            except TimeoutError:
                break

        if flushed:
            logger.debug("BCM transport: flushed %d bytes", flushed)
        return flushed
