"""L1 + L2 查询缓存

L1: (query, collection, top_k, enriched_hash) → SearchResponse  — 跳过整条管线
L2: query_text → embedding_vector                                — 跳过 Ollama 调用
"""

from __future__ import annotations

import hashlib
import logging
import time
from collections import OrderedDict
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    pass

logger = logging.getLogger(__name__)


class QueryCache:
    """双层查询缓存（纯内存，线程安全依赖 GIL）"""

    def __init__(
        self,
        l1_max: int = 500,
        l2_max: int = 1000,
        ttl: float = 600.0,  # 10 分钟
    ):
        self._l1_max = l1_max
        self._l2_max = l2_max
        self._ttl = ttl

        # L1: key → (response, expire_time, collection)
        self._l1: OrderedDict[str, tuple[Any, float, str | None]] = OrderedDict()
        # L2: text_hash → (embedding, expire_time)
        self._l2: OrderedDict[str, tuple[list[float], float]] = OrderedDict()

        # 统计
        self._l1_hits = 0
        self._l1_misses = 0
        self._l2_hits = 0
        self._l2_misses = 0

    # ── L1: 查询结果缓存 ──

    @staticmethod
    def _l1_key(
        query: str, collection: str | None, top_k: int, enriched_hash: str = "",
    ) -> str:
        raw = f"{query}|{collection or ''}|{top_k}|{enriched_hash}"
        return hashlib.sha256(raw.encode()).hexdigest()

    def get_response(
        self, query: str, collection: str | None, top_k: int,
        enriched_hash: str = "",
    ) -> Any | None:
        key = self._l1_key(query, collection, top_k, enriched_hash)
        entry = self._l1.get(key)
        if entry is None:
            self._l1_misses += 1
            return None
        response, expire_at, _col = entry
        if time.monotonic() > expire_at:
            self._l1.pop(key, None)
            self._l1_misses += 1
            return None
        self._l1.move_to_end(key)
        self._l1_hits += 1
        return response

    def put_response(
        self, query: str, collection: str | None, top_k: int, response: Any,
        enriched_hash: str = "",
    ) -> None:
        key = self._l1_key(query, collection, top_k, enriched_hash)
        self._l1[key] = (response, time.monotonic() + self._ttl, collection)
        self._l1.move_to_end(key)
        while len(self._l1) > self._l1_max:
            self._l1.popitem(last=False)

    # ── L2: Embedding 缓存 ──

    @staticmethod
    def _l2_key(text: str) -> str:
        return hashlib.sha256(text.encode()).hexdigest()

    def get_embedding(self, text: str) -> list[float] | None:
        key = self._l2_key(text)
        entry = self._l2.get(key)
        if entry is None:
            self._l2_misses += 1
            return None
        embedding, expire_at = entry
        if time.monotonic() > expire_at:
            self._l2.pop(key, None)
            self._l2_misses += 1
            return None
        self._l2.move_to_end(key)
        self._l2_hits += 1
        return embedding

    def put_embedding(self, text: str, embedding: list[float]) -> None:
        key = self._l2_key(text)
        self._l2[key] = (embedding, time.monotonic() + self._ttl)
        self._l2.move_to_end(key)
        while len(self._l2) > self._l2_max:
            self._l2.popitem(last=False)

    # ── 失效 ──

    def invalidate_collection(self, collection: str) -> int:
        """精确清除指定 collection 的 L1 缓存条目"""
        keys_to_remove = [
            k for k, (_, _, col) in self._l1.items()
            if col == collection or col is None
        ]
        for k in keys_to_remove:
            self._l1.pop(k, None)
        return len(keys_to_remove)

    def clear(self) -> None:
        """清除所有缓存"""
        self._l1.clear()
        self._l2.clear()

    # ── 统计 ──

    def stats(self) -> dict:
        l1_total = self._l1_hits + self._l1_misses
        l2_total = self._l2_hits + self._l2_misses
        return {
            "l1_size": len(self._l1),
            "l1_max": self._l1_max,
            "l1_hits": self._l1_hits,
            "l1_misses": self._l1_misses,
            "l1_hit_rate": round(self._l1_hits / l1_total, 3) if l1_total else 0.0,
            "l2_size": len(self._l2),
            "l2_max": self._l2_max,
            "l2_hits": self._l2_hits,
            "l2_misses": self._l2_misses,
            "l2_hit_rate": round(self._l2_hits / l2_total, 3) if l2_total else 0.0,
        }
