"""LLM response cache — avoid redundant API calls for identical prompts."""

from __future__ import annotations

import hashlib
import json
import logging
import time
from dataclasses import dataclass, field
from typing import Any

logger = logging.getLogger(__name__)


@dataclass
class CacheEntry:
    """A cached LLM response."""

    key: str
    response: dict[str, Any]
    model: str
    created_at: float = field(default_factory=time.time)
    hits: int = 0


class LLMCache:
    """In-memory LLM response cache with TTL and max size.

    Caches responses based on a hash of (model, messages, tools, temperature).
    Useful for avoiding redundant LLM calls when the same question is asked
    within a short time window.
    """

    def __init__(
        self,
        max_size: int = 256,
        ttl_seconds: float = 300.0,
        enabled: bool = True,
    ) -> None:
        self._cache: dict[str, CacheEntry] = {}
        self._max_size = max_size
        self._ttl = ttl_seconds
        self._enabled = enabled
        self._total_hits = 0
        self._total_misses = 0

    @property
    def enabled(self) -> bool:
        return self._enabled

    @enabled.setter
    def enabled(self, value: bool) -> None:
        self._enabled = value

    @property
    def size(self) -> int:
        return len(self._cache)

    @property
    def hit_rate(self) -> float:
        total = self._total_hits + self._total_misses
        return self._total_hits / total if total > 0 else 0.0

    def make_key(
        self,
        model: str,
        messages: list[dict[str, Any]],
        tools: list[dict[str, Any]] | None = None,
        temperature: float | None = None,
    ) -> str:
        """Create a deterministic cache key from request parameters."""
        payload = json.dumps(
            {
                "model": model,
                "messages": messages,
                "tools": tools or [],
                "temperature": temperature,
            },
            sort_keys=True,
        )
        return hashlib.sha256(payload.encode()).hexdigest()[:32]

    def get(self, key: str) -> dict[str, Any] | None:
        """Look up a cached response. Returns None on miss or expired entry."""
        if not self._enabled:
            self._total_misses += 1
            return None

        entry = self._cache.get(key)
        if entry is None:
            self._total_misses += 1
            return None

        # Check TTL
        if time.time() - entry.created_at > self._ttl:
            del self._cache[key]
            self._total_misses += 1
            return None

        entry.hits += 1
        self._total_hits += 1
        return entry.response

    def put(
        self,
        key: str,
        response: dict[str, Any],
        model: str,
    ) -> None:
        """Store a response in the cache."""
        if not self._enabled:
            return

        # Evict oldest entries if at capacity
        if len(self._cache) >= self._max_size:
            self._evict()

        self._cache[key] = CacheEntry(
            key=key,
            response=response,
            model=model,
        )

    def invalidate(self, key: str) -> bool:
        """Remove a specific entry. Returns True if it existed."""
        return self._cache.pop(key, None) is not None

    def clear(self) -> None:
        """Clear the entire cache."""
        self._cache.clear()

    def get_stats(self) -> dict[str, Any]:
        """Return cache statistics."""
        return {
            "size": self.size,
            "max_size": self._max_size,
            "ttl_seconds": self._ttl,
            "total_hits": self._total_hits,
            "total_misses": self._total_misses,
            "hit_rate": round(self.hit_rate, 3),
            "enabled": self._enabled,
        }

    def _evict(self) -> None:
        """Evict the oldest entry."""
        if not self._cache:
            return
        oldest_key = min(self._cache, key=lambda k: self._cache[k].created_at)
        del self._cache[oldest_key]
