"""Redis connection management module.

This module provides the RedisInstance class for managing both
synchronous and asynchronous Redis connections with connection pooling,
URL construction, and context managers for safe resource handling.
"""

import socket as _socket
import urllib.parse
from contextlib import asynccontextmanager, contextmanager
from typing import AsyncGenerator, Dict, Generator, Optional

import redis
from redis.asyncio import Redis as AsyncRedis
from redis.asyncio.connection import ConnectionPool as AsyncConnectionPool
from redis.connection import ConnectionPool

from .config import RedisConfig

__all__ = ("RedisInstance",)

# Aggressive TCP keepalive timers for environments behind reverse proxies
# (e.g., Railway, Heroku). Without these, Linux defaults to TCP_KEEPIDLE=7200
# (2 hours), meaning the first probe fires long after the proxy has already
# killed the idle connection. Only Linux exposes all three knobs.
_KEEPALIVE_OPTIONS: Dict[int, int] = {}
if hasattr(_socket, "TCP_KEEPIDLE"):
    _KEEPALIVE_OPTIONS = {
        _socket.TCP_KEEPIDLE: 15,   # seconds idle before first probe
        _socket.TCP_KEEPINTVL: 5,   # seconds between subsequent probes
        _socket.TCP_KEEPCNT: 3,     # failed probes before declaring dead
    }


