#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FRANCE 2027 — Moteur V2 (Phase 4) : effets de second ordre ENDOGÈNES
=====================================================================
Différences avec moteur.py (V1) :
1. Le boost de croissance n'est plus une hypothèse de scénario : il est CALCULÉ
   à partir des leviers via des multiplicateurs budgétaires, selon 3 écoles
   économiques (keynésienne / libérale / consensus).
   - Court terme : Δcroissance(t) = [mult_dep × Δimpulsion_dépenses
                                     − mult_rec × Δimpulsion_recettes] / PIB
     (impulsion = variation ANNUELLE des leviers, pas le cumul)
   - Long terme : les CAPEX, l'école et la simplification relèvent la croissance
     potentielle après délai (rampe 2031-2035), plafonnée par école.
2. Module retraites démographique (COR, rapport juin 2025) : les dépenses de
   retraite sont isolées et RIGIDES (elles ne baissent pas en crise) :
   retraites(t) = retraites(t-1) × (1+inflation) × (1+drift_volume_COR)
   La réforme des retraites vient en déduction.
3. Kill tests automatiques par configuration.
4. Comparateur de programmes (croissance implicite requise).

Usage :
  python3 moteur_v2.py                         # 3 écoles × 6 scénarios + kill tests
  python3 moteur_v2.py --comparateur FICHIER   # audite un programme tiers
