import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
  Sparkles, RefreshCw, ChevronDown, TrendingUp, TrendingDown,
  Minus, BarChart2, Clock, Calendar, AlertCircle,
} from 'lucide-react'
import axios from 'axios'
import clsx from 'clsx'
import InstrumentChart, { TheoPoint } from '../components/InstrumentChart'

const api = axios.create({ baseURL: '/api' })

// Stable empty array — prevents InstrumentChart useEffect from re-running on every render
const NO_CHART_EVENTS: never[] = []

// ── Types ─────────────────────────────────────────────────────────────────────

interface CausalTemplate {
  id: number; name: string; category: string; sub_type: string
  instruments: string[]; description: string
  graph_json: {
    nodes: { id: string; type: string; label: string; instrument?: string }[]
    edges: { from: string; to: string; lag_days?: number; lag_min?: number; sign?: string }[]
  }
}

interface InstrumentConfig {
  id: string; name: string; yf_ticker: string; category: string; currency: string
  description: string
  regime_labels: string[]
  chart: { ma_periods: number[]; show_volume: boolean }
  correlation_instruments: string[]
}

interface PriceCandle { time: string; open: number; high: number; low: number; close: number; volume: number }
interface LinePoint    { time: string; value: number }

interface SnapshotEvent {
  id?: number; template_id?: number | null
  analyzed_instruments?: string | null   // comma-sep: "EURUSD,SP500" — set when causal analysis exists
  date: string; end_date: string | null; title: string; level: string
  category: string; sub_type?: string; description: string; impact_score: number
  expected_value?: string | null; actual_value?: string | null
  surprise_pct?: number | null; unit?: string | null; absorption_pct?: number | null
  prediction_json?: string | null   // JSON: {node_id: pips} from causal_event_analyses
  actual_json?: string | null       // JSON: {instrument: pips} from causal_event_analyses
  activation_score?: number | null  // stored directional accuracy from causal_event_analyses
}

interface RegimeSignals {
  ma50_above_ma200: boolean | null; ma50_slope_pct: number; ma200_slope_pct: number
  momentum_20d_pct: number; dist_ma200_pct: number; vol_ratio_pct: number
}

interface TrendMetrics {
  ma50_slope_5d: number; ma200_slope_20d: number; rsi14_current: number
  atr14_current: number; atr_vs_3m_avg_pct: number; momentum_1m_pct: number
  momentum_3m_pct: number; dist_ma50_pct: number | null; dist_ma200_pct: number | null
  current_price: number; high_52w: number; low_52w: number
}

interface MacroRegime {
  dominant: string
  label: string
  color: string
  emoji: string
  scores: Record<string, number>
  ranked: string[]
  asset_bias: Record<string, any>
}

interface GaugeValue {
  id: string; label: string; value: number | null; change_pct: number | null
  unit: string; bloc: string; note?: string
}

interface MacroGaugeSnap {
  snapshot_date: string
  dominant: string
  regime_scores: Record<string, number>
  gauges: Record<string, GaugeValue>
}

interface Snapshot {
  instrument: InstrumentConfig
  price_data: PriceCandle[]
  indicators: Record<string, LinePoint[]>
  regime: { current: string; confidence: number; scores: Record<string, number>; signals: RegimeSignals }
  macro_regime: MacroRegime
  trend: TrendMetrics
  events: SnapshotEvent[]
  current_price: number; change_pct: number; change_abs: number; period: string
}

// ── Helpers ───────────────────────────────────────────────────────────────────

const CATEGORY_ORDER = ['equity_index', 'equity_intl', 'metal', 'energy', 'bond', 'credit', 'fx', 'volatility', 'stock', 'crypto']
const CATEGORY_LABELS: Record<string, string> = {
  equity_index: 'Indices US', equity_intl: 'Intl Equity', metal: 'Métaux',
  energy: 'Énergie', bond: 'Obligataire', credit: 'Crédit',
  fx: 'Forex', volatility: 'Volatilité', stock: 'Actions', crypto: 'Crypto',
}

function regimeColor(label: string): string {
  const l = label.toLowerCase()
  if (/bull|expan|recov|rally|strength|risk.on|inflow|demand|falling|weakness/i.test(l)) return 'emerald'
  if (/bear|crash|crunch|shock|squeeze|fear|risk.off|selloff|crisis|deficit/i.test(l)) return 'red'
  if (/volatil|spike|stress/i.test(l)) return 'orange'
  if (/range|neutral|consolid|compress/i.test(l)) return 'slate'
  return 'blue'
}

function pctColor(v: number | null, inverse = false): string {
  if (v === null || v === undefined) return 'text-slate-400'
  const pos = inverse ? v < 0 : v > 0
  const neg = inverse ? v > 0 : v < 0
  return pos ? 'text-emerald-400' : neg ? 'text-red-400' : 'text-slate-400'
}

function Arrow({ v }: { v: number }) {
  if (v > 0.3) return <TrendingUp className="w-3.5 h-3.5 text-emerald-400" />
  if (v < -0.3) return <TrendingDown className="w-3.5 h-3.5 text-red-400" />
  return <Minus className="w-3.5 h-3.5 text-slate-500" />
}

function fmt(v: number | null, digits = 2): string {
  if (v === null || v === undefined) return '—'
  return (v >= 0 ? '+' : '') + v.toFixed(digits)
}

function fmtDateFR(s: string | null): string {
  if (!s) return '—'
  const [y, m, d] = s.split('-')
  return `${d}/${m}/${y}`
}

function pctN(a: number | undefined, b: number | undefined): number {
  if (a === undefined || b === undefined || b === 0) return 0
  return ((a - b) / b) * 100
}

function isActiveAt(ev: SnapshotEvent, selectedDate: string | null): boolean {
  if (!selectedDate) return false
  if (ev.date > selectedDate) return false
  if (ev.end_date) return ev.end_date >= selectedDate
  const diffDays = (new Date(selectedDate).getTime() - new Date(ev.date).getTime()) / 86400000
  return diffDays <= 30
}

// ── Causal graph config ───────────────────────────────────────────────────────

// Maps snapshot event category → causal template category
// market_events.category → causal_graph_templates.category
// event_calendar & fundamental cover both US and EU; geopolitical/report/sentiment map 1-to-1
const EVENT_TO_CAUSAL_CAT: Record<string, string[]> = {
  event_calendar: ['macro_us', 'macro_eu'],
  fundamental:    ['macro_us', 'macro_eu'],
  geopolitical:   ['geopolitical'],
  report:         ['report'],
  sentiment:      ['sentiment'],
  commodity:      ['commodity'],
  technical:      [],
}

// Maps instrument dashboard category → causal lab instrument keys
const CAT_TO_CAUSAL_INST: Record<string, string[]> = {
  equity_index: ['SP500'],
  equity_intl:  ['SP500'],
  metal:        ['XAUUSD'],
  energy:       ['BRENT'],
  fx:           ['EURUSD'],
  bond:         ['US10Y', 'EU10Y'],
  credit:       ['US10Y'],
  volatility:   ['SP500'],
  stock:        ['SP500'],
  crypto:       [],
}

// Keyed by market_event.category — same palette as the ★ stars on the chart
const EV_CAT_TW: Record<string, string> = {
  event_calendar: 'text-amber-400 border-amber-700/40 bg-amber-900/20',
  geopolitical:   'text-red-400 border-red-700/40 bg-red-900/20',
  fundamental:    'text-emerald-400 border-emerald-700/40 bg-emerald-900/20',
  report:         'text-blue-400 border-blue-700/40 bg-blue-900/20',
  sentiment:      'text-violet-400 border-violet-700/40 bg-violet-900/20',
  technical:      'text-cyan-400 border-cyan-700/40 bg-cyan-900/20',
}

// ── Macro regime colour mapping ───────────────────────────────────────────────

const MACRO_COLOR_MAP: Record<string, string> = {
  goldilocks:       'emerald',
  desinflation:     'cyan',
  soft_landing:     'blue',
  reflation:        'amber',
  stagflation:      'orange',
  inflation_shock:  'red',
  recession:        'red',
  crise_liquidite:  'red',
  incertain:        'slate',
}

function macroRegimeColor(dominant: string): string {
  return MACRO_COLOR_MAP[dominant] ?? 'slate'
}

// ── RegimeCard ────────────────────────────────────────────────────────────────

