"""Welche Neustartwege ein Geraet hat -- und welcher davon der beste ist.

Ein Geraet kann auf mehreren Wegen neu startbar sein: ueber den PoE-Port des
Switches, ueber SSH, oder bei Windows ueber RPC. Bisher gab es nur den ersten,
und der Knopf verschwand einfach, wenn Switch oder Port fehlten -- ohne zu
sagen, dass ein anderer Weg offen gestanden haette.

Die Rangfolge ist bewusst festgelegt und nicht dem Zufall ueberlassen:

1. **PoE-Port.** Schaltet die Stromversorgung. Wirkt auch dann noch, wenn das
   Betriebssystem nicht mehr antwortet -- und genau darum geht es meistens,
   wenn jemand einen Neustart ausloest.
2. **SSH** (``reboot``). Sauberes Herunterfahren, setzt aber ein Geraet
   voraus, das noch reagiert.
3. **RPC** (Windows). Wie SSH, nur ueber einen anderen Weg.

Stehen mehrere Wege offen, ist PoE vorausgewaehlt; die Oberflaeche laesst die
Wahl trotzdem zu. Ein sauberes Herunterfahren ist manchmal das Richtige --
etwa bei einem Geraet, das schreibend auf eine Platte zugreift.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

METHOD_POE = "poe"
METHOD_SSH = "ssh"
METHOD_RPC = "rpc"

#: Rangfolge -- der erste verfuegbare Weg ist die Vorauswahl.
PREFERENCE: tuple[str, ...] = (METHOD_POE, METHOD_SSH, METHOD_RPC)


@dataclass(frozen=True, slots=True)
class Method:
    key: str
    label: str
    hint: str
    #: Braucht dieser Weg das lizenzpflichtige Modul "Wartung"?
    licensed: bool = False

    @property
    def is_poe(self) -> bool:
        return self.key == METHOD_POE


DEFINITIONS: dict[str, Method] = {
    METHOD_POE: Method(
        key=METHOD_POE,
        label="PoE-Port",
        hint="Schaltet den Switch-Port kurz ab. Wirkt auch, wenn das Geraet haengt.",
    ),
    METHOD_SSH: Method(
        key=METHOD_SSH,
        label="Neustart ueber SSH",
        hint="Faehrt das Betriebssystem sauber herunter. Setzt ein antwortendes Geraet "
        "und einen freigegebenen Host-Schluessel voraus.",
        licensed=True,
    ),
    METHOD_RPC: Method(
        key=METHOD_RPC,
        label="Neustart ueber RPC",
        hint="Windows: wie 'shutdown /m'. Setzt offene Datei- und Druckerfreigabe voraus.",
        licensed=True,
    ),
}


def available(device: Any, *, maintenance_active: bool = True) -> list[Method]:
    """Alle Wege, die bei diesem Geraet tatsaechlich zu etwas fuehren.

    Ein Weg, der nur *fast* geht -- Switch zugeordnet, aber kein Port --, gilt
    als nicht verfuegbar. Ein Knopf, der verlaesslich nichts bewirkt, ist
    schlimmer als kein Knopf.
    """

    def field(name: str) -> Any:
        try:
            return device[name]
        except (KeyError, IndexError, TypeError):
            return None

    methods: list[Method] = []
    if field("switch_id") and str(field("port") or "").strip():
        methods.append(DEFINITIONS[METHOD_POE])

    category = str(field("credential_category") or "")
    has_login = bool(field("credential_id"))
    if has_login and maintenance_active:
        if category == "windows":
            methods.append(DEFINITIONS[METHOD_RPC])
        elif category == "linux":
            methods.append(DEFINITIONS[METHOD_SSH])

    return sorted(methods, key=lambda entry: PREFERENCE.index(entry.key))


def preferred(device: Any, *, maintenance_active: bool = True) -> Method | None:
    """Der Weg, der vorausgewaehlt wird -- oder ``None``, wenn es keinen gibt."""
    options = available(device, maintenance_active=maintenance_active)
    return options[0] if options else None


def resolve(device: Any, requested: str, *, maintenance_active: bool = True) -> Method | None:
    """Prueft eine Anforderung gegen das, was tatsaechlich moeglich ist.

    Die Oberflaeche darf vorschlagen; entscheiden tut der Server. Ein Weg, der
    fuer dieses Geraet nicht offensteht, wird nicht "irgendwie" ersetzt --
    sonst startet jemand ein Geraet anders neu, als er dachte.
    """
    options = {entry.key: entry for entry in available(device, maintenance_active=maintenance_active)}
    if not options:
        return None
    if not requested:
        return options[sorted(options, key=PREFERENCE.index)[0]]
    return options.get(requested)
