"""
Lightweight TCP probing utility for the Port Agent.

This module performs a simple, controlled TCP connect_ex() test against a
local port and classifies the result into categories such as OPEN_TCP,
CLOSED_RST, or FILTERED_TIMEOUT. It is intentionally minimal: no banner
grabbing, no payloads, and no remote scanning. All targets must resolve to a
loopback address via `resolve_local_address()`, ensuring the agent cannot
probe external hosts.

The probe measures connection latency, captures the raw errno, and maps it to
a human‑readable classification using a small errno taxonomy. This provides
the agent with clear signals about whether a port is open, closed, filtered,
or behaving unexpectedly.

The helper `tcp_probes()` runs the same logic across a list of ports, making
it easy for the agent to sweep local services and reason about patterns of
exposure or misconfiguration.
"""

import socket
import time
from dataclasses import dataclass
from typing import Optional
from .utility import resolve_local_address


@dataclass
class TcpProbeResult:
    errno: int
    classification: str
    duration_ms: float
    resolved_address: Optional[str]


def _classify_errno(err: int) -> str:
    if err == 0:
        return "OPEN_TCP"
    if err in (111, 61, 10061):
        return "CLOSED_RST"
    if err in (110, 60, 10060):
        return "FILTERED_TIMEOUT"
    if err in (115, 36, 10035):
        return "IN_PROGRESS"
    return "FILTERED_TIMEOUT"



def tcp_probe(local_address_mode: str, port: int, timeout: float = 3.0) -> TcpProbeResult:
    """
    Perform a TCP connect_ex() probe, but only if host resolves to loopback.
    Rejects any external host.
    """

    # --- Local-host enforcement gate ---
    local_address = resolve_local_address(local_address_mode)
    if not local_address:
        return TcpProbeResult(
            errno=-1,
            classification="INVALID_HOST_NON_LOCAL",
            duration_ms=0.0,
            resolved_address=None,
        )

    # Resolve address (now guaranteed to be loopback)
    try:
        resolved = socket.getaddrinfo(local_address, port, socket.AF_UNSPEC, socket.SOCK_STREAM)
        family, socktype, proto, canonname, sockaddr = resolved[0]
        resolved_addr = str(sockaddr[0])
    except Exception:
        family = socket.AF_INET
        resolved_addr = None

    s = socket.socket(family, socket.SOCK_STREAM)
    s.settimeout(timeout)

    start = time.perf_counter()
    try:
        err = s.connect_ex((local_address, port))
    except Exception:
        err = 110  # ETIMEDOUT
    end = time.perf_counter()

    duration_ms = (end - start) * 1000.0
    classification = _classify_errno(err)

    s.close()

    return TcpProbeResult(
        errno=err,
        classification=classification,
        duration_ms=duration_ms,
        resolved_address=resolved_addr,
    )


def tcp_probes(local_address_mode: str, ports: list[int], timeout: float = 3.0) -> list[TcpProbeResult]:
    """
    Perform TCP probes on a list of ports.
    Returns a list of TcpProbeResult.
    """
    return [tcp_probe(local_address_mode, port, timeout) for port in ports]

if __name__ == "__main__":
    # Example: external host should fail immediately
    localhost = "hostname_localhost"
    # localhost = "ipv4_loopback"
    port = 9999

    print(f'Running tcp_probe("{localhost}", {port})...')
    result = tcp_probe(localhost, port)

    print("\n=== TCP Probe Result ===")
    print(f"Resolved Address : {result.resolved_address}")
    print(f"Errno            : {result.errno}")
    print(f"Classification   : {result.classification}")
    print(f"Duration (ms)    : {result.duration_ms:.2f}")
