"""Substrat argumentation (ICT-7289 Phase A, graine strate 6, Epic #4588).

La serie ICT vit de matrices de transition. Ce module livre le **grounding**
de la Phase A de l'issue #7289 : un graphe de transition (une TPM) sur des
**etats de croyance** derivé d'une source reelle, l'argumentation abstraite de
Dung (1995) telle qu'outillée dans la serie Tweety
(``SymbolicAI/Tweety/Tweety-5-Abstract-Argumentation.ipynb``).

**Cadre.** Un Argumentation Framework (AF) de Dung est un couple
``(arguments, attaques)`` : un argument ``a`` *attaque* un argument ``b``. Une
**semantique** attribue a chaque argument un **label** parmi ``{in, out,
undec}`` -- son statut d'acceptation. La **semantique grounded** (Dung 1995)
est le plus petit point fixe de la fonction caracteristique : un argument est
``in`` s'il n'est attaque par aucun ``in`` (et commence par les arguments non
attaques), ``out`` s'il est attaque par un ``in``, ``undec`` sinon (cas typique
des cycles d'attaque mutuelle -- ex. le *Nixon diamond*).

**Le substrat ICT.** Quand les arguments *arrivent sequentiellement* (un debat
se construit), le graphe partiel grandit et le labeling grounded evolue pas a
pas : la suite des labelings est une **trajectoire d'etats de croyance**. On
encode chaque labeling en un **etat** (resume compact ``(n_in, n_out,
n_undec)``, ou signature complete base-3 preservant l'identite des arguments),
puis on estime la **TPM** de cette trajectoire via :mod:`ict.tpm_estimation`.
La dynamique discursive devient alors un substrat ICT a part entiere :
soumise a :mod:`ict.time_arrow` (production d'entropie ``sigma`` = fleche du
temps du discours) et, en Phase B, a :mod:`ict.reversibility_budget` (dette
d'irreversibilite culturelle) et :mod:`ict.spectral` (signature spectrale des
structures fallacieuses).

**Honestete (Phase A = GATE dur).** Le verdict de phase A est : *une TPM
exploitable est-elle derivable d'un AF reel ?* OUI : ce module le produit par
script executable, sur des AF canoniques documentes dans la serie Tweety. Le
notebook Phase B est *gated* sur ce PASS.

Numpy uniquement (semantique grounded sur des sets Python, clarte ; TPM et
analyse en numpy), GPU-free. Pas de JVM requise : la semantique grounded est
implementee en pur Python, et peut etre validee contre Tweety dans le
notebook (pont SymbolicAI/Tweety-5).
"""

from __future__ import annotations

import re
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple

import numpy as np

from . import time_arrow
from . import tpm_estimation


# --------------------------------------------------------------------------- #
#  Argumentation Framework de Dung (1995)                                     #
# --------------------------------------------------------------------------- #


AttackRel = Tuple[int, int]


class DungAF:
    """Argumentation Framework abstrait de Dung : arguments + relation d'attaque.

    Parameters
    ----------
    arguments : sequence d'entiers
        Les identifiants d'arguments (typiquement ``range(n)``).
    attacks : sequence de couples ``(a, b)``
        ``a`` attaque ``b`` (``a`` est un *attaquant* de ``b``).

    Notes
    -----
    Representation pedagogique (pas de JVM). La semantique grounded de
    :func:`grounded_labeling` est l'algorithme standard (point fixe de la
    fonction caracteristique) et peut etre croisee avec Tweety dans le
    notebook Phase B pour validation.
    """

    def __init__(self, arguments: Iterable[int], attacks: Iterable[AttackRel]):
        self.arguments: List[int] = sorted(set(int(a) for a in arguments))
        self.attacks: List[AttackRel] = [
            (int(a), int(b)) for (a, b) in attacks
            if int(a) in self.arguments and int(b) in self.arguments
        ]
        # Index inverse : attaquant(s) de chaque argument.
        self._attackers: Dict[int, Set[int]] = {a: set() for a in self.arguments}
        for (a, b) in self.attacks:
            self._attackers[b].add(a)

    def attackers(self, arg: int) -> Set[int]:
        """Renvoie l'ensemble des arguments qui attaquent ``arg``."""
        return self._attackers.get(arg, set())

    def n(self) -> int:
        """Nombre d'arguments."""
        return len(self.arguments)

    def induced(self, subset: Iterable[int]) -> "DungAF":
        """Sous-AF induit par ``subset`` : arguments de ``subset`` + leurs
        attaques mutuelles (les deux extremites dans ``subset``)."""
        keep = set(int(a) for a in subset)
        sub_attacks = [
            (a, b) for (a, b) in self.attacks if a in keep and b in keep
        ]
        return DungAF(sorted(keep), sub_attacks)


