"""
Persistance du sous-domaine et du token DuckDNS.

**Écrit exactement le fichier que lit déjà la minuterie systemd de Linux**
(`packaging/linux/meshamoto-duckdns-update`, point 44), au même emplacement et au
même format. C'est le point d'intégration qui fait tout l'intérêt du module : sur
Arch, configurer DuckDNS dans l'interface suffit ensuite à ce que la minuterie
maintienne l'enregistrement à jour **même quand Meshamoto est fermé**, sans que
personne n'ait à toucher un terminal. Ne pas « ranger » ce fichier sous
`CONFIG_DIR` : il tomberait à côté de ce que le script va chercher, et la mise à
jour redeviendrait silencieusement dépendante du client ouvert.

Le fichier est **lu, jamais exécuté**. La version shell a appris ça de la manière
forte : elle faisait `. "$CONF"`, et un token collé seul sur sa ligne a été
interprété comme une commande, ce qui l'a recraché dans le journal systemd —
lisible sans privilège, malgré le `chmod 600`. La règle générale du projet est
« un fichier de données ne doit jamais devenir du code », et elle ne vaut pas que
pour les chemins privilégiés.
"""

from __future__ import annotations

import logging
import os
import re
import sys
from pathlib import Path
from typing import Optional

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

CLE_DOMAINES = "DUCKDNS_DOMAINS"
CLE_TOKEN = "DUCKDNS_TOKEN"


def lire_cle(contenu: str, cle: str) -> str:
    """
    Extrait `CLE=valeur`. Reproduit volontairement l'analyseur `awk` du script
    shell — espaces optionnels autour du `=`, `\\r` final toléré pour un fichier
    édité sous Windows. Les deux implémentations doivent voir la même chose, sinon
    l'interface et la minuterie liraient deux configurations différentes.
    """
    motif = rf"^[ \t]*{re.escape(cle)}[ \t]*=[ \t]*(.*)$"
    for ligne in contenu.splitlines():
        correspondance = re.match(motif, ligne)
        if correspondance:
            return correspondance.group(1).rstrip("\r").strip()
    return ""


def rendre(domaines: str, token: str) -> str:
    """Contenu du fichier. Séparé de l'écriture pour être testable tel quel."""
    return f"{CLE_DOMAINES}={domaines}\n{CLE_TOKEN}={token}\n"


def valider(domaines: str, token: str) -> Optional[str]:
    """
    Renvoie un message d'erreur, ou None si le couple est acceptable.

    Le refus des sauts de ligne n'est pas cosmétique : une valeur qui en contient
    ajouterait une ligne au fichier, donc **une clé arbitraire** lue ensuite par
    la minuterie. Même raisonnement qu'au point 38 pour le composant privilégié —
    ce qu'on écrit dans un fichier relu par un autre programme doit être validé au
    moment de l'écriture, pas espéré.
    """
    if not domaines.strip():
        return "Indiquez le sous-domaine DuckDNS (sans « .duckdns.org »)."
    if not token.strip():
        return "Indiquez le token DuckDNS (visible sur duckdns.org une fois connecté)."
    for valeur in (domaines, token):
        if "\n" in valeur or "\r" in valeur:
            return "Le sous-domaine et le token ne peuvent pas contenir de saut de ligne."
    return None


def normaliser_domaine(brut: str) -> str:
    """
    DuckDNS attend le sous-domaine SEUL. Quelqu'un qui vient de créer
    `xyz.duckdns.org` recopie naturellement le nom complet — et obtiendrait un
    `KO` sans comprendre, puisque le service ne distingue pas « mauvais domaine »
    de « mauvais token ». On enlève donc le suffixe plutôt que de le refuser.
    """
    valeur = brut.strip().lower().rstrip(".")
    for prefixe in ("https://", "http://"):
        if valeur.startswith(prefixe):
            valeur = valeur[len(prefixe):]
    valeur = valeur.split("/", 1)[0]
    suffixe = ".duckdns.org"
    if valeur.endswith(suffixe):
        valeur = valeur[: -len(suffixe)]
    return valeur


def charger(path: Path) -> tuple[str, str]:
    """(sous-domaine, token), ou deux chaînes vides si le fichier est absent."""
    try:
        contenu = path.read_text(encoding="utf-8")
    except FileNotFoundError:
        return "", ""
    except OSError as exc:
        logger.warning("Lecture de %s impossible : %s", path, exc)
        return "", ""
    return lire_cle(contenu, CLE_DOMAINES), lire_cle(contenu, CLE_TOKEN)


def enregistrer(path: Path, domaines: str, token: str) -> None:
    """
    Écrit le fichier en **0600**, et pose les permissions AVANT d'écrire le
    contenu : créer le fichier puis le restreindre laisse une fenêtre pendant
    laquelle le token est lisible par tout le monde sur la machine.
    """
    path.parent.mkdir(parents=True, exist_ok=True)
    if sys.platform == "win32":
        # Pas de mode POSIX utile ici : %APPDATA% est déjà propre à l'utilisateur.
        path.write_text(rendre(domaines, token), encoding="utf-8")
        return

    descripteur = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(descripteur, "w", encoding="utf-8") as fichier:
        fichier.write(rendre(domaines, token))
    # Le fichier peut préexister avec des permissions plus larges : `O_CREAT` ne
    # les corrige pas.
    os.chmod(path, 0o600)