"""
import json, csv, os, argparse, copy

ANNEES = list(range(2026, 2038))
ICI = os.path.dirname(os.path.abspath(__file__))

ECOLES = {
    "keynesienne": {
        "_doc": "Multiplicateurs dépenses élevés à court terme (OFCE haut de fourchette), faible effet d'offre des baisses d'impôts",
        "mult_depenses_ct": 1.3, "mult_recettes_ct": 0.4,
        "potentiel_lt_max": 0.15, "_potentiel": "CAPEX/école : +0,15 pt de potentiel max"
    },
    "liberale": {
        "_doc": "Effet d'offre fort des baisses de prélèvements (supply-side), multiplicateur dépenses faible (éviction)",
        "mult_depenses_ct": 0.5, "mult_recettes_ct": 0.9,
        "potentiel_lt_max": 0.35
    },
    "consensus": {
        "_doc": "Médiane FMI/littérature : dépenses ~0,9 ct, recettes ~0,6 ; potentiel +0,25",
        "mult_depenses_ct": 0.9, "mult_recettes_ct": 0.6,
        "potentiel_lt_max": 0.25
    }
}

# Module retraites — COR RA juin 2025 : retraités +0,8 %/an (2024-2030), +0,7 (2030-2050) ;
# pension moyenne ~stable en réel (indexation prix). Drift volume retenu :
DRIFT_RETRAITES = {2026: 0.8, 2027: 0.8, 2028: 0.8, 2029: 0.8, 2030: 0.8,
                   2031: 0.7, 2032: 0.7, 2033: 0.7, 2034: 0.7, "defaut": 0.7}
PART_RETRAITES_2025 = 0.14  # ~14 % du PIB (COR : 13,9 % en 2024)

# Spread de marché endogène : au-delà du seuil de dette, chaque point de dette/PIB
# supplémentaire renchérit l'OAT effective (sanction des marchés), plafonné.
SPREAD = {"seuil_dette_pct": 115.0, "par_point_pct": 0.03, "max_pct": 1.5}

# Module fonds de pension souverain (informatif, hors solde budgétaire), articulé aux PER :
# - amorçage 2030 : adossement d'une fraction des encours PER existants (~120 Mds fin 2024, en croissance)
# - collecte : fléchage épargne 0,3 % PIB (2030) montant à 0,8 % (2034+, pan obligatoire de la réforme)
# - rendement nominal 5 % (mix actions monde/émergents, doctrine Jean-Marc Daniel)
FONDS_PENSION = {"debut": 2030, "amorcage_per_mds": 30,
                 "collecte_pct_pib": {2030: 0.3, 2031: 0.4, 2032: 0.5, 2033: 0.65, "defaut": 0.8},
                 "rendement_pct": 5.0}

# ACCEPTABILITÉ SOCIALE — conflictualité par levier (0 = apaisant, 5 = explosif)
# Calibrée sur les précédents : retraites (1995, 2010, 2019, 2023), fonctionnaires, fiscalité conso.
CONFLICTUALITE = {
    "retraites_reforme": 5, "effectifs_500k": 4, "tva_sociale_compensation": 3,
    "rabot_niches_cibles": 3, "rabot_niches_offensif": 3, "simplification_etat": 3, "plafonnement_hauts_salaires": 3,
    "fraude_recouvrement": 0, "recentrage_allegements": 2,
    "impots_production_suppression": 2, "logement_simplification": 2, "sante_plus10pct": 2,
    "charges_bas_salaires": 1, "defense_lpm_plus": 1, "justice_police": 1,
    "capex_infrastructures": 1, "tourisme_grande_cause": 1, "recherche_superieur": 1,
    "ecole_moyens": 1, "culture_5mds": 0, "salaires_professeurs": 0,
    "cannabis_legalisation_taxee": 2, "logement_pour_tous": 1, "grand_age_fragiles": 0,
    "agriculture_transition": 1,
}
SEUIL_CHALEUR = 60   # au-delà : risque de coagulation des colères (référence Juppé 1995)

# Leviers à effet potentiel long terme (OFFRE) et leur poids — élargi V5 :
# simplification, CAPEX, école/profs, mais aussi compétitivité (impôts de production),
# R&D (recherche sup.), attractivité (plafonnement hauts salaires), mobilité (logement), emploi (bas salaires).
LEVIERS_POTENTIEL = {"capex_infrastructures": 0.45, "salaires_professeurs": 0.20,
                     "simplification_etat": 0.35, "impots_production_suppression": 0.30,
                     "recherche_superieur": 0.30, "plafonnement_hauts_salaires": 0.20,
                     "logement_simplification": 0.20, "charges_bas_salaires": 0.15}
RAMPE_POTENTIEL = {2031: 0.2, 2032: 0.4, 2033: 0.6, 2034: 0.8, "defaut": 1.0}
# Le boost de long terme devient PROPORTIONNEL à l'intensité des réformes (complétude), plafonné par l'école.
POTENTIEL_REFERENCE_MDS = 40.0  # somme pondérée des leviers d'offre du programme complet (ordre de grandeur)

# --- EFFETS CROISÉS (V5), versant DÉPENSES (le versant recettes est déjà dans l'élasticité × PIB) ---
OKUN = 0.4                       # 1 pt de croissance endogène cumulée -> -0,4 pt de chômage (loi d'Okun, bas de fourchette)
COUT_POINT_CHOMAGE_PCT_PIB = 0.30  # 1 pt de chômage en moins -> ~0,30 % de PIB de dépenses sociales en moins (indemnisation + minima + induit)
CHOMAGE_REDUCTION_MAX = 3.0      # plancher structurel : on ne descend pas le chômage de plus de 3 pts par la seule croissance
# Économies de prévention/transparence santé (props 29, 30, 124, 125) — montée lente, prudente
SANTE_PREVENTION_PCT_PIB = {2030: 0.02, 2031: 0.03, 2032: 0.05, 2033: 0.07, 2034: 0.09, 2035: 0.10, "defaut": 0.12}

def val(d, annee):
    if d is None: return 0.0
    k = str(annee) if str(annee) in d else (annee if annee in d else None)
    if k is not None: return float(d[k])
    cles = [int(x) for x in d if str(x).isdigit()]
    if cles and annee < min(cles): return 0.0
    return float(d.get("defaut", 0.0))

def simulate_v2(params, nom_scenario, ecole, leviers_override=None):
    sc = params["scenarios"][nom_scenario]
    base = params["base_2025"]; hyp = params["hypotheses_tendancielles"]
    leviers = leviers_override if leviers_override is not None else params["leviers_programme"]
    E = ECOLES[ecole]
    facteur = float(sc.get("facteur_leviers", 1.0))
    suspendus = sc.get("leviers_suspendus_depuis_2029", [])

    pib = base["pib_nominal_mds"]
    part_rec = base["recettes_mds"] / pib
    retraites = pib * PART_RETRAITES_2025
    dep_prim_autres = base["depenses_mds"] - base["charge_interets_mds"] - retraites
    dette = base["dette_mds"]
    taux = base["charge_interets_mds"] / dette
    prix = 1.0
    d29_prec = 0.0
    lev_rec_prec, lev_dep_prec = 0.0, 0.0
    fonds_actifs = 0.0
    cumul_boost = 0.0
    rows = []

    for an in ANNEES:
        vol = val(sc["croissance_volume_pct"], an) / 100
        infl = val(sc["inflation_pct"], an) / 100
        oat = val(sc["oat_pct"], an) / 100
        prix_n = prix * (1 + infl)

        # ---- leviers cumulés (Mds courants) + chaleur sociale de l'année ----
        lev_rec, lev_dep, d29, potentiel_brut, chaleur = 0.0, 0.0, 0.0, 0.0, 0.0
        for nom, lv in leviers.items():
            a_eff = 2028 if (nom in suspendus and an >= 2029) else an
            delta = val(lv["deltas_cumules"], a_eff) * facteur * prix_n
            delta_prec_lv = val(lv["deltas_cumules"], a_eff - 1) * facteur * prix
            chaleur += CONFLICTUALITE.get(nom, 2) * abs(delta - delta_prec_lv)
            if lv["type"] == "recettes": lev_rec += delta
            else: lev_dep += delta
            if nom == "impots_production_suppression": d29 = delta
            if nom in LEVIERS_POTENTIEL:
                potentiel_brut += abs(delta) * LEVIERS_POTENTIEL[nom]

        # retour fiscal conditionnalité D29 (décalage 1 an)
        lev_rec += -d29_prec * hyp.get("taux_retour_impots_production", 0.0)

        # ---- SECOND ORDRE COURT TERME : impulsion annuelle × multiplicateur ----
        imp_dep = (lev_dep - lev_dep_prec)            # >0 = relance dépenses
        imp_rec = (lev_rec - lev_rec_prec)            # <0 = baisse de prélèvements
        boost_ct = (E["mult_depenses_ct"] * imp_dep - E["mult_recettes_ct"] * imp_rec) / pib
        # ---- SECOND ORDRE LONG TERME : potentiel (rampe, plafonné par école) ----
        completude = min(1.0, potentiel_brut / POTENTIEL_REFERENCE_MDS)   # V5 : LT proportionnel à l'intensité des réformes
        boost_lt = E["potentiel_lt_max"] / 100 * completude * val(RAMPE_POTENTIEL, an) * (1 if facteur > 0 else 0)
        boost = max(-0.01, min(0.012, boost_ct)) + boost_lt   # bornes de prudence ±1pt ct

        pib = pib * (1 + vol + boost) * (1 + infl)
        prix = prix_n

        dep_exc = (val(sc.get("depenses_crise_mds"), an) + val(sc.get("defense_guerre_mds"), an)) * prix

        recettes = part_rec * pib * hyp["elasticite_recettes"] + lev_rec
        # ---- retraites rigides (module COR) ; la réforme est déjà dans lev_dep ----
        retraites = retraites * (1 + infl) * (1 + val(DRIFT_RETRAITES, an) / 100)
        derive_autres = (hyp["croissance_volume_depenses_primaires_pct"] - 0.1) / 100  # hors retraites
        dep_prim_autres = dep_prim_autres * (1 + infl) * (1 + derive_autres)

        # spread de marché endogène (sur la dette/PIB de l'année précédente)
        dette_pct_prec = dette / (pib / ((1 + vol + boost) * (1 + infl))) * 100
        spread = min(SPREAD["max_pct"], max(0.0, (dette_pct_prec - SPREAD["seuil_dette_pct"]) * SPREAD["par_point_pct"])) / 100
        taux = taux + hyp["part_refinancement_annuelle"] * (oat + spread - taux)
        interets = taux * dette

        # fonds de pension souverain articulé aux PER (hors solde — patrimoine fléché)
        if an == FONDS_PENSION["debut"]:
            fonds_actifs += FONDS_PENSION["amorcage_per_mds"]
        if an >= FONDS_PENSION["debut"]:
            fonds_actifs = fonds_actifs * (1 + FONDS_PENSION["rendement_pct"] / 100) \
                           + pib * val(FONDS_PENSION["collecte_pct_pib"], an) / 100
        depenses = retraites + dep_prim_autres + lev_dep + dep_exc + interets
        # --- EFFETS CROISÉS V5 : la croissance endogène nourrit l'emploi (-dépenses sociales) ; prévention santé ---
        # (le surcroît de TVA/IR/cotisations est déjà dans recettes via l'élasticité ; on n'ajoute QUE le versant dépenses)
        cumul_boost += max(0.0, boost)
        chomage_red = min(CHOMAGE_REDUCTION_MAX, OKUN * cumul_boost * 100)
        eco_chomage = chomage_red * COUT_POINT_CHOMAGE_PCT_PIB / 100 * pib * (1 if facteur > 0 else 0)
        eco_sante = val(SANTE_PREVENTION_PCT_PIB, an) / 100 * pib * (1 if facteur > 0 else 0)
        depenses = depenses - eco_chomage - eco_sante
        solde = recettes - depenses
        dette = dette - solde

        d29_prec, lev_rec_prec, lev_dep_prec = d29, lev_rec, lev_dep
        rows.append({"annee": an, "pib_mds": round(pib,1),
                     "croissance_totale_pct": round((vol+boost)*100,2),
                     "boost_endogene_pct": round(boost*100,2),
                     "recettes_pct_pib": round(recettes/pib*100,1),
                     "depenses_pct_pib": round(depenses/pib*100,1),
                     "retraites_pct_pib": round(retraites/pib*100,1),
                     "charge_interets_mds": round(interets,1),
                     "solde_pct_pib": round(solde/pib*100,1),
                     "dette_pct_pib": round(dette/pib*100,1),
                     "solde_mds": round(solde,1), "dette_mds": round(dette,1),
                     "spread_pct": round(spread*100,2),
                     "fonds_pension_actifs_mds": round(fonds_actifs,1),
                     "chaleur_sociale": round(chaleur,1)})
    return rows

# ---------------- KILL TESTS ----------------
def kill_tests(params, ecole, leviers=None):
    res = {}
    # KT1 — croissance additionnelle uniforme requise pour solde -1 % en 2032 (scénario réaliste)
    def solde_2032(extra):
        p = copy.deepcopy(params)
        for k in list(p["scenarios"]["realiste"]["croissance_volume_pct"]):
            if k != "_doc":
                p["scenarios"]["realiste"]["croissance_volume_pct"][k] = \
                    float(p["scenarios"]["realiste"]["croissance_volume_pct"][k]) + extra
        r = simulate_v2(p, "realiste", ecole, leviers)
        return next(x for x in r if x["annee"] == 2032)["solde_pct_pib"]
    lo, hi = 0.0, 6.0
    for _ in range(40):
        mid = (lo + hi) / 2
        if solde_2032(mid) < -1.0: lo = mid
        else: hi = mid
    res["croissance_additionnelle_requise_pt"] = round(hi, 2)
    base_g = 1.62
    res["croissance_implicite_totale_pct"] = round(base_g + hi, 2)

    # KT2 — OPEX durables : leviers dépenses (hors CAPEX/défense/école) positifs en 2037 ?
    lv = leviers if leviers is not None else params["leviers_programme"]
    opex = sum(val(l["deltas_cumules"], 2037) for n, l in lv.items()
               if l["type"] == "depenses" and n not in
               ("capex_infrastructures", "defense_lpm_plus", "salaires_professeurs"))
    res["opex_durables_2037_mds"] = round(opex, 1)
    res["kt_opex"] = "ÉCHEC (hausse durable des OPEX)" if opex > 5 else "OK"

    # KT3 — test 0,8 % : solde le plus dégradé en scénario crise
    r = simulate_v2(params, "crise_economique", ecole, leviers)
    pire = min(x["solde_pct_pib"] for x in r)
    res["pire_solde_en_crise_pct"] = pire
    res["kt_crise"] = "FRAGILE (solde < -7 %)" if pire < -7 else "DÉFENDABLE"

    # KT4 — acceptabilité : pic de chaleur sociale (coagulation des colères, réf. 1995)
    rr = simulate_v2(params, "realiste", ecole, leviers)
    pic = max(x["chaleur_sociale"] for x in rr)
    an_pic = next(x["annee"] for x in rr if x["chaleur_sociale"] == pic)
    res["pic_chaleur_sociale"] = pic
    res["annee_pic"] = an_pic
    res["kt_social"] = f"RISQUE DE COAGULATION en {an_pic} (pic {pic:.0f} > seuil {SEUIL_CHALEUR})" \
        if pic > SEUIL_CHALEUR else f"GÉRABLE (pic {pic:.0f} en {an_pic})"
    return res

# ---------------- COMPARATEUR DE PROGRAMMES ----------------
def _decoter(leviers, decote=0.5):
    """Mode prudence : les leviers marqués fiabilite='annonce' (recettes nouvelles
    et économies non documentées) sont décotés de 50 %. Les dépenses nouvelles et
    baisses d'impôts restent à 100 % (elles, on est sûr qu'elles coûteront)."""
    out = copy.deepcopy(leviers)
    n = 0
    for nom, lv in out.items():
        if lv.get("fiabilite") != "annonce":
            continue
        for k in lv["deltas_cumules"]:
            d = float(lv["deltas_cumules"][k])
            gain = (lv["type"] == "recettes" and d > 0) or (lv["type"] == "depenses" and d < 0)
            if gain:
                lv["deltas_cumules"][k] = d * decote
        n += 1
    return out, n

def comparer(params, fichier_programme):
    with open(fichier_programme) as f:
        prog = json.load(f)
    print(f"\n=== AUDIT : {prog['nom']} ===")
    print(prog.get("description", ""))
    jeux = [("AU CHIFFRAGE ANNONCÉ", prog["leviers"])]
    prudents, n = _decoter(prog["leviers"])
    if n:
        jeux.append((f"MODE PRUDENCE (décote 50 % sur {n} levier(s) non documenté(s))", prudents))
    res = {}
    for libelle, lv in jeux:
        print(f"\n──── {libelle} ────")
        for ecole in ECOLES:
            r = simulate_v2(params, "realiste", ecole, lv)
            r32 = next(x for x in r if x["annee"] == 2032)
            kt = kill_tests(params, ecole, lv)
            res[(libelle, ecole)] = (r32, kt)
            print(f"[{ecole:12s}] 2032 : solde {r32['solde_pct_pib']:5.1f} % | dette {r32['dette_pct_pib']:5.1f} % | "
                  f"croissance implicite {kt['croissance_implicite_totale_pct']} %/an | "
                  f"OPEX {kt['opex_durables_2037_mds']:+.0f} Mds ({kt['kt_opex']}) | crise {kt['pire_solde_en_crise_pct']} %")
    return res

def garant(rows):
    """Le moteur comme GARANT des contraintes fondatrices : dette < 100 %,
    deficit < 3 % (Maastricht) puis < 1 %, depenses <= 50 %, charge d'interets en inflexion."""
    by = {r["annee"]: r for r in rows}
    def first(cond):
        for r in rows:
            if r["annee"] >= 2027 and cond(r):
                return r["annee"]
        return None
    r37 = by[2037]
    d100 = first(lambda r: r["dette_pct_pib"] < 100)
    d95  = first(lambda r: r["dette_pct_pib"] <= 95)
    df3  = first(lambda r: r["solde_pct_pib"] >= -3.0)
    df1  = first(lambda r: r["solde_pct_pib"] >= -1.0)
    dp50 = first(lambda r: r["depenses_pct_pib"] <= 50.0)
    peak = max(rows, key=lambda r: r["charge_interets_mds"])
    inflexion = r37["charge_interets_mds"] < by[2036]["charge_interets_mds"]
    f = lambda y: (f"atteinte en {y}" if y else "NON atteinte d'ici 2037")
    lignes = [
        f"Dette < 100 % du PIB        : {f(d100)} (2037 : {r37['dette_pct_pib']} %)",
        f"  cible stricte 95 %        : {f(d95)}",
        f"Deficit < 3 % (Maastricht)  : {f(df3)} (2037 : {r37['solde_pct_pib']} %)",
        f"  cible 1 %                 : {f(df1)}",
        f"Depenses <= 50 % du PIB     : {f(dp50)} (2037 : {r37['depenses_pct_pib']} %)",
        f"Charge d'interets           : pic {peak['charge_interets_mds']} Mds en {peak['annee']}, "
        + ("en inflexion en fin de periode" if inflexion else "encore ascendante en 2037")
        + f" (2037 : {r37['charge_interets_mds']} Mds vs 64,7 en 2025)",
    ]
    tenues = sum(1 for x in (d100, df3, dp50) if x is not None) + (1 if inflexion else 0)
    verdict = f"VERDICT : {tenues}/4 contraintes fondatrices tenues d'ici 2037"
    return {"lignes": lignes, "verdict": verdict,
            "dette100": (f"atteinte en {d100}" if d100 else "NON (> 100 % en 2037)")}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--params", default=os.path.join(ICI, "parametres.json"))
    ap.add_argument("--out", default=os.path.join(ICI, "resultats_v2"))
    ap.add_argument("--comparateur", help="fichier JSON d'un programme tiers à auditer")
    args = ap.parse_args()
    with open(args.params) as f:
        params = json.load(f)

    if args.comparateur:
        comparer(params, args.comparateur); return

    os.makedirs(args.out, exist_ok=True)
    print(f"{'école':12s} {'scénario':26s} {'solde32':>8s} {'dette32':>8s} {'dép.32':>7s} {'boost32':>8s} {'dette37':>8s}")
    for ecole in ECOLES:
        for nom in params["scenarios"]:
            rows = simulate_v2(params, nom, ecole)
            with open(os.path.join(args.out, f"{ecole}_{nom}.csv"), "w", newline="") as f:
                w = csv.DictWriter(f, fieldnames=rows[0].keys()); w.writeheader(); w.writerows(rows)
            r32 = next(r for r in rows if r["annee"] == 2032); r37 = rows[-1]
            print(f"{ecole:12s} {nom:26s} {r32['solde_pct_pib']:8.1f} {r32['dette_pct_pib']:8.1f} "
                  f"{r32['depenses_pct_pib']:7.1f} {r32['boost_endogene_pct']:8.2f} {r37['dette_pct_pib']:8.1f}")
    print("\n--- KILL TESTS (programme Nash, scénario réaliste) ---")
    for ecole in ECOLES:
        kt = kill_tests(params, ecole)
        print(f"[{ecole}] croissance implicite requise : {kt['croissance_implicite_totale_pct']} %/an | "
              f"OPEX 2037 : {kt['opex_durables_2037_mds']} Mds ({kt['kt_opex']}) | "
              f"crise : {kt['pire_solde_en_crise_pct']} % ({kt['kt_crise']}) | "
              f"social : {kt['kt_social']}")

    # --- CONTRAINTES FONDATRICES : le moteur, garant des cibles de depart ---
    print("\n--- CONTRAINTES FONDATRICES (consensus, realiste) ---")
    g = garant(simulate_v2(params, "realiste", "consensus"))
    for line in g["lignes"]:
        print("  " + line)
    print("  >>> " + g["verdict"])
    print("  Dette < 100 % -- annee d'atteinte par scenario :")
    for nom in params["scenarios"]:
        gg = garant(simulate_v2(params, nom, "consensus"))
        print(f"    {nom:26s} : {gg['dette100']}")

if __name__ == "__main__":
    main()
