"""
Enhanced caching service with multiple cache backends and strategies.
"""

import hashlib
import json
import logging
import time
from collections.abc import Callable
from functools import wraps
from typing import Any

from django.core.cache import caches

from apps.core.utils import capture_and_log

logger = logging.getLogger(__name__)


# Define GeoBounds for type hinting
class GeoBounds:
    def __init__(self, min_lat: float, min_lng: float, max_lat: float, max_lng: float):
        self.min_lat = min_lat
        self.min_lng = min_lng
        self.max_lat = max_lat
        self.max_lng = max_lng


class EnhancedCacheService:
    """Comprehensive caching service with multiple cache backends"""

    def __init__(self):
        self.default_cache = caches["default"]
        try:
            self.api_cache = caches["api"]
        except Exception:
            # Fallback to default cache if api cache not configured
            self.api_cache = self.default_cache

    # L1: Query-level caching
    def cache_queryset(
        self,
        cache_key: str,
        queryset_func: Callable,
        timeout: int = 3600,
        **kwargs,
    ) -> Any:
        """Cache expensive querysets"""
        cached_result = self.default_cache.get(cache_key)
        if cached_result is None:
            start_time = time.time()
            result = queryset_func(**kwargs)
            duration = time.time() - start_time

            # Log cache miss and function execution time
            logger.info(
                f"Cache miss for key '{cache_key}', executed in {duration:.3f}s",
                extra={"cache_key": cache_key, "execution_time": duration},
            )

            self.default_cache.set(cache_key, result, timeout)
            return result

        logger.debug(f"Cache hit for key '{cache_key}'")
        return cached_result

    # L2: API response caching
    def cache_api_response(
        self,
        view_name: str,
        params: dict,
        response_data: Any,
        timeout: int = 1800,
    ):
        """Cache API responses based on view and parameters"""
        cache_key = self._generate_api_cache_key(view_name, params)
        self.api_cache.set(cache_key, response_data, timeout)
        logger.debug(f"Cached API response for view '{view_name}'")

    def get_cached_api_response(self, view_name: str, params: dict) -> Any | None:
        """Retrieve cached API response"""
        cache_key = self._generate_api_cache_key(view_name, params)
        result = self.api_cache.get(cache_key)

        if result:
            logger.debug(f"Cache hit for API view '{view_name}'")
        else:
            logger.debug(f"Cache miss for API view '{view_name}'")

        return result

    # L3: Geographic caching (building on existing MapCacheService)
    def cache_geographic_data(
        self,
        bounds: "GeoBounds",
        data: Any,
        zoom_level: int,
        timeout: int = 1800,
    ):
        """Cache geographic data with spatial keys"""
        # Generate spatial cache key based on bounds and zoom level
        cache_key = f"geo:{bounds.min_lat}:{bounds.min_lng}:{bounds.max_lat}:{
            bounds.max_lng
        }:z{zoom_level}"
        self.default_cache.set(cache_key, data, timeout)
        logger.debug(f"Cached geographic data for bounds {bounds}")

    def get_cached_geographic_data(self, bounds: "GeoBounds", zoom_level: int) -> Any | None:
        """Retrieve cached geographic data"""
        cache_key = f"geo:{bounds.min_lat}:{bounds.min_lng}:{bounds.max_lat}:{
            bounds.max_lng
        }:z{zoom_level}"
        return self.default_cache.get(cache_key)

    # Cache invalidation utilities
    def invalidate_pattern(self, pattern: str):
        """Invalidate cache keys matching a pattern (if backend supports it)"""
        try:
            # For Redis cache backends
            if hasattr(self.default_cache, "delete_pattern"):
                deleted_count = self.default_cache.delete_pattern(pattern)
                logger.info(f"Invalidated {deleted_count} cache keys matching pattern '{pattern}'")
                return deleted_count
            else:
                logger.warning(f"Cache backend does not support pattern deletion for pattern '{pattern}'")
        except Exception as e:
            capture_and_log(e, f"Invalidate cache pattern '{pattern}'", source='service', severity='low')

    def invalidate_model_cache(self, model_name: str, instance_id: int | None = None):
        """Invalidate cache keys related to a specific model"""
        pattern = f"*{model_name}:{instance_id}*" if instance_id else f"*{model_name}*"

        self.invalidate_pattern(pattern)

    # Cache warming utilities
    def warm_cache(
        self,
        cache_key: str,
        warm_func: Callable,
        timeout: int = 3600,
        **kwargs,
    ):
        """Proactively warm cache with data"""
        try:
            data = warm_func(**kwargs)
            self.default_cache.set(cache_key, data, timeout)
            logger.info(f"Warmed cache for key '{cache_key}'")
        except Exception as e:
            capture_and_log(e, f"Warm cache for key '{cache_key}'", source='service', severity='low')

    def _generate_api_cache_key(self, view_name: str, params: dict) -> str:
        """Generate consistent cache keys for API responses"""
        # Sort params to ensure consistent key generation
        params_str = json.dumps(params, sort_keys=True, default=str)
        params_hash = hashlib.md5(params_str.encode()).hexdigest()
        return f"api:{view_name}:{params_hash}"


