"""Kernel-level caching framework — TTL-based LRU with registry.

Provides O(1) get/put with monotonic-clock TTL expiry and LRU eviction.
All implementations are stdlib-only (ADR-007 Tier-0 compliance).
"""

from __future__ import annotations

import dataclasses
import functools
import time
from collections import OrderedDict
from collections.abc import Callable
from typing import Any

_SENTINEL = object()  # Distinguishes cache miss from cached ``None``.


@dataclasses.dataclass(slots=True)
class CacheEntry:
    """Single cached value with expiration timestamp."""

    value: Any
    expire_at: float


@dataclasses.dataclass(frozen=True, slots=True)
class CacheStats:
    """Aggregated statistics across registered caches."""

    total_caches: int
    total_entries: int
    total_hits: int
    total_misses: int
    total_evictions: int

    @property
    def hit_rate(self) -> float:
        """Return hit ratio in [0.0, 1.0], or 0.0 if no lookups."""
        total = self.total_hits + self.total_misses
        return self.total_hits / total if total else 0.0


class TTLCache:
    """LRU cache with per-entry time-to-live based on ``time.monotonic``.

    Parameters
    ----------
    maxsize:
        Maximum number of entries before LRU eviction.
    ttl_seconds:
        Default lifetime for each entry in seconds.
    """

    def __init__(self, maxsize: int = 256, ttl_seconds: float = 300.0) -> None:
        self._store: OrderedDict[str, CacheEntry] = OrderedDict()
        self._maxsize = max(1, maxsize)
        self._ttl = ttl_seconds
        self._hits = 0
        self._misses = 0
        self._evictions = 0

    # -- public API ----------------------------------------------------------

    def get(self, key: str) -> Any:
        """Return cached value or ``_SENTINEL`` on miss/expiry."""
        if key in self._store:
            entry = self._store[key]
            if entry.expire_at > time.monotonic():
                self._store.move_to_end(key)
                self._hits += 1
                return entry.value
            # Expired — evict.
            self._store.pop(key)
            self._evictions += 1
        self._misses += 1
        return _SENTINEL

    def put(self, key: str, value: Any) -> None:
        """Insert or update *key* with fresh TTL."""
        expire_at = time.monotonic() + self._ttl
        if key in self._store:
            self._store.move_to_end(key)
        self._store[key] = CacheEntry(value=value, expire_at=expire_at)
        while len(self._store) > self._maxsize:
            self._store.popitem(last=False)
            self._evictions += 1

    def invalidate(self, key: str) -> bool:
        """Remove *key* if present; return whether it was found."""
        if key in self._store:
            self._store.pop(key)
            return True
        return False

    def clear(self) -> None:
        """Drop all entries and reset counters."""
        self._store.clear()
        self._hits = 0
        self._misses = 0
        self._evictions = 0

    # -- introspection -------------------------------------------------------

    def __len__(self) -> int:
        return len(self._store)

    @property
    def hits(self) -> int:
        return self._hits

    @property
    def misses(self) -> int:
        return self._misses

    @property
    def evictions(self) -> int:
        return self._evictions


class ResultCache:
    """Function memoization backed by a :class:`TTLCache`.

    Usage::

        rc = ResultCache(maxsize=128, ttl_seconds=60)

        @rc.cached
        def expensive(x, y):
            ...
    """

    def __init__(self, maxsize: int = 256, ttl_seconds: float = 300.0) -> None:
        self._cache = TTLCache(maxsize=maxsize, ttl_seconds=ttl_seconds)

    @property
    def cache(self) -> TTLCache:
        """Expose underlying cache for stats / invalidation."""
        return self._cache

    def cached(self, func: Callable[..., Any]) -> Callable[..., Any]:
        """Decorator that caches function results by args hash."""

        @functools.wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            kw_key = tuple(sorted(kwargs.items())) if kwargs else ()
            cache_key = f"{func.__qualname__}:{hash((args, kw_key))}"
            result = self._cache.get(cache_key)
            if result is not _SENTINEL:
                return result
            result = func(*args, **kwargs)
            self._cache.put(cache_key, result)
            return result

        wrapper.cache = self._cache  # type: ignore[attr-defined]
        return wrapper


class CacheRegistry:
    """Central registry that tracks all named caches for monitoring."""

    def __init__(self) -> None:
        self._caches: dict[str, TTLCache] = {}

    def register(self, name: str, cache: TTLCache) -> None:
        """Register *cache* under *name*."""
        self._caches[name] = cache

    def unregister(self, name: str) -> bool:
        """Remove *name* from registry; return whether it existed."""
        return self._caches.pop(name, None) is not None

    def get(self, name: str) -> TTLCache | None:
        """Retrieve a registered cache by name."""
        return self._caches.get(name)

    def names(self) -> list[str]:
        """Return sorted list of registered cache names."""
        return sorted(self._caches)

    def clear_all(self) -> None:
        """Clear every registered cache."""
        for cache in self._caches.values():
            cache.clear()

    def summary(self) -> CacheStats:
        """Aggregate stats across all registered caches."""
        total_entries = 0
        total_hits = 0
        total_misses = 0
        total_evictions = 0
        for cache in self._caches.values():
            total_entries += len(cache)
            total_hits += cache.hits
            total_misses += cache.misses
            total_evictions += cache.evictions
        return CacheStats(
            total_caches=len(self._caches),
            total_entries=total_entries,
            total_hits=total_hits,
            total_misses=total_misses,
            total_evictions=total_evictions,
        )


_default_registry = CacheRegistry()


def get_registry() -> CacheRegistry:
    """Return the module-level default :class:`CacheRegistry`."""
    return _default_registry
