"""Background scanner that auto-detects which serial port + baud rate carries a
live NMEA/AIS feed.

Key idea: a passing NMEA checksum proves the baud rate is right, and a validated
!AIVDM/!AIVDO proves it's specifically the AIS source. But two "empty" outcomes
must be told apart:

  * GARBAGE  — bytes arrive but nothing validates  => wrong baud, reject quickly.
  * SILENCE  — no bytes at all                      => could be the right baud
               between AIS bursts, so wait longer before giving up.

AIS is bursty (a receiver may go several seconds between sentences, and there's
often a moment of latency right after opening the port), so on the AIS-standard
baud (38400) we listen patiently and exit the instant a valid AIS sentence
appears. Wrong bauds still bail out fast on garbage. Runs off the GUI thread.
"""
import time

from qgis.PyQt.QtCore import QThread, pyqtSignal

from . import vendor
vendor.setup()  # ensure bundled pyserial is importable

try:
    import serial
    PYSERIAL_AVAILABLE = True
except ImportError:
    serial = None
    PYSERIAL_AVAILABLE = False

from .nmea_utils import is_valid_nmea, is_ais_sentence, is_gps_sentence
from .serial_reader import list_serial_ports

# Tunables --------------------------------------------------------------------
CANDIDATE_BAUDS = [38400, 4800, 9600, 115200]  # AIS standard (38400) first
AIS_LISTEN_SECONDS = 7.0      # patient listen on 38400 (handles bursty AIS)
ALT_LISTEN_SECONDS = 2.5      # shorter listen on fallback bauds
QUICK_REJECT_SECONDS = 1.5    # if only garbage by now, it's the wrong baud
AIS_EARLY_EXIT = 1            # stop the instant this many valid AIS seen
# -----------------------------------------------------------------------------


class PortScanner(QThread):
    """Probes ports/bauds and reports the best live source it can find.

    Signals:
        progress(str)        — human-readable status line
        portResult(dict)     — a finding for one port+baud (kind/valid/ais/gps)
        scanFinished(object) — the best pick dict, or None if nothing found
    """
    progress = pyqtSignal(str)
    portResult = pyqtSignal(dict)
    scanFinished = pyqtSignal(object)

    def __init__(self, ports=None, bauds=None, parent=None):
        """Scan the given devices (or all ports when None) across the candidate
        baud rates; runs as a QThread."""
        super().__init__(parent)
        self._ports = ports               # list of device strings, or None=all
        self._bauds = bauds or CANDIDATE_BAUDS
        self._running = False

    def run(self):
        """Thread body: probe each device/baud, emit findings, and finish with
        the best pick — an AIS source short-circuits the scan immediately."""
        if not PYSERIAL_AVAILABLE:
            self.progress.emit("pyserial not available.")
            self.scanFinished.emit(None)
            return

        devices = self._ports
        if devices is None:
            devices = [p["device"] for p in list_serial_ports()]
        if not devices:
            self.progress.emit("No serial ports to scan.")
            self.scanFinished.emit(None)
            return

        self._running = True
        best = None
        for device in devices:
            if not self._running:
                break
            for baud in self._bauds:
                if not self._running:
                    break
                listen = (AIS_LISTEN_SECONDS if baud == 38400
                          else ALT_LISTEN_SECONDS)
                self.progress.emit(
                    f"Probing {device} @ {baud} (up to {int(listen)}s)…"
                )
                res = self._probe(device, baud, listen)
                if res is None:
                    continue                       # couldn't open — skip
                if res["ais"] > 0:
                    self.portResult.emit(res)
                    self.scanFinished.emit(res)     # AIS source — done
                    return
                if res["valid"] > 0:
                    self.portResult.emit(res)       # right baud, but not AIS
                    if best is None:
                        best = res
                    break                           # move to next port
                # silence or garbage here -> try the next baud

        self.scanFinished.emit(best)

    def _probe(self, device, baud, listen_seconds):
        """Listen on one device/baud and tally valid NMEA / AIS / GPS sentences,
        bailing early on a valid AIS hit or on sustained garbage (wrong baud).
        Returns a counts dict, or None if the port can't be opened."""
        try:
            ser = serial.Serial(device, baud, timeout=0.5)
        except Exception:
            return None
        counts = {"device": device, "baud": baud,
                  "valid": 0, "ais": 0, "gps": 0, "total": 0, "sample": ""}
        start = time.monotonic()
        saw_bytes = False
        try:
            while self._running and (time.monotonic() - start) < listen_seconds:
                raw = ser.readline()
                if raw:
                    saw_bytes = True
                    line = raw.decode("ascii", errors="replace").strip()
                    if line:
                        counts["total"] += 1
                        if is_valid_nmea(line):
                            counts["valid"] += 1
                            if is_ais_sentence(line):
                                counts["ais"] += 1
                                counts["sample"] = line
                                if counts["ais"] >= AIS_EARLY_EXIT:
                                    break
                            elif is_gps_sentence(line):
                                counts["gps"] += 1
                                if not counts["sample"]:
                                    counts["sample"] = line
                # Fast-reject a WRONG baud: bytes flowing but nothing validates.
                # (Silence -> saw_bytes stays False -> we keep waiting instead.)
                if (counts["valid"] == 0 and saw_bytes
                        and (time.monotonic() - start) >= QUICK_REJECT_SECONDS):
                    break
        finally:
            try:
                ser.close()
            except Exception:  # nosec B110
                pass
        counts["kind"] = ("ais" if counts["ais"] else
                          "gps" if counts["gps"] else
                          "nmea" if counts["valid"] else "none")
        return counts

    def stop(self):
        """Ask the scan loop to finish and wait for the thread to exit."""
        self._running = False
        self.wait(int(AIS_LISTEN_SECONDS * 1000) + 1000)
