"""ArubaOS-Kommandozeile: PoE schalten und Zustand auslesen.

Der Vorgaenger steuerte den Switch ueber ein ``expect``-Skript. Das brachte drei
Probleme mit, die hier verschwinden:

1. **Fehlererkennung war unmoeglich.** Ein Versuch, harte Zeitlimits pro
   Schritt einzufuehren, war eine Regression: das Prompt-Muster erkannte nur den
   Operator-Modus (``switch>``), nicht den privilegierten (``switch#``), den
   manche Konten direkt nach der Anmeldung haben -- gueltige Anmeldungen wurden
   als Fehlschlag gewertet.
   Loesung hier: das Prompt-Muster erkennt **beide** Modi, und der Erfolg wird
   nicht am Prompt gemessen, sondern am tatsaechlichen Zustand
   (``show power-over-ethernet``). Damit braucht es kein Raten mehr.
2. **Passwoerter standen in der Prozessliste.** ``expect`` bekam sie als
   Argument. Hier gehen sie nie durch eine Kommandozeile.
3. **Kein Transkript.** Jetzt liefert jeder Vorgang die vollstaendige Ausgabe
   zurueck, die als Ereignis gespeichert wird.
"""

from __future__ import annotations

import logging
import re
import sqlite3
import time
from dataclasses import dataclass, field
from typing import Any

from .ssh import SshError, open_transport

logger = logging.getLogger(__name__)

#: Erkennt Operator- (``>``) und privilegierten Modus (``#``) sowie den
#: Konfigurationsmodus (``switch(config)#``).
PROMPT_RE = re.compile(rb"[\r\n][^\r\n]{0,80}?[>#]\s?$")
PAGER_RE = re.compile(rb"(--\s?MORE\s?--|Press any key to continue)", re.IGNORECASE)
ERROR_RE = re.compile(
    r"(invalid input|unknown command|incomplete|ambiguous|not supported|% *error)", re.IGNORECASE
)

READ_CHUNK = 65536
DEFAULT_COMMAND_TIMEOUT = 20
POE_SETTLE_SECONDS = 2


@dataclass(slots=True)
class CliResult:
    ok: bool
    transcript: str = ""
    error: str = ""
    steps: list[str] = field(default_factory=list)

    def add(self, message: str) -> None:
        self.steps.append(message)


