from __future__ import annotations

import ipaddress
import socket
from pathlib import Path
from queue import Queue
from time import sleep

from agent_guard.events import Event


def _hex_ip(value: str, ipv6: bool) -> str:
    raw = bytes.fromhex(value)
    if ipv6:
        chunks = [raw[i:i + 4][::-1] for i in range(0, 16, 4)]
        return str(ipaddress.IPv6Address(b"".join(chunks)))
    return socket.inet_ntoa(raw[::-1])


def read_listeners(proc_root: Path = Path("/proc")) -> set[tuple[str, str, int]]:
    out: set[tuple[str, str, int]] = set()
    for name, ipv6 in (("tcp", False), ("tcp6", True)):
        path = proc_root / "net" / name
        try:
            lines = path.read_text().splitlines()[1:]
        except OSError:
            continue
        for line in lines:
            parts = line.split()
            if len(parts) < 4 or parts[3] != "0A":
                continue
            addr, port = parts[1].split(":")
            out.add((name, _hex_ip(addr, ipv6), int(port, 16)))
    return out


def _allowed(port: int, rules: list[str]) -> bool:
    for rule in rules:
        if "-" in rule:
            lo, hi = rule.split("-", 1)
            if int(lo) <= port <= int(hi):
                return True
        elif int(rule) == port:
            return True
    return False


def _is_loopback(host: str) -> bool:
    try:
        return ipaddress.ip_address(host).is_loopback
    except ValueError:
        return False


def parse_host_cidrs(rules: list[str]) -> list[ipaddress.IPv4Network | ipaddress.IPv6Network]:
    return [ipaddress.ip_network(rule, strict=False) for rule in rules]


def _is_trusted_host(
    host: str,
    cidrs: list[ipaddress.IPv4Network | ipaddress.IPv6Network],
) -> bool:
    try:
        addr = ipaddress.ip_address(host)
    except ValueError:
        return False
    return any(addr in net for net in cidrs)


def diff_listeners(
    baseline: set[tuple[str, str, int]],
    current: set[tuple[str, str, int]],
    allowlist: list[str],
    host_cidrs: list[ipaddress.IPv4Network | ipaddress.IPv6Network] | None = None,
) -> list[Event]:
    # A new listener bound to loopback is benign dev activity (servers, LSPs, AI
    # agents) -> silent. A new NON-loopback bind (0.0.0.0/::/public) is the real
    # security signal -> notify. Baseline still tracks loopback so it never churns.
    trusted = host_cidrs or []
    events = []
    for proto, host, port in sorted(current - baseline):
        if _is_loopback(host) or _allowed(port, allowlist) or _is_trusted_host(host, trusted):
            continue
        events.append(Event("warning", "ports", f"New non-loopback listener {proto} {host}:{port}", actions=["dismiss"]))
    return events


def tick(queue: Queue, state, cfg, stop) -> None:
    key = "ports"
    baseline = {tuple(item) for item in state.baseline(key, [])}
    if not baseline:
        baseline = read_listeners()
        state.set_baseline(key, [list(item) for item in baseline])
    while not stop.is_set():
        current = read_listeners()
        for event in diff_listeners(
            baseline,
            current,
            cfg.dev_port_allowlist,
            parse_host_cidrs(cfg.trusted_host_cidrs),
        ):
            queue.put(event)
        baseline = current
        state.set_baseline(key, [list(item) for item in baseline])
        sleep(60)
