from __future__ import annotations

import asyncio
import json
import time
from typing import Any, Dict, Optional, Union
from dataclasses import dataclass
from enum import Enum

import redis.asyncio as redis
from .logging import get_logger

logger = get_logger(__name__)


class CacheBackend(str, Enum):
    REDIS = "redis"
    MEMORY = "memory"


@dataclass
class CacheConfig:
    """Configuration for caching system."""
    backend: CacheBackend = CacheBackend.MEMORY
    redis_url: str = "redis://localhost:6379"
    default_ttl: int = 300  # 5 minutes
    max_connections: int = 20
    connection_timeout: float = 5.0
    key_prefix: str = "meta_ads_mcp:"
    enable_compression: bool = True
    compression_threshold: int = 1024  # bytes


@dataclass
class CacheStats:
    """Cache performance statistics."""
    hits: int = 0
    misses: int = 0
    sets: int = 0
    deletes: int = 0
    errors: int = 0
    total_requests: int = 0
    
    @property
    def hit_rate(self) -> float:
        if self.total_requests == 0:
            return 0.0
        return self.hits / self.total_requests


class MemoryCache:
    """In-memory cache implementation with TTL support."""
    
    def __init__(self, default_ttl: int = 300):
        self.default_ttl = default_ttl
        self._cache: Dict[str, Dict[str, Any]] = {}
        self._lock = asyncio.Lock()
    
    async def get(self, key: str) -> Optional[Any]:
        async with self._lock:
            if key not in self._cache:
                return None
            
            entry = self._cache[key]
            if time.time() > entry["expires_at"]:
                del self._cache[key]
                return None
            
            return entry["value"]
    
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        async with self._lock:
            ttl = ttl or self.default_ttl
            self._cache[key] = {
                "value": value,
                "expires_at": time.time() + ttl,
                "created_at": time.time()
            }
            return True
    
    async def delete(self, key: str) -> bool:
        async with self._lock:
            if key in self._cache:
                del self._cache[key]
                return True
            return False
    
    async def clear(self) -> bool:
        async with self._lock:
            self._cache.clear()
            return True
    
    async def cleanup_expired(self) -> int:
        """Remove expired entries and return count of cleaned items."""
        async with self._lock:
            now = time.time()
            expired_keys = [
                key for key, entry in self._cache.items()
                if now > entry["expires_at"]
            ]
            for key in expired_keys:
                del self._cache[key]
            return len(expired_keys)


class RedisCache:
    """Redis-backed cache implementation."""
    
    def __init__(self, config: CacheConfig):
        self.config = config
        self._pool: Optional[redis.ConnectionPool] = None
        self._client: Optional[redis.Redis] = None
    
    async def _ensure_connection(self) -> None:
        """Ensure Redis connection is established."""
        if self._client is None:
            self._pool = redis.ConnectionPool.from_url(
                self.config.redis_url,
                max_connections=self.config.max_connections,
                socket_timeout=self.config.connection_timeout,
                socket_connect_timeout=self.config.connection_timeout,
                retry_on_timeout=True,
                decode_responses=True
            )
            self._client = redis.Redis(connection_pool=self._pool)
    
    def _make_key(self, key: str) -> str:
        """Add prefix to cache key."""
        return f"{self.config.key_prefix}{key}"
    
    async def get(self, key: str) -> Optional[Any]:
        try:
            await self._ensure_connection()
            full_key = self._make_key(key)
            data = await self._client.get(full_key)
            if data is None:
                return None
            
            return json.loads(data)
        except Exception as e:
            logger.error("Redis cache get error", key=key, error=str(e))
            return None
    
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        try:
            await self._ensure_connection()
            full_key = self._make_key(key)
            ttl = ttl or self.config.default_ttl
            data = json.dumps(value)
            await self._client.setex(full_key, ttl, data)
            return True
        except Exception as e:
            logger.error("Redis cache set error", key=key, error=str(e))
            return False
    
    async def delete(self, key: str) -> bool:
        try:
            await self._ensure_connection()
            full_key = self._make_key(key)
            result = await self._client.delete(full_key)
            return result > 0
        except Exception as e:
            logger.error("Redis cache delete error", key=key, error=str(e))
            return False
    
    async def clear(self) -> bool:
        try:
            await self._ensure_connection()
            pattern = f"{self.config.key_prefix}*"
            keys = await self._client.keys(pattern)
            if keys:
                await self._client.delete(*keys)
            return True
        except Exception as e:
            logger.error("Redis cache clear error", error=str(e))
            return False
    
    async def close(self) -> None:
        """Close Redis connections."""
        if self._client:
            await self._client.close()
        if self._pool:
            await self._pool.disconnect()


