"""
Enterprise Redis Caching Layer for Toxo.

This module implements production-grade caching with:
- Multiple cache strategies (LRU, LFU, TTL-based)
- Distributed caching with Redis
- Cache warming and preloading
- Cache invalidation patterns
- Performance monitoring and metrics
- Circuit breaker for cache failures
"""

import asyncio
import json
import time
import hashlib
import pickle
from abc import ABC, abstractmethod
from dataclasses import dataclass, field, asdict
from typing import Dict, List, Optional, Any, Union, Callable, TypeVar, Generic, Tuple
from enum import Enum
from datetime import datetime, timedelta
import threading
from contextlib import asynccontextmanager
import logging

try:
    import redis.asyncio as aioredis
    import redis
    REDIS_AVAILABLE = True
except ImportError:
    redis = None
    aioredis = None
    REDIS_AVAILABLE = False

import numpy as np
from pydantic import BaseModel, Field

from ..utils.logger import get_logger
from ..utils.exceptions import ToxoError
from ..utils.performance import PerformanceTracker

T = TypeVar('T')


class CacheStrategy(Enum):
    """Cache eviction strategies."""
    LRU = "lru"  # Least Recently Used
    LFU = "lfu"  # Least Frequently Used
    TTL = "ttl"  # Time To Live
    FIFO = "fifo"  # First In First Out
    RANDOM = "random"  # Random eviction


class CacheLevel(Enum):
    """Cache levels for hierarchical caching."""
    L1_MEMORY = "l1_memory"  # In-memory cache
    L2_REDIS = "l2_redis"    # Redis cache
    L3_DISK = "l3_disk"      # Disk-based cache


class CachePattern(Enum):
    """Cache access patterns."""
    CACHE_ASIDE = "cache_aside"
    WRITE_THROUGH = "write_through"
    WRITE_BEHIND = "write_behind"
    REFRESH_AHEAD = "refresh_ahead"


@dataclass
class CacheEntry:
    """Represents a cache entry with metadata."""
    key: str
    value: Any
    created_at: datetime
    last_accessed: Optional[datetime] = None
    access_count: int = 0
    ttl: Optional[int] = None  # seconds
    tags: List[str] = field(default_factory=list)
    size_bytes: int = 0
    
    @property
    def is_expired(self) -> bool:
        """Check if cache entry is expired."""
        if not self.ttl:
            return False
        return (datetime.now() - self.created_at).total_seconds() > self.ttl
    
    @property
    def age_seconds(self) -> float:
        """Get age of cache entry in seconds."""
        return (datetime.now() - self.created_at).total_seconds()
    
    def touch(self):
        """Update access metadata."""
        self.last_accessed = datetime.now()
        self.access_count += 1


@dataclass
class CacheMetrics:
    """Cache performance metrics."""
    hits: int = 0
    misses: int = 0
    evictions: int = 0
    size_bytes: int = 0
    entry_count: int = 0
    average_access_time: float = 0.0
    hit_rate: float = 0.0
    
    def calculate_hit_rate(self) -> float:
        """Calculate cache hit rate."""
        total = self.hits + self.misses
        return self.hits / total if total > 0 else 0.0
    
    def update_hit_rate(self):
        """Update hit rate calculation."""
        self.hit_rate = self.calculate_hit_rate()


