"""Windows-Clients aus der Ferne neu starten -- ohne SSH.

Windows-Geraete werden hier **nicht** ueber SSH verwaltet. Sie haben in aller
Regel keinen SSH-Dienst, und damit gibt es auch keinen Host-Schluessel, den
jemand freigeben koennte. Alles, was in TESM mit Host-Schluesseln zu tun hat,
laesst Windows-Geraete deshalb bewusst aus.

Stattdessen der klassische Weg von einem Linux-Host aus: ``net rpc shutdown``
aus ``samba-common-bin``. Das spricht denselben RPC-Dienst an, den auch
``shutdown /m \\\\host`` unter Windows benutzt, und funktioniert mit lokalen
wie mit Domaenenkonten.

Zum Passwort: es geht **nie** ueber die Kommandozeile. ``net -U benutzer%pass``
waere auf dem Zielsystem in ``ps`` sichtbar -- und auch auf diesem hier. Es
wird darum in eine 0600-Datei geschrieben, die nur der Dienstbenutzer lesen
kann, und ueber ``--authentication-file`` uebergeben. Die Datei wird danach
ueberschrieben und geloescht.
"""

from __future__ import annotations

import logging
import os
import shutil
import subprocess
import tempfile
from dataclasses import dataclass

logger = logging.getLogger(__name__)

#: Das Paket, das ``net`` mitbringt. Steht in der Positivliste des Helfers,
#: damit es sich aus der Oberflaeche nachinstallieren laesst.
REQUIRED_PACKAGE = "samba-common-bin"

DEFAULT_TIMEOUT = 45


@dataclass(frozen=True, slots=True)
class RemoteResult:
    ok: bool
    output: str
    hint: str = ""


def available() -> bool:
    """Ist ``net`` auf diesem Host installiert?"""
    return shutil.which("net") is not None


#: Fehlerbilder von ``net rpc``, uebersetzt in etwas Handlungsleitendes.
HINTS: tuple[tuple[str, str], ...] = (
    (
        "NT_STATUS_LOGON_FAILURE",
        "Benutzername oder Passwort wurden vom Windows-Geraet abgewiesen. Bei einem "
        "Domaenenkonto die Form DOMAENE\\benutzer verwenden -- mit *einem* "
        "Backslash.",
    ),
    (
        "NT_STATUS_ACCESS_DENIED",
        "Das Konto darf auf diesem Geraet keinen Neustart ausloesen. Es braucht das Recht "
        "'Herunterfahren von einem Remotesystem aus' -- Mitglied der lokalen "
        "Administratoren genuegt dafuer in der Regel.",
    ),
    (
        "NT_STATUS_CONNECTION_REFUSED",
        "Das Geraet nimmt keine RPC-Verbindung an. Die Datei- und Druckerfreigabe muss "
        "aktiv und Port 445 in der Windows-Firewall freigegeben sein.",
    ),
    (
        "NT_STATUS_IO_TIMEOUT",
        "Keine Antwort auf Port 445. Ist das Geraet erreichbar und die Firewall offen?",
    ),
    (
        "NT_STATUS_HOST_UNREACHABLE",
        "Das Geraet ist nicht erreichbar.",
    ),
    # Diese Meldung liest sich wie ein technisches Detail, hat aber fast immer
    # eine einzige Ursache: das Konto ist auf dem Zielgeraet kein Administrator.
    # Nachgemessen an einem echten Fall -- die Anmeldung gelang, aber schon
    # "net rpc info" und der Dienstmanager antworteten mit ACCESS_DENIED, also
    # bekommt ein Standardkonto die winreg-Pipe gar nicht zu sehen.
    (
        "pipe winreg",
        "Das Konto hat auf diesem Geraet keine Administratorrechte. Ein Neustart ueber "
        "RPC braucht sie: die Pipe, ueber die er laeuft, ist fuer Standardkonten nicht "
        "sichtbar. Ein Konto der lokalen Administratoren hinterlegen -- pruefen laesst "
        "sich das mit 'net rpc service status RemoteRegistry'.",
    ),
    (
        "NT_STATUS_OBJECT_NAME_NOT_FOUND",
        "Der benoetigte RPC-Dienst antwortet auf dem Geraet nicht. Meist fehlen dem "
        "Konto die Administratorrechte; sonst ist der Dienst 'Remoteregistrierung' "
        "gestoppt.",
    ),
)