# --------------------------------------------------------------------------- #
#  Semantique grounded (point fixe de la fonction caracteristique)            #
# --------------------------------------------------------------------------- #


def grounded_labeling(af: DungAF) -> Dict[int, str]:
    """Labeling **grounded** de l'AF ``af`` (Dung 1995).

    Algorithme (iteration du point fixe, le *characteristic function* de Dung)
    exprime en termes de labels ``in`` / ``out`` / ``undec`` :

      1. ``IN_0 = ∅`` (rien n'est accepte au depart) ;
      2. repeter : ``OUT_t`` = arguments attaques par un element de ``IN_t`` ;
         ``IN_{t+1}`` = arguments dont **tous** les attaquants sont dans
         ``OUT_t`` ;
      3. arret au point fixe ``IN_{t+1} = IN_t``.

    Les arguments ni ``in`` ni ``out`` au point fixe sont ``undec`` (cas
    canonique : le *Nixon diamond* -- deux arguments s'attaquant mutuellement
    sans attaquant externe forment un cycle non resolu, les deux ``undec``).

    Retourne un dict ``{argument: 'in' | 'out' | 'undec'}``.
    """
    IN: Set[int] = set()
    IN_prev: Optional[Set[int]] = None
    while IN != IN_prev:
        IN_prev = set(IN)
        OUT: Set[int] = set()
        for b in af.arguments:
            if af.attackers(b) & IN:
                OUT.add(b)
        IN = set()
        for c in af.arguments:
            attackers_c = af.attackers(c)
            # IN : tout attaquant est OUT (un argument sans attaquant satisfait
            # la condition trivialement -> rentre dans IN au tour 1).
            if attackers_c <= OUT:
                IN.add(c)
    OUT = set()
    for b in af.arguments:
        if af.attackers(b) & IN:
            OUT.add(b)
    UNDEC = set(af.arguments) - IN - OUT
    label: Dict[int, str] = {}
    for a in af.arguments:
        if a in IN:
            label[a] = "in"
        elif a in OUT:
            label[a] = "out"
        else:
            label[a] = "undec"
    return label


def est_admissible(af: DungAF, subset: Iterable[int]) -> bool:
    """Un ensemble ``S`` est **admissible** (Dung 1995) s'il est sans conflit
    et si chaque membre se defend : tout attaquant *exterieur* d'un membre
    est contre-attaque par un membre de ``S``.

    Cas limites : l'ensemble vide est admissible ; un argument non attaque
    forme seul un ensemble admissible.
    """
    S = set(int(a) for a in subset)
    # Sans conflit : aucune attaque entre deux membres.
    for (a, b) in af.attacks:
        if a in S and b in S:
            return False
    # Defense : tout attaquant exterieur d'un membre est contre-attaque par S.
    for m in S:
        for att in af.attackers(m):
            if att in S:
                continue
            contre_attaque = any(a in S and b == att for (a, b) in af.attacks)
            if not contre_attaque:
                return False
    return True