# Cache decorators
def cache_api_response(timeout=1800, vary_on=None, key_prefix=""):
    """Decorator for caching API responses"""

    def decorator(view_func):
        @wraps(view_func)
        def wrapper(self, request, *args, **kwargs):
            if request.method != "GET":
                return view_func(self, request, *args, **kwargs)

            # Generate cache key based on view, user, and parameters
            cache_key_parts = [
                key_prefix or view_func.__name__,
                (str(request.user.id) if request.user.is_authenticated else "anonymous"),
                str(hash(frozenset(request.GET.items()))),
            ]

            if vary_on:
                for field in vary_on:
                    cache_key_parts.append(str(getattr(request, field, "")))

            cache_key = ":".join(cache_key_parts)

            # Try to get from cache
            cache_service = EnhancedCacheService()
            cached_response = cache_service.api_cache.get(cache_key)
            if cached_response is not None:
                logger.debug(f"Cache hit for API view {view_func.__name__}")
                return cached_response

            # Execute view and cache result
            response = view_func(self, request, *args, **kwargs)
            if hasattr(response, "status_code") and response.status_code == 200:
                cache_service.api_cache.set(cache_key, response, timeout)
                logger.debug(f"Cached API response for view {view_func.__name__}")

            return response

        return wrapper

    return decorator


def cache_queryset_result(cache_key_template: str, timeout: int = 3600):
    """Decorator for caching queryset results"""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Generate cache key from template and arguments
            cache_key = cache_key_template.format(*args, **kwargs)

            cache_service = EnhancedCacheService()
            return cache_service.cache_queryset(cache_key, func, timeout, *args, **kwargs)

        return wrapper

    return decorator


# Context manager for cache warming
class CacheWarmer:
    """Context manager for batch cache warming operations"""

    def __init__(self):
        self.cache_service = EnhancedCacheService()
        self.warm_operations = []

    def add(
        self,
        cache_key: str,
        warm_func: Callable,
        timeout: int = 3600,
        **kwargs,
    ):
        """Add a cache warming operation to the batch"""
        self.warm_operations.append(
            {
                "cache_key": cache_key,
                "warm_func": warm_func,
                "timeout": timeout,
                "kwargs": kwargs,
            }
        )

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """Execute all cache warming operations"""
        logger.info(f"Warming {len(self.warm_operations)} cache entries")

        for operation in self.warm_operations:
            try:
                self.cache_service.warm_cache(**operation)
            except Exception as e:
                capture_and_log(e, f"Warm cache for {operation['cache_key']}", source='service', severity='low')


# Cache statistics and monitoring
class CacheMonitor:
    """Monitor cache performance and statistics"""

    def __init__(self):
        self.cache_service = EnhancedCacheService()

    def get_cache_stats(self) -> dict[str, Any]:
        """Get cache statistics if available"""
        stats = {}

        try:
            # Try to get Redis cache stats
            cache_backend = self.cache_service.default_cache.__class__.__name__

            if "Redis" in cache_backend:
                # Attempt to get Redis client and stats
                redis_client = self.cache_service.default_cache._cache.get_client()
                info = redis_client.info()
                stats["redis"] = {
                    "used_memory": info.get("used_memory_human"),
                    "connected_clients": info.get("connected_clients"),
                    "total_commands_processed": info.get("total_commands_processed"),
                    "keyspace_hits": info.get("keyspace_hits"),
                    "keyspace_misses": info.get("keyspace_misses"),
                }

                # Calculate hit rate
                hits = info.get("keyspace_hits", 0)
                misses = info.get("keyspace_misses", 0)
                if hits + misses > 0:
                    stats["redis"]["hit_rate"] = hits / (hits + misses) * 100
            else:
                # For local memory cache or other backends
                stats["cache_backend"] = cache_backend
                stats["message"] = f"Cache statistics not available for {cache_backend}"

        except Exception:
            # Don't log as error since this is expected for non-Redis backends
            cache_backend = self.cache_service.default_cache.__class__.__name__
            stats["cache_backend"] = cache_backend
            stats["message"] = f"Cache statistics not available for {cache_backend}"

        return stats

    def log_cache_performance(self):
        """Log cache performance metrics"""
        stats = self.get_cache_stats()
        if stats:
            logger.info("Cache performance statistics", extra=stats)

    def get_cache_statistics(self, key_prefix: str = "") -> dict[str, Any]:
        """
        Get cache statistics for a given key prefix.

        Returns hits, misses, hit_rate, and size if available.
        Falls back to global cache statistics for Redis backends.
        """
        stats = {
            "hits": 0,
            "misses": 0,
            "hit_rate": 0.0,
            "size": 0,
            "backend": "unknown",
        }

        try:
            cache_backend = self.cache_service.default_cache.__class__.__name__
            stats["backend"] = cache_backend

            if "Redis" in cache_backend:
                # Get Redis client and stats
                redis_client = self.cache_service.default_cache._cache.get_client()
                info = redis_client.info()

                hits = info.get("keyspace_hits", 0)
                misses = info.get("keyspace_misses", 0)

                stats["hits"] = hits
                stats["misses"] = misses
                stats["hit_rate"] = (hits / (hits + misses) * 100) if (hits + misses) > 0 else 0.0

                # Get key count for prefix if pattern matching is supported
                if key_prefix:
                    try:
                        keys = redis_client.keys(f"*{key_prefix}*")
                        stats["size"] = len(keys) if keys else 0
                    except Exception:
                        stats["size"] = info.get("db0", {}).get("keys", 0) if isinstance(info.get("db0"), dict) else 0
                else:
                    stats["size"] = info.get("db0", {}).get("keys", 0) if isinstance(info.get("db0"), dict) else 0

            else:
                # For local memory cache - limited statistics available
                stats["message"] = f"Detailed statistics not available for {cache_backend}"

        except Exception as e:
            logger.debug(f"Could not retrieve cache statistics: {e}")
            stats["message"] = "Cache statistics unavailable"

        return stats
