"""Lecture de la bibliographie consolidée (`lois-france-2026-guadeloupe_ref.md`).

481 références au format ::

    [1] Aida. Loi ndeg 2026 554 290626 visant a relancer investissements secteur
        lhydroelectricite [EB/OL]. 29-30 juin 2026 (publication). https://…

C'est la seule liste du corpus intégralement pourvue d'URLs : elle sert de
filet pour garantir qu'aucun texte n'entre en base sans lien vérifiable.

Le titre y est translittéré sans accent ni ponctuation (« ndeg » pour « n° »),
ce qui interdit le rapprochement par libellé. En revanche les numéros de loi
y survivent, sous forme éclatée : « 2026 554 » pour « 2026-554 ». C'est ce
motif qui est exploité pour rattacher une référence à un texte.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import date

from pipeline.journal import logger
from pipeline.modeles import Confiance, Source
from pipeline.parseurs.dates_fr import lire_date, sans_accents
from pipeline.parseurs.markdown import nettoyer

log = logger("parseur.bibliographie")

ANNEE_CORPUS = 2026

_LIGNE_REFERENCE = re.compile(r"^\[(\d+)\]\s+(.*)$")
_URL = re.compile(r"https?://[^\s;,)\]]+")

# « ndeg 2026 554 », « 2026-554 », « n° 2026-491 » : le numéro de loi survit à
# la translittération sous une forme ou une autre.
_NUMERO_LOI = re.compile(r"\b(?:ndeg\s*|n°\s*|no\s*)?(\d{4})[\s-](\d{3,4})\b")

# Numéro d'affaire du Conseil constitutionnel, également translittéré.
_AFFAIRE_CC = re.compile(r"\b(\d{4})[\s-](\d{3,4})\s*DC\b", re.IGNORECASE)

# Numéro de document parlementaire, sous ses trois graphies : la cote brute
# « n° 3025 », la cote de dossier de l'Assemblée « l17b3025 » et celle du Sénat
# « ppl25-691 » / « pjl25-890 ». C'est souvent le seul point commun entre la
# note laconique d'un rapport de dimension (« Texte n° 3025 ») et l'entrée
# bibliographique qui porte l'URL (« L17b3025 proposition loi »).
_DOCUMENT_AN = re.compile(r"\bl\d{2}b(\d{3,4})\b", re.IGNORECASE)
_DOCUMENT_SENAT = re.compile(r"\b(?:ppl|pjl)\d{2}-(\d{3,4})\b", re.IGNORECASE)
_DOCUMENT_NUMERO = re.compile(r"\bn(?:°|deg|o)\s*(\d{3,4})\b", re.IGNORECASE)


@dataclass(slots=True)
class Reference:
    """Une entrée de la bibliographie consolidée."""

    numero: int
    editeur: str | None
    titre: str
    date_publication: date | None
    url: str | None
    numeros_cites: list[str] = field(default_factory=list)
    affaires_citees: list[str] = field(default_factory=list)
    documents_cites: list[str] = field(default_factory=list)

    @property
    def marqueur(self) -> str:
        return f"ref-{self.numero}"

    def en_source(self) -> Source | None:
        if not self.url:
            return None
        return Source(
            url=self.url,
            titre=self.titre[:280] or None,
            editeur=self.editeur,
            date_publication=self.date_publication,
            confiance=Confiance.MEDIUM,
            marqueur=self.marqueur,
            fichier_origine="lois-france-2026-guadeloupe_ref.md",
        )


def parser(markdown: str) -> list[Reference]:
    """Extrait toutes les références de la bibliographie."""
    references: list[Reference] = []

    for ligne in markdown.splitlines():
        correspondance = _LIGNE_REFERENCE.match(ligne.strip())
        if not correspondance:
            continue
        references.append(_lire_reference(int(correspondance.group(1)), correspondance.group(2)))

    avec_url = sum(1 for r in references if r.url)
    log.debug("bibliographie analysée", references=len(references), avec_url=avec_url)
    return references


def _lire_reference(numero: int, corps: str) -> Reference:
    url = None
    if trouvee := _URL.search(corps):
        url = trouvee.group(0).rstrip(".,;)")

    sans_lien = _URL.sub("", corps).strip()

    editeur: str | None = None
    titre = sans_lien
    if ". " in sans_lien:
        candidat, reste = sans_lien.split(". ", 1)
        # Un éditeur est court : au-delà, c'est que la phrase a été coupée sur
        # une abréviation (« n° 2026-491. »).
        if len(candidat) <= 60:
            editeur, titre = candidat.strip(), reste

    titre = titre.split("[EB/OL]", 1)[0].strip(" .;")

    return Reference(
        numero=numero,
        editeur=nettoyer(editeur) if editeur else None,
        titre=nettoyer(titre)[:400],
        date_publication=lire_date(sans_lien, annee_defaut=ANNEE_CORPUS),
        url=url,
        numeros_cites=_numeros_de_loi(sans_lien),
        affaires_citees=_affaires_cc(sans_lien),
        documents_cites=numeros_de_document(f"{sans_lien} {url or ''}"),
    )


def _numeros_de_loi(texte: str) -> list[str]:
    """Numéros de loi cités, normalisés au format « 2026-554 »."""
    plat = sans_accents(texte).lower()
    trouves: list[str] = []

    for annee, rang in _NUMERO_LOI.findall(plat):
        # Seules les années plausibles pour la fenêtre de veille comptent.
        if annee not in {"2024", "2025", "2026"}:
            continue
        # « programmation militaire 2024-2030 » n'est pas la loi n° 2024-2030 :
        # deux millésimes croissants forment une plage, pas un numéro. Le test
        # inverse laisse passer les vrais numéros à quatre chiffres, dont le
        # rang est toujours inférieur au millésime (loi n° 2021-1947).
        if 1900 <= int(rang) <= 2100 and int(rang) > int(annee):
            continue
        numero = f"{annee}-{int(rang)}"
        if numero not in trouves:
            trouves.append(numero)

    return trouves


def _affaires_cc(texte: str) -> list[str]:
    plat = sans_accents(texte)
    trouvees: list[str] = []
    for annee, rang in _AFFAIRE_CC.findall(plat):
        affaire = f"{annee}-{int(rang)} DC"
        if affaire not in trouvees:
            trouvees.append(affaire)
    return trouvees


_LIGNE_CORRESPONDANCE = re.compile(r"^\|\s*(dim\d{2}|phase5|insight)\s*\(?[^|]*\|\s*([^|]*)\|")
_PLAGE = re.compile(r"(\d+)\s*[–—-]\s*(\d+)")


def lire_table_de_correspondance(markdown: str) -> dict[str, set[int]]:
    """Lit la table « préfixe de citation → références [N] correspondantes ».

    La bibliographie se termine par un tableau qui indique, pour chaque rapport
    de dimension, quelles références globales lui appartiennent. C'est ce
    tableau qui rend possible le rapprochement d'une note sans URL avec son
    entrée bibliographique : sans lui, il faudrait chercher parmi 481 candidates
    au lieu d'une cinquantaine.
    """
    correspondances: dict[str, set[int]] = {}

    for ligne in markdown.splitlines():
        entree = _LIGNE_CORRESPONDANCE.match(ligne.strip())
        if not entree:
            continue

        prefixe, cellule = entree.group(1), entree.group(2)
        numeros: set[int] = set()

        for morceau in cellule.split(","):
            morceau = morceau.strip()
            if plage := _PLAGE.search(morceau):
                debut, fin = int(plage.group(1)), int(plage.group(2))
                if debut <= fin:
                    numeros.update(range(debut, fin + 1))
            elif morceau.isdigit():
                numeros.add(int(morceau))

        if numeros:
            correspondances[prefixe] = numeros

    return correspondances


def numeros_de_document(texte: str) -> list[str]:
    """Cotes de documents parlementaires citées, normalisées en chiffres seuls."""
    plat = sans_accents(texte)
    trouves: list[str] = []
    for motif in (_DOCUMENT_AN, _DOCUMENT_SENAT, _DOCUMENT_NUMERO):
        for numero in motif.findall(plat):
            depouille = str(int(numero))
            if depouille not in trouves:
                trouves.append(depouille)
    return trouves


def indexer_par_document(references: list[Reference]) -> dict[str, list[Reference]]:
    """Index cote de document → références qui la citent."""
    index: dict[str, list[Reference]] = {}
    for reference in references:
        for document in reference.documents_cites:
            index.setdefault(document, []).append(reference)
    return index


def indexer_par_numero(references: list[Reference]) -> dict[str, list[Reference]]:
    """Index numéro de loi → références qui le citent."""
    index: dict[str, list[Reference]] = {}
    for reference in references:
        for numero in reference.numeros_cites:
            index.setdefault(numero, []).append(reference)
    return index


def indexer_par_affaire(references: list[Reference]) -> dict[str, list[Reference]]:
    """Index numéro d'affaire DC → références qui la citent."""
    index: dict[str, list[Reference]] = {}
    for reference in references:
        for affaire in reference.affaires_citees:
            index.setdefault(affaire, []).append(reference)
    return index
