"""
Mise à jour d'un enregistrement DuckDNS depuis le client.

Jumeau Python de `packaging/linux/meshamoto-duckdns-update`, qui n'existait que
sur Linux et n'était installé que par le paquet Arch : un hôte Windows n'avait
**aucun** moyen de maintenir son nom de domaine à jour, et rien ne le lui disait.
Le parcours déroulé le 2026-07-31 a montré qu'il n'existait même pas d'endroit où
saisir le token.

DEUX RÈGLES NON NÉGOCIABLES, héritées de la version shell et mesurées contre le
vrai service :

1. **DuckDNS répond `KO` dans le CORPS avec un HTTP 200.** Un appel qui « réussit »
   ne prouve donc rien, et se fier au code de retour ferait passer un token
   invalide pour une mise à jour réussie — exactement le mode de défaillance muet
   que ce module existe pour supprimer. Seule la lecture du corps a du sens.
2. **Le token ne doit jamais se retrouver dans un journal.** Il voyage dans l'URL,
   donc on ne journalise jamais l'URL, jamais l'exception brute de `requests` (qui
   la contient), et jamais la réponse sans réfléchir. Les messages ne nomment que
   le domaine.

Le paramètre `ip` est laissé VIDE délibérément : DuckDNS prend alors l'adresse
source de la requête, c'est-à-dire l'adresse publique réelle, sans faire dépendre
la mise à jour d'un service tiers de découverte d'IP.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass

import requests

logger = logging.getLogger("meshamoto-client.duckdns")

URL = "https://www.duckdns.org/update"
TIMEOUT_SECONDS = 30


@dataclass(frozen=True)
class Resultat:
    """
    `succes` seul ne suffit pas : `message` est affiché tel quel à l'utilisateur,
    et c'est lui qui distingue « token refusé » de « pas de réseau » — deux pannes
    qui appellent des actions opposées.
    """

    succes: bool
    message: str


def interpreter_reponse(corps: str) -> Resultat:
    """
    Traduit le corps renvoyé par DuckDNS. Fonction pure : c'est le cœur du
    module, et le seul endroit où se joue la règle n°1 ci-dessus.

    `OK` peut être suivi d'autres lignes (l'adresse retenue, et `UPDATED`/`NOCHANGE`
    quand `verbose=true`), d'où la comparaison sur le PREMIER mot et non sur la
    chaîne entière.
    """
    premier = corps.strip().split("\n", 1)[0].strip()
    if premier.upper().startswith("OK"):
        return Resultat(True, "Nom de domaine à jour.")
    if premier.upper().startswith("KO"):
        # DuckDNS ne distingue pas les deux cas ; le message doit donc citer les
        # deux, sinon l'utilisateur ne vérifie qu'une moitié.
        return Resultat(
            False,
            "DuckDNS a refusé la mise à jour : le sous-domaine ou le token est "
            "incorrect. Vérifiez les deux sur duckdns.org.",
        )
    # Ni OK ni KO : page de maintenance, portail captif, proxy d'entreprise. Ne
    # PAS renvoyer le corps tel quel — ce peut être une page HTML entière.
    return Resultat(False, "Réponse inattendue de DuckDNS (service indisponible ?).")


def mettre_a_jour(domaines: str, token: str, *, timeout: float = TIMEOUT_SECONDS) -> Resultat:
    """
    Publie l'adresse publique courante pour `domaines` (sous-domaine seul, sans
    `.duckdns.org`, plusieurs séparés par des virgules).

    Les paramètres passent par `params=` et non par une URL assemblée à la main :
    l'encodage est alors correct pour un token contenant un caractère réservé, ce
    qui autrement produirait un `KO` incompréhensible.
    """
    if not domaines or not token:
        return Resultat(False, "Sous-domaine ou token manquant.")
    try:
        reponse = requests.get(
            URL,
            params={"domains": domaines, "token": token, "ip": ""},
            timeout=timeout,
        )
    except requests.RequestException:
        # L'exception porte l'URL COMPLÈTE, donc le token : on ne la journalise
        # ni ne l'affiche jamais, même en debug.
        logger.warning("Échec réseau lors de la mise à jour DuckDNS de %s", domaines)
        return Resultat(False, "DuckDNS est injoignable (réseau ou TLS).")

    resultat = interpreter_reponse(reponse.text)
    if resultat.succes:
        logger.info("DuckDNS : %s à jour", domaines)
    else:
        logger.warning("DuckDNS : %s — %s", domaines, resultat.message)
    return resultat
