"""
Constitutional Search Cache with Semantic Deduplication
X7K9F24 - Entropy Reduction via Intelligent Caching

This module implements a constitutional search cache that provides:
- Semantic deduplication with F2 (ΔS) optimization
- TTL management and cost-aware caching
- Constitutional validation for cached results
- Integration with 12-floor governance system

Status: SEALED
Nonce: X7K9F24
"""

import hashlib
import json
import logging
import time
from typing import Any, Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from collections import OrderedDict

from ..floors import floor_02_clarity as f2

logger = logging.getLogger("arifos_core.search_cache")


@dataclass
class CacheEntry:
    """Constitutional cache entry with governance metadata."""
    query_hash: str
    query: str
    result: Any
    floor_scores: Dict[str, float]
    timestamp: float
    ttl: float
    access_count: int = 0
    last_accessed: float = field(default_factory=time.time)
    constitutional_verdict: str = "SEAL"


class ConstitutionalSearchCache:
    """
    Constitutional search cache with semantic deduplication and F2 optimization.
    
    Features:
    - Semantic similarity matching using F2 (ΔS) clarity detection
    - TTL-based expiration with cost-aware management
    - Constitutional validation of cached results
    - LRU eviction with governance-aware scoring
    """
    
    def __init__(
        self,
        max_size: int = 1000,
        default_ttl: float = 3600,  # 1 hour
        semantic_threshold: float = 0.85,  # Similarity threshold for deduplication
        enable_f2_optimization: bool = True
    ):
        self.max_size = max_size
        self.default_ttl = default_ttl
        self.semantic_threshold = semantic_threshold
        self.enable_f2_optimization = enable_f2_optimization
        
        # LRU cache implementation
        self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
        self._stats = {
            "hits": 0,
            "misses": 0,
            "evictions": 0,
            "semantic_matches": 0,
            "total_requests": 0
        }
        
        logger.info(f"ConstitutionalSearchCache initialized (max_size={max_size}, ttl={default_ttl}s)")
    
    def get(
        self,
        query: str,
        context: Optional[Dict[str, Any]] = None
    ) -> Optional[Any]:
        """
        Get cached result with constitutional validation.
        
        Implements semantic deduplication using F2 (ΔS) optimization:
        1. Check for exact query match
        2. If no exact match, check for semantically similar queries
        3. Validate constitutional compliance of cached result
        4. Update access metrics
        
        Args:
            query: Search query
            context: Optional context for semantic matching
            
        Returns:
            Cached result if valid, None otherwise
        """
        self._stats["total_requests"] += 1
        context = context or {}
        
        # Generate query hash
        query_hash = self._hash_query(query, context)
        
        # Check for exact match
        if query_hash in self._cache:
            entry = self._cache[query_hash]
            
            # Check TTL
            if time.time() - entry.timestamp > entry.ttl:
                # Entry expired
                del self._cache[query_hash]
                self._stats["misses"] += 1
                logger.debug(f"Cache miss (expired): '{query[:50]}...'")
                return None
            
            # Validate constitutional compliance
            if not self._validate_constitutional_compliance(entry):
                # Constitutional violation detected
                del self._cache[query_hash]
                self._stats["misses"] += 1
                logger.warning(f"Cache miss (constitutional violation): '{query[:50]}...'")
                return None
            
            # Update access metrics
            entry.access_count += 1
            entry.last_accessed = time.time()
            
            # Move to end (LRU)
            self._cache.move_to_end(query_hash)
            
            self._stats["hits"] += 1
            logger.debug(f"Cache hit (exact): '{query[:50]}...' (accesses: {entry.access_count})")
            return entry.result
        
        # Check for semantic match if F2 optimization is enabled
        if self.enable_f2_optimization:
            semantic_match = self._find_semantic_match(query, context)
            if semantic_match:
                self._stats["semantic_matches"] += 1
                self._stats["hits"] += 1
                logger.debug(f"Cache hit (semantic): '{query[:50]}...' -> '{semantic_match.query[:50]}...'")
                return semantic_match.result
        
        self._stats["misses"] += 1
        logger.debug(f"Cache miss: '{query[:50]}...'")
        return None
    
    def put(
        self,
        query: str,
        result: Any,
        context: Optional[Dict[str, Any]] = None,
        ttl: Optional[float] = None,
        floor_scores: Optional[Dict[str, float]] = None
    ) -> bool:
        """
        Store result in cache with constitutional validation.
        
        Args:
            query: Search query
            result: Search result to cache
            context: Optional context
            ttl: Optional custom TTL (uses default if None)
            floor_scores: Constitutional floor scores from search
            
        Returns:
            True if stored successfully, False otherwise
        """
        context = context or {}
        ttl = ttl or self.default_ttl
        floor_scores = floor_scores or {}
        
        # Generate query hash
        query_hash = self._hash_query(query, context)
        
        # Validate result constitutionally before caching
        if not self._validate_result_for_caching(result, floor_scores):
            logger.warning(f"Result not cached due to constitutional validation failure: '{query[:50]}...'")
            return False
        
        # Check cache size and evict if necessary
        if len(self._cache) >= self.max_size:
            self._evict_lru()
        
        # Create cache entry
        entry = CacheEntry(
            query_hash=query_hash,
            query=query,
            result=result,
            floor_scores=floor_scores,
            timestamp=time.time(),
            ttl=ttl,
            access_count=1,
            constitutional_verdict=self._compute_constitutional_verdict(floor_scores)
        )
        
        # Store in cache
        self._cache[query_hash] = entry
        self._cache.move_to_end(query_hash)  # Mark as recently used
        
        logger.debug(f"Cached result: '{query[:50]}...' (ttl: {ttl}s)")
        return True
    
    def _hash_query(self, query: str, context: Dict[str, Any]) -> str:
        """Generate hash for query and context."""
        # Include relevant context in hash
        hash_data = {
            "query": query.lower().strip(),
            "user_id": context.get("user_id"),
            "search_providers": sorted(context.get("search_providers", [])),
            "constitutional_mode": context.get("constitutional_mode", "normal")
        }
        
        hash_input = json.dumps(hash_data, sort_keys=True)
        return hashlib.sha256(hash_input.encode()).hexdigest()
    
    def _find_semantic_match(
        self,
        query: str,
        context: Dict[str, Any]
    ) -> Optional[CacheEntry]:
        """
        Find semantically similar query using F2 (ΔS) optimization.
        
        Args:
            query: New query to match
            context: Search context
            
        Returns:
            Best semantic match or None
        """
        best_match = None
        best_score = 0.0
        
        for entry in self._cache.values():
            # Skip expired entries
            if time.time() - entry.timestamp > entry.ttl:
                continue
            
            # Skip constitutionally invalid entries
            if not self._validate_constitutional_compliance(entry):
                continue
            
            # Calculate semantic similarity using F2 clarity
            try:
                # Use F2's clarity detection for semantic similarity
                clarity_result = f2.compute_clarity_delta(query, entry.query)
                similarity_score = clarity_result.get("clarity_score", 0.0)
                
                # Invert for similarity (higher clarity delta = lower similarity)
                semantic_score = max(0.0, 1.0 - similarity_score)
                
                if semantic_score > best_score and semantic_score >= self.semantic_threshold:
                    best_score = semantic_score
                    best_match = entry
                    
            except Exception as e:
                logger.warning(f"F2 semantic matching error: {e}")
                continue
        
        return best_match
    
    def _validate_constitutional_compliance(self, entry: CacheEntry) -> bool:
        """
        Validate cached result for constitutional compliance.
        
        Args:
            entry: Cache entry to validate
            
        Returns:
            True if compliant, False otherwise
        """
        # Check if entry has valid constitutional verdict
        if entry.constitutional_verdict not in ["SEAL", "PARTIAL"]:
            return False
        
        # Check critical floors
        critical_floors = ["F1", "F2", "F6", "F9", "F10", "F11", "F12"]
        for floor in critical_floors:
            score = entry.floor_scores.get(floor, 1.0)
            if score < 0.5:  # Critical floor failure
                return False
        
        return True
    
    def _validate_result_for_caching(
        self,
        result: Any,
        floor_scores: Dict[str, float]
    ) -> bool:
        """
        Validate search result before allowing it to be cached.
        
        Args:
            result: Search result
            floor_scores: Constitutional floor scores
            
        Returns:
            True if valid for caching, False otherwise
        """
        # Don't cache VOID results
        verdict = self._compute_constitutional_verdict(floor_scores)
        if verdict == "VOID":
            return False
        
        # Check for critical floor failures
        critical_floors = ["F1", "F2", "F6", "F9", "F10", "F11", "F12"]
        for floor in critical_floors:
            score = floor_scores.get(floor, 1.0)
            if score < 0.3:  # Severe floor failure
                return False
        
        return True
    
    def _compute_constitutional_verdict(self, floor_scores: Dict[str, float]) -> str:
        """Compute constitutional verdict from floor scores."""
        # Critical floors that must pass
        critical_floors = ["F1", "F2", "F6", "F9", "F10", "F11", "F12"]
        
        # Check for any critical floor failures
        for floor in critical_floors:
            score = floor_scores.get(floor, 1.0)
            if score < 0.5:
                return "VOID"
        
        # Check soft floors
        soft_floors = ["F3", "F4", "F5", "F7", "F8"]
        soft_failures = 0
        
        for floor in soft_floors:
            score = floor_scores.get(floor, 1.0)
            if score < 0.7:
                soft_failures += 1
        
        if soft_failures > 0:
            return "PARTIAL"
        
        return "SEAL"
    
    def _evict_lru(self) -> Optional[CacheEntry]:
        """
        Evict least recently used entry with governance-aware scoring.
        
        Uses constitutional scoring to preferentially keep higher-quality entries.
        
        Returns:
            Evicted entry or None if cache is empty
        """
        if not self._cache:
            return None
        
        # Find entry with lowest governance score (least valuable)
        best_evict_score = float('inf')
        best_evict_key = None
        best_evict_entry = None
        
        for key, entry in self._cache.items():
            # Compute eviction score (lower = better to evict)
            # Factors: age, access frequency, constitutional quality
            age_score = (time.time() - entry.timestamp) / self.default_ttl
            access_score = 1.0 / (entry.access_count + 1)  # Lower access = better to evict
            
            # Constitutional quality score (lower quality = better to evict)
            quality_score = 1.0
            if entry.constitutional_verdict == "PARTIAL":
                quality_score = 0.7
            elif entry.constitutional_verdict == "VOID":
                quality_score = 0.0
            
            # Combine scores (weighted)
            evict_score = (
                age_score * 0.4 +           # Older entries are better to evict
                access_score * 0.3 +        # Less accessed entries are better to evict
                (1.0 - quality_score) * 0.3  # Lower quality entries are better to evict
            )
            
            if evict_score < best_evict_score:
                best_evict_score = evict_score
                best_evict_key = key
                best_evict_entry = entry
        
        if best_evict_key:
            del self._cache[best_evict_key]
            self._stats["evictions"] += 1
            logger.debug(f"Evicted cache entry: '{best_evict_entry.query[:50]}...' (score: {best_evict_score:.2f})")
            return best_evict_entry
        
        return None
    
    def clear_expired(self) -> int:
        """
        Clear expired entries from cache.
        
        Returns:
            Number of entries cleared
        """
        current_time = time.time()
        expired_keys = []
        
        for key, entry in self._cache.items():
            if current_time - entry.timestamp > entry.ttl:
                expired_keys.append(key)
        
        for key in expired_keys:
            del self._cache[key]
        
        if expired_keys:
            logger.info(f"Cleared {len(expired_keys)} expired cache entries")
        
        return len(expired_keys)
    
    def clear(self) -> None:
        """Clear all cache entries."""
        self._cache.clear()
        logger.info("Cache cleared")
    
    def get_stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        total_requests = self._stats["hits"] + self._stats["misses"]
        hit_rate = self._stats["hits"] / total_requests if total_requests > 0 else 0.0
        
        return {
            "size": len(self._cache),
            "max_size": self.max_size,
            "hits": self._stats["hits"],
            "misses": self._stats["misses"],
            "evictions": self._stats["evictions"],
            "semantic_matches": self._stats["semantic_matches"],
            "hit_rate": hit_rate,
            "total_requests": total_requests
        }
    
    def get_entry_info(self, query: str, context: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
        """
        Get information about a specific cache entry.
        
        Args:
            query: Search query
            context: Optional context
            
        Returns:
            Entry information or None if not found
        """
        context = context or {}
        query_hash = self._hash_query(query, context)
        
        entry = self._cache.get(query_hash)
        if not entry:
            return None
        
        return {
            "query": entry.query,
            "timestamp": entry.timestamp,
            "ttl": entry.ttl,
            "age": time.time() - entry.timestamp,
            "access_count": entry.access_count,
            "last_accessed": entry.last_accessed,
            "constitutional_verdict": entry.constitutional_verdict,
            "floor_scores": entry.floor_scores,
            "time_remaining": max(0, entry.ttl - (time.time() - entry.timestamp))
        }


class TTLAwareCache:
    """
    Simple TTL-aware cache for cost tracking and budget management.
    
    Used internally by ConstitutionalSearchCache for managing
    cost-related data with automatic expiration.
    """
    
    def __init__(self):
        self._data: Dict[str, Tuple[Any, float]] = {}  # key -> (value, expiry_time)
    
    def get(self, key: str) -> Optional[Any]:
        """Get value if not expired."""
        if key not in self._data:
            return None
        
        value, expiry_time = self._data[key]
        if time.time() > expiry_time:
            del self._data[key]
            return None
        
        return value
    
    def put(self, key: str, value: Any, ttl: float) -> None:
        """Store value with TTL."""
        expiry_time = time.time() + ttl
        self._data[key] = (value, expiry_time)
    
    def clear_expired(self) -> int:
        """Clear expired entries."""
        current_time = time.time()
        expired_keys = [
            key for key, (_, expiry_time) in self._data.items()
            if current_time > expiry_time
        ]
        
        for key in expired_keys:
            del self._data[key]
        
        return len(expired_keys)