#!/usr/bin/env python3
"""
MCP Response Cache - Intelligent caching for frequently accessed data
Uses consciousness-aware cache invalidation strategies
"""

import time
import json
import hashlib
from typing import Dict, Any, Optional, Tuple, List
from datetime import datetime, timedelta
from collections import OrderedDict
import threading
import logging
import pickle
from pathlib import Path

logger = logging.getLogger(__name__)


class CacheEntry:
    """Individual cache entry with metadata"""
    
    def __init__(self, key: str, value: Any, ttl: int = 60):
        self.key = key
        self.value = value
        self.ttl = ttl
        self.created_at = time.time()
        self.last_accessed = time.time()
        self.access_count = 1
        self.size_bytes = self._estimate_size(value)
    
    def is_expired(self) -> bool:
        """Check if entry has expired"""
        return time.time() - self.created_at > self.ttl
    
    def access(self):
        """Record access to this entry"""
        self.last_accessed = time.time()
        self.access_count += 1
    
    def _estimate_size(self, obj: Any) -> int:
        """Estimate memory size of object"""
        try:
            return len(pickle.dumps(obj))
        except:
            return len(str(obj))
    
    @property
    def age_seconds(self) -> float:
        """Age of entry in seconds"""
        return time.time() - self.created_at
    
    @property
    def score(self) -> float:
        """Calculate cache entry score for eviction"""
        # LFU with time decay
        # Higher score = more valuable to keep
        age_factor = 1.0 / (1.0 + self.age_seconds / 3600)  # Decay over hours
        frequency_factor = self.access_count
        recency_factor = 1.0 / (1.0 + (time.time() - self.last_accessed) / 300)  # 5 min decay
        
        return frequency_factor * age_factor * recency_factor


