# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

"""USB Serial transport for MeshCore companion devices.

Uses pyserial to communicate with MeshCore devices over USB serial ports.
"""

import contextlib
import glob
import logging
import time

from gi.repository import GLib
from meshy.transport_base import ConnectionState, FrameTransport

log = logging.getLogger(__name__)

SERIAL_BAUD_RATE = 115200


class UsbDevice:
    """Represents a discovered USB serial device."""

    def __init__(self, path: str, name: str = ""):
        self.path = path
        self.address = path  # Use path as address for storage key
        self.name = name or path

    def __repr__(self):
        return f"UsbDevice({self.path})"


class UsbSerialTransport(FrameTransport):
    """USB serial transport using pyserial."""

    _LABEL = "USB"

    def __init__(self):
        super().__init__()
        self._port = None
        self._connection_error: str | None = None
        self._is_permission_error: bool = False

    def get_serial_devices(self) -> list[UsbDevice]:
        """Scan for available USB serial devices."""
        devices = []

        from meshy.platforms import is_macos
        if is_macos():
            patterns = ["/dev/cu.usbserial-*", "/dev/cu.usbmodem-*"]
        else:
            patterns = ["/dev/ttyACM*", "/dev/ttyUSB*"]

        for pattern in patterns:
            for path in sorted(glob.glob(pattern)):
                name = path.split("/")[-1]
                devices.append(UsbDevice(path=path, name=name))
        return devices

    def get_paired_devices(self) -> list[UsbDevice]:
        """Return available serial devices (USB doesn't have pairing)."""
        return self.get_serial_devices()

    def connect_device(self, device_path: str):
        """Connect to a USB serial device."""
        import serial

        if self._state == ConnectionState.CONNECTED:
            self.disconnect()

        self._set_state(ConnectionState.CONNECTING)
        self._connected_address = device_path

        try:
            self._port = serial.Serial(
                device_path,
                baudrate=SERIAL_BAUD_RATE,
                bytesize=serial.EIGHTBITS,
                parity=serial.PARITY_NONE,
                stopbits=serial.STOPBITS_ONE,
                xonxoff=False,
                rtscts=False,
                timeout=None,
            )
            self._port.rts = False
            self._port.reset_input_buffer()
            self._port.reset_output_buffer()
            log.info(f"Opened serial port {device_path} at {SERIAL_BAUD_RATE} baud")

            self._start_read_thread()
            self._set_state(ConnectionState.CONNECTED)

        except Exception as e:
            error_msg = str(e)
            log.error(f"Failed to open serial port {device_path}: {error_msg}")
            self._connection_error = error_msg
            self._is_permission_error = "Permission denied" in error_msg or "Errno 13" in error_msg
            self._connected_address = None
            self._set_state(ConnectionState.DISCONNECTED)

    def disconnect(self):
        """Disconnect from the serial device."""
        if self._state in (ConnectionState.DISCONNECTED, ConnectionState.DISCONNECTING):
            return

        self._set_state(ConnectionState.DISCONNECTING)
        self._running = False
        self._close_io()
        self._connected_address = None
        self._set_state(ConnectionState.DISCONNECTED)

    # ─── FrameTransport implementation ────────────────────────

    def _is_io_ready(self) -> bool:
        return self._port is not None

    def _write_bytes(self, frame: bytes):
        self._port.write(frame)

    def _close_io(self):
        if self._port:
            with contextlib.suppress(Exception):
                self._port.close()
            self._port = None

    def _read_loop(self, gen: int):
        """Background thread that reads from the serial port."""
        port = self._port
        while self._running and gen == self._connect_gen and port:
            try:
                waiting = port.in_waiting
                if waiting > 0:
                    data = port.read(waiting)
                    if data:
                        self._rx_buffer.extend(data)
                        self._process_rx_buffer()
                else:
                    time.sleep(0.05)
            except OSError as e:
                if self._running and gen == self._connect_gen:
                    log.error(f"USB read error: {e}")
                    GLib.idle_add(self._handle_disconnect, gen)
                break
            except Exception as e:
                if self._running and gen == self._connect_gen:
                    log.error(f"USB read error: {e}")
                break
