"""
Canal d'activité entre relay.py (qui voit les paquets) et app.py (qui alloue les ports).

Les deux sont des processus séparés et ne partagent aucune mémoire — c'est un choix
d'architecture assumé depuis le point 49, le serveur de signalisation ne relayant rien.
Conséquence longtemps passée inaperçue : app.py n'avait **aucun moyen de savoir qu'une
session de relais travaillait**, donc son allocation expirait au bout de son TTL même en
plein trafic, ce qui produisait une oscillation permanente (voir config.py,
`RELAY_ALLOCATION_TTL_SECONDS`, pour la mesure et l'enchaînement complet).

Le contrat est minuscule et volontairement unidirectionnel : le relais PUBLIE la date du
dernier paquet réexpédié par port, app.py la LIT. Aucun ordre n'est donné, aucune
décision n'est déléguée — app.py reste seul maître de l'allocation.

**Toute erreur de lecture est traitée comme "aucune activité connue"**, jamais comme une
panne. Le relais est optionnel : il peut ne pas tourner, tourner sur une autre machine,
ou ne pas avoir le droit d'écrire (les unités systemd du projet sont cloisonnées). Dans
tous ces cas app.py doit continuer de fonctionner exactement comme avant ce module.
"""

from __future__ import annotations

import json
import logging
import os
import tempfile
from typing import Dict

logger = logging.getLogger("meshamoto-relay-activity")


def publier(activite: Dict[int, float], chemin: str) -> bool:
    """
    Écrit `{port: horodatage du dernier paquet réexpédié}`. Retourne False si l'écriture
    a échoué, sans lever : le relais doit continuer de relayer même s'il ne peut pas
    rendre compte — sa fonction première n'est pas de tenir ce fichier à jour.

    Écriture **atomique** (fichier temporaire puis `os.replace`) : app.py lit ce fichier
    à chaque construction de liste de pairs, donc très souvent, et une lecture tombant au
    milieu d'une écriture directe verrait du JSON tronqué. `os.replace` est atomique sur
    les deux OS, et le fichier temporaire est créé dans le MÊME dossier — un `rename`
    entre systèmes de fichiers différents ne l'est pas.
    """
    dossier = os.path.dirname(os.path.abspath(chemin))
    try:
        os.makedirs(dossier, exist_ok=True)
        fd, temporaire = tempfile.mkstemp(dir=dossier, prefix=".relay-activity-", suffix=".tmp")
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as fichier:
                json.dump({str(port): ts for port, ts in activite.items()}, fichier)
            os.replace(temporaire, chemin)
        except BaseException:
            # Un temporaire laissé derrière s'accumulerait à chaque échec, dans un
            # processus qui tourne des mois.
            try:
                os.unlink(temporaire)
            except OSError:
                pass
            raise
    except OSError:
        logger.debug("Publication de l'activité du relais impossible (%s)", chemin, exc_info=True)
        return False
    return True


def lire(chemin: str) -> Dict[int, float]:
    """
    Relit ce qu'a publié le relais. Renvoie un dictionnaire vide si le fichier n'existe
    pas, n'est pas lisible, ou ne contient pas ce qu'on attend.

    Volontairement tolérant sur le CONTENU et pas seulement sur l'accès : ce fichier est
    écrit par un autre processus, potentiellement d'une autre version après une mise à
    jour partielle. Une clé non numérique ou une valeur non temporelle est ignorée
    individuellement plutôt que de faire perdre tout le reste — un port lisible reste
    utile même si un autre ne l'est pas.
    """
    try:
        with open(chemin, "r", encoding="utf-8") as fichier:
            brut = json.load(fichier)
    except (OSError, ValueError):
        return {}
    if not isinstance(brut, dict):
        return {}

    activite: Dict[int, float] = {}
    for cle, valeur in brut.items():
        try:
            activite[int(cle)] = float(valeur)
        except (TypeError, ValueError):
            continue
    return activite
