import hashlib
import json
import pickle
import threading
import time
from collections import OrderedDict
from datetime import datetime, timedelta
from typing import Any, Callable, Dict, List, Optional, Union


class CacheItem:
    """Represents a single cache item with metadata."""
    
    def __init__(self, key: str, value: Any, ttl: Optional[int] = None):
        self.key = key
        self.value = value
        self.created_at = time.time()
        self.last_accessed = time.time()
        self.access_count = 0
        self.ttl = ttl  # Time to live in seconds
    
    def is_expired(self) -> bool:
        """Check if the cache item has expired."""
        if self.ttl is None:
            return False
        return time.time() - self.created_at > self.ttl
    
    def access(self):
        """Mark the item as accessed."""
        self.last_accessed = time.time()
        self.access_count += 1
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert cache item to dictionary for serialization."""
        return {
            'key': self.key,
            'value': self.value,
            'created_at': self.created_at,
            'last_accessed': self.last_accessed,
            'access_count': self.access_count,
            'ttl': self.ttl
        }


class Cache:
    """
    A comprehensive in-memory caching system with multiple eviction policies,
    TTL support, statistics, and persistence capabilities.
    """
    
    def __init__(self, 
                 max_size: int = 1000,
                 eviction_policy: str = 'lru',
                 default_ttl: Optional[int] = None,
                 enable_stats: bool = True,
                 enable_persistence: bool = False,
                 persistence_file: str = 'cache_data.pkl'):
        """
        Initialize the cache.
        
        Args:
            max_size: Maximum number of items in cache
            eviction_policy: 'lru', 'lfu', 'fifo', or 'random'
            default_ttl: Default time to live in seconds
            enable_stats: Enable cache statistics
            enable_persistence: Enable cache persistence
            persistence_file: File path for persistence
        """
        self.max_size = max_size
        self.eviction_policy = eviction_policy
        self.default_ttl = default_ttl
        self.enable_stats = enable_stats
        self.enable_persistence = enable_persistence
        self.persistence_file = persistence_file
        
        # Core cache storage
        self._cache: Dict[str, CacheItem] = {}
        self._access_order = OrderedDict()  # For LRU/FIFO
        self._access_frequency: Dict[str, int] = {}  # For LFU
        
        # Threading support
        self._lock = threading.RLock()
        
        # Statistics
        self._stats = {
            'hits': 0,
            'misses': 0,
            'sets': 0,
            'deletes': 0,
            'evictions': 0,
            'expirations': 0
        }
        
        # Background cleanup thread
        self._cleanup_thread = None
        self._stop_cleanup = False
        self._start_cleanup_thread()
        
        # Load from persistence if enabled
        if self.enable_persistence:
            self._load_from_persistence()
    
    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
        """
        Set a value in the cache.
        
        Args:
            key: Cache key
            value: Value to cache
            ttl: Time to live in seconds (overrides default_ttl)
        
        Returns:
            True if successful, False otherwise
        """
        with self._lock:
            try:
                # Use provided TTL or default
                item_ttl = ttl if ttl is not None else self.default_ttl
                
                # Create cache item
                cache_item = CacheItem(key, value, item_ttl)
                
                # Check if we need to evict items
                if len(self._cache) >= self.max_size:
                    self._evict_item()
                
                # Store the item
                self._cache[key] = cache_item
                self._access_order[key] = time.time()
                self._access_frequency[key] = 0
                
                # Update statistics
                if self.enable_stats:
                    self._stats['sets'] += 1
                
                # Persist if enabled
                if self.enable_persistence:
                    self._save_to_persistence()
                
                return True
                
            except Exception as e:
                print(f"Error setting cache item: {e}")
                return False
    
    def get(self, key: str, default: Any = None) -> Any:
        """
        Get a value from the cache.
        
        Args:
            key: Cache key
            default: Default value if key not found
        
        Returns:
            Cached value or default
        """
        with self._lock:
            try:
                if key in self._cache:
                    item = self._cache[key]
                    
                    # Check if expired
                    if item.is_expired():
                        self._remove_item(key)
                        if self.enable_stats:
                            self._stats['expirations'] += 1
                        return default
                    
                    # Update access information
                    item.access()
                    self._access_order[key] = time.time()
                    self._access_frequency[key] = self._access_frequency.get(key, 0) + 1
                    
                    # Update statistics
                    if self.enable_stats:
                        self._stats['hits'] += 1
                    
                    return item.value
                else:
                    # Update statistics
                    if self.enable_stats:
                        self._stats['misses'] += 1
                    return default
                    
            except Exception as e:
                print(f"Error getting cache item: {e}")
                return default
    def getCache(self, key: str, cls = None):
        if bool(key) is False:
            return None
        value = self.get(key)
        if value is not None and cls is not None:
            value = cls.from_dict(value)
            return value
        return value
    
    def delete(self, key: str) -> bool:
        """
        Delete a key from the cache.
        
        Args:
            key: Cache key to delete
        
        Returns:
            True if deleted, False if not found
        """
        with self._lock:
            try:
                if key in self._cache:
                    self._remove_item(key)
                    
                    # Update statistics
                    if self.enable_stats:
                        self._stats['deletes'] += 1
                    
                    # Persist if enabled
                    if self.enable_persistence:
                        self._save_to_persistence()
                    
                    return True
                return False
                
            except Exception as e:
                print(f"Error deleting cache item: {e}")
                return False
    
    def exists(self, key: str) -> bool:
        """Check if a key exists in the cache."""
        with self._lock:
            if key in self._cache:
                item = self._cache[key]
                if item.is_expired():
                    self._remove_item(key)
                    return False
                return True
            return False
    
    def clear(self) -> bool:
        """Clear all items from the cache."""
        with self._lock:
            try:
                self._cache.clear()
                self._access_order.clear()
                self._access_frequency.clear()
                
                # Update statistics
                if self.enable_stats:
                    self._stats['deletes'] += len(self._cache)
                
                # Persist if enabled
                if self.enable_persistence:
                    self._save_to_persistence()
                
                return True
                
            except Exception as e:
                print(f"Error clearing cache: {e}")
                return False
    
    def keys(self) -> List[str]:
        """Get all valid (non-expired) keys in the cache."""
        with self._lock:
            valid_keys = []
            expired_keys = []
            
            for key, item in self._cache.items():
                if item.is_expired():
                    expired_keys.append(key)
                else:
                    valid_keys.append(key)
            
            # Remove expired keys
            for key in expired_keys:
                self._remove_item(key)
            
            return valid_keys
    
    def size(self) -> int:
        """Get the current number of items in the cache."""
        with self._lock:
            # Clean expired items first
            self._cleanup_expired()
            return len(self._cache)
    
    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        with self._lock:
            stats = self._stats.copy()
            stats['current_size'] = len(self._cache)
            stats['max_size'] = self.max_size
            stats['hit_rate'] = (stats['hits'] / (stats['hits'] + stats['misses'])) if (stats['hits'] + stats['misses']) > 0 else 0
            return stats
    
    def reset_stats(self):
        """Reset cache statistics."""
        with self._lock:
            self._stats = {
                'hits': 0,
                'misses': 0,
                'sets': 0,
                'deletes': 0,
                'evictions': 0,
                'expirations': 0
            }
    
    def set_ttl(self, key: str, ttl: int) -> bool:
        """
        Set TTL for an existing key.
        
        Args:
            key: Cache key
            ttl: New TTL in seconds
        
        Returns:
            True if successful, False if key not found
        """
        with self._lock:
            if key in self._cache:
                self._cache[key].ttl = ttl
                self._cache[key].created_at = time.time()
                return True
            return False
    
    def get_ttl(self, key: str) -> Optional[int]:
        """Get remaining TTL for a key in seconds."""
        with self._lock:
            if key in self._cache:
                item = self._cache[key]
                if item.ttl is None:
                    return None
                remaining = item.ttl - (time.time() - item.created_at)
                return max(0, int(remaining))
            return None
    
    def touch(self, key: str) -> bool:
        """Update the last access time for a key."""
        with self._lock:
            if key in self._cache:
                item = self._cache[key]
                if not item.is_expired():
                    item.access()
                    self._access_order[key] = time.time()
                    return True
            return False
    
    def increment(self, key: str, delta: int = 1, default: int = 0) -> Optional[int]:
        """
        Increment a numeric value in the cache.
        
        Args:
            key: Cache key
            delta: Amount to increment by
            default: Default value if key doesn't exist
        
        Returns:
            New value or None if not numeric
        """
        with self._lock:
            try:
                current = self.get(key, default)
                if isinstance(current, (int, float)):
                    new_value = current + delta
                    self.set(key, new_value)
                    return new_value
                return None
            except Exception:
                return None
    
    def decrement(self, key: str, delta: int = 1, default: int = 0) -> Optional[int]:
        """Decrement a numeric value in the cache."""
        return self.increment(key, -delta, default)
    
    def get_many(self, keys: List[str]) -> Dict[str, Any]:
        """Get multiple values from the cache."""
        result = {}
        for key in keys:
            value = self.get(key)
            if value is not None:
                result[key] = value
        return result
    
    def set_many(self, data: Dict[str, Any], ttl: Optional[int] = None) -> bool:
        """Set multiple values in the cache."""
        try:
            for key, value in data.items():
                self.set(key, value, ttl)
            return True
        except Exception:
            return False
    
    def delete_many(self, keys: List[str]) -> int:
        """Delete multiple keys from the cache."""
        deleted_count = 0
        for key in keys:
            if self.delete(key):
                deleted_count += 1
        return deleted_count
    
    def _evict_item(self):
        """Evict an item based on the eviction policy."""
        if not self._cache:
            return
        
        key_to_evict = None
        
        if self.eviction_policy == 'lru':
            # Least Recently Used
            key_to_evict = next(iter(self._access_order))
        elif self.eviction_policy == 'lfu':
            # Least Frequently Used
            key_to_evict = min(self._access_frequency.items(), key=lambda x: x[1])[0]
        elif self.eviction_policy == 'fifo':
            # First In First Out
            key_to_evict = next(iter(self._access_order))
        elif self.eviction_policy == 'random':
            # Random selection
            import random
            key_to_evict = random.choice(list(self._cache.keys()))
        
        if key_to_evict:
            self._remove_item(key_to_evict)
            if self.enable_stats:
                self._stats['evictions'] += 1
    
    def _remove_item(self, key: str):
        """Remove an item from the cache."""
        if key in self._cache:
            del self._cache[key]
        if key in self._access_order:
            del self._access_order[key]
        if key in self._access_frequency:
            del self._access_frequency[key]
    
    def _cleanup_expired(self):
        """Remove all expired items from the cache."""
        expired_keys = []
        for key, item in self._cache.items():
            if item.is_expired():
                expired_keys.append(key)
        
        for key in expired_keys:
            self._remove_item(key)
            if self.enable_stats:
                self._stats['expirations'] += 1
    
    def _start_cleanup_thread(self):
        """Start background cleanup thread."""
        def cleanup_worker():
            while not self._stop_cleanup:
                try:
                    with self._lock:
                        self._cleanup_expired()
                    time.sleep(60)  # Clean up every minute
                except Exception:
                    pass
        
        self._cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
        self._cleanup_thread.start()
    
    def _save_to_persistence(self):
        """Save cache data to persistence file."""
        if not self.enable_persistence:
            return
        
        try:
            # Convert cache items to serializable format
            serializable_cache = {}
            for key, item in self._cache.items():
                if not item.is_expired():
                    serializable_cache[key] = item.to_dict()
            
            data = {
                'cache': serializable_cache,
                'access_order': dict(self._access_order),
                'access_frequency': self._access_frequency,
                'stats': self._stats
            }
            
            with open(self.persistence_file, 'wb') as f:
                pickle.dump(data, f)
                
        except Exception as e:
            print(f"Error saving cache to persistence: {e}")
    
    def _load_from_persistence(self):
        """Load cache data from persistence file."""
        if not self.enable_persistence:
            return
        
        try:
            with open(self.persistence_file, 'rb') as f:
                data = pickle.load(f)
            
            # Restore cache items
            for key, item_data in data.get('cache', {}).items():
                item = CacheItem(
                    key=item_data['key'],
                    value=item_data['value'],
                    ttl=item_data['ttl']
                )
                item.created_at = item_data['created_at']
                item.last_accessed = item_data['last_accessed']
                item.access_count = item_data['access_count']
                
                self._cache[key] = item
            
            # Restore other data
            self._access_order = OrderedDict(data.get('access_order', {}))
            self._access_frequency = data.get('access_frequency', {})
            self._stats = data.get('stats', self._stats)
            
        except FileNotFoundError:
            # No persistence file exists yet
            pass
        except Exception as e:
            print(f"Error loading cache from persistence: {e}")
    
    def __enter__(self):
        """Context manager entry."""
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Context manager exit."""
        self.close()
    
    def close(self):
        """Clean up resources."""
        self._stop_cleanup = True
        if self._cleanup_thread and self._cleanup_thread.is_alive():
            self._cleanup_thread.join(timeout=1)
        
        if self.enable_persistence:
            self._save_to_persistence()
    
    def __len__(self) -> int:
        """Return the number of items in the cache."""
        return self.size()
    
    def __contains__(self, key: str) -> bool:
        """Check if a key exists in the cache."""
        return self.exists(key)
    
    def __getitem__(self, key: str) -> Any:
        """Get item using bracket notation."""
        value = self.get(key)
        if value is None:
            raise KeyError(key)
        return value
    
    def __setitem__(self, key: str, value: Any):
        """Set item using bracket notation."""
        self.set(key, value)
    
    def __delitem__(self, key: str):
        """Delete item using bracket notation."""
        if not self.delete(key):
            raise KeyError(key)


# Convenience functions for common use cases
def create_cache(max_size: int = 1000, 
                eviction_policy: str = 'lru',
                default_ttl: Optional[int] = None) -> Cache:
    """Create a new cache instance with common settings."""
    return Cache(max_size=max_size, 
                eviction_policy=eviction_policy,
                default_ttl=default_ttl)


def create_persistent_cache(persistence_file: str = 'cache_data.pkl',
                          max_size: int = 1000,
                          eviction_policy: str = 'lru',
                          default_ttl: Optional[int] = None) -> Cache:
    """Create a new persistent cache instance."""
    return Cache(max_size=max_size,
                eviction_policy=eviction_policy,
                default_ttl=default_ttl,
                enable_persistence=True,
                persistence_file=persistence_file)