"""
Backend Linux de l'hébergement du serveur de signalisation : `systemd --user`.

**Aucun privilège nulle part.** Le port de signalisation est supérieur à 1024,
donc une unité utilisateur suffit : ni pkexec, ni composant privilégié, ni
invite de mot de passe. C'est ce qui rend ce chantier bien plus simple que le
tunnel ou le pare-feu, et c'est la raison pour laquelle il a été retenu en
premier (CLAUDE.md point 1ter-a).

L'unité elle-même est installée par le paquet dans `/usr/lib/systemd/user/`,
pas écrite par le client : elle pointe sur un chemin fixe (`/usr/bin/
meshamoto-server`). Une unité générée par la session pointerait sur un chemin
choisi à l'exécution, ce qui redonnerait à l'interface le pouvoir de faire
lancer un exécutable arbitraire au démarrage — même raisonnement que le refus
du `.conf` fabriqué par la session au point 38, en moins grave puisque tout
reste ici dans les droits de l'utilisateur.
"""

from __future__ import annotations

import getpass
import logging
import os
import shutil
import subprocess

from server_service_common import (
    ServiceStatus,
    parse_linger,
    parse_systemctl_show,
    status_from_systemctl,
)

logger = logging.getLogger("meshamoto-client.server_service.linux")

UNIT_NAME = "meshamoto-server.service"

# Ces commandes sont locales et non privilégiées : elles répondent en quelques
# dizaines de millisecondes. Un délai généreux ne coûte donc rien et protège
# d'un systemd occupé au démarrage de session.
_TIMEOUT_SECONDS = 15


def _run(command: list[str]) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        command, capture_output=True, text=True, timeout=_TIMEOUT_SECONDS, check=False
    )


def _systemctl() -> str | None:
    return shutil.which("systemctl")


def _depuis_appimage() -> bool:
    """
    `APPIMAGE` est posée par le runtime AppImage et contient le chemin du fichier
    lancé. C'est la détection standard, et la seule fiable : `sys.executable`
    pointe dans un montage temporaire dont le nom change à chaque lancement, il
    ne dit donc rien sur la façon dont l'application a été distribuée.
    """
    return bool(os.environ.get("APPIMAGE"))


def status() -> ServiceStatus:
    """
    État de l'unité utilisateur, plus la vérification du linger.

    Le linger est traité comme une condition du démarrage automatique, pas comme
    un réglage séparé : sans lui l'unité est bien « enabled » mais s'arrête à la
    déconnexion, donc la promesse faite à l'utilisateur serait fausse alors que
    tous les voyants seraient au vert.
    """
    systemctl = _systemctl()
    if systemctl is None:
        return ServiceStatus(
            installed=False,
            running=False,
            autostart=False,
            detail="systemd est introuvable : l'hébergement automatique n'est pas "
                   "disponible sur ce système.",
        )

    result = _run([
        systemctl, "--user", "show", UNIT_NAME,
        "--property=LoadState", "--property=ActiveState", "--property=UnitFileState",
    ])
    state = status_from_systemctl(
        parse_systemctl_show(result.stdout), depuis_appimage=_depuis_appimage()
    )

    if state.autostart and not _linger_enabled():
        # Volontairement rétrogradé : « démarre au boot » n'est vrai qu'avec le
        # linger, et l'utilisateur n'a aucun moyen de deviner cette subtilité.
        return ServiceStatus(
            installed=state.installed,
            running=state.running,
            autostart=False,
            detail=state.detail + " Le démarrage automatique est incomplet "
                                  "(session utilisateur non persistante).",
        )
    return state


def _linger_enabled() -> bool:
    loginctl = shutil.which("loginctl")
    if loginctl is None:
        return False
    result = _run([loginctl, "show-user", getpass.getuser(), "-p", "Linger"])
    return parse_linger(result.stdout)


def _enable_linger() -> None:
    """
    Rend la session utilisateur persistante, sinon l'unité s'arrête à la
    déconnexion.

    Mesuré sur cette machine : polkit accorde `set-self-linger` en
    `allow_active`, donc l'opération passe **sans mot de passe** pour
    l'utilisateur de la session graphique. On n'échoue pas si elle est refusée
    (poste administré autrement) : le statut affichera simplement que le
    démarrage automatique est incomplet, ce qui est exact et exploitable.
    """
    loginctl = shutil.which("loginctl")
    if loginctl is None:
        return
    result = _run([loginctl, "enable-linger", getpass.getuser()])
    if result.returncode != 0:
        logger.warning(
            "Activation du linger refusée (%s) : le serveur s'arrêtera à la déconnexion.",
            result.stderr.strip() or f"code {result.returncode}",
        )


def _require_systemctl() -> str:
    systemctl = _systemctl()
    if systemctl is None:
        raise RuntimeError(
            "systemctl est introuvable : impossible de piloter le serveur depuis "
            "l'interface sur ce système."
        )
    return systemctl


def _fail(action: str, result: subprocess.CompletedProcess[str]) -> None:
    message = result.stderr.strip() or result.stdout.strip() or f"code {result.returncode}"
    raise RuntimeError(f"{action} a échoué : {message}")


def start() -> None:
    if status().running:
        logger.info("Le serveur tourne déjà : démarrage ignoré.")
        return
    systemctl = _require_systemctl()
    result = _run([systemctl, "--user", "start", UNIT_NAME])
    if result.returncode != 0:
        _fail("Le démarrage du serveur", result)
    logger.info("Serveur de signalisation démarré (%s).", UNIT_NAME)


def stop() -> None:
    systemctl = _require_systemctl()
    result = _run([systemctl, "--user", "stop", UNIT_NAME])
    if result.returncode != 0:
        _fail("L'arrêt du serveur", result)
    logger.info("Serveur de signalisation arrêté.")


def set_autostart(enabled: bool) -> None:
    systemctl = _require_systemctl()
    if enabled:
        # Le linger d'abord : si l'activation de l'unité réussit et que le linger
        # échoue, l'utilisateur repart avec une promesse à moitié tenue. Dans cet
        # ordre, le statut recalculé ensuite dit la vérité dans tous les cas.
        _enable_linger()
    result = _run([systemctl, "--user", "enable" if enabled else "disable", UNIT_NAME])
    if result.returncode != 0:
        _fail("La configuration du démarrage automatique", result)
    logger.info("Démarrage automatique du serveur : %s", "activé" if enabled else "désactivé")


def recent_logs(lines: int = 40) -> str:
    journalctl = shutil.which("journalctl")
    if journalctl is None:
        return "journalctl est introuvable : journal indisponible."
    result = _run([
        journalctl, "--user", "-u", UNIT_NAME, "-n", str(lines), "--no-pager", "-o", "cat",
    ])
    if result.returncode != 0:
        return result.stderr.strip() or "Journal indisponible."
    return result.stdout.strip() or "Aucune entrée de journal pour le moment."