class CacheManager:
    """Unified cache manager supporting multiple backends."""
    
    def __init__(self, config: CacheConfig):
        self.config = config
        self.stats = CacheStats()
        self._backend: Union[MemoryCache, RedisCache, None] = None
        self._cleanup_task: Optional[asyncio.Task] = None
    
    async def _initialize_backend(self) -> None:
        """Initialize the cache backend."""
        if self._backend is not None:
            return
        
        if self.config.backend == CacheBackend.REDIS:
            self._backend = RedisCache(self.config)
        else:
            self._backend = MemoryCache(self.config.default_ttl)
        
        # Start cleanup task for memory cache
        if isinstance(self._backend, MemoryCache):
            self._cleanup_task = asyncio.create_task(self._cleanup_loop())
    
    async def _cleanup_loop(self) -> None:
        """Periodic cleanup of expired entries in memory cache."""
        while True:
            try:
                await asyncio.sleep(60)  # Cleanup every minute
                if isinstance(self._backend, MemoryCache):
                    cleaned = await self._backend.cleanup_expired()
                    if cleaned > 0:
                        logger.debug("Cleaned up expired cache entries", count=cleaned)
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error("Cache cleanup error", error=str(e))
    
    async def get(self, key: str) -> Optional[Any]:
        """Get value from cache."""
        await self._initialize_backend()
        self.stats.total_requests += 1
        
        try:
            value = await self._backend.get(key)
            if value is not None:
                self.stats.hits += 1
            else:
                self.stats.misses += 1
            return value
        except Exception as e:
            self.stats.errors += 1
            logger.error("Cache get error", key=key, error=str(e))
            return None
    
    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """Set value in cache."""
        await self._initialize_backend()
        
        try:
            success = await self._backend.set(key, value, ttl)
            if success:
                self.stats.sets += 1
            return success
        except Exception as e:
            self.stats.errors += 1
            logger.error("Cache set error", key=key, error=str(e))
            return False
    
    async def delete(self, key: str) -> bool:
        """Delete value from cache."""
        await self._initialize_backend()
        
        try:
            success = await self._backend.delete(key)
            if success:
                self.stats.deletes += 1
            return success
        except Exception as e:
            self.stats.errors += 1
            logger.error("Cache delete error", key=key, error=str(e))
            return False
    
    async def clear(self) -> bool:
        """Clear all cache entries."""
        await self._initialize_backend()
        
        try:
            return await self._backend.clear()
        except Exception as e:
            self.stats.errors += 1
            logger.error("Cache clear error", error=str(e))
            return False
    
    async def close(self) -> None:
        """Close cache connections and cleanup tasks."""
        if self._cleanup_task:
            self._cleanup_task.cancel()
            try:
                await self._cleanup_task
            except asyncio.CancelledError:
                pass
        
        if isinstance(self._backend, RedisCache):
            await self._backend.close()


# Global cache instance
_cache_manager: Optional[CacheManager] = None


def get_cache_manager() -> CacheManager:
    """Get the global cache manager instance."""
    global _cache_manager
    if _cache_manager is None:
        config = CacheConfig()
        _cache_manager = CacheManager(config)
    return _cache_manager


async def close_cache() -> None:
    """Close the global cache manager."""
    global _cache_manager
    if _cache_manager:
        await _cache_manager.close()
        _cache_manager = None


# Cache decorators and utilities
def cache_key(*args, **kwargs) -> str:
    """Generate a cache key from arguments."""
    key_parts = []
    for arg in args:
        if isinstance(arg, (str, int, float, bool)):
            key_parts.append(str(arg))
        else:
            key_parts.append(str(hash(str(arg))))
    
    for k, v in sorted(kwargs.items()):
        key_parts.append(f"{k}:{v}")
    
    return ":".join(key_parts)


def cache_ttl_for_endpoint(endpoint: str) -> int:
    """Get appropriate TTL for different API endpoints."""
    # Campaign data changes less frequently
    if "campaigns" in endpoint:
        return 600  # 10 minutes
    
    # Insights data is more dynamic
    if "insights" in endpoint:
        return 300  # 5 minutes
    
    # Audience data is relatively stable
    if "audience" in endpoint or "customaudience" in endpoint:
        return 900  # 15 minutes
    
    # Default TTL
    return 300  # 5 minutes


async def cached_request(
    cache_manager: CacheManager,
    cache_key: str,
    request_func,
    ttl: Optional[int] = None,
    *args,
    **kwargs
) -> Any:
    """Execute a request with caching."""
    # Try to get from cache first
    cached_result = await cache_manager.get(cache_key)
    if cached_result is not None:
        logger.debug("Cache hit", key=cache_key)
        return cached_result
    
    # Execute request
    logger.debug("Cache miss, executing request", key=cache_key)
    result = await request_func(*args, **kwargs)
    
    # Cache the result
    if result is not None:
        await cache_manager.set(cache_key, result, ttl)
    
    return result
