"""
Bibliothèque de graphes causaux — templates pré-peuplés et helpers.

Chaque template définit une chaîne causale entre un type d'événement de marché
et un ou plusieurs instruments (EURUSD, XAUUSD, SP500, BRENT...).

Structure graph_json :
  nodes[]       — nœuds avec position (x,y) pour le rendu SVG
  edges[]       — arêtes directionnelles entre nœuds
  coefficients  — valeurs heuristiques + calibrées
  instruments[] — instruments cibles du template
  input_mapping — comment extraire les inputs depuis un market_event
"""
import ast
import json
import logging
import operator
from typing import Optional

logger = logging.getLogger(__name__)


# ── Helpers de construction ────────────────────────────────────────────────────

def _n(id_, label, type_, x, y, formula=None, unit="", instrument=None, description=""):
    n = {"id": id_, "label": label, "type": type_, "x": x, "y": y}
    if formula:     n["formula"]     = formula
    if unit:        n["unit"]        = unit
    if instrument:  n["instrument"]  = instrument
    if description: n["description"] = description
    return n

def _e(from_, to_, style="solid", type_="causal", label=""):
    e = {"from": from_, "to": to_, "style": style, "type": type_}
    if label: e["label"] = label
    return e

def _c(value, description=""):
    return {"value": value, "calibrated": None, "description": description}


# ── Templates pré-peuplés ─────────────────────────────────────────────────────

