"""Zeilenweise Kommandoausfuehrung auf einem Linux-Client.

Gegenstueck zu :mod:`switchcli`. Der Unterschied liegt im Verfahren, nicht in
der Bedienung:

* ArubaOS braucht eine **interaktive Shell** mit PTY, und die Antwort endet
  erst am Prompt -- ``switchcli`` muss den Prompt erkennen.
* Ein Linux-Client kann jedes Kommando in einer eigenen ``exec``-Sitzung
  ausfuehren. Das ist deutlich robuster: kein Prompt-Raten, kein PTY, ein
  sauberer Rueckgabewert je Kommando, und ein haengendes Kommando reisst die
  Sitzung nicht mit.

Warum ueberhaupt eine Konsole fuer Clients? Ohne sie gab es keinen Weg, einen
unbekannten Host-Schluessel im laufenden Betrieb zu pruefen und freizugeben --
und ohne freigegebenen Schluessel verweigert die Wartung den Dienst. Genau in
diese Sackgasse laeuft man sonst: die Wartung sagt "Schluessel unbekannt", und
es gibt keine Stelle, an der man ihn ansehen koennte.

Bewusst **keine** interaktive Shell fuer Clients: eine solche Sitzung braucht
einen dauerhaft blockierten Thread je Nutzer und einen Prompt-Parser, der bei
jedem abweichenden ``PS1`` bricht. Wer wirklich interaktiv arbeiten will,
nimmt einen SSH-Client -- dafuer ist er da.
"""

from __future__ import annotations

import sqlite3
import time
from typing import Any

from .ssh import SshError, open_transport

DEFAULT_COMMAND_TIMEOUT = 60
MAX_OUTPUT_BYTES = 256 * 1024


class ShellSession:
    """Offene SSH-Verbindung zu einem Client; ein ``exec``-Kanal je Kommando."""

    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

    def __enter__(self) -> "ShellSession":
        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,
        )
        return self

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

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

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

    def banner(self) -> str:
        """Kurze Auskunft ueber das Gegenueber -- als erste Zeile der Sitzung."""
        try:
            result = self.run("hostname; uname -sr; id -un", timeout=15)
        except SshError:
            return ""
        return result["output"].strip()

    def run(self, command: str, *, timeout: int = DEFAULT_COMMAND_TIMEOUT) -> dict[str, Any]:
        """Fuehrt ein Kommando aus und liefert Ausgabe samt Rueckgabewert.

        Rueckgabe: ``{"output": str, "status": int, "duration_ms": int}``.
        """
        if self._transport is None or not self._transport.is_active():
            raise SshError("Die Verbindung wurde getrennt.")

        started = time.monotonic()
        channel = self._transport.open_session()
        channel.settimeout(timeout)
        channel.set_combine_stderr(True)
        try:
            channel.exec_command(command)
        except Exception as exc:  # noqa: BLE001
            channel.close()
            raise SshError(f"Das Kommando konnte nicht gestartet werden ({exc}).") from exc

        chunks: list[bytes] = []
        size = 0
        deadline = time.monotonic() + timeout
        truncated = False
        try:
            while True:
                if channel.recv_ready():
                    chunk = channel.recv(32768)
                    if not chunk:
                        break
                    size += len(chunk)
                    if size > MAX_OUTPUT_BYTES:
                        truncated = True
                        break
                    chunks.append(chunk)
                elif channel.exit_status_ready():
                    # Rest abholen, bevor der Kanal zugeht.
                    while channel.recv_ready():
                        chunks.append(channel.recv(32768))
                    break
                elif time.monotonic() > deadline:
                    channel.close()
                    raise SshError(
                        f"Das Kommando hat das Zeitlimit von {timeout}s ueberschritten."
                    )
                else:
                    time.sleep(0.05)
            status = channel.recv_exit_status() if not truncated else -1
        finally:
            try:
                channel.close()
            except Exception:  # noqa: BLE001
                pass

        output = b"".join(chunks).decode("utf-8", errors="replace")
        if truncated:
            output += f"\n[Ausgabe nach {MAX_OUTPUT_BYTES // 1024} KB abgeschnitten]"
        return {
            "output": output,
            "status": int(status),
            "duration_ms": int((time.monotonic() - started) * 1000),
        }
