"""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 split_account(username: str, domain: str = "") -> tuple[str, str]:
    """Trennt ``DOMAENE\\benutzer`` in Benutzer und Domaene.

    Toleranter Split: mehrfache Backslashes werden zusammengefasst, leere Teile
    verworfen. Das ist kein Selbstzweck -- der Hinweistext dieser Datei 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.

    Rueckgabe: ``(benutzer, domaene)``.
    """
    if "\\" in username and not domain:
        teile = [teil.strip() for teil in username.split("\\") if teil.strip()]
        if len(teile) >= 2:
            return teile[-1], teile[0]
        if teile:
            return teile[0], domain
    return username, domain


def _net_with_credentials(
    args: list[str],
    *,
    username: str,
    password: str,
    domain: str,
    timeout: int,
) -> RemoteResult | tuple[int, str]:
    """Ruft ``net`` mit einer 0600-Anmeldedatei auf und raeumt sie sicher weg.

    Gemeinsam fuer Neustart und Pruefung -- die Datei traegt ein
    Klartextpasswort, und diese Behandlung will man genau einmal haben.
    Rueckgabe: ``(exitcode, ausgabe)`` oder ein fertiges ``RemoteResult`` bei
    einem Fehler noch vor dem Aufruf.
    """
    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 ansprechen.",
        )
    if not username:
        return RemoteResult(False, "", "Es sind keine Zugangsdaten hinterlegt.")

    username, domain = split_account(username, domain)
    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, *args, "--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))
        return completed.returncode, (completed.stdout + completed.stderr).strip()
    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


def check(
    host: str,
    username: str,
    password: str,
    *,
    domain: str = "",
    timeout: int = DEFAULT_TIMEOUT,
) -> RemoteResult:
    """Prueft Zugangsdaten gegen ein Windows-Geraet -- **ohne** etwas zu aendern.

    ``net rpc info`` ist rein lesend. Der Sinn: ob Zugangsdaten stimmen, soll
    man erfahren, ohne einen Neustart auszuloesen und hinterher im Protokoll zu
    raten. Unterschieden wird ausdruecklich zwischen "Passwort falsch" und
    "Anmeldung in Ordnung, aber keine Administratorrechte" -- das sind zwei
    voellig verschiedene Aufgaben fuer den, der es beheben muss.

    Ein Fehlversuch zaehlt fuer die Sperrrichtlinie der Domaene mit. Deshalb
    genau ein Aufruf je Klick und kein automatisches Wiederholen.
    """
    ergebnis = _net_with_credentials(
        ["rpc", "info", "-I", host],
        username=username,
        password=password,
        domain=domain,
        timeout=timeout,
    )
    if isinstance(ergebnis, RemoteResult):
        return ergebnis
    code, output = ergebnis

    if code == 0:
        return RemoteResult(
            True, output, "Anmeldung erfolgreich, Administratorrechte vorhanden."
        )
    if "ACCESS_DENIED" in output:
        # Wichtig: die Anmeldung *hat* funktioniert. Nur die Rechte fehlen.
        return RemoteResult(
            False,
            output,
            "Anmeldung erfolgreich, aber das Konto hat auf diesem Geraet keine "
            "Administratorrechte. Fuer einen Neustart ueber RPC braucht es sie.",
        )
    return RemoteResult(False, output, hint_for(output) or "Pruefung fehlgeschlagen.")


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."""
    ergebnis = _net_with_credentials(
        [
            "rpc",
            "shutdown",
            "-r",  # neu starten statt herunterfahren
            "-f",  # laufende Anwendungen nicht blockieren lassen
            "-t",
            str(max(0, int(delay))),
            "-C",
            comment,
            "-I",
            host,
        ],
        username=username,
        password=password,
        domain=domain,
        timeout=timeout,
    )
    if isinstance(ergebnis, RemoteResult):
        return ergebnis
    code, output = ergebnis
    if code == 0:
        return RemoteResult(True, output or "Neustart ausgeloest.")
    return RemoteResult(False, output, hint_for(output))
