# -*- coding: utf-8 -*-
"""
Simulation multi-agents du système monétaire Cubi
==================================================
Code de l'annexe A.4 du livre « Cubi — La monnaie ultra fondante »
(Daniel Fichter, avec Claude Fable), publié sur cubishome.com.

Règles simulées (chapitres 8 et 16 du livre) :
  - Création : chaque ménage reçoit V cubis/mois (la DIG) ; la puissance
    publique reçoit une dotation égale à la somme des DIG, dépensée à 100 %.
  - Destruction : tous les soldes fondent de d par mois (modèle à pas
    mensuel, équivalent exact de la fonte continue aux bornes des mois).
  - Le crédit transfère les soldes (les créances ne fondent pas).

Architecture du circuit (fermé : la monnaie est conservée hors création/fonte) :
  Ménages --consommation--> Entreprises --salaires/dividendes--> Ménages
  Ménages --prêts (excédents)--> Entreprises --remboursements+intérêts--> Ménages
  État --achats publics--> Entreprises ; État --salaires publics--> Ménages

Hypothèses comportementales (annexe A.4) :
  - 1 000 000 de ménages ; capacités de gain log-normales calées sur les
    déciles INSEE de niveau de vie (médiane ~2 180 €, D9/médiane ~1,91).
  - Propension à consommer décroissante par décile : 0,95 (D1) -> 0,55 (D10)
    (Dynan-Skinner-Zeldes 2004).
  - Encaisse cible des ménages : 1 mois de consommation ; l'excédent est
    prêté (créances : rendement 1 %/mois, remboursement sur 12 mois,
    défaut 0,1 %/mois de l'encours).
  - 80 000 entreprises ; trésorerie cible tirée entre 15 et 90 jours de
    décaissements.

Usage :
  python cubi_simulation.py                # simulation de base, 120 mois
  python cubi_simulation.py --choc         # ajoute le scénario de choc + contrefactuel
  python cubi_simulation.py --menages 100000   # version rapide

Sorties : résumé console + resultats.json (+ graphiques PNG si matplotlib).
Reproductible : graine aléatoire fixée (--seed).
"""

import argparse
import json
import math

import numpy as np


# ----------------------------------------------------------------------------
# Paramètres
# ----------------------------------------------------------------------------

def build_config(args):
    n_m = args.menages
    cfg = {
        # Population
        "n_menages": n_m,
        "n_entreprises": max(1, int(n_m * 0.08)),
        # Règles Cubi
        "V": 1000.0,            # DIG mensuelle par ménage
        "d": 0.10,              # taux de fonte mensuel
        # Distribution des capacités de gain (log-normale calée INSEE)
        # médiane 2180 ; sigma tel que D9/médiane ~ 1.91 => sigma ~ 0.506
        "revenu_median": 2180.0,
        "revenu_sigma": 0.506,
        # Comportement ménages
        "mpc_d1": 0.95,         # propension à consommer, 1er décile
        "mpc_d10": 0.55,        # propension à consommer, 10e décile
        "conso_min": 150.0,     # consommation incompressible c0
        "encaisse_cible_mois": 1.0,   # cible : 1 mois de consommation
        # Crédit
        "taux_creance": 0.01,   # rendement mensuel des créances
        "duree_pret_mois": 12,
        "taux_defaut": 0.001,   # défaut mensuel sur l'encours
        # Entreprises
        "treso_min_j": 15, "treso_max_j": 90,   # trésorerie cible en jours
        "part_dividendes": 0.10,                 # 10 % des décaissements
        # État (dépense 100 % de sa dotation)
        "part_salaires_publics": 0.5,            # 50 % salaires, 50 % achats
        # Simulation
        "mois": args.mois,
        "seed": args.seed,
    }
    return cfg


# ----------------------------------------------------------------------------
# Modèle
# ----------------------------------------------------------------------------

