"""Redis-based thread-safe rate limiting."""

import os
import time
from typing import Any, Dict, Optional, Tuple

import redis
import structlog

logger = structlog.get_logger(__name__)

# Redis connection constants
REDIS_DEFAULT_HOST = "localhost"
REDIS_DEFAULT_PORT = "6379"
REDIS_DEFAULT_DB = "0"
REDIS_SOCKET_TIMEOUT = 5

# Rate limiting constants
DEFAULT_RATE_LIMIT_REQUESTS = 10
DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 60
FAILED_LOGIN_RETENTION_SECONDS = 3600  # 1 hour
LOGIN_RATE_LIMIT_REQUESTS = 5
LOGIN_RATE_LIMIT_WINDOW_SECONDS = 900  # 15 minutes
DEFAULT_FAILED_LOGIN_WINDOW_MINUTES = 60
SECONDS_PER_MINUTE = 60

# Redis key prefixes
RATE_LIMIT_KEY_PREFIX = "rate_limit:"
FAILED_LOGINS_IP_PREFIX = "failed_logins:ip:"
FAILED_LOGINS_USER_PREFIX = "failed_logins:user:"
LOGIN_LIMIT_PREFIX = "login_limit:"

# Rate limit headers
HEADER_RATE_LIMIT = "X-RateLimit-Limit"
HEADER_RATE_LIMIT_WINDOW = "X-RateLimit-Window"
HEADER_RATE_LIMIT_USED = "X-RateLimit-Used"
HEADER_RATE_LIMIT_REMAINING = "X-RateLimit-Remaining"
HEADER_RATE_LIMIT_RESET = "X-RateLimit-Reset"
HEADER_RATE_LIMIT_STATUS = "X-RateLimit-Status"