class RedisInstance:
    """Manage synchronous and asynchronous Redis connection pools.

    Provides unified access to Redis clients with connection pooling for both
    synchronous and asynchronous operations. Handles connection URL construction
    from configuration parameters and lazy initialization of connection pools.

    Attributes:
        config: RedisConfig object containing connection parameters.
        redis_url: Constructed Redis connection URL string.

    Example:
        >>> config = RedisConfig() \\
        ...     .set_redis_host("localhost") \\
        ...     .set_redis_port(6379) \\
        ...     .set_redis_db("0")
        >>> redis_instance = RedisInstance(config)
        >>> 
        >>> # Synchronous usage
        >>> with redis_instance.get_redis() as client:
        ...     client.set("key", "value")
        ...     value = client.get("key")
        >>> 
        >>> # Asynchronous usage
        >>> async with redis_instance.get_async_redis() as client:
        ...     await client.set("key", "value")
        ...     value = await client.get("key")
    """

    def __init__(self, redis_config: RedisConfig) -> None:
        """Initialize Redis instance from configuration.

        Args:
            redis_config: RedisConfig object containing connection parameters
                such as host, port, database, authentication, etc.
        """
        self.config: RedisConfig = redis_config
        self.redis_url: str = self._set_redis_url()
        self._sync_pool: Optional[ConnectionPool] = None
        self._async_pool: Optional[AsyncConnectionPool] = None

    def _set_redis_url(self) -> str:
        """Construct the Redis connection URL from configuration.

        Builds a connection URL from individual configuration parameters if
        a full URL is not provided. Handles authentication, SSL, and URL encoding
        of credentials to ensure special characters are properly escaped.

        Returns:
            str: A complete Redis connection URL in the format:
                redis[s]://[username:password@]host:port/database

        Note:
            - Uses rediss:// protocol if SSL is enabled, redis:// otherwise
            - URL-encodes username and password to handle special characters
            - Supports username-only, password-only, or both for authentication
        """
        redis_url: Optional[str] = self.config.get("redis_url")
        if redis_url:
            return redis_url

        redis_ssl: bool = self.config.get("redis_ssl", False)
        redis_host: str = self.config.get("redis_host")
        redis_port: int = self.config.get("redis_port")
        redis_db: str = self.config.get("redis_db")
        redis_username: str = self.config.get("redis_username", "")
        redis_password: str = self.config.get("redis_password", "")

        username: str = urllib.parse.quote(redis_username) if redis_username else ""
        password: str = urllib.parse.quote(redis_password) if redis_password else ""

        auth_part: str = (
            f"{username}:{password}@"
            if username and password
            else f"{password}@" if password else f"{username}@" if username else ""
        )
        protocol: str = "rediss" if redis_ssl else "redis"

        return f"{protocol}://{auth_part}{redis_host}:{redis_port}/{redis_db}"

    def get_redis_url(self) -> str:
        """Get the Redis connection URL.

        Returns:
            str: The constructed or configured Redis connection URL.
        """
        return self.redis_url

    def _get_sync_pool(self) -> ConnectionPool:
        """Get or create a synchronous connection pool.

        Creates a connection pool on first access and reuses it for subsequent calls.
        Applies max_connections, timeouts, keepalive, health-check, and retry
        settings from configuration to prevent proxy idle-timeout drops.

        Returns:
            ConnectionPool: A synchronous Redis connection pool instance.

        Note:
            Defaults: 10 max connections, 5s socket/connect timeout, keepalive on,
            15s health-check interval, retry on timeout enabled.
        """
        if self._sync_pool is None:
            keepalive = self.config.get("redis_socket_keepalive", True)
            self._sync_pool = ConnectionPool.from_url(
                self.redis_url,
                max_connections=self.config.get("redis_max_connections", 10),
                socket_timeout=self.config.get("redis_socket_timeout", 5),
                socket_connect_timeout=self.config.get(
                    "redis_socket_connect_timeout", 5
                ),
                socket_keepalive=keepalive,
                socket_keepalive_options=(
                    _KEEPALIVE_OPTIONS if keepalive and _KEEPALIVE_OPTIONS else None
                ),
                health_check_interval=self.config.get(
                    "redis_health_check_interval", 15
                ),
                retry_on_timeout=self.config.get("redis_retry_on_timeout", True),
            )
        return self._sync_pool

    def _get_async_pool(self) -> AsyncConnectionPool:
        """Get or create an asynchronous connection pool.

        Creates a connection pool on first access and reuses it for subsequent calls.
        Applies max_connections, timeouts, keepalive, health-check, and retry
        settings from configuration to prevent proxy idle-timeout drops.

        Returns:
            AsyncConnectionPool: An asynchronous Redis connection pool instance.

        Note:
            Defaults: 10 max connections, 5s socket/connect timeout, keepalive on,
            15s health-check interval, retry on timeout enabled.
        """
        if self._async_pool is None:
            keepalive = self.config.get("redis_socket_keepalive", True)
            self._async_pool = AsyncConnectionPool.from_url(
                self.redis_url,
                max_connections=self.config.get("redis_max_connections", 10),
                socket_timeout=self.config.get("redis_socket_timeout", 5),
                socket_connect_timeout=self.config.get(
                    "redis_socket_connect_timeout", 5
                ),
                socket_keepalive=keepalive,
                socket_keepalive_options=(
                    _KEEPALIVE_OPTIONS if keepalive and _KEEPALIVE_OPTIONS else None
                ),
                health_check_interval=self.config.get(
                    "redis_health_check_interval", 15
                ),
                retry_on_timeout=self.config.get("redis_retry_on_timeout", True),
            )
        return self._async_pool

    def get_redis_client(self) -> redis.Redis:
        """Create a synchronous Redis client using the connection pool.

        Returns:
            redis.Redis: A synchronous Redis client instance configured with
                the connection pool for efficient connection reuse.

        Example:
            >>> redis_instance = RedisInstance(config)
            >>> client = redis_instance.get_redis_client()
            >>> client.set("key", "value")
            >>> client.close()
        """
        return redis.Redis(connection_pool=self._get_sync_pool())

    async def get_async_redis_client(self) -> AsyncRedis:
        """Create an asynchronous Redis client using the connection pool.

        Returns:
            AsyncRedis: An asynchronous Redis client instance configured with
                the connection pool for efficient connection reuse.

        Example:
            >>> redis_instance = RedisInstance(config)
            >>> client = await redis_instance.get_async_redis_client()
            >>> await client.set("key", "value")
            >>> await client.close()
        """
        return AsyncRedis(connection_pool=self._get_async_pool())

    async def reset_async_pool(self) -> None:
        """Reset and disconnect the asynchronous connection pool.

        Disconnects all connections in the async pool and clears the pool reference.
        The pool will be recreated on next access. Useful for cleanup or
        reconfiguration scenarios.

        Example:
            >>> await redis_instance.reset_async_pool()
        """
        if self._async_pool:
            await self._async_pool.disconnect()
        self._async_pool = None

    def reset_sync_pool(self) -> None:
        """Reset and disconnect the synchronous connection pool.

        Disconnects all connections in the sync pool and clears the pool reference.
        The pool will be recreated on next access. Useful for cleanup or
        reconfiguration scenarios.

        Example:
            >>> redis_instance.reset_sync_pool()
        """
        if self._sync_pool:
            self._sync_pool.disconnect()
        self._sync_pool = None

    @contextmanager
    def get_redis(self) -> Generator[redis.Redis, None, None]:
        """Create a context manager for a synchronous Redis client.

        Provides a Redis client within a context manager that properly handles
        cleanup. The underlying connection is returned to the pool on close.

        Yields:
            redis.Redis: A synchronous Redis client instance.

        Example:
            >>> with redis_instance.get_redis() as client:
            ...     client.set("key", "value")
            ...     value = client.get("key")
            ...     print(value)
        """
        client: redis.Redis = self.get_redis_client()
        try:
            yield client
        finally:
            client.close()

    @asynccontextmanager
    async def get_async_redis(self) -> AsyncGenerator[AsyncRedis, None]:
        """Create a context manager for an asynchronous Redis client.

        Provides an async Redis client within a context manager that properly
        handles cleanup. The underlying connection is returned to the pool on close.

        Yields:
            AsyncRedis: An asynchronous Redis client instance.

        Example:
            >>> async with redis_instance.get_async_redis() as client:
            ...     await client.set("key", "value")
            ...     value = await client.get("key")
            ...     print(value)
        """
        client: AsyncRedis = await self.get_async_redis_client()
        try:
            yield client
        finally:
            await client.close()
