"""Rapprochement d'un texte collecté avec les textes déjà en base.

Deux voies, dans cet ordre :

1. **Le numéro officiel.** Deux textes portant le même numéro de loi sont le
   même texte, sans discussion. C'est le cas facile, et le plus fréquent une
   fois la promulgation intervenue.
2. **La similarité de titre**, pour les textes non encore promulgués, qui n'ont
   pas de numéro. Le rapport les désigne par un nom d'usage (« loi Riposte »)
   là où les sources officielles emploient l'intitulé complet (« Loi visant à
   offrir des réponses immédiates aux phénomènes troublant l'ordre public… »).
   La comparaison brute échouerait ; on compare donc des titres normalisés, et
   on tient compte des mots significatifs communs.

Trois issues possibles, et c'est important : rapproché, **à signaler** (au-delà
du seuil bas mais en-deçà du seuil haut — un humain tranche), ou nouveau. Le
pipeline ne fusionne jamais sur une correspondance douteuse.
"""

from __future__ import annotations

import functools
import re
from dataclasses import dataclass
from difflib import SequenceMatcher
from enum import StrEnum

import yaml

from pipeline import chemins
from pipeline.parseurs.dates_fr import sans_accents

# Mots vides et vocabulaire juridique passe-partout : deux lois quelconques les
# partagent, ils ne prouvent donc aucune parenté.
_MOTS_IGNORES = frozenset(
    """loi organique projet proposition relative relatif visant portant diverses
    dispositions de des du la le les l a au aux et en pour par sur dans un une
    ainsi que qui plus mesures texte article n no ndeg""".split()
)

_NON_ALPHANUM = re.compile(r"[^a-z0-9]+")


class Issue(StrEnum):
    RAPPROCHE = "rapproche"
    A_SIGNALER = "a_signaler"
    NOUVEAU = "nouveau"


@dataclass(slots=True)
class Correspondance:
    """Résultat d'un rapprochement."""

    issue: Issue
    texte_id: str | None = None
    score: float = 0.0
    motif: str = ""

    @property
    def est_certaine(self) -> bool:
        return self.issue is Issue.RAPPROCHE


@functools.lru_cache(maxsize=1)
def _seuils() -> tuple[float, float]:
    configuration = yaml.safe_load(chemins.CONFIG.read_text(encoding="utf-8"))
    rapprochement = configuration.get("rapprochement", {})
    return (
        float(rapprochement.get("seuil_similarite", 0.86)),
        float(rapprochement.get("seuil_signalement", 0.72)),
    )


def normaliser(titre: str) -> str:
    """Réduit un titre à ses mots significatifs, sans accent ni ponctuation."""
    plat = sans_accents(titre).lower()
    mots = [m for m in _NON_ALPHANUM.split(plat) if m and m not in _MOTS_IGNORES]
    return " ".join(mots)


def similarite(gauche: str, droite: str) -> float:
    """Similarité de deux titres, entre 0 et 1.

    Combine deux mesures : le recouvrement des mots significatifs, qui résiste
    aux réordonnancements, et la similarité de séquence, qui capte les
    variantes orthographiques. La plus favorable des deux l'emporte — un titre
    court mais entièrement contenu dans un titre long doit être reconnu.
    """
    a, b = normaliser(gauche), normaliser(droite)
    if not a or not b:
        return 0.0
    if a == b:
        return 1.0

    mots_a, mots_b = set(a.split()), set(b.split())
    communs = mots_a & mots_b
    recouvrement = len(communs) / min(len(mots_a), len(mots_b))
    sequence = SequenceMatcher(None, a, b).ratio()

    return max(recouvrement, sequence)


def rapprocher(
    *,
    titre: str,
    numero_officiel: str | None,
    candidats: list[tuple[str, str, str | None]],
) -> Correspondance:
    """Rapproche un texte collecté des textes en base.

    `candidats` est une liste de `(identifiant, titre, numero_officiel)`.
    """
    seuil_haut, seuil_bas = _seuils()

    if numero_officiel:
        for identifiant, _, numero in candidats:
            if numero and numero == numero_officiel:
                return Correspondance(
                    issue=Issue.RAPPROCHE,
                    texte_id=identifiant,
                    score=1.0,
                    motif=f"numéro officiel identique ({numero_officiel})",
                )

    meilleur: tuple[float, str] | None = None
    for identifiant, titre_candidat, _ in candidats:
        score = similarite(titre, titre_candidat)
        if meilleur is None or score > meilleur[0]:
            meilleur = (score, identifiant)

    if meilleur is None:
        return Correspondance(issue=Issue.NOUVEAU, motif="aucun candidat en base")

    score, identifiant = meilleur
    if score >= seuil_haut:
        return Correspondance(
            issue=Issue.RAPPROCHE,
            texte_id=identifiant,
            score=score,
            motif=f"similarité de titre {score:.2f} ≥ {seuil_haut}",
        )
    if score >= seuil_bas:
        return Correspondance(
            issue=Issue.A_SIGNALER,
            texte_id=identifiant,
            score=score,
            motif=(
                f"similarité de titre {score:.2f}, entre le seuil de signalement "
                f"({seuil_bas}) et celui de rapprochement ({seuil_haut}) — "
                "vérification humaine requise"
            ),
        )
    return Correspondance(
        issue=Issue.NOUVEAU, score=score, motif=f"meilleure similarité {score:.2f}, sous les seuils"
    )