BUILT_IN_TEMPLATES = [

  # ── 1. CPI US surprise ──────────────────────────────────────────────────────
  {
    "name": "CPI US — surprise inflationniste",
    "category": "macro_us",
    "sub_type": "CPI",
    "instruments": ["EURUSD", "XAUUSD"],
    "description": "Surprise inflationniste US → anticipations FED → taux longs → EUR/USD & Or",
    "ai_rationale": (
        "Un CPI US au-dessus du consensus renforce les anticipations de hausse FED, "
        "remonte le 10Y américain, élargit le spread US-EU et apprécie le dollar. "
        "L'or est pénalisé par la hausse des taux réels attendus."
    ),
    "graph_json": {
        "nodes": [
            _n("cpi_surprise",  "CPI US surprise",          "input",        200, 45,  unit="%"),
            _n("fed_fwd",       "Signal FED anticipations",  "intermediate", 200, 155, formula="cpi_surprise / 0.1 * {{coef_cpi_fwd}}"),
            _n("us_10y",        "Δ US 10Y",                  "intermediate", 200, 265, formula="fed_fwd * {{coef_fwd_10y}}", unit="%"),
            _n("eurusd",        "EUR/USD",                   "output",       110, 380, formula="-us_10y * {{coef_10y_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("xauusd",        "Or (XAU/USD)",              "output",       290, 380, formula="-fed_fwd * {{coef_fwd_gold}}", unit="$/oz", instrument="XAUUSD"),
        ],
        "edges": [
            _e("cpi_surprise", "fed_fwd",  "dashed", "fwd_guidance",  "anticipations hawkish"),
            _e("fed_fwd",      "us_10y",   "dashed", "expectations"),
            _e("us_10y",       "eurusd",   "solid",  "rate_diff"),
            _e("fed_fwd",      "xauusd",   "dashed", "real_yield"),
        ],
        "coefficients": {
            "coef_cpi_fwd":    _c(0.50,  "Impact de 0.1% de surprise CPI sur signal FED"),
            "coef_fwd_10y":    _c(0.070, "Signal FED → Δ rendement US 10Y (%)"),
            "coef_10y_eurusd": _c(200,   "1% de spread → EUR/USD (pips)"),
            "coef_fwd_gold":   _c(15.0,  "Unité signal FED → Or ($/oz)"),
        },
        "instruments": ["EURUSD", "XAUUSD"],
        "input_mapping": {
            "cpi_surprise": {"source": "surprise", "unit": "%", "description": "actual − consensus (%)"}
        },
    },
  },

  # ── 2. NFP surprise ─────────────────────────────────────────────────────────
  {
    "name": "NFP — surprise emploi non-agricole",
    "category": "macro_us",
    "sub_type": "NFP",
    "instruments": ["EURUSD", "SP500"],
    "description": "Surprise créations d'emploi → signal FED → 2Y + 10Y → EUR/USD & S&P",
    "ai_rationale": (
        "Un NFP supérieur aux attentes signale une économie robuste et renforce "
        "les anticipations restrictives FED. Le dollar s'apprécie via le canal des taux courts. "
        "L'impact sur le S&P500 est négatif net en contexte hawkish (taux > croissance)."
    ),
    "graph_json": {
        "nodes": [
            _n("nfp_surprise",  "NFP surprise",              "input",        200, 45,  unit="k"),
            _n("fed_fwd",       "Signal FED anticipations",  "intermediate", 200, 155, formula="nfp_surprise / 100 * {{coef_nfp_fwd}}"),
            _n("us_2y",         "Δ US 2Y",                   "intermediate", 110, 265, formula="fed_fwd * {{coef_fwd_2y}}", unit="%"),
            _n("us_10y",        "Δ US 10Y",                  "intermediate", 290, 265, formula="fed_fwd * {{coef_fwd_10y}}", unit="%"),
            _n("eurusd",        "EUR/USD",                   "output",       110, 380, formula="-(us_2y * {{coef_2y_fx}} + us_10y * {{coef_10y_fx}})", unit="pips", instrument="EURUSD"),
            _n("sp500",         "S&P 500",                   "output",       290, 380, formula="-fed_fwd * {{coef_fwd_sp}}", unit="pts", instrument="SP500"),
        ],
        "edges": [
            _e("nfp_surprise", "fed_fwd",  "dashed", "fwd_guidance"),
            _e("fed_fwd",      "us_2y",    "solid",  "rate_channel"),
            _e("fed_fwd",      "us_10y",   "dashed", "expectations"),
            _e("us_2y",        "eurusd",   "solid",  "rate_diff"),
            _e("us_10y",       "eurusd",   "dashed", "rate_diff"),
            _e("fed_fwd",      "sp500",    "dashed", "risk_asset"),
        ],
        "coefficients": {
            "coef_nfp_fwd":  _c(0.35,  "100k NFP → signal FED"),
            "coef_fwd_2y":   _c(0.030, "Signal FED → Δ US 2Y (%)"),
            "coef_fwd_10y":  _c(0.070, "Signal FED → Δ US 10Y (%)"),
            "coef_2y_fx":    _c(500,   "1% spread 2Y → EUR/USD (pips)"),
            "coef_10y_fx":   _c(200,   "1% spread 10Y → EUR/USD (pips)"),
            "coef_fwd_sp":   _c(20.0,  "Unité signal FED hawkish → S&P500 (pts, négatif)"),
        },
        "instruments": ["EURUSD", "SP500"],
        "input_mapping": {
            "nfp_surprise": {"source": "surprise", "unit": "k", "description": "actual − consensus (milliers)"}
        },
    },
  },

  # ── 3. FOMC décision de taux ─────────────────────────────────────────────────
  {
    "name": "FOMC — décision de taux + ton",
    "category": "macro_us",
    "sub_type": "FOMC",
    "instruments": ["EURUSD", "XAUUSD", "SP500"],
    "description": "Décision FED : surprise taux (bps) + ton du communiqué → 2 canaux → FX, Or, Actions",
    "ai_rationale": (
        "La décision FOMC active deux canaux distincts : le canal taux (fait accompli → ancre 2Y) "
        "et le canal ton/forward guidance (discours → anticipe trajectoire future → 10Y). "
        "L'or réagit principalement au canal réel (taux nominaux − inflation attendue)."
    ),
    "graph_json": {
        "nodes": [
            _n("rate_surprise_bps", "Surprise taux (bps)",       "input",        110, 45,  unit="bps", description="actual − expected en points de base"),
            _n("tone_score",        "Ton du communiqué",         "input",        290, 45,  unit="score", description="−3 très dovish … +3 très hawkish"),
            _n("fed_rate_p",        "Pression taux directeur",   "intermediate", 110, 165, formula="rate_surprise_bps / 25 * {{coef_rate_mult}}"),
            _n("fed_fwd",           "Signal anticipations FED",  "intermediate", 290, 165, formula="tone_score * {{coef_tone}}"),
            _n("us_2y",             "Δ US 2Y",                   "intermediate", 110, 285, formula="fed_rate_p * 0.085 + fed_fwd * 0.030", unit="%"),
            _n("us_10y",            "Δ US 10Y",                  "intermediate", 290, 285, formula="fed_rate_p * 0.035 + fed_fwd * {{coef_fwd_10y}}", unit="%"),
            _n("eurusd",            "EUR/USD",                   "output",       110, 400, formula="-(us_2y * 500 + us_10y * 200)", unit="pips", instrument="EURUSD"),
            _n("xauusd",            "Or (XAU/USD)",              "output",       290, 400, formula="-(us_10y * {{coef_10y_gold}} + fed_fwd * {{coef_tone_gold}})", unit="$/oz", instrument="XAUUSD"),
        ],
        "edges": [
            _e("rate_surprise_bps", "fed_rate_p", "solid",  "rate_channel"),
            _e("tone_score",        "fed_fwd",    "dashed", "fwd_guidance"),
            _e("fed_rate_p", "us_2y",   "solid",  "rate_anchor",  "canal taux → 2Y"),
            _e("fed_fwd",    "us_2y",   "dashed", "bleed"),
            _e("fed_rate_p", "us_10y",  "solid",  "level_shift"),
            _e("fed_fwd",    "us_10y",  "dashed", "expectations", "canal ton → 10Y"),
            _e("us_2y",      "eurusd",  "solid",  "rate_diff"),
            _e("us_10y",     "eurusd",  "dashed", "rate_diff"),
            _e("us_10y",     "xauusd",  "solid",  "real_yield"),
            _e("fed_fwd",    "xauusd",  "dashed", "expectations"),
        ],
        "coefficients": {
            "coef_rate_mult":  _c(1.0,  "Multiplicateur surprise taux"),
            "coef_tone":       _c(1.2,  "Ton → signal anticipations"),
            "coef_fwd_10y":    _c(0.070,"Signal ton → Δ US 10Y (%)"),
            "coef_10y_gold":   _c(30.0, "1% hausse US 10Y → Or ($/oz, négatif)"),
            "coef_tone_gold":  _c(10.0, "Signal ton hawkish → Or ($/oz, négatif)"),
        },
        "instruments": ["EURUSD", "XAUUSD", "SP500"],
        "input_mapping": {
            "rate_surprise_bps": {"source": "surprise_bps", "unit": "bps"},
            "tone_score":        {"source": "user_input",   "range": [-3, 3]},
        },
    },
  },

  # ── 4. ECB décision de taux ──────────────────────────────────────────────────
  {
    "name": "ECB — décision de taux + ton",
    "category": "macro_eu",
    "sub_type": "ECB",
    "instruments": ["EURUSD"],
    "description": "Décision BCE : surprise taux + ton → taux EU → spread → EUR/USD",
    "ai_rationale": (
        "La BCE active le même schéma dual-canal que la FED mais dans le sens inverse pour EUR/USD. "
        "Une BCE hawkish surprise élargit les spreads courts EU, réduit le spread US-EU et apprécie l'euro. "
        "L'effet sur le 10Y Bund est amplifié par le canal forward guidance."
    ),
    "graph_json": {
        "nodes": [
            _n("ecb_rate_bps", "Surprise taux ECB (bps)",   "input",        110, 45,  unit="bps"),
            _n("ecb_tone",     "Ton du communiqué BCE",     "input",        290, 45,  unit="score", description="−3 dovish … +3 hawkish"),
            _n("ecb_rate_p",   "Pression taux BCE",         "intermediate", 110, 165, formula="ecb_rate_bps / 25 * {{coef_ecb_mult}}"),
            _n("ecb_fwd",      "Signal anticipations BCE",  "intermediate", 290, 165, formula="ecb_tone * {{coef_ecb_tone}}"),
            _n("eu_2y",        "Δ Bund 2Y",                 "intermediate", 110, 285, formula="ecb_rate_p * 0.080 + ecb_fwd * 0.025", unit="%"),
            _n("eu_10y",       "Δ Bund 10Y",                "intermediate", 290, 285, formula="ecb_rate_p * 0.030 + ecb_fwd * {{coef_ecb_fwd_10y}}", unit="%"),
            _n("eurusd",       "EUR/USD",                   "output",       200, 400, formula="eu_2y * 500 + eu_10y * 200", unit="pips", instrument="EURUSD"),
        ],
        "edges": [
            _e("ecb_rate_bps", "ecb_rate_p", "solid",  "rate_channel"),
            _e("ecb_tone",     "ecb_fwd",    "dashed", "fwd_guidance"),
            _e("ecb_rate_p",   "eu_2y",      "solid",  "rate_anchor"),
            _e("ecb_fwd",      "eu_2y",      "dashed", "bleed"),
            _e("ecb_rate_p",   "eu_10y",     "solid",  "level_shift"),
            _e("ecb_fwd",      "eu_10y",     "dashed", "expectations"),
            _e("eu_2y",        "eurusd",     "solid",  "spread_narrow"),
            _e("eu_10y",       "eurusd",     "dashed", "spread_narrow"),
        ],
        "coefficients": {
            "coef_ecb_mult":    _c(1.0,  "Multiplicateur surprise taux BCE"),
            "coef_ecb_tone":    _c(1.2,  "Ton BCE → signal anticipations"),
            "coef_ecb_fwd_10y": _c(0.060,"Signal ton BCE → Δ Bund 10Y (%)"),
        },
        "instruments": ["EURUSD"],
        "input_mapping": {
            "ecb_rate_bps": {"source": "surprise_bps", "unit": "bps"},
            "ecb_tone":     {"source": "user_input",   "range": [-3, 3]},
        },
    },
  },

  # ── 5. Escalade géopolitique Europe ──────────────────────────────────────────
  {
    "name": "Escalade géopolitique — Europe",
    "category": "geopolitical",
    "sub_type": "Geopolitical",
    "instruments": ["EURUSD", "XAUUSD", "BRENT"],
    "description": "Conflit / escalade en Europe → risk-off + choc énergie → EUR/USD, Or, Pétrole",
    "ai_rationale": (
        "Un événement géopolitique européen active deux canaux simultanés : "
        "l'aversion au risque (VIX → USD safe haven → EUR baisse) et le choc énergétique "
        "(disruption approvisionnement → pétrole monte → or monte, coût importations EU → EUR faiblit)."
    ),
    "graph_json": {
        "nodes": [
            _n("severity",       "Sévérité (1−10)",          "input",        200, 45,  unit="score", description="1=faible, 10=crise majeure"),
            _n("risk_aversion",  "Aversion au risque",       "intermediate", 110, 155, formula="severity * {{coef_sev_risk}}"),
            _n("energy_shock",   "Choc énergie EU",          "intermediate", 290, 155, formula="severity * {{coef_sev_energy}}"),
            _n("vix_delta",      "Δ VIX",                    "intermediate", 110, 265, formula="risk_aversion * {{coef_risk_vix}}"),
            _n("oil_delta",      "Δ Pétrole (%)",            "intermediate", 290, 265, formula="energy_shock * {{coef_energy_oil}}"),
            _n("eurusd",         "EUR/USD",                  "output",       80,  390, formula="-(vix_delta * {{coef_vix_fx}} + energy_shock * {{coef_energy_fx}})", unit="pips", instrument="EURUSD"),
            _n("xauusd",         "Or (XAU/USD)",             "output",       200, 390, formula="vix_delta * {{coef_vix_gold}} + oil_delta * {{coef_oil_gold}}", unit="$/oz", instrument="XAUUSD"),
            _n("brent",          "Brent (USD/bbl)",          "output",       320, 390, formula="oil_delta * {{coef_oil_brent}}", unit="$/bbl", instrument="BRENT"),
        ],
        "edges": [
            _e("severity",      "risk_aversion",  "solid",  "transmission",   "perception risque"),
            _e("severity",      "energy_shock",   "solid",  "supply_chain",   "disruption énergie"),
            _e("risk_aversion", "vix_delta",      "solid",  "fear_gauge"),
            _e("energy_shock",  "oil_delta",      "solid",  "supply_shock"),
            _e("vix_delta",     "eurusd",         "solid",  "safe_haven",     "fuite vers USD"),
            _e("energy_shock",  "eurusd",         "solid",  "trade_balance",  "coût imports EU"),
            _e("vix_delta",     "xauusd",         "dashed", "safe_haven",     "fuite vers or"),
            _e("oil_delta",     "xauusd",         "dashed", "inflation_hedge"),
            _e("oil_delta",     "brent",          "solid",  "direct"),
        ],
        "coefficients": {
            "coef_sev_risk":    _c(1.5,  "Sévérité → aversion au risque"),
            "coef_sev_energy":  _c(0.8,  "Sévérité → choc énergie"),
            "coef_risk_vix":    _c(3.0,  "Aversion risque → Δ VIX points"),
            "coef_energy_oil":  _c(2.0,  "Choc énergie → % hausse pétrole"),
            "coef_vix_fx":      _c(4.0,  "1 pt VIX → EUR/USD pips (négatif)"),
            "coef_energy_fx":   _c(8.0,  "Choc énergie → EUR/USD pips (négatif)"),
            "coef_vix_gold":    _c(5.0,  "1 pt VIX → Or $/oz (positif)"),
            "coef_oil_gold":    _c(3.0,  "1% oil → Or $/oz"),
            "coef_oil_brent":   _c(5.0,  "Choc énergie → Brent $/bbl"),
        },
        "instruments": ["EURUSD", "XAUUSD", "BRENT"],
        "input_mapping": {
            "severity": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]}
        },
    },
  },

  # ── 6. Choc offre pétrole ────────────────────────────────────────────────────
  {
    "name": "Choc offre pétrole — OPEC / disruption",
    "category": "commodity",
    "sub_type": "Oil",
    "instruments": ["BRENT", "EURUSD", "XAUUSD"],
    "description": "Choc d'offre pétrolière → prix → inflation EU → trade balance → EUR",
    "ai_rationale": (
        "Un choc d'offre haussier sur le pétrole impacte directement le Brent. "
        "L'Europe, importateur net, voit ses coûts d'importation augmenter (trade balance → EUR négatif), "
        "et ses anticipations d'inflation croître (BCE contrainte entre lutte anti-inflation et croissance)."
    ),
    "graph_json": {
        "nodes": [
            _n("oil_change_pct",   "Choc prix pétrole (%)",   "input",        200, 45,  unit="%"),
            _n("brent_move",       "Δ Brent ($/bbl)",         "intermediate", 110, 155, formula="oil_change_pct * {{coef_oil_brent}}"),
            _n("eu_import_cost",   "Coût imports EU",         "intermediate", 290, 155, formula="oil_change_pct * {{coef_oil_import}}"),
            _n("eu_inflation",     "Anticipations inflation EU","intermediate",290, 265, formula="eu_import_cost * {{coef_import_inf}}"),
            _n("eurusd",           "EUR/USD",                 "output",       110, 380, formula="-(eu_import_cost * {{coef_import_fx}} + eu_inflation * {{coef_inf_fx}})", unit="pips", instrument="EURUSD"),
            _n("xauusd",           "Or (XAU/USD)",            "output",       290, 380, formula="brent_move * {{coef_oil_gold}}", unit="$/oz", instrument="XAUUSD"),
        ],
        "edges": [
            _e("oil_change_pct", "brent_move",    "solid",  "direct"),
            _e("oil_change_pct", "eu_import_cost","solid",  "trade_balance"),
            _e("eu_import_cost", "eu_inflation",  "dashed", "cost_push"),
            _e("eu_import_cost", "eurusd",        "solid",  "trade_balance",  "déficit commercial EU"),
            _e("eu_inflation",   "eurusd",        "dashed", "policy_ambiguity","BCE coincé"),
            _e("brent_move",     "xauusd",        "dashed", "inflation_hedge"),
        ],
        "coefficients": {
            "coef_oil_brent":  _c(1.0,  "% hausse oil → Δ Brent $/bbl (linéaire à calibrer)"),
            "coef_oil_import": _c(0.5,  "% hausse oil → pression imports EU (score)"),
            "coef_import_inf": _c(0.3,  "Pression imports → anticipations inflation EU"),
            "coef_import_fx":  _c(10.0, "Pression imports → EUR/USD pips (négatif)"),
            "coef_inf_fx":     _c(5.0,  "Inflation EU → EUR/USD (ambiguë, négatif net)"),
            "coef_oil_gold":   _c(0.8,  "1$/bbl Brent → Or $/oz"),
        },
        "instruments": ["BRENT", "EURUSD", "XAUUSD"],
        "input_mapping": {
            "oil_change_pct": {"source": "impact_score_pct", "unit": "%"}
        },
    },
  },

  # ── 7. COT repositionnement institutionnel ───────────────────────────────────
  {
    "name": "COT — repositionnement institutionnel EUR",
    "category": "report",
    "sub_type": "COT",
    "instruments": ["EURUSD"],
    "description": "Variation nette des positions institutionnelles EUR → signal momentum → EUR/USD",
    "ai_rationale": (
        "Le rapport COT (Commitment of Traders) révèle les positions nettes des fonds spéculatifs sur EUR. "
        "Un changement significatif des positions nettes agit comme signal de momentum, "
        "les marchés interprétant un repositionnement institutionnel comme un signal directionnel."
    ),
    "graph_json": {
        "nodes": [
            _n("net_longs_delta",  "Δ Positions nettes EUR (k contrats)", "input",        200, 45,  unit="k contracts"),
            _n("momentum_signal",  "Signal momentum institutionnel",       "intermediate", 200, 185, formula="net_longs_delta * {{coef_cot_momentum}}"),
            _n("eurusd",           "EUR/USD",                             "output",       200, 330, formula="momentum_signal * {{coef_momentum_fx}}", unit="pips", instrument="EURUSD"),
        ],
        "edges": [
            _e("net_longs_delta", "momentum_signal", "solid",  "positioning"),
            _e("momentum_signal", "eurusd",          "solid",  "momentum"),
        ],
        "coefficients": {
            "coef_cot_momentum":  _c(0.5, "k contrats → signal momentum"),
            "coef_momentum_fx":   _c(2.0, "Signal momentum → EUR/USD pips"),
        },
        "instruments": ["EURUSD"],
        "input_mapping": {
            "net_longs_delta": {"source": "actual_value", "unit": "k contracts"}
        },
    },
  },

  # ── 8. EIA stocks pétrole ────────────────────────────────────────────────────
  {
    "name": "EIA — stocks pétroliers hebdomadaires",
    "category": "report",
    "sub_type": "EIA",
    "instruments": ["BRENT", "EURUSD"],
    "description": "Surprise stocks EIA → prix pétrole → canal inflation / risk appetite → EUR/USD",
    "ai_rationale": (
        "Une surprise négative sur les stocks EIA (stocks < consensus) signale une demande forte "
        "ou une offre réduite, ce qui fait monter le pétrole. Impact EUR/USD indirect via "
        "le canal inflation EU (coût imports) et le risk appetite."
    ),
    "graph_json": {
        "nodes": [
            _n("eia_surprise_mb",  "Surprise EIA (Mb vs consensus)", "input",        200, 45,  unit="Mb", description="négatif = stocks inférieurs aux attentes"),
            _n("oil_reaction",     "Réaction pétrole (%)",           "intermediate", 200, 165, formula="-eia_surprise_mb * {{coef_eia_oil}}"),
            _n("brent",            "Δ Brent ($/bbl)",                "output",       110, 290, formula="oil_reaction * {{coef_oil_dollar}}", unit="$/bbl", instrument="BRENT"),
            _n("eurusd",           "EUR/USD",                        "output",       290, 290, formula="-oil_reaction * {{coef_oil_eurusd}}", unit="pips", instrument="EURUSD"),
        ],
        "edges": [
            _e("eia_surprise_mb", "oil_reaction", "solid",  "supply_demand",  "stocks bas → oil monte"),
            _e("oil_reaction",    "brent",        "solid",  "direct"),
            _e("oil_reaction",    "eurusd",       "dashed", "trade_balance",  "coût imports EU"),
        ],
        "coefficients": {
            "coef_eia_oil":    _c(0.5,  "1 Mb sous consensus → % hausse pétrole"),
            "coef_oil_dollar": _c(1.0,  "% hausse oil → Δ Brent $/bbl"),
            "coef_oil_eurusd": _c(3.0,  "% hausse oil → EUR/USD pips (négatif pour EUR)"),
        },
        "instruments": ["BRENT", "EURUSD"],
        "input_mapping": {
            "eia_surprise_mb": {"source": "surprise", "unit": "Mb"}
        },
    },
  },

  # ── 9. Risk-off global ───────────────────────────────────────────────────────
  {
    "name": "Risk-off global — fuite vers la sécurité",
    "category": "sentiment",
    "sub_type": "Risk",
    "instruments": ["EURUSD", "XAUUSD", "SP500"],
    "description": "Choc d'aversion au risque → flight-to-safety → USD & Or montent, actions baissent",
    "ai_rationale": (
        "Un épisode de risk-off global (crash, crise financière, choc exogène) provoque "
        "une fuite vers les actifs refuge : USD (liquidity premium), Or (store of value), "
        "et une sortie des actifs risqués (actions, EM, EUR). Le VIX s'envole et amplifie la réaction."
    ),
    "graph_json": {
        "nodes": [
            _n("risk_score",     "Score risk-off (1−10)",     "input",        200, 45,  unit="score", description="1=légère tension, 10=crise majeure"),
            _n("vix_spike",      "Δ VIX",                    "intermediate", 110, 165, formula="risk_score * {{coef_risk_vix}}"),
            _n("gold_demand",    "Demande actifs refuge",    "intermediate", 290, 165, formula="risk_score * {{coef_risk_refuge}}"),
            _n("usd_safe",       "USD safe-haven",           "intermediate", 110, 285, formula="vix_spike * {{coef_vix_usd}}"),
            _n("eurusd",         "EUR/USD",                  "output",       80,  400, formula="-usd_safe * {{coef_usd_eurusd}}", unit="pips", instrument="EURUSD"),
            _n("xauusd",         "Or (XAU/USD)",             "output",       200, 400, formula="gold_demand * {{coef_refuge_gold}}", unit="$/oz", instrument="XAUUSD"),
            _n("sp500",          "S&P 500",                  "output",       320, 400, formula="-vix_spike * {{coef_vix_sp}}", unit="pts", instrument="SP500"),
        ],
        "edges": [
            _e("risk_score",  "vix_spike",   "solid",  "fear_gauge"),
            _e("risk_score",  "gold_demand", "solid",  "safe_haven"),
            _e("vix_spike",   "usd_safe",    "solid",  "liquidity",   "fuite liquidité USD"),
            _e("usd_safe",    "eurusd",      "solid",  "fx_safe_haven"),
            _e("gold_demand", "xauusd",      "solid",  "store_value"),
            _e("vix_spike",   "sp500",       "solid",  "risk_asset",  "risk premium"),
        ],
        "coefficients": {
            "coef_risk_vix":    _c(4.0,  "Score risk → Δ VIX pts"),
            "coef_risk_refuge": _c(2.0,  "Score risk → demande refuges (score)"),
            "coef_vix_usd":     _c(0.8,  "Δ VIX → signal USD safe-haven"),
            "coef_usd_eurusd":  _c(20.0, "Signal USD → EUR/USD pips (négatif)"),
            "coef_refuge_gold": _c(15.0, "Demande refuge → Or $/oz"),
            "coef_vix_sp":      _c(12.0, "1 pt VIX → S&P 500 pts (négatif)"),
        },
        "instruments": ["EURUSD", "XAUUSD", "SP500"],
        "input_mapping": {
            "risk_score": {"source": "impact_score_scaled", "unit": "score", "range": [1, 10]}
        },
    },
  },

  # ── 10. Tensions commerciales / tarifs ──────────────────────────────────────
  {
    "name": "Tarifs douaniers — tensions commerciales",
    "category": "geopolitical",
    "sub_type": "Trade",
    "instruments": ["EURUSD", "SP500"],
    "description": "Annonce tarifs → choc exportations EU + risk-off → EUR/USD & actions",
    "ai_rationale": (
        "Les tarifs douaniers impactent l'EUR/USD via deux canaux : "
        "le choc direct sur les exportations européennes (BCE plus dovish = EUR baisse) "
        "et le canal risk-off global (incertitude → fuite USD → EUR baisse, S&P corrige)."
    ),
    "graph_json": {
        "nodes": [
            _n("tariff_pct",     "Tarifs annoncés (%)",       "input",        200, 45,  unit="%"),
            _n("eu_export_shock","Choc exportations EU",      "intermediate", 110, 165, formula="tariff_pct * {{coef_tariff_export}}"),
            _n("risk_off_signal","Signal risk-off",           "intermediate", 290, 165, formula="tariff_pct * {{coef_tariff_risk}}"),
            _n("ecb_dovish",     "Pression BCE dovish",       "intermediate", 110, 285, formula="eu_export_shock * {{coef_export_ecb}}"),
            _n("eurusd",         "EUR/USD",                   "output",       110, 400, formula="-(ecb_dovish * {{coef_ecb_fx}} + risk_off_signal * {{coef_risk_fx}})", unit="pips", instrument="EURUSD"),
            _n("sp500",          "S&P 500",                   "output",       290, 400, formula="-risk_off_signal * {{coef_risk_sp}}", unit="pts", instrument="SP500"),
        ],
        "edges": [
            _e("tariff_pct",      "eu_export_shock", "solid",  "trade_impact"),
            _e("tariff_pct",      "risk_off_signal", "dashed", "uncertainty"),
            _e("eu_export_shock", "ecb_dovish",      "solid",  "growth_slowdown"),
            _e("ecb_dovish",      "eurusd",          "solid",  "rate_differential"),
            _e("risk_off_signal", "eurusd",          "dashed", "safe_haven"),
            _e("risk_off_signal", "sp500",           "solid",  "risk_asset"),
        ],
        "coefficients": {
            "coef_tariff_export": _c(1.5,  "% tarif → pression exports EU"),
            "coef_tariff_risk":   _c(1.0,  "% tarif → signal risk-off"),
            "coef_export_ecb":    _c(0.5,  "Choc exports → pression dovish BCE"),
            "coef_ecb_fx":        _c(15.0, "Pression BCE dovish → EUR/USD pips (négatif)"),
            "coef_risk_fx":       _c(10.0, "Risk-off → EUR/USD pips (négatif)"),
            "coef_risk_sp":       _c(25.0, "Risk-off → S&P 500 pts (négatif)"),
        },
        "instruments": ["EURUSD", "SP500"],
        "input_mapping": {
            "tariff_pct": {"source": "impact_score_pct", "unit": "%"}
        },
    },
  },
]


