# wcs/caching.py
"""
Unified Cache Manager for the Milionar addon.
Handles both metadata (JSON) and binary assets (images) with:
- In-memory caching for speed
- Atomic writes for safety
- Automatic garbage collection
"""

import os
import json
import time
import hashlib
import threading
import xbmc
import xbmcvfs


class CacheManager:
    """
    Centralized cache manager for metadata and generated assets.
    Thread-safe with atomic writes and automatic cleanup.
    """
    
    # Default settings
    DEFAULT_MAX_AGE_DAYS = 7
    DEFAULT_MAX_SIZE_MB = 100
    METADATA_FILENAME = "cache_metadata.json"
    
    def __init__(self, cache_dir=None):
        """
        Initialize the cache manager.
        
        Args:
            cache_dir: Base directory for cache files. Defaults to addon_data folder.
        """
        if cache_dir:
            self._cache_dir = cache_dir
        else:
            self._cache_dir = xbmcvfs.translatePath(
                'special://profile/addon_data/plugin.video.milionar/cache/'
            )
        
        # Ensure cache directory exists
        if not xbmcvfs.exists(self._cache_dir):
            xbmcvfs.mkdirs(self._cache_dir)
        
        # In-memory metadata cache
        self._metadata = {}
        self._metadata_dirty = False
        self._metadata_lock = threading.Lock()
        
        # Load existing metadata from disk
        self._load_metadata()
        
        # Track last cleanup time to avoid running too often
        self._last_cleanup = 0
    
    # =========================================================================
    # METADATA CACHE (JSON data - programs, episodes, etc.)
    # =========================================================================
    
    def get(self, key, default=None):
        """
        Get a value from the metadata cache.
        
        Args:
            key: Cache key string
            default: Default value if key not found
            
        Returns:
            Cached value or default
        """
        with self._metadata_lock:
            entry = self._metadata.get(key)
            if entry is None:
                return default
            
            # Check TTL if present
            if 'expires' in entry and entry['expires'] < time.time():
                # Expired - remove and return default
                del self._metadata[key]
                self._metadata_dirty = True
                return default
            
            return entry.get('data', default)
    
    def set(self, key, data, ttl_seconds=None):
        """
        Set a value in the metadata cache.
        
        Args:
            key: Cache key string
            data: Data to cache (must be JSON serializable)
            ttl_seconds: Optional time-to-live in seconds
        """
        with self._metadata_lock:
            entry = {'data': data, 'timestamp': time.time()}
            if ttl_seconds:
                entry['expires'] = time.time() + ttl_seconds
            self._metadata[key] = entry
            self._metadata_dirty = True
    
    def delete(self, key):
        """Remove a key from metadata cache."""
        with self._metadata_lock:
            if key in self._metadata:
                del self._metadata[key]
                self._metadata_dirty = True
    
    def delete_prefix(self, prefix):
        """Remove all keys starting with a prefix."""
        with self._metadata_lock:
            keys_to_delete = [k for k in self._metadata if k.startswith(prefix)]
            for key in keys_to_delete:
                del self._metadata[key]
            if keys_to_delete:
                self._metadata_dirty = True
    
    def flush(self):
        """Force save metadata to disk (atomic write)."""
        with self._metadata_lock:
            if not self._metadata_dirty:
                return
            self._save_metadata_locked()
    
    def _load_metadata(self):
        """Load metadata from disk into memory."""
        metadata_path = os.path.join(self._cache_dir, self.METADATA_FILENAME)
        try:
            if xbmcvfs.exists(metadata_path):
                f = xbmcvfs.File(metadata_path, 'r')
                content = f.read()
                f.close()
                if content:
                    self._metadata = json.loads(content)
                    xbmc.log(f"[CacheManager] Loaded {len(self._metadata)} cached entries", xbmc.LOGINFO)
        except Exception as e:
            xbmc.log(f"[CacheManager] Error loading metadata: {e}", xbmc.LOGWARNING)
            self._metadata = {}
    
    def _save_metadata_locked(self):
        """
        Save metadata to disk with atomic write (must hold lock).
        Uses temp file + rename pattern to prevent corruption.
        """
        metadata_path = os.path.join(self._cache_dir, self.METADATA_FILENAME)
        temp_path = metadata_path + '.tmp'
        
        try:
            # Write to temp file first
            content = json.dumps(self._metadata, ensure_ascii=False)
            f = xbmcvfs.File(temp_path, 'w')
            f.write(content)
            f.close()
            
            # Atomic rename (on most filesystems)
            if xbmcvfs.exists(metadata_path):
                xbmcvfs.delete(metadata_path)
            xbmcvfs.rename(temp_path, metadata_path)
            
            self._metadata_dirty = False
        except Exception as e:
            xbmc.log(f"[CacheManager] Error saving metadata: {e}", xbmc.LOGERROR)
            # Clean up temp file if it exists
            if xbmcvfs.exists(temp_path):
                xbmcvfs.delete(temp_path)
    
    # =========================================================================
    # ASSET CACHE (Generated images - collages, blurred backgrounds, etc.)
    # =========================================================================
    
    def get_asset_path(self, cache_key):
        """
        Get the filesystem path for a cached asset.
        
        Args:
            cache_key: Unique identifier for the asset
            
        Returns:
            Absolute path to the cached file, or None if not cached
        """
        filename = self._asset_filename(cache_key)
        filepath = os.path.join(self._cache_dir, filename)
        
        if xbmcvfs.exists(filepath):
            return filepath
        return None
    
    def save_asset(self, cache_key, data, extension='.jpg'):
        """
        Save binary asset to cache.
        
        Args:
            cache_key: Unique identifier for the asset
            data: Binary data (bytes) to save
            extension: File extension (default .jpg)
            
        Returns:
            Absolute path to the saved file
        """
        filename = self._asset_filename(cache_key, extension)
        filepath = os.path.join(self._cache_dir, filename)
        temp_path = filepath + '.tmp'
        
        try:
            # Write to temp file first (atomic pattern)
            f = xbmcvfs.File(temp_path, 'wb')
            f.write(data)
            f.close()
            
            # Rename to final path
            if xbmcvfs.exists(filepath):
                xbmcvfs.delete(filepath)
            xbmcvfs.rename(temp_path, filepath)
            
            return filepath
        except Exception as e:
            xbmc.log(f"[CacheManager] Error saving asset {cache_key}: {e}", xbmc.LOGERROR)
            if xbmcvfs.exists(temp_path):
                xbmcvfs.delete(temp_path)
            return None
    
    def save_pil_image(self, cache_key, pil_image, quality=85):
        """
        Save a PIL Image object to cache as JPEG.
        
        Args:
            cache_key: Unique identifier for the asset
            pil_image: PIL Image object
            quality: JPEG quality (1-100)
            
        Returns:
            Absolute path to the saved file, or None on error
        """
        from io import BytesIO
        
        try:
            buffer = BytesIO()
            pil_image.save(buffer, format='JPEG', quality=quality)
            data = buffer.getvalue()
            return self.save_asset(cache_key, data, '.jpg')
        except Exception as e:
            xbmc.log(f"[CacheManager] Error saving PIL image {cache_key}: {e}", xbmc.LOGERROR)
            return None
    
    def _asset_filename(self, cache_key, extension='.jpg'):
        """Generate a safe filename from cache key using MD5 hash."""
        if isinstance(cache_key, str):
            cache_key = cache_key.encode('utf-8')
        hash_str = hashlib.md5(cache_key).hexdigest()
        return f"asset_{hash_str}{extension}"
    
    # =========================================================================
    # GARBAGE COLLECTION / MAINTENANCE
    # =========================================================================
    
    def maintenance(self, max_age_days=None, max_size_mb=None, force=False):
        """
        Run cache maintenance (cleanup old files).
        Safe to call frequently - will skip if recently run.
        
        Args:
            max_age_days: Delete files older than this (default 7)
            max_size_mb: Target maximum cache size (default 100MB)
            force: Force run even if recently executed
        """
        # Throttle: don't run more than once per hour unless forced
        if not force and (time.time() - self._last_cleanup) < 3600:
            return
        
        self._last_cleanup = time.time()
        
        # Run in background thread to not block UI
        thread = threading.Thread(
            target=self._maintenance_worker,
            args=(max_age_days or self.DEFAULT_MAX_AGE_DAYS,
                  max_size_mb or self.DEFAULT_MAX_SIZE_MB),
            daemon=True
        )
        thread.start()
    
    def _maintenance_worker(self, max_age_days, max_size_mb):
        """Background worker for cache cleanup."""
        try:
            now = time.time()
            max_age_seconds = max_age_days * 24 * 3600
            max_size_bytes = max_size_mb * 1024 * 1024
            
            # 1. Clean expired metadata entries
            with self._metadata_lock:
                expired_keys = []
                for key, entry in self._metadata.items():
                    if 'expires' in entry and entry['expires'] < now:
                        expired_keys.append(key)
                    elif 'timestamp' in entry:
                        age = now - entry['timestamp']
                        if age > max_age_seconds:
                            expired_keys.append(key)
                
                for key in expired_keys:
                    del self._metadata[key]
                
                if expired_keys:
                    self._metadata_dirty = True
                    xbmc.log(f"[CacheManager] Cleaned {len(expired_keys)} expired metadata entries", xbmc.LOGINFO)
            
            # 2. Clean old asset files
            files_info = []  # (filepath, size, mtime)
            total_size = 0
            deleted_count = 0
            
            dirs, files = xbmcvfs.listdir(self._cache_dir)
            for filename in files:
                if filename == self.METADATA_FILENAME or filename.endswith('.tmp'):
                    continue
                
                filepath = os.path.join(self._cache_dir, filename)
                try:
                    stat = xbmcvfs.Stat(filepath)
                    size = stat.st_size()
                    mtime = stat.st_mtime()
                    
                    # Delete if too old
                    if (now - mtime) > max_age_seconds:
                        xbmcvfs.delete(filepath)
                        deleted_count += 1
                    else:
                        files_info.append((filepath, size, mtime))
                        total_size += size
                except:
                    pass
            
            # 3. If still over size limit, delete oldest files
            if total_size > max_size_bytes:
                # Sort by modification time (oldest first)
                files_info.sort(key=lambda x: x[2])
                
                for filepath, size, mtime in files_info:
                    if total_size <= max_size_bytes:
                        break
                    xbmcvfs.delete(filepath)
                    total_size -= size
                    deleted_count += 1
            
            if deleted_count > 0:
                xbmc.log(f"[CacheManager] Deleted {deleted_count} old cache files", xbmc.LOGINFO)
            
            # Save metadata if dirty
            self.flush()
            
        except Exception as e:
            xbmc.log(f"[CacheManager] Maintenance error: {e}", xbmc.LOGERROR)
    
    # =========================================================================
    # UTILITY METHODS
    # =========================================================================
    
    @staticmethod
    def make_key(*args):
        """
        Create a cache key from multiple arguments.
        
        Example:
            key = CacheManager.make_key('program_list', channel_id)
        """
        return '_'.join(str(arg) for arg in args)
    
    def clear_all(self):
        """Clear all cached data (metadata and files)."""
        # Clear metadata
        with self._metadata_lock:
            self._metadata = {}
            self._metadata_dirty = True
        
        # Delete all files except metadata file
        dirs, files = xbmcvfs.listdir(self._cache_dir)
        for filename in files:
            filepath = os.path.join(self._cache_dir, filename)
            xbmcvfs.delete(filepath)
        
        xbmc.log("[CacheManager] Cache cleared", xbmc.LOGINFO)


# Singleton instance for global access
_instance = None
_instance_lock = threading.Lock()


def get_cache_manager():
    """
    Get the global CacheManager instance (singleton).
    
    Returns:
        CacheManager instance
    """
    global _instance
    if _instance is None:
        with _instance_lock:
            if _instance is None:
                _instance = CacheManager()
    return _instance
