"""Probe a TCP AIS endpoint to discover its security posture before connecting.

Given host:port, :func:`probe` answers three questions, in order, with one short
timeout budget and without sending anything sensitive:

  1. Is the port reachable at all?
  2. Does it speak **TLS**?  (attempt a TLS handshake; if the server returns a
     ServerHello the endpoint is TLS — otherwise it's plaintext)
  3. Does it need **auth**?  (heuristic: on a reachable endpoint that does NOT
     immediately stream NMEA, auth is likely — and any text banner it sends, e.g.
     a login prompt, is captured to hint the scheme)

What it CANNOT tell apart is username/password vs token — both are just "send a
secret", and servers don't advertise which; the login dialog lets the user pick.

Pure stdlib (socket + ssl), no Qt/QGIS, so it is unit-testable against a local
mock server. Runs quickly and never raises — failures land in ``error``.
"""
import socket
import ssl


def _looks_nmea(data):
    """True if any line in `data` (bytes) looks like an NMEA/AIS sentence."""
    for line in (data or b"").splitlines():
        line = line.strip()
        if line[:1] in (b"!", b"$") and b"," in line:
            return True
    return False


class ProbeResult:
    """What a probe found about one endpoint."""

    __slots__ = ("host", "port", "reachable", "tls", "streams_nmea",
                 "banner", "auth_required", "error")

    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.reachable = False
        self.tls = False           # endpoint completed a TLS handshake
        self.streams_nmea = False  # NMEA arrived on connect without auth
        self.banner = ""           # any non-NMEA text the server sent first
        self.auth_required = False  # heuristic: reachable but no NMEA streamed
        self.error = None          # connect/probe failure message

    @property
    def secured(self):
        """True if the endpoint needs special handling (TLS and/or auth)."""
        return bool(self.tls or self.auth_required)

    def summary(self):
        """One-line human description of the finding."""
        if not self.reachable:
            return "unreachable%s" % (f" ({self.error})" if self.error else "")
        bits = ["TLS" if self.tls else "plaintext"]
        if self.streams_nmea:
            bits.append("streaming NMEA (no auth)")
        elif self.auth_required:
            bits.append("auth required" if self.banner
                        else "no data on connect (auth likely)")
        if self.banner:
            bits.append("banner: %r" % self.banner[:60])
        return ", ".join(bits)


def _read_first(sock, timeout):
    """Read up to 512 bytes within `timeout`, or b'' (never raises)."""
    try:
        sock.settimeout(timeout)
        return sock.recv(512) or b""
    except Exception:                          # noqa: BLE001  (timeout/reset/etc.)
        return b""


def _classify_first_bytes(r, data):
    """Fill streams_nmea / banner / auth_required from the first bytes seen."""
    if _looks_nmea(data):
        r.streams_nmea = True
        r.auth_required = False
    else:
        text = data.decode("ascii", "replace").strip()
        if text:
            r.banner = text[:200]
        r.auth_required = True                  # reachable but no NMEA -> auth likely


def _probe_plaintext(host, port, timeout, r):
    """Plaintext leg: fresh connect, read the first bytes, classify."""
    try:
        s = socket.create_connection((host, port), timeout=timeout)
        r.reachable = True
        data = _read_first(s, timeout)
        try:
            s.close()
        except Exception:  # nosec B110
            pass
        _classify_first_bytes(r, data)
    except Exception as exc:                    # noqa: BLE001
        r.error = str(exc)
    return r


def probe(host, port, timeout=4.0):
    """Probe ``host:port`` and return a :class:`ProbeResult`. Tries TLS first; if
    the handshake fails, re-probes the endpoint as plaintext."""
    host = str(host).strip()
    try:
        port = int(port)
    except (TypeError, ValueError):
        r = ProbeResult(host, port)
        r.error = "invalid port"
        return r
    r = ProbeResult(host, port)
    # --- TLS attempt -------------------------------------------------------
    raw = None
    try:
        raw = socket.create_connection((host, port), timeout=timeout)
        r.reachable = True
    except Exception as exc:                    # noqa: BLE001
        r.error = str(exc)
        return r                                # unreachable: nothing more to do
    try:
        raw.settimeout(timeout)
        # nosec B323 - detection only: we merely check whether the endpoint speaks
        # TLS and read its banner; nothing sensitive is sent. Whether to trust the
        # certificate for the live connection is decided later, by the user.
        ctx = ssl._create_unverified_context()  # nosec B323
        tls_sock = ctx.wrap_socket(raw, server_hostname=host)
        r.tls = True
        _classify_first_bytes(r, _read_first(tls_sock, timeout))
        try:
            tls_sock.close()
        except Exception:  # nosec B110
            pass
        return r
    except Exception:                           # noqa: BLE001  (not TLS)
        try:
            raw.close()
        except Exception:  # nosec B110
            pass
        # reachable, but not TLS -> classify it as a plaintext endpoint
        return _probe_plaintext(host, port, timeout, r)
