"""L'assistant (D2 — « sourcé ou silencieux ») et la suggestion de bilan.

- `repondre` : recherche sémantique → extraits au-dessus du seuil de
  pertinence, ou refus HONNÊTE qui dit ce qui a été cherché (écran 2 des
  maquettes). La rédaction est déléguée au `Generateur` (opt-in ADR-004 §3) ;
  sans lui, le mode extractif est la réponse.
- `suggerer_bilan` : similarité sémantique entre la description libre et les
  libellés ACTIFS des référentiels (l'IA ne peut suggérer que des codes
  existants) + comptage des bilans similaires du parc. Sans LLM : rapide,
  déterministe, explicable.
"""

from dataclasses import dataclass

import asyncpg

from .embeddings import Embeddeur
from .generation import Generateur
from .recherche import ExtraitTrouve, chercher

# Répliques des libellés français de @siop/shared (BILAN_FIELD_LABELS) —
# utilisés pour contextualiser les embeddings des codes.
CHAMPS_BILAN = {
    "DOOR_STATE": "état des portes",
    "CABIN_POSITION": "position cabine",
    "ANOMALY": "anomalie constatée",
    "EXTERNAL_CAUSE": "cause extérieure",
    "ACTION_TAKEN": "action réalisée",
    "COMPONENT_CONCERNED": "élément concerné",
}

# Défauts — surchargés par la config (AI_SEUIL_*) : calibrage en recette.
SEUIL_PERTINENCE = 0.45  # en dessous : le corpus ne porte pas la réponse
SEUIL_SUGGESTION = 0.40
SEUIL_CONFIANCE_FORTE = 0.55


@dataclass
class ReponseAssistant:
    mode: str  # « extractif » | « genere » | « refus »
    answer: str | None
    extraits: list[ExtraitTrouve]
    documents_corpus: int
    bilans_corpus: int


async def _taille_corpus(cnx: asyncpg.Connection) -> tuple[int, int]:
    ligne = await cnx.fetchrow(
        '''
        SELECT
          (SELECT count(DISTINCT "documentId") FROM "RagChunk"
            WHERE "sourceType" = 'DOCUMENT') AS documents,
          (SELECT count(*) FROM "RagChunk" WHERE "sourceType" = 'WORK_ORDER') AS bilans
        '''
    )
    return ligne["documents"], ligne["bilans"]


async def repondre(
    cnx: asyncpg.Connection,
    embeddeur: Embeddeur,
    generateur: Generateur,
    question: str,
    limite: int = 5,
    seuil_pertinence: float = SEUIL_PERTINENCE,
) -> ReponseAssistant:
    documents, bilans = await _taille_corpus(cnx)
    extraits = await chercher(cnx, embeddeur, question, limite)
    pertinents = [e for e in extraits if e.score >= seuil_pertinence]

    if not pertinents:
        # D2 : refus explicite — on dit ce qu'on a cherché, on n'invente rien.
        return ReponseAssistant(
            mode="refus",
            answer=None,
            extraits=[],
            documents_corpus=documents,
            bilans_corpus=bilans,
        )

    redige = generateur.rediger(question, pertinents)
    return ReponseAssistant(
        mode="genere" if redige else "extractif",
        answer=redige,
        extraits=pertinents,
        documents_corpus=documents,
        bilans_corpus=bilans,
    )


@dataclass
class SuggestionBilan:
    field: str
    value_id: str
    label: str
    confidence: str  # « FORTE » | « MOYENNE »
    similar_reports: int  # bilans du parc portant déjà ce code (« 9 bilans similaires »)
    score: float


def _cosinus(a: list[float], b: list[float]) -> float:
    return sum(x * y for x, y in zip(a, b))  # vecteurs déjà normés


async def suggerer_bilan(
    cnx: asyncpg.Connection,
    embeddeur: Embeddeur,
    description: str,
    seuil_suggestion: float = SEUIL_SUGGESTION,
    seuil_confiance_forte: float = SEUIL_CONFIANCE_FORTE,
) -> list[SuggestionBilan]:
    valeurs = await cnx.fetch(
        'SELECT id, field, label FROM "ReferenceValue" WHERE "isActive" ORDER BY field, label'
    )
    if not valeurs:
        return []

    textes = [description] + [
        f"{CHAMPS_BILAN.get(v['field'], v['field'])} : {v['label']}" for v in valeurs
    ]
    vecteurs = embeddeur.encoder(textes)
    v_description, v_valeurs = vecteurs[0], vecteurs[1:]

    # Le meilleur code par champ, au-dessus du seuil — jamais plus d'une
    # suggestion par champ, jamais un code inventé.
    meilleurs: dict[str, tuple[asyncpg.Record, float]] = {}
    for valeur, vecteur in zip(valeurs, v_valeurs):
        score = _cosinus(v_description, vecteur)
        if score < seuil_suggestion:
            continue
        champ = valeur["field"]
        if champ not in meilleurs or score > meilleurs[champ][1]:
            meilleurs[champ] = (valeur, score)

    suggestions: list[SuggestionBilan] = []
    for valeur, score in meilleurs.values():
        # « 9 bilans similaires sur ce parc » : les bilans clôturés portant ce code
        similaires = await cnx.fetchval(
            'SELECT count(*) FROM "RagChunk" WHERE "sourceType" = \'WORK_ORDER\' AND content ILIKE $1',
            f"%{valeur['label']}%",
        )
        suggestions.append(
            SuggestionBilan(
                field=valeur["field"],
                value_id=str(valeur["id"]),
                label=valeur["label"],
                confidence="FORTE" if score >= seuil_confiance_forte else "MOYENNE",
                similar_reports=similaires,
                score=round(score, 4),
            )
        )
    suggestions.sort(key=lambda s: s.score, reverse=True)
    return suggestions
