"""Простой in-memory rate-limiter для публичного API.

Per-IP sliding window. Достаточно для одного backend-инстанса (MVP).
Защищает от абуза `/api/v1/*` — скрейп-эндпоинты уже за admin-токеном,
но estimate/geocode/suggest публичны.

Лимиты намеренно щедрые — обычный пользователь их не достигнет, режутся боты.
"""

from __future__ import annotations

import time
from collections import defaultdict, deque

from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

from app.core.config import settings

# Окно и лимит читаются из settings (env RATE_LIMIT / RATE_LIMIT_WINDOW_S):
# не более settings.rate_limit запросов за settings.rate_limit_window_s секунд
# с одного IP.


class RateLimitMiddleware(BaseHTTPMiddleware):
    """Sliding-window rate limit на /api/v1/*. Health и статика — без лимита."""

    def __init__(self, app) -> None:  # type: ignore[no-untyped-def]
        super().__init__(app)
        self._hits: dict[str, deque[float]] = defaultdict(deque)

    async def dispatch(self, request: Request, call_next):  # type: ignore[no-untyped-def]
        path = request.url.path
        # Лимитируем только API; health и прочее — пропускаем.
        if not path.startswith("/api/"):
            return await call_next(request)

        # Аутентифицированный трафик не лимитируем: Caddy basic_auth ставит
        # X-Authenticated-User на каждый запрос пилота. Лимит — только для
        # анонимного трафика (заголовок отсутствует/пуст). #655.
        if request.headers.get("x-authenticated-user"):
            return await call_next(request)

        ip = _client_ip(request)
        now = time.monotonic()
        bucket = self._hits[ip]

        # Выкидываем устаревшие отметки за пределами окна.
        cutoff = now - settings.rate_limit_window_s
        while bucket and bucket[0] < cutoff:
            bucket.popleft()

        if len(bucket) >= settings.rate_limit:
            retry = int(settings.rate_limit_window_s - (now - bucket[0])) + 1
            return JSONResponse(
                status_code=429,
                content={"detail": "Слишком много запросов. Попробуйте позже."},
                headers={"Retry-After": str(retry)},
            )

        bucket.append(now)
        # Лёгкая защита от утечки памяти — чистим пустые корзины изредка.
        if len(self._hits) > 10000:
            for k in [k for k, v in self._hits.items() if not v]:
                del self._hits[k]

        return await call_next(request)


def _client_ip(request: Request) -> str:
    """Реальный IP за реверс-прокси (Caddy/nginx ставят X-Forwarded-For)."""
    xff = request.headers.get("x-forwarded-for")
    if xff:
        return xff.split(",")[0].strip()
    return request.client.host if request.client else "unknown"
