"""Serial port helpers for STS devices."""

from __future__ import annotations

import platform

from .exceptions import ConnectionError

try:
    from serial import Serial
    from serial.tools import list_ports
except ImportError:  # pragma: no cover
    Serial = None
    list_ports = None


def get_port_serial_number(port: str) -> str | None:
    """Return the USB serial number for *port*, or ``None`` if unavailable.

    Resolves symlinks (e.g. ``/dev/serial/by-id/...``) before matching
    so that alias paths work correctly.
    """
    if list_ports is None:
        return None

    import os

    real_port = os.path.realpath(port)
    for info in list_ports.comports():
        if os.path.realpath(info.device) == real_port:
            return info.serial_number
    return None


def find_servo_port(return_all: bool = False) -> str | list[str]:
    """Find candidate serial ports for servo adapters."""
    if list_ports is None:
        raise ConnectionError("pyserial is not installed")

    ports: list[str] = []
    system = platform.system()
    for item in list_ports.comports():
        device = item.device
        if system == "Linux" and ("ttyUSB" in device or "ttyACM" in device):
            ports.append(device)
        elif system == "Darwin" and ("usbmodem" in device or "usbserial" in device):
            ports.append(device)
        elif system == "Windows" and device.startswith("COM"):
            ports.append(device)

    if not ports:
        raise ConnectionError("No suitable serial port found")

    ports = sorted(set(ports))
    return ports if return_all else ports[0]


class SerialClient:
    """Thin serial wrapper to simplify testing/injection."""

    def __init__(self, port: str, baudrate: int, timeout: float) -> None:
        self._port = port
        self._baudrate = baudrate
        self._timeout = timeout
        self._serial: Serial | None = None

    @property
    def port(self) -> str:
        """Return the configured port path."""
        return self._port

    @property
    def baudrate(self) -> int:
        """Return the configured baud rate."""
        return self._baudrate

    def open(self) -> None:
        """Open serial device. No-op if already open."""
        if self.is_open:
            return
        if Serial is None:
            raise ConnectionError("pyserial is not installed")

        try:
            self._serial = Serial(
                port=self._port,
                baudrate=self._baudrate,
                timeout=0,
                write_timeout=self._timeout,
            )
        except Exception as exc:
            raise ConnectionError(f"Failed to open serial {self._port}: {exc}") from exc

    def close(self) -> None:
        """Close serial device."""
        if self._serial and self._serial.is_open:
            self._serial.close()

    @property
    def is_open(self) -> bool:
        """Return if serial is open."""
        return bool(self._serial and self._serial.is_open)

    def reset_input_buffer(self) -> None:
        """Reset input buffer."""
        if self._serial is None:
            return
        self._serial.reset_input_buffer()

    def write(self, data: bytes) -> int:
        """Write bytes to serial."""
        if self._serial is None:
            raise ConnectionError("Serial is not open")
        return self._serial.write(data)

    def flush(self) -> None:
        """Flush output."""
        if self._serial is None:
            return
        self._serial.flush()

    def read(self, size: int) -> bytes:
        """Read bytes from serial."""
        if self._serial is None:
            raise ConnectionError("Serial is not open")
        return self._serial.read(size)

    def calc_poll_timeout(self, response_bytes: int) -> float:
        """Compute a baud-rate-aware poll timeout for *response_bytes*.

        Based on the official Feetech SDK formula:
        ``tx_time_per_byte = 10.0 / baudrate`` (seconds, 8N1 = 10 bits),
        total wire time = tx_time_per_byte * response_bytes + 50 ms latency.
        Result is clamped to [5 ms, 1.0 s].
        """
        tx_time = 10.0 / self._baudrate * response_bytes + 0.050
        return max(0.005, min(tx_time, 1.0))

    def read_available(self, max_size: int, poll_timeout: float = 0.005) -> bytes:
        """Read up to *max_size* bytes that are already in the buffer.

        Waits at most *poll_timeout* seconds for additional bytes to arrive,
        then returns whatever is available — avoiding long serial timeout waits
        when some servos do not respond.
        """
        if self._serial is None:
            raise ConnectionError("Serial is not open")
        import time

        deadline = time.perf_counter() + poll_timeout
        while self._serial.in_waiting < max_size:
            if time.perf_counter() >= deadline:
                break
            time.sleep(0.0005)
        avail = min(self._serial.in_waiting, max_size)
        return self._serial.read(avail) if avail > 0 else b""
