"""Protokollansichten, die beide Anwendungen teilen.

* **Laufend** -- die Textdatei ``live.log``.
* **Verlauf** -- die von logrotate rotierten Kopien plus die aus der Datenbank
  ausgelagerten Audit-Tage.
* **Aenderungen** -- die Tabelle ``audit_log`` mit pruefbarer Hash-Kette.

Was ``live.log`` fuellt, entscheidet die Anwendung: TESM schreibt dort
Erreichbarkeit und PoE-Vorgaenge; der Lizenzserver spiegelt dorthin seinen
Audit-Trail, weil es bei ihm keinen Telemetrie-Prozess gibt. Beides landet in
derselben Ansicht -- der Vorgaenger hatte dafuer zwei getrennte Umsetzungen.
"""

from __future__ import annotations

import gzip
import io
import zipfile
from pathlib import Path

from flask import Blueprint, Response, 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 ..extension import core, db, transaction

bp = Blueprint("logs", __name__, url_prefix="/protokolle")

MAX_TAIL_LINES = 2000


def _live_log():
    log = core().registry.get("live_log")
    if log is None:  # pragma: no cover - jede App registriert eines
        abort(404)
    return log


@bp.get("/laufend")
@permission_required("logs_live.view")
def live():
    lines = min(int(request.args.get("lines") or 400), MAX_TAIL_LINES)
    needle = (request.args.get("q") or "").strip()
    return render_template(
        "tesm_core/logs_live.html",
        lines=_live_log().tail(lines, needle=needle),
        path=str(_live_log().path),
        query=needle,
        line_count=lines,
    )


@bp.get("/laufend/inhalt")
@permission_required("logs_live.view")
def live_fragment():
    lines = min(int(request.args.get("lines") or 400), MAX_TAIL_LINES)
    needle = (request.args.get("q") or "").strip()
    return render_template(
        "tesm_core/_log_lines.html", lines=_live_log().tail(lines, needle=needle)
    )


@bp.get("/verlauf")
@permission_required("logs_history.view", "logs_history.export", mode="any")
def history():
    extension = core()
    rotated = []
    for path in sorted(extension.config.log_dir.glob("*.log.*"), reverse=True):
        try:
            stat = path.stat()
        except OSError:
            continue
        rotated.append({"name": path.name, "size": stat.st_size, "modified": stat.st_mtime})
    return render_template(
        "tesm_core/logs_history.html",
        rotated=rotated,
        archived=audit.archived_days(extension.audit_archive_dir),
        log_dir=str(extension.config.log_dir),
    )


@bp.get("/verlauf/datei/<path:name>")
@permission_required("logs_history.view")
def history_file(name: str):
    extension = core()
    target = (extension.config.log_dir / name).resolve()
    if extension.config.log_dir.resolve() not in target.parents or not target.is_file():
        abort(404)
    try:
        if target.suffix == ".gz":
            content = gzip.decompress(target.read_bytes()).decode("utf-8", errors="replace")
        else:
            content = target.read_text(encoding="utf-8", errors="replace")
    except OSError as exc:
        abort(500, description=f"Datei nicht lesbar: {exc}")
    return render_template(
        "tesm_core/logs_file.html", name=name, lines=content.splitlines()[-MAX_TAIL_LINES:]
    )


@bp.get("/verlauf/archiv/<day>")
@permission_required("logs_history.view")
def history_archive(day: str):
    if len(day) != 10 or day.count("-") != 2:
        abort(404)
    rows = audit.read_archived_day(core().audit_archive_dir, day)
    if not rows:
        abort(404)
    return render_template("tesm_core/logs_archive.html", day=day, rows=rows)


@bp.post("/verlauf/archiv-export")
@permission_required("logs_history.export")
def history_export():
    """Packt die archivierten Audit-Tage in ein ZIP.

    Anders als im Vorgaenger loescht der Export die Dateien **nicht**: ein
    Download, der Beweismittel vom Server entfernt, ist keine gute Idee. Das
    Loeschen ist ein eigener, ausdruecklicher Schritt.
    """
    extension = core()
    entries = audit.archived_days(extension.audit_archive_dir)
    if not entries:
        flash("Es sind keine Archivdateien vorhanden.", "info")
        return redirect(url_for("logs.history"))

    buffer = io.BytesIO()
    with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
        for entry in entries:
            archive.write(entry["path"], arcname=Path(entry["path"]).name)
    buffer.seek(0)

    with transaction() as conn:
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="logs.archive_exported",
            detail=f"{len(entries)} Tage",
            ip=client_ip(),
            severity="notice",
        )
    return Response(
        buffer.getvalue(),
        mimetype="application/zip",
        headers={
            "Content-Disposition": f'attachment; filename="{extension.config.app_key}-auditarchiv.zip"'
        },
    )


@bp.post("/verlauf/archiv-loeschen")
@permission_required("logs_history.delete")
def history_purge():
    removed = 0
    for entry in audit.archived_days(core().audit_archive_dir):
        try:
            Path(entry["path"]).unlink()
            removed += 1
        except OSError:
            continue
    with transaction() as conn:
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="logs.archive_deleted",
            detail=f"{removed} Dateien",
            ip=client_ip(),
            severity="warning",
        )
    flash(f"{removed} Archivdatei(en) entfernt.", "success")
    return redirect(url_for("logs.history"))


@bp.get("/aenderungen")
@permission_required("logs_audit.view", "logs_audit.export", mode="any")
def audit_view():
    conn = db()
    page = max(1, int(request.args.get("seite") or 1))
    per_page = 100
    return render_template(
        "tesm_core/logs_audit.html",
        rows=audit.search(
            conn,
            query=(request.args.get("q") or "").strip(),
            action=(request.args.get("aktion") or "").strip(),
            actor=(request.args.get("benutzer") or "").strip(),
            severity=(request.args.get("stufe") or "").strip(),
            limit=per_page,
            offset=(page - 1) * per_page,
        ),
        total=audit.count(conn),
        actions=audit.distinct_actions(conn),
        chain=audit.verify_chain(conn, limit=2000),
        page=page,
        per_page=per_page,
        filters={
            "q": request.args.get("q") or "",
            "aktion": request.args.get("aktion") or "",
            "benutzer": request.args.get("benutzer") or "",
            "stufe": request.args.get("stufe") or "",
        },
    )


@bp.get("/aenderungen/export")
@permission_required("logs_audit.export")
def audit_export():
    conn = db()
    rows = audit.search(conn, limit=100000)
    lines = ["ts;actor;action;target;detail;ip;severity;hash"]
    for row in rows:
        lines.append(
            ";".join(
                str(row.get(key) or "").replace(";", ",").replace("\n", " ")
                for key in ("ts", "actor", "action", "target", "detail", "ip", "severity", "hash")
            )
        )
    with transaction():
        audit.log(
            conn,
            actor=current_user.username,
            actor_id=current_user.id,
            action="logs.audit_exported",
            detail=f"{len(rows)} Zeilen",
            ip=client_ip(),
            severity="notice",
        )
    return Response(
        "\n".join(lines).encode("utf-8-sig"),
        mimetype="text/csv; charset=utf-8",
        headers={
            "Content-Disposition": (
                f'attachment; filename="{core().config.app_key}-aenderungsprotokoll.csv"'
            )
        },
    )
