"""Recette fictive, Python 3 standard. Aucun micro, SDK, modèle, CRM ou réseau.

Les révisions et confirmations sont un contrat applicatif inventé. La table
SQLite en mémoire représente un destinataire fictif qui déduplique les demandes.
Elle ne prouve aucune propriété de Mistral ni d'un CRM réel.
"""
import json
import sqlite3
from dataclasses import dataclass


class Refus(ValueError):
    pass


@dataclass(frozen=True)
class Proposition:
    session: str
    revision: int
    site: str
    palettes: int


class Destination:
    def __init__(self):
        self.db = sqlite3.connect(":memory:")
        self.db.execute("CREATE TABLE demandes (cle TEXT PRIMARY KEY, contenu TEXT NOT NULL)")

    def recevoir(self, cle, proposition):
        contenu = json.dumps(proposition.__dict__, sort_keys=True)
        old = self.db.execute("SELECT contenu FROM demandes WHERE cle=?", (cle,)).fetchone()
        if old:
            if old[0] != contenu:
                raise Refus("même clé, autre contenu")
            return "retrouvee"
        with self.db:
            self.db.execute("INSERT INTO demandes VALUES (?, ?)", (cle, contenu))
        return "creee"

    def lignes(self):
        return [json.loads(x[0]) for x in self.db.execute("SELECT contenu FROM demandes ORDER BY cle")]


class Parcours:
    def __init__(self, session="S1", site="LYON-A"):
        self.session, self.site = session, site
        self.revision = 0
        self.proposition = None
        self.accord = None
        self.etat = "ecoute"
        self.audio = "actif"
        self.habilite = True

    def proposer(self, *, session, role, final, palettes):
        if self.etat in ("inconnu", "confirme"):
            raise Refus("rapprocher ou ouvrir une correction métier")
        if session != self.session or role != "user" or final is not True:
            raise Refus("transcription non admissible")
        if type(palettes) is not int or not 1 <= palettes <= 20:
            raise Refus("quantité hors contrat")
        self.revision += 1
        self.proposition = Proposition(self.session, self.revision, self.site, palettes)
        self.accord = None
        self.etat = "a_confirmer"
        return self.proposition

    def confirmer(self, proposition):
        # Geste authentifié fourni par l'application, pas un « oui » analysé ici.
        if self.etat != "a_confirmer" or proposition != self.proposition:
            raise Refus("accord absent ou proposition ancienne")
        self.accord = proposition
        self.etat = "autorise"

    def interrompre_audio(self):
        self.audio = "arrete"
        # Aucun changement de l'état métier ni de l'accord.

    def retirer_avant_envoi(self):
        if self.etat in ("inconnu", "confirme"):
            raise Refus("effet déjà engagé : pas d'annulation locale")
        self.accord = None
        self.etat = "retire"

    def envoyer(self, destination, *, perdre_reponse=False):
        if not self.habilite or self.etat != "autorise" or self.accord != self.proposition:
            raise Refus("écriture non autorisée")
        self.etat = "inconnu"
        resultat = destination.recevoir(self.session + ":collecte", self.accord)
        if not perdre_reponse:
            self.etat = "confirme"
        return None if perdre_reponse else resultat

    def rapprocher(self, destination):
        if self.etat != "inconnu" or self.accord is None:
            raise Refus("pas de demande à rapprocher")
        resultat = destination.recevoir(self.session + ":collecte", self.accord)
        self.etat = "confirme"
        return resultat


