"""Redis caching layer implementation for mockupAWS.

Provides multi-level caching strategy:
- L1: DB query results (scenario list, metrics) - TTL: 5 minutes
- L2: Report generation (PDF cache) - TTL: 1 hour
- L3: AWS pricing data - TTL: 24 hours
"""

import json
import hashlib
import pickle
from typing import Any, Callable, Optional, Union
from functools import wraps
from datetime import timedelta
import asyncio

import redis.asyncio as redis
from redis.asyncio.connection import ConnectionPool

from src.core.config import settings


class CacheManager:
    """Redis cache manager with connection pooling."""

    _instance: Optional["CacheManager"] = None
    _pool: Optional[ConnectionPool] = None
    _redis: Optional[redis.Redis] = None

    # Cache TTL configurations (in seconds)
    TTL_L1_QUERIES = 300  # 5 minutes
    TTL_L2_REPORTS = 3600  # 1 hour
    TTL_L3_PRICING = 86400  # 24 hours
    TTL_SESSION = 1800  # 30 minutes

    # Cache key prefixes
    PREFIX_L1 = "l1:query"
    PREFIX_L2 = "l2:report"
    PREFIX_L3 = "l3:pricing"
    PREFIX_SESSION = "session"
    PREFIX_LOCK = "lock"
    PREFIX_WARM = "warm"

    def __new__(cls) -> "CacheManager":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    async def initialize(self) -> None:
        """Initialize Redis connection pool."""
        if self._pool is None:
            redis_url = getattr(settings, "redis_url", "redis://localhost:6379/0")
            self._pool = ConnectionPool.from_url(
                redis_url,
                max_connections=50,
                socket_connect_timeout=5,
                socket_timeout=5,
                health_check_interval=30,
            )
            self._redis = redis.Redis(connection_pool=self._pool)

    async def close(self) -> None:
        """Close Redis connection pool."""
        if self._pool:
            await self._pool.disconnect()
            self._pool = None
            self._redis = None

    @property
    def redis(self) -> redis.Redis:
        """Get Redis client."""
        if self._redis is None:
            raise RuntimeError("CacheManager not initialized. Call initialize() first.")
        return self._redis

    def _generate_key(self, prefix: str, *args, **kwargs) -> str:
        """Generate a cache key from arguments."""
        key_data = json.dumps(
            {"args": args, "kwargs": kwargs}, sort_keys=True, default=str
        )
        hash_suffix = hashlib.sha256(key_data.encode()).hexdigest()[:16]
        return f"{prefix}:{hash_suffix}"

    async def get(self, key: str) -> Optional[Any]:
        """Get value from cache."""
        try:
            data = await self.redis.get(key)
            if data:
                return pickle.loads(data)
            return None
        except Exception:
            return None

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

        Args:
            key: Cache key
            value: Value to cache
            ttl: Time to live in seconds
            nx: Only set if key does not exist
        """
        try:
            data = pickle.dumps(value)
            if nx:
                result = await self.redis.setnx(key, data)
                if result and ttl:
                    await self.redis.expire(key, ttl)
                return bool(result)
            else:
                await self.redis.setex(key, ttl or self.TTL_L1_QUERIES, data)
                return True
        except Exception:
            return False

    async def delete(self, key: str) -> bool:
        """Delete value from cache."""
        try:
            result = await self.redis.delete(key)
            return result > 0
        except Exception:
            return False

    async def delete_pattern(self, pattern: str) -> int:
        """Delete all keys matching pattern."""
        try:
            keys = []
            async for key in self.redis.scan_iter(match=pattern):
                keys.append(key)
            if keys:
                return await self.redis.delete(*keys)
            return 0
        except Exception:
            return 0

    async def exists(self, key: str) -> bool:
        """Check if key exists in cache."""
        try:
            return await self.redis.exists(key) > 0
        except Exception:
            return False

    async def ttl(self, key: str) -> int:
        """Get remaining TTL for key."""
        try:
            return await self.redis.ttl(key)
        except Exception:
            return -2

    async def increment(self, key: str, amount: int = 1) -> int:
        """Increment a counter."""
        try:
            return await self.redis.incrby(key, amount)
        except Exception:
            return 0

    async def expire(self, key: str, seconds: int) -> bool:
        """Set expiration on key."""
        try:
            return await self.redis.expire(key, seconds)
        except Exception:
            return False

    # Level-specific cache methods

    async def get_l1(self, func_name: str, *args, **kwargs) -> Optional[Any]:
        """Get from L1 cache (DB queries)."""
        key = self._generate_key(f"{self.PREFIX_L1}:{func_name}", *args, **kwargs)
        return await self.get(key)

    async def set_l1(self, func_name: str, value: Any, *args, **kwargs) -> bool:
        """Set in L1 cache (DB queries)."""
        key = self._generate_key(f"{self.PREFIX_L1}:{func_name}", *args, **kwargs)
        return await self.set(key, value, ttl=self.TTL_L1_QUERIES)

    async def invalidate_l1(self, func_name: str) -> int:
        """Invalidate L1 cache for a function."""
        pattern = f"{self.PREFIX_L1}:{func_name}:*"
        return await self.delete_pattern(pattern)

    async def get_l2(self, report_id: str) -> Optional[Any]:
        """Get from L2 cache (reports)."""
        key = f"{self.PREFIX_L2}:{report_id}"
        return await self.get(key)

    async def set_l2(self, report_id: str, value: Any) -> bool:
        """Set in L2 cache (reports)."""
        key = f"{self.PREFIX_L2}:{report_id}"
        return await self.set(key, value, ttl=self.TTL_L2_REPORTS)

    async def get_l3(self, pricing_key: str) -> Optional[Any]:
        """Get from L3 cache (AWS pricing)."""
        key = f"{self.PREFIX_L3}:{pricing_key}"
        return await self.get(key)

    async def set_l3(self, pricing_key: str, value: Any) -> bool:
        """Set in L3 cache (AWS pricing)."""
        key = f"{self.PREFIX_L3}:{pricing_key}"
        return await self.set(key, value, ttl=self.TTL_L3_PRICING)

    # Cache warming

    async def warm_cache(
        self, func: Callable, *args, ttl: Optional[int] = None, **kwargs
    ) -> Any:
        """Warm cache by pre-computing and storing value."""
        key = self._generate_key(f"{self.PREFIX_WARM}:{func.__name__}", *args, **kwargs)

        # Try to get lock
        lock_key = f"{self.PREFIX_LOCK}:{key}"
        lock_acquired = await self.redis.setnx(lock_key, "1")

        if not lock_acquired:
            # Another process is warming this cache
            await asyncio.sleep(0.1)
            return await self.get(key)

        try:
            # Set lock expiration
            await self.redis.expire(lock_key, 60)

            # Compute and store value
            if asyncio.iscoroutinefunction(func):
                value = await func(*args, **kwargs)
            else:
                value = func(*args, **kwargs)

            await self.set(key, value, ttl=ttl or self.TTL_L1_QUERIES)
            return value
        finally:
            await self.redis.delete(lock_key)

    # Statistics

    async def get_stats(self) -> dict:
        """Get cache statistics."""
        try:
            info = await self.redis.info()
            return {
                "used_memory_human": info.get("used_memory_human", "N/A"),
                "connected_clients": info.get("connected_clients", 0),
                "total_commands_processed": info.get("total_commands_processed", 0),
                "keyspace_hits": info.get("keyspace_hits", 0),
                "keyspace_misses": info.get("keyspace_misses", 0),
                "hit_rate": (
                    info.get("keyspace_hits", 0)
                    / (info.get("keyspace_hits", 0) + info.get("keyspace_misses", 1))
                    * 100
                ),
            }
        except Exception as e:
            return {"error": str(e)}


# Global cache manager instance
cache_manager = CacheManager()


def cached(
    ttl: Optional[int] = None,
    key_prefix: Optional[str] = None,
    invalidate_on: Optional[list[str]] = None,
):
    """Decorator for caching function results.

    Args:
        ttl: Time to live in seconds
        key_prefix: Custom key prefix
        invalidate_on: List of events that invalidate this cache
    """

    def decorator(func: Callable) -> Callable:
        prefix = key_prefix or func.__name__

        @wraps(func)
        async def async_wrapper(*args, **kwargs):
            # Skip cache if disabled
            if getattr(settings, "cache_disabled", False):
                return await func(*args, **kwargs)

            # Generate cache key
            cache_key = cache_manager._generate_key(prefix, *args[1:], **kwargs)

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

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

            # Store in cache
            await cache_manager.set(cache_key, result, ttl=ttl)

            return result

        @wraps(func)
        def sync_wrapper(*args, **kwargs):
            # For sync functions, run in async context
            if getattr(settings, "cache_disabled", False):
                return func(*args, **kwargs)

            cache_key = cache_manager._generate_key(prefix, *args[1:], **kwargs)

            # Try to get from cache (run async operation)
            try:
                loop = asyncio.get_event_loop()
                cached_value = loop.run_until_complete(cache_manager.get(cache_key))
                if cached_value is not None:
                    return cached_value
            except RuntimeError:
                pass

            result = func(*args, **kwargs)

            try:
                loop = asyncio.get_event_loop()
                loop.run_until_complete(cache_manager.set(cache_key, result, ttl=ttl))
            except RuntimeError:
                pass

            return result

        if asyncio.iscoroutinefunction(func):
            wrapper = async_wrapper
        else:
            wrapper = sync_wrapper

        # Attach cache invalidation method
        wrapper.cache_invalidate = lambda: asyncio.create_task(
            cache_manager.delete_pattern(f"{prefix}:*")
        )

        return wrapper

    return decorator


def cache_invalidate(pattern: str):
    """Invalidate cache keys matching pattern."""

    async def _invalidate():
        return await cache_manager.delete_pattern(pattern)

    try:
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(_invalidate())
    except RuntimeError:
        return asyncio.create_task(_invalidate())


# Convenience functions for common operations


async def get_cache_stats() -> dict:
    """Get cache statistics."""
    return await cache_manager.get_stats()


async def clear_cache() -> bool:
    """Clear all cache."""
    try:
        await cache_manager.redis.flushdb()
        return True
    except Exception:
        return False
