"""Collecteur du Conseil constitutionnel.

Deux pages, deux usages :

- **Affaires en instance**, filtrées sur les saisines DC par le paramètre
  `?id=32246` — sans lui, la page ne rend que des questions prioritaires de
  constitutionnalité et aucune affaire DC. C'est là que se lit le sort des
  textes adoptés mais non promulgués.
- **Décisions rendues**, pour détecter le passage d'une affaire en instance à
  une décision, et donc le déblocage d'une promulgation.

Vérifié le 25 juillet 2026 : la page des affaires en instance rendait les sept
affaires DC attendues par le corpus, et une de plus que lui — l'affaire
n° 2026-907 DC sur la programmation militaire, que le rapport mentionnait sans
numéro. C'est précisément ce qu'un pipeline de veille doit rattraper.
"""

from __future__ import annotations

import re
from datetime import date

from selectolax.parser import HTMLParser, Node

from pipeline.collecteurs.base import Collecteur, ResultatCollecte
from pipeline.collecteurs.http import ClientHttp
from pipeline.journal import logger
from pipeline.modeles import DecisionCC, ResultatCC
from pipeline.parseurs.dates_fr import lire_date, sans_accents

log = logger("collecteur.conseil_constitutionnel")

BASE = "https://www.conseil-constitutionnel.fr"
URL_AFFAIRES_DC = f"{BASE}/decisions/affaires-en-instances?id=32246"
URL_DECISIONS = f"{BASE}/decisions"

_AFFAIRE = re.compile(r"\b(\d{4}-\d{1,4})\s*DC\b")
_DECISION_TITRE = re.compile(
    r"D[ée]cision\s+n°\s*(\d{4}-\d{1,4})\s*DC\s+du\s+([^-–—]+?)\s{2,}(.*)", re.IGNORECASE
)

# Formulations employées par le Conseil dans ses intitulés de décision, testées
# du plus spécifique au plus général. L'ordre compte : « non conformité
# partielle » contient « conformité », et « conformité » ne contient pas
# « conforme » — c'est bien « Conformité » et « Conformité - réserve » que la
# page publie, pas « conforme ».
_RESULTATS = (
    ("non conformite partielle", ResultatCC.NON_CONFORMITE_PARTIELLE),
    ("non conformite totale", ResultatCC.NON_CONFORMITE_TOTALE),
    ("non conformite", ResultatCC.NON_CONFORMITE_TOTALE),
    ("non conforme", ResultatCC.NON_CONFORMITE_TOTALE),
    ("conformite - reserve", ResultatCC.CONFORME_AVEC_RESERVES),
    ("conformite reserve", ResultatCC.CONFORME_AVEC_RESERVES),
    ("conformite avec reserve", ResultatCC.CONFORME_AVEC_RESERVES),
    ("conforme avec reserve", ResultatCC.CONFORME_AVEC_RESERVES),
    ("conformite", ResultatCC.CONFORME),
    ("conforme", ResultatCC.CONFORME),
)

# Le tableau des affaires DC n'a pas les mêmes colonnes que celui des QPC :
# les positions ne sont donc pas fiables, seuls les en-têtes le sont.
_COLONNES = {
    "affaire": ("affaire",),
    "disposition": ("disposition",),
    "saisissants": ("auteur de la saisine", "auteur"),
    "date_saisine": ("date de la saisine",),
    "date_decision": ("date de lecture de la decision", "decision rendue le"),
}