class CacheBackend(ABC):
    """Abstract base class for cache backends."""
    
    @abstractmethod
    async def get(self, key: str) -> Optional[Any]:
        """Get value from cache."""
        pass
    
    @abstractmethod
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in cache."""
        pass
    
    @abstractmethod
    async def delete(self, key: str) -> bool:
        """Delete key from cache."""
        pass
    
    @abstractmethod
    async def exists(self, key: str) -> bool:
        """Check if key exists in cache."""
        pass
    
    @abstractmethod
    async def clear(self) -> bool:
        """Clear all cache entries."""
        pass
    
    @abstractmethod
    async def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        pass


class MemoryCache(CacheBackend):
    """In-memory cache backend with configurable eviction strategies."""
    
    def __init__(
        self,
        max_size: int = 1000,
        max_memory_mb: int = 100,
        strategy: CacheStrategy = CacheStrategy.LRU,
        default_ttl: Optional[int] = None
    ):
        self.max_size = max_size
        self.max_memory_bytes = max_memory_mb * 1024 * 1024
        self.strategy = strategy
        self.default_ttl = default_ttl
        
        self._cache: Dict[str, CacheEntry] = {}
        self._lock = asyncio.Lock()
        self.metrics = CacheMetrics()
        self.logger = get_logger(f"{__name__}.memory_cache")
    
    async def get(self, key: str) -> Optional[Any]:
        """Get value from memory cache."""
        async with self._lock:
            entry = self._cache.get(key)
            if not entry:
                self.metrics.misses += 1
                self.metrics.update_hit_rate()
                return None
            
            if entry.is_expired:
                await self._remove_entry(key)
                self.metrics.misses += 1
                self.metrics.update_hit_rate()
                return None
            
            entry.touch()
            self.metrics.hits += 1
            self.metrics.update_hit_rate()
            return entry.value
    
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in memory cache."""
        async with self._lock:
            # Calculate entry size
            size_bytes = len(pickle.dumps(value))
            
            # Check memory limits
            if size_bytes > self.max_memory_bytes:
                self.logger.warning(f"Value too large for cache: {size_bytes} bytes")
                return False
            
            # Evict if necessary
            await self._ensure_capacity(size_bytes)
            
            # Create cache entry
            entry = CacheEntry(
                key=key,
                value=value,
                created_at=datetime.now(),
                ttl=ttl or self.default_ttl,
                size_bytes=size_bytes
            )
            
            # Remove old entry if exists
            if key in self._cache:
                await self._remove_entry(key)
            
            # Add new entry
            self._cache[key] = entry
            self.metrics.entry_count += 1
            self.metrics.size_bytes += size_bytes
            
            return True
    
    async def delete(self, key: str) -> bool:
        """Delete key from memory cache."""
        async with self._lock:
            if key in self._cache:
                await self._remove_entry(key)
                return True
            return False
    
    async def exists(self, key: str) -> bool:
        """Check if key exists in memory cache."""
        async with self._lock:
            entry = self._cache.get(key)
            if not entry:
                return False
            
            if entry.is_expired:
                await self._remove_entry(key)
                return False
            
            return True
    
    async def clear(self) -> bool:
        """Clear all cache entries."""
        async with self._lock:
            self._cache.clear()
            self.metrics = CacheMetrics()
            return True
    
    async def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        async with self._lock:
            return {
                'backend': 'memory',
                'strategy': self.strategy.value,
                'entry_count': self.metrics.entry_count,
                'size_bytes': self.metrics.size_bytes,
                'size_mb': self.metrics.size_bytes / (1024 * 1024),
                'hits': self.metrics.hits,
                'misses': self.metrics.misses,
                'hit_rate': self.metrics.hit_rate,
                'evictions': self.metrics.evictions,
                'max_size': self.max_size,
                'max_memory_mb': self.max_memory_bytes / (1024 * 1024)
            }
    
    async def _ensure_capacity(self, new_entry_size: int):
        """Ensure cache has capacity for new entry."""
        # Check entry count limit
        while len(self._cache) >= self.max_size:
            await self._evict_entry()
        
        # Check memory limit
        while (self.metrics.size_bytes + new_entry_size) > self.max_memory_bytes:
            await self._evict_entry()
    
    async def _evict_entry(self):
        """Evict an entry based on the configured strategy."""
        if not self._cache:
            return
        
        if self.strategy == CacheStrategy.LRU:
            # Evict least recently used
            key = min(
                self._cache.keys(),
                key=lambda k: self._cache[k].last_accessed or self._cache[k].created_at
            )
        elif self.strategy == CacheStrategy.LFU:
            # Evict least frequently used
            key = min(self._cache.keys(), key=lambda k: self._cache[k].access_count)
        elif self.strategy == CacheStrategy.FIFO:
            # Evict oldest entry
            key = min(self._cache.keys(), key=lambda k: self._cache[k].created_at)
        else:
            # Random eviction
            import random
            key = random.choice(list(self._cache.keys()))
        
        await self._remove_entry(key)
        self.metrics.evictions += 1
    
    async def _remove_entry(self, key: str):
        """Remove an entry from cache."""
        if key in self._cache:
            entry = self._cache.pop(key)
            self.metrics.entry_count -= 1
            self.metrics.size_bytes -= entry.size_bytes