function RegimeCard({
  regime, macroRegime, signalsAt, dateLabel,
}: {
  regime: Snapshot['regime']
  macroRegime: MacroRegime | null
  signalsAt: RegimeSignals | null
  dateLabel: string
}) {
  const signals = signalsAt ?? regime.signals
  const col = regimeColor(regime.current)

  const borderMap: Record<string, string> = {
    emerald: 'border-emerald-700/40 bg-emerald-950/30',
    red:     'border-red-700/40 bg-red-950/30',
    orange:  'border-orange-700/40 bg-orange-950/30',
    slate:   'border-slate-700/40 bg-slate-900/30',
    blue:    'border-blue-700/40 bg-blue-950/30',
    cyan:    'border-cyan-700/40 bg-cyan-950/30',
    amber:   'border-amber-700/40 bg-amber-950/30',
  }

  const ma50Color = signals.ma50_above_ma200 ? 'text-emerald-400' : 'text-red-400'
  const volColor  = (signals.vol_ratio_pct ?? 0) > 130 ? 'text-orange-400' : (signals.vol_ratio_pct ?? 0) < 70 ? 'text-cyan-400' : 'text-slate-300'

  const metrics: { label: string; value: string; sub?: string; color: string }[] = [
    {
      label: 'MA50 / MA200',
      value: signals.ma50_above_ma200 === true ? 'Au-dessus' : signals.ma50_above_ma200 === false ? 'En-dessous' : '—',
      sub: signals.ma50_above_ma200 === true ? '↑ Golden cross' : signals.ma50_above_ma200 === false ? '↓ Death cross' : '',
      color: ma50Color,
    },
    { label: 'Slope MA50 (10j)',  value: fmt(signals.ma50_slope_pct) + '%',  color: pctColor(signals.ma50_slope_pct) },
    { label: 'Slope MA200 (10j)', value: fmt(signals.ma200_slope_pct) + '%', color: pctColor(signals.ma200_slope_pct) },
    { label: 'Momentum 20j',      value: fmt(signals.momentum_20d_pct) + '%', color: pctColor(signals.momentum_20d_pct) },
    {
      label: 'Distance MA200',
      value: fmt(signals.dist_ma200_pct) + '%',
      sub: signals.dist_ma200_pct > 10 ? 'Surextension' : signals.dist_ma200_pct < -10 ? 'Survendu' : 'Neutre',
      color: pctColor(signals.dist_ma200_pct),
    },
    {
      label: 'Volatilité ATR',
      value: (signals.vol_ratio_pct ?? 0).toFixed(0) + '%',
      sub: (signals.vol_ratio_pct ?? 0) > 130 ? 'Élevée' : (signals.vol_ratio_pct ?? 0) < 70 ? 'Comprimée' : 'Normale',
      color: volColor,
    },
  ]

  const macroCol = macroRegime ? macroRegimeColor(macroRegime.dominant) : 'slate'
  const top3Macro = macroRegime?.ranked?.slice(0, 3) ?? []

  return (
    <div className={clsx('rounded-xl border p-4 space-y-3', borderMap[col])}>
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2">
          <BarChart2 className={clsx('w-4 h-4', `text-${col}-400`)} />
          <span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Régime</span>
        </div>
        <span className="text-xs text-slate-600">{dateLabel}</span>
      </div>

      {macroRegime && macroRegime.dominant !== 'incertain' && (
        <div className="rounded-lg border border-slate-700/30 bg-dark-700/40 px-3 py-2 space-y-1.5">
          <div className="flex items-center justify-between">
            <span className="text-xs text-slate-500 uppercase tracking-wide">Cycle macro global</span>
            <span className="text-xs text-slate-600">Fed cycle</span>
          </div>
          <div className="flex items-center gap-2">
            <span className="text-base">{macroRegime.emoji || '🌍'}</span>
            <span className={clsx('text-sm font-bold', `text-${macroCol}-300`)}>{macroRegime.label || macroRegime.dominant}</span>
          </div>
          {top3Macro.length > 0 && (
            <div className="flex flex-wrap gap-1 pt-0.5">
              {top3Macro.map((key, i) => {
                const score = macroRegime.scores?.[key]
                const c = macroRegimeColor(key)
                return (
                  <span key={key} className={clsx(
                    'text-xs px-1.5 py-0.5 rounded border',
                    i === 0 ? `text-${c}-300 bg-${c}-900/30 border-${c}-700/40` : 'text-slate-500 bg-dark-800/40 border-slate-700/30'
                  )}>
                    {key} {score !== undefined ? Math.round(score) + '%' : ''}
                  </span>
                )
              })}
            </div>
          )}
        </div>
      )}

      <div className={clsx('rounded-lg px-3 py-2', `bg-${col}-900/20`)}>
        <div className="text-xs text-slate-500 mb-0.5">Régime technique</div>
        <div className={clsx('text-sm font-bold leading-tight', `text-${col}-300`)}>{regime.current}</div>
        <div className="flex items-center gap-2 mt-1">
          <div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
            <div className={clsx('h-full rounded-full', `bg-${col}-500`)}
              style={{ width: `${Math.round(regime.confidence * 100)}%` }} />
          </div>
          <span className="text-xs text-slate-500">{Math.round(regime.confidence * 100)}%</span>
        </div>
      </div>

      <div className="grid grid-cols-2 gap-2">
        {metrics.map(m => (
          <div key={m.label} className="rounded-lg bg-dark-700/50 px-2.5 py-2">
            <div className="text-xs text-slate-600 mb-0.5 truncate">{m.label}</div>
            <div className={clsx('text-sm font-bold', m.color)}>{m.value}</div>
            {m.sub && <div className="text-xs text-slate-600 mt-0.5">{m.sub}</div>}
          </div>
        ))}
      </div>
    </div>
  )
}

// ── TrendCard ─────────────────────────────────────────────────────────────────

function TrendCard({ trend, dateLabel }: { trend: TrendMetrics; dateLabel: string }) {
  const rsi = trend.rsi14_current ?? 50
  const rsiColor = rsi > 70 ? 'text-orange-400' : rsi < 30 ? 'text-cyan-400' : 'text-slate-300'
  const rsiZone  = rsi > 70 ? 'Suracheté' : rsi < 30 ? 'Survendu' : 'Neutre'

  const items = [
    { group: 'Tendance', rows: [
      { label: 'Slope MA50 (5j)',   value: fmt(trend.ma50_slope_5d) + '%',   arrow: trend.ma50_slope_5d   },
      { label: 'Slope MA200 (20j)', value: fmt(trend.ma200_slope_20d) + '%', arrow: trend.ma200_slope_20d },
      { label: 'Distance MA50',     value: trend.dist_ma50_pct  != null ? fmt(trend.dist_ma50_pct)  + '%' : '—', arrow: trend.dist_ma50_pct  ?? 0 },
      { label: 'Distance MA200',    value: trend.dist_ma200_pct != null ? fmt(trend.dist_ma200_pct) + '%' : '—', arrow: trend.dist_ma200_pct ?? 0, bold: true },
    ]},
    { group: 'Momentum', rows: [
      { label: 'Momentum 1M', value: fmt(trend.momentum_1m_pct) + '%', arrow: trend.momentum_1m_pct },
      { label: 'Momentum 3M', value: fmt(trend.momentum_3m_pct) + '%', arrow: trend.momentum_3m_pct, bold: true },
    ]},
  ]

  const pct52w = trend.high_52w && trend.low_52w && (trend.high_52w - trend.low_52w) > 0
    ? ((trend.current_price - trend.low_52w) / (trend.high_52w - trend.low_52w)) * 100 : null

  return (
    <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4 space-y-3">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2">
          <TrendingUp className="w-4 h-4 text-blue-400" />
          <span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Indicateurs de Tendance</span>
        </div>
        <span className="text-xs text-slate-600">{dateLabel}</span>
      </div>

      <div className="flex items-center justify-between bg-dark-700/40 rounded-lg px-3 py-1.5">
        <span className="text-xs text-slate-500">Prix</span>
        <span className="text-sm font-bold text-white">
          {trend.current_price?.toLocaleString('fr-FR', { maximumFractionDigits: 4 })}
        </span>
      </div>

      {items.map(group => (
        <div key={group.group}>
          <div className="text-xs text-slate-600 uppercase tracking-wide mb-1.5">{group.group}</div>
          {group.rows.map(row => (
            <div key={row.label} className="flex items-center justify-between text-xs py-0.5">
              <span className="text-slate-500">{row.label}</span>
              <span className={clsx('flex items-center gap-1', (row as any).bold ? 'font-semibold' : '', pctColor(row.arrow ?? 0))}>
                <Arrow v={row.arrow ?? 0} />{row.value}
              </span>
            </div>
          ))}
        </div>
      ))}

      <div className="border-t border-slate-700/30 pt-3">
        <div className="flex items-center justify-between text-xs mb-1.5">
          <span className="text-slate-500">RSI(14)</span>
          <span className={clsx('font-semibold', rsiColor)}>{rsi.toFixed(0)} — {rsiZone}</span>
        </div>
        <div className="relative h-2 bg-slate-800 rounded-full overflow-hidden">
          <div className="absolute left-0 top-0 h-full w-[30%] bg-cyan-900/50" />
          <div className="absolute right-0 top-0 h-full w-[30%] bg-orange-900/50" />
          <div className="absolute top-0 h-full w-2 bg-white rounded-full transition-all"
            style={{ left: `calc(${Math.min(98, rsi)}% - 4px)` }} />
        </div>
        <div className="flex justify-between text-xs text-slate-600 mt-0.5">
          <span>0 Survendu</span><span>50</span><span>Suracheté 100</span>
        </div>
      </div>

      {pct52w !== null && (
        <div>
          <div className="flex items-center justify-between text-xs mb-1">
            <span className="text-slate-500">Range 52 semaines</span>
            <span className="text-slate-400">{Math.round(pct52w)}e percentile</span>
          </div>
          <div className="relative h-2 bg-slate-800 rounded-full overflow-hidden">
            <div className="absolute top-0 h-full bg-blue-500/60 rounded-full" style={{ width: `${pct52w}%` }} />
          </div>
          <div className="flex justify-between text-xs text-slate-600 mt-0.5">
            <span>{trend.low_52w?.toFixed(2)}</span><span>{trend.high_52w?.toFixed(2)}</span>
          </div>
        </div>
      )}

      <div className="flex items-center justify-between text-xs">
        <span className="text-slate-500">ATR(14) vs moy. 3M</span>
        <span className={clsx((trend.atr_vs_3m_avg_pct ?? 100) > 130 ? 'text-orange-400' : (trend.atr_vs_3m_avg_pct ?? 100) < 70 ? 'text-cyan-400' : 'text-slate-400')}>
          {(trend.atr_vs_3m_avg_pct ?? 100).toFixed(0)}%
        </span>
      </div>
    </div>
  )
}

// ── EventsCard ────────────────────────────────────────────────────────────────

