"""Embeddeurs (ADR-004) : le vrai modèle local ONNX, et un déterministe pour
tests/CI — même interface, mêmes dimensions (DIMENSIONS), aucun téléchargement en test.
"""

import hashlib
import math
from typing import Protocol

DIMENSIONS = 768
# mpnet remplace MiniLM-384 : décision de recette R5 (journal 17/07) — sur le
# banc français, MiniLM classait la page-réponse DERRIÈRE des passages sans
# rapport (0,24 vs 0,41) ; mpnet rétablit le classement et une marge
# signal/bruit exploitable (≥ 0,46 vs ≤ 0,42).
MODELE_LOCAL = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2"


class Embeddeur(Protocol):
    def encoder(self, textes: list[str]) -> list[list[float]]: ...


class EmbeddeurDeterministe:
    """Sac de tri-grammes haché puis normalisé : stable, sans réseau, et les
    textes proches partagent des composantes — assez pour tester le circuit
    complet (ingestion → pgvector → similarité)."""

    def encoder(self, textes: list[str]) -> list[list[float]]:
        return [self._un(t) for t in textes]

    def _un(self, texte: str) -> list[float]:
        vecteur = [0.0] * DIMENSIONS
        mots = texte.lower().split()
        grammes = mots + [" ".join(mots[i : i + 3]) for i in range(max(0, len(mots) - 2))]
        for gramme in grammes:
            empreinte = hashlib.sha256(gramme.encode()).digest()
            indice = int.from_bytes(empreinte[:4], "big") % DIMENSIONS
            signe = 1.0 if empreinte[4] % 2 == 0 else -1.0
            vecteur[indice] += signe
        norme = math.sqrt(sum(v * v for v in vecteur)) or 1.0
        return [v / norme for v in vecteur]


class EmbeddeurLocal:
    """fastembed (ONNX, CPU) — chargé paresseusement, jamais importé en test.
    Sortie NORMÉE : fastembed ne garantit pas des vecteurs unitaires, or la
    similarité par produit scalaire (suggestions) l'exige — pgvector, lui,
    normalise dans son opérateur cosinus, ce qui masquait l'écart."""

    def __init__(self) -> None:
        from fastembed import TextEmbedding  # import différé (dépendance optionnelle)

        self._modele = TextEmbedding(model_name=MODELE_LOCAL)

    def encoder(self, textes: list[str]) -> list[list[float]]:
        vecteurs = []
        for vecteur in self._modele.embed(textes):
            liste = vecteur.tolist()
            norme = math.sqrt(sum(v * v for v in liste)) or 1.0
            vecteurs.append([v / norme for v in liste])
        return vecteurs


def construire_embeddeur(mode: str) -> Embeddeur:
    if mode == "deterministe":
        return EmbeddeurDeterministe()
    return EmbeddeurLocal()
