"""Lecture des dates françaises telles qu'elles apparaissent dans le corpus.

Le corpus mélange les formes : « 23 avr. 2026 », « 1er juill. 2026 »,
« 12 juin 2026 », « 24.07.2026 », « 22/07/2026 », « ~24 août 2026 »,
« Fin août 2026 », « Automne 2026 ». Les trois premières familles donnent une
date exacte ; les suivantes ne donnent qu'une intention de calendrier, qu'on ne
convertit jamais en date certaine — le libellé est conservé tel quel.
"""

from __future__ import annotations

import re
import unicodedata
from datetime import date

# Formes rencontrées dans le corpus, abrégées ou non, avec ou sans accent.
MOIS: dict[str, int] = {
    "janvier": 1, "janv": 1,
    "fevrier": 2, "fevr": 2, "fev": 2,
    "mars": 3,
    "avril": 4, "avr": 4,
    "mai": 5,
    "juin": 6,
    "juillet": 7, "juill": 7, "juil": 7,
    "aout": 8,
    "septembre": 9, "sept": 9,
    "octobre": 10, "oct": 10,
    "novembre": 11, "nov": 11,
    "decembre": 12, "dec": 12,
}

# Découpages de saison utilisés pour situer une échéance sans date exacte.
SAISONS: dict[str, tuple[int, int]] = {
    "printemps": (4, 1),
    "ete": (7, 1),
    "automne": (10, 1),
    "hiver": (1, 15),
    "rentree": (9, 1),
}

_LITTERALE = re.compile(
    r"\b(\d{1,2})\s*(?:er|ᵉʳ|re|ère)?\.?\s+"
    r"(janvier|janv|fevrier|fevr|fev|mars|avril|avr|mai|juin|juillet|juill|juil|"
    r"aout|septembre|sept|octobre|oct|novembre|nov|decembre|dec)\.?\s*(\d{4})?",
    re.IGNORECASE,
)
_NUMERIQUE = re.compile(r"\b(\d{1,2})[./](\d{1,2})[./](\d{4})\b")
_ISO = re.compile(r"\b(\d{4})-(\d{2})-(\d{2})\b")
_MOIS_SEUL = re.compile(
    r"\b(debut|mi|fin|courant)?\s*-?\s*"
    r"(janvier|janv|fevrier|fevr|fev|mars|avril|avr|mai|juin|juillet|juill|juil|"
    r"aout|septembre|sept|octobre|oct|novembre|nov|decembre|dec)\.?"
    r"\s+(\d{4})\b",
    re.IGNORECASE,
)
_SAISON = re.compile(
    r"\b(printemps|ete|automne|hiver|rentree)\s+(?:de\s+)?(\d{4})\b", re.IGNORECASE
)


def sans_accents(texte: str) -> str:
    """Retire les diacritiques : « août » → « aout », « févr. » → « fevr. »."""
    decompose = unicodedata.normalize("NFD", texte)
    return "".join(c for c in decompose if unicodedata.category(c) != "Mn")


def lire_date(fragment: str, *, annee_defaut: int | None = None) -> date | None:
    """Extrait la première date **exacte** d'un fragment, sinon `None`.

    Une date exacte suppose un jour identifié. « Fin août 2026 » ou
    « Automne 2026 » renvoient `None` : voir `lire_echeance`.
    """
    if not fragment:
        return None
    plat = sans_accents(fragment).lower()

    if m := _ISO.search(plat):
        return _construire(int(m.group(1)), int(m.group(2)), int(m.group(3)))

    if m := _NUMERIQUE.search(plat):
        return _construire(int(m.group(3)), int(m.group(2)), int(m.group(1)))

    if m := _LITTERALE.search(plat):
        jour = int(m.group(1))
        mois = MOIS[m.group(2)]
        annee = int(m.group(3)) if m.group(3) else annee_defaut
        if annee is None:
            return None
        return _construire(annee, mois, jour)

    return None


def lire_echeance(fragment: str) -> tuple[date | None, str]:
    """Interprète une échéance prévisionnelle.

    Retourne `(date approchée ou None, libellé nettoyé)`. Le libellé est
    toujours conservé : c'est lui qui est affiché, la date ne sert qu'au tri et
    au filtrage par période.
    """
    libelle = " ".join(fragment.split()).strip(" .;")
    if not libelle:
        return None, ""

    if exacte := lire_date(libelle):
        return exacte, libelle

    plat = sans_accents(libelle).lower()

    if m := _MOIS_SEUL.search(plat):
        qualificatif, mois, annee = m.group(1), MOIS[m.group(2)], int(m.group(3))
        # Un mois sans jour est situé conventionnellement : début = 5, fin = 25,
        # milieu ou absence de qualificatif = 15. La date sert au tri, jamais à
        # affirmer un fait — c'est le libellé qui est affiché.
        jour = {"debut": 5, "fin": 25}.get(qualificatif or "", 15)
        return _construire(annee, mois, jour), libelle

    if m := _SAISON.search(plat):
        mois, jour = SAISONS[m.group(1)]
        return _construire(int(m.group(2)), mois, jour), libelle

    # « Indéterminée », « Sans échéance », « Incertaine »… : aucune date.
    return None, libelle


def _construire(annee: int, mois: int, jour: int) -> date | None:
    try:
        return date(annee, mois, jour)
    except ValueError:
        return None


def toutes_les_dates(fragment: str, *, annee_defaut: int | None = None) -> list[date]:
    """Toutes les dates exactes d'un fragment, dans l'ordre d'apparition.

    Utile pour les cellules qui en contiennent plusieurs, par exemple
    « Adoptée (20-21 juill.) ; saisine du 24 juill. ».
    """
    plat = sans_accents(fragment).lower()
    trouvees: list[tuple[int, date]] = []

    for motif, ordre in ((_ISO, "ymd"), (_NUMERIQUE, "dmy"), (_LITTERALE, "litterale")):
        for m in motif.finditer(plat):
            if ordre == "ymd":
                d = _construire(int(m.group(1)), int(m.group(2)), int(m.group(3)))
            elif ordre == "dmy":
                d = _construire(int(m.group(3)), int(m.group(2)), int(m.group(1)))
            else:
                annee = int(m.group(3)) if m.group(3) else annee_defaut
                d = _construire(annee, MOIS[m.group(2)], int(m.group(1))) if annee else None
            if d:
                trouvees.append((m.start(), d))

    return [d for _, d in sorted(trouvees)]
