"""
Caching layer for CHRONOS performance optimization.

This module provides multi-tier caching (in-memory + Redis) for frequently
accessed data like vulnerability patterns, cryptographic algorithms, and
network service definitions.
"""

import hashlib
import json
import pickle
from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, Optional, Type
from functools import wraps
from datetime import datetime, timedelta
import logging

logger = logging.getLogger(__name__)


class CacheBackend(ABC):
    """Abstract base class for cache backends."""
    
    @abstractmethod
    def get(self, key: str) -> Optional[Any]:
        """Retrieve value from cache."""
        pass
        
    @abstractmethod
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        """Store value in cache."""
        pass
        
    @abstractmethod
    def delete(self, key: str) -> None:
        """Remove value from cache."""
        pass
        
    @abstractmethod
    def clear(self) -> None:
        """Clear all cached values."""
        pass
        
    @abstractmethod
    def exists(self, key: str) -> bool:
        """Check if key exists in cache."""
        pass


class InMemoryCache(CacheBackend):
    """In-memory cache with TTL support for fast lookups."""
    
    def __init__(self, max_size: int = 10000):
        self.cache: Dict[str, tuple] = {}  # (value, expiry_time)
        self.max_size = max_size
        self.hits = 0
        self.misses = 0
        
    def get(self, key: str) -> Optional[Any]:
        """Retrieve value from in-memory cache."""
        if key not in self.cache:
            self.misses += 1
            return None
            
        value, expiry = self.cache[key]
        if datetime.now() > expiry:
            del self.cache[key]
            self.misses += 1
            return None
            
        self.hits += 1
        return value
        
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        """Store value in in-memory cache with TTL."""
        # Simple LRU: remove random entry if full
        if len(self.cache) >= self.max_size:
            oldest_key = next(iter(self.cache))
            del self.cache[oldest_key]
            logger.debug(f"Cache evicted oldest entry: {oldest_key}")
            
        expiry = datetime.now() + timedelta(seconds=ttl_seconds)
        self.cache[key] = (value, expiry)
        
    def delete(self, key: str) -> None:
        """Remove value from cache."""
        if key in self.cache:
            del self.cache[key]
            
    def clear(self) -> None:
        """Clear all cached values."""
        self.cache.clear()
        
    def exists(self, key: str) -> bool:
        """Check if key exists and is not expired."""
        return self.get(key) is not None
        
    def get_stats(self) -> Dict[str, int]:
        """Get cache statistics."""
        total = self.hits + self.misses
        hit_rate = (self.hits / total * 100) if total > 0 else 0
        return {
            'hits': self.hits,
            'misses': self.misses,
            'hit_rate_percent': hit_rate,
            'size': len(self.cache),
            'max_size': self.max_size
        }


class RedisCache(CacheBackend):
    """Redis-backed cache for distributed caching."""
    
    def __init__(self, host: str = 'localhost', port: int = 6379, db: int = 0):
        try:
            import redis
            self.redis = redis.Redis(host=host, port=port, db=db)
            self.redis.ping()
            self.available = True
            logger.info(f"Connected to Redis at {host}:{port}")
        except Exception as e:
            logger.warning(f"Redis connection failed: {e}. Using fallback cache.")
            self.available = False
            self.redis = None
            
    def get(self, key: str) -> Optional[Any]:
        """Retrieve value from Redis."""
        if not self.available or not self.redis:
            return None
            
        try:
            value = self.redis.get(key)
            if value:
                return pickle.loads(value)
        except Exception as e:
            logger.error(f"Redis get failed for key {key}: {e}")
        return None
        
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        """Store value in Redis with TTL."""
        if not self.available or not self.redis:
            return
            
        try:
            serialized = pickle.dumps(value)
            self.redis.setex(key, ttl_seconds, serialized)
        except Exception as e:
            logger.error(f"Redis set failed for key {key}: {e}")
            
    def delete(self, key: str) -> None:
        """Remove value from Redis."""
        if not self.available or not self.redis:
            return
            
        try:
            self.redis.delete(key)
        except Exception as e:
            logger.error(f"Redis delete failed for key {key}: {e}")
            
    def clear(self) -> None:
        """Clear all values in Redis database."""
        if not self.available or not self.redis:
            return
            
        try:
            self.redis.flushdb()
        except Exception as e:
            logger.error(f"Redis flushdb failed: {e}")
            
    def exists(self, key: str) -> bool:
        """Check if key exists in Redis."""
        if not self.available or not self.redis:
            return False
            
        try:
            return self.redis.exists(key) > 0
        except Exception as e:
            logger.error(f"Redis exists check failed: {e}")
            return False


class TieredCache:
    """Two-tier cache: in-memory (fast) + Redis (distributed)."""
    
    def __init__(self, redis_host: str = 'localhost'):
        self.memory_cache = InMemoryCache()
        self.redis_cache = RedisCache(host=redis_host)
        
    def get(self, key: str) -> Optional[Any]:
        """Try to get value from memory cache first, then Redis."""
        # Try memory cache first (fastest)
        value = self.memory_cache.get(key)
        if value is not None:
            return value
            
        # Try Redis second (distributed)
        value = self.redis_cache.get(key)
        if value is not None:
            self.memory_cache.set(key, value)  # Populate memory cache
            return value
            
        return None
        
    def set(self, key: str, value: Any, ttl_seconds: int = 3600) -> None:
        """Store in both tiers."""
        self.memory_cache.set(key, value, ttl_seconds)
        self.redis_cache.set(key, value, ttl_seconds)
        
    def delete(self, key: str) -> None:
        """Delete from both tiers."""
        self.memory_cache.delete(key)
        self.redis_cache.delete(key)
        
    def clear(self) -> None:
        """Clear both tiers."""
        self.memory_cache.clear()
        self.redis_cache.clear()


