"""
Rate Limiter Implementation

Thread-safe rate limiter with sliding window algorithm for API key-based limiting.
"""

import logging
import time
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
from threading import Lock
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from backend.data.async_request_db import AsyncRequestDB

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class RateLimitConfig:
    """Rate limit configuration for an API key."""

    requests_per_minute: int = 10
    max_concurrent_jobs: int = 3
    min_poll_interval_ms: int = 1000  # Minimum time between status polls


@dataclass
class RateLimitStatus:
    """Current rate limit status."""

    allowed: bool
    remaining_requests: int
    reset_at: datetime
    retry_after_seconds: int | None = None
    reason: str | None = None


class RateLimiter:
    """
    Thread-safe rate limiter with sliding window algorithm.

    Tracks:
    - Requests per minute (sliding window)
    - Concurrent active jobs
    - Poll frequency per request_id
    """

    def __init__(self, db: "AsyncRequestDB") -> None:
        self._db = db
        self._lock = Lock()
        # In-memory tracking for fast checks
        self._request_windows: dict[str, list[float]] = defaultdict(list)
        # (api_key, request_id) -> last_poll timestamp
        self._poll_timestamps: dict[tuple[str, str], float] = {}
        # Cache for API key configs (TTL 60 seconds)
        self._config_cache: dict[str, tuple[RateLimitConfig, float]] = {}
        self._config_cache_ttl = 60.0

    def check_submit_limit(self, api_key: str) -> RateLimitStatus:
        """Check if API key can submit a new request."""
        config = self._get_config(api_key)

        with self._lock:
            now = time.time()
            window_start = now - 60  # 1 minute window

            # Clean old entries
            self._request_windows[api_key] = [
                ts for ts in self._request_windows[api_key]
                if ts > window_start
            ]

            current_count = len(self._request_windows[api_key])

            if current_count >= config.requests_per_minute:
                oldest = min(self._request_windows[api_key])
                retry_after = int(oldest + 60 - now) + 1
                return RateLimitStatus(
                    allowed=False,
                    remaining_requests=0,
                    reset_at=datetime.utcnow() + timedelta(seconds=retry_after),
                    retry_after_seconds=max(1, retry_after),
                    reason="Rate limit exceeded: too many requests per minute",
                )

            # Check concurrent jobs (query database) - inside lock for thread safety
            active_jobs = self._db.count_active_jobs(api_key)
            if active_jobs >= config.max_concurrent_jobs:
                return RateLimitStatus(
                    allowed=False,
                    remaining_requests=config.requests_per_minute - current_count,
                    reset_at=datetime.utcnow() + timedelta(seconds=30),
                    retry_after_seconds=30,
                    reason=f"Max concurrent jobs ({config.max_concurrent_jobs}) reached",
                )

            return RateLimitStatus(
                allowed=True,
                remaining_requests=config.requests_per_minute - current_count - 1,
                reset_at=datetime.utcnow() + timedelta(seconds=60),
            )

    def record_request(self, api_key: str) -> None:
        """Record a successful request submission."""
        with self._lock:
            self._request_windows[api_key].append(time.time())

        # Also record in database for persistence
        try:
            self._db.record_rate_limit_event(api_key, "request")
        except Exception as e:
            logger.warning(f"Failed to record rate limit event: {e}")

    def check_poll_limit(self, api_key: str, request_id: str) -> RateLimitStatus:
        """Check if polling is allowed (prevent abuse)."""
        config = self._get_config(api_key)
        key = (api_key, request_id)

        with self._lock:
            now = time.time()
            last_poll = self._poll_timestamps.get(key, 0)
            elapsed_ms = (now - last_poll) * 1000

            if elapsed_ms < config.min_poll_interval_ms:
                # Suggest exponential backoff
                wait_ms = min(
                    config.min_poll_interval_ms * 2,
                    5000,  # Max 5 seconds
                )
                retry_after = int(wait_ms / 1000) + 1
                return RateLimitStatus(
                    allowed=False,
                    remaining_requests=0,
                    reset_at=datetime.utcnow() + timedelta(milliseconds=wait_ms),
                    retry_after_seconds=retry_after,
                    reason="Polling too frequently. Please wait before retrying.",
                )

            # Update poll timestamp
            self._poll_timestamps[key] = now

            return RateLimitStatus(
                allowed=True,
                remaining_requests=999,  # No limit on poll count, just frequency
                reset_at=datetime.utcnow(),
            )

    def _get_config(self, api_key: str) -> RateLimitConfig:
        """Get rate limit config for API key with caching."""
        now = time.time()

        # Check cache
        if api_key in self._config_cache:
            cached_config, cached_at = self._config_cache[api_key]
            if now - cached_at < self._config_cache_ttl:
                return cached_config

        # Query database
        db_config = self._db.get_api_key_config(api_key)
        if db_config:
            config = RateLimitConfig(
                requests_per_minute=db_config.requests_per_minute,
                max_concurrent_jobs=db_config.max_concurrent_jobs,
            )
        else:
            config = RateLimitConfig()  # Default limits

        # Cache result
        self._config_cache[api_key] = (config, now)
        return config

    def cleanup_poll_timestamps(self, max_age_seconds: int = 3600) -> int:
        """Clean up old poll timestamps to prevent memory leak."""
        with self._lock:
            now = time.time()
            cutoff = now - max_age_seconds
            old_keys = [
                k for k, v in self._poll_timestamps.items()
                if v < cutoff
            ]
            for key in old_keys:
                del self._poll_timestamps[key]
            return len(old_keys)

    def cleanup_request_windows(self) -> None:
        """Clean up expired entries from request windows."""
        with self._lock:
            now = time.time()
            window_start = now - 60

            for api_key in list(self._request_windows.keys()):
                self._request_windows[api_key] = [
                    ts for ts in self._request_windows[api_key]
                    if ts > window_start
                ]
                # Remove empty entries
                if not self._request_windows[api_key]:
                    del self._request_windows[api_key]

    def get_rate_limit_headers(self, status: RateLimitStatus) -> dict[str, str]:
        """Generate rate limit headers for HTTP response."""
        headers = {
            "X-RateLimit-Remaining": str(status.remaining_requests),
            "X-RateLimit-Reset": status.reset_at.isoformat(),
        }
        if status.retry_after_seconds:
            headers["Retry-After"] = str(status.retry_after_seconds)
        return headers
