"""Tool-class rate limiting for agent access.

Reuses the fixed-window Redis pattern already used for auth email throttling
(``src/domain/auth/request_throttle.py``) and the builder chat/transcribe/screenshot
limits (``src/domain/builder/router.py``): INCR + EXPIRE on a 60-second key.

Classes, not individual tools, are limited so a new read tool is safe by default
(unclassified tools are not limited here; add them to TOOL_RATE_CLASS explicitly).
Limiting is keyed on both the grant and the workspace so multiple grants in the
same workspace cannot add up to an unbounded aggregate.
"""
from __future__ import annotations

from uuid import UUID

from redis import asyncio as aioredis

from src.core.exceptions import RateLimitError

READ_LIGHT = "read.light"
READ_EXPENSIVE = "read.expensive"
WRITE_STANDARD = "write.standard"
COMMERCE = "commerce"

TOOL_RATE_CLASS: dict[str, str] = {
    "list_workspaces": READ_LIGHT,
    "list_projects": READ_LIGHT,
    "get_project": READ_LIGHT,
    "list_builds": READ_LIGHT,
    "get_build": READ_LIGHT,
    "get_operation": READ_LIGHT,
    "list_credit_packages": READ_LIGHT,
    "estimate_build": READ_EXPENSIVE,
    "create_project": WRITE_STANDARD,
    "start_build": WRITE_STANDARD,
    "request_asset_upload": WRITE_STANDARD,
    "confirm_asset_upload": WRITE_STANDARD,
    "create_credit_checkout": COMMERCE,
    "purchase_credits": COMMERCE,
}

# (grant limit, workspace limit) per rolling 60-second window.
_LIMITS: dict[str, tuple[int, int]] = {
    READ_LIGHT: (120, 600),
    READ_EXPENSIVE: (20, 100),
    WRITE_STANDARD: (20, 60),
    COMMERCE: (10, 20),
}

# High-impact classes must fail closed: a limiter that is temporarily unreachable
# must not silently become unlimited spend or unlimited build submission. Read
# classes fail open so a Redis hiccup does not take down inspection tools.
_FAIL_CLOSED = {WRITE_STANDARD, COMMERCE}


async def _window_incr(redis: aioredis.Redis, key: str) -> int:
    count = await redis.incr(key)
    if count == 1:
        await redis.expire(key, 60)
    return count


async def enforce_tool_rate_limit(
    redis: aioredis.Redis | None,
    *,
    grant_id: UUID,
    workspace_id: UUID | None,
    tool_name: str,
) -> None:
    """Raise RateLimitError (-> 429) when the grant or workspace exceeds its class limit.

    Called for read tools before dispatch, and for mutation tools only after an
    idempotency-key cache hit has already returned — so retrying an accepted
    request with the same request_key never consumes or is blocked by the limiter.
    """
    rate_class = TOOL_RATE_CLASS.get(tool_name)
    if rate_class is None or redis is None:
        return
    grant_limit, workspace_limit = _LIMITS[rate_class]
    try:
        grant_count = await _window_incr(redis, f"p2hs:agent-rl:{rate_class}:grant:{grant_id}")
        # No workspace counter when the call does not target one (e.g. list_workspaces).
        workspace_count = (
            await _window_incr(redis, f"p2hs:agent-rl:{rate_class}:workspace:{workspace_id}")
            if workspace_id is not None
            else 0
        )
    except Exception:
        if rate_class in _FAIL_CLOSED:
            raise RateLimitError(
                "Agent access is temporarily unavailable. Try again shortly."
            ) from None
        return
    if grant_count > grant_limit or workspace_count > workspace_limit:
        raise RateLimitError(f"Too many {rate_class} requests for this grant")
