"""
Comptabilité du trafic relayé — mesurer avant de brider.

Le relais réexpédie sans aucune limite, et **l'hôte paie ce trafic deux fois** :
une fois en entrée, une fois en sortie de sa propre connexion, dont l'upload est
la partie étroite sur un lien résidentiel.

La réaction naturelle est de plafonner le débit. Elle est prématurée : une
partie de jeu à travers le relais a été mesurée à **32 kbit/s entrant et
68 kbit/s sortant** (CLAUDE.md point 50), soit un centième d'un lien ordinaire.
Le seul scénario réellement coûteux est un transfert de fichiers — or le relais
est **aveugle par conception** et ne peut pas distinguer un paquet de jeu d'une
copie SMB : un plafond s'appliquerait aux deux, et casserait délibérément une
fonctionnalité qui marche.

Ce module ne change donc **aucun comportement**. Il compte, et il dit. On saura
ensuite si le problème existe, et à quel seuil il commence — au lieu de choisir
un chiffre au hasard.

Logique pure : aucune entrée-sortie ici, l'horloge est fournie par l'appelant.
Ça la rend testable sans attendre, et sans dépendre de la charge de la machine.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional


def format_octets(octets: int) -> str:
    """Taille lisible. Les unités binaires (Kio/Mio) seraient plus exactes, mais
    personne ne raisonne en Kio devant un journal — la lisibilité prime ici."""
    for unite, seuil in (("Go", 1024 ** 3), ("Mo", 1024 ** 2), ("Ko", 1024)):
        if octets >= seuil:
            return f"{octets / seuil:.1f} {unite}"
    return f"{octets} o"


def format_debit(bits_par_seconde: float) -> str:
    """Débit lisible. En **bits** par seconde, l'unité dans laquelle les liens
    Internet sont vendus — c'est à cette échelle que l'hôte comparera."""
    if bits_par_seconde >= 1_000_000:
        return f"{bits_par_seconde / 1_000_000:.2f} Mbit/s"
    if bits_par_seconde >= 1_000:
        return f"{bits_par_seconde / 1_000:.0f} kbit/s"
    return f"{bits_par_seconde:.0f} bit/s"


@dataclass
class CompteurDeSession:
    """
    Trafic relayé sur un port, depuis l'ouverture de la session.

    `octets` compte ce qui est **réexpédié**, pas ce qui est reçu : un paquet
    arrivé alors qu'une seule adresse est connue est jeté, et le compter
    laisserait croire à un trafic qui n'a jamais coûté de bande passante en
    sortie.
    """

    octets: int = 0
    paquets: int = 0
    _octets_au_dernier_releve: int = field(default=0, repr=False)
    # None et NON 0.0 : une sentinelle numérique serait ambiguë avec un
    # horodatage légitime. En production `time.time()` n'est jamais nul, donc le
    # défaut ne se voyait pas — il ne se voyait que dans un test, ce qui est
    # précisément à quoi servent les tests.
    _instant_dernier_releve: Optional[float] = field(default=None, repr=False)

    def compter(self, taille: int, maintenant: float) -> None:
        """
        Comptabilise un paquet réexpédié.

        L'instant est exigé pour armer la fenêtre de mesure **au premier paquet**
        et non au premier relevé. Sans ça, la première ligne de journal d'une
        session affichait `0 bit/s` alors que des dizaines de kilo-octets
        venaient de passer — correct au sens strict, mais activement trompeur :
        on en conclut qu'il ne se passe rien. Armer à la création du compteur ne
        conviendrait pas non plus, le port pouvant rester inutilisé des heures,
        ce qui diluerait le débit jusqu'à l'insignifiance.
        """
        if self._instant_dernier_releve is None:
            self._instant_dernier_releve = maintenant
            self._octets_au_dernier_releve = self.octets
        self.octets += taille
        self.paquets += 1

    def debit_depuis_le_dernier_releve(self, maintenant: float) -> float:
        """
        Débit en bits/s depuis l'appel précédent, puis réarme le compteur.

        Un débit instantané entre deux relevés, et non une moyenne depuis le
        début : une moyenne globale masquerait exactement ce qu'on cherche, une
        pointe brève qui sature l'upload. C'est la pointe qui gêne, pas le total.
        """
        if self._instant_dernier_releve is None:
            self._instant_dernier_releve = maintenant
            self._octets_au_dernier_releve = self.octets
            return 0.0
        ecoule = maintenant - self._instant_dernier_releve
        if ecoule <= 0:
            return 0.0
        delta = self.octets - self._octets_au_dernier_releve
        self._instant_dernier_releve = maintenant
        self._octets_au_dernier_releve = self.octets
        return delta * 8 / ecoule

    def resume(self, maintenant: float) -> str:
        """Ligne prête à journaliser."""
        return (
            f"{format_octets(self.octets)} relayés en {self.paquets} paquets, "
            f"{format_debit(self.debit_depuis_le_dernier_releve(maintenant))}"
        )
