"""Papierkorb -- generisch ueber angemeldete Anbieter.

Der Vorgaenger hatte den Papierkorb nur in TESM und dort fuer fuenf fest
verdrahtete Tabellen; der Lizenzserver-Fork hatte gar keinen, obwohl die
``deleted_at``-Spalten noch existierten (ein Artefakt, das die eigene
Spezifikation als "nicht nachbauen" markiert).

Hier ist der Papierkorb ein Kern-Feature mit einem Register: jede Anwendung
meldet ihre loeschbaren Ressourcen an, die Oberflaeche ist fuer alle gleich.
Benutzer und Gruppen registriert der Kern selbst -- damit haben **beide**
Anwendungen einen funktionierenden Papierkorb, statt einer von beiden gar
keinen.
"""

from __future__ import annotations

import sqlite3
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Callable

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

from .. import audit
from ..auth.models import current_user, permission_required
from ..auth.service import client_ip
from ..db import Database
from ..extension import core, db, transaction
from ..rbac import service as rbac

bp = Blueprint("trash", __name__, url_prefix="/papierkorb")


@dataclass(frozen=True, slots=True)
class TrashProvider:
    """Eine im Papierkorb verwaltbare Ressourcenart."""

    key: str
    label: str
    icon: str
    list_fn: Callable[[sqlite3.Connection], list[dict[str, Any]]]
    restore_fn: Callable[[sqlite3.Connection, str], None]
    purge_fn: Callable[[sqlite3.Connection, str], None]
    hint: str = ""


def register(provider: TrashProvider) -> None:
    """Meldet einen Anbieter an. Wird beim App-Aufbau aufgerufen."""
    providers: dict[str, TrashProvider] = core().registry.setdefault("trash_providers", {})
    providers[provider.key] = provider


def providers() -> dict[str, TrashProvider]:
    return core().registry.get("trash_providers", {})


# --------------------------------------------------------------------------- #
# Kern-Anbieter: Benutzer und Gruppen
# --------------------------------------------------------------------------- #


def _users_in_trash(conn: sqlite3.Connection) -> list[dict[str, Any]]:
    return [
        {
            "id": str(row["id"]),
            "label": str(row["username"]),
            "detail": f"{row['first_name']} {row['last_name']}".strip() or "-",
            "deleted_at": row["deleted_at"],
        }
        for row in Database.all(
            conn,
            "SELECT id, username, first_name, last_name, deleted_at FROM users "
            "WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC",
        )
    ]


def _groups_in_trash(conn: sqlite3.Connection) -> list[dict[str, Any]]:
    return [
        {
            "id": str(row["id"]),
            "label": str(row["name"]),
            "detail": str(row["description"] or "-"),
            "deleted_at": row["deleted_at"],
        }
        for row in Database.all(
            conn,
            "SELECT id, name, description, deleted_at FROM groups "
            "WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC",
        )
    ]


def register_core_providers() -> None:
    register(
        TrashProvider(
            key="users",
            label="Benutzerkonten",
            icon="users",
            list_fn=_users_in_trash,
            restore_fn=lambda conn, ident: rbac.restore_user(conn, int(ident)),
            purge_fn=lambda conn, ident: rbac.purge_user(conn, int(ident)),
            hint="Verzeichniskonten landen nie hier -- sie legen sich bei der naechsten "
            "Anmeldung von selbst wieder an.",
        )
    )
    register(
        TrashProvider(
            key="groups",
            label="Gruppen",
            icon="shield-users",
            list_fn=_groups_in_trash,
            restore_fn=lambda conn, ident: rbac.restore_group(conn, int(ident)),
            purge_fn=lambda conn, ident: rbac.purge_group(conn, int(ident)),
            hint="Beim endgueltigen Loeschen verschwinden auch Mitgliedschaften und Rechte.",
        )
    )


# --------------------------------------------------------------------------- #
# Ansicht
# --------------------------------------------------------------------------- #


def _retention_days() -> int:
    return int(core().settings.get(db(), "trash_retention_days") or 30)


