"""
Point d'entrée unique utilisé par network_client.py — masque la différence entre
Windows (wireguard.exe) et Linux (wg-quick) derrière une API commune :

    load_own_keypair() -> KeyPair
    PeerConfig(...)
    restart_tunnel_with_peers(private_key, local_virtual_ip, peers)
    uninstall_tunnel()

Le reste du projet n'a jamais besoin de savoir sur quel OS il tourne.
"""

from __future__ import annotations

import logging
import sys

import config
from wireguard_backend_common import (
    address_line,
    DEFAULT_ADDRESS_PREFIX_LEN,
    KeyPair,
    PeerConfig,
    TunnelSpec,
    load_or_create_keypair,
    render_conf,
)

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

# Sélection du backend au chargement du module, une seule fois.
if sys.platform == "win32":
    import wireguard_backend_windows as _backend
elif sys.platform.startswith("linux"):
    import wireguard_backend_linux as _backend
else:
    raise RuntimeError(
        f"Plateforme non supportée : {sys.platform}. "
        "Seuls Windows et Linux sont pris en charge par ce client."
    )

__all__ = [
    "KeyPair", "PeerConfig", "load_own_keypair", "restart_tunnel_with_peers",
    "uninstall_tunnel", "get_latest_handshakes", "is_tunnel_active",
]


def is_tunnel_active() -> bool:
    """Vrai si l'interface du tunnel existe déjà. Ne nécessite aucun privilège
    (`ip link show` / `sc query`) — voir les backends."""
    return _backend.is_tunnel_active()


def load_own_keypair() -> KeyPair:
    """Charge notre identité WireGuard persistée, ou en crée une nouvelle au premier lancement."""
    config.ensure_directories()
    return load_or_create_keypair(config.IDENTITY_KEY_PATH)


def write_conf(content: str) -> None:
    config.ensure_directories()
    config.WIREGUARD_CONF_PATH.write_text(content, encoding="utf-8", newline="\n")
    logger.info("Config WireGuard écrite dans %s", config.WIREGUARD_CONF_PATH)


def uninstall_tunnel() -> None:
    """
    Désactive le tunnel s'il est actif. La vérification préalable ne nécessite aucun
    privilège (voir `is_tunnel_active` dans les backends : `ip link show` / `sc query`)
    alors que la désinstallation en demande — sans elle, fermer l'appli après un échec
    de jointure (tunnel jamais monté) réclamait un mot de passe pour rien.
    """
    if not _backend.is_tunnel_active():
        logger.info("Aucun tunnel actif : rien à désinstaller.")
        return
    _backend.uninstall_tunnel()


def get_latest_handshakes() -> dict[str, float | None]:
    """Clé publique -> secondes depuis le dernier handshake WireGuard (None si jamais).
    Dict vide si non disponible sur cet OS (voir le backend Windows)."""
    return _backend.get_latest_handshakes()


def _can_sync(installed: str | None, nouvelle: str) -> bool:
    """
    Vrai si le passage d'une configuration à l'autre peut se faire à chaud.

    Le critère est l'**adresse de l'interface** : `wg syncconf` ne touche ni à
    l'adresse ni aux routes, seulement aux pairs, à la clé privée et au port
    d'écoute. Un changement d'IP virtuelle ou de préfixe exige donc une vraie
    réinstallation — s'en passer laisserait l'interface avec une adresse
    obsolète et un tunnel silencieusement inopérant, la pire des pannes.

    Sans configuration installée, il n'y a rien à synchroniser : c'est un premier
    montage.
    """
    if not installed:
        return False
    return address_line(installed) == address_line(nouvelle) and address_line(nouvelle) != ""


def _installed_conf() -> str | None:
    """Contenu du .conf actuellement sur disque, ou None s'il n'existe pas encore."""
    try:
        return config.WIREGUARD_CONF_PATH.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError):
        return None


def restart_tunnel_with_peers(
    private_key: str,
    local_virtual_ip: str,
    peers: list[PeerConfig],
    address_prefix_len: int = DEFAULT_ADDRESS_PREFIX_LEN,
) -> None:
    """
    Point d'entrée principal appelé à chaque mise à jour de la liste de pairs :
    réécrit le .conf puis réinstalle/réactive le tunnel (coupure brève).

    Ne fait RIEN si la configuration rendue est identique à celle déjà installée et
    que le tunnel tourne. C'est la seule comparaison qui fasse foi : le serveur diffuse
    par pair neuf champs (`display_name`, `hosted_service_label`, `relay_port`...) dont
    la plupart n'influencent pas le tunnel, et `NetworkClient` comparait les
    dictionnaires bruts — n'importe lequel de ces champs suffisait donc à provoquer un
    `down`+`up` complet, soit deux invites de mot de passe sur Linux et une coupure,
    pour une configuration WireGuard rigoureusement identique.
    """
    content = render_conf(
        private_key, local_virtual_ip, config.WIREGUARD_LISTEN_PORT, peers, address_prefix_len
    )
    installed = _installed_conf()
    if content == installed and _backend.is_tunnel_active():
        logger.info(
            "Configuration du tunnel inchangée (%d pair(s)) : réinstallation évitée.", len(peers)
        )
        return

    spec = TunnelSpec(
        private_key=private_key,
        local_virtual_ip=local_virtual_ip,
        address_prefix_len=address_prefix_len,
        listen_port=config.WIREGUARD_LISTEN_PORT,
        peers=peers,
    )

    # Mise à jour À CHAUD quand seuls les pairs ont changé : plus de coupure pour
    # personne. C'était le premier goulot de montée en charge du projet — chaque
    # arrivée ou départ démontait puis remontait l'interface de TOUS les pairs
    # (voir README, « Montée en charge »).
    if _can_sync(installed, content) and _backend.is_tunnel_active():
        if _backend.sync_tunnel(spec):
            write_conf(content)
            logger.info(
                "Tunnel mis à jour à chaud : %d pair(s), sans démonter l'interface.",
                len(peers),
            )
            return
        logger.info("Mise à jour à chaud indisponible : réinstallation complète.")

    write_conf(content)
    uninstall_tunnel()  # no-op (et sans invite de mot de passe) au premier montage
    # Le .conf reste écrit pour le chemin historique (pkexec wg-quick) et pour la
    # comparaison qui évite les réinstallations inutiles ; le composant privilégié
    # Linux, lui, ne le lit pas et refabrique le sien à partir de ces paramètres.
    _backend.install_tunnel(spec)
    logger.info("Tunnel redémarré avec %d pair(s)", len(peers))
