"""
cortex/hippo/cache.py — LRU cache with TTL for frequently accessed summaries.

Reduces DB round-trips for hot summaries during expand/describe operations.
Pure Python, no external dependencies.

Issue: #1190
"""

from __future__ import annotations

import time
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any, Optional


@dataclass
class CacheEntry:
    """A single cache entry with its timestamp."""

    record: Any  # SummaryRecord
    cached_at: float


class SummaryCache:
    """LRU cache with TTL for frequently accessed summaries.

    Thread-safe within a single asyncio event loop (no concurrent mutation).
    """

    def __init__(self, max_size: int = 256, ttl_seconds: float = 300.0) -> None:
        self._max_size = max_size
        self._ttl = ttl_seconds
        self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
        self._hits = 0
        self._misses = 0

    def get(self, summary_id: str) -> Optional[Any]:
        """Get a cached summary record, or None if miss/expired."""
        entry = self._cache.get(summary_id)
        if entry is None:
            self._misses += 1
            return None
        if time.monotonic() - entry.cached_at > self._ttl:
            del self._cache[summary_id]
            self._misses += 1
            return None
        self._cache.move_to_end(summary_id)
        self._hits += 1
        return entry.record

    def get_batch(self, summary_ids: list[str]) -> tuple[list[Any], list[str]]:
        """Get multiple cached records. Returns (found_records, missing_ids)."""
        found = []
        missing = []
        for sid in summary_ids:
            record = self.get(sid)
            if record is not None:
                found.append(record)
            else:
                missing.append(sid)
        return found, missing

    def put(self, summary_id: str, record: Any) -> None:
        """Insert or update a cache entry."""
        if summary_id in self._cache:
            self._cache.move_to_end(summary_id)
            self._cache[summary_id] = CacheEntry(record, time.monotonic())
        else:
            if len(self._cache) >= self._max_size:
                self._cache.popitem(last=False)
            self._cache[summary_id] = CacheEntry(record, time.monotonic())

    def put_batch(self, records: list[Any]) -> None:
        """Insert multiple records (each must have .summary_id)."""
        for record in records:
            self.put(record.summary_id, record)

    def invalidate(self, summary_id: str) -> None:
        """Remove a specific entry from cache."""
        self._cache.pop(summary_id, None)

    def clear(self) -> None:
        """Clear all entries and reset stats."""
        self._cache.clear()
        self._hits = 0
        self._misses = 0

    @property
    def size(self) -> int:
        """Current number of entries."""
        return len(self._cache)

    @property
    def hit_rate(self) -> float:
        """Cache hit rate (0.0 to 1.0)."""
        total = self._hits + self._misses
        return self._hits / total if total > 0 else 0.0

    @property
    def stats(self) -> dict:
        """Cache statistics."""
        return {
            "hits": self._hits,
            "misses": self._misses,
            "hit_rate": self.hit_rate,
            "size": len(self._cache),
            "max_size": self._max_size,
            "ttl_seconds": self._ttl,
        }