function EventsCard({ events, selectedDate }: { events: SnapshotEvent[]; selectedDate: string | null }) {
  const navigate = useNavigate()
  const LEVEL_C: Record<string, string> = {
    long:   'text-violet-400 bg-violet-900/30 border-violet-700/30',
    medium: 'text-blue-400 bg-blue-900/30 border-blue-700/30',
    short:  'text-emerald-400 bg-emerald-900/30 border-emerald-700/30',
  }

  function isActiveAt(ev: SnapshotEvent): boolean {
    if (!selectedDate) return false
    const evStart = ev.date
    const evEnd   = ev.end_date
    if (evStart > selectedDate) return false
    if (evEnd) return evEnd >= selectedDate
    // point event: active if within 30 days after
    const diffDays = (new Date(selectedDate).getTime() - new Date(evStart).getTime()) / 86400000
    return diffDays <= 30
  }

  function surpriseColor(v: number | null | undefined): string {
    if (v === null || v === undefined) return 'text-slate-400'
    if (v > 5) return 'text-emerald-400'
    if (v < -5) return 'text-red-400'
    return 'text-slate-400'
  }

  return (
    <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4 space-y-3">
      <div className="flex items-center gap-2">
        <Calendar className="w-4 h-4 text-amber-400" />
        <span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Événements Macro</span>
      </div>
      {events.length === 0 ? (
        <div className="text-xs text-slate-600 italic py-2">Aucun événement lié trouvé</div>
      ) : (
        <div className="space-y-2 max-h-[380px] overflow-y-auto pr-1">
          {events.map((ev, i) => {
            const active = isActiveAt(ev)
            return (
              <div key={i}
                className={clsx(
                  'rounded-lg border p-2 cursor-pointer transition-colors',
                  active
                    ? 'border-amber-600/50 bg-amber-900/20 hover:bg-amber-900/30'
                    : 'border-slate-700/30 bg-dark-700/40 hover:bg-dark-700/70'
                )}
                onClick={() => navigate(`/timeline?date=${ev.date}`)}
              >
                <div className="flex items-start justify-between gap-2">
                  <div className="flex items-center gap-1.5 min-w-0">
                    {active && <span className="shrink-0 w-1.5 h-1.5 rounded-full bg-amber-400 mt-0.5" />}
                    <span className="text-xs font-medium text-slate-300 leading-tight truncate">{ev.title}</span>
                  </div>
                  <div className="flex items-center gap-1 shrink-0">
                    {ev.sub_type && (
                      <span className="text-xs px-1 py-0 rounded bg-slate-700/50 text-slate-400 border border-slate-600/30"
                        style={{ fontSize: 8, paddingTop: 1, paddingBottom: 1 }}>
                        {ev.sub_type}
                      </span>
                    )}
                    <span className={clsx('text-xs px-1.5 py-0.5 rounded border', LEVEL_C[ev.level] ?? 'text-slate-400 bg-slate-800 border-slate-700/30')}>
                      {ev.level === 'long' ? 'LT' : ev.level === 'medium' ? 'MT' : 'CT'}
                    </span>
                  </div>
                </div>

                {/* Eco calendar fields */}
                {(ev.expected_value || ev.actual_value) && (
                  <div className="flex items-center gap-3 mt-1.5 text-xs">
                    {ev.expected_value && (
                      <span className="text-slate-500">Att. <span className="text-slate-300">{ev.expected_value}</span></span>
                    )}
                    {ev.actual_value && (
                      <span className="text-slate-500">Réel <span className="font-semibold text-white">{ev.actual_value}</span></span>
                    )}
                    {ev.surprise_pct !== null && ev.surprise_pct !== undefined && (
                      <span className={clsx('font-semibold', surpriseColor(ev.surprise_pct))}>
                        {ev.surprise_pct > 0 ? '+' : ''}{ev.surprise_pct.toFixed(0)}%
                        {ev.surprise_pct > 5 ? ' ↑' : ev.surprise_pct < -5 ? ' ↓' : ''}
                      </span>
                    )}
                    {ev.unit && <span className="text-slate-600">{ev.unit}</span>}
                  </div>
                )}

                {ev.absorption_pct !== null && ev.absorption_pct !== undefined && (
                  <div className="mt-1.5">
                    <div className="flex items-center justify-between text-xs text-slate-600 mb-0.5">
                      <span>Absorption marché</span><span>{ev.absorption_pct}%</span>
                    </div>
                    <div className="h-1 bg-slate-800 rounded-full overflow-hidden">
                      <div className="h-full bg-amber-600/60 rounded-full" style={{ width: `${ev.absorption_pct}%` }} />
                    </div>
                  </div>
                )}

                <div className="flex items-center gap-2 mt-1 text-xs text-slate-500">
                  <Clock className="w-3 h-3" />
                  {fmtDateFR(ev.date)}{ev.end_date ? ` → ${fmtDateFR(ev.end_date)}` : ''}
                  {ev.impact_score > 0.7 && <AlertCircle className="w-3 h-3 text-orange-400" />}
                </div>
                {ev.description && <p className="text-xs text-slate-600 mt-1 line-clamp-1">{ev.description}</p>}
              </div>
            )
          })}
        </div>
      )}
    </div>
  )
}

// ── NarrativeCard ─────────────────────────────────────────────────────────────

function NarrativeCard({ narrative, loading, onLoad, instrument }: {
  narrative: string; loading: boolean; onLoad: () => void; instrument: InstrumentConfig
}) {
  return (
    <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
      <div className="flex items-center justify-between mb-3">
        <div className="flex items-center gap-2">
          <Sparkles className="w-4 h-4 text-violet-400" />
          <span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Narration IA — {instrument.name}</span>
        </div>
        <button onClick={onLoad} disabled={loading}
          className="flex items-center gap-1.5 text-xs px-3 py-1.5 bg-violet-800/30 hover:bg-violet-800/50 text-violet-300 border border-violet-700/40 rounded-lg transition-colors disabled:opacity-40"
        >
          {loading ? <RefreshCw className="w-3 h-3 animate-spin" /> : <Sparkles className="w-3 h-3" />}
          {loading ? 'Génération...' : narrative ? 'Actualiser' : 'Générer'}
        </button>
      </div>
      {narrative ? (
        <p className="text-sm text-slate-300 leading-relaxed">{narrative}</p>
      ) : loading ? (
        <div className="space-y-2">
          {[95, 88, 70].map(w => <div key={w} className="h-3 bg-slate-800 rounded animate-pulse" style={{ width: `${w}%` }} />)}
        </div>
      ) : (
        <div className="text-sm text-slate-600 italic">
          Cliquez "Générer" pour une analyse IA pour {instrument.name}.
        </div>
      )}
    </div>
  )
}

// ── Macro gauge helpers ───────────────────────────────────────────────────────

const SCENARIO_META: Record<string, { label: string; emoji: string; color: string }> = {
  goldilocks:       { label: 'Goldilocks',             emoji: '🟢', color: '#10b981' },
  desinflation:     { label: 'Désinflation',           emoji: '🔵', color: '#3b82f6' },
  soft_landing:     { label: 'Soft Landing',           emoji: '🔷', color: '#06b6d4' },
  reflation:        { label: 'Reflation',              emoji: '🟠', color: '#f97316' },
  stagflation:      { label: 'Stagflation',            emoji: '🟡', color: '#f59e0b' },
  inflation_shock:  { label: 'Choc Inflationniste',    emoji: '🔥', color: '#dc2626' },
  recession:        { label: 'Récession',              emoji: '🔴', color: '#ef4444' },
  crise_liquidite:  { label: 'Crise de liquidité',     emoji: '🟣', color: '#7c3aed' },
  incertain:        { label: 'Incertain',              emoji: '⬜', color: '#64748b' },
}

function snapToMacroRegime(snap: MacroGaugeSnap): MacroRegime {
  const scores = snap.regime_scores ?? {}
  const meta   = SCENARIO_META[snap.dominant] ?? SCENARIO_META.incertain
  const ranked = Object.entries(scores)
    .sort(([, a], [, b]) => b - a)
    .map(([k]) => k)
  return {
    dominant:   snap.dominant,
    label:      meta.label,
    color:      meta.color,
    emoji:      meta.emoji,
    scores,
    ranked,
    asset_bias: {},
  }
}

const BLOC_LABELS: Record<string, string> = {
  liquidite:  'Liquidité / Taux',
  credit:     'Crédit / Vol',
  energie:    'Énergie',
  metaux:     'Métaux',
  croissance: 'Croissance US',
  secteurs:   'Secteurs',
  volatilite: 'Volatilité surf.',
  global:     'Global / EM',
  forex_ro:   'Forex Risk-Off',
  derive:     'Dérivés',
}

