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 LeakyBucketAlgorithm:
    """Leaky bucket rate limiting algorithm."""

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

        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())

        # Leaky bucket parameters
        # Capacity = limit (bucket size)
        # Leak rate = limit / window (requests per second)
        capacity = rule.limit
        leak_rate = rule.limit / rule.window

        # Use Lua script for atomic leaky bucket operations
        script = """
        local key = KEYS[1]
        local current_time = tonumber(ARGV[1])
        local capacity = tonumber(ARGV[2])
        local leak_rate = tonumber(ARGV[3])
        local ttl = tonumber(ARGV[4])

        local bucket = redis.call('HMGET', key, 'level', 'last_leak')
        local level = tonumber(bucket[1]) or 0
        local last_leak = tonumber(bucket[2]) or current_time

        -- Leak water based on time elapsed
        local time_elapsed = current_time - last_leak
        local leaked = math.floor(time_elapsed * leak_rate)
        level = math.max(0, level - leaked)
        last_leak = current_time

        -- Check if bucket has space
        if level < capacity then
            level = level + 1
            redis.call('HMSET', key, 'level', level, 'last_leak', last_leak)
            redis.call('EXPIRE', key, ttl)
            local retry_after = 0
            if level >= capacity then
                retry_after = math.ceil((level - capacity + 1) / leak_rate)
            end
            return {1, capacity - level, retry_after}
        else
            -- Bucket is full, calculate retry_after
            local retry_after = math.ceil((level - capacity + 1) / leak_rate)
            redis.call('HMSET', key, 'level', level, 'last_leak', last_leak)
            redis.call('EXPIRE', key, ttl)
            return {0, 0, retry_after}
        end
        """

        try:
            result = await storage.execute_script(
                script,
                keys=[key],
                args=[current_time, capacity, leak_rate, 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
                return await self._fallback_allow(storage, key, rule, current_time)
        except Exception:
            # Fallback to simple counter
            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
