"""
Mesure de latence approximative vers un pair, pour l'affichage temps réel dans la
liste de pairs façon "lobby" (voir gui.py).

Délègue à la commande `ping` du système plutôt que d'implémenter ICMP nous-mêmes : pas
de dépendance supplémentaire, ni de socket raw exigeant des privilèges au-delà de ceux
déjà requis pour le tunnel.

Le round-trip est lu dans la sortie de la commande. Le libellé qui le précède est bien
traduit selon la langue du système ("time=" en anglais, "temps=" en français), mais
l'unité "ms" ne l'est jamais — s'ancrer dessus est donc robuste à la localisation.

La mesure du seul temps d'exécution du sous-processus, utilisée auparavant, incluait le
coût de création du processus : négligeable sur Linux (~1 ms) mais de l'ordre de 45 ms
sur Windows, et surtout très variable (ordonnanceur, antivirus, cache disque). Un lien
LAN à 0,8 ms s'affichait donc à 47 ms sur Windows, et cette variabilité suffisait à
elle seule à dépasser le seuil de gigue de connection_quality.py (15 ms) : la pastille
de qualité annonçait "instable" sur une connexion parfaitement saine. Le repli sur
cette ancienne méthode reste en place quand le format de sortie n'est pas reconnu.
"""

from __future__ import annotations

import re
import subprocess
import sys
import time
from typing import Optional

import tunnel_probe

# Budget d'attente de l'écho applicatif avant de retomber sur l'ICMP. Court
# DÉLIBÉRÉMENT : un pair qui ne parle pas ce protocole (client antérieur) le coûte
# à chaque cycle, et il s'ajoute alors au timeout du ping. 0,6 + 1,0 = 1,6 s au pire
# par pair, ce qui reste sous le cycle de 5 s grâce à parallel_probe.
ECHO_TIMEOUT_SECONDS = 0.6

# Couvre "time=0.796 ms" (Linux), "time=47ms" (Windows), "temps<1ms" (Windows français,
# réponse sous la milliseconde) et les virgules décimales.
_RTT_PATTERN = re.compile(r"[=<]\s*(\d+(?:[.,]\d+)?)\s*ms")


def _parse_rtt_ms(output: str) -> Optional[float]:
    """
    Extrait le round-trip réel de la sortie de `ping`, ou None si le format n'est pas
    reconnu. Logique pure, testable sans réseau ni sous-processus.
    """
    match = _RTT_PATTERN.search(output)
    if match is None:
        return None
    try:
        return float(match.group(1).replace(",", "."))
    except ValueError:
        return None


def ping(host: str, timeout: float = 1.0) -> Optional[float]:
    """Retourne le round-trip approximatif vers `host` en millisecondes, ou None si injoignable."""
    extra_kwargs = {}
    if sys.platform == "win32":
        cmd = ["ping", "-n", "1", "-w", str(int(timeout * 1000)), host]
        # Sans ça, Windows affiche brièvement une fenêtre console à chaque appel —
        # très visible avec un ping toutes les 5s depuis une app sans console (.exe
        # empaqueté avec console=False).
        extra_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
    else:
        cmd = ["ping", "-c", "1", "-W", str(max(1, int(timeout))), host]

    start = time.monotonic()
    try:
        result = subprocess.run(cmd, capture_output=True, timeout=timeout + 1, **extra_kwargs)
    except (subprocess.TimeoutExpired, OSError):
        return None

    if result.returncode != 0:
        return None

    parsed = _parse_rtt_ms(result.stdout.decode("utf-8", errors="replace"))
    if parsed is not None:
        return parsed

    # Format non reconnu : on retombe sur le temps d'exécution. Imprécis, mais on ne
    # renvoie SURTOUT pas None — un None compte comme un ping échoué, et trois échecs
    # consécutifs déclenchent une demande de relais de secours
    # (RELAY_FALLBACK_FAILURE_THRESHOLD). Mieux vaut une mesure approximative qu'une
    # bascule sur relais provoquée par une expression régulière.
    return (time.monotonic() - start) * 1000


def mesurer_latence(
    host: str,
    timeout: float = 1.0,
    echo_timeout: float = ECHO_TIMEOUT_SECONDS,
) -> Optional[float]:
    """
    Latence vers un pair : écho applicatif d'abord, ICMP en repli.

    **L'ordre n'est pas une préférence, c'est une correction de bug.** Tant que
    la vivacité ne reposait que sur l'ICMP, un pare-feu tiers qui le jette
    suffisait à faire afficher « injoignable » pour un pair parfaitement joignable
    et à déclencher une demande de relais chez CHAQUE autre pair — 168,2 Mo
    mesurés sur un lien dont le chemin direct marchait (point 67bis). Et ce n'est
    pas rattrapable côté utilisateur : sept manipulations administrateur n'ont pas
    suffi à rétablir l'ICMP sur la machine témoin.

    **L'écho est aussi une MEILLEURE mesure**, ce qui est un bénéfice inattendu :
    il ne lance aucun sous-processus, donc il échappe entièrement au coût de
    création de processus qui faussait la latence Windows d'environ 45 ms et
    faisait mentir la pastille de qualité (point 34). Quand l'écho répond, la
    valeur renvoyée est plus juste que celle du `ping` système.

    **Le repli ICMP est indispensable et ne doit pas être retiré** : un pair qui
    tourne un client antérieur ne sert pas l'écho. Le déclarer injoignable pour ce
    seul motif reproduirait le bug corrigé ici, en pire — il frapperait tous les
    pairs plus anciens d'un coup, au lieu des seuls pairs derrière un pare-feu
    hostile.
    """
    latence = tunnel_probe.sonder(host, timeout=echo_timeout)
    if latence is not None:
        return latence
    return ping(host, timeout=timeout)
