"""SSH-Host-Schluessel -- alle Ziele auf einer Seite.

Die Freigabe einzelner Schluessel gibt es auch auf den Detailseiten von
Clients und Switchen. Diese Seite beantwortet die andere Frage, die im Betrieb
haeufiger ist: *wo fehlt noch eine Freigabe?* Sie vorher zu beantworten hiess,
jede Detailseite einzeln aufzurufen.

Gezeigt werden beide Richtungen:

* Jedes Ziel (Client oder Switch) mit einer SSH-Adresse -- mit oder ohne
  Freigabe.
* Freigegebene Schluessel, zu denen es kein Ziel mehr gibt. Die sind kein
  Fehler, aber sie sollten auffallen: ein Geraet wurde geloescht oder hat eine
  neue IP, und die alte Freigabe steht noch.

Die Rechte bleiben an den Objekten: wer Clients bearbeiten darf, gibt
Client-Schluessel frei; fuer Switche gilt ``switches.edit``. Diese Seite
buendelt die Ansicht, nicht die Berechtigung.
"""

from __future__ import annotations

from typing import Any

from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
from tesm_core import audit
from tesm_core.auth.models import current_user, permission_required, reauth_required
from tesm_core.auth.service import client_ip
from tesm_core.db import Database
from tesm_core.extension import core, db, transaction

from ..services import inventory, ssh

bp = Blueprint("hostkeys", __name__, url_prefix="/hostschluessel")

KIND_DEVICE = "client"
KIND_SWITCH = "switch"

#: Welches Recht welche Art von Ziel freigibt.
EDIT_RIGHT = {KIND_DEVICE: "devices.edit", KIND_SWITCH: "switches.edit"}


def _audit(action: str, target: str, detail: str = "", severity: str = "info") -> None:
    with transaction() as conn:
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action=action,
            target=target,
            detail=detail,
            ip=client_ip(),
            severity=severity,
        )


def _targets(conn: Any) -> list[dict[str, Any]]:
    """Alle Ziele mit SSH-Adresse, jeweils mit ihren freigegebenen Schluesseln."""
    rows: list[dict[str, Any]] = []
    for switch in inventory.list_switches(conn):
        rows.append(
            {
                "kind": KIND_SWITCH,
                "id": int(switch["id"]),
                "label": str(switch["hostname"]),
                "host": str(switch["ip"]),
                "port": int(switch["ssh_port"] or 22),
                "detail_endpoint": "switches.detail",
                "credential": switch["credential_name"] if "credential_name" in switch.keys()
                else "",
            }
        )
    for device in inventory.list_devices(conn):
        # Windows-Geraete werden nicht ueber SSH bedient -- sie haetten hier
        # ewig den Zustand "Freigabe offen", ohne dass es je etwas freizugeben
        # gaebe.
        if not inventory.is_ssh_managed(device["credential_category"]):
            continue
        rows.append(
            {
                "kind": KIND_DEVICE,
                "id": int(device["id"]),
                "label": str(device["name"]),
                "host": str(device["ip"]),
                "port": int(device["ssh_port"] or 22),
                "detail_endpoint": "devices.detail",
                "credential": device["credential_name"] or "",
            }
        )
    for row in rows:
        # Nicht "keys" nennen: Jinja loest ``row.keys`` auf die dict-Methode
        # auf, nicht auf den Eintrag -- die Vorlage bekaeme dann eine Funktion.
        found = ssh.all_known_keys(conn, row["host"], row["port"])
        row["host_keys"] = found
        row["trusted"] = bool(found)
    rows.sort(key=lambda item: (item["trusted"], item["label"].lower()))
    return rows