def make_cache_key(*args, **kwargs) -> str:
    """Generate cache key from function arguments.
    
    Args:
        *args: Positional arguments
        **kwargs: Keyword arguments
        
    Returns:
        SHA256 hash as cache key
    """
    key_data = json.dumps({
        'args': str(args),
        'kwargs': str(sorted(kwargs.items()))
    }, sort_keys=True)
    return hashlib.sha256(key_data.encode()).hexdigest()


class CachedFunction:
    """Decorator for caching function results."""
    
    def __init__(
        self,
        cache: Optional[CacheBackend] = None,
        ttl_seconds: int = 3600,
        key_prefix: str = ""
    ):
        self.cache = cache or InMemoryCache()
        self.ttl = ttl_seconds
        self.prefix = key_prefix
        
    def __call__(self, func: Callable) -> Callable:
        """Wrap function with caching."""
        @wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            # Generate cache key
            cache_key = f"{self.prefix}:{make_cache_key(*args, **kwargs)}"
            
            # Try to get from cache
            cached = self.cache.get(cache_key)
            if cached is not None:
                logger.debug(f"Cache hit for {func.__name__}")
                return cached
                
            # Call function and cache result
            logger.debug(f"Cache miss for {func.__name__}")
            result = func(*args, **kwargs)
            self.cache.set(cache_key, result, self.ttl)
            
            return result
        return wrapper


def cached_result(
    ttl_seconds: int = 3600,
    key_prefix: str = ""
):
    """Decorator to cache function results.
    
    Usage:
        @cached_result(ttl_seconds=3600, key_prefix="crypto")
        def get_algorithm_strength(algo_name):
            ...
    """
    def decorator(func: Callable) -> Callable:
        cache = InMemoryCache()
        cached_func = CachedFunction(cache, ttl_seconds, key_prefix)
        return cached_func(func)
    return decorator


# Specific cache instances for CHRONOS components

class VulnerabilityPatternCache:
    """Cache for vulnerability pattern matching."""
    
    def __init__(self):
        self.cache = InMemoryCache(max_size=5000)
        
    def get_pattern(self, pattern_id: str) -> Optional[Dict]:
        """Get vulnerability pattern from cache."""
        return self.cache.get(f"pattern:{pattern_id}")
        
    def set_pattern(self, pattern_id: str, pattern: Dict) -> None:
        """Cache vulnerability pattern."""
        self.cache.set(f"pattern:{pattern_id}", pattern, ttl_seconds=86400)
        
    def clear_patterns(self) -> None:
        """Clear pattern cache."""
        self.cache.clear()


class CryptographyCache:
    """Cache for cryptographic algorithm metadata."""
    
    def __init__(self):
        self.cache = InMemoryCache(max_size=1000)
        
    def get_algorithm(self, algo_name: str) -> Optional[Dict]:
        """Get algorithm metadata from cache."""
        return self.cache.get(f"crypto:{algo_name}")
        
    def set_algorithm(self, algo_name: str, metadata: Dict) -> None:
        """Cache algorithm metadata."""
        self.cache.set(f"crypto:{algo_name}", metadata, ttl_seconds=604800)


class NetworkServiceCache:
    """Cache for network service definitions."""
    
    def __init__(self):
        self.cache = InMemoryCache(max_size=2000)
        
    def get_service(self, ip: str, port: int) -> Optional[Dict]:
        """Get service info from cache."""
        key = f"service:{ip}:{port}"
        return self.cache.get(key)
        
    def set_service(self, ip: str, port: int, service_info: Dict) -> None:
        """Cache service information."""
        key = f"service:{ip}:{port}"
        self.cache.set(key, service_info, ttl_seconds=3600)


class ScanResultsCache:
    """Cache scan results for comparison."""
    
    def __init__(self):
        self.cache = InMemoryCache(max_size=100)
        
    def get_latest_scan(self, project_id: str) -> Optional[Dict]:
        """Get latest scan results."""
        return self.cache.get(f"scan:latest:{project_id}")
        
    def set_scan_result(self, project_id: str, scan_id: str, results: Dict) -> None:
        """Cache scan results."""
        self.cache.set(
            f"scan:{scan_id}",
            results,
            ttl_seconds=86400
        )
        self.cache.set(
            f"scan:latest:{project_id}",
            results,
            ttl_seconds=3600
        )
        
    def get_scan(self, scan_id: str) -> Optional[Dict]:
        """Get specific scan results."""
        return self.cache.get(f"scan:{scan_id}")


# Global cache instances
vulnerability_pattern_cache = VulnerabilityPatternCache()
cryptography_cache = CryptographyCache()
network_service_cache = NetworkServiceCache()
scan_results_cache = ScanResultsCache()


def invalidate_all_caches() -> None:
    """Clear all caches (use after major updates)."""
    vulnerability_pattern_cache.clear_patterns()
    cryptography_cache.cache.clear()
    network_service_cache.cache.clear()
    scan_results_cache.cache.clear()
    logger.info("All caches invalidated")