def recette():
    checks = 0

    def verifie(condition):
        nonlocal checks
        assert condition
        checks += 1

    def refuse(fn):
        nonlocal checks
        try:
            fn()
        except Refus:
            checks += 1
        else:
            raise AssertionError("le refus attendu manque")

    def proposer(p, n, **overrides):
        args = dict(session=p.session, role="user", final=True, palettes=n)
        args.update(overrides)
        return p.proposer(**args)

    # A : première valeur partielle, puis correction finale. Rien n'est écrit
    # avant la confirmation explicite de la proposition à deux palettes.
    d = Destination()
    p = Parcours()
    refuse(lambda: proposer(p, 12, final=False))
    verifie(d.lignes() == [] and p.proposition is None)
    deux = proposer(p, 2)
    refuse(lambda: p.envoyer(d))
    verifie(d.lignes() == [] and p.etat == "a_confirmer")
    p.confirmer(deux)
    verifie(p.envoyer(d) == "creee")
    verifie(len(d.lignes()) == 1 and d.lignes()[0]["palettes"] == 2)
    p.interrompre_audio()
    verifie(p.audio == "arrete" and p.etat == "confirme" and len(d.lignes()) == 1)
    refuse(p.retirer_avant_envoi)
    refuse(lambda: p.envoyer(d))

    # B : même une première transcription finale peut être corrigée ensuite.
    d2, p2 = Destination(), Parcours("S2")
    douze = proposer(p2, 12)
    p2.confirmer(douze)
    deux_b = proposer(p2, 2)
    refuse(lambda: p2.confirmer(douze))
    refuse(lambda: p2.envoyer(d2))
    verifie(d2.lignes() == [] and p2.accord is None)
    p2.confirmer(deux_b)
    p2.envoyer(d2)
    verifie(d2.lignes()[0]["palettes"] == 2 and deux_b.revision == 2)

    # C : réponse perdue après création, interruption audio, rapprochement.
    d3, p3 = Destination(), Parcours("S3")
    p3.confirmer(proposer(p3, 2))
    verifie(p3.envoyer(d3, perdre_reponse=True) is None)
    verifie(p3.etat == "inconnu" and len(d3.lignes()) == 1)
    p3.interrompre_audio()
    verifie(p3.etat == "inconnu")
    refuse(p3.retirer_avant_envoi)
    refuse(lambda: proposer(p3, 12))
    verifie(p3.rapprocher(d3) == "retrouvee")
    verifie(p3.etat == "confirme" and len(d3.lignes()) == 1)
    refuse(lambda: d3.recevoir("S3:collecte", Proposition("S3", 2, "LYON-A", 12)))
    verifie(len(d3.lignes()) == 1 and d3.lignes()[0]["palettes"] == 2)

    # D : retrait explicite avant envoi ; arrêter l'audio ne suffit pas.
    d4, p4 = Destination(), Parcours("S4")
    p4.confirmer(proposer(p4, 2))
    p4.interrompre_audio()
    verifie(p4.etat == "autorise")
    p4.retirer_avant_envoi()
    refuse(lambda: p4.envoyer(d4))
    verifie(d4.lignes() == [] and p4.etat == "retire")

    # Entrées explicitement hors contrat : aucun changement silencieux.
    p5 = Parcours("S5")
    valide = proposer(p5, 2)
    for n in (0, -1, 21, True, 2.0, "2", None):
        refuse(lambda n=n: proposer(p5, n))
        verifie(p5.proposition == valide and p5.revision == 1)
    for overrides in ({"session": "AUTRE"}, {"role": "assistant"}, {"final": 1}, {"final": "true"}):
        refuse(lambda overrides=overrides: proposer(p5, 2, **overrides))
        verifie(p5.proposition == valide)
    p5.confirmer(valide)
    p5.habilite = False
    d5 = Destination()
    refuse(lambda: p5.envoyer(d5))
    verifie(d5.lignes() == [])

    # Trois politiques fautives, évaluées sur les mêmes nombres fictifs.
    mauvais_partiel = 12 * 45
    bon_final = 2 * 45
    mauvais_double = 2 + 2
    mauvais_annule = 0
    verifie((mauvais_partiel, bon_final, mauvais_partiel - bon_final) == (540, 90, 450))
    verifie(mauvais_double != d3.lignes()[0]["palettes"])
    verifie(mauvais_annule != len(d.lignes()))
    verifie(25 * 8 == 200)
    return {"status": "PASS", "checks": checks,
            "cas_A": d.lignes(), "cas_B": d2.lignes(), "cas_C": d3.lignes(), "cas_D": d4.lignes(),
            "hypothese_euros": {"douze": mauvais_partiel, "deux": bon_final, "ecart": 450},
            "contre_exemples": {"doublon_palettes": mauvais_double, "fausse_annulation_nombre_demandes": mauvais_annule},
            "limites": "aucun audio, SDK, modèle, réseau, CRM réel, redémarrage ou concurrence"}


if __name__ == "__main__":
    print(json.dumps(recette(), ensure_ascii=False, indent=2))
