"""
Enhanced Caching System for Connectors
Multi-level caching with TTL, LRU, and persistence capabilities
"""

import json
import time
import hashlib
import pickle
from typing import Any, Dict, Optional, Union, Callable, TypeVar, Generic
from pathlib import Path
from threading import RLock
from collections import OrderedDict
from dataclasses import dataclass
from .exceptions import CacheError
from .logging_config import get_logger, log_performance

logger = get_logger(__name__)

T = TypeVar('T')


@dataclass
class CacheEntry:
    """Cache entry with metadata"""
    value: Any
    created_at: float
    accessed_at: float
    access_count: int
    ttl: Optional[float]
    
    @property
    def is_expired(self) -> bool:
        """Check if the cache entry is expired"""
        if self.ttl is None:
            return False
        return time.time() > (self.created_at + self.ttl)
    
    def touch(self) -> None:
        """Update access timestamp and count"""
        self.accessed_at = time.time()
        self.access_count += 1


class MemoryCache(Generic[T]):
    """In-memory LRU cache with TTL support"""
    
    def __init__(
        self, 
        max_size: int = 1000, 
        default_ttl: Optional[float] = None,
        cleanup_interval: float = 60.0
    ):
        self.max_size = max_size
        self.default_ttl = default_ttl
        self.cleanup_interval = cleanup_interval
        self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
        self._lock = RLock()
        self._last_cleanup = time.time()
    
    @log_performance
    def get(self, key: str) -> Optional[T]:
        """Get value from cache"""
        with self._lock:
            self._cleanup_expired()
            
            if key not in self._cache:
                return None
            
            entry = self._cache[key]
            
            if entry.is_expired:
                del self._cache[key]
                logger.debug(f"Cache entry expired: {key}")
                return None
            
            # Move to end (LRU)
            self._cache.move_to_end(key)
            entry.touch()
            
            logger.debug(f"Cache hit: {key}")
            return entry.value
    
    @log_performance
    def set(
        self, 
        key: str, 
        value: T, 
        ttl: Optional[float] = None
    ) -> None:
        """Set value in cache"""
        with self._lock:
            self._cleanup_expired()
            
            # Calculate TTL
            if ttl is None:
                ttl = self.default_ttl
            
            # Create entry
            entry = CacheEntry(
                value=value,
                created_at=time.time(),
                accessed_at=time.time(),
                access_count=1,
                ttl=ttl
            )
            
            # Check size limit
            if len(self._cache) >= self.max_size and key not in self._cache:
                # Remove oldest entry (LRU)
                oldest_key = next(iter(self._cache))
                del self._cache[oldest_key]
                logger.debug(f"Evicted cache entry: {oldest_key}")
            
            self._cache[key] = entry
            self._cache.move_to_end(key)
            logger.debug(f"Cache set: {key}")
    
    def delete(self, key: str) -> bool:
        """Delete key from cache"""
        with self._lock:
            if key in self._cache:
                del self._cache[key]
                logger.debug(f"Cache delete: {key}")
                return True
            return False
    
    def clear(self) -> None:
        """Clear all cache entries"""
        with self._lock:
            count = len(self._cache)
            self._cache.clear()
            logger.info(f"Cache cleared: {count} entries removed")
    
    def size(self) -> int:
        """Get current cache size"""
        with self._lock:
            return len(self._cache)
    
    def keys(self) -> list[str]:
        """Get all cache keys"""
        with self._lock:
            return list(self._cache.keys())
    
    def stats(self) -> Dict[str, Any]:
        """Get cache statistics"""
        with self._lock:
            total_entries = len(self._cache)
            expired_entries = sum(1 for entry in self._cache.values() if entry.is_expired)
            
            return {
                'size': total_entries,
                'max_size': self.max_size,
                'expired_count': expired_entries,
                'hit_rate': getattr(self, '_hit_count', 0) / max(getattr(self, '_total_requests', 1), 1),
                'keys': list(self._cache.keys())
            }
    
    def _cleanup_expired(self) -> None:
        """Remove expired entries"""
        current_time = time.time()
        
        # Only cleanup periodically
        if current_time - self._last_cleanup < self.cleanup_interval:
            return
        
        expired_keys = [
            key for key, entry in self._cache.items() 
            if entry.is_expired
        ]
        
        for key in expired_keys:
            del self._cache[key]
        
        if expired_keys:
            logger.debug(f"Cleaned up {len(expired_keys)} expired cache entries")
        
        self._last_cleanup = current_time


