"""Cache key-value donde cada entry es un archivo JSON en disco."""

import hashlib
import json
import os
import threading
import time


class FileCache:
    """Cache key-value respaldado en archivos JSON, con metadata sidecar .meta."""

    def __init__(self, cache_dir: str, namespace: str = "default") -> None:
        self._base = os.path.join(cache_dir, namespace)
        self._hits = 0
        self._misses = 0
        self._lock = threading.Lock()
        os.makedirs(self._base, exist_ok=True)

    def _hash_key(self, key: str) -> str:
        return hashlib.sha256(key.encode("utf-8")).hexdigest()

    def _value_path(self, hashed: str) -> str:
        return os.path.join(self._base, f"{hashed}.json")

    def _meta_path(self, hashed: str) -> str:
        return os.path.join(self._base, f"{hashed}.meta")

    def _is_expired(self, meta: dict) -> bool:
        expires_at = meta.get("expires_at")
        if expires_at is None:
            return False
        return time.time() >= expires_at

    def _load_meta(self, hashed: str) -> dict | None:
        path = self._meta_path(hashed)
        if not os.path.exists(path):
            return None
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)

    def get(self, key: str) -> object:
        """Retorna el valor o None si no existe o esta expirado."""
        hashed = self._hash_key(key)
        with self._lock:
            meta = self._load_meta(hashed)
            if meta is None:
                self._misses += 1
                return None
            if self._is_expired(meta):
                self._delete_files(hashed)
                self._misses += 1
                return None
            value_path = self._value_path(hashed)
            if not os.path.exists(value_path):
                self._misses += 1
                return None
            with open(value_path, "r", encoding="utf-8") as f:
                self._hits += 1
                return json.load(f)

    def set(self, key: str, value: object, ttl: float = 0) -> None:
        """Almacena un valor. ttl en segundos; 0 = sin expiracion."""
        hashed = self._hash_key(key)
        now = time.time()
        expires_at = (now + ttl) if ttl > 0 else None
        meta = {"created_at": now, "expires_at": expires_at, "original_key": key}
        with self._lock:
            with open(self._value_path(hashed), "w", encoding="utf-8") as f:
                json.dump(value, f)
            with open(self._meta_path(hashed), "w", encoding="utf-8") as f:
                json.dump(meta, f)

    def _delete_files(self, hashed: str) -> bool:
        vp = self._value_path(hashed)
        mp = self._meta_path(hashed)
        deleted = False
        if os.path.exists(vp):
            os.remove(vp)
            deleted = True
        if os.path.exists(mp):
            os.remove(mp)
            deleted = True
        return deleted

    def delete(self, key: str) -> bool:
        """Elimina una entrada. Retorna True si existia."""
        hashed = self._hash_key(key)
        with self._lock:
            return self._delete_files(hashed)

    def clear(self) -> int:
        """Elimina todas las entradas del namespace. Retorna pares eliminados."""
        with self._lock:
            count = 0
            if not os.path.isdir(self._base):
                return 0
            for fname in os.listdir(self._base):
                if fname.endswith(".json"):
                    count += 1
                fpath = os.path.join(self._base, fname)
                os.remove(fpath)
            return count

    def stats(self) -> dict:
        """Retorna estadisticas del store: hits, misses y size actual."""
        with self._lock:
            if not os.path.isdir(self._base):
                size = 0
            else:
                size = sum(
                    1 for f in os.listdir(self._base) if f.endswith(".json")
                )
            return {"hits": self._hits, "misses": self._misses, "size": size}

    def get_or_set(self, key: str, factory: callable, ttl: float = 0) -> object:
        """Retorna el valor cacheado o llama factory() y lo almacena."""
        value = self.get(key)
        if value is None:
            value = factory()
            self.set(key, value, ttl)
        return value


def cache_to_file(cache_dir: str, namespace: str = "default") -> FileCache:
    """Crea un FileCache respaldado en archivos JSON en disco.

    Args:
        cache_dir: Directorio raiz donde se almacenan los archivos de cache.
        namespace: Subdirectorio logico dentro de cache_dir.

    Returns:
        FileCache con metodos get/set/delete/clear/stats/get_or_set.
    """
    return FileCache(cache_dir, namespace)
