"""Lecture des chapitres du rapport consolidé (`_sec00.md` → `_sec10.md`).

Les chapitres sont la seule partie rédigée du corpus : c'est d'eux que
proviennent les résumés et les points clés. Ils désignent les textes de deux
façons — par numéro (« la loi n° 2026-403 du 26 mai ») et par nom d'usage
(« la loi "Riposte" », « l'aide à mourir ») — et sèment des marqueurs de
citation à chaque affirmation.

Le rattachement se fait **phrase par phrase** : les marqueurs d'une phrase vont
au texte que cette phrase nomme, pas à tous les textes du paragraphe. Sans quoi
un paragraphe traitant de six lois attribuerait ses vingt sources aux six.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

from pipeline import chemins
from pipeline.journal import logger
from pipeline.parseurs.dates_fr import sans_accents
from pipeline.parseurs.markdown import decouper_sections, extraire_marqueurs, nettoyer

log = logger("parseur.chapitres")

# « loi n° 2026-403 », « loi organique n° 2026-410 », « n° 2026-491 du 12 juin ».
# La négation finale écarte les numéros d'affaire du Conseil constitutionnel :
# sans elle, « déclarée conforme par la décision n° 2026-903 DC, puis promulguée
# le 26 mai » désignerait une loi « 2026-903 » qui n'existe pas, et la phrase —
# avec ses sources — serait volée à la loi n° 2026-403 dont elle parle.
_NUMERO_LOI = re.compile(r"n°\s*(\d{4}-\d{1,4})\b(?!\s*DC)")
# Découpage en phrases : point suivi d'une espace et d'une majuscule ou d'un
# guillemet ouvrant. Les abréviations du corpus (« n° », « art. », « déc. »,
# « juill. ») sont protégées par l'exigence de majuscule qui suit.
_FIN_DE_PHRASE = re.compile(r"(?<=[.!?])\s+(?=[A-ZÀÂÉÈÊÎÔÙÜ«])")

# Noms d'usage employés par les chapitres pour désigner un texte non promulgué.
# La clé est cherchée sans accent ni casse ; la valeur est l'identifiant posé
# par le parseur de l'annexe.
NOMS_D_USAGE: dict[str, str] = {
    "riposte": "loi-riposte",
    "ripost": "loi-riposte",
    "aide a mourir": "ppl-aide-a-mourir",
    "philippine": "loi-philippine",
    "reseaux sociaux": "ppl-reseaux-sociaux-mineurs",
    "moins de 15 ans": "ppl-reseaux-sociaux-mineurs",
    "urgence agricole": "loi-urgence-agricole",
    "programmation militaire": "loi-programmation-militaire",
    "lpm": "loi-programmation-militaire",
    "montagne vivante": "ppl-montagne",
    "ppl montagne": "ppl-montagne",
    "patrimoine immobilier": "ppl-patrimoine-immobilier-etat",
    "pacte migration": "pjl-pacte-migration",
    "pacte europeen sur la migration": "pjl-pacte-migration",
    "protection des enfants": "pjl-protection-enfants",
    "cohesion republicaine": "pjl-cohesion-republicaine",
    "logement (jeanbrun)": "pjl-logement",
    "jeanbrun": "pjl-logement",
    "globe": "accord-globe",
    "entrisme": "ppl-entrisme",
    "separatisme": "pjl-separatisme",
    "nunez": "pjl-separatisme",
    "outrage a l'hymne": "ppl-outrage-hymne-drapeau",
    "code noir": "ppl-abrogation-code-noir",
    "mathiasin": "ppl-abrogation-code-noir",
    "adaptation du droit des outre-mer": "ppl-adaptation-droit-outre-mer",
    "emploi durable": "ppl-emploi-ultramarin",
    "ratenon": "ppl-emploi-ultramarin",
    "entites naturelles": "ppl-entites-naturelles",
    "devoir conjugal": "ppl-devoir-conjugal",
    "kazakhstan": "accord-kazakhstan-readmission",
    "colombie": "accord-colombie-extradition",
    "macf": "pjl-ratification-ordonnance-macf",
}


@dataclass(slots=True)
class MentionTexte:
    """Ce qu'un chapitre dit d'un texte donné."""

    texte_id: str
    marqueurs: list[str] = field(default_factory=list)
    phrases: list[str] = field(default_factory=list)
    chapitre: str = ""
    section: str = ""
    # Vrai lorsque le titre de section nomme explicitement ce texte : la
    # section lui est alors consacrée, et sa prose peut servir de résumé.
    section_dediee: bool = False


@dataclass(slots=True)
class LectureChapitres:
    mentions: dict[str, list[MentionTexte]] = field(default_factory=dict)

    def marqueurs_de(self, texte_id: str) -> list[str]:
        vus: list[str] = []
        for mention in self.mentions.get(texte_id, []):
            for marqueur in mention.marqueurs:
                if marqueur not in vus:
                    vus.append(marqueur)
        return vus

    def resume_de(self, texte_id: str, *, phrases_max: int = 5) -> str | None:
        """Résumé tiré des sections consacrées au texte, sinon `None`.

        Aucune phrase n'est reformulée : ce sont celles du rapport, nettoyées
        de leur balisage et de leurs marqueurs de citation.
        """
        dediees = [m for m in self.mentions.get(texte_id, []) if m.section_dediee]
        candidates = dediees or self.mentions.get(texte_id, [])

        phrases: list[str] = []
        for mention in candidates:
            for phrase in mention.phrases:
                propre = nettoyer(phrase)
                if len(propre) > 40 and propre not in phrases:
                    phrases.append(propre)
                if len(phrases) >= phrases_max:
                    break
            if len(phrases) >= phrases_max:
                break

        if not phrases:
            return None
        return " ".join(phrases[:phrases_max])


def parser() -> LectureChapitres:
    """Analyse les onze chapitres et rattache leurs mentions aux textes."""
    lecture = LectureChapitres()

    for numero in range(11):
        fichier = chemins.chapitre(numero)
        if not fichier.exists():
            continue
        nom = f"sec{numero:02d}"

        for section in decouper_sections(fichier.read_text(encoding="utf-8"), niveau_max=4):
            nommes_au_titre = _textes_designes(section.titre)
            # Un titre qui nomme plusieurs textes — « 3.2.3 Cohésion
            # républicaine, logement, GloBE, entrisme » — n'est dédié à aucun.
            # Sans cette restriction, chaque phrase de transition de la section
            # (« Septembre 2026 sera un mois réglementaire… ») atterrirait dans
            # le résumé des quatre textes à la fois.
            cibles_du_titre = nommes_au_titre if len(nommes_au_titre) == 1 else []

            for paragraphe in _paragraphes(section.corps):
                for phrase in _phrases(paragraphe):
                    cibles = _textes_designes(phrase) or cibles_du_titre
                    if not cibles:
                        continue
                    marqueurs = [m.brut for m in extraire_marqueurs(phrase)]
                    for cible in cibles:
                        _ajouter(
                            lecture,
                            cible,
                            marqueurs=marqueurs,
                            phrase=phrase,
                            chapitre=nom,
                            section=section.titre,
                            dediee=cible in cibles_du_titre,
                        )

    log.info(
        "chapitres analysés",
        textes_mentionnes=len(lecture.mentions),
        mentions=sum(len(m) for m in lecture.mentions.values()),
    )
    return lecture


def _ajouter(
    lecture: LectureChapitres,
    texte_id: str,
    *,
    marqueurs: list[str],
    phrase: str,
    chapitre: str,
    section: str,
    dediee: bool,
) -> None:
    mentions = lecture.mentions.setdefault(texte_id, [])
    courante = next(
        (m for m in mentions if m.chapitre == chapitre and m.section == section), None
    )
    if courante is None:
        courante = MentionTexte(
            texte_id=texte_id, chapitre=chapitre, section=section, section_dediee=dediee
        )
        mentions.append(courante)

    courante.section_dediee = courante.section_dediee or dediee
    courante.phrases.append(phrase)
    for marqueur in marqueurs:
        if marqueur not in courante.marqueurs:
            courante.marqueurs.append(marqueur)


def _paragraphes(corps: str) -> list[str]:
    """Paragraphes de prose, tableaux et listes exclus."""
    blocs: list[str] = []
    for bloc in corps.split("\n\n"):
        propre = bloc.strip()
        if not propre or propre.startswith(("|", "#", "- ", "* ", ">")):
            continue
        blocs.append(" ".join(propre.split()))
    return blocs


def _phrases(paragraphe: str) -> list[str]:
    return [p for p in _FIN_DE_PHRASE.split(paragraphe) if p.strip()]


def _textes_designes(fragment: str) -> list[str]:
    """Identifiants des textes qu'un fragment nomme, par numéro ou nom d'usage."""
    trouves: list[str] = []

    for numero in _NUMERO_LOI.findall(fragment):
        identifiant = f"loi-{numero}"
        if identifiant not in trouves:
            trouves.append(identifiant)

    plat = sans_accents(fragment).lower()
    for nom, identifiant in NOMS_D_USAGE.items():
        if nom in plat and identifiant not in trouves:
            trouves.append(identifiant)

    return trouves