def _purge_expired(conn: sqlite3.Connection) -> int:
    """Entfernt abgelaufene Eintraege beim Seitenaufruf.

    Bewusst ohne eigenen Scheduler: ein zusaetzlicher Timer waere ein weiterer
    Prozess, der schiefgehen kann. Die Frist ist eine Obergrenze, kein
    sekundengenauer Vorgang.
    """
    horizon = (datetime.now(timezone.utc) - timedelta(days=_retention_days())).isoformat(
        timespec="seconds"
    )
    removed = 0
    for provider in providers().values():
        for entry in provider.list_fn(conn):
            if str(entry.get("deleted_at") or "") and str(entry["deleted_at"]) < horizon:
                provider.purge_fn(conn, str(entry["id"]))
                removed += 1
    return removed


@bp.get("/")
@permission_required("trash.view")
def overview():
    conn = db()
    removed = 0
    if current_user.has("trash.delete"):
        with transaction():
            removed = _purge_expired(conn)
            if removed:
                audit.log(
                    conn,
                    actor="system",
                    action="trash.auto_purged",
                    detail=f"{removed} abgelaufene Eintraege",
                    severity="notice",
                )
    if removed:
        flash(f"{removed} abgelaufene Eintraege wurden endgueltig entfernt.", "info")

    sections = []
    for provider in providers().values():
        entries = provider.list_fn(conn)
        sections.append(
            {
                "key": provider.key,
                "label": provider.label,
                "icon": provider.icon,
                "hint": provider.hint,
                "entries": entries,
            }
        )
    return render_template(
        "tesm_core/trash.html",
        sections=sections,
        retention_days=_retention_days(),
        total=sum(len(section["entries"]) for section in sections),
    )


def _provider_or_404(key: str) -> TrashProvider:
    provider = providers().get(key)
    if provider is None:
        abort(404)
    return provider


@bp.post("/<kind>/<ident>/wiederherstellen")
@permission_required("trash.edit")
def restore(kind: str, ident: str):
    extension = core()
    if extension.license is not None and not extension.license.feature_allowed("trash_restore"):
        flash(
            "Das Wiederherstellen aus dem Papierkorb erfordert eine gueltige, "
            "aktivierte Lizenz. Ansehen bleibt jederzeit moeglich.",
            "danger",
        )
        return redirect(url_for("trash.overview"))

    provider = _provider_or_404(kind)
    try:
        with transaction() as conn:
            provider.restore_fn(conn, ident)
            audit.log(
                conn,
                actor=current_user.username,
                actor_id=current_user.id,
                action="trash.restored",
                target=f"{kind}:{ident}",
                ip=client_ip(),
                severity="notice",
            )
        flash("Eintrag wiederhergestellt.", "success")
    except (rbac.ValidationError, rbac.PermissionDenied) as exc:
        flash(str(exc), "danger")
    return redirect(url_for("trash.overview"))


@bp.post("/<kind>/<ident>/endgueltig-loeschen")
@permission_required("trash.delete")
def purge(kind: str, ident: str):
    extension = core()
    if extension.license is not None and not extension.license.feature_allowed("trash_restore"):
        flash(
            "Das endgueltige Loeschen erfordert eine gueltige, aktivierte Lizenz.",
            "danger",
        )
        return redirect(url_for("trash.overview"))

    provider = _provider_or_404(kind)
    try:
        with transaction() as conn:
            provider.purge_fn(conn, ident)
            audit.log(
                conn,
                actor=current_user.username,
                actor_id=current_user.id,
                action="trash.purged",
                target=f"{kind}:{ident}",
                ip=client_ip(),
                severity="warning",
            )
        flash("Eintrag endgueltig entfernt.", "success")
    except (rbac.ValidationError, rbac.PermissionDenied) as exc:
        flash(str(exc), "danger")
    return redirect(url_for("trash.overview"))


@bp.post("/leeren")
@permission_required("trash.delete")
def purge_all():
    extension = core()
    if extension.license is not None and not extension.license.feature_allowed("trash_restore"):
        flash("Das endgueltige Loeschen erfordert eine gueltige, aktivierte Lizenz.", "danger")
        return redirect(url_for("trash.overview"))

    kind = request.form.get("kind") or ""
    removed = 0
    with transaction() as conn:
        targets = [providers()[kind]] if kind in providers() else list(providers().values())
        for provider in targets:
            for entry in provider.list_fn(conn):
                provider.purge_fn(conn, str(entry["id"]))
                removed += 1
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="trash.emptied",
            target=kind or "alle",
            detail=f"{removed} Eintraege",
            ip=client_ip(),
            severity="warning",
        )
    flash(f"{removed} Eintraege endgueltig entfernt.", "success")
    return redirect(url_for("trash.overview"))
