"""
Habilitation : ce serveur exige-t-il une clé de licence, et ce jeton est-il valable ?

**DÉSACTIVÉE PAR DÉFAUT, et c'est la règle la plus importante de ce module.** Un
serveur auto-hébergé n'exige aucune clé, jamais. Sans cette précaution, la première
personne qui suit la voie de l'auto-hébergement se retrouverait verrouillée hors de
son propre serveur — et l'argument central du produit (« hébergez-la vous-même si
vous préférez ») s'effondrerait au moment précis où on le démontre. Seul le serveur
géré pose `MESHAMOTO_LICENCE_REQUISE=1`.

Ce module ne connaît **que la clé publique**. Il ne lit pas la base des licences, ne
parle pas au service de licence, et n'a besoin d'aucun des deux pour fonctionner :
c'est ce qui permet au service de vente de tomber sans déconnecter personne. Voir
l'en-tête de `licence_common.py` pour le raisonnement complet.
"""

from __future__ import annotations

import logging
import os
import time
from dataclasses import dataclass

import licence_common

logger = logging.getLogger("meshamoto-server")

LICENCE_REQUISE = os.environ.get("MESHAMOTO_LICENCE_REQUISE", "0") == "1"

# Même variable que celle du service de licence, qui écrit ce fichier : les deux
# services tournent sous des comptes distincts, et `ProtectSystem=strict` ne restreint
# que l'écriture. La lecture croisée ne demande donc aucun droit supplémentaire.
CLE_PUBLIQUE_PATH = os.environ.get(
    "MESHAMOTO_LICENCE_CLE_PUBLIQUE",
    os.path.join(os.path.dirname(os.path.abspath(__file__)), "signature.pub"),
)

# En-tête portant le jeton à l'ouverture de la WebSocket. Un en-tête plutôt qu'un
# paramètre d'URL : le jeton est un porteur d'autorisation, et une URL se retrouve
# dans les journaux d'accès de tout reverse proxy traversé.
ENTETE_JETON = "x-meshamoto-licence"

_cle_publique = None


@dataclass(frozen=True)
class Verdict:
    """
    Résultat d'un contrôle d'habilitation.

    `motif` est destiné à la MACHINE et `message` à l'humain. Le client s'en sert pour
    trancher entre réessayer et abandonner : un jeton expiré appelle une réactivation
    silencieuse, tout le reste appelle un message et l'arrêt des tentatives. Sans
    cette distinction, un jeton périmé ferait renoncer un client parfaitement en règle.
    """

    autorise: bool
    motif: str = ""
    message: str = ""
    licence_id: str = ""

    @property
    def reessayable(self) -> bool:
        """Vrai quand une réactivation auprès du service de licence peut débloquer."""
        return self.motif == licence_common.MotifDeRefus.EXPIRE.value


_AUTORISE = Verdict(autorise=True)

_MESSAGES = {
    "absent": (
        "Ce serveur demande une clé de licence. Inscrivez-vous pour en obtenir une, "
        "ou hébergez votre propre serveur — Meshamoto reste gratuit dans ce cas."
    ),
    licence_common.MotifDeRefus.EXPIRE.value: (
        "Votre habilitation est arrivée à échéance. Elle se renouvelle toute seule ; "
        "si le message persiste, vérifiez votre clé de licence."
    ),
    licence_common.MotifDeRefus.SIGNATURE.value: (
        "Cette habilitation n'a pas été émise par ce serveur. Réactivez votre clé."
    ),
    licence_common.MotifDeRefus.FORME.value: (
        "Habilitation illisible. Réactivez votre clé de licence."
    ),
    licence_common.MotifDeRefus.CHARGE.value: (
        "Habilitation illisible. Réactivez votre clé de licence."
    ),
}


def charger_au_demarrage() -> None:
    """
    Charge la clé publique si l'habilitation est exigée. **Lève si elle est illisible.**

    Le choix de planter au démarrage est délibéré, et c'est le seul honnête. Les deux
    alternatives sont pires :

      - continuer en laissant tout passer transformerait silencieusement le serveur
        géré en serveur gratuit, sans qu'aucune trace ne le dise ;
      - continuer en refusant tout le monde produirait la même panne, mais au premier
        utilisateur et non à l'administrateur.

    Une clé de vérification absente alors qu'on a demandé l'habilitation est une
    erreur de CONFIGURATION : elle doit se voir à l'endroit et au moment où on peut la
    corriger. Même raisonnement que le refus du repli silencieux ailleurs dans le
    projet — un défaut nommé au bon moment vaut mieux qu'une dégradation muette.
    """
    global _cle_publique
    if not LICENCE_REQUISE:
        logger.info(
            "Habilitation par licence DÉSACTIVÉE (serveur auto-hébergé ou de "
            "développement) — aucune clé n'est demandée aux pairs."
        )
        return

    chemin = os.path.abspath(CLE_PUBLIQUE_PATH)
    try:
        with open(chemin, "rb") as fichier:
            _cle_publique = licence_common.charger_cle_publique(fichier.read())
    except OSError as erreur:
        raise RuntimeError(
            f"MESHAMOTO_LICENCE_REQUISE=1 mais la clé de vérification est illisible "
            f"({chemin}) : {erreur}. Sans elle, aucun jeton ne peut être vérifié — "
            f"le serveur refuse de démarrer plutôt que de laisser passer tout le monde."
        ) from erreur

    logger.info("Habilitation par licence ACTIVE, clé de vérification : %s", chemin)


def verifier(jeton: str | None, maintenant: int | None = None) -> Verdict:
    """Verdict d'habilitation pour un jeton présenté. Toujours autorisé si désactivée."""
    if not LICENCE_REQUISE:
        return _AUTORISE

    if _cle_publique is None:
        # Ne peut se produire que si `charger_au_demarrage` n'a pas été appelé — donc
        # un défaut de câblage, pas une condition d'exécution. On refuse : laisser
        # passer serait exactement la dégradation muette que le démarrage évite.
        logger.error("Habilitation exigée mais clé de vérification non chargée")
        return Verdict(False, "interne", _MESSAGES["absent"])

    if not jeton:
        return Verdict(False, "absent", _MESSAGES["absent"])

    verif = licence_common.verifier_jeton(
        _cle_publique, jeton, maintenant=int(time.time() if maintenant is None else maintenant)
    )
    if verif.valide:
        return Verdict(True, licence_id=verif.jeton.licence_id)

    motif = verif.motif.value
    return Verdict(False, motif, _MESSAGES.get(motif, _MESSAGES["absent"]))