def preferred_extensions(af: DungAF, limite: int = 15) -> List[Set[int]]:
    """**Extensions preferees** de ``af`` : ensembles admissibles **maximaux
    pour l'inclusion** (Dung 1995).

    Ajoutee pour la strate 6 (acceptabilite QBF, distillation EPITA
    ``qbf_native`` #17339) : la question sceptique -- ``target`` est-il dans
    TOUTES les extensions preferees ? -- exige cette semantique, que la
    grounded seule ne fournit pas.

    Enumeration naive assumee (meme style que le reste de la serie) : tous
    les sous-ensembles parcourus, les admissibles gardes, puis filtre des
    maximaux pour l'inclusion. Cout ``2**n`` -- au-dela de ``limite``
    arguments l'organe refuse de mesurer (``ValueError``), borne rendue
    visible plutot qu'un temps de calcul silencieux.

    Retourne la liste triee des extensions (chacune un ``set`` d'arguments).
    """
    n = af.n()
    if n > limite:
        raise ValueError(
            f"{n} arguments > borne {limite} : enumeration 2**n refusee "
            "(augmenter explicitement la borne si le cadre est vraiment petit)"
        )
    arguments = af.arguments
    admissibles: List[Set[int]] = []
    for bits in range(1 << n):
        S = {arguments[i] for i in range(n) if bits & (1 << i)}
        if est_admissible(af, S):
            admissibles.append(S)
    preferred: List[Set[int]] = []
    for S in admissibles:
        if not any(S < T for T in admissibles):
            preferred.append(S)
    return sorted(preferred, key=lambda s: (len(s), sorted(s)))


# --------------------------------------------------------------------------- #
#  Trajectoire de croyance sous arrivee sequentielle d'arguments              #
# --------------------------------------------------------------------------- #


def belief_trajectory(
    af: DungAF,
    order: Sequence[int],
    include_empty: bool = False,
) -> List[Dict[int, str]]:
    """Trajectoire des labelings grounded sous **arrivee sequentielle**.

    Au pas ``k`` (k = 1..n), le graphe contient les ``k`` premiers arguments de
    ``order`` (et les attaques mutuelles). On calcule le labeling grounded du
    sous-AF induit. La suite des labelings est la **trajectoire d'etats de
    croyance** : le debat se construit, l'etat d'acceptation evolue.

    Parameters
    ----------
    af : DungAF
        AF complet (tous les arguments possibles + leurs attaques).
    order : sequence d'arguments
        Ordre d'arrivee (un argument par pas). Doit couvrir un sous-ensemble
        des arguments de ``af``.
    include_empty : bool
        Si vrai, ajoute l'etat vide ``{}`` en tete (etat initial avant toute
        arrivee).

    Retourne une liste de dicts (un labeling par pas). Le pas ``k`` ne porte
    que sur les ``k`` premiers arguments arrives.
    """
    traj: List[Dict[int, str]] = []
    if include_empty:
        traj.append({})
    arrived: List[int] = []
    for arg in order:
        if int(arg) not in af.arguments:
            raise ValueError(f"belief_trajectory : argument {arg} hors AF")
        arrived.append(int(arg))
        sub = af.induced(arrived)
        traj.append(grounded_labeling(sub))
    return traj


def belief_summary(labeling: Dict[int, str]) -> Tuple[int, int, int]:
    """Resume compact d'un etat de croyance : ``(n_in, n_out, n_undec)``.

    Encodage lisible (interpretable) d'un labeling en un etat agrége. Les
    transitions entre resumes ont du sens : a mesure que les arguments
    arrivent, les compteurs evoluent (un nouvel argument ``in`` gonfle
    ``n_in`` et peut basculer un ``in`` existant en ``out``).
    """
    counts = {"in": 0, "out": 0, "undec": 0}
    for v in labeling.values():
        counts[v] = counts.get(v, 0) + 1
    return (counts["in"], counts["out"], counts["undec"])


