"""
Rate Limiter - Enforce rate limits on tool execution.

Parses rate limit declarations like "100/hour" and enforces them
during tool execution using a sliding window algorithm.
"""
import re
import time
import threading
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
from typing import Optional


# Regex to parse rate limit strings: "100/hour", "1000/day", "10/minute"
RATE_LIMIT_PATTERN = re.compile(
    r"^(\d+)\s*/\s*(second|minute|hour|day)s?$",
    re.IGNORECASE
)

# Time units in seconds
TIME_UNITS = {
    "second": 1,
    "minute": 60,
    "hour": 3600,
    "day": 86400,
}


class RateLimitError(Exception):
    """Error parsing or enforcing rate limit."""
    pass


class RateLimitExceeded(Exception):
    """Rate limit has been exceeded."""

    def __init__(
        self,
        message: str,
        tool_name: str,
        limit: int,
        window_seconds: int,
        retry_after: Optional[float] = None,
    ):
        super().__init__(message)
        self.tool_name = tool_name
        self.limit = limit
        self.window_seconds = window_seconds
        self.retry_after = retry_after


@dataclass
class RateLimitSpec:
    """Parsed rate limit specification."""
    limit: int  # Number of requests allowed
    window_seconds: int  # Time window in seconds
    original: str  # Original string representation

    @classmethod
    def parse(cls, rate_limit: str) -> "RateLimitSpec":
        """
        Parse a rate limit string like "100/hour".

        Args:
            rate_limit: Rate limit string (e.g., "100/hour", "1000/day")

        Returns:
            RateLimitSpec with parsed values

        Raises:
            RateLimitError: If the string format is invalid
        """
        match = RATE_LIMIT_PATTERN.match(rate_limit.strip())
        if not match:
            raise RateLimitError(
                f"Invalid rate limit format: '{rate_limit}'. "
                "Expected format: '<number>/<unit>' (e.g., '100/hour', '1000/day')"
            )

        limit = int(match.group(1))
        unit = match.group(2).lower()

        if limit <= 0:
            raise RateLimitError(f"Rate limit must be positive: {limit}")

        window_seconds = TIME_UNITS[unit]

        return cls(
            limit=limit,
            window_seconds=window_seconds,
            original=rate_limit,
        )

    def __str__(self) -> str:
        return self.original


@dataclass
class RateLimitResult:
    """Result of a rate limit check."""
    allowed: bool
    remaining: int  # Requests remaining in current window
    reset_after: float  # Seconds until window resets
    limit: int  # Total limit
    window_seconds: int  # Window size in seconds


class RateLimiterBackend(ABC):
    """Abstract base class for rate limiter backends."""

    @abstractmethod
    def check_and_increment(
        self,
        key: str,
        limit: int,
        window_seconds: int,
    ) -> RateLimitResult:
        """
        Check if a request is allowed and increment the counter if so.

        Args:
            key: Unique key for the rate limit (e.g., tool name)
            limit: Maximum requests allowed in the window
            window_seconds: Window size in seconds

        Returns:
            RateLimitResult with allowed status and metadata
        """
        pass

    @abstractmethod
    def get_status(
        self,
        key: str,
        limit: int,
        window_seconds: int,
    ) -> RateLimitResult:
        """
        Get current rate limit status without incrementing.

        Args:
            key: Unique key for the rate limit
            limit: Maximum requests allowed in the window
            window_seconds: Window size in seconds

        Returns:
            RateLimitResult with current status
        """
        pass

    @abstractmethod
    def reset(self, key: str) -> None:
        """
        Reset the rate limit counter for a key.

        Args:
            key: Unique key to reset
        """
        pass

    @abstractmethod
    def clear_all(self) -> None:
        """Clear all rate limit counters."""
        pass


class InMemoryRateLimiter(RateLimiterBackend):
    """
    In-memory rate limiter using sliding window algorithm.

    Thread-safe implementation suitable for single-process applications.
    For distributed systems, use a Redis-backed implementation.
    """

    def __init__(self):
        self._windows: dict[str, deque[float]] = {}
        self._lock = threading.Lock()

    def check_and_increment(
        self,
        key: str,
        limit: int,
        window_seconds: int,
    ) -> RateLimitResult:
        now = time.time()
        window_start = now - window_seconds

        with self._lock:
            # Get or create the window for this key
            if key not in self._windows:
                self._windows[key] = deque()

            window = self._windows[key]

            # Remove expired timestamps
            while window and window[0] < window_start:
                window.popleft()

            current_count = len(window)

            if current_count < limit:
                # Request allowed - add timestamp
                window.append(now)
                remaining = limit - current_count - 1
                reset_after = window_seconds if not window else (window[0] + window_seconds - now)
                return RateLimitResult(
                    allowed=True,
                    remaining=remaining,
                    reset_after=max(0, reset_after),
                    limit=limit,
                    window_seconds=window_seconds,
                )
            else:
                # Request denied
                oldest = window[0] if window else now
                reset_after = oldest + window_seconds - now
                return RateLimitResult(
                    allowed=False,
                    remaining=0,
                    reset_after=max(0, reset_after),
                    limit=limit,
                    window_seconds=window_seconds,
                )

    def get_status(
        self,
        key: str,
        limit: int,
        window_seconds: int,
    ) -> RateLimitResult:
        now = time.time()
        window_start = now - window_seconds

        with self._lock:
            if key not in self._windows:
                return RateLimitResult(
                    allowed=True,
                    remaining=limit,
                    reset_after=window_seconds,
                    limit=limit,
                    window_seconds=window_seconds,
                )

            window = self._windows[key]

            # Count non-expired timestamps
            count = sum(1 for ts in window if ts >= window_start)
            remaining = max(0, limit - count)

            # Calculate reset time
            oldest_valid = next((ts for ts in window if ts >= window_start), now)
            reset_after = oldest_valid + window_seconds - now

            return RateLimitResult(
                allowed=count < limit,
                remaining=remaining,
                reset_after=max(0, reset_after),
                limit=limit,
                window_seconds=window_seconds,
            )

    def reset(self, key: str) -> None:
        with self._lock:
            if key in self._windows:
                del self._windows[key]

    def clear_all(self) -> None:
        with self._lock:
            self._windows.clear()


