#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
"""autoloadctl — jetons clients du provisionnement Auto-Load (#2185). Réservé à root sur l'infrastructure.

  autoloadctl emettre --client C --profil P [--lot L] [--serie S] [--duree-jours N]   imprime le jeton UNE fois
  autoloadctl preenregistrer --serie S --client C --profil P [--lot L]
  autoloadctl lister [--etat E] [--client C] [--lot L] [--json]
  autoloadctl revoquer (--id N | --lot L) --motif M
  autoloadctl abonnement --client C --statut actif|suspendu|revoque
  autoloadctl tunnel-init                  crée la clé du hub (0600) et affiche sa clé publique
  autoloadctl tunnel-sync                  régénère wg-autoload.conf depuis les pairs actifs et l'applique (wg syncconf)
"""
import argparse
import json
import os
import pwd
import subprocess
import sys
import time
from pathlib import Path

sys.path.insert(0, "/usr/lib/secubox/autoload")
_ici = Path(__file__).resolve().parents[1]
if (_ici / "autoload").is_dir():
    sys.path.insert(0, str(_ici))

from autoload import jetons as J, tunnel as T  # noqa: E402


UTILISATEUR = "secubox-autoload"


def _abaisser_privileges():
    """Lancé en root, les commandes du registre tournent SOUS L'UTILISATEUR DU SERVICE : la base et le journal d'audit gardent leur propriétaire
    (une base créée par root ne serait plus ouvrable par le service). tunnel-init et tunnel-sync, elles, ont besoin de root."""
    if os.geteuid() != 0:
        return
    try:
        pw = pwd.getpwnam(UTILISATEUR)
    except KeyError:
        return
    os.initgroups(UTILISATEUR, pw.pw_gid)
    os.setgid(pw.pw_gid)
    os.setuid(pw.pw_uid)


class _PairsVides:
    def actifs(self):
        return []


def main(argv=None):
    ap = argparse.ArgumentParser(prog="autoloadctl", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = ap.add_subparsers(dest="cmd", required=True)
    e = sub.add_parser("emettre")
    e.add_argument("--client", required=True)
    e.add_argument("--profil", required=True)
    e.add_argument("--lot")
    e.add_argument("--serie")
    e.add_argument("--duree-jours", type=int, default=J.DUREE_DEFAUT_S // 86400)
    p = sub.add_parser("preenregistrer")
    p.add_argument("--serie", required=True)
    p.add_argument("--client", required=True)
    p.add_argument("--profil", required=True)
    p.add_argument("--lot")
    li = sub.add_parser("lister")
    li.add_argument("--etat")
    li.add_argument("--client")
    li.add_argument("--lot")
    li.add_argument("--json", action="store_true")
    r = sub.add_parser("revoquer")
    g = r.add_mutually_exclusive_group(required=True)
    g.add_argument("--id", type=int)
    g.add_argument("--lot")
    r.add_argument("--motif", required=True)
    ab = sub.add_parser("abonnement")
    ab.add_argument("--client", required=True)
    ab.add_argument("--statut", required=True)
    sub.add_parser("tunnel-init")
    sub.add_parser("tunnel-sync")
    a = ap.parse_args(argv)

    db = Path(os.environ.get("SECUBOX_AUTOLOAD_DB", J.DB_DEFAUT))
    if a.cmd not in ("tunnel-init", "tunnel-sync"):
        _abaisser_privileges()
        reg = J.Registre(db, Path(os.environ.get("SECUBOX_AUTOLOAD_AUDIT", J.AUDIT_DEFAUT)))
        pairs = T.Pairs(reg.db)
    try:
        if a.cmd == "emettre":
            em = reg.emettre(a.client, a.profil, lot=a.lot, serie=a.serie, duree_s=a.duree_jours * 86400)
            print(f"jeton {em.id}, expire le {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(em.expire_le))} — la valeur ne sera plus affichée :", file=sys.stderr)
            print(em.valeur)
        elif a.cmd == "preenregistrer":
            reg.preenregistrer(a.serie, a.client, a.profil, lot=a.lot)
            print("série préenregistrée")
        elif a.cmd == "lister":
            lignes = reg.lister(a.etat, a.client, a.lot)
            if a.json:
                print(json.dumps(lignes, ensure_ascii=False, indent=1))
            else:
                for x in lignes:
                    print(f"{x['id']:>5}  {x['etat']:<8} {x['abonnement']:<8} {x['client']:<20} {x['profil']:<8} {x['lot'] or '-'}")
        elif a.cmd == "revoquer":
            if a.id is not None:
                cle = reg.revoquer(a.id, a.motif)
                retire = pairs.retirer(cle)
                print("révoqué" + (" ; pair retiré (lancer tunnel-sync pour l'appliquer)" if retire else ""))
            else:
                cles = reg.revoquer_lot_cles(a.lot, a.motif)
                retires = sum(1 for c in cles if pairs.retirer(c))
                print(f"lot révoqué ; {retires} pair(s) retiré(s)" + (" (lancer tunnel-sync pour l'appliquer)" if retires else ""))
        elif a.cmd == "tunnel-init":
            cle = Path(os.environ.get("SECUBOX_AUTOLOAD_CLE_HUB", T.CLE_HUB_DEFAUT))
            if cle.exists():
                print("clé du hub déjà présente (inchangée)", file=sys.stderr)
            else:
                cle.parent.mkdir(parents=True, exist_ok=True)
                g = subprocess.run(["wg", "genkey"], capture_output=True, text=True, timeout=30, check=True)
                fd = os.open(cle, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
                with os.fdopen(fd, "w") as f:
                    f.write(g.stdout.strip() + "\n")
            pub = subprocess.run(["wg", "pubkey"], input=cle.read_text().strip() + "\n", capture_output=True, text=True, timeout=30, check=True)
            pub_cle = pub.stdout.strip()
            (db.parent / "hub.pub").write_text(pub_cle + "\n", encoding="ascii")                 # lue par le service pour répondre aux box
            os.chmod(db.parent / "hub.pub", 0o644)
            print(pub_cle)
        elif a.cmd == "tunnel-sync":
            pairs_root = T.Pairs(db, lecture_seule=True) if db.exists() else _PairsVides()          # root ne CRÉE jamais la base : elle appartient au service
            n = T.synchroniser(pairs_root, Path(os.environ.get("SECUBOX_AUTOLOAD_CONF_WG", T.CONF_DEFAUT)), Path(os.environ.get("SECUBOX_AUTOLOAD_CLE_HUB", T.CLE_HUB_DEFAUT)))
            print(f"{n} pair(s) appliqué(s)")
        elif a.cmd == "abonnement":
            print(f"{reg.fixer_abonnement(a.client, a.statut)} jeton(s) du client")
    except T.TunnelErreur as err:
        print(f"tunnel : {err}", file=sys.stderr)
        return 1
    except ValueError as err:
        print(f"refusé : {err}", file=sys.stderr)
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())