function MacroGaugePanel({ snap, dateLabel }: { snap: MacroGaugeSnap; dateLabel: string }) {
  const byBloc: Record<string, GaugeValue[]> = {}
  for (const g of Object.values(snap.gauges)) {
    if (g.value === null && g.change_pct === null) continue
    const b = g.bloc ?? 'derive'
    if (!byBloc[b]) byBloc[b] = []
    byBloc[b].push(g)
  }
  const meta = SCENARIO_META[snap.dominant] ?? SCENARIO_META.incertain

  return (
    <div className="bg-dark-800/60 rounded-xl border border-slate-700/30 p-4 space-y-3">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2">
          <span className="text-base">{meta.emoji}</span>
          <span className="text-sm font-semibold text-white">{meta.label}</span>
          <span className="text-xs text-slate-500">— contexte macro au {dateLabel}</span>
        </div>
        <span className="text-xs text-slate-600">{snap.snapshot_date}</span>
      </div>

      {/* Regime scores mini bar */}
      <div className="flex gap-1 h-1.5">
        {Object.entries(snap.regime_scores)
          .sort(([,a],[,b]) => b - a)
          .slice(0, 6)
          .map(([k, v]) => (
            <div key={k} title={`${SCENARIO_META[k]?.label ?? k}: ${(v*100).toFixed(0)}%`}
              style={{ width: `${v*100}%`, background: SCENARIO_META[k]?.color ?? '#64748b' }}
              className="rounded-full transition-all"
            />
          ))}
      </div>

      {/* Gauges by bloc */}
      <div className="grid grid-cols-2 gap-x-6 gap-y-3">
        {Object.entries(byBloc).map(([bloc, gauges]) => (
          <div key={bloc}>
            <div className="text-xs text-slate-600 uppercase tracking-wide mb-1">{BLOC_LABELS[bloc] ?? bloc}</div>
            <div className="space-y-0.5">
              {gauges.map(g => (
                <div key={g.id} className="flex items-center justify-between text-xs">
                  <span className="text-slate-400 truncate max-w-[120px]" title={g.label}>{g.label}</span>
                  <div className="flex items-center gap-2">
                    {g.value !== null && (
                      <span className="text-white font-mono">
                        {g.unit === '%' ? g.value.toFixed(2) + '%'
                          : g.unit === 'pts' ? g.value.toFixed(1)
                          : g.unit === 'ratio' ? g.value.toFixed(3)
                          : g.value.toFixed(2)}
                      </span>
                    )}
                    {g.change_pct !== null && g.change_pct !== undefined && (
                      <span className={clsx('font-mono', g.change_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
                        {g.change_pct >= 0 ? '+' : ''}{g.change_pct.toFixed(1)}%
                      </span>
                    )}
                  </div>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

// ── Shared: trading-day-index x-coordinate ────────────────────────────────────
// Uses the same time scale as LightweightCharts (trading days, weekends skipped).
// For a date D, snaps to the nearest available trading day index.
function makeTdToX(priceData: PriceCandle[], width: number): (d: string) => number {
  const dates = priceData.map(c => c.time)
  const N = dates.length
  if (N < 2) return () => 0
  function snap(d: string): number {
    if (d <= dates[0]) return 0
    if (d >= dates[N - 1]) return N - 1
    let lo = 0, hi = N - 1
    while (lo < hi) {
      const mid = (lo + hi) >> 1
      if (dates[mid] < d) lo = mid + 1
      else hi = mid
    }
    return lo
  }
  return (d: string) => (snap(d) / (N - 1)) * width
}

// ── Comprehension scoring ─────────────────────────────────────────────────────

function templateAbsorptionDays(tmpl: CausalTemplate): number {
  const maxLag = Math.max(0, ...tmpl.graph_json.edges.map(e => e.lag_days || 0))
  return maxLag > 0 ? maxLag : 30
}

/** Score 0-100 measuring how well the graph predicted actual pips for an instrument. */
function comprehensionScore(ev: SnapshotEvent, tmpl: CausalTemplate, instrument: string): number | null {
  // Prefer the stored activation_score from causal_event_analyses (consistent with MarketEvents panel)
  if (ev.activation_score != null) return Math.round(ev.activation_score * 100)

  if (!ev.prediction_json || !ev.actual_json) return null
  try {
    const preds:   Record<string, number> = JSON.parse(ev.prediction_json)
    const actuals: Record<string, number> = JSON.parse(ev.actual_json)
    const actual = actuals[instrument] ?? actuals[instrument.toUpperCase()] ?? actuals[instrument.toLowerCase()]
    if (actual == null) return null

    const outputNode = tmpl.graph_json.nodes.find(n =>
      (n.type === 'market_asset' || n.type === 'output') && n.instrument?.toUpperCase() === instrument.toUpperCase()
    )
    const predicted = outputNode ? (preds[outputNode.id] ?? null) : null
    if (predicted == null) return null
    if (predicted === 0 && actual === 0) return 100
    if (predicted === 0) return 40

    const sameDir = predicted * actual > 0
    if (!sameDir) return 0
    const ratio = Math.min(Math.abs(actual), Math.abs(predicted)) / Math.max(Math.abs(actual), Math.abs(predicted))
    return Math.round(50 + ratio * 50)
  } catch { return null }
}

// ── CausalFrise ───────────────────────────────────────────────────────────────

const FRISE_LANE_H   = 24   // px par lane
const FRISE_CHIP_H   = 18   // hauteur d'un chip
const FRISE_CHIP_PAD = (FRISE_LANE_H - FRISE_CHIP_H) / 2
const FRISE_AXIS_H   = 20   // axe temporel en bas
const FRISE_MIN_W    = 44   // largeur minimale d'un chip (en px)
const FRISE_POPUP_W  = 220  // largeur du popup

function CausalFrise({
  events, templates, priceData, selectedDate, causalInsts,
  chartDateToX, chartCanvasLeft, chartReady,
}: {
  events:          SnapshotEvent[]
  templates:       CausalTemplate[]
  priceData:       PriceCandle[]
  selectedDate:    string | null
  causalInsts:     string[]
  chartDateToX?:   (d: string) => number | null
  chartCanvasLeft?: number
  chartReady?:     number
}) {
  const navigate     = useNavigate()
  const containerRef = useRef<HTMLDivElement>(null)
  const [contWidth, setContWidth]   = useState(400)
  const [contLeft,  setContLeft]    = useState(0)
  const [activeChip, setActiveChip] = useState<{
    tmpl: CausalTemplate; ev: SnapshotEvent; chipX: number; chipY: number
  } | null>(null)
  const [popupScore, setPopupScore] = useState<number | null | 'loading'>('loading')

  // Fetch analysis live when popup opens — compute same precision as MarketEvents expanded view
  useEffect(() => {
    const evId = activeChip?.ev.id
    if (!evId) { setPopupScore(null); return }
    setPopupScore('loading')
    api.get(`/causal-lab/analyses?market_event_id=${evId}&limit=1`)
      .then(r => {
        const analysis = r.data?.[0]
        console.log('[CausalFrise] analysis for event', evId, analysis)
        if (!analysis) { setPopupScore(null); return }

        // Compute magnitude-weighted precision (same formula as MarketEvents expanded panel)
        const preds: Record<string, number> = analysis.prediction_json || {}
        const actuals: Record<string, number> = analysis.actual_json || {}
        const graph = analysis.graph_json || {}
        const outputNodes: { id: string; instrument?: string }[] = (graph.nodes || [])
          .filter((n: { type?: string }) => n.type === 'market_asset' || n.type === 'output')

        console.log('[CausalFrise] preds:', preds, 'actuals:', actuals, 'outputNodes:', outputNodes)

        const nodeScores = outputNodes.map((n: { id: string; instrument?: string }) => {
          const pred = preds[n.id]
          const act = actuals[n.instrument || n.id] ?? actuals[n.id]
          if (pred == null || act == null) return null
          if (pred === 0 && act === 0) return 100
          if (pred === 0) return 40
          const sameDir = pred * act > 0
          if (!sameDir) return 0
          const ratio = Math.min(Math.abs(act), Math.abs(pred)) / Math.max(Math.abs(act), Math.abs(pred))
          return Math.round(50 + ratio * 50)
        }).filter((s): s is number => s != null)

        console.log('[CausalFrise] nodeScores:', nodeScores)

        if (nodeScores.length > 0) {
          setPopupScore(Math.round(nodeScores.reduce((a, b) => a + b, 0) / nodeScores.length))
        } else {
          // Fallback: use stored activation_score if no prediction data
          const stored = analysis.activation_score
          setPopupScore(stored != null ? Math.round(stored * 100) : null)
        }
      })
      .catch(err => { console.error('[CausalFrise] fetch error:', err); setPopupScore(null) })
  }, [activeChip?.ev.id])

  useEffect(() => {
    const el = containerRef.current; if (!el) return
    const update = () => {
      const rect = el.getBoundingClientRect()
      setContWidth(rect.width)
      setContLeft(rect.left)
    }
    update()
    const obs = new ResizeObserver(update)
    obs.observe(el)
    return () => obs.disconnect()
  }, [chartReady])  // re-measure when chart is ready (layout may have changed)

  // Close popup on outside click (tiny delay avoids self-close)
  useEffect(() => {
    if (!activeChip) return
    let tid: ReturnType<typeof setTimeout>
    const close = () => setActiveChip(null)
    tid = setTimeout(() => document.addEventListener('mousedown', close), 60)
    return () => { clearTimeout(tid); document.removeEventListener('mousedown', close) }
  }, [activeChip])

  const linked = events.filter(ev => {
    if (!ev.analyzed_instruments || ev.template_id == null) return false
    const insts = ev.analyzed_instruments.split(',')
    return causalInsts.some(ci => insts.includes(ci))
  })

  if (!priceData.length || !linked.length) {
    return (
      <div ref={containerRef} className="h-12 flex items-center justify-center text-xs text-slate-600 italic">
        Aucun graphe causal lié pour cet instrument
      </div>
    )
  }

  const minDate = priceData[0].time
  const maxDate = priceData[priceData.length - 1].time
  const usable  = Math.max(contWidth, 1)

  // If chart coordinate bridge is available, use it for pixel-perfect alignment.
  // chart.timeToCoordinate() gives x relative to chart canvas left edge.
  // We subtract (chartCanvasLeft - contLeft) to convert to frise container coords.
  // canvasOffset > 0 means chart canvas starts to the right of the frise container.
  const canvasOffset = (chartCanvasLeft ?? 0) - contLeft
  const tdToX: (d: string) => number = chartDateToX
    ? (d: string) => {
        const x = chartDateToX(d)
        if (x !== null) return x - canvasOffset
        // Fallback for out-of-range dates: clamp to edges
        return d <= minDate ? -canvasOffset : usable - canvasOffset
      }
    : makeTdToX(priceData, usable)

  // Build chips (one per event × template)
  type Chip = {
    ev: SnapshotEvent; tmpl: CausalTemplate
    x1: number; x2: number; w: number; active: boolean
  }
  const chips: Chip[] = linked
    .filter(ev => ev.date <= maxDate)
    .map(ev => {
      const tmpl = templates.find(t => t.id === ev.template_id)
      if (!tmpl) return null
      const absorptionDays = templateAbsorptionDays(tmpl)
      const endDate = ev.end_date ?? (() => {
        const d = new Date(ev.date); d.setDate(d.getDate() + absorptionDays)
        return d.toISOString().slice(0, 10)
      })()
      const x1  = tdToX(ev.date)
      const raw = tdToX(endDate) - x1
      const w   = Math.max(raw, FRISE_MIN_W)
      return { ev, tmpl, x1, x2: x1 + w, w, active: isActiveAt(ev, selectedDate) }
    })
    .filter((c): c is Chip => c !== null)
    .sort((a, b) => a.x1 - b.x1)

  // Greedy lane assignment — no overlap
  const laneEnds: number[] = []
  const placed = chips.map(chip => {
    const GAP = 3
    let lane = laneEnds.findIndex(end => end + GAP <= chip.x1)
    if (lane === -1) { lane = laneEnds.length; laneEnds.push(0) }
    laneEnds[lane] = chip.x2
    return { ...chip, lane }
  })

  const numLanes   = Math.max(laneEnds.length, 1)
  const containerH = numLanes * FRISE_LANE_H + FRISE_AXIS_H + 4

  // Month tick marks — skip some if too crowded
  const ticks: { label: string; x: number }[] = []
  {
    const start = new Date(minDate)
    let cur = new Date(start.getFullYear(), start.getMonth() + 1, 1)
    while (cur.toISOString().slice(0, 10) <= maxDate) {
      ticks.push({
        label: cur.toLocaleDateString('fr-FR', {
          month: 'short',
          ...(cur.getFullYear() !== start.getFullYear() ? { year: '2-digit' } : {}),
        }),
        x: tdToX(cur.toISOString().slice(0, 10)),
      })
      cur = new Date(cur.getFullYear(), cur.getMonth() + 1, 1)
    }
  }
  // Keep 1 label every ~70px to avoid crowding
  const tickStep = Math.max(1, Math.ceil(ticks.length / (usable / 70)))
  const visTicks = ticks.filter((_, i) => i % tickStep === 0)

  const crossX = selectedDate && selectedDate >= minDate && selectedDate <= maxDate
    ? tdToX(selectedDate) : null

  return (
    <div
      ref={containerRef}
      className="relative w-full select-none"
      style={{ height: containerH }}
      onClick={() => setActiveChip(null)}
    >
      {/* Month grid lines */}
      {visTicks.map((t, i) => (
        <div key={i} className="absolute top-0 pointer-events-none"
          style={{ left: t.x, bottom: FRISE_AXIS_H, width: 1, background: 'rgba(100,116,139,0.12)' }} />
      ))}

      {/* Crosshair */}
      {crossX !== null && (
        <div className="absolute top-0 bottom-0 pointer-events-none"
          style={{ left: crossX, width: 1, background: 'rgba(96,165,250,0.3)' }} />
      )}

      {/* Chips */}
      {placed.map(({ ev, tmpl, x1, w, lane, active }) => {
        const catTw    = EV_CAT_TW[ev.category] ?? 'text-slate-400 border-slate-700/30 bg-slate-800/40'
        const chipY    = lane * FRISE_LANE_H + FRISE_CHIP_PAD
        const isOpen   = activeChip?.ev.id === ev.id && activeChip?.tmpl.id === tmpl.id
        const charsFit = Math.floor((w - 18) / 5.5)
        const label    = charsFit < 3 ? '' : tmpl.name.length > charsFit
          ? tmpl.name.slice(0, charsFit - 1) + '…' : tmpl.name

        return (
          <div
            key={`${tmpl.id}-${ev.id ?? ev.date}`}
            className={clsx(
              'absolute rounded border cursor-pointer transition-all flex items-center gap-1 overflow-hidden px-1.5',
              'text-[9px] font-medium leading-none',
              active
                ? catTw + (isOpen ? ' ring-1 ring-current/40 brightness-125' : ' hover:brightness-110')
                : clsx('text-slate-500 border-slate-700/40 bg-slate-800/25',
                    'hover:text-slate-300 hover:border-slate-600/60 hover:bg-slate-800/50'),
            )}
            style={{ left: x1, top: chipY, width: w, height: FRISE_CHIP_H }}
            onMouseDown={e => e.stopPropagation()}
            onClick={e => {
              e.stopPropagation()
              setActiveChip(isOpen ? null : { tmpl, ev, chipX: x1, chipY })
            }}
            title={`${tmpl.name} · ${ev.title}\n${fmtDateFR(ev.date)} → ${ev.end_date ? fmtDateFR(ev.end_date) : '+30j'}`}
          >
            <span className={clsx('w-1.5 h-1.5 rounded-full bg-current shrink-0', !active && 'opacity-40')} />
            {label && <span className="truncate">{label}</span>}
          </div>
        )
      })}

      {/* Popup */}
      {activeChip && (() => {
        const { tmpl, ev, chipX, chipY } = activeChip
        const popH   = 108
        const popTop = chipY - popH - 6 >= 0 ? chipY - popH - 6 : chipY + FRISE_CHIP_H + 4
        const popLeft = Math.max(0, Math.min(chipX, contWidth - FRISE_POPUP_W - 4))
        return (
          <div
            className="absolute z-30 bg-dark-800 border border-slate-600/40 rounded-lg shadow-2xl p-3"
            style={{ left: popLeft, top: popTop, width: FRISE_POPUP_W }}
            onMouseDown={e => e.stopPropagation()}
            onClick={e => e.stopPropagation()}
          >
            <button
              onClick={() => setActiveChip(null)}
              className="absolute top-1.5 right-1.5 w-4 h-4 flex items-center justify-center text-slate-600 hover:text-slate-300 text-xs leading-none"
            >×</button>

            <div className="flex items-start gap-2 mb-1.5 pr-4">
              <span className="flex-1 text-xs font-semibold text-slate-200 leading-tight line-clamp-2">{tmpl.name}</span>
              <span className={clsx('text-[9px] px-1 py-0.5 rounded border shrink-0',
                EV_CAT_TW[ev.category] ?? 'text-slate-500 border-slate-700/30'
              )}>{ev.category}</span>
            </div>

            <div className="text-[10px] text-slate-400 truncate mb-1.5">{ev.title}</div>

            <div className="flex items-center gap-3 mb-2 text-[9px] text-slate-600">
              <span className="flex items-center gap-1">
                <Clock className="w-2.5 h-2.5" />
                {fmtDateFR(ev.date)} → {ev.end_date ? fmtDateFR(ev.end_date) : `+${templateAbsorptionDays(tmpl)}j`}
              </span>
              {ev.impact_score != null && (
                <span className="text-amber-600">★ {ev.impact_score.toFixed(1)}</span>
              )}
              {ev.surprise_pct != null && (
                <span className={ev.surprise_pct > 0 ? 'text-emerald-600' : 'text-red-600'}>
                  {ev.surprise_pct > 0 ? '+' : ''}{ev.surprise_pct.toFixed(1)}%
                </span>
              )}
            </div>

            {/* Précision — fetch live depuis causal_event_analyses au moment du clic */}
            {popupScore === 'loading' ? (
              <div className="mb-2 text-[9px] text-slate-600 italic">Chargement score…</div>
            ) : popupScore != null ? (() => {
              const barCls = popupScore >= 70 ? 'bg-emerald-500' : popupScore >= 40 ? 'bg-amber-500' : 'bg-red-500'
              const txtCls = popupScore >= 70 ? 'text-emerald-400' : popupScore >= 40 ? 'text-amber-400' : 'text-red-400'
              return (
                <div className="mb-2">
                  <div className="flex items-center justify-between text-[9px] text-slate-600 mb-0.5">
                    <span>Précision prédiction</span>
                    <span className={txtCls}>{popupScore}%</span>
                  </div>
                  <div className="h-1 bg-slate-800 rounded-full overflow-hidden">
                    <div className={clsx('h-full rounded-full', barCls)} style={{ width: `${popupScore}%` }} />
                  </div>
                </div>
              )
            })() : null}

            <div className="flex gap-1.5">
              <button
                onClick={() => { if (ev.id) navigate(`/market-events?event=${ev.id}`); setActiveChip(null) }}
                className="flex-1 text-[10px] text-slate-400 hover:text-slate-200 bg-slate-700/40 hover:bg-slate-700/60 rounded px-2 py-1 transition-colors"
              >Événement →</button>
              <button
                onClick={() => { navigate(`/causal-lab?template=${tmpl.id}`); setActiveChip(null) }}
                className="flex-1 text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 rounded px-2 py-1 transition-colors"
              >CausalLab →</button>
            </div>
          </div>
        )
      })()}

      {/* X-axis */}
      <div className="absolute left-0 right-0 border-t border-slate-700/30" style={{ bottom: FRISE_AXIS_H }}>
        {visTicks.map((t, i) => (
          <span key={i} className="absolute text-[9px] text-slate-600 -translate-x-1/2 pt-0.5" style={{ left: t.x }}>
            {t.label}
          </span>
        ))}
      </div>
    </div>
  )
}

// ── ExplanationScore ──────────────────────────────────────────────────────────

function computeMagnitudeScore(analysis: { prediction_json: Record<string, number>; actual_json: Record<string, number>; graph_json: { nodes?: { id: string; type?: string; instrument?: string }[] } }): number | null {
  const preds = analysis.prediction_json || {}
  const actuals = analysis.actual_json || {}
  const outputNodes = (analysis.graph_json?.nodes || []).filter(n => n.type === 'market_asset' || n.type === 'output')
  const nodeScores = outputNodes.map(n => {
    const pred = preds[n.id]
    const act = actuals[n.instrument || n.id] ?? actuals[n.id]
    if (pred == null || act == null) return null
    if (pred === 0 && act === 0) return 100
    if (pred === 0) return 40
    if (pred * act <= 0) return 0
    const ratio = Math.min(Math.abs(act), Math.abs(pred)) / Math.max(Math.abs(act), Math.abs(pred))
    return Math.round(50 + ratio * 50)
  }).filter((s): s is number => s != null)
  return nodeScores.length > 0 ? Math.round(nodeScores.reduce((a, b) => a + b, 0) / nodeScores.length) : null
}

function ExplanationScore({
  events, templates, causalInsts, onRefreshDone, onDebugResult, debugInfo,
}: {
  events: SnapshotEvent[]
  templates: CausalTemplate[]
  causalInsts: string[]
  onRefreshDone?: () => void
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  onDebugResult?: (data: any) => void
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  debugInfo?: any
}) {
  const [refreshing, setRefreshing] = useState(false)
  const [refreshDone, setRefreshDone] = useState(false)
  const [liveScores, setLiveScores] = useState<Record<number, number | null>>({})

  // Fetch all linked event analyses dynamically — same formula as MarketEvents expanded panel
  const linkedIds = events.filter(ev => ev.template_id && ev.id).map(ev => ev.id as number)
  const linkedKey = linkedIds.join(',')
  useEffect(() => {
    if (!linkedIds.length) return
    Promise.all(linkedIds.map(id =>
      api.get(`/causal-lab/analyses?market_event_id=${id}&limit=1`)
        .then(r => ({ id, score: computeMagnitudeScore(r.data?.[0]) }))
        .catch(() => ({ id, score: null }))
    )).then(results => {
      setLiveScores(Object.fromEntries(results.map(r => [r.id, r.score])))
    })
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [linkedKey])

  const scored: number[] = []
  const totalLinked: number[] = []

  for (const ev of events) {
    if (!ev.template_id) continue
    const tmpl = templates.find(t => t.id === ev.template_id)
    if (!tmpl) continue
    if (!causalInsts.some(ci => tmpl.instruments.includes(ci))) continue
    totalLinked.push(ev.id ?? 0)
    const s = ev.id != null ? liveScores[ev.id] : null
    if (s != null) scored.push(s)
  }

  const globalScore = scored.length > 0
    ? Math.round(scored.reduce((a, b) => a + b, 0) / scored.length)
    : null

  const verdict = globalScore == null ? 'En attente'
    : globalScore >= 70 ? 'Bien expliqué' : globalScore >= 40 ? 'Partiellement' : 'Peu lisible'
  const badgeCls = globalScore == null
    ? 'text-slate-500 bg-slate-800/60 border-slate-700/30'
    : globalScore >= 70
    ? 'text-emerald-400 bg-emerald-900/30 border-emerald-700/40'
    : globalScore >= 40
    ? 'text-amber-400 bg-amber-900/30 border-amber-700/40'
    : 'text-red-400 bg-red-900/20 border-red-700/30'
  const barCls = globalScore == null ? 'bg-slate-600'
    : globalScore >= 70 ? 'bg-emerald-500' : globalScore >= 40 ? 'bg-amber-500' : 'bg-red-500'

  const handleRefresh = async () => {
    onDebugResult?.(null)
    setRefreshing(true)
    try {
      const res = await api.post('/causal-lab/auto-analyze/refresh')
      const data = res.data
      onDebugResult?.(data)
      setRefreshDone(true)
      onRefreshDone?.()
      setTimeout(() => setRefreshDone(false), 8000)
    } catch (err: any) {
      onDebugResult?.({ error: String(err) })
    } finally {
      setRefreshing(false)
    }
  }

  const needsRefresh = totalLinked.length > 0 && scored.length === 0 && !refreshDone

  return (
    <div className="space-y-2">
      <div className="flex items-center gap-3 px-4 py-2.5 rounded-xl border border-slate-700/30 bg-dark-800/50">
        <span className="text-xs text-slate-500 whitespace-nowrap">Note globale</span>
        <div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden">
          <div className={clsx('h-full rounded-full transition-all', barCls)} style={{ width: `${globalScore ?? 0}%` }} />
        </div>
        <span className={clsx('text-xs font-semibold px-2 py-1 rounded-lg border whitespace-nowrap', badgeCls)}>
          {globalScore != null ? `${globalScore}% — ` : ''}{verdict}
        </span>
        <span className="text-[10px] text-slate-600 whitespace-nowrap">
          {scored.length}/{totalLinked.length} graphes
        </span>
        {(needsRefresh || scored.length === 0) && !refreshDone && totalLinked.length > 0 && (
          <button
            onClick={handleRefresh}
            disabled={refreshing}
            className="text-[10px] text-violet-400 hover:text-violet-200 bg-violet-900/20 hover:bg-violet-900/40 border border-violet-800/40 rounded px-2 py-1 whitespace-nowrap disabled:opacity-50 transition-colors"
          >
            {refreshing ? '…' : 'Recalculer'}
          </button>
        )}
      </div>

      {/* Refresh result panel */}
      {debugInfo && (
        <div className="rounded-lg border border-slate-700/30 bg-dark-900/60 p-3 text-[10px] font-mono text-slate-600">
          {debugInfo.error
            ? <span className="text-red-500">Erreur: {debugInfo.error}</span>
            : <span>Refresh: {debugInfo.refreshed} ok / {debugInfo.failed} err / {debugInfo.total} total</span>
          }
        </div>
      )}
    </div>
  )
}

// ── Main page ─────────────────────────────────────────────────────────────────

const PERIODS = [
  { key: '5d',  label: '5D'  },
  { key: '1mo', label: '1M'  },
  { key: '3mo', label: '3M'  },
  { key: '6mo', label: '6M'  },
  { key: '1y',  label: '1Y'  },
  { key: '2y',  label: '2Y'  },
  { key: '5y',  label: '5Y'  },
]

export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { instrumentIdProp?: string; isVisible?: boolean } = {}) {
  const { id: paramId = localStorage.getItem('last_instrument') || 'EURUSD=X' } = useParams<{ id: string }>()
  const navigate = useNavigate()
  const [period, setPeriod]           = useState('1y')
  const [chartStyle, setChartStyle]   = useState<'candles' | 'line'>('candles')
  const [instruments, setInstruments] = useState<InstrumentConfig[]>([])
  const [snapshot, setSnapshot]       = useState<Snapshot | null>(null)
  const [narrative, setNarrative]     = useState('')
  const [loading, setLoading]         = useState(false)
  const [refreshDebug, setRefreshDebug] = useState<any>(null)
  const [loadingNarr, setLoadingNarr] = useState(false)
  const [selectorOpen, setSelectorOpen] = useState(false)
  const [selectedDate, setSelectedDate] = useState<string | null>(null)
  const [tabUnder, setTabUnder]         = useState<'counters' | 'analyse'>('counters')
  const [templates, setTemplates]       = useState<CausalTemplate[]>([])
  const [causalScores, setCausalScores] = useState<Record<number, number | null>>({}) // eventId → activation_score*100
  const [macroAtDate, setMacroAtDate]   = useState<MacroGaugeSnap | null>(null)
  const [theoryCurve, setTheoryCurve]   = useState<TheoPoint[] | null>(null)
  const [loadingTheory, setLoadingTheory] = useState(false)
  const [showTheory, setShowTheory]     = useState(false)
  // Chart coordinate bridge — lets CausalFrise align with the chart's x-axis
  const chartDateToXRef = useRef<((d: string) => number | null) | null>(null)
  const chartCanvasLeftRef = useRef<number>(0)
  const [chartReady, setChartReady] = useState(0)   // bump to trigger frise re-render

  // instrumentIdProp is set when mounted as a keep-alive tab (so URL params don't bleed across instances)
  const instrumentId = (instrumentIdProp ?? paramId).toUpperCase()

  useEffect(() => {
    api.get('/instruments').then(r => setInstruments(r.data)).catch(() => {})
    api.get('/causal-lab/templates').then(r => setTemplates(r.data)).catch(() => {})
  }, [])

  // Silent refresh when the tab becomes visible again (e.g. after analysis updated from MarketEvents)
  const mountedRef = useRef(false)
  useEffect(() => {
    if (!mountedRef.current) { mountedRef.current = true; return }
    if (isVisible) fetchSnapshotSilent()
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isVisible])

  const fetchSnapshot = useCallback(() => {
    setLoading(true)
    api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
      .then(r => {
        setSnapshot(r.data)
        const pd: PriceCandle[] = r.data.price_data
        if (pd?.length) setSelectedDate(pd[pd.length - 1].time)
      })
      .catch(() => {})
      .finally(() => setLoading(false))
  }, [instrumentId, period])

  // Silent re-fetch (no loading spinner) — used after Recalculer so ExplanationScore keeps its state
  const fetchSnapshotSilent = useCallback(() => {
    api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
      .then(r => { setSnapshot(r.data) })
      .catch(() => {})
  }, [instrumentId, period])

  // Fetch causal scores directly from the analyses API (not via snapshot) so the popup
  // always shows the DB-stored activation_score, independent of snapshot staleness
  const refreshCausalScores = useCallback((events: SnapshotEvent[]) => {
    const ids = events.map(e => e.id).filter(Boolean) as number[]
    if (!ids.length) return
    Promise.all(ids.map(id =>
      api.get(`/causal-lab/analyses?market_event_id=${id}&limit=1`)
        .then(r => ({ id, score: r.data[0]?.activation_score ?? null }))
        .catch(() => ({ id, score: null }))
    )).then(results => {
      setCausalScores(Object.fromEntries(results.map(r => [r.id, r.score != null ? Math.round(r.score * 100) : null])))
    })
  }, [])

  useEffect(() => {
    setSnapshot(null)
    setNarrative('')
    setSelectedDate(null)
    setMacroAtDate(null)
    setTheoryCurve(null)
    setShowTheory(false)
    chartDateToXRef.current = null
    setChartReady(0)
    fetchSnapshot()
  }, [instrumentId, period])

  // Refresh causal scores whenever the snapshot events change
  useEffect(() => {
    if (snapshot?.events?.length) refreshCausalScores(snapshot.events)
  }, [snapshot?.events, refreshCausalScores])

  const loadNarrative = useCallback(() => {
    setLoadingNarr(true)
    api.post(`/instruments/${instrumentId}/narrative`)
      .then(r => setNarrative(r.data.narrative))
      .catch(() => {})
      .finally(() => setLoadingNarr(false))
  }, [instrumentId])

  const handleDateHover = useCallback((date: string | null) => {
    if (date) setSelectedDate(date)
  }, [])

  const handleChartReady = useCallback((fn: ((d: string) => number | null) | null, left: number) => {
    chartDateToXRef.current = fn
    chartCanvasLeftRef.current = left
    setChartReady(k => k + 1)
  }, [])

  const toggleTheory = useCallback(() => {
    if (showTheory) {
      setShowTheory(false)
      setTheoryCurve(null)
      return
    }
    if (theoryCurve) { setShowTheory(true); return }
    setLoadingTheory(true)
    api.get(`/instruments/${instrumentId}/theoretical-curve?period=${period}`)
      .then(r => { setTheoryCurve(r.data); setShowTheory(true) })
      .catch(() => {})
      .finally(() => setLoadingTheory(false))
  }, [showTheory, theoryCurve, instrumentId, period])

  const { priceMap, indMap, sortedDates, dateIndex } = useMemo(() => {
    if (!snapshot) return { priceMap: {} as Record<string, PriceCandle>, indMap: {} as Record<string, Record<string, number>>, sortedDates: [] as string[], dateIndex: {} as Record<string, number> }
    const priceMap:   Record<string, PriceCandle>            = {}
    const indMap:     Record<string, Record<string, number>> = {}
    const sortedDates: string[] = []
    const dateIndex:  Record<string, number>                  = {}
    for (const c of snapshot.price_data) { priceMap[c.time] = c; sortedDates.push(c.time) }
    sortedDates.forEach((d, i) => { dateIndex[d] = i })
    for (const [key, pts] of Object.entries(snapshot.indicators)) {
      for (const pt of pts) { if (!indMap[pt.time]) indMap[pt.time] = {}; indMap[pt.time][key] = pt.value }
    }
    return { priceMap, indMap, sortedDates, dateIndex }
  }, [snapshot])

  // Fetch macro gauge context when crosshair date changes (after sortedDates is available)
  useEffect(() => {
    if (!selectedDate || !sortedDates.length) return
    const isLast = selectedDate === sortedDates[sortedDates.length - 1]
    if (isLast) { setMacroAtDate(null); return }
    api.get(`/market/macro-gauges/at?date=${selectedDate}`)
      .then(r => { if (r.data?.snapshot_date) setMacroAtDate(r.data) })
      .catch(() => {})
  }, [selectedDate, sortedDates])

  const effectiveDate = useMemo(() => {
    if (selectedDate && dateIndex[selectedDate] !== undefined) return selectedDate
    return sortedDates[sortedDates.length - 1] ?? null
  }, [selectedDate, sortedDates, dateIndex])

  const dateTrend = useMemo((): TrendMetrics | null => {
    if (!effectiveDate || !snapshot) return null
    const candle = priceMap[effectiveDate]; if (!candle) return null
    const idx = dateIndex[effectiveDate], price = candle.close, ind = indMap[effectiveDate] ?? {}
    const ma50 = ind.ma50, ma200 = ind.ma200, atr14 = ind.atr14 ?? 0
    const d5  = idx >= 5  ? sortedDates[idx - 5]  : null
    const d20 = idx >= 20 ? sortedDates[idx - 20] : null
    const d21 = idx >= 21 ? sortedDates[idx - 21] : null
    const d63 = idx >= 63 ? sortedDates[idx - 63] : null
    let atrSum = 0, atrCnt = 0
    for (let i = Math.max(0, idx - 65); i <= idx; i++) { const v = indMap[sortedDates[i]]?.atr14; if (v) { atrSum += v; atrCnt++ } }
    let high52 = candle.high, low52 = candle.low
    for (let i = Math.max(0, idx - 252); i <= idx; i++) { const c = priceMap[sortedDates[i]]; if (c) { if (c.high > high52) high52 = c.high; if (c.low < low52) low52 = c.low } }
    return {
      ma50_slope_5d:    pctN(ma50, d5  ? indMap[d5]?.ma50  : undefined),
      ma200_slope_20d:  pctN(ma200, d20 ? indMap[d20]?.ma200 : undefined),
      rsi14_current:    ind.rsi14 ?? 50,
      atr14_current:    atr14,
      atr_vs_3m_avg_pct: atrCnt > 0 && atr14 ? (atr14 / (atrSum / atrCnt)) * 100 : 100,
      momentum_1m_pct:  d21 && priceMap[d21] ? pctN(price, priceMap[d21].close) : 0,
      momentum_3m_pct:  d63 && priceMap[d63] ? pctN(price, priceMap[d63].close) : 0,
      dist_ma50_pct:    ma50  ? pctN(price, ma50)  : null,
      dist_ma200_pct:   ma200 ? pctN(price, ma200) : null,
      current_price: price, high_52w: high52, low_52w: low52,
    }
  }, [effectiveDate, priceMap, indMap, sortedDates, dateIndex, snapshot])

  const dateSignals = useMemo((): RegimeSignals | null => {
    if (!effectiveDate || !snapshot) return null
    const candle = priceMap[effectiveDate]; if (!candle) return null
    const idx = dateIndex[effectiveDate], price = candle.close, ind = indMap[effectiveDate] ?? {}
    const ma50 = ind.ma50, ma200 = ind.ma200, atr14 = ind.atr14
    const d10 = idx >= 10 ? sortedDates[idx - 10] : null
    const d20 = idx >= 20 ? sortedDates[idx - 20] : null
    let atrSum = 0, atrCnt = 0
    for (let i = Math.max(0, idx - 65); i <= idx; i++) { const v = indMap[sortedDates[i]]?.atr14; if (v) { atrSum += v; atrCnt++ } }
    return {
      ma50_above_ma200: ma50 !== undefined && ma200 !== undefined ? ma50 > ma200 : null,
      ma50_slope_pct:   pctN(ma50,  d10 ? indMap[d10]?.ma50  : undefined),
      ma200_slope_pct:  pctN(ma200, d10 ? indMap[d10]?.ma200 : undefined),
      momentum_20d_pct: d20 && priceMap[d20] ? pctN(price, priceMap[d20].close) : 0,
      dist_ma200_pct:   ma200 ? pctN(price, ma200) : 0,
      vol_ratio_pct:    atrCnt > 0 && atr14 ? (atr14 / (atrSum / atrCnt)) * 100 : 0,
    }
  }, [effectiveDate, priceMap, indMap, sortedDates, dateIndex, snapshot])

  const grouped = CATEGORY_ORDER.map(cat => ({
    cat, label: CATEGORY_LABELS[cat] ?? cat,
    items: instruments.filter(i => i.category === cat),
  })).filter(g => g.items.length > 0)

  const selected     = instruments.find(i => i.id === instrumentId)
  const isLastDate   = effectiveDate === sortedDates[sortedDates.length - 1]
  const dateLabel    = effectiveDate ? fmtDateFR(effectiveDate) : '—'
  const displayPrice = dateTrend?.current_price ?? snapshot?.current_price

  return (
    <div className="p-6 max-w-screen-xl mx-auto space-y-4">

      {/* ── Header ── */}
      <div className="flex flex-wrap items-center gap-3">
        <div className="relative">
          <button onClick={() => setSelectorOpen(s => !s)}
            className="flex items-center gap-2 px-4 py-2 bg-dark-800 border border-slate-700/40 rounded-xl text-white hover:bg-dark-700 transition-colors"
          >
            <BarChart2 className="w-4 h-4 text-blue-400" />
            <span className="font-semibold">{selected?.name ?? instrumentId}</span>
            <span className="text-xs text-slate-500">{selected?.id}</span>
            <ChevronDown className="w-3.5 h-3.5 text-slate-500" />
          </button>
          {selectorOpen && (
            <div className="absolute top-full left-0 mt-1 z-50 bg-dark-800 border border-slate-700/40 rounded-xl shadow-2xl p-3 w-[480px] max-h-[60vh] overflow-y-auto">
              {grouped.map(g => (
                <div key={g.cat} className="mb-3">
                  <div className="text-xs text-slate-500 uppercase tracking-wide mb-1.5 px-1">{g.label}</div>
                  <div className="grid grid-cols-3 gap-1">
                    {g.items.map(inst => (
                      <button key={inst.id}
                        onClick={() => { navigate(`/instruments/${inst.id}`); setSelectorOpen(false) }}
                        className={clsx('text-left px-2.5 py-1.5 rounded-lg text-xs transition-colors',
                          inst.id === instrumentId
                            ? 'bg-blue-800/40 text-blue-300 border border-blue-700/40'
                            : 'text-slate-400 hover:bg-dark-700 hover:text-white'
                        )}
                      >
                        <div className="font-semibold">{inst.id}</div>
                        <div className="text-slate-500 truncate">{inst.name}</div>
                      </button>
                    ))}
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>

        {selected && (
          <span className="text-xs px-2 py-1 bg-dark-700 border border-slate-700/30 rounded-lg text-slate-400">
            {CATEGORY_LABELS[selected.category] ?? selected.category}
          </span>
        )}

        {displayPrice !== undefined && (
          <div className="flex items-center gap-3">
            <span className="text-xl font-bold text-white">
              {displayPrice.toLocaleString('fr-FR', { maximumFractionDigits: 4 })}
            </span>
            {isLastDate && snapshot && (
              <span className={clsx('text-sm font-semibold', (snapshot.change_pct ?? 0) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
                {(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_abs?.toFixed(2)} ({(snapshot.change_pct ?? 0) >= 0 ? '+' : ''}{snapshot.change_pct?.toFixed(2)}%)
              </span>
            )}
          </div>
        )}

        <div className="ml-auto flex items-center gap-2">
          {/* Chart style toggle */}
          <div className="flex items-center bg-dark-800 border border-slate-700/40 rounded-xl p-1">
            <button
              onClick={() => setChartStyle('candles')}
              title="Chandeliers"
              className={clsx('p-1.5 rounded-lg transition-colors', chartStyle === 'candles' ? 'bg-blue-700/50 text-blue-300' : 'text-slate-500 hover:text-white hover:bg-dark-700')}
            ><BarChart2 className="w-3.5 h-3.5" /></button>
            <button
              onClick={() => setChartStyle('line')}
              title="Courbe lisse"
              className={clsx('p-1.5 rounded-lg transition-colors', chartStyle === 'line' ? 'bg-blue-700/50 text-blue-300' : 'text-slate-500 hover:text-white hover:bg-dark-700')}
            ><TrendingUp className="w-3.5 h-3.5" /></button>
          </div>
          {/* Period selector */}
          <div className="flex items-center gap-1 bg-dark-800 border border-slate-700/40 rounded-xl p-1">
            {PERIODS.map(p => (
              <button key={p.key} onClick={() => setPeriod(p.key)}
                className={clsx('px-3 py-1 text-xs rounded-lg transition-colors',
                  period === p.key ? 'bg-blue-700/50 text-blue-300' : 'text-slate-400 hover:text-white hover:bg-dark-700'
                )}
              >
                {p.label}
              </button>
            ))}
          </div>
        </div>

        {selected && <p className="w-full text-xs text-slate-500">{selected.description}</p>}
      </div>

      {/* ── Loading ── */}
      {loading && (
        <div className="space-y-4">
          <div className="bg-dark-800/60 rounded-xl border border-slate-700/40 animate-pulse" style={{ height: 420 }} />
          <div className="bg-dark-800/60 rounded-xl border border-slate-700/40 animate-pulse h-20" />
          <div className="grid grid-cols-3 gap-4">
            {[1, 2, 3].map(i => <div key={i} className="bg-dark-800/60 rounded-xl border border-slate-700/40 animate-pulse h-72" />)}
          </div>
        </div>
      )}

      {/* ── Content ── */}
      {!loading && snapshot && (
        <>
          <InstrumentChart
            priceData={snapshot.price_data}
            indicators={snapshot.indicators}
            events={NO_CHART_EVENTS}
            height={420}
            chartType={chartStyle}
            onDateHover={handleDateHover}
            theoryCurve={showTheory && theoryCurve ? theoryCurve : undefined}
            onChartReady={handleChartReady}
          />

          {/* Date badge */}
          <div className="flex items-center gap-2">
            <div className={clsx(
              'flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border',
              isLastDate
                ? 'bg-slate-800 border-slate-700/40 text-slate-400'
                : 'bg-blue-900/40 border-blue-700/50 text-blue-300'
            )}>
              <Calendar className="w-3 h-3" />
              Snapshot au {dateLabel}
              {!isLastDate && <span className="text-blue-500 ml-1">← survol du graphe</span>}
            </div>
          </div>

          {/* ── Tabs sous la courbe ── */}
          <div className="border-b border-slate-700/40 flex gap-1">
            {([
              { key: 'counters', label: 'Compteurs' },
              { key: 'analyse',  label: 'Analyse de la courbe' },
            ] as const).map(t => (
              <button
                key={t.key}
                onClick={() => setTabUnder(t.key)}
                className={clsx(
                  'px-4 py-2 text-xs font-medium border-b-2 -mb-px transition-colors',
                  tabUnder === t.key
                    ? 'text-blue-300 border-blue-500'
                    : 'text-slate-500 border-transparent hover:text-slate-300 hover:border-slate-600'
                )}
              >
                {t.label}
              </button>
            ))}
          </div>

          {tabUnder === 'counters' && (
            <>
              <div className="grid grid-cols-3 gap-4">
                <RegimeCard
                  regime={snapshot.regime}
                  macroRegime={macroAtDate ? snapToMacroRegime(macroAtDate) : (snapshot.macro_regime ?? null)}
                  signalsAt={dateSignals}
                  dateLabel={dateLabel}
                />
                <TrendCard
                  trend={dateTrend ?? snapshot.trend}
                  dateLabel={dateLabel}
                />
                <EventsCard
                  events={snapshot.events}
                  selectedDate={effectiveDate}
                />
              </div>
              {macroAtDate && <MacroGaugePanel snap={macroAtDate} dateLabel={dateLabel} />}
            </>
          )}

          {tabUnder === 'analyse' && (() => {
            const causalInsts = CAT_TO_CAUSAL_INST[selected?.category ?? ''] ?? []
            const theoPt = effectiveDate && theoryCurve
              ? theoryCurve.find(p => p.date === effectiveDate) ?? null
              : null
            return (
            <div className="space-y-3">
              {/* Frise des graphes causaux (inclut l'event) */}
              <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
                <div className="flex items-center gap-2 mb-3">
                  <BarChart2 className="w-4 h-4 text-violet-400" />
                  <span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Frise des graphes</span>
                  <button
                    onClick={toggleTheory}
                    disabled={loadingTheory}
                    className={`ml-auto px-2.5 py-1 rounded text-xs font-medium border transition-colors ${
                      showTheory
                        ? 'bg-violet-700/60 border-violet-600/60 text-violet-200'
                        : 'bg-dark-900/40 border-slate-600/40 text-slate-400 hover:border-violet-600/40 hover:text-violet-300'
                    }`}
                  >
                    {loadingTheory ? '↻ Chargement…' : showTheory ? '⟁ Théorie ON' : '⟁ Courbe théorique'}
                  </button>
                </div>
                <CausalFrise
                  events={snapshot.events}
                  templates={templates}
                  priceData={snapshot.price_data}
                  selectedDate={effectiveDate}
                  causalInsts={causalInsts}
                  chartDateToX={chartDateToXRef.current ?? undefined}
                  chartCanvasLeft={chartCanvasLeftRef.current}
                  chartReady={chartReady}
                />
              </div>

              {/* Décomposition théorique au curseur */}
              {showTheory && theoPt && (
                <div className="rounded-xl border border-violet-800/40 bg-violet-950/20 p-4">
                  <div className="flex items-center gap-2 mb-3">
                    <span className="text-violet-400 text-xs">⟁</span>
                    <span className="text-xs font-semibold text-violet-300 uppercase tracking-wide">
                      Contributions théoriques — {dateLabel}
                    </span>
                    <span className={`ml-auto text-sm font-mono font-bold ${theoPt.cumulative_pips >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
                      {theoPt.cumulative_pips >= 0 ? '+' : ''}{theoPt.cumulative_pips} pips
                    </span>
                  </div>
                  {theoPt.contributions.length === 0 ? (
                    <p className="text-xs text-slate-500 italic">Aucun événement actif à cette date.</p>
                  ) : (
                    <div className="space-y-2">
                      {theoPt.contributions.map((c, i) => (
                        <div key={i} className="flex items-center gap-2 text-xs">
                          <div className="flex-1 min-w-0">
                            <span className="text-slate-300 font-medium truncate block">{c.event_name}</span>
                            <span className="text-slate-500">{c.template_name} · depuis {c.event_date} · {Math.round(c.decay_factor * 100)}% actif</span>
                          </div>
                          <span className={`font-mono font-semibold shrink-0 ${c.pips >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
                            {c.pips >= 0 ? '+' : ''}{c.pips} pip
                          </span>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
              )}

              {showTheory && !theoPt && theoryCurve && (
                <div className="rounded-xl border border-violet-800/30 bg-violet-950/10 p-3 text-xs text-slate-500 text-center">
                  ⟁ Aucune contribution théorique pour cette date
                </div>
              )}

              {/* Note globale */}
              <ExplanationScore
                events={snapshot.events}
                templates={templates}
                causalInsts={causalInsts}
                onRefreshDone={fetchSnapshotSilent}
                onDebugResult={setRefreshDebug}
                debugInfo={refreshDebug}
              />
            </div>
            )
          })()}

          <NarrativeCard
            narrative={narrative}
            loading={loadingNarr}
            onLoad={loadNarrative}
            instrument={snapshot.instrument}
          />
        </>
      )}

      {!loading && !snapshot && (
        <div className="text-center py-20 text-slate-500">
          <BarChart2 className="w-8 h-8 mx-auto mb-3 opacity-30" />
          <p>Aucune donnée disponible pour {instrumentId}</p>
        </div>
      )}

      {selectorOpen && <div className="fixed inset-0 z-40" onClick={() => setSelectorOpen(false)} />}
    </div>
  )
}
