"""
Async rate limiter for parallel ETL extraction.

Provides a sliding-window token-bucket limiter that enforces
max_calls_per_minute across concurrent asyncio tasks.
"""

from __future__ import annotations

import asyncio
import logging
import time
from collections import deque

logger = logging.getLogger(__name__)


class AsyncRateLimiter:
    """
    Sliding-window rate limiter for async contexts.

    Usage::

        limiter = AsyncRateLimiter(max_calls_per_minute=300)

        async def fetch(symbol):
            await limiter.acquire()
            ...  # make API call

        await asyncio.gather(*[fetch(s) for s in symbols])
    """

    def __init__(self, max_calls_per_minute: int):
        self.max_calls_per_minute = max_calls_per_minute
        self._call_times: deque[float] = deque()
        self._lock = asyncio.Lock()

    async def acquire(self) -> None:
        """Wait until a call slot is available, then record the call."""
        async with self._lock:
            now = time.time()

            # Evict calls older than 60 s
            while self._call_times and self._call_times[0] < now - 60.0:
                self._call_times.popleft()

            # If at limit, sleep until oldest call expires
            if len(self._call_times) >= self.max_calls_per_minute:
                wait = 60.0 - (now - self._call_times[0]) + 0.1
                if wait > 0:
                    logger.debug(
                        "Rate limit reached (%d calls/min), waiting %.2fs",
                        self.max_calls_per_minute,
                        wait,
                    )
                    await asyncio.sleep(wait)
                    now = time.time()
                    while self._call_times and self._call_times[0] < now - 60.0:
                        self._call_times.popleft()

            self._call_times.append(time.time())
