"""
Phase 93: Platform-Wide Caching Layer.

In-memory caching with TTL, tags for group invalidation, stats tracking,
a function decorator, and optional persistent backing store.

Storage:
  In-memory dict (primary)
  state/broker/cache/persistent.json  -- optional persistent cache
"""

import functools
import json
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Optional

from brynq.server import BROKER_DIR, _write_json_sync

# ── Paths ──────────────────────────────────────────────────────────────────

CACHE_DIR = BROKER_DIR / "cache"
CACHE_DIR.mkdir(parents=True, exist_ok=True)
PERSISTENT_FILE = CACHE_DIR / "persistent.json"

# ── Internal State ─────────────────────────────────────────────────────────

_lock = threading.Lock()
_cache: dict[str, dict] = {}  # key -> {value, expires_at, tags}

_stats = {
    "hits": 0,
    "misses": 0,
    "evictions": 0,
    "sets": 0,
}

# ── Persistence Helpers ────────────────────────────────────────────────────


def _persist() -> None:
    """Write cache to disk for crash recovery (best-effort)."""
    try:
        snapshot = {}
        for k, entry in _cache.items():
            snapshot[k] = {
                "value": entry["value"],
                "expires_at": entry["expires_at"],
                "tags": entry.get("tags", []),
            }
        _write_json_sync(PERSISTENT_FILE, snapshot)
    except Exception:
        pass


def _load_persistent() -> None:
    """Load persistent cache from disk (called once at import)."""
    try:
        if PERSISTENT_FILE.exists():
            data = json.loads(PERSISTENT_FILE.read_text("utf-8"))
            now = time.time()
            for k, entry in data.items():
                if entry.get("expires_at", 0) > now:
                    _cache[k] = entry
    except (json.JSONDecodeError, OSError):
        pass


# ── Public API ─────────────────────────────────────────────────────────────


def cache_set(key: str, value: Any, ttl_seconds: int = 300,
              tags: Optional[list[str]] = None) -> dict:
    """Store *value* under *key* with TTL in seconds."""
    with _lock:
        _cache[key] = {
            "value": value,
            "expires_at": time.time() + ttl_seconds,
            "tags": tags or [],
        }
        _stats["sets"] += 1
        _persist()
    return {"ok": True, "key": key, "ttl_seconds": ttl_seconds}


def cache_get(key: str) -> Any:
    """Retrieve value for *key*. Returns None if missing or expired."""
    with _lock:
        entry = _cache.get(key)
        if entry is None:
            _stats["misses"] += 1
            return None
        if time.time() > entry["expires_at"]:
            del _cache[key]
            _stats["misses"] += 1
            _stats["evictions"] += 1
            return None
        _stats["hits"] += 1
        return entry["value"]


def cache_delete(key: str) -> dict:
    """Remove a single cache entry."""
    with _lock:
        removed = key in _cache
        _cache.pop(key, None)
        if removed:
            _stats["evictions"] += 1
            _persist()
    return {"ok": True, "key": key, "removed": removed}


def cache_clear() -> dict:
    """Remove all cache entries."""
    with _lock:
        count = len(_cache)
        _stats["evictions"] += count
        _cache.clear()
        _persist()
    return {"ok": True, "cleared": count}


def invalidate_by_tag(tag: str) -> dict:
    """Clear all entries tagged with *tag*."""
    with _lock:
        to_remove = [k for k, v in _cache.items() if tag in v.get("tags", [])]
        for k in to_remove:
            del _cache[k]
            _stats["evictions"] += 1
        _persist()
    return {"ok": True, "tag": tag, "invalidated": len(to_remove)}


def get_cache_stats() -> dict:
    """Return cache statistics: hits, misses, hit_rate, size, evictions."""
    with _lock:
        total = _stats["hits"] + _stats["misses"]
        hit_rate = round(_stats["hits"] / total, 4) if total else 0.0
        return {
            "hits": _stats["hits"],
            "misses": _stats["misses"],
            "hit_rate": hit_rate,
            "size": len(_cache),
            "evictions": _stats["evictions"],
            "sets": _stats["sets"],
        }


def cache_decorator(ttl_seconds: int = 300) -> Callable:
    """Function decorator that caches return values based on arguments."""
    def decorator(fn: Callable) -> Callable:
        @functools.wraps(fn)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            # Build a deterministic cache key from function name + args
            key_parts = [fn.__module__, fn.__qualname__, repr(args), repr(sorted(kwargs.items()))]
            cache_key = ":".join(key_parts)
            result = cache_get(cache_key)
            if result is not None:
                return result
            result = fn(*args, **kwargs)
            cache_set(cache_key, result, ttl_seconds=ttl_seconds)
            return result
        return wrapper
    return decorator


# ── Reset (for testing) ───────────────────────────────────────────────────


def _reset() -> None:
    """Clear all runtime state. For tests only."""
    global _cache, _stats
    _cache = {}
    _stats = {"hits": 0, "misses": 0, "evictions": 0, "sets": 0}


# ── Load persistent on import ─────────────────────────────────────────────

_load_persistent()