class RedisCache(CacheBackend):
    """Redis cache backend with advanced features."""
    
    def __init__(
        self,
        redis_url: str = "redis://localhost:6379",
        key_prefix: str = "toxo:",
        default_ttl: Optional[int] = None,
        serializer: str = "json"  # json, pickle, msgpack
    ):
        self.redis_url = redis_url
        self.key_prefix = key_prefix
        self.default_ttl = default_ttl
        self.serializer = serializer
        
        self.redis_client: Optional[aioredis.Redis] = None
        self.metrics = CacheMetrics()
        self.logger = get_logger(f"{__name__}.redis_cache")
        
        if not REDIS_AVAILABLE:
            raise ToxoError("Redis is not available. Install with: pip install redis")
    
    async def connect(self):
        """Connect to Redis."""
        try:
            self.redis_client = aioredis.from_url(
                self.redis_url,
                decode_responses=False,  # We handle encoding/decoding
                retry_on_timeout=True,
                socket_connect_timeout=5,
                socket_timeout=5
            )
            
            # Test connection
            await self.redis_client.ping()
            self.logger.info(f"Connected to Redis: {self.redis_url}")
            
        except Exception as e:
            self.logger.error(f"Failed to connect to Redis: {e}")
            raise ToxoError(f"Redis connection failed: {e}")
    
    async def disconnect(self):
        """Disconnect from Redis."""
        if self.redis_client:
            await self.redis_client.close()
            self.logger.info("Disconnected from Redis")
    
    def _make_key(self, key: str) -> str:
        """Create Redis key with prefix."""
        return f"{self.key_prefix}{key}"
    
    def _serialize(self, value: Any) -> bytes:
        """Serialize value for Redis storage."""
        if self.serializer == "json":
            return json.dumps(value, default=str).encode('utf-8')
        elif self.serializer == "pickle":
            return pickle.dumps(value)
        else:
            raise ToxoError(f"Unsupported serializer: {self.serializer}")
    
    def _deserialize(self, data: bytes) -> Any:
        """Deserialize value from Redis storage."""
        if self.serializer == "json":
            return json.loads(data.decode('utf-8'))
        elif self.serializer == "pickle":
            return pickle.loads(data)
        else:
            raise ToxoError(f"Unsupported serializer: {self.serializer}")
    
    async def get(self, key: str) -> Optional[Any]:
        """Get value from Redis cache."""
        if not self.redis_client:
            await self.connect()
        
        try:
            redis_key = self._make_key(key)
            data = await self.redis_client.get(redis_key)
            
            if data is None:
                self.metrics.misses += 1
                self.metrics.update_hit_rate()
                return None
            
            value = self._deserialize(data)
            self.metrics.hits += 1
            self.metrics.update_hit_rate()
            return value
            
        except Exception as e:
            self.logger.error(f"Redis GET error: {e}")
            self.metrics.misses += 1
            self.metrics.update_hit_rate()
            return None
    
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in Redis cache."""
        if not self.redis_client:
            await self.connect()
        
        try:
            redis_key = self._make_key(key)
            data = self._serialize(value)
            
            if ttl or self.default_ttl:
                await self.redis_client.setex(
                    redis_key, 
                    ttl or self.default_ttl, 
                    data
                )
            else:
                await self.redis_client.set(redis_key, data)
            
            return True
            
        except Exception as e:
            self.logger.error(f"Redis SET error: {e}")
            return False
    
    async def delete(self, key: str) -> bool:
        """Delete key from Redis cache."""
        if not self.redis_client:
            await self.connect()
        
        try:
            redis_key = self._make_key(key)
            result = await self.redis_client.delete(redis_key)
            return result > 0
            
        except Exception as e:
            self.logger.error(f"Redis DELETE error: {e}")
            return False
    
    async def exists(self, key: str) -> bool:
        """Check if key exists in Redis cache."""
        if not self.redis_client:
            await self.connect()
        
        try:
            redis_key = self._make_key(key)
            result = await self.redis_client.exists(redis_key)
            return result > 0
            
        except Exception as e:
            self.logger.error(f"Redis EXISTS error: {e}")
            return False
    
    async def clear(self) -> bool:
        """Clear all cache entries with prefix."""
        if not self.redis_client:
            await self.connect()
        
        try:
            pattern = f"{self.key_prefix}*"
            async for key in self.redis_client.scan_iter(match=pattern):
                await self.redis_client.delete(key)
            return True
            
        except Exception as e:
            self.logger.error(f"Redis CLEAR error: {e}")
            return False
    
    async def get_stats(self) -> Dict[str, Any]:
        """Get Redis cache statistics."""
        if not self.redis_client:
            return {'backend': 'redis', 'status': 'disconnected'}
        
        try:
            info = await self.redis_client.info()
            keyspace = await self.redis_client.info('keyspace')
            
            return {
                'backend': 'redis',
                'status': 'connected',
                'redis_version': info.get('redis_version'),
                'used_memory': info.get('used_memory'),
                'used_memory_human': info.get('used_memory_human'),
                'hits': self.metrics.hits,
                'misses': self.metrics.misses,
                'hit_rate': self.metrics.hit_rate,
                'keyspace': keyspace
            }
            
        except Exception as e:
            self.logger.error(f"Redis STATS error: {e}")
            return {'backend': 'redis', 'status': 'error', 'error': str(e)}


class HierarchicalCache:
    """
    Multi-level hierarchical cache with L1 (memory) and L2 (Redis) layers.
    """
    
    def __init__(
        self,
        l1_config: Optional[Dict[str, Any]] = None,
        l2_config: Optional[Dict[str, Any]] = None,
        promotion_threshold: int = 2,  # Promote to L1 after N accesses
        coherence_strategy: str = "write_through"
    ):
        self.promotion_threshold = promotion_threshold
        self.coherence_strategy = coherence_strategy
        
        # Initialize cache layers
        l1_config = l1_config or {}
        self.l1_cache = MemoryCache(**l1_config)
        
        if l2_config and REDIS_AVAILABLE:
            self.l2_cache = RedisCache(**l2_config)
        else:
            self.l2_cache = None
        
        self.access_counts: Dict[str, int] = {}
        self.metrics = CacheMetrics()
        self.logger = get_logger(f"{__name__}.hierarchical_cache")
    
    async def get(self, key: str) -> Optional[Any]:
        """Get value from hierarchical cache."""
        # Try L1 first
        value = await self.l1_cache.get(key)
        if value is not None:
            self.metrics.hits += 1
            self.metrics.update_hit_rate()
            return value
        
        # Try L2 if available
        if self.l2_cache:
            value = await self.l2_cache.get(key)
            if value is not None:
                # Track access for promotion
                self.access_counts[key] = self.access_counts.get(key, 0) + 1
                
                # Promote to L1 if accessed frequently
                if self.access_counts[key] >= self.promotion_threshold:
                    await self.l1_cache.set(key, value)
                    self.logger.debug(f"Promoted key {key} to L1 cache")
                
                self.metrics.hits += 1
                self.metrics.update_hit_rate()
                return value
        
        self.metrics.misses += 1
        self.metrics.update_hit_rate()
        return None
    
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in hierarchical cache."""
        success = True
        
        if self.coherence_strategy == "write_through":
            # Write to both layers
            l1_success = await self.l1_cache.set(key, value, ttl)
            if self.l2_cache:
                l2_success = await self.l2_cache.set(key, value, ttl)
                success = l1_success and l2_success
            else:
                success = l1_success
        
        elif self.coherence_strategy == "write_behind":
            # Write to L1 immediately, L2 asynchronously
            success = await self.l1_cache.set(key, value, ttl)
            if self.l2_cache:
                asyncio.create_task(self.l2_cache.set(key, value, ttl))
        
        return success
    
    async def delete(self, key: str) -> bool:
        """Delete key from hierarchical cache."""
        success = True
        
        # Delete from both layers
        l1_success = await self.l1_cache.delete(key)
        if self.l2_cache:
            l2_success = await self.l2_cache.delete(key)
            success = l1_success or l2_success
        else:
            success = l1_success
        
        # Clean up access tracking
        self.access_counts.pop(key, None)
        
        return success
    
    async def exists(self, key: str) -> bool:
        """Check if key exists in any cache layer."""
        if await self.l1_cache.exists(key):
            return True
        
        if self.l2_cache and await self.l2_cache.exists(key):
            return True
        
        return False
    
    async def clear(self) -> bool:
        """Clear all cache layers."""
        success = True
        
        l1_success = await self.l1_cache.clear()
        if self.l2_cache:
            l2_success = await self.l2_cache.clear()
            success = l1_success and l2_success
        else:
            success = l1_success
        
        self.access_counts.clear()
        self.metrics = CacheMetrics()
        
        return success
    
    async def get_stats(self) -> Dict[str, Any]:
        """Get hierarchical cache statistics."""
        l1_stats = await self.l1_cache.get_stats()
        l2_stats = await self.l2_cache.get_stats() if self.l2_cache else {}
        
        return {
            'cache_type': 'hierarchical',
            'coherence_strategy': self.coherence_strategy,
            'promotion_threshold': self.promotion_threshold,
            'total_hits': self.metrics.hits,
            'total_misses': self.metrics.misses,
            'total_hit_rate': self.metrics.hit_rate,
            'l1_cache': l1_stats,
            'l2_cache': l2_stats,
            'pending_promotions': len([
                k for k, count in self.access_counts.items() 
                if count >= self.promotion_threshold
            ])
        }


