"""USB transport wrapper for one LibreVNA in a synchronized device group."""

from __future__ import annotations

import logging
import queue
import threading
import time

from python_app.hardware_full.librevna_driver.enums import PacketType as NativePacketType
from python_app.hardware_full.librevna_driver.models import Packet
from python_app.hardware_full.librevna_driver.protocol import FrameScanner, encode_frame
from python_app.hardware_full.librevna_driver.transport.usb import USBTransport
from python_app.hardware_full.librevna_multi_device_driver.protocol import PacketType

logger = logging.getLogger(__name__)


class LibreVnaUsbBulkConnection:
    """Minimal packet transport for one LibreVNA device."""

    def __init__(self, serial_number: str) -> None:
        if not serial_number:
            raise ValueError("serial_number is required for multi-device acquisition")
        self.serial_number = serial_number
        self._scanner = FrameScanner()
        self._received_packets: queue.Queue[tuple[int, bytes]] = queue.Queue()
        self._fatal_error: Exception | None = None
        self._fatal_lock = threading.Lock()
        self._transport = USBTransport(
            on_data=self._on_data,
            on_disconnect=self._on_disconnect,
            read_chunk_size=4096,
        )
        self._transport.connect(serial=serial_number, timeout_s=2.0)

    def close(self) -> None:
        """Close USB resources."""
        self._transport.disconnect()

    def drain_received_packets(self) -> list[tuple[int, bytes]]:
        """Remove all already-buffered packets without blocking."""
        drained_packets: list[tuple[int, bytes]] = []
        while True:
            try:
                drained_packets.append(self._received_packets.get_nowait())
            except queue.Empty:
                return drained_packets

    def send_packet(self, packet_type: int, payload: bytes = b"") -> None:
        """Send one framed packet."""
        self._raise_if_failed()
        native_packet_type = NativePacketType(int(packet_type))
        self._transport.write(
            encode_frame(Packet(native_packet_type, payload)),
            timeout_s=1.0,
        )

    def receive_packet(self, timeout_seconds: float = 2.0) -> tuple[int, bytes]:
        """Wait for the next received packet."""
        deadline = time.monotonic() + timeout_seconds
        while True:
            self._raise_if_failed()
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise TimeoutError(f"Packet not received from {self.serial_number} within {timeout_seconds} s")
            try:
                return self._received_packets.get(timeout=min(0.2, remaining))
            except queue.Empty:
                continue

    def wait_for_acknowledgement(self, timeout_seconds: float = 2.0) -> None:
        """Wait for an ACK packet while ignoring unrelated asynchronous packets."""
        deadline = time.monotonic() + timeout_seconds
        while True:
            remaining = deadline - time.monotonic()
            if remaining <= 0:
                raise TimeoutError(f"ACK not received from {self.serial_number} within {timeout_seconds} s")
            packet_type, _payload = self.receive_packet(timeout_seconds=remaining)
            if packet_type == PacketType.ACKNOWLEDGE:
                return
            if packet_type == PacketType.NEGATIVE_ACKNOWLEDGE:
                raise RuntimeError(f"Device {self.serial_number} returned NACK")

    def _on_data(self, chunk: bytes) -> None:
        try:
            packets = self._scanner.feed(chunk)
        except Exception as exc:  # noqa: BLE001
            self._set_fatal_error(exc)
            return
        for packet in packets:
            self._received_packets.put((int(packet.type), bytes(packet.payload)))

    def _on_disconnect(self, exc: Exception) -> None:
        self._set_fatal_error(exc)

    def _set_fatal_error(self, exc: Exception) -> None:
        with self._fatal_lock:
            if self._fatal_error is None:
                logger.error("LibreVNA USB transport failed for %s: %s", self.serial_number, exc)
                self._fatal_error = exc

    def _raise_if_failed(self) -> None:
        with self._fatal_lock:
            if self._fatal_error is None:
                return
            raise RuntimeError(f"USB transport failed for {self.serial_number}: {self._fatal_error}") from self._fatal_error


def list_connected_device_serial_numbers() -> list[str]:
    """Return serial numbers of all connected LibreVNA devices."""
    return [device.serial for device in USBTransport.list_devices()]
