"""
Plages de ports que Windows se réserve au démarrage, et détection d'un conflit.

**Pourquoi ce module existe.** Windows réserve des plages de ports par blocs de
100 (Hyper-V, WinNAT, conteneurs), allouées au démarrage. Un `bind` dedans est
refusé avec `WSAEACCES` **alors que personne ne détient le port** — et le code
d'erreur évoque un défaut de privilèges, ce qui envoie chercher très loin de la
vraie cause. Mesuré le 2026-08-03 sur la machine multi-domiciliée du parc
(point 73) : l'écho de vivacité ne se liait pas au port 50820, qui tombait dans
`50814-50913`, et le diagnostic a coûté une session entière — pare-feu,
antivirus, règles `Block` applicatives, tout y est passé avant qu'un `bind`
manuel donne la réponse en une commande.

**Ce que ce module ajoute par rapport à ce qui existait.** Cette connaissance
était déjà dans `tunnel_probe.expliquer_echec_bind`, mais **réactive** : elle ne
sert qu'une fois le bind échoué, ce qui suppose que c'est NOUS qui lions le
port. Or le port WireGuard est lié par WireGuard, pas par nous : sous Windows le
service se crée puis meurt au démarrage (point 32), le symptôme est un tunnel
qui ne monte pas, et rien ne pointerait vers une plage réservée. La même machine
avait `51570-51669` exclue, **à 150 ports de 51820** — le jour où le tirage
tombe sur le port du tunnel, Meshamoto est inutilisable et personne ne devine
pourquoi.

**Constater et dire, jamais toucher** — règle du projet, déjà appliquée à `ufw`
(point 71) et aux règles `Block` de Windows Firewall (point 72). Les exclusions
sont un réglage système ; on nomme le conflit et on rappelle que le port est
configurable, on ne réserve ni ne libère rien.

**Analyse de la sortie de `netsh`, et pourquoi c'est acceptable ici.** La règle
du projet est de ne jamais analyser ce qu'un outil Windows AFFICHE, mais le
format qu'il produit pour être relu (points 32, 34, 45, 72). `netsh` n'a aucune
sortie structurée pour ces plages, et il n'existe pas d'équivalent PowerShell.
On s'ancre donc sur les seules **lignes de deux entiers**, qui ne sont pas
traduites, en ignorant complètement les en-têtes et le pied de page, qui le
sont — exactement le raisonnement du point 34, où le round-trip du `ping` est lu
sur l'unité `ms` et non sur le libellé qui la précède.

L'astérisque qui marque les exclusions « administrées » n'est délibérément pas
retenu : il distingue une plage fixe d'une plage tirée au démarrage, ce qui
compte pour comprendre le phénomène mais pas pour dire à quelqu'un que SON port
est pris. L'utilisateur le verra en lançant la commande que le message lui donne.
"""

from __future__ import annotations

import logging
import re
import subprocess

import config

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

COMMANDE = ("netsh", "int", "ipv4", "show", "excludedportrange", "protocol=udp")

# Deux entiers seuls sur leur ligne, éventuellement suivis de l'astérisque des
# exclusions administrées. Tout le reste — titre, séparateur, note de bas de
# page — est traduit d'un Windows à l'autre, donc jamais interprété.
_LIGNE_DE_PLAGE = re.compile(r"^\s*(\d+)\s+(\d+)\s*\*?\s*$")

_DELAI_SECONDES = 5.0


def analyser_plages(sortie: str) -> list[tuple[int, int]]:
    """Plages `(debut, fin)` lues dans la sortie de `netsh`. Logique pure."""
    plages: list[tuple[int, int]] = []
    for ligne in sortie.splitlines():
        correspondance = _LIGNE_DE_PLAGE.match(ligne)
        if correspondance is None:
            continue
        debut, fin = int(correspondance.group(1)), int(correspondance.group(2))
        if debut <= fin:
            plages.append((debut, fin))
    return plages


def plage_contenant(port: int, plages: list[tuple[int, int]]) -> tuple[int, int] | None:
    """La plage qui contient ce port, ou None. Logique pure."""
    for debut, fin in plages:
        if debut <= port <= fin:
            return (debut, fin)
    return None


def lire_plages_udp(os_family: str | None = None) -> list[tuple[int, int]]:
    """
    Interroge Windows. Renvoie une liste VIDE ailleurs, et sur toute erreur.

    Ne jamais faire échouer l'appelant : c'est un diagnostic de confort, et une
    machine où `netsh` manque ou refuse de répondre doit démarrer normalement.
    """
    if (os_family or config.OS_FAMILY) != "windows":
        return []
    try:
        resultat = subprocess.run(
            COMMANDE, capture_output=True, text=True, timeout=_DELAI_SECONDES
        )
    except (OSError, subprocess.SubprocessError):
        logger.debug("Lecture des plages de ports réservées impossible", exc_info=True)
        return []
    return analyser_plages(resultat.stdout)


def avertissement_si_port_reserve(
    port: int, nom_du_port: str, variable: str, os_family: str | None = None
) -> str | None:
    """
    Message à journaliser si `port` tombe dans une plage réservée, sinon None.

    `nom_du_port` et `variable` rendent la fonction réutilisable : le port
    d'écho a le même problème et le même remède, à la variable d'environnement
    près.
    """
    plage = plage_contenant(port, lire_plages_udp(os_family))
    if plage is None:
        return None
    return (
        f"{nom_du_port} ({port}) tombe dans une plage de ports RÉSERVÉE par "
        f"Windows ({plage[0]}-{plage[1]}) : rien ne pourra s'y lier, alors que "
        f"personne ne détient ce port. Ces plages sont allouées au démarrage "
        f"(Hyper-V, WinNAT), donc elles changent d'un redémarrage à l'autre. "
        f"Pour le vérifier : {' '.join(COMMANDE)} — puis choisir un autre port "
        f"avec {variable} ou depuis l'interface."
    )
