"""File-based cache para data fetchers. JSON on disk con TTL.

Estructura:
    .cache/data_fetchers/<namespace>_<hash16>.json

Cada entry:
    {"cached_at": <epoch_seconds>, "key": "<original_key>", "data": {...}}

TTL se evalua en get() — si la entrada esta vencida, devuelve None
(no la borra; la sobreescribe el siguiente set()).
"""

from __future__ import annotations

import hashlib
import json
import time
from pathlib import Path
from typing import Optional


class FileCache:
    def __init__(self, cache_dir: str | Path):
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)

    def _path(self, namespace: str, key: str) -> Path:
        safe_key = hashlib.sha1(key.encode("utf-8")).hexdigest()[:16]
        safe_ns = "".join(c if c.isalnum() else "_" for c in namespace)[:24]
        return self.cache_dir / f"{safe_ns}_{safe_key}.json"

    def get(self, namespace: str, key: str, ttl_days: float) -> Optional[dict]:
        """Devuelve el dict cacheado si existe y no esta vencido. Sino None."""
        p = self._path(namespace, key)
        if not p.exists():
            return None
        try:
            with p.open(encoding="utf-8") as f:
                entry = json.load(f)
            cached_at = entry.get("cached_at", 0)
            age_days = (time.time() - cached_at) / 86400.0
            if age_days > ttl_days:
                return None
            return entry.get("data")
        except (json.JSONDecodeError, OSError):
            return None

    def set(self, namespace: str, key: str, data: dict) -> None:
        """Guarda data al cache. Errores de escritura son silenciados (non-fatal)."""
        p = self._path(namespace, key)
        entry = {
            "cached_at": time.time(),
            "namespace": namespace,
            "key": key,
            "data": data,
        }
        try:
            p.write_text(
                json.dumps(entry, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except OSError:
            pass  # cache failures are non-fatal

    def clear(self, namespace: Optional[str] = None) -> int:
        """Borra entradas de cache. Si namespace, solo de esa namespace.

        Devuelve cantidad de archivos borrados.
        """
        count = 0
        pattern = f"{namespace}_*.json" if namespace else "*.json"
        for p in self.cache_dir.glob(pattern):
            try:
                p.unlink()
                count += 1
            except OSError:
                pass
        return count
