"""Lizenzverwaltung auf Kundenseite.

Diese Seite bleibt **immer** erreichbar -- auch ohne gueltige Lizenz. Sonst
liesse sich eine neue Lizenz nie einspielen (Henne-Ei).
"""

from __future__ import annotations

import socket

from flask import Blueprint, flash, redirect, render_template, request, url_for
from tesm_core import audit
from tesm_core.auth.models import (
    admin_required,
    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 tesm_core.licensing_client import LICENSED_FEATURES, LicenseError

import tesm_licensing as lic

bp = Blueprint("license", __name__, url_prefix="/lizenz")

MAX_UPLOAD_BYTES = 256 * 1024


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,
        )


@bp.get("/")
@permission_required("settings_license.view")
def overview():
    extension = core()
    conn = db()
    manager = extension.license
    state = manager.reload(conn) if manager else None
    pending = manager._pending(conn) if manager else {}  # noqa: SLF001 - eigenes Paket
    return render_template(
        "tesm/license.html",
        state=state,
        pending=pending,
        server_certificate=extension.settings.get(conn, "license_server_ca") or "",
        modules=lic.ALL_MODULES,
        module_labels=lic.MODULE_LABELS,
        type_labels=lic.LICENSE_TYPE_LABELS,
        role_labels=lic.ROLE_LABELS,
        features=LICENSED_FEATURES,
        trusted_keys=Database.all(
            conn, "SELECT * FROM trusted_issuer_keys ORDER BY added_at DESC"
        ),
        fingerprint=lic.system_fingerprint(),
        hostname=socket.gethostname(),
        grace_days=lic.GRACE_PERIOD_DAYS,
    )


@bp.post("/einspielen")
@permission_required("settings_license.edit")
def upload():
    extension = core()
    manager = extension.license
    if manager is None:
        flash("Diese Installation nutzt keine Lizenzierung.", "info")
        return redirect(url_for("license.overview"))

    upload_file = request.files.get("license_file")
    if upload_file is None or not upload_file.filename:
        flash("Keine Datei ausgewaehlt.", "danger")
        return redirect(url_for("license.overview"))
    raw = upload_file.stream.read(MAX_UPLOAD_BYTES + 1)
    if len(raw) > MAX_UPLOAD_BYTES:
        flash("Die Datei ist zu gross fuer eine Lizenzdatei.", "danger")
        return redirect(url_for("license.overview"))

    allow_new_issuer = request.form.get("allow_new_issuer") == "1" and current_user.is_admin
    try:
        with transaction() as conn:
            state = manager.import_bundle(
                conn, raw, actor=current_user.username, allow_new_issuer=allow_new_issuer
            )
    except LicenseError as exc:
        flash(str(exc), "danger")
        return redirect(url_for("license.overview"))

    flash(
        "Lizenz eingespielt. Bitte jetzt aktivieren, damit die Funktionen freigeschaltet werden.",
        "success",
    )
    if state.status and state.status.role != "customer":
        flash(
            f"Achtung: diese Lizenz hat die Rolle {state.status.role!r}. "
            "In einer TESM-Kundeninstanz ist das ungewoehnlich.",
            "warning",
        )
    return redirect(url_for("license.overview"))


@bp.post("/aktivieren")
@permission_required("settings_license.edit")
def activate():
    return _handshake("activate")


@bp.post("/deaktivieren")
@permission_required("settings_license.edit")
@reauth_required
def deactivate():
    """Gibt die Lizenz frei, damit sie auf einer anderen Maschine genutzt werden kann.

    Verlangt eine frische Passwortbestaetigung: eine versehentliche
    Deaktivierung schaltet lizenzpflichtige Funktionen ab.
    """
    return _handshake("deactivate")


@bp.post("/heartbeat")
@permission_required("settings_license.edit")
def heartbeat():
    return _handshake("heartbeat")


def _handshake(action: str):
    extension = core()
    manager = extension.license
    if manager is None:
        flash("Diese Installation nutzt keine Lizenzierung.", "info")
        return redirect(url_for("license.overview"))
    try:
        with transaction() as conn:
            result = manager.handshake(
                conn, action, actor=current_user.username, hostname=socket.gethostname()
            )
    except LicenseError as exc:
        flash(str(exc), "danger")
        return redirect(url_for("license.overview"))

    if result["ok"]:
        flash(result["message"], "success")
    elif result["transport"] == "offline":
        flash(result["message"], "warning")
    else:
        flash(result["message"], "danger")
    return redirect(url_for("license.overview"))


@bp.post("/offline-antwort")
@permission_required("settings_license.edit")
def offline_response():
    extension = core()
    manager = extension.license
    if manager is None:
        return redirect(url_for("license.overview"))
    try:
        with transaction() as conn:
            result = manager.submit_offline_response(
                conn, request.form.get("code", ""), actor=current_user.username
            )
    except LicenseError as exc:
        flash(str(exc), "danger")
        return redirect(url_for("license.overview"))
    flash(result["message"], "success" if result["ok"] else "danger")
    return redirect(url_for("license.overview"))


@bp.post("/serverzertifikat")
@permission_required("settings_license.edit")
def server_certificate():
    """Hinterlegt das Zertifikat des Lizenzservers.

    Ein Lizenzserver im eigenen Netz hat meist ein selbstsigniertes
    Zertifikat. Ohne diesen Anker scheitert jeder Online-Handshake an der
    Pruefung und faellt auf den Offline-Weg zurueck. Die Pruefung deshalb
    abzuschalten waere der falsche Weg -- hier wird sie stattdessen auf genau
    dieses Zertifikat gerichtet.
    """
    extension = core()
    conn = db()
    pem = (request.form.get("certificate") or "").strip()
    if pem and "BEGIN CERTIFICATE" not in pem:
        flash(
            "Das sieht nicht nach einem Zertifikat aus. Erwartet wird PEM, beginnend mit "
            "'-----BEGIN CERTIFICATE-----'.",
            "danger",
        )
        return redirect(url_for("license.overview"))
    if pem:
        # Lieber jetzt scheitern als beim naechsten Handshake.
        try:
            extension.license._tls_context(pem)  # noqa: SLF001 - eigenes Paket
        except ValueError as exc:
            flash(str(exc), "danger")
            return redirect(url_for("license.overview"))

    with transaction():
        extension.settings.set(conn, "license_server_ca", pem, actor=current_user.username)
    _audit(
        "license.server_certificate_set" if pem else "license.server_certificate_cleared",
        detail="PEM hinterlegt" if pem else "entfernt",
        severity="notice",
    )
    flash(
        "Zertifikat hinterlegt. Der naechste Versuch laeuft wieder ueber den Online-Weg."
        if pem
        else "Zertifikat entfernt -- es gelten wieder die Systemanker.",
        "success",
    )
    return redirect(url_for("license.overview"))


@bp.post("/aussteller/<key_id>/entfernen")
@admin_required
@reauth_required
def remove_trusted_key(key_id: str):
    """Entfernt einen gepinnten Aussteller-Schluessel.

    Nur echte Administratoren, nur mit frischer Bestaetigung: das ist die
    Vertrauensgrundlage der Lizenzpruefung.
    """
    with transaction() as conn:
        conn.execute("DELETE FROM trusted_issuer_keys WHERE key_id=?", (key_id,))
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="license.issuer_unpinned",
            target=key_id,
            ip=client_ip(),
            severity="warning",
        )
    manager = core().license
    if manager is not None:
        with core().database.session() as conn:
            manager.reload(conn)
    flash("Aussteller-Schluessel entfernt.", "info")
    return redirect(url_for("license.overview"))
