"""Strukturiertes Entscheidungsprotokoll (JSON Lines).

Jede Zeile ist ein vollständiger Datensatz einer Entscheidung oder eines
Ergebnisses. Prompt-, Antwort- oder Memory-Klartext wird nie geschrieben,
nur Digest und Länge. Der Wächter verweigert Datensätze mit Klartext-
Schlüsseln (text, prompt, content, ...), damit ein späterer Fehler im
Aufrufer nicht still Inhalte protokolliert.
"""
from __future__ import annotations

import fcntl
import json
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable

from .state import ensure_private_dir

FORBIDDEN_KEYS = frozenset({
    "text", "prompt", "content", "reply_text", "output", "message_text", "memory_text",
    "stdin", "data", "clearance", "mac",
})


class DecisionLogError(RuntimeError):
    pass


def _check(value: Any, path: str = "") -> None:
    if isinstance(value, dict):
        for key, item in value.items():
            if not isinstance(key, str):
                raise DecisionLogError(f"NON_STRING_KEY:{path}")
            if key in FORBIDDEN_KEYS:
                raise DecisionLogError(f"FORBIDDEN_LOG_FIELD:{path}{key}")
            _check(item, f"{path}{key}.")
    elif isinstance(value, (list, tuple)):
        for item in value:
            _check(item, path)


class DecisionLog:
    """Append-only JSON Lines. Mit ``retention_seconds > 0`` werden Datensätze, deren ``ts`` älter ist,
    höchstens einmal je ``prune_interval_seconds`` entfernt (E17). Das Kürzen läuft unter derselben
    Dateisperre an Ort und Stelle (gleiche Inode), damit kein paralleler Schreiber in eine ersetzte
    Datei schreibt. Zeilen ohne lesbaren ``ts`` bleiben erhalten."""

    def __init__(self, path: Path, retention_seconds: int = 0, *, clock: Callable[[], float] = time.time,
                 prune_interval_seconds: int = 86400):
        self.path = Path(path)
        self._retention = int(retention_seconds)
        self._clock = clock
        self._interval = int(prune_interval_seconds)
        self._marker = self.path.with_name(self.path.name + ".pruned")

    def write(self, record: dict) -> None:
        _check(record)
        line = json.dumps(record, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
        ensure_private_dir(self.path.parent)
        fd = os.open(self.path, os.O_RDWR | os.O_CREAT | os.O_APPEND, 0o600)
        try:
            fcntl.flock(fd, fcntl.LOCK_EX)
            os.write(fd, (line + "\n").encode("utf-8"))
            os.fsync(fd)
            if self._retention > 0 and self._prune_due():
                self._prune(fd)
        finally:
            try:
                fcntl.flock(fd, fcntl.LOCK_UN)
            finally:
                os.close(fd)

    def _prune_due(self) -> bool:
        try:
            return os.stat(self._marker).st_mtime + self._interval <= self._clock()
        except FileNotFoundError:
            return True

    def _prune(self, fd: int) -> None:
        cutoff = self._clock() - self._retention
        os.lseek(fd, 0, os.SEEK_SET)
        chunks = []
        while True:
            chunk = os.read(fd, 1 << 20)
            if not chunk:
                break
            chunks.append(chunk)
        lines = b"".join(chunks).decode("utf-8").splitlines(keepends=True)
        kept = [entry for entry in lines if entry.strip() and not _older_than(entry, cutoff)]
        if len(kept) != len([entry for entry in lines if entry.strip()]):
            os.ftruncate(fd, 0)
            if kept:
                os.write(fd, "".join(kept).encode("utf-8"))
            os.fsync(fd)
        mfd = os.open(self._marker, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
        os.close(mfd)
        now = self._clock()
        os.utime(self._marker, (now, now))

    def read_all(self) -> list[dict]:
        try:
            with open(self.path, "r", encoding="utf-8") as handle:
                return [json.loads(line) for line in handle if line.strip()]
        except FileNotFoundError:
            return []


def _older_than(line: str, cutoff: float) -> bool:
    try:
        ts = json.loads(line).get("ts")
        return datetime.fromisoformat(str(ts).replace("Z", "+00:00")).timestamp() < cutoff
    except (ValueError, TypeError, AttributeError):
        return False
