"""
Backend WireGuard pour Linux : pilote `wg-quick`, l'outil standard fourni par le
paquet `wireguard-tools` (déjà présent par défaut sur la plupart des noyaux récents,
y compris CachyOS où le module wireguard est intégré au noyau Linux).

Installation du paquet si absent :
    CachyOS / Arch  : sudo pacman -S wireguard-tools
    Debian / Ubuntu : sudo apt install wireguard-tools
    Fedora          : sudo dnf install wireguard-tools

IMPORTANT — privilèges : la création d'une interface réseau (via `ip link`, appelé en
interne par wg-quick) nécessite la capability CAP_NET_ADMIN, obtenue en lançant l'app
en root. Deux options :
  1. Lancer tout le client avec `sudo python3 gui.py` (simple, mais toute la GUI tourne
     en root — à réserver à un usage personnel/confiance totale sur la machine).
  2. Utiliser `pkexec` (prompt graphique de mot de passe), utilisé automatiquement ici
     si disponible et que le processus ne tourne pas déjà en root — évite de lancer
     toute la GUI en root, seule la commande wg-quick l'est.
"""

from __future__ import annotations

import logging
import os
import shutil
import subprocess
import time

import config
import privileged_helper
from linux_privilege import run_privileged
from wireguard_backend_common import (
    TunnelSpec,
    parse_latest_handshakes,
    render_sync_conf,
)

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



def _helper_args(spec: TunnelSpec) -> list[str]:
    """
    Traduit la description du tunnel en arguments pour le helper.

    La clé privée n'y figure PAS : elle passe par l'entrée standard, `/proc/<pid>/cmdline`
    étant lisible par tout utilisateur de la machine.
    """
    args = [
        privileged_helper.HELPER_PATH, "set-tunnel",
        "--address", f"{spec.local_virtual_ip}/{spec.address_prefix_len}",
        "--listen-port", str(spec.listen_port),
    ]
    for peer in spec.peers:
        descriptor = f"{peer.public_key},{peer.allowed_ip}"
        if peer.endpoint:
            descriptor += f",{peer.endpoint}"
        args += ["--peer", descriptor]
    return args


def _helper_sync_args(spec: TunnelSpec) -> list[str]:
    """Comme `_helper_args`, mais sans l'adresse : `wg syncconf` ne touche ni à
    l'adresse ni aux routes, seulement aux pairs, à la clé et au port."""
    args = [
        privileged_helper.HELPER_PATH, "sync-tunnel",
        "--listen-port", str(spec.listen_port),
    ]
    for peer in spec.peers:
        descriptor = f"{peer.public_key},{peer.allowed_ip}"
        if peer.endpoint:
            descriptor += f",{peer.endpoint}"
        args += ["--peer", descriptor]
    return args


def sync_tunnel(spec: TunnelSpec) -> bool:
    """
    Met à jour les pairs À CHAUD, sans démonter l'interface. Renvoie False si ce
    n'est pas possible — l'appelant retombe alors sur une réinstallation.

    C'est ce qui supprime la coupure de plusieurs centaines de millisecondes que
    TOUS les pairs subissaient à chaque arrivée ou départ, et avec elle le
    plafond de montée en charge du projet (voir README, « Montée en charge » :
    la réinstallation était « le vrai goulot »).

    Deux chemins, comme pour `install_tunnel` : le composant privilégié d'abord
    (aucune invite), puis `pkexec wg syncconf`.

    **Le repli écrit un fichier lu par root, et c'est acceptable ICI** alors que
    ça ne l'est pas pour le `.conf` complet (point 38) : le format de `wg
    syncconf` n'accepte aucune directive exécutable — `PostUp` & co appartiennent
    à `wg-quick`, et `wg` refuse les clés qu'il ne connaît pas. Le pire qu'on
    puisse y injecter concerne notre propre interface, ce qui n'est pas une
    élévation de privilège.
    """
    if privileged_helper.helper_available():
        result = run_privileged(*_helper_sync_args(spec), stdin_data=spec.private_key)
        if result.returncode == 0:
            return True
        logger.info(
            "Mise à jour à chaud refusée par le composant privilégié (code %d) : %s",
            result.returncode, result.stderr.strip(),
        )
        return False

    wg = shutil.which("wg")
    if wg is None:
        return False
    config.ensure_directories()
    chemin = config.WIREGUARD_CONF_PATH.with_name(f"{config.TUNNEL_NAME}-sync.conf")
    try:
        chemin.write_text(
            render_sync_conf(spec.private_key, spec.listen_port, spec.peers),
            encoding="utf-8", newline="\n",
        )
        chemin.chmod(0o600)
        result = run_privileged(wg, "syncconf", config.TUNNEL_NAME, str(chemin))
    except OSError as exc:
        logger.info("Mise à jour à chaud impossible : %s", exc)
        return False
    finally:
        chemin.unlink(missing_ok=True)
    if result.returncode != 0:
        logger.info("`wg syncconf` a échoué : %s", result.stderr.strip())
        return False
    return True


