"""Disk cache manager for PyPI metadata.

Stores fetched package metadata as JSON files keyed by SHA-256 hash
of the package name and version. Uses file mtime for TTL expiry.
"""

from __future__ import annotations

import hashlib
import json
import logging
import time
from pathlib import Path
from typing import Any

import platformdirs

logger = logging.getLogger(__name__)


class DiskCache:
    """Best-effort disk cache for PyPI package metadata.

    Cache files are stored as ``{sha256}.json`` under the platform-specific
    user cache directory for ``depwhy``.  Failures to read or write are
    logged as warnings but never raised to the caller.
    """

    def __init__(self, ttl_hours: int = 24, *, cache_dir: Path | None = None) -> None:
        """Initialise the disk cache.

        Args:
            ttl_hours: Number of hours before a cached entry is considered stale.
            cache_dir: Override the default cache directory (useful for testing).
        """
        self._ttl_seconds = ttl_hours * 3600
        self._cache_dir = cache_dir or Path(platformdirs.user_cache_dir("depwhy"))
        try:
            self._cache_dir.mkdir(parents=True, exist_ok=True)
        except OSError:
            logger.warning("Cannot create cache directory: %s", self._cache_dir)

    def _key(self, package_name: str, version: str) -> str:
        """Return the SHA-256 hex digest used as the cache filename."""
        return hashlib.sha256(f"{package_name}:{version}".encode()).hexdigest()

    def _path(self, package_name: str, version: str) -> Path:
        """Return the full path for a cache entry."""
        return self._cache_dir / f"{self._key(package_name, version)}.json"

    def get(self, package_name: str, version: str) -> dict[str, Any] | None:
        """Return cached metadata or ``None`` if missing, stale, or corrupted.

        Args:
            package_name: The PyPI package name.
            version: The package version string.

        Returns:
            The cached dict, or ``None`` on cache miss.
        """
        path = self._path(package_name, version)
        try:
            if not path.exists():
                return None
            mtime = path.stat().st_mtime
            if time.time() - mtime > self._ttl_seconds:
                return None
            data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError, ValueError) as exc:
            logger.warning("Cache read failed for %s:%s — %s", package_name, version, exc)
            return None
        return data

    def set(self, package_name: str, version: str, data: dict[str, Any]) -> None:
        """Write metadata to the cache.

        Args:
            package_name: The PyPI package name.
            version: The package version string.
            data: The metadata dict to cache.
        """
        path = self._path(package_name, version)
        try:
            path.write_text(json.dumps(data), encoding="utf-8")
        except OSError as exc:
            logger.warning("Cache write failed for %s:%s — %s", package_name, version, exc)

    def clear(self) -> None:
        """Remove all cached files from the cache directory."""
        try:
            for entry in self._cache_dir.iterdir():
                if entry.is_file() and entry.suffix == ".json":
                    entry.unlink(missing_ok=True)
        except OSError as exc:
            logger.warning("Cache clear failed — %s", exc)
