"""
Backend Windows de l'hébergement du serveur de signalisation.

**Pourquoi pas un vrai service Windows.** `sc create` accepte n'importe quel
chemin d'exécutable, mais un service doit dialoguer avec le Service Control
Manager dans les premières secondes ; un exécutable console ordinaire ne le fait
pas et le démarrage échoue avec l'erreur 1053 (« le service n'a pas répondu à
temps »). Rendre `MeshamotoServer.exe` conforme imposerait `pywin32` et une
refonte du `.spec`, donc une dépendance de plus — ce que le projet refuse par
principe. Le Planificateur de tâches est intégré à Windows, ne demande aucune
dépendance, et tient exactement la même promesse.

**Deux mécanismes, pas un** — dissymétrie assumée avec Linux, où `systemd`
couvre les deux besoins à lui seul :
  - démarrage immédiat : l'exécutable est lancé **détaché**, pour qu'il survive
    à la fermeture de la fenêtre Meshamoto ;
  - démarrage automatique : une tâche planifiée `ONSTART` exécutée par SYSTEM,
    donc au démarrage de la machine et sans qu'une session soit ouverte.

Le compte SYSTEM est choisi pour ne demander aucun mot de passe : une tâche
`/ru <utilisateur>` exigerait de le saisir et de le stocker, ce qui serait à la
fois hostile et pire du point de vue sécurité. Le client tourne déjà élevé
(`uac_admin=True` dans le `.spec`), donc la création de la tâche ne déclenche
aucune invite supplémentaire.
"""

from __future__ import annotations

import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path

from server_service_common import (
    ServiceStatus,
    parse_schtasks_xml,
    parse_tasklist,
    status_from_windows,
)

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

TASK_NAME = "Meshamoto Server"
EXECUTABLE_NAME = "MeshamotoServer.exe"

_TIMEOUT_SECONDS = 20

# Empêche l'apparition d'une fenêtre de console noire à chaque appel : le client
# est une GUI, et ces commandes sont fréquentes (rafraîchissement de l'état).
_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)

# Détache complètement le serveur du processus GUI, sans quoi il mourrait avec
# lui — ce qui viderait de son sens la promesse « le serveur continue de
# tourner quand vous fermez Meshamoto ».
_DETACHED = getattr(subprocess, "DETACHED_PROCESS", 0) | getattr(
    subprocess, "CREATE_NEW_PROCESS_GROUP", 0
)


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


def server_executable_path() -> Path | None:
    """
    Localise `MeshamotoServer.exe`, ou None s'il est absent.

    Cherché à côté du client : c'est la disposition du `.exe` portable, qui est
    le mode de distribution retenu pour Windows. Un chemin explicite reste
    possible par variable d'environnement, pour une installation qui range les
    deux exécutables séparément.
    """
    override = os.environ.get("MESHAMOTO_SERVER_EXE")
    if override:
        candidate = Path(override)
        return candidate if candidate.is_file() else None

    # Figé par PyInstaller : sys.executable est le .exe du client lui-même.
    # Depuis les sources, c'est l'interpréteur Python — le dossier du module
    # est alors le repère utile.
    if getattr(sys, "frozen", False):
        roots = [Path(sys.executable).parent]
    else:
        roots = [Path(__file__).resolve().parent]

    for root in roots:
        candidate = root / EXECUTABLE_NAME
        if candidate.is_file():
            return candidate
    return None


def _schtasks() -> str | None:
    return shutil.which("schtasks")


def _task_state() -> tuple[bool, bool]:
    """(tâche présente, activée). Absente si `schtasks` est introuvable."""
    schtasks = _schtasks()
    if schtasks is None:
        return (False, False)
    # `/xml ONE` et non `/fo LIST` : c'est le seul format que Windows ne traduit
    # pas. Voir `parse_schtasks_xml` — un Windows français écrit « Désactivé »,
    # et l'analyse de l'affichage faisait passer une tâche désactivée pour active.
    result = _run([schtasks, "/query", "/tn", TASK_NAME, "/xml", "ONE"])
    if result.returncode != 0:
        # Code non nul = tâche inexistante, cas normal tant que le démarrage
        # automatique n'a jamais été activé.
        return (False, False)
    return parse_schtasks_xml(result.stdout)


