"""Serial transport abstraction for LED clients.

Wraps the platform-specific way to open a serial line so callers
(AdalightClient, etc.) don't import ``pyserial`` directly. The primary
motivation is Android: under Chaquopy, ``pyserial`` exists but cannot
touch USB ports, so we route writes through a Kotlin
``UsbSerialBridge`` instead.

URL format
----------
- ``"COM3"`` or ``"COM3:115200"`` — desktop COM port (Windows)
- ``"/dev/ttyUSB0"`` or ``"/dev/ttyUSB0:115200"`` — desktop tty
- ``"usb:VID:PID"`` or ``"usb:VID:PID:serial@115200"`` — Android USB device

Selection happens in :func:`open_transport`: anything starting with
``usb:`` goes through :class:`AndroidSerialTransport`, everything else
through :class:`PySerialTransport`. :func:`list_serial_ports` returns
desktop COM ports on desktop, USB devices on Android.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import List, Protocol

from ledgrab.utils import get_logger
from ledgrab.utils.platform import is_android

logger = get_logger(__name__)

DEFAULT_BAUD_RATE = 115200


@dataclass(frozen=True)
class SerialPortInfo:
    """A discovered serial port, regardless of source platform."""

    device: str  # URL-like identifier passed back to open_transport()
    description: str  # Human-friendly name for the UI


class SerialTransport(Protocol):
    """Minimal serial line — open, write bytes, close. No reads (LED out only)."""

    @property
    def is_open(self) -> bool: ...
    def open(self) -> None: ...
    def write(self, data: bytes) -> None: ...
    def flush(self) -> None: ...
    def close(self) -> None: ...


def parse_serial_url(url: str) -> tuple[str, int]:
    """Parse a serial URL into (device, baud_rate).

    Recognized formats::

        "COM3"               -> ("COM3",            115200)
        "COM3:230400"        -> ("COM3",            230400)
        "/dev/ttyUSB0"       -> ("/dev/ttyUSB0",    115200)
        "/dev/ttyUSB0:230400"-> ("/dev/ttyUSB0",    230400)
        "usb:1a86:7523"      -> ("usb:1a86:7523",   115200)
        "usb:1a86:7523@250k" -> raises (only ints accepted for baud)
        "usb:1a86:7523@230400" -> ("usb:1a86:7523", 230400)

    USB URLs use ``@`` to separate baud because the colon is already
    the field separator between vendor and product IDs.
    """
    raw = url.strip()
    if raw.startswith("usb:"):
        if "@" in raw:
            device, _, baud_str = raw.rpartition("@")
            try:
                return device, int(baud_str)
            except ValueError:
                pass
        return raw, DEFAULT_BAUD_RATE

    # Desktop COM/tty paths use a trailing :BAUD suffix.
    if ":" in raw:
        head, _, tail = raw.rpartition(":")
        try:
            return head, int(tail)
        except ValueError:
            pass
    return raw, DEFAULT_BAUD_RATE


def list_serial_ports() -> List[SerialPortInfo]:
    """Enumerate serial ports available on this host (or USB devices on Android)."""
    if is_android():
        try:
            from ledgrab.core.devices.android_serial_transport import (
                list_android_usb_devices,
            )

            return list_android_usb_devices()
        except Exception as e:
            logger.warning("Android USB enumeration failed: %s", e)
            return []

    try:
        import serial.tools.list_ports
    except ImportError:
        logger.warning("pyserial not available — serial enumeration disabled")
        return []

    return [
        SerialPortInfo(device=p.device, description=p.description or p.device)
        for p in serial.tools.list_ports.comports()
    ]


def port_exists(device: str) -> bool:
    """Return True if a port with this device id is currently visible."""
    target = device.upper()
    return any(p.device.upper() == target for p in list_serial_ports())


def open_transport(
    url: str,
    baud_rate: int | None = None,
    timeout: float = 1.0,
) -> SerialTransport:
    """Construct an unopened transport for ``url``. Caller invokes ``open()``."""
    device, parsed_baud = parse_serial_url(url)
    effective_baud = baud_rate or parsed_baud

    if device.startswith("usb:"):
        from ledgrab.core.devices.android_serial_transport import (
            AndroidSerialTransport,
        )

        return AndroidSerialTransport(device, effective_baud)

    return PySerialTransport(device, effective_baud, timeout)


class PySerialTransport:
    """Default serial transport backed by ``pyserial`` on desktop hosts."""

    def __init__(self, device: str, baud_rate: int, timeout: float = 1.0) -> None:
        self._device = device
        self._baud_rate = baud_rate
        self._timeout = timeout
        self._serial = None

    @property
    def is_open(self) -> bool:
        return self._serial is not None and self._serial.is_open

    def open(self) -> None:
        import serial  # imported here so Android (no real pyserial) doesn't fail at import

        self._serial = serial.Serial(
            port=self._device, baudrate=self._baud_rate, timeout=self._timeout
        )

    def write(self, data: bytes) -> None:
        if self._serial is None:
            raise RuntimeError(f"Serial port {self._device} is not open")
        self._serial.write(data)

    def flush(self) -> None:
        if self._serial is not None:
            self._serial.flush()

    def close(self) -> None:
        if self._serial is not None and self._serial.is_open:
            try:
                self._serial.close()
            except Exception as e:
                logger.warning("Error closing %s: %s", self._device, e)
        self._serial = None
