# services/orchestrator/app/utils/cache.py
"""
Redis caching utilities for dashboard endpoints
"""

import json
import redis.asyncio as redis
from typing import Optional, Any, Callable
from functools import wraps
import structlog
from app.core.config import settings
from pydantic import BaseModel

logger = structlog.get_logger()

# Redis client instance
_redis_client: Optional[redis.Redis] = None


async def get_redis_client() -> redis.Redis:
    """Get or create Redis client"""
    global _redis_client

    if _redis_client is None:
        try:
            # Check if TLS is enabled - convert string to boolean properly
            redis_tls_str = str(getattr(settings, 'REDIS_TLS_ENABLED', 'false')).lower()
            redis_tls_enabled = redis_tls_str in ('true', '1', 'yes', 'on')

            connection_kwargs = {
                'host': str(getattr(settings, 'REDIS_HOST', 'localhost')),
                'port': int(getattr(settings, 'REDIS_PORT', 6379)),
                'db': int(getattr(settings, 'REDIS_DB', 0)),
                'decode_responses': True,
                'socket_connect_timeout': 5,
                'socket_timeout': 5
            }

            # Add password if configured
            redis_password = getattr(settings, 'REDIS_PASSWORD', None)
            if redis_password:
                connection_kwargs['password'] = redis_password

            # Add SSL/TLS support if enabled
            if redis_tls_enabled:
                import ssl
                connection_kwargs['ssl'] = True
                connection_kwargs['ssl_cert_reqs'] = ssl.CERT_NONE
                logger.debug(f"Redis TLS enabled - connecting with SSL to {connection_kwargs['host']}:{connection_kwargs['port']}")

            _redis_client = redis.Redis(**connection_kwargs)

            # Test connection
            await _redis_client.ping()
            logger.info(f"Redis client connected successfully (TLS: {redis_tls_enabled})")
        except Exception as e:
            logger.warning(f"Failed to connect to Redis: {e}. Caching will be disabled.")
            _redis_client = None

    return _redis_client


async def close_redis():
    """Close Redis connection"""
    global _redis_client
    if _redis_client:
        await _redis_client.close()
        _redis_client = None
        logger.info("Redis connection closed")


async def get_cached(key: str) -> Optional[Any]:
    """
    Get cached value by key

    Args:
        key: Cache key

    Returns:
        Cached value (deserialized from JSON) or None if not found or error
    """
    try:
        client = await get_redis_client()
        if not client:
            return None

        cached = await client.get(key)
        if cached:
            logger.debug(f"Cache hit: {key}")
            return json.loads(cached)
        else:
            logger.debug(f"Cache miss: {key}")
            return None
    except Exception as e:
        logger.warning(f"Cache get error for key {key}: {e}")
        return None


def _serialize_value(value: Any) -> Any:
    """
    Recursively serialize values for JSON storage, handling Pydantic models properly.

    Args:
        value: Value to serialize

    Returns:
        JSON-serializable value
    """
    if isinstance(value, BaseModel):
        # Convert Pydantic model to dictionary
        return value.model_dump()
    elif isinstance(value, (list, tuple)):
        # Recursively serialize list/tuple elements
        return [_serialize_value(item) for item in value]
    elif isinstance(value, dict):
        # Recursively serialize dictionary values
        return {key: _serialize_value(val) for key, val in value.items()}
    else:
        # For other types, use default serialization
        return value


async def set_cached(key: str, value: Any, ttl: int = 60) -> bool:
    """
    Set cached value with TTL

    Args:
        key: Cache key
        value: Value to cache (will be JSON serialized)
        ttl: Time to live in seconds

    Returns:
        True if successful, False otherwise
    """
    try:
        client = await get_redis_client()
        if not client:
            return False

        # Serialize value properly before JSON encoding
        serialized_value = _serialize_value(value)
        serialized = json.dumps(serialized_value)
        await client.setex(key, ttl, serialized)
        logger.debug(f"Cache set: {key} (TTL: {ttl}s)")
        return True
    except Exception as e:
        logger.warning(f"Cache set error for key {key}: {e}")
        return False


async def delete_cached(key: str) -> bool:
    """
    Delete cached value

    Args:
        key: Cache key

    Returns:
        True if successful, False otherwise
    """
    try:
        client = await get_redis_client()
        if not client:
            return False

        await client.delete(key)
        logger.debug(f"Cache deleted: {key}")
        return True
    except Exception as e:
        logger.warning(f"Cache delete error for key {key}: {e}")
        return False


async def delete_pattern(pattern: str) -> int:
    """
    Delete all keys matching pattern

    Args:
        pattern: Redis key pattern (e.g., "dashboard:*")

    Returns:
        Number of keys deleted
    """
    try:
        client = await get_redis_client()
        if not client:
            return 0

        keys = []
        async for key in client.scan_iter(match=pattern):
            keys.append(key)

        if keys:
            deleted = await client.delete(*keys)
            logger.info(f"Deleted {deleted} keys matching pattern: {pattern}")
            return deleted
        return 0
    except Exception as e:
        logger.warning(f"Cache delete pattern error for {pattern}: {e}")
        return 0


def cache_response(key_prefix: str, ttl: int = 60):
    """
    Decorator to cache endpoint responses

    Args:
        key_prefix: Prefix for cache key (will be combined with tenant_id)
        ttl: Time to live in seconds

    Usage:
        @cache_response("dashboard:health", ttl=30)
        async def get_health(tenant_id: str):
            ...
    """
    def decorator(func: Callable):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Extract tenant_id from kwargs or args
            tenant_id = kwargs.get('tenant_id')
            if not tenant_id and args:
                # Try to find tenant_id in args (assuming it's the first argument)
                tenant_id = args[0] if len(args) > 0 else None

            if not tenant_id:
                # No tenant_id, skip caching
                return await func(*args, **kwargs)

            # Build cache key
            cache_key = f"{key_prefix}:{tenant_id}"

            # Try to get from cache
            cached_value = await get_cached(cache_key)
            if cached_value is not None:
                return cached_value

            # Execute function
            result = await func(*args, **kwargs)

            # Cache result
            await set_cached(cache_key, result, ttl)

            return result

        return wrapper
    return decorator


def make_cache_key(prefix: str, tenant_id: str, **params) -> str:
    """
    Create a cache key with optional parameters

    Args:
        prefix: Key prefix
        tenant_id: Tenant ID
        **params: Additional parameters to include in key

    Returns:
        Cache key string
    """
    key_parts = [prefix, tenant_id]
    for k, v in sorted(params.items()):
        if v is not None:
            key_parts.append(f"{k}:{v}")
    return ":".join(key_parts)