def state_signature(
    labeling: Dict[int, str],
    n_args: Optional[int] = None,
) -> Tuple[int, ...]:
    """Signature complete base-3 preservant l'identite de chaque argument.

    Chaque argument (dans l'ordre ``0..n_args-1``) est encode : ``out=0``,
    ``undec=1``, ``in=2``. Un argument non encore arrive (absent du labeling)
    vaut ``-1`` (placeholder). Cette signature distingue *quel* argument est
    accepte, pas seulement combien -- utile pour la dette d'irreversibilite en
    Phase B (bascules a sens unique sur un argument donne).
    """
    code = {"out": 0, "undec": 1, "in": 2}
    if n_args is None:
        n_args = max(labeling) + 1 if labeling else 0
    return tuple(code.get(labeling.get(a, ""), -1) for a in range(n_args))


# --------------------------------------------------------------------------- #
#  TPM sur etats de croyance                                                  #
# --------------------------------------------------------------------------- #


def belief_transition_matrix(
    af: DungAF,
    order: Sequence[int],
    encoding: str = "summary",
) -> Tuple[np.ndarray, dict]:
    """Estime la **TPM** d'un debat (arrivee sequentielle sur ``order``).

    Pipeline : trajectoire de labelings (:func:`belief_trajectory`) -> encodage
    des etats (:func:`belief_summary` ou :func:`state_signature`) -> estimation
    empirique de la TPM via :func:`ict.tpm_estimation.tpm_from_trajectory`.

    Parameters
    ----------
    af : DungAF
    order : sequence d'arguments (ordre d'arrivee).
    encoding : ``"summary"`` (defaut, compact et lisible) ou ``"signature"``
      (complet, preserve l'identite des arguments).

    Retourne ``(TPM, mapping)`` ou ``mapping`` associe chaque etat a son index.
    """
    traj = belief_trajectory(af, order, include_empty=True)
    if encoding == "summary":
        states = [belief_summary(lab) for lab in traj]
    elif encoding == "signature":
        states = [state_signature(lab, n_args=af.n()) for lab in traj]
    else:
        raise ValueError(f"encoding inconnu : {encoding!r} (summary|signature)")
    return tpm_estimation.tpm_from_trajectory(states, unseen="self")


# --------------------------------------------------------------------------- #
#  Lecture ICT : fleche du temps du discours (sigma)                          #
# --------------------------------------------------------------------------- #


def discourse_irreversibility(P: np.ndarray) -> dict:
    """Mesure la **fleche du temps du discours** depuis sa TPM.

    La production d'entropie ``sigma`` (Schnakenberg / Seifert, cf.
    :mod:`ict.time_arrow`) quantifie la violation de detailed balance : un
    regime discursif **reversible** (chaque mise a jour de croyance peut etre
    defaite symetriquement) a ``sigma ~ 0`` ; un regime **irreversible``
    (bascules de croyance a sens unique, hysteresis) a ``sigma > 0``.

    C'est le candidat de mesure pour la **dette d'irreversibilite du discours**
    (Phase B, pred. pre-enregistree : regime populiste = ``sigma`` eleve avec
    ``K`` faible vs regime deliberatif = ``sigma`` faible avec ``K`` eleve).

    Retourne ``{"sigma", "pi", "P_rev_distance"}`` :
      - ``sigma`` : production d'entropie (nats) ;
      - ``pi`` : distribution stationnaire ;
      - ``P_rev_distance`` : distance L1/2 a la projection reversible
        (quantite de dynamique irreversible portee par la chaine).
    """
    P = np.asarray(P, dtype=float)
    pi = time_arrow.stationary_distribution(P)
    sigma = time_arrow.entropy_production(P, pi)
    P_rev = time_arrow.reversibilize(P, pi)
    dist = 0.5 * float(np.sum(np.abs(P - P_rev)))
    return {"sigma": float(sigma), "pi": pi, "P_rev_distance": dist}


# --------------------------------------------------------------------------- #
#  Refus de mesurer (tranche temoins #7742) : un refus n'est pas un zero      #
# --------------------------------------------------------------------------- #

REFUS_CODES = (
    "ENTREE_VIDE",
    "AUCUN_ENONCE_CODABLE",
    "CANAUX_CONTRADICTOIRES",
    "SOURCE_INDISPONIBLE",
)


