"""
rate_limit.py — limiteur de tentatives PERSISTANT (MySQL), par clé (ex. IP).

Stocké en base et non en mémoire : la fenêtre est partagée entre tous les
workers uvicorn et survit à un redémarrage — un attaquant ne peut plus
remettre son compteur à zéro en provoquant un restart.

    from core.rate_limit import enforce
    enforce(f"photos-lookup:{ip}", max_attempts=20, window_seconds=300)
"""
import time
from typing import Optional

from fastapi import HTTPException

from core.db import get_db, named_lock
from core.security import client_ip  # noqa: F401  (ré-export historique)


def enforce(
    key: str, max_attempts: int, window_seconds: int,
    *, audit_action: Optional[str] = None, ip_address: Optional[str] = None,
) -> None:
    """Lève HTTPException(429) si `key` a dépassé `max_attempts` dans les
    `window_seconds` dernières secondes. Sinon enregistre la tentative."""
    now = time.time()
    cutoff = now - window_seconds
    # Count-then-insert must be atomic per key or a burst of parallel requests slips past the limit.
    with named_lock(f"rate:{key}"), get_db() as conn:
        conn.execute("DELETE FROM rate_hits WHERE key=? AND ts<?", (key, cutoff))
        n = conn.execute("SELECT COUNT(*) AS n FROM rate_hits WHERE key=?", (key,)).fetchone()["n"]
        blocked = n >= max_attempts
        if not blocked:
            conn.execute("INSERT INTO rate_hits (key, ts) VALUES (?,?)", (key, now))
        conn.commit()
    if blocked:
        if audit_action:
            from services import audit_service
            audit_service.log_event(
                audit_action, ip_address=ip_address,
                details={"max_attempts": max_attempts, "window_seconds": window_seconds},
                severity="critical",
            )
        raise HTTPException(429, detail="Too many attempts — try again later.")


def purge_old(max_age_seconds: int = 86400) -> int:
    with get_db() as conn:
        cur = conn.execute("DELETE FROM rate_hits WHERE ts<?", (time.time() - max_age_seconds,))
        conn.commit()
        return cur.rowcount


def reset_all() -> None:
    """Vide tous les compteurs (tests)."""
    with get_db() as conn:
        conn.execute("DELETE FROM rate_hits")
        conn.commit()


def is_blocked(key: str, max_attempts: int, window_seconds: int) -> bool:
    """Vrai si `key` a déjà `max_attempts` événements enregistrés dans la fenêtre.
    Ne compte rien lui-même : à associer à `record_hit` (ex. n'enregistrer que les échecs)."""
    cutoff = time.time() - window_seconds
    with get_db() as conn:
        n = conn.execute("SELECT COUNT(*) AS n FROM rate_hits WHERE key=? AND ts>=?", (key, cutoff)).fetchone()["n"]
    return n >= max_attempts


def record_hit(key: str) -> None:
    with get_db() as conn:
        conn.execute("INSERT INTO rate_hits (key, ts) VALUES (?,?)", (key, time.time()))
        conn.commit()
