"""Two-tier query cache with adaptive similarity thresholds.

Provides exact hash match (in-memory TTLCache) and adaptive EMA-based
threshold tuning for semantic similarity lookups. Inspired by vCache
(arXiv:2502.03771) but simplified to global EMA.

The daemon orchestrates the two tiers:
  1. Check QueryCache.get_exact() -- sub-microsecond hash lookup
  2. If miss, do storage.search() with QueryCache.similarity_threshold
  3. If semantic hit, call record_semantic_hit()
  4. If miss, call record_miss()
"""

from __future__ import annotations

import hashlib
from typing import Any

from cachetools import TTLCache

# Default similarity threshold, calibrated for MiniLM-L6-v2 cosine output:
#   related content ≈ 0.4-0.6, paraphrases ≈ 0.7-0.9.
# Single value for all profiles — context window is identical across all
# Anthropic models, so per-tier thresholds add complexity without benefit.
DEFAULT_SIMILARITY_THRESHOLD: float = 0.45


class AdaptiveThreshold:
    """EMA-based similarity threshold with hit-rate feedback.

    Tracks cache hit/miss rates using exponential moving average (EMA)
    and adjusts the similarity threshold to maintain a healthy hit rate.

    - Below HEALTHY_HIT_MIN: threshold too strict, lower it (accept more)
    - Above HEALTHY_HIT_MAX: threshold too permissive, raise it (be selective)
    - During warmup (first WARMUP_QUERIES): no adjustment
    - Hard limits prevent extreme values: [HARD_MIN, HARD_MAX]
    """

    HARD_MIN: float = 0.30
    HARD_MAX: float = 0.70
    HEALTHY_HIT_MIN: float = 0.15
    HEALTHY_HIT_MAX: float = 0.60
    WARMUP_QUERIES: int = 20
    WINDOW_SIZE: int = 100
    ADJUSTMENT_STEP: float = 0.01

    def __init__(self, initial: float = 0.45) -> None:
        self._initial = initial
        self._threshold = initial
        self._total = 0
        self._hits = 0
        self._ema_hit_rate = 0.0
        self._alpha = 2.0 / (self.WINDOW_SIZE + 1)

    @property
    def threshold(self) -> float:
        """Current adaptive similarity threshold."""
        return self._threshold

    @property
    def hit_rate(self) -> float:
        """Current EMA-smoothed hit rate."""
        return self._ema_hit_rate

    @property
    def total_queries(self) -> int:
        """Total queries observed (hits + misses)."""
        return self._total

    def record(self, hit: bool) -> None:
        """Record a cache hit or miss and adjust threshold if needed.

        Args:
            hit: True if the semantic lookup found a sufficiently similar result.
        """
        self._total += 1
        if hit:
            self._hits += 1

        # Update EMA
        self._ema_hit_rate = (
            self._alpha * (1.0 if hit else 0.0) + (1.0 - self._alpha) * self._ema_hit_rate
        )

        # No adjustment during warmup period
        if self._total < self.WARMUP_QUERIES:
            return

        # Adjust threshold if hit rate outside healthy range
        if self._ema_hit_rate < self.HEALTHY_HIT_MIN:
            # Too few hits -> lower threshold (accept more)
            self._threshold = max(self.HARD_MIN, self._threshold - self.ADJUSTMENT_STEP)
        elif self._ema_hit_rate > self.HEALTHY_HIT_MAX:
            # Too many hits -> raise threshold (be more selective)
            self._threshold = min(self.HARD_MAX, self._threshold + self.ADJUSTMENT_STEP)

    def reset(self, initial: float | None = None) -> None:
        """Reset all state, optionally with a new initial threshold.

        Args:
            initial: New initial threshold. If None, uses the original value.
        """
        if initial is not None:
            self._initial = initial
        self._threshold = self._initial
        self._total = 0
        self._hits = 0
        self._ema_hit_rate = 0.0


class QueryCache:
    """Two-tier query cache: exact hash match + semantic similarity support.

    Tier 1 (exact): In-memory TTLCache keyed by SHA-256 hash of the query string.
    Sub-microsecond lookups with automatic TTL eviction and maxsize cap.

    Tier 2 (semantic): Managed externally by the daemon via storage.search().
    QueryCache provides the adaptive similarity threshold and stats tracking.

    The daemon orchestrates both tiers:
        result = cache.get_exact(query)
        if result is None:
            result = storage.search(query, threshold=cache.similarity_threshold)
            if result:
                cache.record_semantic_hit()
                cache.put(query, result)  # promote to exact cache
            else:
                cache.record_miss()
    """

    def __init__(
        self,
        maxsize: int = 256,
        ttl: float = 300.0,
        similarity_threshold: float = 0.45,
    ) -> None:
        """Initialize the two-tier query cache.

        Args:
            maxsize: Maximum number of entries in the exact cache.
            ttl: Time-to-live in seconds for exact cache entries (default 5 min).
            similarity_threshold: Initial cosine similarity threshold for semantic tier.
        """
        self._exact_cache: TTLCache[str, dict[str, Any]] = TTLCache(maxsize=maxsize, ttl=ttl)
        self._threshold = AdaptiveThreshold(initial=similarity_threshold)
        self._stats: dict[str, int] = {
            "hits_exact": 0,
            "hits_semantic": 0,
            "misses": 0,
        }

    def _hash_key(self, query: str) -> str:
        """Compute SHA-256 hex digest of the query string."""
        return hashlib.sha256(query.encode("utf-8")).hexdigest()

    def get_exact(self, query: str) -> dict[str, Any] | None:
        """Check exact cache for a matching query.

        Args:
            query: The search query string.

        Returns:
            Cached result dict if found, None otherwise.
        """
        key = self._hash_key(query)
        result: dict[str, Any] | None = self._exact_cache.get(key)
        if result is not None:
            self._stats["hits_exact"] += 1
        return result

    def put(self, query: str, result: dict[str, Any]) -> None:
        """Store a result in the exact cache.

        Args:
            query: The search query string (used as cache key via SHA-256).
            result: The result dict to cache.
        """
        key = self._hash_key(query)
        self._exact_cache[key] = result

    def record_semantic_hit(self) -> None:
        """Record a semantic tier cache hit. Updates stats and adaptive threshold."""
        self._stats["hits_semantic"] += 1
        self._threshold.record(hit=True)

    def record_miss(self) -> None:
        """Record a complete cache miss (both tiers). Updates stats and threshold."""
        self._stats["misses"] += 1
        self._threshold.record(hit=False)

    @property
    def similarity_threshold(self) -> float:
        """Current adaptive similarity threshold for semantic lookups."""
        return self._threshold.threshold

    @property
    def stats(self) -> dict[str, Any]:
        """Cache statistics including hit counts and threshold info.

        Returns:
            Dict with hits_exact, hits_semantic, misses, similarity_threshold,
            ema_hit_rate, and total_semantic_queries.
        """
        return {
            **self._stats,
            "similarity_threshold": self._threshold.threshold,
            "ema_hit_rate": self._threshold.hit_rate,
            "total_semantic_queries": self._threshold.total_queries,
        }

    def clear(self) -> None:
        """Clear exact cache and reset all stats."""
        self._exact_cache.clear()
        self._stats = {
            "hits_exact": 0,
            "hits_semantic": 0,
            "misses": 0,
        }
        self._threshold.reset()