class RefusDeMesure(Exception):
    """Le pipeline refuse de produire une mesure — distingué d'un zéro mesuré.

    Un zéro (ex. ``sigma = 0.0`` sur un débat déterministe à ordre fixe,
    §5 du notebook) est un **nombre produit par une mesure qui a tourné**.
    Un refus ne produit **aucun nombre** : l'entrée ne porte pas ce que
    l'instrument prétend mesurer. Sans ce chemin explicite, une entrée sans
    contenu propositionnel sortirait un score par construction (graphe vide
    ou valeur par défaut) et le contrôle négatif serait vide avant d'avoir
    tourné (arbitrage #7742, tranche témoins).
    """

    def __init__(self, code: str, raison: str = ""):
        if code not in REFUS_CODES:
            raise ValueError(f"code de refus inconnu : {code!r} ({REFUS_CODES})")
        self.code = code
        super().__init__(f"[{code}] {raison}")


# Signature lexicale minimale d'une langue naturelle : les mots fonctionnels
# (grammaticaux) couvrent typiquement 35-55 % des tokens d'un texte anglais
# ordinaire. Une glossolalie (pseudo-langue sans entrée lexicale) en couvre
# ZERO — par construction, pas par réglage. Liste fermée, ~80 items.
MOTS_FONCTIONNELS = frozenset("""
the of and a to in is you that it he was for on are as with his they i at be
this have from or one had by word but not what all were we when your can said
there use an each which she do how their if will up other about out many then
them these so some her would make like him into time has look two more write
go see no way could my than first been call who its now find long down day did
get come made may part over new sound take only little work know place year
live me back give most very after thing our just name good sentence man think
say great where help through much before line right too mean old any same
tell boy follow came want show also around form three small set put end does
another well large must big even such because turn here why ask went men read
need land different home us move try kind hand picture again change off play
spell air away animal house point page letter mother answer found study still
learn should world high every near add food between own below country plant
""".split())


def segmenter_enonces(texte: str) -> List[str]:
    """Segmentation en énoncés candidats (phrases). Purge : vides, < 2 mots."""
    brut = [t.strip() for t in re.split(r"[.!?]+", texte)]
    return [t for t in brut if len(t.split()) >= 2]


def couverture_fonctionnels(texte: str) -> float:
    """Proportion de mots fonctionnels du texte — signature de langue.

    Anglais ordinaire : 0.35-0.55. Glossolalie (pseudo-langue) : 0.0.
    """
    mots = re.findall(r"[a-zà-öø-ÿ']+", texte.lower())
    if not mots:
        return 0.0
    return sum(1 for m in mots if m in MOTS_FONCTIONNELS) / len(mots)


def est_codable(enonce: str, seuil_fonctionnels: float = 0.15) -> bool:
    """Test lexical explicite de codabilité propositionnelle.

    Un énoncé candidat est **codable** s'il porte la signature minimale d'une
    langue naturelle : au moins ``seuil_fonctionnels`` de mots fonctionnels.
    Ce test ne comprend pas l'énoncé — il ne teste que s'il s'agit de LANGUE.
    La glossolalie pseudo-allemande échoue mécaniquement (couverture 0) ;
    l'anglais ordinaire passe avec une marge large (0.35-0.55). Le seuil à
    0.15 est choisi sous le plancher de l'anglais réel, pas au-dessus du
    score de la glossolalie : il sépare deux régimes disjoints.
    """
    mots = re.findall(r"[a-zà-öø-ÿ']+", enonce.lower())
    if len(mots) < 3:
        return False
    return couverture_fonctionnels(enonce) >= seuil_fonctionnels