class CollecteurConseilConstitutionnel(Collecteur):
    nom = "conseil_constitutionnel"

    def __init__(self, client: ClientHttp) -> None:
        self.client = client

    def _collecter(self) -> ResultatCollecte:
        resultat = ResultatCollecte(collecteur=self.nom)

        try:
            resultat.decisions_cc.extend(self.affaires_en_instance())
        except Exception as erreur:  # noqa: BLE001
            resultat.erreurs.append(f"affaires en instance : {erreur}")

        try:
            rendues = self.decisions_rendues()
        except Exception as erreur:  # noqa: BLE001
            resultat.erreurs.append(f"décisions rendues : {erreur}")
            rendues = []

        # Une décision rendue prime sur l'inscription au rôle : si une affaire
        # figure dans les deux listes, c'est qu'elle vient d'être tranchée.
        par_numero = {d.numero_affaire: d for d in resultat.decisions_cc}
        for decision in rendues:
            par_numero[decision.numero_affaire] = decision
        resultat.decisions_cc = list(par_numero.values())

        return resultat

    # ── Affaires en instance ─────────────────────────────────────────────────
    def affaires_en_instance(self) -> list[DecisionCC]:
        reponse = self.client.get(URL_AFFAIRES_DC)
        if not reponse.a_reussi:
            raise RuntimeError(f"HTTP {reponse.code}")

        document = HTMLParser(reponse.texte)
        tableau = document.css_first("table")
        if tableau is None:
            raise RuntimeError("tableau des affaires introuvable")

        entetes = _index_des_colonnes(tableau)
        affaires: list[DecisionCC] = []
        for ligne in tableau.css("tbody tr"):
            if affaire := _lire_ligne_instance(ligne, entetes):
                affaires.append(affaire)

        log.debug("affaires DC en instance", nombre=len(affaires))
        return affaires

    # ── Décisions rendues ────────────────────────────────────────────────────
    def decisions_rendues(self) -> list[DecisionCC]:
        reponse = self.client.get(URL_DECISIONS)
        if not reponse.a_reussi:
            raise RuntimeError(f"HTTP {reponse.code}")

        document = HTMLParser(reponse.texte)
        decisions: list[DecisionCC] = []

        for article in document.css("article"):
            texte = " ".join(article.text(separator=" ", strip=True).split())
            if " DC " not in texte and " DC " not in texte:
                continue
            if decision := _lire_decision_rendue(texte, _lien(article)):
                decisions.append(decision)

        log.debug("décisions DC rendues", nombre=len(decisions))
        return decisions


def _index_des_colonnes(tableau: Node) -> dict[str, int]:
    """Associe chaque rôle de colonne à sa position, d'après les en-têtes."""
    entetes = [
        sans_accents(th.text(separator=" ", strip=True)).lower()
        for th in tableau.css("th")
    ]
    index: dict[str, int] = {}
    for role, variantes in _COLONNES.items():
        for position, entete in enumerate(entetes):
            if any(entete.startswith(variante) for variante in variantes):
                index[role] = position
                break
    return index


def _lire_ligne_instance(ligne: Node, colonnes: dict[str, int]) -> DecisionCC | None:
    cellules = [" ".join(c.text(separator=" ", strip=True).split()) for c in ligne.css("td")]
    if not cellules:
        return None

    def cellule(role: str) -> str:
        position = colonnes.get(role)
        return cellules[position] if position is not None and position < len(cellules) else ""

    correspondance = _AFFAIRE.search(cellule("affaire") or cellules[0])
    if not correspondance:
        return None

    date_decision = lire_date(cellule("date_decision"))
    saisissants = cellule("saisissants")
    intitule = cellule("disposition")

    return DecisionCC(
        numero_affaire=f"{correspondance.group(1)} DC",
        date_saisine=lire_date(cellule("date_saisine")),
        date_decision=date_decision,
        resultat=ResultatCC.EN_INSTANCE if date_decision is None else None,
        saisissants=saisissants[:300] or None,
        resume=intitule[:500] or None,
        url=URL_AFFAIRES_DC,
    )


def _lire_decision_rendue(texte: str, lien: str | None) -> DecisionCC | None:
    correspondance = _AFFAIRE.search(texte)
    if not correspondance:
        return None

    numero = correspondance.group(1)
    apres = texte[correspondance.end() :]

    date_decision = _date_apres_du(apres)
    resultat = _resultat_du_texte(apres)

    return DecisionCC(
        numero_affaire=f"{numero} DC",
        date_decision=date_decision,
        resultat=resultat,
        resume=" ".join(apres.split())[:500] or None,
        url=lien,
    )


_APRES_DU = re.compile(r"\bdu\s+(\d{1,2}(?:er)?\s+\w+\s+\d{4})", re.IGNORECASE)


def _date_apres_du(fragment: str) -> date | None:
    if trouve := _APRES_DU.search(fragment):
        return lire_date(trouve.group(1))
    return lire_date(fragment)


def _resultat_du_texte(fragment: str) -> ResultatCC | None:
    plat = sans_accents(fragment).lower()
    for forme, resultat in _RESULTATS:
        if forme in plat:
            return resultat
    return None


def _lien(article: Node) -> str | None:
    ancre = article.css_first("a")
    if ancre is None:
        return None
    href = ancre.attributes.get("href") or ""
    if href.startswith("http"):
        return href
    if href.startswith("/"):
        return BASE + href
    return None
