#!/usr/bin/env python3
from __future__ import annotations

import logging
import threading
import time
from collections import deque
from typing import Callable, Dict


_logger = logging.getLogger(__name__)


class RateLimiter:
    """Simple in-process rate limiter (max_calls per period seconds).

    Thread-safe; not multi-process safe. When the limit is reached, callers
    block (sleep) until a slot becomes available.
    """

    def __init__(self, *, name: str, max_calls: int, period: float) -> None:
        self.name = name
        self.max_calls = int(max_calls)
        self.period = float(period)
        self._lock = threading.Lock()
        self._calls: deque[float] = deque()

    def acquire(self) -> None:
        while True:
            with self._lock:
                now = time.monotonic()
                # drop expired timestamps
                while self._calls and (now - self._calls[0]) >= self.period:
                    self._calls.popleft()
                if len(self._calls) < self.max_calls:
                    self._calls.append(now)
                    return
                # need to wait until oldest exits the window
                sleep_for = max(0.0, self.period - (now - self._calls[0]))
            if sleep_for > 0:
                _logger.info(
                    f"rate-limit: throttling {self.name}: sleeping {sleep_for:.2f}s (limit {self.max_calls}/{int(self.period)}s)"
                )
                time.sleep(sleep_for)


_limiters: Dict[str, RateLimiter] = {}
_limiters_lock = threading.Lock()


def _get_limiter(key: str, *, max_calls: int, period: float) -> RateLimiter:
    with _limiters_lock:
        lim = _limiters.get(key)
        if lim is None:
            lim = RateLimiter(name=key, max_calls=max_calls, period=period)
            _limiters[key] = lim
        return lim


def rate_limited(*, max_calls: int = 8, period: float = 60.0, key: str | None = None) -> Callable:
    """Decorator to rate-limit a function globally within this process.

    Args:
        max_calls: Max invocations allowed within the period
        period: Sliding window size in seconds
        key: Optional limiter key (defaults to module.function)
    """

    def _decorator(fn: Callable) -> Callable:
        limiter_key = key or f"{fn.__module__}.{getattr(fn, '__name__', 'call')}"
        limiter = _get_limiter(limiter_key, max_calls=max_calls, period=period)

        def _wrapped(*args, **kwargs):
            limiter.acquire()
            return fn(*args, **kwargs)

        return _wrapped

    return _decorator