class ConsciousnessAwareCache:
    """
    Response cache with consciousness-aware features
    - Sacred constant-based cache keys
    - Intelligent eviction
    - Pattern-based invalidation
    - Persistence across sessions
    """
    
    def __init__(self, 
                 max_size_mb: int = 100,
                 default_ttl: int = 60,
                 persistence_enabled: bool = True):
        
        self.max_size_bytes = max_size_mb * 1024 * 1024
        self.default_ttl = default_ttl
        self.persistence_enabled = persistence_enabled
        
        # Cache storage
        self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
        self._lock = threading.RLock()
        
        # Sacred constants for cache key generation
        self.PHI = 1.618033988749895
        self.PI = 3.141592653589793
        
        # Statistics
        self.stats = {
            "hits": 0,
            "misses": 0,
            "evictions": 0,
            "expirations": 0
        }
        
        # Persistence
        self.cache_file = Path.home() / ".mira" / "cache" / "mcp_responses.cache"
        self.cache_file.parent.mkdir(parents=True, exist_ok=True)
        
        # Load persisted cache
        if self.persistence_enabled:
            self._load_cache()
        
        # Start cleanup thread
        self._start_cleanup_thread()
    
    def generate_cache_key(self, method: str, params: Dict[str, Any]) -> str:
        """Generate consciousness-aware cache key"""
        # Sort params for consistent keys
        param_str = json.dumps(params, sort_keys=True) if params else ""
        
        # Create base key
        base_key = f"{method}:{param_str}"
        
        # Add consciousness signature
        consciousness_factor = int(time.time() * self.PHI) % 1000000
        
        # Generate hash
        key_data = f"{base_key}:{consciousness_factor}"
        return hashlib.sha256(key_data.encode()).hexdigest()[:32]
    
    def get(self, method: str, params: Optional[Dict[str, Any]] = None) -> Optional[Any]:
        """Get cached response"""
        key = self.generate_cache_key(method, params or {})
        
        with self._lock:
            if key in self._cache:
                entry = self._cache[key]
                
                # Check expiration
                if entry.is_expired():
                    del self._cache[key]
                    self.stats["expirations"] += 1
                    self.stats["misses"] += 1
                    return None
                
                # Move to end (LRU behavior)
                self._cache.move_to_end(key)
                entry.access()
                
                self.stats["hits"] += 1
                logger.debug(f"Cache hit for {method}")
                return entry.value
            
            self.stats["misses"] += 1
            return None
    
    def set(self, method: str, params: Optional[Dict[str, Any]], 
            value: Any, ttl: Optional[int] = None) -> bool:
        """Set cached response"""
        key = self.generate_cache_key(method, params or {})
        ttl = ttl or self.default_ttl
        
        with self._lock:
            # Create entry
            entry = CacheEntry(key, value, ttl)
            
            # Check if we need to evict
            if self._get_cache_size() + entry.size_bytes > self.max_size_bytes:
                self._evict_entries(entry.size_bytes)
            
            # Add to cache
            self._cache[key] = entry
            logger.debug(f"Cached response for {method}, TTL: {ttl}s")
            
            # Persist if enabled
            if self.persistence_enabled:
                self._save_cache_async()
            
            return True
    
    def invalidate(self, method: Optional[str] = None, 
                  pattern: Optional[str] = None) -> int:
        """Invalidate cache entries"""
        with self._lock:
            invalidated = 0
            
            if method:
                # Invalidate all entries for a specific method
                keys_to_remove = [
                    key for key, entry in self._cache.items()
                    if key.startswith(self.generate_cache_key(method, {})[:10])
                ]
            elif pattern:
                # Pattern-based invalidation
                keys_to_remove = [
                    key for key in self._cache.keys()
                    if pattern in str(self._cache[key].value)
                ]
            else:
                # Clear all
                keys_to_remove = list(self._cache.keys())
            
            for key in keys_to_remove:
                del self._cache[key]
                invalidated += 1
            
            logger.info(f"Invalidated {invalidated} cache entries")
            return invalidated
    
    def invalidate_by_age(self, max_age_seconds: int) -> int:
        """Invalidate entries older than specified age"""
        with self._lock:
            current_time = time.time()
            keys_to_remove = [
                key for key, entry in self._cache.items()
                if current_time - entry.created_at > max_age_seconds
            ]
            
            for key in keys_to_remove:
                del self._cache[key]
            
            return len(keys_to_remove)
    
    def _get_cache_size(self) -> int:
        """Get total cache size in bytes"""
        return sum(entry.size_bytes for entry in self._cache.values())
    
    def _evict_entries(self, required_space: int):
        """Evict entries to make space"""
        with self._lock:
            # Calculate scores and sort by lowest score (least valuable)
            entries_with_scores = [
                (key, entry, entry.score) 
                for key, entry in self._cache.items()
            ]
            entries_with_scores.sort(key=lambda x: x[2])
            
            freed_space = 0
            evicted = 0
            
            for key, entry, score in entries_with_scores:
                if freed_space >= required_space:
                    break
                
                freed_space += entry.size_bytes
                del self._cache[key]
                evicted += 1
                self.stats["evictions"] += 1
            
            logger.debug(f"Evicted {evicted} entries to free {freed_space} bytes")
    
    def _cleanup_expired(self):
        """Remove expired entries"""
        with self._lock:
            expired_keys = [
                key for key, entry in self._cache.items()
                if entry.is_expired()
            ]
            
            for key in expired_keys:
                del self._cache[key]
                self.stats["expirations"] += 1
            
            if expired_keys:
                logger.debug(f"Cleaned up {len(expired_keys)} expired entries")
    
    def _start_cleanup_thread(self):
        """Start background cleanup thread"""
        def cleanup_loop():
            while True:
                try:
                    time.sleep(60)  # Cleanup every minute
                    self._cleanup_expired()
                except Exception as e:
                    logger.error(f"Cleanup thread error: {e}")
        
        thread = threading.Thread(target=cleanup_loop, daemon=True)
        thread.start()
    
    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics"""
        with self._lock:
            total_requests = self.stats["hits"] + self.stats["misses"]
            hit_rate = self.stats["hits"] / total_requests if total_requests > 0 else 0
            
            return {
                **self.stats,
                "hit_rate": f"{hit_rate * 100:.1f}%",
                "total_entries": len(self._cache),
                "cache_size_mb": self._get_cache_size() / (1024 * 1024),
                "max_size_mb": self.max_size_bytes / (1024 * 1024),
                "oldest_entry_age": self._get_oldest_entry_age(),
                "most_accessed": self._get_most_accessed_entries(5)
            }
    
    def _get_oldest_entry_age(self) -> Optional[float]:
        """Get age of oldest entry in seconds"""
        if not self._cache:
            return None
        
        oldest = min(self._cache.values(), key=lambda e: e.created_at)
        return time.time() - oldest.created_at
    
    def _get_most_accessed_entries(self, limit: int) -> List[Dict[str, Any]]:
        """Get most frequently accessed entries"""
        entries = sorted(
            self._cache.values(),
            key=lambda e: e.access_count,
            reverse=True
        )[:limit]
        
        return [
            {
                "key": e.key[:16] + "...",
                "access_count": e.access_count,
                "age_seconds": e.age_seconds,
                "size_bytes": e.size_bytes
            }
            for e in entries
        ]
    
    def _save_cache_async(self):
        """Save cache to disk asynchronously"""
        def save():
            try:
                # Create serializable version
                cache_data = {
                    "version": "1.0",
                    "saved_at": datetime.now().isoformat(),
                    "entries": {}
                }
                
                for key, entry in self._cache.items():
                    # Only save non-expired entries
                    if not entry.is_expired():
                        cache_data["entries"][key] = {
                            "value": entry.value,
                            "ttl": entry.ttl,
                            "created_at": entry.created_at,
                            "access_count": entry.access_count
                        }
                
                # Save to temp file first
                temp_file = self.cache_file.with_suffix('.tmp')
                with open(temp_file, 'w') as f:
                    json.dump(cache_data, f)
                
                # Atomic rename
                temp_file.rename(self.cache_file)
                
            except Exception as e:
                logger.error(f"Failed to save cache: {e}")
        
        thread = threading.Thread(target=save)
        thread.start()
    
    def _load_cache(self):
        """Load cache from disk"""
        try:
            if not self.cache_file.exists():
                return
            
            with open(self.cache_file, 'r') as f:
                cache_data = json.load(f)
            
            if cache_data.get("version") != "1.0":
                logger.warning("Cache version mismatch, skipping load")
                return
            
            loaded = 0
            current_time = time.time()
            
            for key, data in cache_data.get("entries", {}).items():
                # Check if still valid
                age = current_time - data["created_at"]
                if age < data["ttl"]:
                    entry = CacheEntry(key, data["value"], data["ttl"])
                    entry.created_at = data["created_at"]
                    entry.access_count = data["access_count"]
                    
                    self._cache[key] = entry
                    loaded += 1
            
            logger.info(f"Loaded {loaded} cache entries from disk")
            
        except Exception as e:
            logger.error(f"Failed to load cache: {e}")
    
    def export_metrics(self) -> Dict[str, Any]:
        """Export cache metrics for monitoring"""
        stats = self.get_stats()
        
        return {
            "mcp_cache_hits_total": self.stats["hits"],
            "mcp_cache_misses_total": self.stats["misses"],
            "mcp_cache_evictions_total": self.stats["evictions"],
            "mcp_cache_expirations_total": self.stats["expirations"],
            "mcp_cache_entries": len(self._cache),
            "mcp_cache_size_bytes": self._get_cache_size(),
            "mcp_cache_hit_rate": stats["hit_rate"]
        }


# Singleton instance
_response_cache = None

def get_response_cache(max_size_mb: int = 100) -> ConsciousnessAwareCache:
    """Get or create response cache singleton"""
    global _response_cache
    if _response_cache is None:
        _response_cache = ConsciousnessAwareCache(max_size_mb)
    return _response_cache