"""Trazado de la ruta de red hacia un host via el binario `traceroute` (Linux).

Funcion IMPURA: ejecuta `traceroute -m <max_hops> -w 2 <host>` como subprocess
y parsea best-effort cada hop: numero de salto, hosts (nombre + IP) y los rtt
detectados. Un hop sin respuesta ("* * *") se representa con `hosts` vacio. No
busca un parseo perfecto: captura el numero de hop y las IPs por regex. Nunca
lanza; devuelve dict de estado con `raw` siempre presente.
"""

import re
import subprocess

# Inicio de una linea de hop: numero de salto al principio.
_HOP_LINE_RE = re.compile(r"^\s*(\d+)\s+(.*)$")
# IPv4 entre parentesis o suelta.
_IP_RE = re.compile(r"\b(\d{1,3}(?:\.\d{1,3}){3})\b")
# "nombre (ip)" o "ip"
_HOST_PAREN_RE = re.compile(r"([\w.\-]+)\s+\((\d{1,3}(?:\.\d{1,3}){3})\)")
# rtt en milisegundos, p.ej "1.234 ms"
_RTT_RE = re.compile(r"([\d.]+)\s*ms")


def _parse_hop(hop_num: int, rest: str) -> dict:
    """Parsea best-effort el cuerpo de una linea de hop."""
    hosts: list[dict] = []

    # Caso sin respuesta: solo asteriscos.
    if rest.replace("*", "").strip() == "":
        return {"hop": hop_num, "hosts": []}

    # rtt globales de la linea (se asocian al/los host(s) detectados).
    rtts = [float(x) for x in _RTT_RE.findall(rest)]

    # Hosts con formato "nombre (ip)".
    paren_matches = _HOST_PAREN_RE.findall(rest)
    seen_ips: set[str] = set()
    for name, ip in paren_matches:
        seen_ips.add(ip)
        hosts.append({"name": name, "ip": ip, "rtt_ms": rtts})

    # IPs sueltas no capturadas por el patron "nombre (ip)".
    for ip in _IP_RE.findall(rest):
        if ip not in seen_ips:
            seen_ips.add(ip)
            hosts.append({"name": "", "ip": ip, "rtt_ms": rtts})

    # Si no detectamos ningun host pero la linea tiene contenido (raro),
    # dejamos un host placeholder con el texto en name para no perder info.
    if not hosts and rest.strip() and rest.strip() != "*":
        hosts.append({"name": rest.strip(), "ip": "", "rtt_ms": rtts})

    return {"hop": hop_num, "hosts": hosts}


def traceroute_host(host: str, max_hops: int = 30, timeout_s: int = 60) -> dict:
    """Traza la ruta de red hacia un host y parsea los hops best-effort.

    Args:
        host: Hostname o IP destino (ej. ``"google.com"`` o ``"1.1.1.1"``).
        max_hops: Maximo numero de saltos a sondear (`traceroute -m`).
        timeout_s: Timeout duro del subprocess en segundos.

    Returns:
        Dict de estado. En exito::

            {
                "status": "ok",
                "host": <host>,
                "hops": [
                    {"hop": 1, "hosts": [{"name": str, "ip": str, "rtt_ms": [float, ...]}]},
                    {"hop": 2, "hosts": []},  # "* * *" sin respuesta
                    ...
                ],
                "raw": <stdout>,
            }

        En fallo (binario ausente, host vacio, timeout duro)::

            {"status": "error", "error": <str>, "host": <host>, "raw": <stdout|"">}
    """
    if not host or not host.strip():
        return {"status": "error", "error": "traceroute_host: host vacio", "host": host, "raw": ""}

    host = host.strip()

    try:
        proc = subprocess.run(
            ["traceroute", "-m", str(max_hops), "-w", "2", host],
            capture_output=True,
            text=True,
            timeout=float(timeout_s),
        )
    except FileNotFoundError:
        return {
            "status": "error",
            "error": "traceroute_host: binario `traceroute` no encontrado en PATH",
            "host": host,
            "raw": "",
        }
    except subprocess.TimeoutExpired as exc:
        partial = exc.stdout or ""
        if isinstance(partial, bytes):
            partial = partial.decode(errors="replace")
        return {
            "status": "error",
            "error": f"traceroute_host: timeout duro del subprocess tras {timeout_s}s",
            "host": host,
            "raw": partial,
        }

    raw = proc.stdout or ""
    hops: list[dict] = []

    for line in raw.splitlines():
        m = _HOP_LINE_RE.match(line)
        if not m:
            continue  # cabecera "traceroute to ..." u otras lineas no-hop
        hop_num = int(m.group(1))
        hops.append(_parse_hop(hop_num, m.group(2)))

    return {
        "status": "ok",
        "host": host,
        "hops": hops,
        "raw": raw,
    }


if __name__ == "__main__":
    try:
        result = traceroute_host("1.1.1.1", max_hops=15, timeout_s=40)
        print(result["status"])
        if result["status"] == "ok":
            print(f"hops detectados: {len(result['hops'])}")
            for hop in result["hops"][:5]:
                ips = [h["ip"] for h in hop["hosts"] if h["ip"]]
                print(f"  hop {hop['hop']}: {ips or '* * *'}")
            print("--- raw ---")
            print(result["raw"])
        else:
            print("error:", result.get("error"))
    except Exception as exc:  # smoke: tolera cualquier fallo de red sin romper
        print("smoke fallo (tolerado):", exc)
