"""Briques de lecture du markdown du corpus.

Trois besoins récurrents : découper les tableaux, retirer le balisage d'emphase
sans perdre le texte, et récolter les marqueurs de citation `[^dim07-24^]` qui
relient un fait de chapitre à sa source dans un rapport de dimension.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field

# Marqueurs du corpus : [^dim07-24^], [^phase5-3.1^], [^insight-5^], [^481^]
MOTIF_MARQUEUR = re.compile(r"\[\^([a-zA-Z0-9]+(?:-[0-9.]+)?)\^\]")

# Le marqueur avec l'espace qui le précède, pour un retrait sans cicatrice.
_MARQUEUR_ET_ESPACE = re.compile(r"[  \t]*(?:\[\^[a-zA-Z0-9]+(?:-[0-9.]+)?\^\])+")

_EMPHASE = re.compile(r"\*{1,3}(.+?)\*{1,3}", re.DOTALL)
_LIEN_MD = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
_CODE = re.compile(r"`([^`]+)`")
_SEPARATEUR_TABLEAU = re.compile(r"^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$")


@dataclass(slots=True)
class Tableau:
    """Un tableau markdown, avec le titre de section qui le précède."""

    entetes: list[str]
    lignes: list[dict[str, str]]
    section: str = ""
    ligne_debut: int = 0

    def __len__(self) -> int:
        return len(self.lignes)

    def colonne(self, prefixe: str) -> str | None:
        """Retrouve un nom d'en-tête par son début, insensible à la casse."""
        prefixe = prefixe.lower()
        for entete in self.entetes:
            if entete.lower().startswith(prefixe):
                return entete
        return None


@dataclass(slots=True)
class Marqueur:
    """Une référence `[^dimXX-N^]` rencontrée dans un texte."""

    brut: str          # « dim07-24 »
    dimension: str     # « dim07 », « phase5 », « insight », ou « ref »
    numero: str        # « 24 », « 3.1 »

    @property
    def cle(self) -> str:
        return f"{self.dimension}-{self.numero}" if self.numero else self.dimension


def extraire_tableaux(markdown: str) -> list[Tableau]:
    """Découpe tous les tableaux d'un document markdown.

    Une ligne d'en-tête suivie d'une ligne de séparation ouvre un tableau ; la
    première ligne qui ne commence pas par `|` le referme.
    """
    lignes = markdown.splitlines()
    tableaux: list[Tableau] = []
    section = ""
    i = 0

    while i < len(lignes):
        ligne = lignes[i]

        if ligne.lstrip().startswith("#"):
            section = ligne.lstrip("#").strip()
            i += 1
            continue

        est_ligne_tableau = ligne.strip().startswith("|")
        separateur_suivant = i + 1 < len(lignes) and _SEPARATEUR_TABLEAU.match(lignes[i + 1])

        if est_ligne_tableau and separateur_suivant:
            entetes = _decouper_ligne(ligne)
            corps: list[dict[str, str]] = []
            j = i + 2
            while j < len(lignes) and lignes[j].strip().startswith("|"):
                cellules = _decouper_ligne(lignes[j])
                if any(c for c in cellules):
                    # Tolère les lignes plus courtes ou plus longues que l'en-tête.
                    cellules += [""] * (len(entetes) - len(cellules))
                    corps.append(dict(zip(entetes, cellules[: len(entetes)], strict=True)))
                j += 1
            tableaux.append(
                Tableau(entetes=entetes, lignes=corps, section=section, ligne_debut=i + 1)
            )
            i = j
            continue

        i += 1

    return tableaux


def _decouper_ligne(ligne: str) -> list[str]:
    """Découpe une ligne de tableau en cellules, en respectant les `\\|` échappés."""
    interne = ligne.strip()
    if interne.startswith("|"):
        interne = interne[1:]
    if interne.endswith("|"):
        interne = interne[:-1]
    cellules = re.split(r"(?<!\\)\|", interne)
    return [c.replace(r"\|", "|").strip() for c in cellules]


def nettoyer(texte: str, *, garder_marqueurs: bool = False) -> str:
    """Retire le balisage d'emphase, les liens et les marqueurs de citation."""
    resultat = _CODE.sub(r"\1", texte)
    resultat = _LIEN_MD.sub(r"\1", resultat)
    resultat = _EMPHASE.sub(r"\1", resultat)
    if not garder_marqueurs:
        # L'espace qui précède le marqueur est consommée avec lui, sinon le
        # retrait laisse « … dans les DROM . ». On ne touche surtout pas aux
        # autres espaces : la typographie française en veut une avant « ; »,
        # « : », « ! », « ? » et à l'intérieur des guillemets « … ».
        resultat = _MARQUEUR_ET_ESPACE.sub("", resultat)
    return " ".join(resultat.split()).strip(" ;,")


def extraire_marqueurs(texte: str) -> list[Marqueur]:
    """Récolte les marqueurs de citation d'un fragment, sans doublon."""
    marqueurs: list[Marqueur] = []
    vus: set[str] = set()

    for brut in MOTIF_MARQUEUR.findall(texte):
        if brut in vus:
            continue
        vus.add(brut)
        if "-" in brut:
            dimension, numero = brut.split("-", 1)
        elif brut.isdigit():
            # Marqueur local à un fichier de dimension : [^24^]
            dimension, numero = "", brut
        else:
            dimension, numero = brut, ""
        marqueurs.append(Marqueur(brut=brut, dimension=dimension, numero=numero))

    return marqueurs


@dataclass(slots=True)
class Section:
    """Un bloc de texte délimité par un titre markdown."""

    niveau: int
    titre: str
    corps: str
    ligne_debut: int
    sous_sections: list[Section] = field(default_factory=list)


def decouper_sections(markdown: str, *, niveau_max: int = 3) -> list[Section]:
    """Découpe un document en sections à plat, jusqu'au niveau de titre demandé."""
    lignes = markdown.splitlines()
    sections: list[Section] = []
    courante: Section | None = None
    tampon: list[str] = []

    for numero, ligne in enumerate(lignes, start=1):
        entete = re.match(r"^(#{1,6})\s+(.*)$", ligne)
        if entete and len(entete.group(1)) <= niveau_max:
            if courante:
                courante.corps = "\n".join(tampon).strip()
                sections.append(courante)
            courante = Section(
                niveau=len(entete.group(1)),
                titre=entete.group(2).strip(),
                corps="",
                ligne_debut=numero,
            )
            tampon = []
        else:
            tampon.append(ligne)

    if courante:
        courante.corps = "\n".join(tampon).strip()
        sections.append(courante)

    return sections
