"""
Configuration partagée entre app.py (allocation) et relay.py (écoute réseau) pour le
relais UDP de secours (voir relay.py pour le principe).
"""

from __future__ import annotations

import os
import sys

# Plage de ports UDP sur laquelle relay.py écoute en permanence, un port par session
# de relais active. app.py alloue un port libre de cette plage à chaque paire de
# pairs qui en a besoin (voir app.py::_allocate_relay_port) — les deux processus
# doivent s'accorder sur la même plage, d'où ce module partagé.
RELAY_PORT_POOL_START = int(os.environ.get("MESHAMOTO_RELAY_PORT_START", "8766"))
RELAY_PORT_POOL_SIZE = int(os.environ.get("MESHAMOTO_RELAY_PORT_POOL_SIZE", "20"))

# Durée d'inactivité au-delà de laquelle relay.py "oublie" les adresses apprises sur un
# port et le rend disponible pour une autre paire.
#
# Valait 30 s, ce qui laissait **5 secondes de marge** avec le `PersistentKeepalive = 25`
# des configurations WireGuard générées par le client. Trop peu : sous Windows il n'y a
# pas de `wg syncconf`, donc chaque changement d'effectif détruit et recrée la carte
# réseau, et cette interruption dépasse largement 5 s. Le relais perdait alors sa paire
# d'adresses alors que la session était parfaitement vivante.
RELAY_SESSION_TTL_SECONDS = 120.0

# Durée au-delà de laquelle app.py cesse d'annoncer un port de relais à une paire, faute
# de tout signe de vie.
#
# **Constante DISTINCTE de celle du dessus, et c'est le correctif du 2026-08-05.** Les
# deux étaient confondues, et l'allocation n'était rafraîchie qu'à la réception d'une
# NOUVELLE demande — jamais par le trafic qui traversait le relais. Une paire qui avait
# réellement besoin du relais le perdait donc toutes les 30 s, quoi qu'il arrive :
#   t=0    port alloué, diffusé, les deux pairs l'utilisent
#   t=30s  diffusion suivante -> plus de relay_port -> retour au direct
#          or un pair en NAT symétrique n'a AUCUN chemin direct
#   t=~57s 15 s de grâce + 3 cycles d'escalade -> nouvelle demande -> nouveau port
#          -> nouvelle diffusion -> tout le monde reconfigure -> on recommence
# Oscillation permanente d'environ 60 s, mesurée le 2026-08-05 : **39 bascules
# d'endpoint en une journée** pour le seul pair en CGNAT. Chacune forçait tous les
# postes Windows à recréer leur interface, donc à disparaître quelques secondes, ce qui
# faisait escalader leurs voisins — jusqu'à ouvrir une session de relais entre deux
# machines du même commutateur, à 0,57 ms l'une de l'autre.
#
# **INVARIANT : doit rester >= RELAY_SESSION_TTL_SECONDS** (test dans
# tests/test_relay_activity.py). L'inverse ferait réattribuer par app.py un port sur
# lequel relay.py garde encore une paire d'adresses apprises — les paquets de la
# nouvelle paire seraient réexpédiés à l'ancienne.
RELAY_ALLOCATION_TTL_SECONDS = 300.0

# Réévaluation périodique du chemin direct (2026-08-06, point 104).
#
# Le correctif ci-dessus a un effet de bord démontré au banc : en maintenant
# l'allocation en vie par le trafic, il a fermé la SEULE porte de sortie qui existait.
# Le point 66 notait que le retour au direct dépendait de l'expiration de l'allocation ;
# or les sondes de vivacité passent par le relais dès qu'il est adopté, donc elles
# entretiennent elles-mêmes l'allocation. Mesuré le 2026-08-06 : une coupure de 30 s a
# laissé un pair relayé indéfiniment, alors que son chemin direct était redevenu viable
# — le blocage est auto-entretenu par les DEUX côtés (point 103d).
#
# Le serveur cesse donc d'annoncer le port passé ce délai, ce qui fait retenter le
# direct aux deux pairs EN MÊME TEMPS. La coordination doit venir d'ici : le roaming de
# WireGuard (il apprend l'endpoint d'après la source des paquets reçus) annule toute
# tentative unilatérale — le côté resté sur le relais y ramène l'autre.
#
# **Ce délai réintroduit délibérément l'oscillation que RELAY_ALLOCATION_TTL_SECONDS a
# supprimée**, mais deux ordres de grandeur plus lentement : une réévaluation ratée
# coûte ~15 s de dégradation (3 échecs de sonde cadencés) au lieu des ~60 s de cycle
# mesurées le 2026-08-05, et elle survient toutes les 30 min au lieu de chaque minute.
RELAY_REEVALUATION_BASE_SECONDS = 1800.0

# Recul en cas d'échec répété. Un pair réellement en CGNAT symétrique n'a AUCUN chemin
# direct (point 49) : lui infliger une réévaluation toutes les 30 minutes pour toujours
# serait payer un coût permanent pour un résultat connu d'avance. L'intervalle double à
# chaque échec, plafonné ici — même motif que le recul de reconnexion du point 70.
RELAY_REEVALUATION_MAX_SECONDS = 7200.0

