"""Ratenbegrenzung und Kontosperre, in SQLite persistiert.

Der Vorgaenger hatte keinerlei Bremse: ein Skript konnte beliebig schnell
Passwoerter gegen ``/login`` durchprobieren, und ein Neustart des Dienstes
haette einen In-Memory-Zaehler ohnehin geleert.

Hier: ein Sliding-Window-Log in der Tabelle ``rate_events``. Persistent, damit
ein Dienst-Neustart die Bremse nicht aufhebt, und mit derselben Verbindung
nutzbar, die der Request schon offen hat (keine zweite Verbindung -> kein
Deadlock).
"""

from __future__ import annotations

import sqlite3
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

from ..db import Database


@dataclass(frozen=True, slots=True)
class RateVerdict:
    allowed: bool
    remaining: int
    retry_after_seconds: int

    @property
    def blocked(self) -> bool:
        return not self.allowed


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _iso(value: datetime) -> str:
    return value.isoformat(timespec="seconds")


def record(conn: sqlite3.Connection, bucket: str, key: str) -> None:
    """Vermerkt ein Ereignis (z. B. einen Fehlversuch)."""
    conn.execute(
        "INSERT INTO rate_events (bucket, subject, ts) VALUES (?,?,?)",
        (bucket, key, _iso(_now())),
    )


def check(
    conn: sqlite3.Connection,
    bucket: str,
    key: str,
    *,
    limit: int,
    window_minutes: int,
) -> RateVerdict:
    """Prueft, ob noch Versuche frei sind. Zaehlt selbst nichts hoch."""
    horizon = _iso(_now() - timedelta(minutes=window_minutes))
    rows = Database.all(
        conn,
        "SELECT ts FROM rate_events WHERE bucket=? AND subject=? AND ts>=? ORDER BY ts",
        (bucket, key, horizon),
    )
    used = len(rows)
    if used < limit:
        return RateVerdict(allowed=True, remaining=limit - used, retry_after_seconds=0)

    oldest = datetime.fromisoformat(rows[0]["ts"])
    if oldest.tzinfo is None:
        oldest = oldest.replace(tzinfo=timezone.utc)
    retry_after = int((oldest + timedelta(minutes=window_minutes) - _now()).total_seconds())
    return RateVerdict(allowed=False, remaining=0, retry_after_seconds=max(1, retry_after))


def clear(conn: sqlite3.Connection, bucket: str, key: str) -> None:
    """Loescht die Ereignisse eines Subjekts -- nach erfolgreicher Anmeldung."""
    conn.execute("DELETE FROM rate_events WHERE bucket=? AND subject=?", (bucket, key))


def prune(conn: sqlite3.Connection, *, keep_hours: int = 48) -> int:
    """Raeumt alte Ereignisse ab. Laeuft im Wartungslauf mit."""
    horizon = _iso(_now() - timedelta(hours=keep_hours))
    cursor = conn.execute("DELETE FROM rate_events WHERE ts < ?", (horizon,))
    return cursor.rowcount or 0
