"""
Cache Service for Reports System

Provides caching functionality for expensive calculations and report data
to improve performance and reduce database load.
"""

from django.core.cache import cache
from django.conf import settings
from typing import Any, Optional, Callable
from functools import wraps
import hashlib
import json
import logging

logger = logging.getLogger(__name__)


class CacheService:
    """
    Service for caching expensive report calculations and data.
    
    Provides methods to cache and retrieve report data with automatic
    cache key generation and expiration management.
    """
    
    # Default cache timeout (5 minutes)
    DEFAULT_TIMEOUT = 300
    
    # Cache key prefixes for different data types
    PREFIX_CLIENT_METRICS = 'client_metrics'
    PREFIX_LOAN_CALCULATIONS = 'loan_calc'
    PREFIX_REPORT_DATA = 'report_data'
    PREFIX_DASHBOARD_DATA = 'dashboard'
    PREFIX_EXPORT_DATA = 'export'
    
    @staticmethod
    def generate_cache_key(prefix: str, *args, **kwargs) -> str:
        """
        Generate a unique cache key based on prefix and parameters.
        
        Args:
            prefix: Cache key prefix (e.g., 'client_metrics')
            *args: Positional arguments to include in key
            **kwargs: Keyword arguments to include in key
            
        Returns:
            Unique cache key string
        """
        # Combine all parameters into a string
        params_str = f"{args}_{kwargs}"
        
        # Create hash of parameters for consistent key length
        params_hash = hashlib.md5(params_str.encode()).hexdigest()
        
        # Combine prefix and hash
        cache_key = f"{prefix}_{params_hash}"
        
        return cache_key
    
    @staticmethod
    def get_cached_data(key: str) -> Optional[Any]:
        """
        Retrieve data from cache.
        
        Args:
            key: Cache key
            
        Returns:
            Cached data or None if not found
        """
        try:
            data = cache.get(key)
            if data is not None:
                logger.debug(f"Cache hit for key: {key}")
            else:
                logger.debug(f"Cache miss for key: {key}")
            return data
        except Exception as e:
            logger.error(f"Error retrieving from cache: {str(e)}")
            return None
    
    @staticmethod
    def set_cached_data(key: str, data: Any, timeout: int = DEFAULT_TIMEOUT) -> bool:
        """
        Store data in cache.
        
        Args:
            key: Cache key
            data: Data to cache
            timeout: Cache timeout in seconds (default: 5 minutes)
            
        Returns:
            True if successful, False otherwise
        """
        try:
            cache.set(key, data, timeout)
            logger.debug(f"Cached data with key: {key} (timeout: {timeout}s)")
            return True
        except Exception as e:
            logger.error(f"Error setting cache: {str(e)}")
            return False
    
    @staticmethod
    def invalidate_cache(key: str) -> bool:
        """
        Invalidate (delete) cached data.
        
        Args:
            key: Cache key to invalidate
            
        Returns:
            True if successful, False otherwise
        """
        try:
            cache.delete(key)
            logger.debug(f"Invalidated cache key: {key}")
            return True
        except Exception as e:
            logger.error(f"Error invalidating cache: {str(e)}")
            return False
    
    @staticmethod
    def invalidate_pattern(pattern: str) -> bool:
        """
        Invalidate all cache keys matching a pattern.
        
        Args:
            pattern: Pattern to match (e.g., 'client_metrics_*')
            
        Returns:
            True if successful, False otherwise
        """
        try:
            # Note: This requires a cache backend that supports pattern deletion
            # (e.g., Redis with django-redis)
            cache.delete_pattern(pattern)
            logger.debug(f"Invalidated cache pattern: {pattern}")
            return True
        except AttributeError:
            logger.warning("Cache backend does not support pattern deletion")
            return False
        except Exception as e:
            logger.error(f"Error invalidating cache pattern: {str(e)}")
            return False
    
    @staticmethod
    def cache_client_metrics(branch_id: Optional[str] = None, timeout: int = DEFAULT_TIMEOUT):
        """
        Decorator to cache client metrics calculations.
        
        Args:
            branch_id: Optional branch ID for cache key
            timeout: Cache timeout in seconds
            
        Returns:
            Decorator function
        """
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args, **kwargs):
                # Generate cache key
                cache_key = CacheService.generate_cache_key(
                    CacheService.PREFIX_CLIENT_METRICS,
                    branch_id,
                    *args,
                    **kwargs
                )
                
                # Try to get from cache
                cached_data = CacheService.get_cached_data(cache_key)
                if cached_data is not None:
                    return cached_data
                
                # Calculate and cache
                result = func(*args, **kwargs)
                CacheService.set_cached_data(cache_key, result, timeout)
                
                return result
            return wrapper
        return decorator
    
    @staticmethod
    def cache_report_data(report_type: str, filters: dict, timeout: int = DEFAULT_TIMEOUT):
        """
        Decorator to cache report data.
        
        Args:
            report_type: Type of report
            filters: Filter parameters
            timeout: Cache timeout in seconds
            
        Returns:
            Decorator function
        """
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args, **kwargs):
                # Generate cache key
                cache_key = CacheService.generate_cache_key(
                    CacheService.PREFIX_REPORT_DATA,
                    report_type,
                    json.dumps(filters, sort_keys=True),
                    *args,
                    **kwargs
                )
                
                # Try to get from cache
                cached_data = CacheService.get_cached_data(cache_key)
                if cached_data is not None:
                    return cached_data
                
                # Calculate and cache
                result = func(*args, **kwargs)
                CacheService.set_cached_data(cache_key, result, timeout)
                
                return result
            return wrapper
        return decorator
    
    @staticmethod
    def get_or_set_client_metrics(branch_id: Optional[str], calculator_func: Callable, 
                                  timeout: int = DEFAULT_TIMEOUT) -> Any:
        """
        Get client metrics from cache or calculate and cache them.
        
        Args:
            branch_id: Optional branch ID
            calculator_func: Function to calculate metrics if not cached
            timeout: Cache timeout in seconds
            
        Returns:
            Client metrics data
        """
        cache_key = CacheService.generate_cache_key(
            CacheService.PREFIX_CLIENT_METRICS,
            branch_id
        )
        
        # Try to get from cache
        cached_data = CacheService.get_cached_data(cache_key)
        if cached_data is not None:
            return cached_data
        
        # Calculate and cache
        result = calculator_func()
        CacheService.set_cached_data(cache_key, result, timeout)
        
        return result
    
    @staticmethod
    def get_or_set_dashboard_data(branch_id: Optional[str], portfolio_manager_id: Optional[str],
                                  calculator_func: Callable, timeout: int = DEFAULT_TIMEOUT) -> Any:
        """
        Get dashboard data from cache or calculate and cache it.
        
        Args:
            branch_id: Optional branch ID
            portfolio_manager_id: Optional portfolio manager ID
            calculator_func: Function to calculate dashboard data if not cached
            timeout: Cache timeout in seconds
            
        Returns:
            Dashboard data
        """
        cache_key = CacheService.generate_cache_key(
            CacheService.PREFIX_DASHBOARD_DATA,
            branch_id,
            portfolio_manager_id
        )
        
        # Try to get from cache
        cached_data = CacheService.get_cached_data(cache_key)
        if cached_data is not None:
            return cached_data
        
        # Calculate and cache
        result = calculator_func()
        CacheService.set_cached_data(cache_key, result, timeout)
        
        return result
    
    @staticmethod
    def invalidate_client_metrics(branch_id: Optional[str] = None):
        """
        Invalidate client metrics cache.
        
        Args:
            branch_id: Optional branch ID to invalidate specific branch cache
        """
        if branch_id:
            cache_key = CacheService.generate_cache_key(
                CacheService.PREFIX_CLIENT_METRICS,
                branch_id
            )
            CacheService.invalidate_cache(cache_key)
        else:
            # Invalidate all client metrics
            CacheService.invalidate_pattern(f"{CacheService.PREFIX_CLIENT_METRICS}_*")
    
    @staticmethod
    def invalidate_dashboard_data(branch_id: Optional[str] = None):
        """
        Invalidate dashboard data cache.
        
        Args:
            branch_id: Optional branch ID to invalidate specific branch cache
        """
        if branch_id:
            cache_key = CacheService.generate_cache_key(
                CacheService.PREFIX_DASHBOARD_DATA,
                branch_id,
                None
            )
            CacheService.invalidate_cache(cache_key)
        else:
            # Invalidate all dashboard data
            CacheService.invalidate_pattern(f"{CacheService.PREFIX_DASHBOARD_DATA}_*")
    
    @staticmethod
    def invalidate_all_report_caches():
        """
        Invalidate all report-related caches.
        
        This should be called when loan data is updated to ensure
        reports show current data.
        """
        CacheService.invalidate_pattern(f"{CacheService.PREFIX_CLIENT_METRICS}_*")
        CacheService.invalidate_pattern(f"{CacheService.PREFIX_REPORT_DATA}_*")
        CacheService.invalidate_pattern(f"{CacheService.PREFIX_DASHBOARD_DATA}_*")
        logger.info("Invalidated all report caches")


# Convenience function for use in views
def cached_calculation(cache_key: str, calculator_func: Callable, 
                      timeout: int = CacheService.DEFAULT_TIMEOUT) -> Any:
    """
    Convenience function to get cached data or calculate and cache it.
    
    Args:
        cache_key: Cache key
        calculator_func: Function to calculate data if not cached
        timeout: Cache timeout in seconds
        
    Returns:
        Calculated or cached data
    """
    cached_data = CacheService.get_cached_data(cache_key)
    if cached_data is not None:
        return cached_data
    
    result = calculator_func()
    CacheService.set_cached_data(cache_key, result, timeout)
    
    return result
