"""A sliding-window rate limiter (lesson 18).

In-process, so each worker or replica counts separately. With several replicas, keep the same
interface and store the windows in Redis (a sorted set per key) instead.
"""
import math
import time
from collections import defaultdict, deque
from collections.abc import Callable

from fastapi import Request

from .errors import ApiError
from .security import CurrentUser


class SlidingWindowLimiter:
    def __init__(self, clock: Callable[[], float] = time.monotonic) -> None:
        self.clock = clock
        self.hits: dict[str, deque[float]] = defaultdict(deque)

    def retry_after(self, key: str, limit: int, window: float = 60.0) -> float | None:
        """Seconds to wait if the key is at its limit, else None (records nothing)."""
        now = self.clock()
        recent = self.hits[key]
        while recent and recent[0] <= now - window:
            recent.popleft()
        return recent[0] + window - now if len(recent) >= limit else None

    def hit(self, key: str, limit: int, window: float = 60.0) -> float | None:
        """Record one hit unless the key is over its limit; return seconds to wait, else None."""
        wait = self.retry_after(key, limit, window)
        if wait is None:
            self.hits[key].append(self.clock())
        return wait


def raise_if_limited(retry_after: float | None) -> None:
    if retry_after is not None:
        seconds = max(1, math.ceil(retry_after))
        raise ApiError(429, f"too many requests; retry in {seconds} s", type_="rate-limited",
                       headers={"Retry-After": str(seconds)})


async def per_user_limit(request: Request, principal: CurrentUser) -> None:
    limiter: SlidingWindowLimiter = request.state.limiter
    raise_if_limited(limiter.hit(f"user:{principal.id}", request.state.settings.rate_limit_per_minute))
