"""High-level wafer device communication service.

Coordinates port enumeration, detect, read, and erase operations
using settings from LocalSettingsModel.  Designed to be called from
QML via @Slot methods on a QObject controller.
"""

import logging
from typing import Optional

import serial.tools.list_ports

from pygui.backend.data.local_settings import LocalSettings
from pygui.serialcomm.serial_port import SerialPort, WaferInfo

log = logging.getLogger(__name__)


class DeviceService:
    """High-level wafer device communication."""

    def __init__(self, settings: LocalSettings) -> None:
        self._settings = settings
        self._cached_ports: list[str] = []
        self._last_port_scan_time = 0.0

    def clear_port_scan_cache(self) -> None:
        """Force the next scan to bypass the cache."""
        self._last_port_scan_time = 0.0

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def enumerate_ports(self) -> list[str]:
        """Return list of available serial port names."""
        import time
        now = time.monotonic()
        if now - self._last_port_scan_time < 2.0:
            return self._cached_ports

        hardware_ports = [p.device for p in serial.tools.list_ports.comports()]
        virtual_ports = []

        # On macOS/Linux, also detect virtual loopback serial ports (PTYs) opened by socat
        import platform
        import re
        import subprocess

        if platform.system() in ("Darwin", "Linux"):
            try:
                # Find PTYs opened by socat
                result = subprocess.run(
                    ["lsof", "-c", "socat"],
                    capture_output=True,
                    text=True,
                    check=False
                )
                if result.returncode == 0:
                    for line in result.stdout.splitlines():
                        parts = line.strip().split()
                        if len(parts) >= 9:
                            fd = parts[3]
                            # Exclude standard FDs (0, 1, 2) which are typically the controlling terminal
                            if fd.startswith(('0', '1', '2')):
                                continue
                            name = parts[-1]
                            match = re.search(r'(/dev/(?:ttys\d+|pts/\d+))\b', name)
                            if match:
                                port = match.group(1)
                                if port not in virtual_ports:
                                    virtual_ports.append(port)
            except Exception as e:
                log.debug("Error listing virtual ports via lsof: %s", e)

        # Prioritize physical USB serial interfaces first -> generic hardware -> virtual ports
        # Virtual ports (PTYs detected via lsof) should NOT appear in hardware_ports,
        # so we filter them out explicitly.
        virtual_set = set(virtual_ports)
        ordered_ports = []
        for p in hardware_ports:
            if "usbserial" in p.lower() or "usb" in p.lower() or "ttyusb" in p.lower():
                if p not in ordered_ports:
                    ordered_ports.append(p)
        for p in hardware_ports:
            if p not in ordered_ports and p not in virtual_set:
                ordered_ports.append(p)
        for p in virtual_ports:
            if p not in ordered_ports:
                ordered_ports.append(p)

        self._cached_ports = ordered_ports
        self._last_port_scan_time = time.monotonic()
        return ordered_ports


    def detect_wafer(self, port: str) -> Optional[WaferInfo]:
        """Try to detect a wafer on the given port.

        Opens the port, sends s0 command, parses response.
        Returns WaferInfo if detected, None otherwise.
        """
        try:
            sp = SerialPort(port)
            info = sp.detect(self._settings.wafer_detect_timeout)
            return info
        except Exception as exc:
            log.error("Detect failed on %s: %s", port, exc)
            return None

    SCAN_PER_PORT_TIMEOUT_MS = 1500
    """Per-port timeout while scanning. Wafer responds fast or not at all."""

    def detect_all_ports(self) -> tuple[Optional[str], Optional[WaferInfo]]:
        """Scan every available serial port until one responds to s0.

        Mirrors C# chkConnection(): enumerates all ports, tries each one,
        returns the first that responds with a valid 1024-byte payload.

        Uses a short per-port timeout (SCAN_PER_PORT_TIMEOUT_MS) so a
        machine with many serial devices (Bluetooth, debug consoles, etc.)
        doesn't take 30+ seconds to scan. The configured wafer_detect_timeout
        is reserved for confirmed-port operations.

        Returns:
            (port_name, WaferInfo) on success, (None, None) if nothing found.
        """
        ports = self.enumerate_ports()
        log.info("Scanning %d port(s) for wafer ...", len(ports))
        for port in ports:
            log.info("  trying %s ...", port)
            try:
                sp = SerialPort(port)
                info = sp.detect(self.SCAN_PER_PORT_TIMEOUT_MS)
            except Exception as exc:
                log.warning("  %s: %s", port, exc)
                info = None
            if info is not None:
                log.info("  found wafer on %s (family=%s)", port, info.family_code)
                return port, info
        log.info("  no wafer detected on any port")
        return None, None

    def read_wafer_data(
        self,
        port: str,
        family_code: str = "",
        cmd: str = "D1",
        timeout_ms: int | None = None,
        retries: int | None = None,
    ) -> Optional[bytes]:
        """Full read workflow: open → send command → retry → close.

        Args:
            port: Serial port name (e.g. "/dev/ttyUSB0").
            family_code: Wafer family ("P", "X", etc.) for size validation.
            cmd: Command string ("D1" for sensor data, "F1" for debug data).
            timeout_ms: Read timeout in ms (uses settings default if None).
            retries: Max retries on bad read (uses settings default if None).

        Returns:
            Raw bytes from the wafer, or None on failure.
        """
        if timeout_ms is None:
            timeout_ms = self._settings.wafer_read_timeout
        if retries is None:
            retries = self._settings.wafer_read_retries

        # Determine expected hex string length.
        # get_wafer_data_size returns BYTES; hex string is 2× that.
        if family_code:
            expected_hex_len = self._settings.get_wafer_data_size(family_code) * 2
        else:
            expected_hex_len = 0  # no validation

        try:
            sp = SerialPort(port)
            data = sp.read_memory(
                cmd=cmd,
                expected_hex_len=expected_hex_len,
                timeout_ms=timeout_ms,
                retries=retries,
            )
            if data is not None:
                log.info("Read %d bytes from wafer on %s", len(data), port)
            else:
                log.warning("Read returned no data from %s", port)
            return data
        except Exception as exc:
            log.error("Read failed on %s: %s", port, exc)
            return None

    def erase_wafer(self, port: str) -> bool:
        """Send p1 erase command. Wafer takes ~15s to erase.

        Returns True if command was sent successfully.
        """
        try:
            sp = SerialPort(port)
            return sp.erase_memory()
        except Exception as exc:
            log.error("Erase failed on %s: %s", port, exc)
            return False
