"""Embedding-Service für den Claim-Matcher.

Lädt ein multilinguales SentenceTransformer-Modell als Singleton.
Erzeugt L2-normalisierte 384-dim Vektoren, sodass Kosinus-Ähnlichkeit
einem einfachen Skalarprodukt entspricht.
"""
from __future__ import annotations

import asyncio
import logging
import threading
from typing import Iterable

import numpy as np

logger = logging.getLogger("osint.embeddings")

MODEL_NAME = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
EMBED_DIM = 384
DTYPE = np.float32

# Threshold-Empfehlungen (empirisch aus Sanity-Tests):
#   >= 0.85 -> sehr wahrscheinlich identische Behauptung
#   >= 0.75 -> ähnliche Behauptung, dem User zur Auswahl vorschlagen
#   <  0.60 -> wahrscheinlich verschiedene Behauptungen
DEFAULT_MATCH_THRESHOLD = 0.75  # fuer Duplikat-Warnung beim Anlegen
LIVE_SEARCH_THRESHOLD = 0.55  # fuer Live-Suche im Modal, mehr Recall

_model = None
_model_lock = threading.Lock()


def _get_model():
    """Lädt das Modell einmalig (lazy) und gibt es zurück."""
    global _model
    if _model is None:
        with _model_lock:
            if _model is None:
                from sentence_transformers import SentenceTransformer
                logger.info("Lade Embedding-Modell %s ...", MODEL_NAME)
                _model = SentenceTransformer(MODEL_NAME)
                logger.info("Embedding-Modell geladen, dim=%d", EMBED_DIM)
    return _model


def _encode_sync(texts: list[str]) -> np.ndarray:
    """Synchroner Encode (CPU-bound, sollte im Executor laufen)."""
    model = _get_model()
    vecs = model.encode(
        texts,
        normalize_embeddings=True,
        convert_to_numpy=True,
        show_progress_bar=False,
    )
    return vecs.astype(DTYPE, copy=False)


async def encode_text(text: str) -> bytes:
    """Encodet einen Text und gibt das Embedding als Bytes (BLOB-tauglich) zurück."""
    if not text or not text.strip():
        raise ValueError("Leerer Text kann nicht embedded werden")
    loop = asyncio.get_running_loop()
    vec = await loop.run_in_executor(None, _encode_sync, [text])
    return vec[0].tobytes()


async def encode_batch(texts: list[str]) -> list[bytes]:
    """Encodet mehrere Texte in einem Batch (effizienter als einzeln)."""
    texts = [t for t in texts if t and t.strip()]
    if not texts:
        return []
    loop = asyncio.get_running_loop()
    vecs = await loop.run_in_executor(None, _encode_sync, texts)
    return [v.tobytes() for v in vecs]


def decode_embedding(blob: bytes | None) -> np.ndarray | None:
    """Decodet einen BLOB zurück in einen numpy-Vektor."""
    if blob is None or len(blob) == 0:
        return None
    return np.frombuffer(blob, dtype=DTYPE)


def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
    """Kosinus-Ähnlichkeit zweier Vektoren.

    Da wir L2-normalisiert encoden, reicht das Skalarprodukt.
    Defensiv: wenn ein Vektor nicht normalisiert ist, fängt diese Variante das ab.
    """
    na = float(np.linalg.norm(a))
    nb = float(np.linalg.norm(b))
    if na == 0.0 or nb == 0.0:
        return 0.0
    return float(np.dot(a, b) / (na * nb))


def find_similar(
    query: np.ndarray,
    candidates: Iterable[tuple[int, np.ndarray]],
    top_k: int = 5,
    threshold: float = DEFAULT_MATCH_THRESHOLD,
) -> list[tuple[int, float]]:
    """Sucht in einer Kandidaten-Menge die top_k ähnlichsten Embeddings.

    Args:
        query: L2-normalisierter Query-Vektor.
        candidates: Iterable von (id, embedding-Vektor)-Tupeln.
        top_k: maximale Anzahl Treffer.
        threshold: minimaler Score, alles darunter wird verworfen.

    Returns:
        Liste von (id, score), absteigend sortiert.
    """
    scored: list[tuple[int, float]] = []
    for cid, vec in candidates:
        if vec is None:
            continue
        score = cosine_similarity(query, vec)
        if score >= threshold:
            scored.append((cid, score))
    scored.sort(key=lambda x: x[1], reverse=True)
    return scored[:top_k]


def warm_up() -> None:
    """Lädt das Modell vor (kann beim App-Start in einem Thread aufgerufen werden)."""
    _get_model()