def hint_for(output: str) -> str:
    for needle, hint in HINTS:
        if needle in output:
            return hint
    return ""


def reboot(
    host: str,
    username: str,
    password: str,
    *,
    domain: str = "",
    comment: str = "Neustart durch TESM-Wartung",
    delay: int = 5,
    timeout: int = DEFAULT_TIMEOUT,
) -> RemoteResult:
    """Loest einen Neustart auf einem Windows-Geraet aus."""
    binary = shutil.which("net")
    if binary is None:
        return RemoteResult(
            False,
            "",
            f"Auf diesem Server fehlt das Paket {REQUIRED_PACKAGE}. Ohne 'net' laesst sich "
            "ein Windows-Geraet von hier nicht neu starten.",
        )
    if not username:
        return RemoteResult(False, "", "Fuer das Geraet sind keine Zugangsdaten hinterlegt.")

    # Ein Benutzer der Form DOMAENE\benutzer bringt seine Domaene selbst mit.
    #
    # Toleranter Split: mehrfache Backslashes werden zusammengefasst, leere
    # Teile verworfen. Das ist kein Selbstzweck -- der Hinweistext hier oben
    # empfahl selbst einmal "DOMAENE\\benutzer" mit zwei Backslashes, und genau
    # so wurde es dann eingetragen. Ein einfacher split() schickte daraufhin
    # "\benutzer" an Windows, und die Anmeldung scheiterte mit LOGON_FAILURE --
    # bei formal richtigen Zugangsdaten.
    if "\\" in username and not domain:
        teile = [teil.strip() for teil in username.split("\\") if teil.strip()]
        if len(teile) >= 2:
            domain, username = teile[0], teile[-1]
        elif teile:
            username = teile[0]

    handle, path = tempfile.mkstemp(prefix="tesm-winauth-")
    try:
        # mkstemp legt die Datei auf POSIX bereits mit 0600 an. Hier trotzdem
        # ausdruecklich -- die Datei traegt gleich ein Klartextpasswort, das
        # will man nicht von einer Vorgabe abhaengig machen.
        try:
            os.chmod(path, 0o600)
        except OSError:  # pragma: no cover - Windows kennt den Modus nicht
            pass
        with os.fdopen(handle, "w", encoding="utf-8", newline="\n") as stream:
            stream.write(f"username={username}\npassword={password}\n")
            if domain:
                stream.write(f"domain={domain}\n")

        command = [
            binary,
            "rpc",
            "shutdown",
            "-r",  # neu starten statt herunterfahren
            "-f",  # laufende Anwendungen nicht blockieren lassen
            "-t",
            str(max(0, int(delay))),
            "-C",
            comment,
            "-I",
            host,
            "--authentication-file",
            path,
        ]
        try:
            completed = subprocess.run(  # noqa: S603 - feste Argumentliste, keine Shell
                command, capture_output=True, text=True, timeout=timeout, check=False
            )
        except subprocess.TimeoutExpired:
            return RemoteResult(False, "", f"Zeitlimit von {timeout}s ueberschritten.")
        except OSError as exc:
            return RemoteResult(False, "", str(exc))

        output = (completed.stdout + completed.stderr).strip()
        if completed.returncode == 0:
            return RemoteResult(True, output or "Neustart ausgeloest.")
        return RemoteResult(False, output, hint_for(output))
    finally:
        # Ueberschreiben, dann loeschen -- die Datei enthaelt ein Klartextpasswort.
        try:
            with open(path, "r+b") as stream:
                length = stream.seek(0, os.SEEK_END)
                stream.seek(0)
                stream.write(b"\0" * length)
                stream.flush()
                os.fsync(stream.fileno())
        except OSError:  # pragma: no cover - defensiv
            logger.warning("Anmeldedatei konnte nicht ueberschrieben werden: %s", path)
        try:
            os.unlink(path)
        except OSError:  # pragma: no cover - defensiv
            pass
