import time
from typing import Tuple

from rate_limiter.core.algorithms.base import RateLimitAlgorithm
from rate_limiter.core.rules import RateLimitRule, Scope
from rate_limiter.core.storage.backend import StorageBackend


class SlidingWindowAlgorithm:
    """Sliding window rate limiting algorithm using sorted sets."""

    async def allow(
        self,
        storage: StorageBackend,
        identity: str,
        rule: RateLimitRule,
        endpoint: str = "",
    ) -> Tuple[bool, int, int]:
        """
        Check if request should be allowed using sliding window.

        Args:
            storage: Storage backend
            identity: Request identity
            rule: Rate limit rule
            endpoint: Endpoint identifier

        Returns:
            Tuple of (allowed, remaining, retry_after)
        """
        # Create key based on scope
        # Include limit and window in key to ensure each rule has its own counter
        rule_id = f"{rule.limit}:{rule.window}"
        if rule.scope == Scope.GLOBAL:
            key = f"{rule.key}:{identity}:global:{rule_id}"
        elif rule.scope == Scope.ROUTER:
            key = f"{rule.key}:{identity}:router:{endpoint}:{rule_id}"
        else:  # endpoint
            key = f"{rule.key}:{identity}:endpoint:{endpoint}:{rule_id}"

        current_time = int(time.time())
        window_start = current_time - rule.window

        # Use Lua script for atomic sliding window calculation
        script = """
        local key = KEYS[1]
        local window_start = tonumber(ARGV[1])
        local limit = tonumber(ARGV[2])
        local current_time = tonumber(ARGV[3])
        local ttl = tonumber(ARGV[4])

        -- Remove old entries
        redis.call('ZREMRANGEBYSCORE', key, 0, window_start)

        -- Count current entries
        local count = redis.call('ZCARD', key)

        if count < limit then
            -- Add current request
            redis.call('ZADD', key, current_time, current_time)
            redis.call('EXPIRE', key, ttl)
            return {1, limit - count - 1, 0}
        else
            -- Get oldest entry to calculate retry_after
            local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
            local retry_after = 0
            if oldest and #oldest > 0 then
                retry_after = tonumber(oldest[2]) + ttl - current_time
            end
            return {0, 0, retry_after}
        end
        """

        try:
            result = await storage.execute_script(
                script,
                keys=[key],
                args=[window_start, rule.limit, current_time, rule.window],
            )

            if isinstance(result, list) and len(result) >= 3:
                allowed = bool(result[0])
                remaining = int(result[1])
                retry_after = max(0, int(result[2]))
            else:
                # Fallback to simple counter if script execution fails
                return await self._fallback_allow(storage, key, rule, current_time)
        except Exception:
            # Fallback to simple counter if script execution fails
            return await self._fallback_allow(storage, key, rule, current_time)

        return allowed, remaining, retry_after

    async def _fallback_allow(
        self,
        storage: StorageBackend,
        key: str,
        rule: RateLimitRule,
        current_time: int,
    ) -> Tuple[bool, int, int]:
        """Fallback to simple counter-based approach."""
        window_key = f"{key}:window:{current_time // rule.window}"
        count = await storage.incr(window_key, rule.window)
        allowed = count <= rule.limit
        remaining = max(0, rule.limit - count)
        retry_after = rule.window - (current_time % rule.window)
        return allowed, remaining, retry_after
