# #region RateLimiterModule [C:4] [TYPE Module] [SEMANTICS rate_limit, security, throttle, auth]
# @BRIEF Simple in-memory rate limiter for auth endpoints.
#   Tracks request frequency per IP and blocks excessive requests.
#   For production, replace with Redis-based limiter.
# @LAYER Core
# @INVARIANT  Never blocks successful auth — only repeated failures.
#             Window resets after ATTEMPT_WINDOW seconds of no activity.
# @REJECTED Redis-backed limiter — not available in all deployments.
#           Per-user limiting — user isn't known until after auth.
from collections import defaultdict
import threading
import time

# ── Configuration ──
MAX_ATTEMPTS: int = 10          # Max failed attempts per window
ATTEMPT_WINDOW: int = 300       # Window in seconds (5 min)
BAN_DURATION: int = 900         # Ban duration after limit reached (15 min)


class RateLimiter:
    """In-memory sliding-window rate limiter keyed by IP address.

    Thread-safe. Tracks failed attempts. After MAX_ATTEMPTS failures within
    ATTEMPT_WINDOW seconds, the IP is banned for BAN_DURATION seconds.
    """

    def __init__(self):
        self._lock = threading.Lock()
        # ip -> [timestamp, ...] sorted list of attempt timestamps
        self._attempts: dict[str, list[float]] = defaultdict(list)
        # ip -> ban_expires_at
        self._bans: dict[str, float] = {}

    # #region is_banned [TYPE Function]
    # @BRIEF Check if IP is currently banned.
    # @PRE  ip is a valid IP string.
    # @POST  Returns True if IP is banned and ban hasn't expired.
    def is_banned(self, ip: str) -> bool:
        with self._lock:
            expires = self._bans.get(ip)
            if expires is None:
                return False
            if time.monotonic() > expires:
                del self._bans[ip]
                return False
            return True
    # #endregion is_banned

    # #region record_attempt [TYPE Function]
    # @BRIEF Record a failed attempt from an IP.
    # @PRE  ip is a valid IP string.
    # @POST  If MAX_ATTEMPTS exceeded within window, IP is banned.
    def record_attempt(self, ip: str) -> None:
        with self._lock:
            now = time.monotonic()
            window_start = now - ATTEMPT_WINDOW

            # Prune old attempts
            self._attempts[ip] = [t for t in self._attempts[ip] if t > window_start]
            self._attempts[ip].append(now)

            if len(self._attempts[ip]) > MAX_ATTEMPTS:
                self._bans[ip] = now + BAN_DURATION
                del self._attempts[ip]
    # #endregion record_attempt

    # #region record_success [TYPE Function]
    # @BRIEF Clear attempt history on successful auth.
    # @PRE  ip is a valid IP string.
    # @POST  Attempt history for IP is cleared.
    def record_success(self, ip: str) -> None:
        with self._lock:
            self._attempts.pop(ip, None)
    # #endregion record_success


# Singleton
rate_limiter = RateLimiter()
# #endregion RateLimiterModule