def enonces_codables(texte: str) -> List[str]:
    """Chaîne de codage : texte -> énoncés codables, ou REFUS (jamais vide).

    Lève :func:`RefusDeMesure` — ``ENTREE_VIDE`` si le texte est vide,
    ``AUCUN_ENONCE_CODABLE`` si aucun candidat ne passe le test de langue.
    Ne retourne JAMAIS une liste vide silencieuse : c'est le chemin de refus
    qui rend le contrôle négatif du fragment Hynkel informatif.
    """
    if not texte or not texte.strip():
        raise RefusDeMesure("ENTREE_VIDE", "aucun texte en entrée")
    candidats = segmenter_enonces(texte)
    codables = [e for e in candidats if est_codable(e)]
    if not codables:
        raise RefusDeMesure(
            "AUCUN_ENONCE_CODABLE",
            f"{len(candidats)} candidats, 0 codable (signature de langue absente)",
        )
    return codables


def verdict_canaux(canaux: Dict[str, str]) -> str:
    """Règle inter-canaux pré-enregistrée (tranche témoins #7742, Hynkel).

    Un matériau dont le canal **principal** est incodable et dont l'autre
    canal se présente comme sa **traduction** ne se mesure pas : la mesure
    lirait la traduction, pas le discours. Dans la scène du meeting, le
    traducteur radio euphémise systématiquement le discours qu'il prétend
    traduire — les deux canaux se contredisent, et tout nombre sorti sur ce
    matériau nommerait le canal réellement lu (arbitrage #7742 : « toute
    sortie non dégénérée nomme le canal réellement lu »).

    ``canaux`` : dict ``{role: texte}`` avec ``role`` parmi
    ``{"principal", "traduction"}``. Retourne le **verdict** :
    ``"MESURABLE"`` ou ``"REFUS:CANAUX_CONTRADICTOIRES"``
    (le détail par canal — notamment ``REFUS:AUCUN_ENONCE_CODABLE`` sur le
    principal — se lit via :func:`enonces_codables` appelée par canal).
    """
    roles = set(canaux)
    if roles - {"principal", "traduction"}:
        raise ValueError(f"rôles inconnus : {roles - {'principal', 'traduction'}}")
    if "principal" not in canaux:
        raise ValueError("verdict_canaux exige un canal 'principal'")
    try:
        enonces_codables(canaux["principal"])
        principal_codable = True
    except RefusDeMesure:
        principal_codable = False
    if not principal_codable and "traduction" in canaux:
        return "REFUS:CANAUX_CONTRADICTOIRES"
    return "MESURABLE"


# --------------------------------------------------------------------------- #
#  AF canoniques (sources reelles, documentees dans la serie Tweety)          #
# --------------------------------------------------------------------------- #


def nixon_diamond() -> DungAF:
    """Le *Nixon diamond* (Pollock 1987) : AF canonique du cycle non resolu.

    Nixon est un republican (argument A) ; les republicains sont pacifistes
    non, c'est l'exemple classique : Nixon est republicain (A), les
    republicains ne sont pas pacifistes (B) ; Nixon est quaker (C), les
    quakers sont pacifistes (D). A et C attaquent le statut pacifiste de Nixon
    via B et D. Forme minimale : deux arguments s'attaquant mutuellement, sans
    attaquant externe -> **les deux ``undec``** (le cas test de la semantique
    grounded).

    Simplification pedagogique : deux arguments 0 et 1 qui s'attaquent
    mutuellement.
    """
    return DungAF([0, 1], [(0, 1), (1, 0)])


def tweety_bird() -> DungAF:
    """L'exemple *Tweety* (Tweety est un oiseau ; les oiseaux volent ; mais
    les autruches ne volent pas).

    Trois arguments :
      - ``0`` : Tweety vole (puisque c'est un oiseau).
      - ``1`` : Tweety ne vole pas (c'est une autruche).
      - ``2`` : Tweety est un oiseau (soutien indirect -- ici represente comme
        attaquant la negation, simplification).

    Forme canonique de la litterature (cf. ``Tweety-5-Abstract-Argumentation``
    dans la serie SymbolicAI) : conflit direct ``0 <-> 1`` (deux conclusions
    opposees), avec un sous-ensemble qui resolve le conflit. Ici on encode le
    conflit nucleaire : ``0`` et ``1`` s'attaquent mutuellement ; ``1`` est
    soutenu-attaque par ``2`` (le fait de l'oiseau). Version minimale
    reproductible.
    """
    return DungAF([0, 1, 2], [(0, 1), (1, 0), (2, 1)])