class SimulationCubi:
    def __init__(self, cfg, avec_cubi=True):
        """avec_cubi=False : contrefactuel « monnaie fixe » (ni DIG ni fonte),
        masse initialisée au même niveau, pour comparaison des chocs."""
        self.cfg = cfg
        self.avec_cubi = avec_cubi
        rng = np.random.default_rng(cfg["seed"])
        self.rng = rng
        n = cfg["n_menages"]

        # --- Ménages ---------------------------------------------------------
        mu = math.log(cfg["revenu_median"])
        self.capacite = rng.lognormal(mu, cfg["revenu_sigma"], n)  # poids salarial
        ordre = np.argsort(self.capacite)
        self.decile = np.empty(n, dtype=np.int8)
        self.decile[ordre] = np.arange(n) * 10 // n               # 0..9 (pour les stats)
        # Hétérogénéité CONTINUE (pas de paliers : les populations réelles n'ont
        # ni déciles de comportement, ni frontière binaire riches/pauvres —
        # des règles discrètes créeraient des « trous » artificiels dans la
        # distribution des soldes).
        rang = np.empty(n)
        rang[ordre] = np.arange(n) / (n - 1)                       # rang de revenu 0..1
        # propension à consommer : décroissante continûment avec le rang + bruit
        self.mpc = cfg["mpc_d1"] + (cfg["mpc_d10"] - cfg["mpc_d1"]) * rang
        self.mpc = np.clip(self.mpc + rng.normal(0, 0.03, n), 0.40, 0.99)
        # encaisse cible : hétérogène, 0,5 à 1,5 mois de consommation
        self.cible_mois = rng.uniform(0.5, 1.5, n) * cfg["encaisse_cible_mois"]
        self.solde_m = np.zeros(n)        # soldes monétaires des ménages
        self.creances = np.zeros(n)       # encours de créances détenues (ne fond pas)
        self.conso_prec = np.full(n, cfg["conso_min"])  # mémoire pour l'encaisse cible
        # détention d'actions : continue et concentrée (poids = capacité²),
        # pas de seuil binaire — tout le monde peut détenir, les hauts revenus dominent
        self.poids_div = self.capacite ** 2
        self.actionnaire = self.decile >= 8   # conservé pour les statistiques seulement

        # --- Entreprises -----------------------------------------------------
        ne = cfg["n_entreprises"]
        self.solde_e = np.zeros(ne)
        self.treso_cible_j = rng.uniform(cfg["treso_min_j"], cfg["treso_max_j"], ne)
        # parts de marché log-normales (concentration réaliste des ventes)
        pm = rng.lognormal(0.0, 1.0, ne)
        self.part_marche = pm / pm.sum()
        self.dette_e = 0.0                # encours agrégé emprunté aux ménages
        self.decaiss_prec = np.zeros(ne)

        # --- État -------------------------------------------------------------
        self.solde_etat = 0.0

        # --- Contrefactuel : initialisation de la masse -----------------------
        if not avec_cubi:
            # même masse totale que l'équilibre Cubi, répartie au prorata
            M = cfg["n_menages"] * cfg["V"] * 2 / cfg["d"] * (1 - cfg["d"])
            self.solde_m[:] = M * 0.6 / n
            self.solde_e[:] = M * 0.4 * self.part_marche

        # --- Journaux ----------------------------------------------------------
        self.hist = {"masse": [], "conso": [], "fonte_menages_decile": None,
                     "fonte_e": 0.0, "fonte_etat": 0.0, "fonte_m": 0.0}
        self._fonte_m_par_decile = np.zeros(10)

    # ------------------------------------------------------------------ outils
    def masse_totale(self):
        return self.solde_m.sum() + self.solde_e.sum() + self.solde_etat

    # ------------------------------------------------------------------- mois
    def tour(self, mois, choc=False):
        cfg, rng = self.cfg, self.rng
        n = cfg["n_menages"]

        # 1) CRÉATION : DIG aux ménages + dotation symétrique à l'État
        if self.avec_cubi:
            self.solde_m += cfg["V"]
            self.solde_etat += cfg["V"] * n

        # 2) L'ÉTAT dépense 100 % : salaires publics + achats aux entreprises
        dep = self.solde_etat
        sal_pub = dep * cfg["part_salaires_publics"]
        self.solde_m += sal_pub * (self.capacite / self.capacite.sum())
        self.solde_e += (dep - sal_pub) * self.part_marche
        self.solde_etat = 0.0

        # 3) ENTREPRISES : remboursent les prêts, paient salaires et dividendes
        #    (décaissement = tout ce qui dépasse la trésorerie cible)
        if self.dette_e > 0:
            interet = self.dette_e * cfg["taux_creance"]
            principal = self.dette_e / cfg["duree_pret_mois"]
            service = min(interet + principal, self.solde_e.sum() * 0.5)
            part = service / self.solde_e.sum() if self.solde_e.sum() > 0 else 0
            self.solde_e *= (1 - part)
            # versé aux créanciers au prorata de leurs encours
            cr_tot = self.creances.sum()
            if cr_tot > 0:
                self.solde_m += service * (self.creances / cr_tot)
                rembours_principal = max(0.0, service - interet)
                self.creances *= (1 - rembours_principal / cr_tot)
                self.dette_e = max(0.0, self.dette_e - rembours_principal)
            # défauts : pertes des créanciers, dette effacée
            defaut = self.dette_e * cfg["taux_defaut"]
            self.dette_e -= defaut
            if cr_tot > 0:
                self.creances *= (1 - defaut / max(cr_tot, 1e-9))

        treso_cible = self.decaiss_prec * (self.treso_cible_j / 30.0)
        decaisse = np.maximum(0.0, self.solde_e - treso_cible)
        self.solde_e -= decaisse
        masse_sal = decaisse.sum() * (1 - cfg["part_dividendes"])
        masse_div = decaisse.sum() * cfg["part_dividendes"]
        self.solde_m += masse_sal * (self.capacite / self.capacite.sum())
        self.solde_m += masse_div * (self.poids_div / self.poids_div.sum())
        self.decaiss_prec = decaisse

        # 4) MÉNAGES : consommation = c0 + mpc × revenu du mois
        revenu = cfg["V"] * self.avec_cubi + \
            (sal_pub + masse_sal) * (self.capacite / self.capacite.sum()) + \
            masse_div * (self.poids_div / self.poids_div.sum())
        mpc = self.mpc * (0.9 if choc else 1.0)   # choc : -10 % de propension
        conso = np.minimum(self.solde_m, cfg["conso_min"] + mpc * revenu)
        self.solde_m -= conso
        self.solde_e += conso.sum() * self.part_marche
        self.conso_prec = conso

        # 5) PRÊTS : l'excédent au-delà de l'encaisse cible est prêté
        #    (le solde est transféré aux entreprises : investissement)
        if self.avec_cubi:
            cible = self.cible_mois * np.maximum(conso, cfg["conso_min"])
            excedent = np.maximum(0.0, self.solde_m - cible)
            pret = excedent.sum()
            if pret > 0:
                self.solde_m -= excedent
                self.creances += excedent
                self.solde_e += pret * self.part_marche
                self.dette_e += pret

        # 6) FONTE : tous les soldes × (1-d) ; rien d'autre ne fond
        if self.avec_cubi:
            d = cfg["d"]
            fm = self.solde_m * d
            fe = self.solde_e.sum() * d
            fs = self.solde_etat * d
            np.add.at(self._fonte_m_par_decile, self.decile, fm)
            self.hist["fonte_m"] += fm.sum()
            self.hist["fonte_e"] += fe
            self.hist["fonte_etat"] += fs
            self.solde_m *= (1 - d)
            self.solde_e *= (1 - d)
            self.solde_etat *= (1 - d)

        # 7) Journaux
        self.hist["masse"].append(self.masse_totale())
        self.hist["conso"].append(conso.sum())

    def run(self, choc_debut=None, choc_duree=0):
        for m in range(self.cfg["mois"]):
            choc = (choc_debut is not None and choc_debut <= m < choc_debut + choc_duree)
            self.tour(m, choc=choc)
        self.hist["fonte_menages_decile"] = self._fonte_m_par_decile
        return self.hist


