"""Diagnoseseite: Schemastand, Protokollintegritaet, Dateirechte, Lizenzzustand.

Diese Seite gab es im Vorgaenger nicht. Sie beantwortet die Fragen, die bei
jedem Zwischenfall zuerst gestellt werden -- laeuft das Schema auf dem
erwarteten Stand, ist das Aenderungsprotokoll unversehrt, liegen die
Schluesseldateien mit korrekten Rechten, ist der privilegierte Helfer
erreichbar.
"""

from __future__ import annotations

import os
import platform
import sys
from datetime import datetime, timezone

from flask import Blueprint, flash, redirect, render_template, url_for

from .. import audit
from ..auth.models import admin_required, current_user, permission_required
from ..db import Database
from ..extension import core, db, transaction
from ..keystore import check_private_file
from ..security import ratelimit
from ..security import sessions as session_store

bp = Blueprint("diagnostics", __name__, url_prefix="/diagnose")


@bp.get("/")
@permission_required("diagnostics.view")
def overview():
    extension = core()
    conn = db()
    config = extension.config

    chain = audit.verify_chain(conn, limit=5000)
    key_problems: list[str] = []
    for path in (config.data_key_file, config.secret_key_file, config.license_key_path):
        key_problems.extend(check_private_file(path))

    counts = {
        "Benutzer": Database.value(conn, "SELECT COUNT(*) FROM users WHERE deleted_at IS NULL", default=0),
        "Gruppen": Database.value(conn, "SELECT COUNT(*) FROM groups WHERE deleted_at IS NULL", default=0),
        "Aktive Sitzungen": Database.value(
            conn,
            "SELECT COUNT(*) FROM user_sessions WHERE revoked_at IS NULL "
            "AND idle_expires_at > datetime('now')",
            default=0,
        ),
        "Protokollzeilen": audit.count(conn),
    }

    try:
        db_size = config.db_path.stat().st_size
    except OSError:
        db_size = 0

    return render_template(
        "tesm_core/diagnostics.html",
        migrations=extension.migration_status(),
        chain=chain,
        key_problems=key_problems,
        counts=counts,
        db_size=db_size,
        db_path=str(config.db_path),
        log_dir=str(config.log_dir),
        keystore_active=extension.keystore.active_key_id,
        keystore_keys=extension.keystore.key_ids(),
        sysops_available=bool(extension.sysops and extension.sysops.available),
        sysops_helper=str(extension.sysops.helper) if extension.sysops else "",
        # Nicht "liegt die Datei da", sondern "laesst er sich aufrufen". Der
        # Unterschied hat einmal gekostet: die Seite meldete "erreichbar",
        # waehrend jeder Aufruf an NoNewPrivileges scheiterte.
        sysops_selftest=(extension.sysops.selftest() if extension.sysops else None),
        license_state=extension.license.state if extension.license else None,
        runtime={
            "Python": sys.version.split()[0],
            "Plattform": platform.platform(),
            "Prozess": f"PID {os.getpid()}",
            "Webprozess-Kennzeichen": "gesetzt" if config.web_process else "nicht gesetzt",
            "Debug": "ja" if config.debug else "nein",
            "Cookie Secure": "ja" if config.security.cookie_secure else "nein",
            "HSTS": "ja" if config.security.hsts_enabled else "nein",
            "2FA-Pflicht fuer Admins": "ja" if config.security.require_totp_for_admins else "nein",
        },
        now=datetime.now(timezone.utc),
    )


@bp.post("/aufraeumen")
@admin_required
def housekeeping():
    """Raeumt abgelaufene Sitzungen, alte Ratenbegrenzungs-Ereignisse und Protokolle."""
    extension = core()
    with transaction() as conn:
        sessions_removed = session_store.prune(conn)
        rate_removed = ratelimit.prune(conn)
        archived = audit.archive_old_rows(conn, extension.audit_archive_dir)
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="system.housekeeping",
            detail=(
                f"{sessions_removed} Sitzungen, {rate_removed} Ratenereignisse, "
                f"{len(archived)} Protokolltage archiviert"
            ),
            severity="notice",
        )
    flash(
        f"Aufgeraeumt: {sessions_removed} Sitzungen, {rate_removed} Ratenereignisse, "
        f"{len(archived)} archivierte Protokolltage.",
        "success",
    )
    return redirect(url_for("diagnostics.overview"))


@bp.post("/schluessel-rotieren")
@admin_required
def rotate_keys():
    """Legt einen neuen Datenschluessel an und verschluesselt bestehende Werte neu.

    Der Vorgaenger hatte genau einen, nie wechselbaren Fernet-Schluessel. Hier
    bleibt der alte Schluessel zum Lesen erhalten, bis alle Werte umgezogen sind.
    """
    extension = core()
    keystore = extension.keystore
    new_id = keystore.rotate()
    migrated = 0

    reencrypt = extension.registry.get("reencrypt_secrets")
    with transaction() as conn:
        if callable(reencrypt):
            migrated = int(reencrypt(conn, keystore))
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="system.data_key_rotated",
            target=new_id,
            detail=f"{migrated} Werte neu verschluesselt",
            severity="notice",
        )
    flash(
        f"Neuer Datenschluessel {new_id} aktiv, {migrated} gespeicherte Geheimnisse "
        "wurden neu verschluesselt.",
        "success",
    )
    return redirect(url_for("diagnostics.overview"))
