"""Privilegierte Systemaktionen ueber einen einzigen, engen Helfer.

Der Vorgaenger lief als ``root`` -- inklusive Flask, Jinja, Template-Rendering,
Datei-Uploads und LDAP-Client. Eine einzige Luecke in irgendeinem dieser Pfade
haette vollstaendige Kontrolle ueber den Host bedeutet. Die eigene
Dokumentation vermerkte das als "bewusst, mit Verbesserungshinweis".

Hier ist der Verbesserungshinweis umgesetzt:

* Der Webprozess laeuft als unprivilegierter Dienstbenutzer.
* Alles, was Root braucht (systemctl, nginx, netplan, hostnamectl, mount.cifs,
  apt, certbot), laeuft ueber **ein** Skript: ``tesm-helper``. Es gehoert root,
  ist per ``sudoers`` mit ``NOPASSWD`` nur fuer genau diesen Pfad erlaubt und
  validiert jedes Argument gegen eine Positivliste.
* Die Sicherheitsgrenze ist damit klein, an einer Stelle und pruefbar -- statt
  ueber Tausende Zeilen Anwendungscode verteilt.

Lesende Systemabfragen (``systemctl is-active``, ``ip addr``) brauchen keine
Rechte und laufen direkt. Nur Aenderungen gehen ueber den Helfer.
"""

from __future__ import annotations

import logging
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Sequence

logger = logging.getLogger(__name__)

DEFAULT_HELPER = Path("/usr/local/lib/tesm/tesm-helper")
#: Verben, die der Helfer kennt. Muss mit dem Skript in ``deploy/`` uebereinstimmen.
HELPER_VERBS = (
    "service-status",
    "service-start",
    "service-stop",
    "service-restart",
    "service-enable",
    "service-disable",
    "nginx-test",
    "nginx-reload",
    "nginx-write",
    "nginx-apply",
    "netplan-write",
    "netplan-apply",
    "hostname-set",
    "timezone-set",
    "kea-write",
    "logrotate-write",
    "mount-cifs",
    "umount",
    "apt-install",
    "certbot-issue",
    "selfsigned-issue",
    "file-read",
)


@dataclass(frozen=True, slots=True)
class Result:
    ok: bool
    stdout: str = ""
    stderr: str = ""
    code: int = 0

    @property
    def output(self) -> str:
        return (self.stdout + ("\n" + self.stderr if self.stderr else "")).strip()

    @property
    def message(self) -> str:
        return self.stderr.strip() or self.stdout.strip()


UNAVAILABLE = Result(
    ok=False,
    stderr=(
        "Systemaktionen sind in dieser Umgebung nicht verfuegbar "
        "(Helfer nicht installiert oder kein Linux-Host)."
    ),
    code=127,
)


