"""Direct USB transport using libusb1 for LibreVNA devices."""

from __future__ import annotations

from contextlib import suppress
import logging
import threading
from typing import Callable

from ..exceptions import DeviceDisconnectedError, TimeoutError
from ..models import USBDeviceDescriptor

import usb1

logger = logging.getLogger(__name__)


class USBTransport:
    """USB bulk transport for LibreVNA protocol endpoints."""

    DATA_EP_OUT = 0x01
    DATA_EP_IN = 0x81
    INTERFACE = 0
    VALID_USB_IDS = (
        (0x0483, 0x564E),
        (0x0483, 0x4121),
        (0x1209, 0x4121),
    )

    def __init__(
        self,
        *,
        on_data: Callable[[bytes], None],
        on_disconnect: Callable[[Exception], None] | None = None,
        read_chunk_size: int = 65536,
    ) -> None:
        """Create transport with RX callback and optional disconnect callback."""
        self._on_data = on_data
        self._on_disconnect = on_disconnect
        self._read_chunk_size = read_chunk_size

        self._ctx: usb1.USBContext | None = None  # type: ignore[valid-type]
        self._handle: usb1.USBDeviceHandle | None = None  # type: ignore[valid-type]
        self._rx_thread: threading.Thread | None = None
        self._stop_event = threading.Event()
        self._tx_lock = threading.Lock()

        self.connected_serial: str | None = None

    @property
    def is_connected(self) -> bool:
        """Return `True` when USB handle is open."""
        return self._handle is not None

    @staticmethod
    def list_devices() -> list[USBDeviceDescriptor]:
        """Enumerate attached LibreVNA USB devices."""
        devices: list[USBDeviceDescriptor] = []
        with usb1.USBContext() as ctx:
            for device, vid, pid in USBTransport._iter_matching_devices(ctx):
                handle: usb1.USBDeviceHandle | None = None
                try:
                    handle = device.open()
                    serial = handle.getSerialNumber() or ""
                except usb1.USBError as exc:
                    logger.debug(
                        "Skipping USB device during discovery vid=0x%04x pid=0x%04x: %s",
                        vid,
                        pid,
                        exc,
                    )
                    continue
                finally:
                    if handle is not None:
                        with suppress(usb1.USBError):
                            handle.close()

                devices.append(USBDeviceDescriptor(serial=serial, vendor_id=vid, product_id=pid))

        devices.sort(key=lambda item: (item.serial, item.vendor_id, item.product_id))
        logger.debug("USB discovery completed, devices=%d", len(devices))
        return devices

    def connect(self, *, serial: str | None = None, timeout_s: float = 1.0) -> None:
        """Open USB device, claim interface, and start RX thread."""
        if self._handle is not None:
            logger.debug("USB connect skipped: already connected")
            return

        logger.debug("Opening libusb context for connect(serial=%s)", serial)
        self._ctx = usb1.USBContext()
        selected_handle: usb1.USBDeviceHandle | None = None
        selected_serial = ""

        for device, _, _ in self._iter_matching_devices(self._ctx):
            handle: usb1.USBDeviceHandle | None = None
            try:
                handle = device.open()
                found_serial = handle.getSerialNumber() or ""
                if serial is not None and found_serial != serial:
                    handle.close()
                    continue

                selected_handle = handle
                selected_serial = found_serial
                break
            except usb1.USBError as exc:
                if handle is not None:
                    with suppress(usb1.USBError):
                        handle.close()
                logger.debug("Skipping USB candidate during connect due to error: %s", exc)
                continue

        if selected_handle is None:
            if self._ctx is not None:
                self._ctx.close()
                self._ctx = None
            serial_msg = f" with serial '{serial}'" if serial else ""
            raise DeviceDisconnectedError(f"No compatible LibreVNA USB device found{serial_msg}")

        try:
            selected_handle.setAutoDetachKernelDriver(True)
            if selected_handle.kernelDriverActive(self.INTERFACE):
                selected_handle.detachKernelDriver(self.INTERFACE)
        except usb1.USBError as exc:
            selected_handle.close()
            if self._ctx is not None:
                self._ctx.close()
                self._ctx = None
            raise DeviceDisconnectedError(f"Failed to prepare USB kernel driver state: {exc}") from exc

        try:
            selected_handle.claimInterface(self.INTERFACE)
        except usb1.USBError as exc:
            selected_handle.close()
            if self._ctx is not None:
                self._ctx.close()
                self._ctx = None
            raise DeviceDisconnectedError(f"Failed to claim USB interface {self.INTERFACE}: {exc}") from exc

        self._handle = selected_handle
        self.connected_serial = selected_serial
        self._stop_event.clear()
        logger.info(
            "USB connected (serial=%s, timeout=%.2fs, endpoint_out=0x%02x, endpoint_in=0x%02x)",
            self.connected_serial,
            timeout_s,
            self.DATA_EP_OUT,
            self.DATA_EP_IN,
        )

        self._rx_thread = threading.Thread(target=self._rx_loop, name="librevna-usb-rx", daemon=True)
        self._rx_thread.start()

    def disconnect(self) -> None:
        """Stop RX thread and close USB resources."""
        logger.debug("USB disconnect requested")
        self._stop_event.set()

        if self._rx_thread is not None and self._rx_thread.is_alive():
            self._rx_thread.join(timeout=1.0)
        self._rx_thread = None

        if self._handle is not None:
            self._handle.releaseInterface(self.INTERFACE)
            self._handle.close()
            self._handle = None

        if self._ctx is not None:
            self._ctx.close()
            self._ctx = None

        logger.info("USB disconnected (serial=%s)", self.connected_serial)
        self.connected_serial = None

    def write(self, data: bytes, *, timeout_s: float = 0.5) -> None:
        """Write one framed packet to device bulk-out endpoint."""
        handle = self._handle
        if handle is None:
            raise DeviceDisconnectedError("USB device is not connected")

        timeout_ms = max(1, int(timeout_s * 1000.0))
        with self._tx_lock:
            try:
                written = handle.bulkWrite(self.DATA_EP_OUT, data, timeout=timeout_ms)
            except usb1.USBErrorTimeout as exc:
                raise TimeoutError("Timed out writing USB bulk packet") from exc
            except usb1.USBErrorNoDevice as exc:
                raise DeviceDisconnectedError("USB device disconnected during write") from exc
            except usb1.USBError as exc:
                raise DeviceDisconnectedError(f"USB write failed: {exc}") from exc

        if written != len(data):
            raise DeviceDisconnectedError(
                f"USB bulk write incomplete: wrote {written}/{len(data)} bytes"
            )
        if logger.isEnabledFor(logging.DEBUG):
            logger.debug("USB TX %d bytes", len(data))

    def _rx_loop(self) -> None:
        """Continuously read bulk-in data and forward to frame scanner callback."""
        handle = self._handle
        if handle is None:
            return

        logger.debug("USB RX thread started")
        timeout_ms = 100
        while not self._stop_event.is_set():
            try:
                data = handle.bulkRead(self.DATA_EP_IN, self._read_chunk_size, timeout=timeout_ms)
            except usb1.USBErrorTimeout:
                continue
            except usb1.USBErrorInterrupted:
                continue
            except usb1.USBErrorNoDevice as exc:
                if self._on_disconnect is not None:
                    self._on_disconnect(DeviceDisconnectedError("USB device disconnected"))
                logger.warning("USB RX stopped: device disconnected")
                return
            except usb1.USBError as exc:
                if self._stop_event.is_set():
                    return
                if self._on_disconnect is not None:
                    self._on_disconnect(DeviceDisconnectedError(f"USB read failed: {exc}"))
                logger.error("USB RX failed: %s", exc)
                return

            if data:
                if logger.isEnabledFor(logging.DEBUG):
                    logger.debug("USB RX %d bytes", len(data))
                self._on_data(bytes(data))
        logger.debug("USB RX thread stopped")

    @classmethod
    def _iter_matching_devices(
        cls,
        ctx: "usb1.USBContext",  # type: ignore[name-defined]
    ):
        """Yield USB devices matching supported LibreVNA VID/PID pairs."""
        for device in ctx.getDeviceList(skip_on_error=True):
            vid = int(device.getVendorID())
            pid = int(device.getProductID())
            if (vid, pid) in cls.VALID_USB_IDS:
                yield device, vid, pid
