from __future__ import annotations

import math
from collections import OrderedDict

from nexus.core.state.embedding_fallback import deterministic_embedding
from nexus.core.state.hot_layer import HotLayer


class Deduplicator:
    """Suppresses semantically duplicate inter-agent messages."""

    def __init__(self, embedding_model: str = "all-MiniLM-L6-v2", similarity_threshold: float = 0.85) -> None:
        self.embedding_model = embedding_model
        self.similarity_threshold = similarity_threshold
        self._model: object | None = None
        self._use_fallback = False
        self._cache: OrderedDict[str, list[float]] = OrderedDict()
        self._cache_limit = 128

    def _get_model(self) -> object | None:
        if self._model is None and not self._use_fallback:
            try:
                from sentence_transformers import SentenceTransformer  # lazy import

                self._model = SentenceTransformer(self.embedding_model)
            except Exception:
                self._use_fallback = True
        return self._model

    async def should_suppress(self, message: str, receiver_hot: HotLayer) -> tuple[bool, float]:
        """Return suppression decision and max similarity against receiver hot text values."""
        msg_vec = self._embed(message)
        max_sim = 0.0
        for entry in receiver_hot.to_dict().values():
            value = entry.get("value")
            if isinstance(value, str):
                sim = self._cosine(msg_vec, self._embed(value))
                if sim > max_sim:
                    max_sim = sim
        return (max_sim >= self.similarity_threshold, max_sim)

    def _embed(self, text: str) -> list[float]:
        """Embed text with small LRU cache."""
        if text in self._cache:
            self._cache.move_to_end(text)
            return self._cache[text]
        model = self._get_model()
        if model is None or self._use_fallback:
            vec = deterministic_embedding(text)
        else:
            vec = [float(x) for x in model.encode(text).tolist()]  # type: ignore[attr-defined]
        self._cache[text] = vec
        if len(self._cache) > self._cache_limit:
            self._cache.popitem(last=False)
        return vec

    def _cosine(self, a: list[float], b: list[float]) -> float:
        dot = sum(x * y for x, y in zip(a, b))
        na = math.sqrt(sum(x * x for x in a))
        nb = math.sqrt(sum(y * y for y in b))
        if na == 0.0 or nb == 0.0:
            return 0.0
        return dot / (na * nb)