class RedisRateLimiter:
    """Thread-safe Redis-based rate limiter with sliding window algorithm."""

    def __init__(self) -> None:
        """Initialize Redis rate limiter."""
        self.redis_client = redis.Redis(
            host=os.getenv("REDIS_HOST", REDIS_DEFAULT_HOST),
            port=int(os.getenv("REDIS_PORT", REDIS_DEFAULT_PORT)),
            password=os.getenv("REDIS_PASSWORD"),
            db=int(os.getenv("REDIS_DB", REDIS_DEFAULT_DB)),
            decode_responses=True,
            socket_connect_timeout=REDIS_SOCKET_TIMEOUT,
            socket_timeout=REDIS_SOCKET_TIMEOUT,
            retry_on_timeout=True,
        )

        try:
            self.redis_client.ping()
            logger.info("Rate limiter Redis connection established")
        except redis.ConnectionError as e:
            logger.error("Failed to connect to Redis for rate limiting", error=str(e))
            raise
    
    def check_rate_limit(
        self,
        client_ip: str,
        requests_limit: int = DEFAULT_RATE_LIMIT_REQUESTS,
        window_seconds: int = DEFAULT_RATE_LIMIT_WINDOW_SECONDS,
    ) -> Tuple[bool, Dict[str, int]]:
        """
        Check rate limit using sliding window algorithm.

        Args:
            client_ip: Client IP address.
            requests_limit: Maximum number of requests.
            window_seconds: Time window in seconds.

        Returns:
            Tuple of (allowed: bool, headers: Dict[str, int]).
        """
        try:
            current_time = int(time.time())
            window_start = current_time - window_seconds

            key = f"{RATE_LIMIT_KEY_PREFIX}{client_ip}"

            lua_script = """
            local key = KEYS[1]
            local window_start = tonumber(ARGV[1])
            local current_time = tonumber(ARGV[2])
            local requests_limit = tonumber(ARGV[3])
            local window_seconds = tonumber(ARGV[4])

            -- Remove old entries (outside window)
            redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)

            -- Count current requests
            local current_requests = redis.call('ZCARD', key)

            -- Check limit
            if current_requests >= requests_limit then
                -- Return blocking information
                local oldest_request = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
                local reset_time = oldest_request[2] + window_seconds
                return {0, current_requests, reset_time}
            else
                -- Add current request
                redis.call('ZADD', key, current_time, current_time)
                redis.call('EXPIRE', key, window_seconds)

                -- Count updated requests
                local new_count = redis.call('ZCARD', key)
                return {1, new_count, 0}
            end
            """

            result = self.redis_client.eval(
                lua_script, 1, key, window_start, current_time, requests_limit, window_seconds
            )

            allowed = bool(result[0])
            current_requests = result[1]
            reset_time = result[2] if result[2] > 0 else 0

            headers = {
                HEADER_RATE_LIMIT: requests_limit,
                HEADER_RATE_LIMIT_WINDOW: window_seconds,
                HEADER_RATE_LIMIT_USED: current_requests,
                HEADER_RATE_LIMIT_REMAINING: max(0, requests_limit - current_requests),
            }

            if reset_time > 0:
                headers[HEADER_RATE_LIMIT_RESET] = reset_time

            if allowed:
                logger.debug(
                    "Rate limit check passed",
                    client_ip=client_ip,
                    current_requests=current_requests,
                    limit=requests_limit,
                )
            else:
                logger.warning(
                    "Rate limit exceeded",
                    client_ip=client_ip,
                    current_requests=current_requests,
                    limit=requests_limit,
                    reset_time=reset_time,
                )

            return allowed, headers

        except redis.RedisError as e:
            logger.error(
                "Redis error during rate limit check", client_ip=client_ip, error=str(e)
            )
            return True, {
                HEADER_RATE_LIMIT: requests_limit,
                HEADER_RATE_LIMIT_WINDOW: window_seconds,
                HEADER_RATE_LIMIT_USED: 0,
                HEADER_RATE_LIMIT_REMAINING: requests_limit,
                HEADER_RATE_LIMIT_STATUS: "redis_error",
            }
        except Exception as e:
            logger.error(
                "Unexpected error during rate limit check", client_ip=client_ip, error=str(e)
            )
            return True, {
                HEADER_RATE_LIMIT: requests_limit,
                HEADER_RATE_LIMIT_WINDOW: window_seconds,
                HEADER_RATE_LIMIT_USED: 0,
                HEADER_RATE_LIMIT_REMAINING: requests_limit,
                HEADER_RATE_LIMIT_STATUS: "error",
            }
    
    def check_login_rate_limit(
        self, client_ip: str, username: Optional[str] = None
    ) -> Tuple[bool, Dict[str, int]]:
        """
        Special rate limit for login endpoint.

        Args:
            client_ip: Client IP address.
            username: Username (optional).

        Returns:
            Tuple of (allowed: bool, headers: Dict[str, int]).
        """
        allowed, headers = self.check_rate_limit(
            client_ip, LOGIN_RATE_LIMIT_REQUESTS, LOGIN_RATE_LIMIT_WINDOW_SECONDS
        )

        if username and allowed:
            user_key = f"{LOGIN_LIMIT_PREFIX}{username}"
            user_allowed, user_headers = self.check_rate_limit(
                user_key, LOGIN_RATE_LIMIT_REQUESTS, LOGIN_RATE_LIMIT_WINDOW_SECONDS
            )

            if not user_allowed:
                logger.warning(
                    "Login rate limit exceeded for user",
                    username=username,
                    client_ip=client_ip,
                )
                return False, user_headers

        return allowed, headers
    
    def record_failed_login(self, client_ip: str, username: str) -> None:
        """
        Record failed login attempt for brute-force attack tracking.

        Args:
            client_ip: Client IP address.
            username: Username.
        """
        try:
            current_time = int(time.time())

            ip_key = f"{FAILED_LOGINS_IP_PREFIX}{client_ip}"
            self.redis_client.zadd(ip_key, {current_time: current_time})
            self.redis_client.expire(ip_key, FAILED_LOGIN_RETENTION_SECONDS)

            user_key = f"{FAILED_LOGINS_USER_PREFIX}{username}"
            self.redis_client.zadd(user_key, {current_time: current_time})
            self.redis_client.expire(user_key, FAILED_LOGIN_RETENTION_SECONDS)

            logger.debug("Failed login recorded", client_ip=client_ip, username=username)

        except Exception as e:
            logger.error(
                "Failed to record failed login attempt",
                client_ip=client_ip,
                username=username,
                error=str(e),
            )
    
    def get_failed_login_count(
        self,
        client_ip: str,
        username: Optional[str] = None,
        window_minutes: int = DEFAULT_FAILED_LOGIN_WINDOW_MINUTES,
    ) -> Dict[str, int]:
        """
        Get count of failed login attempts.

        Args:
            client_ip: Client IP address.
            username: Username (optional).
            window_minutes: Time window in minutes.

        Returns:
            Dictionary with failed login counts.
        """
        try:
            current_time = int(time.time())
            window_start = current_time - (window_minutes * SECONDS_PER_MINUTE)

            result = {"ip_failed_count": 0, "user_failed_count": 0}

            ip_key = f"{FAILED_LOGINS_IP_PREFIX}{client_ip}"
            ip_count = self.redis_client.zcount(ip_key, window_start, current_time)
            result["ip_failed_count"] = ip_count

            if username:
                user_key = f"{FAILED_LOGINS_USER_PREFIX}{username}"
                user_count = self.redis_client.zcount(user_key, window_start, current_time)
                result["user_failed_count"] = user_count

            return result

        except Exception as e:
            logger.error(
                "Failed to get failed login count",
                client_ip=client_ip,
                username=username,
                error=str(e),
            )
            return {"ip_failed_count": 0, "user_failed_count": 0}
    
    def clear_failed_logins(
        self, client_ip: str, username: Optional[str] = None
    ) -> None:
        """
        Clear failed login attempt records.

        Args:
            client_ip: Client IP address.
            username: Username (optional).
        """
        try:
            ip_key = f"{FAILED_LOGINS_IP_PREFIX}{client_ip}"
            self.redis_client.delete(ip_key)

            if username:
                user_key = f"{FAILED_LOGINS_USER_PREFIX}{username}"
                self.redis_client.delete(user_key)

            logger.debug(
                "Failed login records cleared", client_ip=client_ip, username=username
            )

        except Exception as e:
            logger.error(
                "Failed to clear failed login records",
                client_ip=client_ip,
                username=username,
                error=str(e),
            )
    
    def get_rate_limit_stats(self) -> Dict[str, Any]:
        """
        Get rate limiting statistics.

        Returns:
            Rate limiting statistics dictionary.
        """
        try:
            rate_limit_keys = self.redis_client.keys(f"{RATE_LIMIT_KEY_PREFIX}*")
            failed_login_keys = self.redis_client.keys(f"{FAILED_LOGINS_IP_PREFIX}*")

            return {
                "active_rate_limits": len(rate_limit_keys),
                "failed_login_trackers": len(failed_login_keys),
                "redis_memory_usage": (
                    self.redis_client.memory_usage(f"{RATE_LIMIT_KEY_PREFIX}*")
                    + self.redis_client.memory_usage(f"{FAILED_LOGINS_IP_PREFIX}*")
                ),
            }

        except Exception as e:
            logger.error("Failed to get rate limit stats", error=str(e))
            return {
                "active_rate_limits": 0,
                "failed_login_trackers": 0,
                "redis_memory_usage": 0,
                "error": str(e),
            }


redis_rate_limiter = RedisRateLimiter()