def _orphans(conn: Any, targets: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Freigaben, zu denen kein Ziel mehr passt."""
    used = {(row["host"], row["port"]) for row in targets}
    return [
        dict(row)
        for row in Database.all(
            conn,
            "SELECT host, port, key_type, fingerprint, added_at, added_by "
            "FROM switch_host_keys ORDER BY host, port, key_type",
        )
        if (str(row["host"]), int(row["port"])) not in used
    ]


@bp.get("/")
@permission_required("devices.view", "switches.view", mode="any")
def overview():
    conn = db()
    targets = _targets(conn)
    return render_template(
        "tesm/hostkeys.html",
        targets=targets,
        orphans=_orphans(conn, targets),
        open_count=sum(1 for row in targets if not row["trusted"]),
        edit_right=EDIT_RIGHT,
        probed_host=request.args.get("probed_host", ""),
        probed_port=request.args.get("probed_port", ""),
        probed_type=request.args.get("probed_type", ""),
        probed_fingerprint=request.args.get("probed_fingerprint", ""),
    )


def _load(conn: Any, kind: str, target_id: int) -> dict[str, Any] | None:
    for row in _targets(conn):
        if row["kind"] == kind and row["id"] == target_id:
            return row
    return None


def _require(kind: str) -> None:
    right = EDIT_RIGHT.get(kind)
    if right is None or not current_user.has(right):
        abort(403)


@bp.post("/<kind>/<int:target_id>/pruefen")
@permission_required("devices.view", "switches.view", mode="any")
def probe(kind: str, target_id: int):
    conn = db()
    _require(kind)
    target = _load(conn, kind, target_id)
    if target is None:
        abort(404)
    try:
        record = ssh.probe_host_key(target["host"], target["port"])
    except ssh.SshError as exc:
        flash(str(exc), "danger")
        return redirect(url_for("hostkeys.overview"))

    level, message = ssh.describe_probe(conn, record)
    flash(f"{target['label']}: {message}", level)
    return redirect(
        url_for(
            "hostkeys.overview",
            probed_host=record.host,
            probed_port=record.port,
            probed_type=record.key_type,
            probed_fingerprint=record.fingerprint,
        )
    )


@bp.post("/<kind>/<int:target_id>/freigeben")
@permission_required("devices.view", "switches.view", mode="any")
@reauth_required
def trust(kind: str, target_id: int):
    conn = db()
    _require(kind)
    target = _load(conn, kind, target_id)
    if target is None:
        abort(404)
    key_type = (request.form.get("key_type") or "").strip()
    fingerprint = (request.form.get("fingerprint") or "").strip()
    if not key_type or not ssh.valid_fingerprint(fingerprint):
        flash("Es wurde kein gueltiger Fingerprint uebergeben.", "danger")
        return redirect(url_for("hostkeys.overview"))

    with transaction():
        ssh.trust_key(
            conn,
            host=target["host"],
            port=target["port"],
            key_type=key_type,
            fingerprint=fingerprint,
            actor=current_user.username,
        )
    _audit(
        f"{'switch' if kind == KIND_SWITCH else 'device'}.host_key_trusted",
        target["label"],
        f"{key_type} {fingerprint}",
        severity="notice",
    )
    flash(f"Host-Schluessel von {target['label']} freigegeben.", "success")
    return redirect(url_for("hostkeys.overview"))


@bp.post("/<kind>/<int:target_id>/verwerfen")
@permission_required("devices.view", "switches.view", mode="any")
def forget(kind: str, target_id: int):
    conn = db()
    _require(kind)
    target = _load(conn, kind, target_id)
    if target is None:
        abort(404)
    with transaction():
        ssh.forget_key(conn, target["host"], target["port"])
    _audit(
        f"{'switch' if kind == KIND_SWITCH else 'device'}.host_key_forgotten",
        target["label"],
        severity="warning",
    )
    flash(f"Gespeicherte Schluessel von {target['label']} entfernt.", "info")
    return redirect(url_for("hostkeys.overview"))


@bp.post("/verwaist/verwerfen")
@permission_required("devices.edit", "switches.edit", mode="any")
def forget_orphan():
    """Entfernt eine Freigabe, zu der es kein Ziel mehr gibt."""
    host = (request.form.get("host") or "").strip()
    try:
        port = int(request.form.get("port") or 0)
    except ValueError:
        port = 0
    if not host or not 1 <= port <= 65535:
        flash("Ungueltiges Ziel.", "danger")
        return redirect(url_for("hostkeys.overview"))

    conn = db()
    if (host, port) in {(row["host"], row["port"]) for row in _targets(conn)}:
        # Ein Ziel, das es noch gibt, wird ueber seine eigene Zeile verworfen --
        # sonst umginge dieser Weg die Rechtepruefung je Objektart.
        flash("Zu diesem Eintrag gibt es ein Ziel -- bitte dort verwerfen.", "warning")
        return redirect(url_for("hostkeys.overview"))

    with transaction():
        ssh.forget_key(conn, host, port)
    _audit("host_key.orphan_forgotten", f"{host}:{port}", severity="warning")
    flash(f"Verwaiste Freigabe fuer {host}:{port} entfernt.", "info")
    return redirect(url_for("hostkeys.overview"))