# ----------------------------------------------------------------------------
# Analyses
# ----------------------------------------------------------------------------

def resume(cfg, sim, hist):
    n = cfg["n_menages"]
    F = cfg["V"] * n * 2                       # flux mensuel total (ménages + État)
    M_theorique = F / cfg["d"] * (1 - cfg["d"])  # masse de fin de mois (après fonte)
    M_finale = hist["masse"][-1]
    soldes = sim.solde_m

    fonte_tot = hist["fonte_m"] + hist["fonte_e"] + hist["fonte_etat"]
    fonte_top10 = hist["fonte_menages_decile"][9]
    out = {
        "masse_finale": M_finale,
        "masse_theorique_F_sur_d": M_theorique,
        "ecart_masse_pct": 100 * (M_finale - M_theorique) / M_theorique,
        "mois_pour_95pct": int(np.argmax(np.array(hist["masse"]) >= 0.95 * M_theorique)) + 1,
        "soldes_menages": {
            "mediane": float(np.median(soldes)),
            "moyenne": float(soldes.mean()),
            "P90": float(np.percentile(soldes, 90)),
            "P95": float(np.percentile(soldes, 95)),
            "P99": float(np.percentile(soldes, 99)),
        },
        "part_fonte": {
            "menages_pct": 100 * hist["fonte_m"] / fonte_tot,
            "entreprises_pct": 100 * hist["fonte_e"] / fonte_tot,
            "etat_pct": 100 * hist["fonte_etat"] / fonte_tot,
            "top10pct_menages_dans_fonte_menages_pct":
                100 * fonte_top10 / hist["fonte_m"],
        },
    }
    return out