class CacheManager:
    """
    Enterprise cache manager with advanced features.
    """
    
    def __init__(
        self,
        default_cache: Optional[CacheBackend] = None,
        cache_pattern: CachePattern = CachePattern.CACHE_ASIDE,
        enable_metrics: bool = True
    ):
        self.caches: Dict[str, CacheBackend] = {}
        self.default_cache = default_cache or MemoryCache()
        self.cache_pattern = cache_pattern
        self.enable_metrics = enable_metrics
        
        self.metrics = CacheMetrics()
        self.performance_tracker = PerformanceTracker() if enable_metrics else None
        self.logger = get_logger(f"{__name__}.cache_manager")
        
        # Cache warming and preloading
        self.warm_up_tasks: List[Callable] = []
        self.invalidation_patterns: Dict[str, List[str]] = {}
    
    def register_cache(self, name: str, cache: CacheBackend):
        """Register a named cache."""
        self.caches[name] = cache
        self.logger.info(f"Registered cache: {name}")
    
    def get_cache(self, name: Optional[str] = None) -> CacheBackend:
        """Get cache by name or default."""
        if name and name in self.caches:
            return self.caches[name]
        return self.default_cache
    
    async def get(
        self, 
        key: str, 
        cache_name: Optional[str] = None,
        loader: Optional[Callable] = None,
        ttl: Optional[int] = None
    ) -> Optional[Any]:
        """
        Get value from cache with optional loader function.
        
        Args:
            key: Cache key
            cache_name: Specific cache to use
            loader: Function to load value if not in cache
            ttl: TTL for loaded value
        
        Returns:
            Cached or loaded value
        """
        cache = self.get_cache(cache_name)
        
        # Try to get from cache
        with self._track_performance("cache_get"):
            value = await cache.get(key)
        
        if value is not None:
            self.metrics.hits += 1
            self.metrics.update_hit_rate()
            return value
        
        # Cache miss - try loader if provided
        if loader:
            try:
                with self._track_performance("cache_load"):
                    value = await self._call_loader(loader, key)
                
                if value is not None:
                    # Store in cache
                    await cache.set(key, value, ttl)
                    self.logger.debug(f"Loaded and cached key: {key}")
            
            except Exception as e:
                self.logger.error(f"Loader failed for key {key}: {e}")
        
        self.metrics.misses += 1
        self.metrics.update_hit_rate()
        return value
    
    async def set(
        self, 
        key: str, 
        value: Any, 
        ttl: Optional[int] = None,
        cache_name: Optional[str] = None,
        tags: Optional[List[str]] = None
    ) -> bool:
        """Set value in cache with optional tags for invalidation."""
        cache = self.get_cache(cache_name)
        
        with self._track_performance("cache_set"):
            success = await cache.set(key, value, ttl)
        
        # Store tags for invalidation
        if tags and success:
            for tag in tags:
                if tag not in self.invalidation_patterns:
                    self.invalidation_patterns[tag] = []
                self.invalidation_patterns[tag].append(key)
        
        return success
    
    async def delete(
        self, 
        key: str, 
        cache_name: Optional[str] = None
    ) -> bool:
        """Delete key from cache."""
        cache = self.get_cache(cache_name)
        
        with self._track_performance("cache_delete"):
            return await cache.delete(key)
    
    async def invalidate_by_tag(self, tag: str) -> int:
        """Invalidate all keys associated with a tag."""
        if tag not in self.invalidation_patterns:
            return 0
        
        keys = self.invalidation_patterns[tag]
        deleted_count = 0
        
        for key in keys:
            for cache in [self.default_cache] + list(self.caches.values()):
                if await cache.delete(key):
                    deleted_count += 1
        
        # Clean up tag mapping
        del self.invalidation_patterns[tag]
        
        self.logger.info(f"Invalidated {deleted_count} keys for tag: {tag}")
        return deleted_count
    
    async def warm_up(self, cache_name: Optional[str] = None):
        """Warm up cache with predefined data."""
        if not self.warm_up_tasks:
            return
        
        cache = self.get_cache(cache_name)
        
        for task in self.warm_up_tasks:
            try:
                await task(cache)
                self.logger.debug(f"Executed warm-up task: {task.__name__}")
            except Exception as e:
                self.logger.error(f"Warm-up task failed: {e}")
    
    def register_warm_up_task(self, task: Callable):
        """Register a cache warm-up task."""
        self.warm_up_tasks.append(task)
        self.logger.info(f"Registered warm-up task: {task.__name__}")
    
    async def get_stats(self) -> Dict[str, Any]:
        """Get comprehensive cache statistics."""
        stats = {
            'manager_metrics': {
                'hits': self.metrics.hits,
                'misses': self.metrics.misses,
                'hit_rate': self.metrics.hit_rate
            },
            'caches': {}
        }
        
        # Default cache stats
        stats['caches']['default'] = await self.default_cache.get_stats()
        
        # Named cache stats
        for name, cache in self.caches.items():
            stats['caches'][name] = await cache.get_stats()
        
        # Performance stats
        if self.performance_tracker:
            stats['performance'] = self.performance_tracker.get_summary()
        
        return stats
    
    async def _call_loader(self, loader: Callable, key: str) -> Any:
        """Call loader function (sync or async)."""
        if asyncio.iscoroutinefunction(loader):
            return await loader(key)
        else:
            return loader(key)
    
    def _track_performance(self, operation: str):
        """Context manager for tracking performance."""
        if self.performance_tracker:
            return self.performance_tracker.track(operation)
        else:
            return self._null_context()
    
    @asynccontextmanager
    async def _null_context(self):
        """Null context manager when performance tracking is disabled."""
        yield
    
    def get_state(self) -> Dict[str, Any]:
        """Get the current state for serialization."""
        return {
            "default_cache_available": getattr(self, 'default_cache', None) is not None,
            "caches_count": len(getattr(self, 'caches', {})),
            "performance_tracker_available": getattr(self, 'performance_tracker', None) is not None,
            "invalidation_patterns_count": len(getattr(self, 'invalidation_patterns', {})),
            "system_type": "CacheManager"
        }


