"""Ayudas de la semana 15: levantar la topología de los dos sitios, correr comandos en sus
nodos, generar claves de WireGuard y elegir el firewall del laboratorio.
"""
import os
import subprocess
import time
import unittest

HERE = os.path.dirname(os.path.abspath(__file__))
CAPTURES = os.path.join(HERE, "captures")
KEYS = os.path.join(HERE, "build", "keys")
# La solución no se publica. Sin ella, se usa el firewall que ustedes escriben en lab/.
ANSWERS = os.path.join(HERE, "solution")
if not os.path.isdir(ANSWERS):
    ANSWERS = os.path.join(HERE, "lab")


def answer_path(name):
    """Devuelve la ruta de un archivo del laboratorio, o salta si sigue con el TODO."""
    path = os.path.join(ANSWERS, name)
    with open(path, encoding="utf-8") as file:
        if "TODO" in file.read():
            raise unittest.SkipTest(f"falta completar lab/{name}")
    return path


class Lab:
    """La topología de Containerlab, que se levanta y se destruye desde las pruebas."""

    def __init__(self, folder="sitios", name="w15-sitios", network="redes-w15-sitios"):
        self.folder = os.path.join(HERE, folder)
        self.topology = f"{folder}.clab.yml"
        self.prefix = f"clab-{name}-"
        self.network = network

    def deploy(self):
        done = subprocess.run(
            ["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
            cwd=self.folder, capture_output=True, text=True, timeout=300)
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)

    def destroy(self):
        subprocess.run(["containerlab", "destroy", "-t", self.topology, "--cleanup"],
                       cwd=self.folder, capture_output=True, text=True, timeout=300)
        subprocess.run(["docker", "network", "rm", self.network], capture_output=True, text=True)

    def sh(self, node, *command, timeout=90):
        """Corre un comando en un nodo y devuelve el resultado completo."""
        return subprocess.run(["docker", "exec", self.prefix + node, *command],
                              capture_output=True, text=True, timeout=timeout)

    def out(self, node, *command, timeout=90):
        """Corre un comando en un nodo y devuelve stdout y stderr juntos."""
        done = self.sh(node, *command, timeout=timeout)
        return done.stdout + done.stderr

    def shell(self, node, script, timeout=90):
        """Corre una línea de shell (sh -c) en un nodo."""
        return subprocess.run(["docker", "exec", self.prefix + node, "sh", "-c", script],
                              capture_output=True, text=True, timeout=timeout)

    def daemon(self, node, command):
        """Deja un comando corriendo en segundo plano dentro de un nodo."""
        subprocess.run(["docker", "exec", "-d", self.prefix + node, "sh", "-c", command],
                       capture_output=True, text=True, check=True)

    def load_nft(self, node, path):
        """Carga un archivo de reglas de nftables en un nodo, desde el host."""
        with open(path, encoding="utf-8") as file:
            done = subprocess.run(["docker", "exec", "-i", self.prefix + node, "nft", "-f", "-"],
                                  input=file.read(), capture_output=True, text=True, timeout=30)
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)


def gen_keys():
    """Crea las claves de WireGuard de fw y gw2 en build/keys, con permisos restringidos."""
    os.makedirs(KEYS, exist_ok=True)
    os.chmod(KEYS, 0o700)
    for who in ("fw", "gw2"):
        key = os.path.join(KEYS, f"{who}.key")
        pub = os.path.join(KEYS, f"{who}.pub")
        private = subprocess.run(["wg", "genkey"], capture_output=True, text=True, check=True).stdout.strip()
        public = subprocess.run(["wg", "pubkey"], input=private, capture_output=True, text=True, check=True).stdout.strip()
        old = os.umask(0o077)
        try:
            with open(key, "w", encoding="utf-8") as file:
                file.write(private + "\n")
        finally:
            os.umask(old)
        with open(pub, "w", encoding="utf-8") as file:
            file.write(public + "\n")
    return KEYS


def read_key(name):
    with open(os.path.join(KEYS, name), encoding="utf-8") as file:
        return file.read().strip()


def setup_tunnel(lab):
    """Levanta el túnel WireGuard entre fw y gw2 con las claves de build/keys.

    La tabla de cryptokey routing: cada peer solo acepta las direcciones de la lista allowed-ips.
    En fw, el peer gw2 trae la red de b (10.215.3.0/24); en gw2, el peer fw trae la oficina.
    """
    fw_priv, fw_pub = read_key("fw.key"), read_key("fw.pub")
    gw2_priv, gw2_pub = read_key("gw2.key"), read_key("gw2.pub")
    lab.shell("fw", f"""
        set -e
        printf '%s' '{fw_priv}' > /tmp/wg.key
        ip link add wg0 type wireguard
        wg set wg0 private-key /tmp/wg.key listen-port 51820
        rm -f /tmp/wg.key
        wg set wg0 peer '{gw2_pub}' endpoint 100.64.15.70:51820 \
            allowed-ips 10.215.3.0/24,10.215.9.2/32 persistent-keepalive 25
        ip addr add 10.215.9.1/30 dev wg0
        ip link set wg0 up
        ip route add 10.215.3.0/24 dev wg0
    """)
    lab.shell("gw2", f"""
        set -e
        printf '%s' '{gw2_priv}' > /tmp/wg.key
        ip link add wg0 type wireguard
        wg set wg0 private-key /tmp/wg.key listen-port 51820
        rm -f /tmp/wg.key
        wg set wg0 peer '{fw_pub}' endpoint 100.64.15.66:51820 \
            allowed-ips 10.215.1.0/24,10.215.9.1/32 persistent-keepalive 25
        ip addr add 10.215.9.2/30 dev wg0
        ip link set wg0 up
        ip route add 10.215.1.0/24 dev wg0
    """)
    # Si el firewall está cargado (tabla inet fw), hay que dejar pasar el tráfico de la oficina
    # hacia el túnel y de vuelta; sin firewall el forwarding ya está abierto.
    if lab.shell("fw", "nft list table inet fw").returncode == 0:
        lab.shell("fw", "nft add rule inet fw forward iifname eth1 oifname wg0 accept; "
                        "nft add rule inet fw forward iifname wg0 oifname eth1 accept")
    return fw_pub, gw2_pub


def wait_until(predicate, seconds=30, step=0.25):
    """Espera a que una condición se cumpla; devuelve si se cumplió."""
    deadline = time.monotonic() + seconds
    while time.monotonic() < deadline:
        if predicate():
            return True
        time.sleep(step)
    return predicate()
