"""Ingestion du corpus (D3) : bibliothèque R3 (PDF, MinIO) + bilans codés.
Chaque texte passe par l'anonymisation (D4) AVANT découpage et embeddings.
Les chunks vivent dans `RagChunk` (pgvector, schéma possédé par Prisma).
"""

import io
import json
from dataclasses import dataclass

import asyncpg
from minio import Minio
from pypdf import PdfReader

from .anonymisation import anonymiser
from .config import Reglages
from .decoupage import decouper
from .embeddings import Embeddeur


@dataclass
class ResultatIngestion:
    documents_indexes: int
    documents_ignores: int
    bilans_indexes: int
    extraits: int


def _vecteur_sql(vecteur: list[float]) -> str:
    return "[" + ",".join(f"{v:.6f}" for v in vecteur) + "]"


async def noms_a_anonymiser(cnx: asyncpg.Connection) -> list[str]:
    """Toutes les identités connues de la base (D4) : utilisateurs, gardiens,
    contacts tiers, demandeurs du portail."""
    lignes = await cnx.fetch(
        '''
        SELECT "displayName" AS nom FROM "User"
        UNION SELECT "guardianName" FROM "Location" WHERE "guardianName" IS NOT NULL
        UNION SELECT "contactName" FROM "Partner" WHERE "contactName" IS NOT NULL
        '''
    )
    return [ligne["nom"] for ligne in lignes if ligne["nom"]]


def extraire_texte_pdf(octets: bytes) -> list[tuple[str, str]]:
    """[(texte, localisation)] par page — la citation doit pointer la page."""
    lecteur = PdfReader(io.BytesIO(octets))
    pages: list[tuple[str, str]] = []
    for numero, page in enumerate(lecteur.pages, start=1):
        texte = page.extract_text() or ""
        if texte.strip():
            pages.append((texte, f"p. {numero}"))
    return pages


async def indexer_documents(
    cnx: asyncpg.Connection,
    reglages: Reglages,
    embeddeur: Embeddeur,
    noms: list[str],
) -> tuple[int, int, int]:
    minio = Minio(
        f"{reglages.minio_endpoint}:{reglages.minio_port}",
        access_key=reglages.minio_access_key,
        secret_key=reglages.minio_secret_key,
        secure=reglages.minio_use_ssl,
    )
    documents = await cnx.fetch(
        'SELECT id, "fileName", "storageKey", "contentType", "inCorpus" FROM "Document"'
    )
    indexes, ignores, total_extraits = 0, 0, 0
    for doc in documents:
        # réindexation idempotente : on repart de zéro pour ce document
        await cnx.execute('DELETE FROM "RagChunk" WHERE "documentId" = $1', doc["id"])
        if not doc["inCorpus"] or doc["contentType"] != "application/pdf":
            await cnx.execute(
                'UPDATE "Document" SET "indexedAt" = NULL, "chunkCount" = 0 WHERE id = $1',
                doc["id"],
            )
            ignores += 1
            continue
        reponse = minio.get_object(reglages.minio_bucket, doc["storageKey"])
        try:
            octets = reponse.read()
        finally:
            reponse.close()
            reponse.release_conn()
        extraits: list[tuple[str, str]] = []
        for texte_page, localisation in extraire_texte_pdf(octets):
            texte_sur = anonymiser(texte_page, noms)
            extraits += [(morceau, localisation) for morceau in decouper(texte_sur)]
        if extraits:
            vecteurs = embeddeur.encoder([contenu for contenu, _ in extraits])
            await cnx.executemany(
                '''
                INSERT INTO "RagChunk"
                  (id, "sourceType", "documentId", locator, content, embedding)
                VALUES (gen_random_uuid(), 'DOCUMENT', $1, $2, $3, $4::vector)
                ''',
                [
                    (doc["id"], localisation, contenu, _vecteur_sql(vecteur))
                    for (contenu, localisation), vecteur in zip(extraits, vecteurs)
                ],
            )
        await cnx.execute(
            'UPDATE "Document" SET "indexedAt" = now(), "chunkCount" = $2 WHERE id = $1',
            doc["id"],
            len(extraits),
        )
        indexes += 1
        total_extraits += len(extraits)
    return indexes, ignores, total_extraits


async def indexer_bilans(
    cnx: asyncpg.Connection, embeddeur: Embeddeur, noms: list[str]
) -> tuple[int, int]:
    """Les bilans codés clôturés — « sur votre parc, ce réglage a déjà… »."""
    await cnx.execute('DELETE FROM "RagChunk" WHERE "sourceType" = \'WORK_ORDER\'')
    bilans = await cnx.fetch(
        '''
        SELECT wo.id, wo.reference, wo.title, wo."completedAt", ir.note,
               a.reference AS asset_ref, a.brand, a.model,
               (SELECT json_object_agg(rv.field, rv.label)
                  FROM "InterventionReport" ir2
                  JOIN "ReferenceValue" rv ON rv.id IN (
                    ir2."doorStateId", ir2."cabinPositionId", ir2."anomalyId",
                    ir2."externalCauseId", ir2."actionTakenId", ir2."componentConcernedId")
                 WHERE ir2."workOrderId" = wo.id) AS bilan
          FROM "WorkOrder" wo
          JOIN "InterventionReport" ir ON ir."workOrderId" = wo.id
          JOIN "Asset" a ON a.id = wo."assetId"
         WHERE wo.status = 'DONE'
        '''
    )
    lignes = []
    for bilan in bilans:
        champs = json.loads(bilan["bilan"]) if bilan["bilan"] else {}
        codes = " ; ".join(f"{champ} : {label}" for champ, label in champs.items())
        # note (D5) : dictée ou saisie libre relue par l'humain — même pipeline
        # d'anonymisation que le reste, aucun traitement à part.
        note = f" Description libre : {bilan['note']}." if bilan["note"] else ""
        contenu = anonymiser(
            f"Intervention {bilan['reference']} — {bilan['title']}. "
            f"Appareil {bilan['asset_ref']} ({bilan['brand']} {bilan['model'] or ''}). "
            f"Bilan codé : {codes}.{note}",
            noms,
        )
        quand = bilan["completedAt"].date().isoformat() if bilan["completedAt"] else "date inconnue"
        lignes.append((bilan["id"], f"bilan du {quand}", contenu))
    if lignes:
        vecteurs = embeddeur.encoder([contenu for _, _, contenu in lignes])
        await cnx.executemany(
            '''
            INSERT INTO "RagChunk"
              (id, "sourceType", "workOrderId", locator, content, embedding)
            VALUES (gen_random_uuid(), 'WORK_ORDER', $1, $2, $3, $4::vector)
            ''',
            [
                (wo_id, localisation, contenu, _vecteur_sql(vecteur))
                for (wo_id, localisation, contenu), vecteur in zip(lignes, vecteurs)
            ],
        )
    return len(lignes), len(lignes)


async def reindexer_tout(
    cnx: asyncpg.Connection, reglages: Reglages, embeddeur: Embeddeur
) -> ResultatIngestion:
    noms = await noms_a_anonymiser(cnx)
    docs_ok, docs_non, extraits_docs = await indexer_documents(cnx, reglages, embeddeur, noms)
    bilans, extraits_bilans = await indexer_bilans(cnx, embeddeur, noms)
    return ResultatIngestion(
        documents_indexes=docs_ok,
        documents_ignores=docs_non,
        bilans_indexes=bilans,
        extraits=extraits_docs + extraits_bilans,
    )