def scenario_choc(cfg):
    """Choc de -10 % de propension à consommer pendant 6 mois (mois 60-65),
    comparé au contrefactuel « monnaie fixe sans DIG ni fonte »."""
    res = {}
    for nom, avec in [("cubi", True), ("monnaie_fixe", False)]:
        base = SimulationCubi(cfg, avec_cubi=avec)
        hb = base.run()
        choc = SimulationCubi(cfg, avec_cubi=avec)
        hc = choc.run(choc_debut=60, choc_duree=6)
        cb = np.array(hb["conso"]); cc = np.array(hc["conso"])
        ref = cb[55:60].mean()
        creux = 100 * (cc[60:70].min() - ref) / ref
        res[nom] = {"creux_conso_pct": float(creux)}
    a, b = res["cubi"]["creux_conso_pct"], res["monnaie_fixe"]["creux_conso_pct"]
    res["attenuation_pct"] = float(100 * (1 - a / b)) if b != 0 else None
    return res


# ----------------------------------------------------------------------------
# Programme principal
# ----------------------------------------------------------------------------

def main():
    p = argparse.ArgumentParser(description="Simulation multi-agents Cubi (annexe A.4)")
    p.add_argument("--menages", type=int, default=1_000_000)
    p.add_argument("--mois", type=int, default=120)
    p.add_argument("--seed", type=int, default=2026)
    p.add_argument("--choc", action="store_true", help="scénario de choc + contrefactuel")
    args = p.parse_args()
    cfg = build_config(args)

    print(f"Simulation Cubi : {cfg['n_menages']:,} ménages, {cfg['n_entreprises']:,} "
          f"entreprises, {cfg['mois']} mois, V={cfg['V']:.0f}, d={cfg['d']:.0%}")
    sim = SimulationCubi(cfg, avec_cubi=True)
    hist = sim.run()
    out = resume(cfg, sim, hist)

    print(f"\n--- Masse monétaire ---")
    print(f"  finale {out['masse_finale']/1e9:,.2f} Mds ; théorique F(1-d)/d "
          f"{out['masse_theorique_F_sur_d']/1e9:,.2f} Mds ; écart {out['ecart_masse_pct']:+.3f} %")
    print(f"  95 % de la masse d'équilibre atteints au mois {out['mois_pour_95pct']}")
    s = out["soldes_menages"]
    print(f"\n--- Soldes des ménages (fin de simulation, après fonte) ---")
    print(f"  médiane {s['mediane']:,.0f} ; moyenne {s['moyenne']:,.0f} ; "
          f"P90 {s['P90']:,.0f} ; P95 {s['P95']:,.0f} ; P99 {s['P99']:,.0f}")
    f = out["part_fonte"]
    print(f"\n--- Qui paie la fonte (cumul) ---")
    print(f"  ménages {f['menages_pct']:.1f} % ; entreprises {f['entreprises_pct']:.1f} % ; "
          f"État {f['etat_pct']:.1f} %")
    print(f"  part du décile supérieur dans la fonte des ménages : "
          f"{f['top10pct_menages_dans_fonte_menages_pct']:.1f} %")

    if args.choc:
        print(f"\n--- Choc de demande (-10 % de mpc, 6 mois) ---")
        ch = scenario_choc(cfg)
        print(f"  creux de consommation : Cubi {ch['cubi']['creux_conso_pct']:.1f} % ; "
              f"monnaie fixe {ch['monnaie_fixe']['creux_conso_pct']:.1f} %")
        print(f"  atténuation du creux par Cubi : {ch['attenuation_pct']:.0f} %")
        out["choc"] = ch

    with open("resultats.json", "w", encoding="utf-8") as fjs:
        json.dump(out, fjs, ensure_ascii=False, indent=2)
    print("\nRésultats écrits dans resultats.json")

    try:
        import matplotlib
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        fig, ax = plt.subplots(1, 2, figsize=(11, 4))
        ax[0].plot(np.array(hist["masse"]) / 1e9)
        ax[0].axhline(out["masse_theorique_F_sur_d"] / 1e9, ls="--", c="r")
        ax[0].set_title("Masse monétaire (Mds) — convergence vers F(1-d)/d")
        ax[0].set_xlabel("mois")
        ax[1].hist(sim.solde_m, bins=150, range=(0, 15000))
        ax[1].axvline(cfg["V"] / cfg["d"], ls="--", c="r")
        ax[1].set_title("Soldes des ménages (pointillé : plafond d'indifférence V/d)")
        ax[1].set_xlabel("cubis")
        fig.tight_layout()
        fig.savefig("resultats.png", dpi=130)
        print("Graphiques écrits dans resultats.png")
    except ImportError:
        pass


if __name__ == "__main__":
    main()
