"""Protokollierung: strukturierte Zeilen, Vorgangsnummer, Rotation via logrotate.

Jede Anfrage bekommt eine ``request_id``. Sie steht in jeder Logzeile, in
Fehlerseiten und im Audit-Log -- damit ein Anruf "bei mir kam Fehler 500"
in Sekunden auffindbar ist. Der Vorgaenger loggte ohne Korrelations-ID.

Rotation macht ``logrotate`` mit ``copytruncate`` (siehe ``deploy/``), nicht
Python: mehrere Prozesse (Web, Monitor) schreiben in dasselbe Verzeichnis, und
keiner von ihnen kann ein Reopen-Signal verarbeiten.
"""

from __future__ import annotations

import json
import logging
import logging.handlers
import os
import sys
import time
import uuid
from pathlib import Path

from flask import Flask, g, has_request_context, request

ACCESS_LOGGER = "tesm.access"


class RequestFormatter(logging.Formatter):
    """Textformat fuer Menschen, mit Vorgangsnummer und Benutzer."""

    def format(self, record: logging.LogRecord) -> str:
        if has_request_context():
            record.request_id = getattr(g, "request_id", "-")
            record.actor = getattr(getattr(g, "user", None), "username", "-") or "-"
        else:
            record.request_id = getattr(record, "request_id", "-")
            record.actor = getattr(record, "actor", "-")
        return super().format(record)


class JsonFormatter(logging.Formatter):
    """Maschinenlesbares Format -- fuer die Weiterverarbeitung in einem SIEM."""

    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime(record.created)),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        if has_request_context():
            payload["request_id"] = getattr(g, "request_id", "")
            payload["path"] = request.path
            payload["method"] = request.method
            payload["actor"] = getattr(getattr(g, "user", None), "username", "") or ""
        if record.exc_info:
            payload["exception"] = self.formatException(record.exc_info)
        for key, value in getattr(record, "extra_fields", {}).items():
            payload[key] = value
        return json.dumps(payload, ensure_ascii=False)


def init_app(app: Flask, *, log_dir: Path, debug: bool, json_logs: bool = False) -> None:
    log_dir.mkdir(parents=True, exist_ok=True)
    level = logging.DEBUG if debug else logging.INFO

    root = logging.getLogger()
    root.setLevel(level)
    for handler in list(root.handlers):
        root.removeHandler(handler)

    formatter: logging.Formatter = (
        JsonFormatter()
        if json_logs
        else RequestFormatter(
            "%(asctime)s %(levelname)-7s [%(request_id)s] %(actor)s %(name)s: %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
        )
    )

    stream = logging.StreamHandler(sys.stderr)
    stream.setFormatter(formatter)
    root.addHandler(stream)

    try:
        app_log = log_dir / "app.log"
        file_handler = logging.FileHandler(app_log, encoding="utf-8", delay=True)
        file_handler.setFormatter(formatter)
        file_handler.setLevel(level)
        root.addHandler(file_handler)
        if app_log.exists():
            try:
                os.chmod(app_log, 0o640)
            except OSError:
                pass
    except OSError as exc:  # pragma: no cover - z. B. fehlende Rechte
        root.warning("Konnte app.log nicht oeffnen: %s", exc)

    # Zugriffsprotokoll separat: eine Zeile pro Anfrage, ohne Stacktraces.
    access = logging.getLogger(ACCESS_LOGGER)
    access.propagate = False
    access.setLevel(logging.INFO)
    try:
        access_handler = logging.FileHandler(log_dir / "access.log", encoding="utf-8", delay=True)
        access_handler.setFormatter(
            JsonFormatter() if json_logs else logging.Formatter("%(asctime)s %(message)s")
        )
        access.addHandler(access_handler)
    except OSError:  # pragma: no cover
        pass

    logging.getLogger("werkzeug").setLevel(logging.WARNING)

    @app.before_request
    def _assign_request_id() -> None:
        incoming = request.headers.get("X-Request-Id", "")
        g.request_id = incoming[:64] if incoming.isascii() and incoming else uuid.uuid4().hex[:16]
        g.request_started = time.perf_counter()

    @app.after_request
    def _log_access(response):  # type: ignore[no-untyped-def]
        if request.path.startswith("/static/"):
            return response
        duration = (time.perf_counter() - getattr(g, "request_started", time.perf_counter())) * 1000
        actor = getattr(getattr(g, "user", None), "username", "") or "-"
        access.info(
            "%s %s %s %s %s %.1fms",
            getattr(g, "request_id", "-"),
            actor,
            request.method,
            request.full_path if request.query_string else request.path,
            response.status_code,
            duration,
        )
        response.headers.setdefault("X-Request-Id", getattr(g, "request_id", ""))
        return response
