"""PyPI API Cache Implementation.

This module provides caching functionality for PyPI API responses to reduce
network calls and improve performance for repeated dependency checks.
"""

import hashlib
import json
import os
import time
from pathlib import Path


class PyPICache:
    """Cache for PyPI API responses with TTL-based expiration.

    Args:
        cache_dir: Directory to store cache files. If None, uses
                  platform-specific cache directory.
        ttl_hours: Time-to-live for cache entries in hours.
        enabled: Whether caching is enabled.

    """

    def __init__(
        self,
        cache_dir: Path | None = None,
        ttl_hours: int = 24,
        enabled: bool = True,
    ):
        """Initialize the PyPI cache."""
        self.enabled = enabled
        self.ttl_seconds = ttl_hours * 3600

        if cache_dir is None:
            # Use platform-specific cache directory
            cache_base = Path.home() / ".cache"
            if "XDG_CACHE_HOME" in os.environ:
                cache_base = Path(os.environ["XDG_CACHE_HOME"])
            self.cache_dir = cache_base / "zaojun" / "pypi_cache"
        else:
            self.cache_dir = cache_dir

        # Ensure cache directory exists
        self.cache_dir.mkdir(parents=True, exist_ok=True)

        # Cache statistics
        self.stats = {"hits": 0, "misses": 0, "expired": 0, "errors": 0}

    def _get_cache_key(self, package_name: str) -> str:
        """Generate a cache key for a package.

        Args:
            package_name: Name of the package.

        Returns:
            Cache key string.

        """
        # Normalize package name (lowercase as per PyPI convention)
        normalized_name = package_name.lower().replace("_", "-")
        # Create a hash for the key to avoid filesystem issues
        # Using md5 for cache key generation only, not for security
        key_hash = hashlib.md5(normalized_name.encode()).hexdigest()  # noqa: S324
        return f"{normalized_name}_{key_hash[:8]}.json"

    def _get_cache_path(self, package_name: str) -> Path:
        """Get the full cache file path for a package.

        Args:
            package_name: Name of the package.

        Returns:
            Path to cache file.

        """
        cache_key = self._get_cache_key(package_name)
        return self.cache_dir / cache_key

    def get(self, package_name: str) -> dict | None:
        """Get cached data for a package.

        Args:
            package_name: Name of the package.

        Returns:
            Cached data dictionary or None if not found/expired.

        """
        if not self.enabled:
            self.stats["misses"] += 1
            return None

        cache_file = self._get_cache_path(package_name)

        if not cache_file.exists():
            self.stats["misses"] += 1
            return None

        try:
            with open(cache_file) as f:
                cache_data = json.load(f)

            # Check if cache entry has expired
            cached_time = cache_data.get("timestamp", 0)
            current_time = time.time()

            if current_time - cached_time > self.ttl_seconds:
                self.stats["expired"] += 1
                # Remove expired cache file
                try:
                    cache_file.unlink()
                except OSError:
                    pass
                return None

            self.stats["hits"] += 1
            return cache_data.get("data")

        except (json.JSONDecodeError, OSError, KeyError):
            self.stats["errors"] += 1
            # Remove corrupted cache file
            try:
                cache_file.unlink()
            except OSError:
                pass
            return None

    def set(self, package_name: str, data: dict) -> bool:
        """Cache data for a package.

        Args:
            package_name: Name of the package.
            data: Data to cache.

        Returns:
            True if successful, False otherwise.

        """
        if not self.enabled:
            return False

        cache_file = self._get_cache_path(package_name)

        cache_entry = {
            "timestamp": time.time(),
            "package_name": package_name.lower(),
            "data": data,
        }

        try:
            # Write to temporary file first, then rename (atomic operation)
            temp_file = cache_file.with_suffix(".tmp")
            with open(temp_file, "w") as f:
                json.dump(cache_entry, f, indent=2)

            # Atomic rename
            temp_file.rename(cache_file)
            return True

        except (OSError, TypeError, ValueError):
            self.stats["errors"] += 1
            # Clean up temp file if it exists
            try:
                if temp_file.exists():
                    temp_file.unlink()
            except OSError:
                pass
            return False

    def clear(self, package_name: str | None = None) -> int:
        """Clear cache entries.

        Args:
            package_name: If specified, clear only this package's cache.
                         If None, clear all cache entries.

        Returns:
            Number of cache entries cleared.

        """
        cleared = 0

        if package_name:
            cache_file = self._get_cache_path(package_name)
            if cache_file.exists():
                try:
                    cache_file.unlink()
                    cleared = 1
                except OSError:
                    pass
        else:
            # Clear all cache files
            for cache_file in self.cache_dir.glob("*.json"):
                try:
                    cache_file.unlink()
                    cleared += 1
                except OSError:
                    pass

        return cleared

    def get_stats(self) -> dict:
        """Get cache statistics.

        Returns:
            Dictionary with cache statistics.

        """
        # Count total cache files
        cache_files = list(self.cache_dir.glob("*.json"))
        total_files = len(cache_files)

        # Calculate total cache size
        total_size = sum(f.stat().st_size for f in cache_files if f.exists())

        return {
            **self.stats,
            "enabled": self.enabled,
            "cache_dir": str(self.cache_dir),
            "total_entries": total_files,
            "total_size_bytes": total_size,
            "ttl_hours": self.ttl_seconds / 3600,
        }

    def is_expired(self, package_name: str) -> bool:
        """Check if a cache entry exists and is expired.

        Args:
            package_name: Name of the package.

        Returns:
            True if cache entry exists and is expired, False otherwise.

        """
        if not self.enabled:
            return True

        cache_file = self._get_cache_path(package_name)

        if not cache_file.exists():
            return True

        try:
            with open(cache_file) as f:
                cache_data = json.load(f)

            cached_time = cache_data.get("timestamp", 0)
            current_time = time.time()

            return current_time - cached_time > self.ttl_seconds

        except (json.JSONDecodeError, OSError, KeyError):
            return True


def get_default_cache() -> PyPICache:
    """Get the default cache instance.

    Returns:
        Default PyPICache instance.

    """
    return PyPICache()
