"""Intelligent caching system for optimized performance."""

from __future__ import annotations

import functools
import hashlib
import json
import logging
import pickle
import time
from pathlib import Path
from typing import Any, Callable, TypeVar

T = TypeVar("T")
logger = logging.getLogger(__name__)


class CacheManager:
    """Intelligent cache management system."""

    def __init__(self, cache_dir: Path | None = None, max_size_mb: int = 100):
        """Initialize cache manager.

        Args:
            cache_dir: Directory for persistent cache storage
            max_size_mb: Maximum cache size in MB
        """
        self.cache_dir = cache_dir or Path.home() / ".pypack" / "cache"
        self.max_size_mb = max_size_mb
        self._memory_cache = {}
        self._access_times = {}
        self._setup_cache_dir()

    def _setup_cache_dir(self) -> None:
        """Set up cache directory."""
        try:
            self.cache_dir.mkdir(parents=True, exist_ok=True)
        except Exception as e:
            logger.warning(f"Failed to setup cache directory: {e}")

    @staticmethod
    def _get_cache_key(func: Callable, *args, **kwargs) -> str:
        """Generate cache key from function and arguments."""
        # Create hash of function name and arguments
        key_data = {"function": func.__name__, "args": args, "kwargs": kwargs}
        key_string = json.dumps(key_data, sort_keys=True, default=str)
        return hashlib.md5(key_string.encode()).hexdigest()

    def _get_cache_path(self, key: str) -> Path:
        """Get file path for cached item."""
        return self.cache_dir / f"{key}.cache"

    def get(self, key: str) -> Any | None:
        """Get item from cache.

        Args:
            key: Cache key

        Returns
        -------
            Cached value or None if not found/expired
        """
        # Check memory cache first
        if key in self._memory_cache:
            self._access_times[key] = time.time()
            return self._memory_cache[key]

        # Check disk cache
        cache_path = self._get_cache_path(key)
        if cache_path.exists():
            try:
                with cache_path.open("rb") as f:
                    cached_data = pickle.load(f)

                # Check if expired (24 hours default)
                if time.time() - cached_data.get("timestamp", 0) < 86400:
                    self._memory_cache[key] = cached_data["data"]
                    self._access_times[key] = time.time()
                    return cached_data["data"]
                # Remove expired cache
                cache_path.unlink()
            except Exception as e:
                logger.debug(f"Failed to read cache {key}: {e}")
                self.invalidate(key)

        return None

    def set(self, key: str, value: Any, ttl: int = 86400) -> None:
        """Set item in cache.

        Args:
            key: Cache key
            value: Value to cache
            ttl: Time to live in seconds
        """
        # Update memory cache
        self._memory_cache[key] = value
        self._access_times[key] = time.time()

        # Save to disk cache
        try:
            cache_path = self._get_cache_path(key)
            cached_data = {"data": value, "timestamp": time.time(), "ttl": ttl}
            with cache_path.open("wb") as f:
                pickle.dump(cached_data, f)
        except Exception as e:
            logger.debug(f"Failed to write cache {key}: {e}")

        # Check cache size and cleanup if needed
        self._cleanup_if_needed()

    def invalidate(self, key: str) -> None:
        """Invalidate cache entry.

        Args:
            key: Cache key to invalidate
        """
        # Remove from memory cache
        self._memory_cache.pop(key, None)
        self._access_times.pop(key, None)

        # Remove from disk cache
        cache_path = self._get_cache_path(key)
        try:
            if cache_path.exists():
                cache_path.unlink()
        except Exception as e:
            logger.debug(f"Failed to remove cache file {key}: {e}")

    def invalidate_pattern(self, pattern: str) -> None:
        """Invalidate cache entries matching pattern.

        Args:
            pattern: Pattern to match cache keys
        """
        keys_to_remove = [key for key in self._memory_cache.keys() if pattern in key]

        for key in keys_to_remove:
            self.invalidate(key)

    def clear(self) -> None:
        """Clear all cache."""
        self._memory_cache.clear()
        self._access_times.clear()

        # Clear disk cache
        try:
            for cache_file in self.cache_dir.glob("*.cache"):
                cache_file.unlink()
        except Exception as e:
            logger.warning(f"Failed to clear disk cache: {e}")

    def _cleanup_if_needed(self) -> None:
        """Clean up cache if size exceeds limit."""
        # Check memory cache size
        if len(self._memory_cache) > 1000:  # Arbitrary limit
            self._evict_least_used()

        # Check disk cache size
        try:
            total_size = sum(f.stat().st_size for f in self.cache_dir.glob("*.cache") if f.is_file())
            if total_size > self.max_size_mb * 1024 * 1024:
                self._evict_oldest_files()
        except Exception as e:
            logger.debug(f"Failed to check cache size: {e}")

    def _evict_least_used(self) -> None:
        """Evict least recently used items from memory cache."""
        if not self._access_times:
            return

        # Sort by access time and remove oldest 20%
        sorted_items = sorted(self._access_times.items(), key=lambda x: x[1])
        items_to_remove = len(sorted_items) // 5

        for key, _ in sorted_items[:items_to_remove]:
            self._memory_cache.pop(key, None)
            self._access_times.pop(key, None)

    def _evict_oldest_files(self) -> None:
        """Evict oldest files from disk cache."""
        try:
            cache_files = [(f, f.stat().st_mtime) for f in self.cache_dir.glob("*.cache") if f.is_file()]
            cache_files.sort(key=lambda x: x[1])

            # Remove oldest 20% of files
            files_to_remove = len(cache_files) // 5
            for file_path, _ in cache_files[:files_to_remove]:
                file_path.unlink()
        except Exception as e:
            logger.debug(f"Failed to evict old cache files: {e}")

    def get_stats(self) -> dict[str, Any]:
        """Get cache statistics.

        Returns
        -------
            Cache statistics
        """
        try:
            disk_files = list(self.cache_dir.glob("*.cache"))
            total_size = sum(f.stat().st_size for f in disk_files if f.is_file())

            return {
                "memory_items": len(self._memory_cache),
                "disk_items": len(disk_files),
                "disk_size_mb": total_size / (1024 * 1024),
                "max_size_mb": self.max_size_mb,
            }
        except Exception as e:
            logger.debug(f"Failed to get cache stats: {e}")
            return {
                "memory_items": len(self._memory_cache),
                "disk_items": 0,
                "disk_size_mb": 0,
                "max_size_mb": self.max_size_mb,
            }


# Decorator for easy caching
def cached(ttl: int = 3600, cache_manager: CacheManager | None = None):
    """Cache function results.

    Args:
        ttl: Time to live in seconds
        cache_manager: Cache manager instance to use
    """

    def decorator(func: Callable[..., T]) -> Callable[..., T]:
        cache = cache_manager or CacheManager()

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Generate cache key
            cache_key = cache._get_cache_key(func, *args, **kwargs)

            # Try to get from cache
            cached_result = cache.get(cache_key)
            if cached_result is not None:
                logger.debug(f"Cache hit for {func.__name__}")
                return cached_result

            # Execute function and cache result
            logger.debug(f"Cache miss for {func.__name__}")
            result = func(*args, **kwargs)
            cache.set(cache_key, result, ttl)

            return result

        return wrapper

    return decorator


# Predefined cache managers
default_cache = CacheManager()
dependency_cache = CacheManager(max_size_mb=50)