def is_tunnel_active() -> bool:
    """
    Vrai si l'interface WireGuard existe déjà. Ne nécessite aucun privilège
    (`ip link show` est une simple lecture) — sert à éviter un appel `wg-quick down`
    inutile (donc un prompt pkexec inutile) quand il n'y a rien à désinstaller.
    """
    ip = shutil.which("ip")
    if ip is None:
        return True  # ne peut pas vérifier : on suppose actif, comportement historique
    result = subprocess.run(
        [ip, "link", "show", config.TUNNEL_NAME], capture_output=True, text=True, timeout=5,
    )
    return result.returncode == 0


def _find_wg_quick() -> str:
    path = shutil.which("wg-quick")
    if path is None:
        raise FileNotFoundError(
            "wg-quick introuvable. Installez le paquet wireguard-tools, par exemple sur "
            "CachyOS/Arch : sudo pacman -S wireguard-tools"
        )
    return path


def install_tunnel(spec: TunnelSpec | None = None) -> None:
    """
    Active l'interface WireGuard.

    Deux chemins possibles, dans cet ordre :
    1. le composant privilégié, s'il est installé et que `spec` est fourni — aucune
       invite de mot de passe (règle polkit `allow_active`) ;
    2. sinon `pkexec wg-quick up <conf>`, le comportement historique, avec une invite.

    Le repli n'est pas un cas d'erreur : c'est le fonctionnement normal d'une AppImage
    ou d'un lancement depuis les sources, où aucun composant système n'est installé.
    """
    if spec is not None and privileged_helper.helper_available():
        result = run_privileged(*_helper_args(spec), stdin_data=spec.private_key)
        if result.returncode == 0:
            return
        # Un helper installé mais défaillant ne doit pas laisser l'utilisateur sans
        # tunnel : on retente par la voie historique, quitte à demander un mot de passe.
        logger.warning(
            "Le composant privilégié a échoué (code %d), repli sur wg-quick : %s",
            result.returncode, result.stderr.strip(),
        )

    wg_quick = _find_wg_quick()
    result = run_privileged(wg_quick, "up", str(config.WIREGUARD_CONF_PATH))
    if result.returncode != 0:
        raise RuntimeError(
            "Impossible d'activer le tunnel WireGuard (wg-quick up a échoué). "
            "Vérifiez les privilèges et que le module noyau 'wireguard' est chargé "
            "('sudo modprobe wireguard' si besoin). Détail : " + result.stderr
        )


def uninstall_tunnel() -> None:
    """Désactive l'interface via `wg-quick down <nom-ou-chemin>`. Tolère l'absence d'interface active."""
    if privileged_helper.helper_available():
        result = run_privileged(privileged_helper.HELPER_PATH, "stop-tunnel")
        if result.returncode == 0:
            return
        logger.warning(
            "Le composant privilégié a échoué au démontage (code %d), repli sur wg-quick : %s",
            result.returncode, result.stderr.strip(),
        )

    wg_quick = _find_wg_quick()
    result = run_privileged(wg_quick, "down", str(config.WIREGUARD_CONF_PATH))
    if result.returncode != 0 and "is not a WireGuard interface" not in result.stderr:
        logger.warning("wg-quick down a renvoyé une erreur (probablement sans conséquence) : %s", result.stderr)


def get_latest_handshakes() -> dict[str, float | None]:
    """
    Retourne, pour chaque pair (clé publique), le nombre de secondes écoulées depuis
    son dernier handshake WireGuard réussi (None si jamais négocié). Utilisé par le
    panneau de diagnostic pour distinguer un tunnel réellement actif d'un tunnel
    simplement configuré mais qui n'a jamais établi de session avec ce pair. `wg` fait
    partie de `wireguard-tools`, déjà un prérequis de ce backend (voir `wg-quick`
    ci-dessus) — n'introduit aucune nouvelle dépendance.
    """
    wg = shutil.which("wg")
    if wg is None:
        return {}
    result = run_privileged(wg, "show", config.TUNNEL_NAME, "latest-handshakes")
    if result.returncode != 0:
        return {}

    return parse_latest_handshakes(result.stdout, time.time())
