"""
Mode isolation — Linux : restreint les connexions ENTRANTES non sollicitées au seul
réseau virtuel Meshamoto, via une table nftables dédiée.

Principe : nftables évalue toutes les chaînes de base accrochées à un même hook
(ex: "input"), dans l'ordre de leur priorité — un verdict DROP dans N'IMPORTE LAQUELLE
d'entre elles termine immédiatement le traitement du paquet, quelles que soient les
règles ACCEPT d'autres tables (ufw, firewalld...). On crée donc notre propre table
isolée qui n'a besoin de connaître ni modifier les règles existantes de l'utilisateur :

    table inet meshamoto {
        chain input {
            type filter hook input priority filter; policy accept;
            udp dport 51820 accept                 # tunnel WireGuard : voir _build_ruleset
            ip saddr <pair_restreint> tcp dport {<ports>} accept   # ACL : voir ci-dessous
            ip saddr <pair_restreint> udp dport {<ports>} accept
            ip saddr <pair_restreint> drop
            ip saddr <virtual_cidr> accept        # reste du réseau virtuel : accès complet
            ct state established,related accept   # réponses à NOS connexions sortantes : toujours OK
            iif lo accept                          # loopback : toujours OK
            drop                                    # tout le reste entrant : bloqué
        }
    }

IMPORTANT : comme pour Windows, le trafic SORTANT n'est jamais touché — seules les
nouvelles connexions entrantes non sollicitées sont concernées (ligne `ct state
established,related accept` : les réponses à ta propre navigation web restent autorisées).

ACLs par pair (optionnel, voir acl_rules.py) : un pair listé dans `peer_acls` est
restreint aux ports donnés au lieu de tout le réseau virtuel. Les règles nftables
étant évaluées séquentiellement avec verdict terminal (accept/drop arrête la chaîne
pour ce paquet), les règles ACL doivent précéder la règle générale `ip saddr
<virtual_cidr> accept` — sinon cette dernière autoriserait le pair sur tous les ports
avant que sa restriction n'ait la moindre chance de s'appliquer.

Nécessite des privilèges root (comme wg-quick), via linux_privilege.run_privileged.
"""

from __future__ import annotations

import logging
import shutil
import tempfile
from pathlib import Path

import config
import privileged_helper
from linux_privilege import run_privileged

logger = logging.getLogger("meshamoto-client.firewall.linux")

_TABLE_NAME = "meshamoto"


def _build_ruleset(virtual_cidr: str, peer_acls: dict[str, list[int]] | None = None) -> str:
    """
    Construit le ruleset nftables complet (logique pure, testable sans privilèges).

    Le ruleset commence par l'idiome standard de remplacement atomique de nftables :
    `add table` (no-op si la table existe déjà) puis `delete table`, ce qui garantit
    de repartir d'une table vide sans avoir à savoir si une version précédente
    traînait. Tout tient donc dans UN seul appel `nft -f`, donc **une seule invite
    pkexec** — auparavant `enable_isolation` appelait `disable_isolation()` d'abord,
    ce qui demandait systématiquement deux mots de passe à l'utilisateur (dont un
    pour supprimer une table souvent inexistante). Même raisonnement que le
    `wg-quick down` inutile évité par `is_tunnel_active()` côté WireGuard.
    """
    lines = [
        f"add table inet {_TABLE_NAME}",
        f"delete table inet {_TABLE_NAME}",
        f"table inet {_TABLE_NAME} {{",
        "    chain input {",
        "        type filter hook input priority filter; policy accept;",
        # Le tunnel lui-même, AVANT tout le reste : les paquets WireGuard entrants
        # portent l'adresse PHYSIQUE du pair (LAN ou publique), jamais son adresse
        # virtuelle — la règle `ip saddr <virtual_cidr> accept` plus bas ne les couvre
        # donc jamais. Sans cette ligne, un handshake entrant sur un flux que conntrack
        # ne connaît pas encore (pair qui redémarre, entrée conntrack expirée) est
        # jeté par le `drop` final : le mode isolation casse le tunnel qu'il est censé
        # protéger. Accepter ce port de tout le monde est sans risque : WireGuard
        # authentifie cryptographiquement chaque paquet et ignore silencieusement ceux
        # qui ne correspondent à aucun pair connu (pas de réponse, port indétectable).
        f"        udp dport {config.WIREGUARD_LISTEN_PORT} accept",
    ]

    for peer_ip, ports in (peer_acls or {}).items():
        if not ports:
            continue
        port_list = ", ".join(str(p) for p in ports)
        lines.append(f"        ip saddr {peer_ip} tcp dport {{ {port_list} }} accept")
        lines.append(f"        ip saddr {peer_ip} udp dport {{ {port_list} }} accept")
        lines.append(f"        ip saddr {peer_ip} drop")

    lines.append(f"        ip saddr {virtual_cidr} accept")
    lines.append("        ct state established,related accept")
    lines.append("        iif lo accept")
    lines.append("        drop")
    lines.append("    }")
    lines.append("}")
    return "\n".join(lines)