# Decorators for caching
def cache(
    key_func: Optional[Callable] = None,
    ttl: Optional[int] = None,
    cache_name: Optional[str] = None,
    tags: Optional[List[str]] = None
):
    """
    Decorator for caching function results.
    
    Args:
        key_func: Function to generate cache key from arguments
        ttl: Time to live in seconds
        cache_name: Specific cache to use
        tags: Tags for invalidation
    """
    def decorator(func):
        async def async_wrapper(*args, **kwargs):
            # Generate cache key
            if key_func:
                cache_key = key_func(*args, **kwargs)
            else:
                cache_key = f"{func.__name__}:{hash((args, tuple(kwargs.items())))}"
            
            # Try to get from cache
            cached_value = await cache_manager.get(cache_key, cache_name)
            if cached_value is not None:
                return cached_value
            
            # Execute function
            result = await func(*args, **kwargs)
            
            # Store in cache
            await cache_manager.set(cache_key, result, ttl, cache_name, tags)
            
            return result
        
        def sync_wrapper(*args, **kwargs):
            # For sync functions, use asyncio to run cache operations
            loop = asyncio.get_event_loop()
            return loop.run_until_complete(async_wrapper(*args, **kwargs))
        
        if asyncio.iscoroutinefunction(func):
            return async_wrapper
        else:
            return sync_wrapper
    
    return decorator