class FileCache(Generic[T]):
    """File-based cache with persistence"""
    
    def __init__(
        self, 
        cache_dir: str, 
        max_size: int = 10000,
        default_ttl: Optional[float] = None
    ):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)
        self.max_size = max_size
        self.default_ttl = default_ttl
        self._index_file = self.cache_dir / "cache_index.json"
        self._index: Dict[str, Dict[str, Any]] = {}
        self._load_index()
    
    def _load_index(self) -> None:
        """Load cache index from file"""
        try:
            if self._index_file.exists():
                with open(self._index_file, 'r') as f:
                    self._index = json.load(f)
        except Exception as e:
            logger.warning(f"Failed to load cache index: {e}")
            self._index = {}
    
    def _save_index(self) -> None:
        """Save cache index to file"""
        try:
            with open(self._index_file, 'w') as f:
                json.dump(self._index, f)
        except Exception as e:
            logger.warning(f"Failed to save cache index: {e}")
    
    def _get_file_path(self, key: str) -> Path:
        """Get file path for cache key"""
        # Use hash of key for filename
        key_hash = hashlib.md5(key.encode()).hexdigest()
        return self.cache_dir / f"{key_hash}.cache"
    
    @log_performance
    def get(self, key: str) -> Optional[T]:
        """Get value from file cache"""
        if key not in self._index:
            return None
        
        entry_info = self._index[key]
        
        # Check TTL
        if entry_info.get('ttl') and time.time() > (entry_info['created_at'] + entry_info['ttl']):
            self.delete(key)
            return None
        
        try:
            file_path = self._get_file_path(key)
            if file_path.exists():
                with open(file_path, 'rb') as f:
                    value = pickle.load(f)
                
                # Update access info
                self._index[key]['accessed_at'] = time.time()
                self._index[key]['access_count'] += 1
                self._save_index()
                
                logger.debug(f"File cache hit: {key}")
                return value
            else:
                # File doesn't exist, remove from index
                del self._index[key]
                self._save_index()
                return None
                
        except Exception as e:
            logger.error(f"Failed to read cache file for key {key}: {e}")
            self.delete(key)
            return None
    
    @log_performance
    def set(self, key: str, value: T, ttl: Optional[float] = None) -> None:
        """Set value in file cache"""
        try:
            # Check size limit
            if len(self._index) >= self.max_size and key not in self._index:
                # Remove oldest entry
                oldest_key = min(
                    self._index.keys(),
                    key=lambda k: self._index[k].get('accessed_at', 0)
                )
                self.delete(oldest_key)
            
            # Calculate TTL
            if ttl is None:
                ttl = self.default_ttl
            
            # Save value to file
            file_path = self._get_file_path(key)
            with open(file_path, 'wb') as f:
                pickle.dump(value, f)
            
            # Update index
            current_time = time.time()
            self._index[key] = {
                'created_at': current_time,
                'accessed_at': current_time,
                'access_count': 1,
                'ttl': ttl
            }
            
            self._save_index()
            logger.debug(f"File cache set: {key}")
            
        except Exception as e:
            logger.error(f"Failed to write cache file for key {key}: {e}")
            raise CacheError(f"Failed to set cache value: {e}")
    
    def delete(self, key: str) -> bool:
        """Delete key from file cache"""
        if key not in self._index:
            return False
        
        try:
            # Remove file
            file_path = self._get_file_path(key)
            if file_path.exists():
                file_path.unlink()
            
            # Remove from index
            del self._index[key]
            self._save_index()
            
            logger.debug(f"File cache delete: {key}")
            return True
            
        except Exception as e:
            logger.error(f"Failed to delete cache file for key {key}: {e}")
            return False
    
    def clear(self) -> None:
        """Clear all cache entries"""
        try:
            # Remove all cache files
            for cache_file in self.cache_dir.glob("*.cache"):
                cache_file.unlink()
            
            # Clear index
            self._index.clear()
            self._save_index()
            
            logger.info("File cache cleared")
            
        except Exception as e:
            logger.error(f"Failed to clear file cache: {e}")
            raise CacheError(f"Failed to clear cache: {e}")
    
    def size(self) -> int:
        """Get current cache size"""
        return len(self._index)
    
    def stats(self) -> Dict[str, Any]:
        """Get cache statistics"""
        return {
            'size': len(self._index),
            'max_size': self.max_size,
            'cache_dir': str(self.cache_dir),
            'keys': list(self._index.keys())
        }