def _find_nft() -> str:
    path = shutil.which("nft")
    if path is None:
        raise FileNotFoundError(
            "nft (nftables) introuvable. Installez-le, par exemple sur CachyOS/Arch : "
            "sudo pacman -S nftables"
        )
    return path


def enable_isolation(virtual_cidr: str, peer_acls: dict[str, list[int]] | None = None) -> None:
    """Crée la table nftables dédiée qui isole les connexions entrantes au réseau
    virtuel, avec restriction par port pour les pairs listés dans `peer_acls`."""
    if privileged_helper.helper_available():
        args = [privileged_helper.HELPER_PATH, "set-firewall",
                "--virtual-cidr", virtual_cidr,
                "--listen-port", str(config.WIREGUARD_LISTEN_PORT)]
        for peer_ip, ports in (peer_acls or {}).items():
            if ports:
                args += ["--acl", f"{peer_ip}={','.join(str(p) for p in ports)}"]
        result = run_privileged(*args)
        if result.returncode == 0:
            logger.info("Mode isolation activé (réseau virtuel autorisé : %s)", virtual_cidr)
            return
        logger.warning("Composant privilégié en échec, repli sur nft : %s", result.stderr.strip())

    nft = _find_nft()

    # On écrit la règle dans un fichier temporaire plutôt que de l'inliner en argument
    # de commande : plus lisible dans les logs et évite les soucis d'échappement de shell.
    # Le ruleset est idempotent par construction (voir _build_ruleset) : pas d'appel
    # séparé à disable_isolation(), qui coûterait une invite pkexec supplémentaire.
    ruleset = _build_ruleset(virtual_cidr, peer_acls)

    with tempfile.NamedTemporaryFile("w", suffix=".nft", delete=False) as f:
        f.write(ruleset)
        temp_path = f.name

    try:
        result = run_privileged(nft, "-f", temp_path)
        if result.returncode != 0:
            raise RuntimeError(
                "Impossible de charger les règles nftables du mode isolation. "
                "Vérifiez les privilèges root. Détail : " + result.stderr
            )
        logger.info("Mode isolation activé (réseau virtuel autorisé : %s)", virtual_cidr)
    finally:
        Path(temp_path).unlink(missing_ok=True)


def disable_isolation(peer_ips: list[str] | None = None) -> None:
    """Supprime la table nftables dédiée. Ne touche à aucune autre règle existante
    (ufw, firewalld...). `peer_ips` est ignoré ici : une table nftables se supprime
    intégralement en une seule commande, contrairement aux règles nommées
    individuellement de Windows (voir firewall_backend_windows.py)."""
    if privileged_helper.helper_available():
        result = run_privileged(privileged_helper.HELPER_PATH, "clear-firewall")
        if result.returncode == 0:
            logger.info("Mode isolation désactivé")
            return
        logger.warning("Composant privilégié en échec, repli sur nft : %s", result.stderr.strip())

    nft = _find_nft()
    result = run_privileged(nft, "delete", "table", "inet", _TABLE_NAME)
    if result.returncode != 0 and "No such file or directory" not in result.stderr:
        logger.warning("Suppression de la table nftables : %s (probablement déjà absente)", result.stderr)
    else:
        logger.info("Mode isolation désactivé")


# ---------------------------------------------------------------------------
# Autorisation de notre propre exécutable — sans objet sur Linux
# ---------------------------------------------------------------------------


def autoriser_application(virtual_cidr: str) -> bool:
    """
    Sans objet sur Linux, et ce n'est pas une lacune : la contrepartie du
    problème Windows y est déjà traitée ailleurs, autrement, pour une raison de
    fond.

    Le pare-feu Linux ne raisonne pas par APPLICATION et n'affiche aucune alerte
    au premier bind : il n'existe donc pas de règle `Block` fabriquée dans notre
    dos, qui est exactement ce que la version Windows va désamorcer (voir le
    commentaire de section de firewall_backend_windows.py). Notre propre table
    nftables autorise déjà toute la plage virtuelle quand le mode isolation est
    actif.

    Reste `ufw`, qui peut jeter l'écho de vivacité. C'est un système SÉPARÉ du
    nôtre : la règle du projet, posée après les trois heures de diagnostic du
    point 36, est de le CONSTATER et de le dire — jamais d'y toucher. C'est fait
    dans `tunnel_probe.ufw_actif` / `commande_ufw_a_suggerer`, et c'est
    l'équivalent linuxien de `blocages_applicatifs` ci-dessous : dans les deux
    cas on nomme la cause et on laisse l'utilisateur décider.

    Renvoie False — aucune règle posée, aucune ne l'était attendue.
    """
    return False


def blocages_applicatifs() -> list[dict]:
    """Aucun équivalent Linux — voir `autoriser_application`."""
    return []


def commande_retrait_a_suggerer(blocages: list[dict]) -> str:
    """Aucun équivalent Linux — voir `autoriser_application`."""
    return ""
