"""
grantflow/api/rate_limit.py
---------------------------
Sliding-window rate limiter for agent-facing endpoints.

Disabled by default.  Enable via environment variable:

    GRANTFLOW_RATE_LIMITING_ENABLED=true

When Redis is available (GRANTFLOW_REDIS_URL set), the limiter uses Redis for
cross-replica enforcement.  Otherwise, an in-process counter is used (effective
for single-process deployments).

Usage inside a FastAPI route:

    from grantflow.api.rate_limit import rate_limit
    from fastapi import Depends

    @router.post("/generate", dependencies=[Depends(rate_limit(limit=20, window_seconds=60))])
    async def generate(...):
        ...
"""

from __future__ import annotations

import os
import threading
import time
from typing import Callable

from fastapi import HTTPException, Request

from grantflow.api.errors import agent_error_detail


def _rate_limiting_enabled() -> bool:
    return os.getenv("GRANTFLOW_RATE_LIMITING_ENABLED", "false").strip().lower() == "true"


def _client_key(request: Request) -> str:
    """Derive a rate-limit bucket key from the request.

    Uses the first 20 characters of the API key / Bearer token when present,
    so legitimate callers are not pooled into a single IP bucket.
    Falls back to the client IP (or X-Forwarded-For) for unauthenticated traffic.
    """
    cred = (
        request.headers.get("x-api-key", "") or request.headers.get("authorization", "").removeprefix("Bearer ").strip()
    )
    if cred:
        return f"cred:{cred[:20]}"
    forwarded = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
    ip = forwarded or (request.client.host if request.client else "unknown")
    return f"ip:{ip}"


# ---------------------------------------------------------------------------
# In-process fixed-window counter
# ---------------------------------------------------------------------------

_RATE_STORE: dict[str, tuple[int, float]] = {}
_RATE_LOCK = threading.Lock()


def _in_process_increment(bucket: str, window_seconds: float) -> int:
    """Increment the counter for *bucket* within the current window.

    Returns the new count.  Resets automatically when the window expires.
    """
    now = time.monotonic()
    with _RATE_LOCK:
        count, window_start = _RATE_STORE.get(bucket, (0, now))
        if now - window_start >= window_seconds:
            count, window_start = 0, now
        count += 1
        _RATE_STORE[bucket] = (count, window_start)
    return count


# ---------------------------------------------------------------------------
# Redis fixed-window counter
# ---------------------------------------------------------------------------


def _redis_increment(bucket: str, window_seconds: int) -> int:
    """Increment the Redis counter and set a TTL on the first write.

    Returns the new count, or -1 if Redis is unavailable.
    """
    try:
        from grantflow.api.redis_store import _get_redis

        r = _get_redis()
        if r is None:
            return -1
        pipe = r.pipeline()
        pipe.incr(bucket)
        pipe.expire(bucket, window_seconds, nx=True)
        results = pipe.execute()
        return int(results[0])
    except Exception:
        return -1


# ---------------------------------------------------------------------------
# Public factory
# ---------------------------------------------------------------------------


def rate_limit(
    limit: int = 60,
    window_seconds: int = 60,
    endpoint: str = "",
) -> Callable:
    """Return a FastAPI dependency that enforces a per-client rate limit.

    Args:
        limit:          Maximum number of requests per window.
        window_seconds: Duration of the window in seconds.
        endpoint:       Short label used as part of the Redis key (e.g. "generate").

    Raises:
        HTTPException(429) when the limit is exceeded.
    """

    def dependency(request: Request) -> None:
        if not _rate_limiting_enabled():
            return

        client = _client_key(request)
        window_ts = int(time.time()) // window_seconds
        bucket = f"grantflow:rl:{endpoint}:{client}:{window_ts}"

        # Prefer Redis (shared across replicas); fall back to in-process.
        from grantflow.api.redis_store import redis_available

        if redis_available():
            count = _redis_increment(bucket, window_seconds)
            if count < 0:
                count = _in_process_increment(bucket, float(window_seconds))
        else:
            count = _in_process_increment(bucket, float(window_seconds))

        if count > limit:
            raise HTTPException(
                status_code=429,
                headers={
                    "Retry-After": str(window_seconds),
                    "X-RateLimit-Limit": str(limit),
                    "X-RateLimit-Remaining": "0",
                },
                detail=agent_error_detail(
                    code="rate_limit_exceeded",
                    message=f"Rate limit exceeded: {limit} requests per {window_seconds}s.",
                    retryable=True,
                    next_action=f"Wait up to {window_seconds} seconds and retry.",
                    limit=limit,
                    window_seconds=window_seconds,
                ),
            )

    return dependency
