"""
In-memory rate limiter for matrix-bridge-dagi (H1)

Sliding window algorithm — no external dependencies (no Redis needed for M1).
Thread-safe within a single asyncio event loop.

Two independent limiters per message:
  - room limiter:   max N messages per room per minute
  - sender limiter: max N messages per sender per minute

Usage:
    rl = InMemoryRateLimiter(room_rpm=20, sender_rpm=10)
    allowed, limit_type = rl.check(room_id="!abc:server", sender="@user:server")
    if not allowed:
        # reject, audit matrix.rate_limited
"""

import time
from collections import defaultdict, deque
from typing import Deque, Dict, Optional, Tuple


class InMemoryRateLimiter:
    """
    Sliding-window rate limiter, independent per room and per sender.

    Windows are pruned lazily on each check — no background task needed.
    Each bucket stores timestamps (float) of accepted events within the window.
    """

    _WINDOW_S: float = 60.0  # 1-minute window

    def __init__(self, room_rpm: int = 20, sender_rpm: int = 10) -> None:
        if room_rpm < 1 or sender_rpm < 1:
            raise ValueError("RPM limits must be >= 1")
        self._room_rpm = room_rpm
        self._sender_rpm = sender_rpm

        # buckets: key → deque of accepted timestamps
        self._room_buckets: Dict[str, Deque[float]] = defaultdict(deque)
        self._sender_buckets: Dict[str, Deque[float]] = defaultdict(deque)

    @property
    def room_rpm(self) -> int:
        return self._room_rpm

    @property
    def sender_rpm(self) -> int:
        return self._sender_rpm

    def check(self, room_id: str, sender: str) -> Tuple[bool, Optional[str]]:
        """
        Check if message is allowed.

        Returns:
            (True, None)          — allowed
            (False, "room")       — room limit exceeded
            (False, "sender")     — sender limit exceeded

        Room is checked first; if room limit hit, sender bucket is NOT updated
        to avoid penalising user's quota for messages already blocked.
        """
        now = time.monotonic()
        cutoff = now - self._WINDOW_S

        # Check room
        room_bucket = self._room_buckets[room_id]
        self._prune(room_bucket, cutoff)
        if len(room_bucket) >= self._room_rpm:
            return False, "room"

        # Check sender
        sender_bucket = self._sender_buckets[sender]
        self._prune(sender_bucket, cutoff)
        if len(sender_bucket) >= self._sender_rpm:
            return False, "sender"

        # Both allowed — record
        room_bucket.append(now)
        sender_bucket.append(now)
        return True, None

    def reset(self, room_id: Optional[str] = None, sender: Optional[str] = None) -> None:
        """Clear buckets — useful for tests or manual ops reset."""
        if room_id is not None:
            self._room_buckets.pop(room_id, None)
        if sender is not None:
            self._sender_buckets.pop(sender, None)

    def stats(self) -> dict:
        """Read-only snapshot of current bucket sizes (for /health or ops)."""
        now = time.monotonic()
        cutoff = now - self._WINDOW_S
        return {
            "room_rpm_limit": self._room_rpm,
            "sender_rpm_limit": self._sender_rpm,
            "active_rooms": sum(
                1 for b in self._room_buckets.values()
                if any(t > cutoff for t in b)
            ),
            "active_senders": sum(
                1 for b in self._sender_buckets.values()
                if any(t > cutoff for t in b)
            ),
        }

    @staticmethod
    def _prune(bucket: Deque[float], cutoff: float) -> None:
        """Remove timestamps older than the window."""
        while bucket and bucket[0] <= cutoff:
            bucket.popleft()