# ── DB Helpers ─────────────────────────────────────────────────────────────────

def init_tables(conn):
    conn.execute("""
        CREATE TABLE IF NOT EXISTS causal_graph_templates (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            name            TEXT NOT NULL UNIQUE,
            category        TEXT NOT NULL,
            sub_type        TEXT,
            instruments     TEXT NOT NULL DEFAULT '[]',
            description     TEXT DEFAULT '',
            graph_json      TEXT NOT NULL,
            ai_rationale    TEXT DEFAULT '',
            calibration_json TEXT DEFAULT '{}',
            created_by      TEXT DEFAULT 'system',
            heuristic_ver   INTEGER DEFAULT 1,
            created_at      TEXT DEFAULT (datetime('now')),
            updated_at      TEXT DEFAULT (datetime('now'))
        )
    """)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS causal_event_analyses (
            id                 INTEGER PRIMARY KEY AUTOINCREMENT,
            market_event_id    INTEGER,
            template_id        INTEGER,
            instrument         TEXT NOT NULL DEFAULT 'EURUSD',
            inputs_json        TEXT DEFAULT '{}',
            override_params    TEXT DEFAULT '{}',
            prediction_json    TEXT,
            actual_json        TEXT,
            activation_score   REAL,
            drift_json         TEXT,
            ai_recommendation  TEXT,
            analyzed_at        TEXT DEFAULT (datetime('now')),
            FOREIGN KEY (template_id) REFERENCES causal_graph_templates(id)
        )
    """)
    conn.commit()


def seed_templates(conn):
    """Insère les templates built-in s'ils n'existent pas encore."""
    for t in BUILT_IN_TEMPLATES:
        existing = conn.execute(
            "SELECT id FROM causal_graph_templates WHERE name = ?", (t["name"],)
        ).fetchone()
        if existing:
            continue
        conn.execute("""
            INSERT INTO causal_graph_templates
                (name, category, sub_type, instruments, description, graph_json, ai_rationale, created_by)
            VALUES (?, ?, ?, ?, ?, ?, ?, 'system')
        """, (
            t["name"],
            t["category"],
            t.get("sub_type", ""),
            json.dumps(t.get("instruments", [])),
            t.get("description", ""),
            json.dumps(t["graph_json"]),
            t.get("ai_rationale", ""),
        ))
    conn.commit()


def get_templates(conn, category: str = "") -> list:
    where = f"WHERE category = '{category}'" if category else ""
    rows = conn.execute(
        f"SELECT * FROM causal_graph_templates {where} ORDER BY category, name"
    ).fetchall()
    result = []
    for r in rows:
        d = dict(r)
        d["graph_json"]       = json.loads(d["graph_json"] or "{}")
        d["instruments"]      = json.loads(d["instruments"] or "[]")
        d["calibration_json"] = json.loads(d["calibration_json"] or "{}")
        result.append(d)
    return result


def get_template(conn, template_id: int) -> Optional[dict]:
    row = conn.execute(
        "SELECT * FROM causal_graph_templates WHERE id = ?", (template_id,)
    ).fetchone()
    if not row:
        return None
    d = dict(row)
    d["graph_json"]       = json.loads(d["graph_json"] or "{}")
    d["instruments"]      = json.loads(d["instruments"] or "[]")
    d["calibration_json"] = json.loads(d["calibration_json"] or "{}")
    return d


def update_coefficients(conn, template_id: int, coef_updates: dict):
    tmpl = get_template(conn, template_id)
    if not tmpl:
        return False
    graph = tmpl["graph_json"]
    for k, v in coef_updates.items():
        if k in graph.get("coefficients", {}):
            graph["coefficients"][k]["value"] = float(v)
    conn.execute(
        "UPDATE causal_graph_templates SET graph_json = ?, updated_at = datetime('now') WHERE id = ?",
        (json.dumps(graph), template_id)
    )
    conn.commit()
    return True


def update_calibration(conn, template_id: int, analysis_result: dict):
    """Met à jour les stats de calibration après une nouvelle analyse."""
    tmpl = get_template(conn, template_id)
    if not tmpl:
        return
    calib = tmpl.get("calibration_json") or {}
    n = calib.get("n_events", 0) + 1
    # Running average of activation score
    prev_act = calib.get("avg_activation", 0) or 0
    new_act  = analysis_result.get("activation_score") or 0
    # Running average of pred / actual pips
    instrument = analysis_result.get("instrument", "EURUSD")
    pred_pips  = analysis_result.get("pred_pips", 0) or 0
    act_pips   = analysis_result.get("actual_pips", 0) or 0

    calib["n_events"]       = n
    calib["avg_activation"] = round((prev_act * (n - 1) + new_act) / n, 3)
    calib["last_analyzed"]  = analysis_result.get("analyzed_at", "")

    if instrument not in calib:
        calib[instrument] = {"n": 0, "avg_pred": 0, "avg_actual": 0}
    ci = calib[instrument]
    ni = ci["n"] + 1
    ci["n"]          = ni
    ci["avg_pred"]   = round((ci["avg_pred"]   * (ni - 1) + pred_pips) / ni, 1)
    ci["avg_actual"] = round((ci["avg_actual"] * (ni - 1) + act_pips)  / ni, 1)
    if ci["avg_pred"] != 0:
        ci["coef_ratio"] = round(ci["avg_actual"] / ci["avg_pred"], 2)

    conn.execute(
        "UPDATE causal_graph_templates SET calibration_json = ?, updated_at = datetime('now') WHERE id = ?",
        (json.dumps(calib), template_id)
    )
    conn.commit()


# ── Évaluateur de graphe ───────────────────────────────────────────────────────

def _safe_eval(expr: str, context: dict) -> float:
    """Évalue une expression arithmétique simple avec des variables."""
    _ops = {
        ast.Add:  operator.add,
        ast.Sub:  operator.sub,
        ast.Mult: operator.mul,
        ast.Div:  operator.truediv,
        ast.USub: operator.neg,
    }

    def _eval(node):
        if isinstance(node, ast.Constant):
            return float(node.value)
        if isinstance(node, ast.Name):
            if node.id not in context:
                raise ValueError(f"Variable inconnue : {node.id}")
            return float(context[node.id])
        if isinstance(node, ast.BinOp):
            op = _ops.get(type(node.op))
            if op is None:
                raise ValueError(f"Opérateur non supporté : {type(node.op)}")
            return op(_eval(node.left), _eval(node.right))
        if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
            return -_eval(node.operand)
        raise ValueError(f"Nœud AST non supporté : {type(node)}")

    tree = ast.parse(expr.strip(), mode="eval")
    return _eval(tree.body)


def evaluate_graph(graph_json: dict, inputs: dict, coef_overrides: Optional[dict] = None) -> dict:
    """
    Évalue un graphe causal depuis les inputs, retourne dict {node_id: valeur}.
    Les {{coef_xxx}} sont substitués depuis graph_json["coefficients"] (ou overrides).
    """
    coefs = {k: v["value"] for k, v in graph_json.get("coefficients", {}).items()}
    if coef_overrides:
        coefs.update({k: float(v) for k, v in coef_overrides.items()})

    def sub(formula: str) -> str:
        for k, v in coefs.items():
            formula = formula.replace(f"{{{{{k}}}}}", str(v))
        return formula

    values = dict(inputs)
    nodes  = graph_json.get("nodes", [])
    # Évaluation en ordre topologique (max N passes)
    for _ in range(len(nodes) + 2):
        changed = False
        for node in nodes:
            if node["id"] in values or not node.get("formula"):
                continue
            try:
                result = _safe_eval(sub(node["formula"]), values)
                values[node["id"]] = round(result, 4)
                changed = True
            except Exception:
                pass
        if not changed:
            break

    return values