# Une paire qui redemande un relais dans cette fenêtre après une réévaluation prouve que
# le direct n'a pas tenu. Au-delà, c'est une panne neuve et le compteur repart de zéro —
# même raisonnement que le compteur de reconnexion, qui se réarme dès qu'une connexion a
# réellement abouti (point 70).
RELAY_REEVALUATION_ECHEC_SECONDS = 120.0

# Période de la tâche de fond qui applique la réévaluation.
#
# **Elle n'est pas un détail d'implémentation : sans elle, le mécanisme ne se déclenche
# jamais.** Premier jet du point 104, mesuré en production le 2026-08-06 : la
# réévaluation vivait dans `_active_relay_port`, qui n'est appelé qu'à la CONSTRUCTION
# d'une liste de pairs — donc à une diffusion. Or un réseau stable ne diffuse plus rien
# : quatre minutes après l'échéance, zéro diffusion, zéro réévaluation. Le correctif
# attendait un événement que sa propre réussite rendait impossible.
# C'est le raisonnement déjà écrit au point 95 (« un réseau stable ne produit plus de
# diffusions, le silence y est normal »), qu'il a fallu payer une seconde fois.
#
# 30 s de granularité sur un intervalle de 30 min est négligeable, et c'est assez lent
# pour ne rien coûter : un passage ne parcourt qu'un dictionnaire en mémoire.
RELAY_REEVALUATION_CHECK_SECONDS = 30.0

# Fichier par lequel relay.py signale à app.py quels ports ont vu passer du trafic.
#
# Les deux sont des PROCESSUS SÉPARÉS (voir relay.py) et ne partagent aucune mémoire :
# sans ce canal, app.py ne peut pas distinguer une session vivante d'une session morte,
# ce qui est précisément le défaut corrigé ci-dessus. Un fichier plutôt qu'un port de
# contrôle — le relais n'a aucune raison d'exposer une surface réseau de plus, et la
# donnée est minuscule.
#
# **Son absence n'est pas une erreur** : le relais est un composant optionnel, qui peut
# ne pas tourner du tout ou tourner sur une autre machine. app.py retombe alors sur
# l'horodatage de la demande, avec le TTL long ci-dessus — la correction de
# l'oscillation tient donc sur deux couches indépendantes, et la seconde suffit.
# Attention au déploiement : les unités systemd du projet ont `PrivateTmp=yes`, donc un
# chemin sous /tmp ne serait PAS partagé entre les deux services.
def dossier_de_base(fige: bool, executable: str, module: str) -> str:
    """
    Dossier où placer un fichier partagé entre le serveur et le relais, par défaut.

    **Une fois figé par PyInstaller, `__file__` ne convient pas** : les trois `.spec` du
    projet sont en `--onefile`, donc il pointe dans le dossier d'extraction temporaire
    (`_MEIPASS`), **différent à chaque lancement ET par processus**, et supprimé à la
    sortie. Le serveur et le relais étant deux exécutables distincts, ils ne
    partageraient donc jamais le même fichier — le canal serait mort sous Windows.
    `sys.executable` reste stable et désigne l'emplacement réel de l'`.exe` ; les deux
    binaires étant installés côte à côte (vérifié au point 55), leur DOSSIER est le même.

    Fonction pure et parametrée plutôt que lecture directe de `sys` : le cas figé est
    précisément celui qu'on ne peut pas exercer depuis la suite de tests autrement.
    Même mécanisme que `database._default_db_dir`, délibérément — un second concept pour
    le même problème finirait par diverger, et si la base peut s'écrire là, ce fichier
    aussi.
    """
    return os.path.dirname(executable) if fige else os.path.dirname(os.path.abspath(module))


# Porteur WebSocket/TLS du relais (point 108) — dernier recours quand l'UDP est
# TOTALEMENT bloqué chez un pair. Voir relay_ws.py pour le principe.
#
# Lié à la BOUCLE LOCALE, comme le serveur de signalisation (point 42) : c'est le
# reverse proxy qui le publie sur 443, donc sur le port et le certificat qui
# fonctionnent déjà là où l'UDP ne passe pas. L'exposer directement n'apporterait rien
# — un réseau qui bloque l'UDP bloque aussi les ports exotiques — et ajouterait une
# surface d'attaque.
RELAY_WS_HOST = os.environ.get("MESHAMOTO_RELAY_WS_HOST", "127.0.0.1")
RELAY_WS_PORT = int(os.environ.get("MESHAMOTO_RELAY_WS_PORT", "8787"))

# Désactivable, et c'est délibéré : une instance qui n'a pas de reverse proxy devant
# elle n'a aucune raison d'ouvrir ce point d'écoute.
RELAY_WS_ENABLED = os.environ.get("MESHAMOTO_RELAY_WS", "1") != "0"

# Plafond de taille d'une trame. Un datagramme WireGuard tient très largement dedans
# (MTU de l'ordre de 1500 o) ; ce n'est pas un réglage de performance mais une borne.
RELAY_WS_MAX_FRAME = 65536

RELAY_ACTIVITY_PATH = os.environ.get(
    "MESHAMOTO_RELAY_ACTIVITY_PATH",
    os.path.join(
        dossier_de_base(getattr(sys, "frozen", False), sys.executable, __file__),
        "relay-activity.json",
    ),
)
