"""In-memory rate limiter with tenant, app, and IP dimensions.

The public interface is intentionally simple so it can be swapped for Redis or
another distributed backend later without changing the gateway contract.
"""

from __future__ import annotations

import threading
import time
from collections import defaultdict, deque
from dataclasses import dataclass
from typing import Deque


@dataclass(frozen=True, slots=True)
class RateLimitResult:
    """Outcome of a rate-limit check."""

    allowed: bool
    key: str
    remaining: int
    limit: int
    window_seconds: int


class RateLimiter:
    """Windowed in-memory limiter prepared for future Redis replacement."""

    def __init__(self, *, default_limit: int = 120, window_seconds: int = 60) -> None:
        self.default_limit = default_limit
        self.window_seconds = window_seconds
        self._events: dict[str, Deque[float]] = defaultdict(deque)
        self._lock = threading.Lock()

    def build_key(self, *, tenant_id: str | None, app_id: str | None, ip_address: str | None) -> str:
        if tenant_id and app_id:
            return f"tenant:{tenant_id}:app:{app_id}"
        if tenant_id:
            return f"tenant:{tenant_id}"
        return f"ip:{ip_address or 'unknown'}"

    def check(self, *, request=None, tenant=None, app_id: str | None = None) -> RateLimitResult:
        tenant_id = (tenant or {}).get("tenant_id") if tenant else None
        ip_address = getattr(request, "remote_addr", None) if request is not None else None
        key = self.build_key(tenant_id=tenant_id, app_id=app_id, ip_address=ip_address)
        now = time.time()
        with self._lock:
            events = self._events[key]
            while events and (now - events[0]) >= self.window_seconds:
                events.popleft()
            if len(events) >= self.default_limit:
                return RateLimitResult(False, key, 0, self.default_limit, self.window_seconds)
            events.append(now)
            remaining = self.default_limit - len(events)
            return RateLimitResult(True, key, remaining, self.default_limit, self.window_seconds)

