"""Disk-based cache using diskcache with SQLite backend and LRU eviction.

Cache structure:
    ~/.bandito/cache/
        {project}.cache (SQLite file per project)

Uses diskcache's LRU eviction based on size limits.
"""

from __future__ import annotations

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

import diskcache

logger = logging.getLogger(__name__)

DEFAULT_CACHE_DIR = Path.home() / ".bandito" / "cache"
MAX_CACHE_SIZE_BYTES = 5 * 1024 * 1024 * 1024  # 5GB


class DiskCache:
    """Thread-safe disk cache using diskcache with SQLite backend and LRU eviction."""

    def __init__(
        self,
        cache_dir: Path | None = None,
        max_size: int = MAX_CACHE_SIZE_BYTES,
    ):
        self._cache_dir = cache_dir or DEFAULT_CACHE_DIR
        self._max_size = max_size
        self._cache_dir.mkdir(parents=True, exist_ok=True)
        self._caches: dict[str, diskcache.Cache] = {}
        self._caches_lock = threading.RLock()
        self._default_cache: diskcache.Cache | None = None

    def _get_cache(self, project: str) -> diskcache.Cache:
        """Get or create a cache for a project."""
        with self._caches_lock:
            if project not in self._caches:
                cache_path = self._cache_dir / f"{self._sanitize_project(project)}.cache"
                self._caches[project] = diskcache.Cache(
                    str(cache_path),
                    size_limit=self._max_size,
                    disk_pickle_protocol=4,
                )
            return self._caches[project]

    def _sanitize_project(self, project: str) -> str:
        return "".join(c if c.isalnum() or c in "-_" else "_" for c in project)

    def _make_key(self, project: str, params: dict[str, Any]) -> str:
        normalized = json.dumps(params, sort_keys=True, default=str)
        hash_input = f"{project}:{normalized}"
        return hashlib.sha256(hash_input.encode()).hexdigest()

    def get(
        self,
        project: str,
        params: dict[str, Any],
    ) -> Any | None:
        """Get cached data. Returns data if found, None if not."""
        key = self._make_key(project, params)
        cache = self._get_cache(project)

        try:
            return cache.get(key)
        except Exception as e:
            logger.debug(f"Cache read error for {project}/{key}: {e}")
            return None

    def set(
        self,
        project: str,
        params: dict[str, Any],
        data: Any,
    ) -> None:
        """Store data in cache. LRU handles eviction automatically."""
        key = self._make_key(project, params)
        cache = self._get_cache(project)

        try:
            cache.set(key, data)
        except Exception as e:
            logger.debug(f"Cache write error for {project}/{key}: {e}")

    def delete(self, project: str, params: dict[str, Any] | None = None) -> None:
        """Delete cache entry(ies). If params is None, deletes entire project cache."""
        with self._caches_lock:
            if project in self._caches:
                if params is None:
                    cache_path = self._cache_dir / f"{self._sanitize_project(project)}.cache"
                    # Delete file FIRST, then dict entry
                    try:
                        cache_path.unlink(missing_ok=True)
                    except OSError:
                        pass
                    try:
                        self._caches[project].close()
                    except Exception:
                        pass
                    del self._caches[project]
                else:
                    key = self._make_key(project, params)
                    try:
                        del self._caches[project][key]
                    except KeyError:
                        pass

    def invalidate_project(self, project: str) -> None:
        """Invalidate all cache entries for a project."""
        self.delete(project, None)

    def cleanup(self) -> int:
        """Force cache eviction to respect size limits.

        Returns 0 (diskcache handles eviction automatically).
        """
        with self._caches_lock:
            for cache in self._caches.values():
                try:
                    cache.expire()
                except Exception:
                    pass
        return 0

    def stats(self, project: str | None = None) -> dict[str, Any]:
        """Get cache statistics."""
        stats = {}
        with self._caches_lock:
            if project and project in self._caches:
                caches_to_check = [self._caches[project]]
            else:
                caches_to_check = list(self._caches.values())

            for cache in caches_to_check:
                try:
                    stats[cache.directory] = {"size": cache.volume()}
                except Exception:
                    pass
        return stats


_cache: DiskCache | None = None
_cache_lock = threading.Lock()


def get_cache() -> DiskCache:
    """Get the global cache instance."""
    global _cache
    with _cache_lock:
        if _cache is None:
            _cache = DiskCache()
        return _cache


def reset_cache() -> None:
    """Reset the global cache (mainly for testing)."""
    global _cache
    with _cache_lock:
        if _cache is not None:
            for cache in _cache._caches.values():
                try:
                    cache.close()
                except Exception:
                    pass
        _cache = None