class RateLimiter:
    """
    High-level rate limiter for tool execution.

    Wraps a backend and provides convenient methods for checking
    rate limits on tools.
    """

    def __init__(self, backend: Optional[RateLimiterBackend] = None):
        """
        Initialize the rate limiter.

        Args:
            backend: Rate limiter backend (defaults to InMemoryRateLimiter)
        """
        self._backend = backend or InMemoryRateLimiter()
        self._specs: dict[str, RateLimitSpec] = {}

    def configure_tool(self, tool_name: str, rate_limit: str) -> None:
        """
        Configure rate limit for a tool.

        Args:
            tool_name: Name of the tool
            rate_limit: Rate limit string (e.g., "100/hour")
        """
        self._specs[tool_name] = RateLimitSpec.parse(rate_limit)

    def check(
        self,
        tool_name: str,
        rate_limit: Optional[str] = None,
        key_suffix: Optional[str] = None,
    ) -> RateLimitResult:
        """
        Check if a tool execution is allowed and consume a request if so.

        Args:
            tool_name: Name of the tool
            rate_limit: Rate limit string (uses configured limit if not provided)
            key_suffix: Optional suffix for the key (e.g., user_id for per-user limits)

        Returns:
            RateLimitResult with allowed status

        Raises:
            RateLimitError: If no rate limit is configured or provided
        """
        # Get or parse rate limit spec
        if rate_limit:
            spec = RateLimitSpec.parse(rate_limit)
        elif tool_name in self._specs:
            spec = self._specs[tool_name]
        else:
            raise RateLimitError(f"No rate limit configured for tool: {tool_name}")

        # Build the key
        key = f"tool:{tool_name}"
        if key_suffix:
            key = f"{key}:{key_suffix}"

        return self._backend.check_and_increment(key, spec.limit, spec.window_seconds)

    def check_or_raise(
        self,
        tool_name: str,
        rate_limit: Optional[str] = None,
        key_suffix: Optional[str] = None,
    ) -> RateLimitResult:
        """
        Check rate limit and raise RateLimitExceeded if exceeded.

        Args:
            tool_name: Name of the tool
            rate_limit: Rate limit string (uses configured limit if not provided)
            key_suffix: Optional suffix for the key

        Returns:
            RateLimitResult if allowed

        Raises:
            RateLimitExceeded: If rate limit is exceeded
            RateLimitError: If no rate limit is configured
        """
        result = self.check(tool_name, rate_limit, key_suffix)

        if not result.allowed:
            raise RateLimitExceeded(
                f"Rate limit exceeded for tool '{tool_name}': "
                f"{result.limit} requests per {result.window_seconds} seconds",
                tool_name=tool_name,
                limit=result.limit,
                window_seconds=result.window_seconds,
                retry_after=result.reset_after,
            )

        return result

    def get_status(
        self,
        tool_name: str,
        rate_limit: Optional[str] = None,
        key_suffix: Optional[str] = None,
    ) -> RateLimitResult:
        """
        Get current rate limit status without consuming a request.

        Args:
            tool_name: Name of the tool
            rate_limit: Rate limit string (uses configured limit if not provided)
            key_suffix: Optional suffix for the key

        Returns:
            RateLimitResult with current status
        """
        if rate_limit:
            spec = RateLimitSpec.parse(rate_limit)
        elif tool_name in self._specs:
            spec = self._specs[tool_name]
        else:
            raise RateLimitError(f"No rate limit configured for tool: {tool_name}")

        key = f"tool:{tool_name}"
        if key_suffix:
            key = f"{key}:{key_suffix}"

        return self._backend.get_status(key, spec.limit, spec.window_seconds)

    def reset(self, tool_name: str, key_suffix: Optional[str] = None) -> None:
        """
        Reset rate limit counter for a tool.

        Args:
            tool_name: Name of the tool
            key_suffix: Optional suffix for the key
        """
        key = f"tool:{tool_name}"
        if key_suffix:
            key = f"{key}:{key_suffix}"
        self._backend.reset(key)

    def clear_all(self) -> None:
        """Clear all rate limit counters."""
        self._backend.clear_all()
        self._specs.clear()


def parse_rate_limit(rate_limit: str) -> RateLimitSpec:
    """
    Parse a rate limit string.

    Convenience function for parsing rate limits without a RateLimiter instance.

    Args:
        rate_limit: Rate limit string (e.g., "100/hour")

    Returns:
        RateLimitSpec with parsed values
    """
    return RateLimitSpec.parse(rate_limit)


def validate_rate_limit(rate_limit: str) -> list[str]:
    """
    Validate a rate limit string.

    Args:
        rate_limit: Rate limit string to validate

    Returns:
        List of validation warnings (empty if valid)
    """
    warnings = []
    try:
        spec = RateLimitSpec.parse(rate_limit)
        if spec.limit > 10000:
            warnings.append(f"Very high rate limit ({spec.limit}) may not provide effective protection")
        if spec.window_seconds < 60:
            warnings.append("Rate limit window less than 1 minute may cause burst issues")
    except RateLimitError as e:
        warnings.append(str(e))
    return warnings
