"""
Force la résolution d'un nom d'hôte vers une adresse, dans ce processus seulement.

Sert au cas du serveur auto-hébergé chez soi, joint par son nom public depuis le
même réseau local : la box ne renvoie pas vers l'intérieur une connexion
destinée à sa propre adresse publique (« hairpin NAT »), et la tentative expire.
Le contournement habituel est une ligne dans le fichier hosts de la machine —
mais elle est **globale, permanente et à la main**, donc elle devient un piège
dès qu'on change de réseau : rencontré en conditions réelles le 2026-07-30, un
PC passé sur un partage de connexion mobile ne joignait plus rien du tout parce
que le nom pointait encore vers une adresse LAN inexistante là-bas.

Ici, la substitution vit dans le processus, ne survit pas à sa fermeture, et
s'annule dès qu'elle n'est plus utile. C'est exactement la sémantique de
`curl --resolve`.

**Pourquoi `socket.getaddrinfo` et pas une réécriture d'URL.** Remplacer le nom
par l'adresse dans l'URL casserait TLS : le certificat est émis pour le nom, la
vérification échouerait, et le SNI partirait avec une adresse. En agissant sur
la résolution, **tout le reste du programme continue de manipuler le nom** —
en-tête `Host`, SNI, vérification du certificat — et seule la destination du
paquet change. C'est aussi la seule façon de couvrir d'un coup les appels REST
(`requests`) et la WebSocket (`websocket-client`), qui n'offrent pas le même
point d'extension.
"""

from __future__ import annotations

import ipaddress
import logging
import socket
import threading
from typing import Optional

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

# Résolution d'origine, capturée une seule fois. Conservée à part du drapeau
# d'installation : réinstaller par-dessus soi-même empilerait les substitutions.
_getaddrinfo_origine = socket.getaddrinfo
_verrou = threading.Lock()
_substitution: Optional[tuple[str, str]] = None  # (nom, adresse)
_installe = False


def adresse_valide(adresse: str) -> bool:
    """Vrai si la chaîne est une adresse IPv4 littérale utilisable comme cible."""
    try:
        ipaddress.IPv4Address(adresse.strip())
        return True
    except ValueError:
        return False


def _getaddrinfo_substitue(host, port, *args, **kwargs):
    substitution = _substitution
    if substitution is not None and isinstance(host, str) and host == substitution[0]:
        return _getaddrinfo_origine(substitution[1], port, *args, **kwargs)
    return _getaddrinfo_origine(host, port, *args, **kwargs)


def appliquer(nom: str, adresse: str) -> bool:
    """
    Fait résoudre `nom` vers `adresse` pour tout ce processus.

    Renvoie False si l'adresse n'est pas une IPv4 littérale — on ne substitue
    jamais un nom par un autre nom, ce qui rouvrirait la porte au problème qu'on
    cherche justement à contourner.
    """
    global _substitution, _installe
    if not nom or not adresse_valide(adresse):
        return False
    with _verrou:
        _substitution = (nom, adresse.strip())
        if not _installe:
            socket.getaddrinfo = _getaddrinfo_substitue
            _installe = True
    logger.info("Résolution forcée : %s -> %s (ce processus uniquement)", nom, adresse.strip())
    return True


def annuler() -> None:
    """
    Retire la substitution. La fonction d'origine est restaurée telle quelle,
    pas seulement neutralisée : laisser un intermédiaire en place ferait payer
    un appel Python à chaque résolution pour rien, et compliquerait le
    diagnostic de toute anomalie DNS ultérieure.
    """
    global _substitution, _installe
    with _verrou:
        if not _installe:
            return
        socket.getaddrinfo = _getaddrinfo_origine
        _substitution = None
        _installe = False
    logger.info("Résolution forcée retirée.")


def substitution_active() -> Optional[tuple[str, str]]:
    """(nom, adresse) actuellement forcés, ou None."""
    return _substitution