def _process_running() -> bool:
    tasklist = shutil.which("tasklist")
    if tasklist is None:
        return False
    result = _run([tasklist, "/fi", f"imagename eq {EXECUTABLE_NAME}"])
    return parse_tasklist(result.stdout, EXECUTABLE_NAME)


def status() -> ServiceStatus:
    task_present, task_enabled = _task_state()
    return status_from_windows(
        task_present=task_present,
        task_enabled=task_enabled,
        process_running=_process_running(),
        executable_found=server_executable_path() is not None,
    )


def _require_executable() -> Path:
    executable = server_executable_path()
    if executable is None:
        raise RuntimeError(
            f"{EXECUTABLE_NAME} est introuvable à côté du client. Placez-le dans le "
            "même dossier que Meshamoto.exe pour pouvoir héberger un serveur."
        )
    return executable


def _require_schtasks() -> str:
    schtasks = _schtasks()
    if schtasks is None:
        raise RuntimeError(
            "schtasks est introuvable : impossible de configurer le démarrage "
            "automatique sur ce système."
        )
    return schtasks


def start() -> None:
    if _process_running():
        logger.info("Le serveur tourne déjà : démarrage ignoré.")
        return
    executable = _require_executable()
    # Pas de capture des flux : ce processus doit survivre au client, or un tube
    # capturé se remplit puis bloque l'écriture dès que personne ne le lit.
    subprocess.Popen(  # noqa: S603 - chemin validé ci-dessus, aucun argument utilisateur
        [str(executable)],
        cwd=str(executable.parent),
        creationflags=_DETACHED | _NO_WINDOW,
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        close_fds=True,
    )
    logger.info("Serveur de signalisation démarré (%s).", executable)


def stop() -> None:
    if not _process_running():
        logger.info("Le serveur ne tourne pas : arrêt ignoré.")
        return
    taskkill = shutil.which("taskkill")
    if taskkill is None:
        raise RuntimeError("taskkill est introuvable : impossible d'arrêter le serveur.")
    result = _run([taskkill, "/im", EXECUTABLE_NAME, "/f"])
    if result.returncode != 0:
        message = result.stderr.strip() or result.stdout.strip() or f"code {result.returncode}"
        raise RuntimeError(f"L'arrêt du serveur a échoué : {message}")
    logger.info("Serveur de signalisation arrêté.")


def set_autostart(enabled: bool) -> None:
    schtasks = _require_schtasks()
    if not enabled:
        result = _run([schtasks, "/delete", "/tn", TASK_NAME, "/f"])
        # Supprimer une tâche absente n'est pas une erreur du point de vue de
        # l'utilisateur : le résultat voulu est atteint.
        if result.returncode != 0 and _task_state()[0]:
            message = result.stderr.strip() or f"code {result.returncode}"
            raise RuntimeError(f"La désactivation du démarrage automatique a échoué : {message}")
        logger.info("Démarrage automatique du serveur : désactivé")
        return

    executable = _require_executable()
    result = _run([
        schtasks, "/create", "/tn", TASK_NAME,
        "/tr", f'"{executable}"',
        "/sc", "onstart",
        "/ru", "SYSTEM",
        "/rl", "HIGHEST",
        "/f",  # remplace une tâche existante plutôt que d'échouer
    ])
    if result.returncode != 0:
        message = result.stderr.strip() or result.stdout.strip() or f"code {result.returncode}"
        raise RuntimeError(f"La configuration du démarrage automatique a échoué : {message}")
    logger.info("Démarrage automatique du serveur : activé")


def recent_logs(lines: int = 40) -> str:
    """
    Windows n'a pas d'équivalent de `journalctl` pour un processus ordinaire :
    le serveur écrit sur sa console, et il n'y en a pas quand il est lancé
    détaché ou par le Planificateur.

    On le dit plutôt que de renvoyer un journal vide qui laisserait croire que
    le serveur n'a rien à raconter — même choix que pour le diagnostic de
    handshake WireGuard, indisponible sous Windows et signalé comme tel.
    """
    del lines
    return (
        "Le journal détaillé n'est pas disponible sous Windows : le serveur tourne "
        "sans console rattachée. L'état ci-dessus (démarré / arrêté) reste fiable."
    )