# Global cache manager instance
cache_manager = CacheManager()


# Factory functions
def create_memory_cache(**kwargs) -> MemoryCache:
    """Create memory cache with configuration."""
    return MemoryCache(**kwargs)


def create_redis_cache(**kwargs) -> RedisCache:
    """Create Redis cache with configuration."""
    return RedisCache(**kwargs)


def create_hierarchical_cache(**kwargs) -> HierarchicalCache:
    """Create hierarchical cache with configuration."""
    return HierarchicalCache(**kwargs)


async def setup_caching(config: Dict[str, Any]) -> CacheManager:
    """
    Setup caching system with configuration.
    
    Args:
        config: Cache configuration
    
    Returns:
        Configured CacheManager
    """
    manager = CacheManager()
    
    # Setup default cache
    if config.get('default_cache'):
        cache_config = config['default_cache']
        cache_type = cache_config.get('type', 'memory')
        
        if cache_type == 'memory':
            default_cache = create_memory_cache(**cache_config.get('config', {}))
        elif cache_type == 'redis':
            default_cache = create_redis_cache(**cache_config.get('config', {}))
        elif cache_type == 'hierarchical':
            default_cache = create_hierarchical_cache(**cache_config.get('config', {}))
        else:
            raise ToxoError(f"Unknown cache type: {cache_type}")
        
        manager.default_cache = default_cache
    
    # Setup named caches
    if config.get('named_caches'):
        for name, cache_config in config['named_caches'].items():
            cache_type = cache_config.get('type', 'memory')
            
            if cache_type == 'memory':
                cache = create_memory_cache(**cache_config.get('config', {}))
            elif cache_type == 'redis':
                cache = create_redis_cache(**cache_config.get('config', {}))
            elif cache_type == 'hierarchical':
                cache = create_hierarchical_cache(**cache_config.get('config', {}))
            else:
                raise ToxoError(f"Unknown cache type: {cache_type}")
            
            manager.register_cache(name, cache)
    
    return manager 