class MultiLevelCache(Generic[T]):
    """Multi-level cache with memory and file storage"""
    
    def __init__(
        self,
        memory_size: int = 1000,
        file_cache_dir: Optional[str] = None,
        file_cache_size: int = 10000,
        default_ttl: Optional[float] = None
    ):
        self.memory_cache = MemoryCache[T](memory_size, default_ttl)
        
        if file_cache_dir:
            self.file_cache = FileCache[T](file_cache_dir, file_cache_size, default_ttl)
        else:
            self.file_cache = None
        
        self.default_ttl = default_ttl
        logger.info(f"Multi-level cache initialized: memory={memory_size}, file={file_cache_size}")
    
    def get(self, key: str) -> Optional[T]:
        """Get value from cache (memory first, then file)"""
        # Try memory cache first
        value = self.memory_cache.get(key)
        if value is not None:
            return value
        
        # Try file cache
        if self.file_cache:
            value = self.file_cache.get(key)
            if value is not None:
                # Promote to memory cache
                self.memory_cache.set(key, value, self.default_ttl)
                return value
        
        return None
    
    def set(self, key: str, value: T, ttl: Optional[float] = None) -> None:
        """Set value in both cache levels"""
        self.memory_cache.set(key, value, ttl)
        
        if self.file_cache:
            self.file_cache.set(key, value, ttl)
    
    def delete(self, key: str) -> bool:
        """Delete key from both cache levels"""
        memory_deleted = self.memory_cache.delete(key)
        file_deleted = self.file_cache.delete(key) if self.file_cache else False
        
        return memory_deleted or file_deleted
    
    def clear(self) -> None:
        """Clear both cache levels"""
        self.memory_cache.clear()
        if self.file_cache:
            self.file_cache.clear()
    
    def stats(self) -> Dict[str, Any]:
        """Get comprehensive cache statistics"""
        stats = {
            'memory_cache': self.memory_cache.stats(),
        }
        
        if self.file_cache:
            stats['file_cache'] = self.file_cache.stats()
        
        return stats


def cache_key(*args, **kwargs) -> str:
    """Generate a cache key from function arguments"""
    key_parts = []
    
    # Add positional arguments
    for arg in args:
        if isinstance(arg, (str, int, float, bool)):
            key_parts.append(str(arg))
        else:
            # For complex objects, use repr
            key_parts.append(repr(arg))
    
    # Add keyword arguments (sorted for consistency)
    for k, v in sorted(kwargs.items()):
        key_parts.append(f"{k}={v}")
    
    return hashlib.md5("|".join(key_parts).encode()).hexdigest()


def cached(
    cache: MultiLevelCache,
    ttl: Optional[float] = None,
    key_func: Optional[Callable] = None
):
    """Decorator for caching function results"""
    def decorator(func: Callable) -> Callable:
        def wrapper(*args, **kwargs):
            # Generate cache key
            if key_func:
                key = key_func(*args, **kwargs)
            else:
                key = f"{func.__name__}:{cache_key(*args, **kwargs)}"
            
            # Try to get from cache
            result = cache.get(key)
            if result is not None:
                logger.debug(f"Cache hit for function {func.__name__}")
                return result
            
            # Execute function
            result = func(*args, **kwargs)
            
            # Cache result
            cache.set(key, result, ttl)
            logger.debug(f"Cached result for function {func.__name__}")
            
            return result
        
        return wrapper
    return decorator


# Global cache instance
_global_cache: Optional[MultiLevelCache] = None


def get_global_cache() -> MultiLevelCache:
    """Get the global cache instance"""
    global _global_cache
    if _global_cache is None:
        _global_cache = MultiLevelCache()
    return _global_cache
