import time 
import functools
import inspect
from collections import defaultdict
from typing import Callable, Optional


class RateLimitExceeded(Exception):
    """Exception raised when the rate limit is exceeded."""
    pass


class RateLimiter:
    """
    Advanced class-based rate limiter decorator.

    Features:
    - Global or per-user rate limiting
    - Stores timestamps in attributes
    - Allows dynamic updates of limit/window/cooldown
    - Tracks cooldown durations
    """


    def __init__(
            self, 
            limit:int = 5,
            window:int = 10,
            per_user:bool = False,
            cooldown:int = 0
    ):
        """
        :param limit: Max number of calls allowed.
        :param window: Time window in seconds, it stores the time range (in seconds) where counting is applied..
        :param per_user: If True → rate limit per user. If False → global.
        :param cooldown: Optional cooldown after limit exceeded, cooldown = 30 → After user hits rate limit, they must wait 30s before calling again.
        """
        self._limit = limit
        self._window = window
        self._per_user = per_user
        self._cooldown = cooldown
        # Store call timestamps and cooldowns
        self._calls = defaultdict(list)
        # Track cooldown end times
        self._cooldowns = defaultdict(float)

    
    def update_limit(self, new_limit:int):
        """Update the rate limit."""
        self._limit = new_limit

    def update_window(self, new_window:int):
        """Update the time window."""
        self._window = new_window

    def update_cooldown(self, new_cooldown:int):
        """Update the cooldown period."""
        self._cooldown = new_cooldown

    def update_per_user(self, per_user:bool):
        """Update the per_user setting."""
        self._per_user = per_user


    def __call__(self, func: Callable):

            is_async = inspect.iscoroutinefunction(func)

            @functools.wraps(func)
            async def async_wrapper(*args, **kwargs):
                self._check_limit(kwargs)

                result = await func(*args, **kwargs)
                return result

            @functools.wraps(func)
            def sync_wrapper(*args, **kwargs):
                self._check_limit(kwargs)

                result = func(*args, **kwargs)
                return result

            async_wrapper.rate_limiter = self
            sync_wrapper.rate_limiter = self

            return async_wrapper if is_async else sync_wrapper
    

    def _check_limit(self, kwargs):
        key = kwargs.get("user_id") if self._per_user else "GLOBAL"
        if not key:
            raise RateLimitExceeded("user_id is required for per-user rate limit")

        now = time.time()
        timestamps = self._calls[key]

        self._calls[key] = [ts for ts in timestamps if now - ts < self._window]
        timestamps = self._calls[key]

        if now < self._cooldowns[key]:
            remaining = round(self._cooldowns[key] - now, 2)
            raise RateLimitExceeded(
                f"Cooldown active. Try again in {remaining}s"
            )

        if len(timestamps) >= self._limit:
            if self._cooldown > 0:
                self._cooldowns[key] = now + self._cooldown
            raise RateLimitExceeded(
                f"Rate limit exceeded ({self._limit} calls / {self._window}s)"
            )

        self._calls[key].append(now)

    
