"""
Redis Service Wrapper

This module provides a wrapper around the redis-py client for handling caching,
job queue data, and temporary storage. It provides connection pooling, automatic
reconnection, and a clean interface for common Redis operations.

Usage:
    from app.services.redis_service import RedisService

    # Initialize service
    redis = RedisService()

    # Basic operations
    redis.set("key", "value", ttl=3600)  # Set with 1 hour TTL
    value = redis.get("key")
    redis.delete("key")

    # Health check
    if redis.health_check():
        print("Redis is healthy")
"""

import os
import json
from typing import Optional, Any, Union

import redis
from redis.exceptions import RedisError, ConnectionError as RedisConnectionError

from app.utils.logging import get_logger


# Initialize logger
logger = get_logger(__file__)


class RedisServiceError(Exception):
    """Base exception for Redis service errors."""
    pass


class RedisConnectionFailed(RedisServiceError):
    """Raised when Redis connection cannot be established."""
    pass


class RedisService:
    """
    Service class for interacting with Redis.

    This class provides:
    - Connection pooling for efficient connection management
    - Basic operations: get, set, delete, exists
    - TTL support for caching
    - Health check for monitoring
    - Automatic JSON serialization for complex objects

    Attributes:
        pool: Redis connection pool
        client: Redis client instance
    """

    def __init__(self, redis_url: Optional[str] = None):
        """
        Initialize the Redis service.

        Reads configuration from environment variables if not provided:
        - REDIS_URL: Full Redis URL (e.g., redis://localhost:6379/0)

        Args:
            redis_url: Optional Redis URL to override environment variable

        Raises:
            RedisConnectionFailed: If connection to Redis fails
        """
        self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')

        if not self.redis_url:
            logger.error("Missing Redis URL configuration")
            raise ValueError("Redis URL not configured. Set REDIS_URL environment variable.")

        try:
            # Create connection pool for efficient connection management
            # Connection pooling allows multiple operations to share connections
            # and automatically manages connection lifecycle
            self.pool = redis.ConnectionPool.from_url(
                self.redis_url,
                max_connections=10,
                decode_responses=True,  # Return strings instead of bytes
                socket_connect_timeout=5,  # Connection timeout in seconds
                socket_timeout=5,  # Operation timeout in seconds
                retry_on_timeout=True,  # Retry on timeout
            )

            # Create client using the connection pool
            self.client = redis.Redis(connection_pool=self.pool)

            # Test connection
            self.client.ping()

            logger.info("Redis service initialized", redis_url=self._sanitize_url(self.redis_url))

        except RedisConnectionError as e:
            logger.error("Failed to connect to Redis", redis_url=self._sanitize_url(self.redis_url), error=str(e))
            raise RedisConnectionFailed(f"Could not connect to Redis at {self._sanitize_url(self.redis_url)}: {e}")
        except RedisError as e:
            logger.error("Redis initialization error", error=str(e))
            raise RedisServiceError(f"Redis initialization failed: {e}")

    def get(self, key: str) -> Optional[str]:
        """
        Get a value from Redis by key.

        Args:
            key: The key to retrieve

        Returns:
            The value as string if found, None if key doesn't exist

        Raises:
            RedisServiceError: If the operation fails
        """
        try:
            value = self.client.get(key)

            if value is not None:
                logger.debug("Redis GET", key=key, found=True)
            else:
                logger.debug("Redis GET", key=key, found=False)

            return value

        except RedisError as e:
            logger.error("Redis GET failed", key=key, error=str(e))
            raise RedisServiceError(f"Failed to get key '{key}': {e}")

    def get_json(self, key: str) -> Optional[Any]:
        """
        Get a value from Redis and deserialize it from JSON.

        Args:
            key: The key to retrieve

        Returns:
            The deserialized value if found, None if key doesn't exist

        Raises:
            RedisServiceError: If the operation fails or JSON is invalid
        """
        value = self.get(key)

        if value is None:
            return None

        try:
            return json.loads(value)
        except json.JSONDecodeError as e:
            logger.error("Failed to decode JSON from Redis", key=key, error=str(e))
            raise RedisServiceError(f"Failed to decode JSON for key '{key}': {e}")

    def set(
        self,
        key: str,
        value: str,
        ttl: Optional[int] = None,
        nx: bool = False,
        xx: bool = False
    ) -> bool:
        """
        Set a value in Redis.

        Args:
            key: The key to set
            value: The value to store (must be string)
            ttl: Time to live in seconds (None for no expiration)
            nx: Only set if key does not exist (for locking)
            xx: Only set if key already exists

        Returns:
            True if the key was set, False if not set (due to nx/xx conditions)

        Raises:
            RedisServiceError: If the operation fails
        """
        try:
            result = self.client.set(
                key,
                value,
                ex=ttl,  # Expiration in seconds
                nx=nx,   # Only set if not exists
                xx=xx    # Only set if exists
            )

            # set() returns True if set, None if not set due to nx/xx
            success = result is True or result == 1

            logger.debug("Redis SET", key=key, ttl=ttl, nx=nx, xx=xx, success=success)

            return success

        except RedisError as e:
            logger.error("Redis SET failed", key=key, error=str(e))
            raise RedisServiceError(f"Failed to set key '{key}': {e}")

    def set_json(
        self,
        key: str,
        value: Any,
        ttl: Optional[int] = None,
        nx: bool = False,
        xx: bool = False
    ) -> bool:
        """
        Serialize a value to JSON and store it in Redis.

        Args:
            key: The key to set
            value: The value to serialize and store (must be JSON-serializable)
            ttl: Time to live in seconds (None for no expiration)
            nx: Only set if key does not exist
            xx: Only set if key already exists

        Returns:
            True if the key was set, False if not set (due to nx/xx conditions)

        Raises:
            RedisServiceError: If the operation fails or value is not JSON-serializable
        """
        try:
            json_value = json.dumps(value)
        except (TypeError, ValueError) as e:
            logger.error("Failed to serialize value to JSON", key=key, error=str(e))
            raise RedisServiceError(f"Failed to serialize value for key '{key}': {e}")

        return self.set(key, json_value, ttl=ttl, nx=nx, xx=xx)

    def delete(self, *keys: str) -> int:
        """
        Delete one or more keys from Redis.

        Args:
            *keys: One or more keys to delete

        Returns:
            The number of keys that were deleted

        Raises:
            RedisServiceError: If the operation fails
        """
        if not keys:
            return 0

        try:
            deleted_count = self.client.delete(*keys)

            logger.debug("Redis DELETE", keys=keys, deleted_count=deleted_count)

            return deleted_count

        except RedisError as e:
            logger.error("Redis DELETE failed", keys=keys, error=str(e))
            raise RedisServiceError(f"Failed to delete keys {keys}: {e}")

    def exists(self, *keys: str) -> int:
        """
        Check if one or more keys exist in Redis.

        Args:
            *keys: One or more keys to check

        Returns:
            The number of keys that exist

        Raises:
            RedisServiceError: If the operation fails
        """
        if not keys:
            return 0

        try:
            exists_count = self.client.exists(*keys)

            logger.debug("Redis EXISTS", keys=keys, exists_count=exists_count)

            return exists_count

        except RedisError as e:
            logger.error("Redis EXISTS failed", keys=keys, error=str(e))
            raise RedisServiceError(f"Failed to check existence of keys {keys}: {e}")

    def expire(self, key: str, ttl: int) -> bool:
        """
        Set a TTL (time to live) on an existing key.

        Args:
            key: The key to set expiration on
            ttl: Time to live in seconds

        Returns:
            True if the timeout was set, False if key doesn't exist

        Raises:
            RedisServiceError: If the operation fails
        """
        try:
            result = self.client.expire(key, ttl)

            logger.debug("Redis EXPIRE", key=key, ttl=ttl, success=result)

            return result

        except RedisError as e:
            logger.error("Redis EXPIRE failed", key=key, ttl=ttl, error=str(e))
            raise RedisServiceError(f"Failed to set expiration for key '{key}': {e}")

    def ttl(self, key: str) -> int:
        """
        Get the remaining TTL (time to live) for a key.

        Args:
            key: The key to check

        Returns:
            TTL in seconds, -1 if key exists but has no expiry, -2 if key doesn't exist

        Raises:
            RedisServiceError: If the operation fails
        """
        try:
            remaining = self.client.ttl(key)

            logger.debug("Redis TTL", key=key, remaining=remaining)

            return remaining

        except RedisError as e:
            logger.error("Redis TTL failed", key=key, error=str(e))
            raise RedisServiceError(f"Failed to get TTL for key '{key}': {e}")

    def incr(self, key: str, amount: int = 1) -> int:
        """
        Increment a key's value by the given amount.

        If the key doesn't exist, it will be created with the increment value.

        Args:
            key: The key to increment
            amount: Amount to increment by (default 1)

        Returns:
            The new value after incrementing

        Raises:
            RedisServiceError: If the operation fails or value is not an integer
        """
        try:
            new_value = self.client.incrby(key, amount)

            logger.debug("Redis INCR", key=key, amount=amount, new_value=new_value)

            return new_value

        except RedisError as e:
            logger.error("Redis INCR failed", key=key, amount=amount, error=str(e))
            raise RedisServiceError(f"Failed to increment key '{key}': {e}")

    def decr(self, key: str, amount: int = 1) -> int:
        """
        Decrement a key's value by the given amount.

        If the key doesn't exist, it will be created with the negative increment value.

        Args:
            key: The key to decrement
            amount: Amount to decrement by (default 1)

        Returns:
            The new value after decrementing

        Raises:
            RedisServiceError: If the operation fails or value is not an integer
        """
        try:
            new_value = self.client.decrby(key, amount)

            logger.debug("Redis DECR", key=key, amount=amount, new_value=new_value)

            return new_value

        except RedisError as e:
            logger.error("Redis DECR failed", key=key, amount=amount, error=str(e))
            raise RedisServiceError(f"Failed to decrement key '{key}': {e}")

    def health_check(self) -> bool:
        """
        Check if Redis connection is healthy.

        This performs a PING command to verify the connection is working.

        Returns:
            True if Redis is healthy and responding, False otherwise
        """
        try:
            response = self.client.ping()

            if response:
                logger.debug("Redis health check passed")
                return True
            else:
                logger.warning("Redis health check failed - unexpected response", response=response)
                return False

        except RedisError as e:
            logger.error("Redis health check failed", error=str(e))
            return False

    def info(self) -> dict:
        """
        Get Redis server information.

        Returns:
            Dictionary containing server info (version, memory, clients, etc.)

        Raises:
            RedisServiceError: If the operation fails
        """
        try:
            info = self.client.info()

            logger.debug("Redis INFO retrieved", redis_version=info.get('redis_version'))

            return info

        except RedisError as e:
            logger.error("Redis INFO failed", error=str(e))
            raise RedisServiceError(f"Failed to get Redis info: {e}")

    def flush_db(self) -> bool:
        """
        Delete all keys in the current database.

        WARNING: This is a destructive operation. Use with caution.

        Returns:
            True if successful

        Raises:
            RedisServiceError: If the operation fails
        """
        try:
            self.client.flushdb()

            logger.warning("Redis database flushed")

            return True

        except RedisError as e:
            logger.error("Redis FLUSHDB failed", error=str(e))
            raise RedisServiceError(f"Failed to flush database: {e}")

    def close(self) -> None:
        """
        Close all connections in the pool.

        Call this when shutting down the application to cleanly release connections.
        """
        try:
            self.pool.disconnect()
            logger.info("Redis connection pool closed")
        except Exception as e:
            logger.error("Error closing Redis connection pool", error=str(e))

    def _sanitize_url(self, url: str) -> str:
        """
        Remove password from Redis URL for safe logging.

        Args:
            url: Redis URL that may contain password

        Returns:
            URL with password masked
        """
        # Simple sanitization - mask password if present
        # Format: redis://user:password@host:port/db
        if '@' in url:
            # Split on @ and mask everything before it except the protocol
            parts = url.split('@')
            protocol_and_creds = parts[0]
            host_and_rest = parts[1]

            if '://' in protocol_and_creds:
                protocol = protocol_and_creds.split('://')[0]
                return f"{protocol}://***@{host_and_rest}"

        return url

    def __enter__(self):
        """Context manager entry."""
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit - close connections."""
        self.close()
        return False