class SysOps:
    """Zugang zu privilegierten Aktionen. In der Entwicklung ein No-Op."""

    def __init__(
        self,
        *,
        app_key: str,
        site_key: str = "",
        helper: Path | None = None,
        timeout: int = 120,
        enabled: bool = True,
    ) -> None:
        self.app_key = app_key
        #: Name dieser Installation auf dem Host. Alles, was ausserhalb von
        #: /srv/<name> liegt, wird darueber benannt -- sonst schreibt eine
        #: Testinstanz in die Systemdateien der Produktion.
        self.site_key = site_key or app_key
        self.helper = Path(helper or os.environ.get("TESM_HELPER", DEFAULT_HELPER))
        self.timeout = timeout
        self.enabled = enabled

    # ------------------------------------------------------------------ #

    @property
    def available(self) -> bool:
        """Ist der Helfer *vorhanden*? Sagt noch nichts darueber, ob er laeuft."""
        return self.enabled and self.helper.is_file() and os.name == "posix"

    def selftest(self) -> Result:
        """Ruft den Helfer wirklich auf -- die einzige ehrliche Auskunft.

        Dass die Datei existiert, hat nichts zu bedeuten. Auf beiden Testhosts
        stand monatelang "Helfer erreichbar", waehrend jeder Aufruf an
        ``NoNewPrivileges`` scheiterte und damit jede Systemaktion tot war.
        Geprueft wird ``service-status nginx``: der Helfer beantwortet das
        immer, egal wie es dem Dienst geht. ``nginx-test`` waere der falsche
        Kandidat -- sein Fehlschlag hiesse "Konfiguration kaputt", nicht
        "Helfer kaputt", und die Seite meldete dann das Falsche.
        """
        if not self.available:
            return Result(False, "", f"Der Helfer {self.helper} fehlt.", 1)
        result = self.run("service-status", "nginx")
        if not result.ok and "no new privileges" in result.output.lower():
            return Result(
                False,
                "",
                "sudo wird durch 'NoNewPrivileges' der systemd-Unit blockiert. Ohne sudo "
                "kann der Dienst den Helfer nicht aufrufen -- keine Systemaktion "
                "funktioniert. In der Unit 'NoNewPrivileges=yes' entfernen und "
                "'systemctl daemon-reload && systemctl restart' ausfuehren.",
                result.code,
            )
        if not result.ok and "a password is required" in result.output.lower():
            return Result(
                False,
                "",
                "sudo verlangt ein Passwort. Die sudoers-Regel fuer den Helfer fehlt oder "
                "greift nicht.",
                result.code,
            )
        return result

    def _sudo_prefix(self) -> list[str]:
        if os.geteuid() == 0 if hasattr(os, "geteuid") else False:
            return []
        sudo = shutil.which("sudo")
        return [sudo, "-n"] if sudo else []

    def run(self, verb: str, *args: str, stdin: str = "", timeout: int | None = None) -> Result:
        """Ruft den Helfer auf. Argumente werden nie durch eine Shell geschickt."""
        if verb not in HELPER_VERBS:
            raise ValueError(f"Unbekanntes Helfer-Verb: {verb!r}")
        if not self.available:
            return UNAVAILABLE
        command = [*self._sudo_prefix(), str(self.helper), verb, *[str(a) for a in args]]
        try:
            completed = subprocess.run(  # noqa: S603 - feste Argumentliste, keine Shell
                command,
                input=stdin,
                capture_output=True,
                text=True,
                timeout=timeout or self.timeout,
                check=False,
            )
        except subprocess.TimeoutExpired:
            return Result(False, stderr=f"Zeitlimit ueberschritten ({verb}).", code=124)
        except OSError as exc:
            return Result(False, stderr=str(exc), code=126)
        return Result(
            ok=completed.returncode == 0,
            stdout=completed.stdout or "",
            stderr=completed.stderr or "",
            code=completed.returncode,
        )

    # ------------------------------------------------------------------ #
    # Lesende Abfragen -- ohne Rechte, direkt
    # ------------------------------------------------------------------ #

    @staticmethod
    def read_command(command: Sequence[str], *, timeout: int = 10) -> Result:
        """Fuehrt eine rein lesende Systemabfrage aus."""
        if not command:
            return Result(False, stderr="Leeres Kommando", code=2)
        binary = shutil.which(command[0])
        if binary is None:
            return Result(False, stderr=f"{command[0]} ist nicht installiert.", code=127)
        try:
            completed = subprocess.run(  # noqa: S603
                [binary, *command[1:]],
                capture_output=True,
                text=True,
                timeout=timeout,
                check=False,
            )
        except subprocess.TimeoutExpired:
            return Result(False, stderr="Zeitlimit ueberschritten.", code=124)
        except OSError as exc:
            return Result(False, stderr=str(exc), code=126)
        return Result(
            ok=completed.returncode == 0,
            stdout=completed.stdout or "",
            stderr=completed.stderr or "",
            code=completed.returncode,
        )

    def service_state(self, unit: str) -> dict[str, str]:
        """``{"active": ..., "enabled": ..., "installed": ...}`` fuer eine Unit."""
        installed = shutil.which("systemctl") is not None
        if not installed:
            return {"active": "unbekannt", "enabled": "unbekannt", "installed": "nein"}
        active = self.read_command(["systemctl", "is-active", unit]).stdout.strip() or "unknown"
        enabled = self.read_command(["systemctl", "is-enabled", unit]).stdout.strip() or "unknown"
        # ``list-unit-files`` erwartet ein Muster **mit** Endung: fuer
        # "kea-dhcp4-server" liefert es nichts, fuer
        # "kea-dhcp4-server.service" die Zeile. Ohne diese Ergaenzung stand
        # ueberall "installiert: nein" -- auch bei laufendem Dienst, was zum
        # Installieren-Knopf verleitet hat.
        pattern = unit if "." in unit.rsplit("/", 1)[-1] else f"{unit}.service"
        exists = self.read_command(
            ["systemctl", "list-unit-files", pattern, "--no-legend"]
        ).stdout.strip()
        return {
            "active": active,
            "enabled": enabled,
            "installed": "ja" if exists else "nein",
        }

    # ------------------------------------------------------------------ #
    # Bequeme Wrapper
    # ------------------------------------------------------------------ #

    def service(self, action: str, unit: str) -> Result:
        verb = f"service-{action}"
        return self.run(verb, unit)

    def nginx_test(self) -> Result:
        return self.run("nginx-test")

    def nginx_reload(self) -> Result:
        return self.run("nginx-reload")

    def write_nginx(self, content: str) -> Result:
        return self.run("nginx-write", self.site_key, stdin=content)

    def apply_nginx(self, content: str) -> Result:
        """Schreiben, pruefen und neu laden -- als eine Operation mit Ruecknahme.

        Bewusst nicht als drei Aufrufe: ``nginx-write`` aktiviert die Site
        sofort. Zwischen Schreiben und einem fehlgeschlagenen ``nginx -t`` lag
        sonst ein Zeitfenster, in dem eine kaputte Konfiguration aktiviert war
        -- der naechste Reload durch irgendwen haette den ganzen Webserver
        lahmgelegt, auch fuer alle anderen Sites.
        """
        return self.run("nginx-apply", self.site_key, stdin=content, timeout=60)

    def write_netplan(self, content: str) -> Result:
        return self.run("netplan-write", self.site_key, stdin=content)

    def apply_netplan(self) -> Result:
        return self.run("netplan-apply", timeout=90)

    def set_hostname(self, hostname: str) -> Result:
        return self.run("hostname-set", hostname)

    def set_timezone(self, timezone: str) -> Result:
        return self.run("timezone-set", timezone)

    def write_kea_config(self, content: str) -> Result:
        return self.run("kea-write", stdin=content)

    def write_logrotate(self, content: str) -> Result:
        return self.run("logrotate-write", self.site_key, stdin=content)

    def mount_cifs(self, unc: str, target: str, username: str, password: str, domain: str = "") -> Result:
        # Das Passwort geht ueber stdin an den Helfer, der es in eine
        # 0600-Credentials-Datei schreibt. Nie als Argument (Prozessliste!),
        # nie ueber die Umgebung (/proc/<pid>/environ).
        payload = f"username={username}\npassword={password}\n" + (
            f"domain={domain}\n" if domain else ""
        )
        return self.run("mount-cifs", unc, target, stdin=payload, timeout=45)

    def umount(self, target: str) -> Result:
        return self.run("umount", target, timeout=45)

    def issue_self_signed(self, common_name: str, *alt_names: str) -> Result:
        """Erzeugt ein selbstsigniertes Zertifikat fuer diese Installation.

        Fuer Geraete in geschlossenen Netzen ohne oeffentliche Domain -- dort
        gibt es kein Let's Encrypt, und ohne Zertifikat gaebe es kein HTTPS.
        """
        return self.run("selfsigned-issue", self.site_key, common_name, *alt_names, timeout=90)

    def apt_install(self, *packages: str) -> Result:
        return self.run("apt-install", *packages, timeout=600)

    def certbot_issue(self, domain: str, email: str, webroot: str) -> Result:
        return self.run("certbot-issue", domain, email, webroot, timeout=300)

    def read_privileged_file(self, path: str) -> Result:
        """Liest eine Datei, die dem Dienstbenutzer nicht gehoert (z. B. Kea-Log)."""
        return self.run("file-read", path)
