"""
Logique pure d'injection des pairs Meshamoto dans les favoris du navigateur de serveurs
Steam — alternative au relais de découverte LAN, condamné le 2026-07-29 (voir README,
« Gaming LAN virtuel »).

Le relais visait à faire apparaître les parties des pairs dans l'onglet **LAN** du jeu.
C'est impossible : sous Linux le noyau ne délivre aucune copie d'un broadcast émis
localement, et les serveurs Source ne répondent de toute façon pas aux requêtes
diffusées. Écrire directement dans les **favoris** contourne les deux obstacles — ni
capture de broadcast, ni requête diffusée, ni privilège, et le comportement est
identique sur Windows et Linux.

Fichier visé (format VDF texte, inspecté sur une installation réelle) :
    Linux   : ~/.local/share/Steam/userdata/<steamid>/7/remote/serverbrowser_hist.vdf
    Windows : %ProgramFiles(x86)%\\Steam\\userdata\\<steamid>\\7\\remote\\...

    "Filters"
    {
        "favorites" { }
        "history"   { "1" { "name" "..." "address" "ip:port" ... } }
    }

**Steam doit être fermé pendant l'écriture** : il conserve son propre état en mémoire et
réécrit ce fichier en quittant, ce qui annulerait toute modification externe. Le dossier
`remote/` est par ailleurs synchronisé par Steam Cloud (voir `remotecache.vdf`, qui
mémorise taille et SHA) : une modification faite Steam fermé est vue au démarrage suivant
comme une modification locale légitime et remonte normalement.

**Mesuré en conditions réelles le 2026-07-30** (première écriture face à un vrai Steam) :
nos entrées apparaissent bien dans l'onglet Favoris, et un cycle complet lancement →
consultation du navigateur → fermeture les a laissées intactes — Steam Cloud n'a rien
annulé, et le fichier n'a même pas été réécrit (date de modification inchangée). Steam
ne le réécrit donc que si l'état du navigateur change de SON côté ; la précaution
« Steam fermé » vise ce cas-là, pas le simple fait qu'il tourne. Ne pas la relâcher pour
autant : c'est précisément quand l'utilisateur touche à ses favoris que l'écrasement
serait le plus coûteux.

Identification de NOS entrées : toute favorite dont l'adresse tombe dans la plage
virtuelle du réseau nous appartient. Pas besoin de marqueur — un utilisateur n'ajoute
jamais à la main une adresse de cette plage, et ça évite d'inventer une convention que
Steam pourrait ne pas préserver.

Aucune I/O ici : voir steam_favorites_manager.py pour la localisation des fichiers et
l'écriture réelle.
"""

from __future__ import annotations

import ipaddress
import re

# Une ligne "clé" "valeur", avec des tabulations comme séparateur dans le format Steam.
_PAIR_RE = re.compile(r'^\s*"([^"]*)"\s+"([^"]*)"\s*$')
_KEY_RE = re.compile(r'^\s*"([^"]*)"\s*$')

FAVORITES_KEY = "favorites"


def parse_vdf(text: str) -> dict:
    """
    Analyse un VDF texte en dictionnaires imbriqués, en préservant l'ordre d'insertion.

    Volontairement minimal : couvre la forme de ce fichier précis (blocs imbriqués et
    paires clé/valeur entre guillemets), pas la spécification VDF complète — ni
    échappements, ni `#include`, ni conditionnels de plateforme, qui n'y apparaissent
    jamais.
    """
    root: dict = {}
    pile = [root]
    attente_cle: list[str] = []

    for ligne in text.splitlines():
        nue = ligne.strip()
        if not nue or nue.startswith("//"):
            continue
        if nue == "{":
            nouveau: dict = {}
            cle = attente_cle.pop() if attente_cle else ""
            pile[-1][cle] = nouveau
            pile.append(nouveau)
            continue
        if nue == "}":
            if len(pile) > 1:
                pile.pop()
            continue
        paire = _PAIR_RE.match(ligne)
        if paire:
            pile[-1][paire.group(1)] = paire.group(2)
            continue
        seule = _KEY_RE.match(ligne)
        if seule:
            attente_cle.append(seule.group(1))
    return root


def render_vdf(data: dict, indent: int = 0) -> str:
    """Sérialise en VDF avec les tabulations et l'ordre attendus par Steam."""
    tab = "\t" * indent
    lignes = []
    for cle, valeur in data.items():
        if isinstance(valeur, dict):
            lignes.append(f'{tab}"{cle}"')
            lignes.append(f"{tab}{{")
            lignes.append(render_vdf(valeur, indent + 1))
            lignes.append(f"{tab}}}")
        else:
            lignes.append(f'{tab}"{cle}"\t\t"{valeur}"')
    return "\n".join(l for l in lignes if l != "")


def _is_ours(address: str, virtual_cidr: str) -> bool:
    """Une favorite nous appartient si son adresse est dans la plage virtuelle."""
    hote = address.rsplit(":", 1)[0]
    try:
        return ipaddress.ip_address(hote) in ipaddress.ip_network(virtual_cidr)
    except ValueError:
        return False


def _favorites_block(racine: dict) -> dict:
    """Bloc favorites du fichier, créé s'il manque (il existe vide sur une install neuve)."""
    filtres = racine.setdefault("Filters", {})
    return filtres.setdefault(FAVORITES_KEY, {})


def build_updated_favorites(
    existing_text: str,
    peers: dict[str, str],
    virtual_cidr: str,
    game_port: int,
    last_played: int = 0,
    peer_ports: dict[str, int] | None = None,
) -> str:
    """
    Remplace nos entrées par `peers` ({pseudo: ip_virtuelle}), sans toucher aux favorites
    de l'utilisateur ni au reste du fichier.

    `peer_ports` ({pseudo: port}) l'emporte sur `game_port` pour les pairs qu'il nomme :
    un pair qui annonce un service hébergé (voir gui.py, « Mon service hébergé ») doit
    apparaître avec SON port, sinon la favorite pointe dans le vide. `game_port` reste le
    défaut pour les autres.

    Idempotent : appelé deux fois avec les mêmes pairs, il produit le même contenu.
    """
    ports = peer_ports or {}
    racine = parse_vdf(existing_text) if existing_text.strip() else {}
    favorites = _favorites_block(racine)

    conserves = [
        entree for entree in favorites.values()
        if isinstance(entree, dict) and not _is_ours(entree.get("address", ""), virtual_cidr)
    ]
    nouvelles = [
        {
            "name": f"{pseudo} (Meshamoto)",
            "address": f"{ip}:{ports.get(pseudo, game_port)}",
            "LastPlayed": str(last_played),
            "appid": "0",
            "accountid": "0",
        }
        for pseudo, ip in sorted(peers.items())
    ]

    favorites.clear()
    for index, entree in enumerate(conserves + nouvelles, start=1):
        favorites[str(index)] = entree

    racine.setdefault("Filters", {}).setdefault("history", {})
    return render_vdf(racine) + "\n"


def build_cleared_favorites(existing_text: str, virtual_cidr: str) -> str:
    """Retire nos entrées, laisse celles de l'utilisateur et le reste du fichier intacts."""
    return build_updated_favorites(existing_text, {}, virtual_cidr, game_port=0)