def controversial_chain(n: int = 4) -> DungAF:
    """Chaine lineaire d'attaques ``0 -> 1 -> 2 -> ... -> n-1``.

    Semantique grounded deterministe : ``0`` (non attaque) est ``in``, ``1``
    est ``out``, ``2`` est ``in``, etc. (alternance). La trajectoire d'arrivee
    dans l'ordre ``0, 1, 2, ...`` exhibe des bascules propres : a chaque paire
    d'arrivees, un argument ``in`` devient ``out``. Bon substrat de test pour
    une TPM non triviale.
    """
    attacks = [(i, i + 1) for i in range(n - 1)]
    return DungAF(list(range(n)), attacks)


# --------------------------------------------------------------------------- #
#  Verdict Phase A : script executable qui produit la TPM + verdict           #
#  (lance via ``python -m ict.argumentation`` depuis ICT-Series/)             #
# --------------------------------------------------------------------------- #


def _phase_a_report() -> str:
    """Construit le verdict Phase A : TPM derivee d'AF canoniques reels.

    Source reelle : les AF canoniques documentes dans la serie Tweety
    (``SymbolicAI/Tweety/Tweety-5-Abstract-Argumentation.ipynb``) -- Nixon
    diamond (Pollock 1987), Tweety bird (Tweety-fly example), chaine lineaire
    d'attaques. Pour chacun : arrivee sequentielle -> trajectoire de labelings
    -> TPM ligne-stochastique -> fleche du temps du discours (sigma).
    """
    lines = ["=" * 72]
    lines.append("ICT-7289 PHASE A — Verdict de grounding (substrat argumentation)")
    lines.append("=" * 72)
    lines.append("Source reelle : AF canoniques de la serie Tweety "
                 "(SymbolicAI/Tweety/Tweety-5-Abstract-Argumentation.ipynb).")
    lines.append("")
    verdict_pass = True
    cases = [
        ("Nixon diamond (cycle non resolu)", nixon_diamond(), [0, 1]),
        ("Tweety bird (conflit nucleaire)", tweety_bird(), [0, 1, 2]),
        ("Chaine lineaire 0->1->2->3->4", controversial_chain(5),
         [0, 1, 2, 3, 4]),
    ]
    for name, af, order in cases:
        P, mapping = belief_transition_matrix(af, order=order)
        n_states = len(mapping)
        diag = discourse_irreversibility(P)
        # GATE Phase A : la TPM doit avoir >= 2 etats (substrat exploitable).
        ok = n_states >= 2
        verdict_pass = verdict_pass and ok
        lines.append(f"- {name}")
        lines.append(f"    args={af.arguments}, attaques={af.attacks}")
        lines.append(f"    TPM {P.shape[0]}x{P.shape[1]}, {n_states} etats, "
                     f"sigma={diag['sigma']:.4f} nats, "
                     f"P_rev_dist={diag['P_rev_distance']:.4f}")
        lines.append(f"    Phase A [{'PASS' if ok else 'FAIL'}] "
                     f"(>= 2 etats requis)")
        lines.append("")
    lines.append("-" * 72)
    lines.append(f"VERDICT PHASE A GLOBAL : {'PASS' if verdict_pass else 'FAIL'}")
    lines.append("  Une TPM exploitable est derivable d'un AF de Dung reel")
    lines.append("  (documente serie Tweety). -> le notebook Phase B est debloque.")
    lines.append("  NB : sigma=0 sur un debat deterministe a ordre fixe est")
    lines.append("  HONNETE (pas de cycle de flux) ; la dette d'irreversibilite")
    lines.append("  du discours (Phase B) emergera sous bruit / multi-ordres.")
    lines.append("=" * 72)
    return "\n".join(lines)


if __name__ == "__main__":
    print(_phase_a_report())