class SwitchCli:
    """Interaktive CLI-Sitzung auf einem ArubaOS-Switch."""

    def __init__(
        self,
        conn: sqlite3.Connection,
        *,
        host: str,
        port: int,
        username: str,
        password: str,
        allow_unknown_host_key: bool = False,
    ) -> None:
        self.conn = conn
        self.host = host
        self.port = int(port)
        self.username = username
        self.password = password
        self.allow_unknown_host_key = allow_unknown_host_key
        self._transport: Any = None
        self._channel: Any = None
        self._buffer = b""
        self.transcript: list[str] = []

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

    def __enter__(self) -> "SwitchCli":
        self._transport = open_transport(
            self.conn,
            host=self.host,
            port=self.port,
            username=self.username,
            password=self.password,
            allow_unknown=self.allow_unknown_host_key,
        )
        self._channel = self._transport.open_session()
        self._channel.settimeout(DEFAULT_COMMAND_TIMEOUT)
        # ArubaOS braucht ein PTY; ohne bleibt die Shell stumm.
        self._channel.get_pty(term="vt100", width=200, height=1000)
        self._channel.invoke_shell()
        self._read_until_prompt(timeout=DEFAULT_COMMAND_TIMEOUT, initial=True)
        # Seitenweise Ausgabe abschalten -- sonst blockiert jedes "show" am Pager.
        for command in ("no page", "terminal length 1000"):
            self.send(command, tolerate_error=True)
        return self

    def __exit__(self, *_exc: object) -> None:
        self.close()

    def close(self) -> None:
        for closer in (self._channel, self._transport):
            try:
                if closer is not None:
                    closer.close()
            except Exception:  # noqa: BLE001
                pass
        self._channel = None
        self._transport = None

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

    def _read_until_prompt(self, *, timeout: int, initial: bool = False) -> str:
        assert self._channel is not None
        deadline = time.monotonic() + timeout
        collected = b""
        while time.monotonic() < deadline:
            if self._channel.recv_ready():
                chunk = self._channel.recv(READ_CHUNK)
                if not chunk:
                    break
                collected += chunk
                if PAGER_RE.search(collected[-200:]):
                    self._channel.send(" ")
                    collected = PAGER_RE.sub(b"", collected)
                    continue
                if PROMPT_RE.search(collected[-200:]):
                    break
                # Der Login-Banner mancher Geraete verlangt eine Bestaetigung.
                if initial and collected.rstrip().endswith(b"continue"):
                    self._channel.send("\r\n")
            else:
                time.sleep(0.05)
        text = collected.decode("utf-8", errors="replace")
        self.transcript.append(text)
        return text

    def send(
        self, command: str, *, timeout: int = DEFAULT_COMMAND_TIMEOUT, tolerate_error: bool = False
    ) -> str:
        """Schickt ein Kommando und liest bis zum Prompt.

        Ein erkannter Fehlertext fuehrt zu einer Ausnahme -- ausser bei
        ``tolerate_error`` (z. B. ``no page``, das nicht jede Firmware kennt).
        """
        if self._channel is None:
            raise SshError("Die Sitzung ist nicht geoeffnet.")
        self.transcript.append(f"\n$ {command}\n")
        self._channel.send(command + "\r\n")
        output = self._read_until_prompt(timeout=timeout)
        if not tolerate_error and ERROR_RE.search(output):
            raise SshError(f"Der Switch hat das Kommando abgelehnt: {command!r}")
        return output

    @property
    def full_transcript(self) -> str:
        return "".join(self.transcript)

    # ------------------------------------------------------------------ #
    # PoE
    # ------------------------------------------------------------------ #

    def set_poe(self, port: str, enabled: bool) -> None:
        self.send("configure terminal")
        try:
            self.send(f"interface {port}")
            self.send("power-over-ethernet" if enabled else "no power-over-ethernet")
        finally:
            # Immer zurueck in den privilegierten Modus, auch nach einem Fehler --
            # sonst bleibt die Sitzung im Konfigurationsmodus haengen.
            self.send("exit", tolerate_error=True)
            self.send("exit", tolerate_error=True)

    def poe_state(self, port: str) -> str:
        """``enabled`` / ``disabled`` / ``unknown`` -- gelesen, nicht geraten."""
        output = self.send(f"show power-over-ethernet {port}", tolerate_error=True)
        lowered = output.lower()
        if "power enable" in lowered:
            for line in lowered.splitlines():
                if "power enable" in line:
                    if "yes" in line or "enabled" in line:
                        return "enabled"
                    if "no" in line or "disabled" in line:
                        return "disabled"
        if re.search(r"poe port status\s*:\s*enabled", lowered):
            return "enabled"
        if re.search(r"poe port status\s*:\s*disabled", lowered):
            return "disabled"
        return "unknown"

    def cycle_poe(self, port: str) -> CliResult:
        """Schaltet PoE aus, wartet und wieder ein. Prueft das Ergebnis."""
        result = CliResult(ok=False)
        try:
            before = self.poe_state(port)
            result.add(f"Zustand vor dem Vorgang: {before}")

            self.set_poe(port, enabled=False)
            result.add("PoE ausgeschaltet")
            time.sleep(POE_SETTLE_SECONDS)

            off_state = self.poe_state(port)
            if off_state == "enabled":
                result.error = (
                    "Der Switch meldet den Port nach dem Abschalten weiter als aktiv -- "
                    "der Neustart hat nicht stattgefunden."
                )
                result.transcript = self.full_transcript
                return result
            result.add(f"Zustand im ausgeschalteten Zustand: {off_state}")

            self.set_poe(port, enabled=True)
            result.add("PoE wieder eingeschaltet")

            after = self.poe_state(port)
            result.add(f"Zustand nach dem Vorgang: {after}")
            if after == "disabled":
                result.error = "Der Port ist nach dem Vorgang weiter abgeschaltet."
                result.transcript = self.full_transcript
                return result

            result.ok = True
        except SshError as exc:
            result.error = str(exc)
        except Exception as exc:  # noqa: BLE001 - Netzabbruch mitten im Vorgang
            logger.exception("PoE-Vorgang auf %s Port %s fehlgeschlagen", self.host, port)
            result.error = f"Unerwarteter Fehler: {exc}"
        result.transcript = self.full_transcript
        return result

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

    def interface_overview(self) -> str:
        return self.send("show power-over-ethernet brief", tolerate_error=True)

    def system_info(self) -> str:
        return self.send("show system information", tolerate_error=True)


#: Kommandos, die die Oberflaeche ohne freies Eingabefeld anbietet.
SAFE_COMMANDS: tuple[tuple[str, str], ...] = (
    ("show system information", "Systeminformationen"),
    ("show power-over-ethernet brief", "PoE-Uebersicht aller Ports"),
    ("show interfaces brief", "Portstatus"),
    ("show vlans", "VLAN-Uebersicht"),
    ("show mac-address", "MAC-Adresstabelle"),
    ("show lldp info remote-device", "Nachbargeraete (LLDP)"),
    ("show version", "Firmware-Version"),
    ("show logging", "Switch-Protokoll"),
)
