/**
 * InstrumentModels — graphes causaux exhaustifs par instrument.
 * Architecture 3 couches : inputs (events + manuels) → intermédiaires → output
 * Vues : DAG visuel | Tableau éditable | Timeline évolution
 */
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import {
  RefreshCw, Edit3, X, Trash2, ChevronDown, ChevronUp,
  TrendingUp, TrendingDown, Minus, LineChart, Table2, Network, Activity,
  Plus, Zap, Settings2, Check, Eye, EyeOff,
} from 'lucide-react'
import clsx from 'clsx'
import axios from 'axios'

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

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

type NodeType = 'input_event' | 'input_manual' | 'intermediate' | 'output'

interface ModelNode {
  id: string
  label: string
  node_type: NodeType
  category: string
  unit: string
  description: string
  coefficient_to_pips?: number
  event_category?: string
  formula?: string
  display_col: number
  macro_key?: string
  // computed
  computed_value: number
  pip_contribution: number
  raw_value?: number
  source: 'manual' | 'events' | 'neutral' | 'computed'
  override_note?: string
  override_set_at?: string
  // Phase 2
  pip_linear?: number
  pip_saturated?: number
  saturation_pct?: number
  regime_weight?: number
  // Phase 3 — event nodes: baseline (structurel) + surprise (événement)
  baseline_value?: number
  event_surprise?: number
}

interface RegimeInfo {
  regime: string
  label: string
  dominant_cat: string | null
  scores: Record<string, number>
  weights: Record<string, number>
}

interface ModelState {
  instrument: string
  name: string
  description: string
  at_date: string
  net_pips: number
  structural_pips: number
  event_pips: number
  price_intercept: number
  pip_to_price: number
  yf_ticker: string
  fundamental_level: number
  synthetic_price: number
  direction: 'bullish' | 'bearish' | 'neutral'
  nodes: ModelNode[]
  output_node: string
  regime: RegimeInfo
  event_details: Record<string, EventDetail[]>
  col_labels?: string[]
}

interface ActiveEvent {
  label: string
  pips: number
  lifecycle_factor: number
  contribution: number
  date: string
  category: string
}

interface TimelinePoint {
  date: string
  net_pips: number
  structural_pips: number
  event_pips: number
  fundamental_level: number
  synthetic_price: number
  regime?: string
  nodes: Record<string, number>
  active_events?: ActiveEvent[]
}

interface PricePoint {
  date: string
  close: number
}

interface VirtualEventForm {
  id: string
  date: string
  category: string
  pips: number
  label: string
  absorption_days: number
  rise_days: number
  plateau_days: number
  decay_type: string
  // calendar metadata (FF import)
  currency?: string
  impact?: string
  has_surprise?: boolean
  actual_value?: string | null
  forecast_value?: string | null
  event_name?: string
  routed_node?: string
  disabled?: boolean
}

interface EventDetail {
  analysis_id: number
  event_id: number
  title: string
  start_date: string
  days_since: number
  pip_prediction: number
  lifecycle_factor: number
  remaining_pips: number
  calibration: {
    absorption_days: number
    rise_days: number
    plateau_days: number
    decay_type: string
  }
}

interface CalendarEvent {
  date: string
  event_time: string
  currency: string
  impact: string
  event_name: string
  label: string
  actual_value: string | null
  forecast_value: string | null
  previous_value: string | null
  category: string
  routed_node: string
  is_future: boolean
  estimated_pips: number
  has_surprise: boolean
  surprise_delta: number | null
}

interface MacroGuidanceItem {
  node_id: string
  node_label: string
  macro_key: string
  unit: string
  current_value: number | null
  next_event: {
    date: string
    name: string
    forecast: number | null
    days_until: number
  } | null
}

interface NodeScenario {
  id: number
  instrument: string
  node_id: string
  label: string
  horizon: 'ct' | 'mt' | 'lt'
  target_date: string
  target_value: number
  confidence: number
  trajectory: 'step' | 'linear' | 'exp'
  absorption_days: number
  notes?: string
  created_at: string
}

// ── Constants ─────────────────────────────────────────────────────────────────

const INSTRUMENTS = ['EURUSD','USDJPY','XAUUSD','SP500','TLT','GBPUSD','EEM','QQQ']

const MACRO_KEY_OPTIONS = [
  { value: '',                label: '— aucune clé macro —' },
  { value: 'fed_rate',        label: 'Taux Fed (USD)' },
  { value: 'ecb_rate',        label: 'Taux BCE (EUR)' },
  { value: 'boe_rate',        label: 'Taux BoE (GBP)' },
  { value: 'boj_rate',        label: 'Taux BoJ (JPY)' },
  { value: 'us_cpi',          label: 'CPI US MoM' },
  { value: 'us_cpi_yoy',      label: 'CPI US YoY' },
  { value: 'eu_cpi_yoy',      label: 'HICP Eurozone YoY' },
  { value: 'us_nfp',          label: 'NFP US (emplois K)' },
  { value: 'us_pmi',          label: 'PMI US (ISM)' },
  { value: 'eu_pmi',          label: 'PMI Eurozone' },
  { value: 'us_gdp',          label: 'GDP US QoQ %' },
  { value: 'eu_gdp',          label: 'GDP Eurozone %' },
  { value: 'us_unemployment', label: 'Chômage US %' },
  { value: 'eu_unemployment', label: 'Chômage Eurozone %' },
  { value: 'us_retail_sales', label: 'Ventes détail US MoM' },
]

const NODE_TYPE_META: Record<NodeType, { label: string; color: string; bg: string }> = {
  input_event:  { label: 'Events',        color: 'text-sky-400',     bg: 'bg-sky-900/30 border-sky-700/40'       },
  input_manual: { label: 'Manuel',        color: 'text-violet-400',  bg: 'bg-violet-900/30 border-violet-700/40' },
  intermediate: { label: 'Intermédiaire', color: 'text-amber-400',   bg: 'bg-amber-900/20 border-amber-700/40'   },
  output:       { label: 'Résultat',      color: 'text-emerald-400', bg: 'bg-emerald-900/20 border-emerald-700/40' },
}

const SOURCE_META: Record<string, { dot: string; label: string }> = {
  manual:   { dot: 'bg-violet-400', label: 'Override' },
  events:   { dot: 'bg-sky-400',    label: 'Events'   },
  neutral:  { dot: 'bg-slate-600',  label: 'Neutre'   },
  computed: { dot: 'bg-amber-400',  label: 'Calculé'  },
}

const PERIODS = ['5d','1mo','3mo','6mo','1y','2y'] as const
type Period = typeof PERIODS[number]

const LAYER_COLORS: Record<string, string> = {
  'net_pips':               'bg-emerald-500',
  'layer_monetary':         'bg-blue-400',
  'layer_rates':            'bg-blue-400',
  'layer_growth':           'bg-violet-400',
  'layer_uk_macro':         'bg-violet-400',
  'layer_risk':             'bg-red-400',
  'layer_risk_credit':      'bg-red-400',
  'layer_positioning':      'bg-orange-400',
  'layer_policy':           'bg-orange-400',
  'layer_flows':            'bg-orange-400',
  'layer_demand':           'bg-teal-400',
  'layer_refuge':           'bg-pink-400',
  'layer_tech_fundamental': 'bg-purple-400',
  'layer_supply':           'bg-slate-400',
  'layer_china':            'bg-yellow-400',
  'layer_dollar':           'bg-sky-400',
  'layer_global':           'bg-gray-400',
  'layer_macro':            'bg-lime-400',
}

const CANVAS_COLORS: Record<string, string> = {
  'net_pips':               '#10b981',
  'layer_monetary':         '#60a5fa',
  'layer_rates':            '#60a5fa',
  'layer_growth':           '#a78bfa',
  'layer_uk_macro':         '#a78bfa',
  'layer_risk':             '#f87171',
  'layer_risk_credit':      '#f87171',
  'layer_positioning':      '#fb923c',
  'layer_policy':           '#fb923c',
  'layer_flows':            '#fb923c',
  'layer_demand':           '#2dd4bf',
  'layer_refuge':           '#f472b6',
  'layer_tech_fundamental': '#c084fc',
  'layer_supply':           '#94a3b8',
  'layer_china':            '#fbbf24',
  'layer_dollar':           '#38bdf8',
  'layer_global':           '#cbd5e1',
  'layer_macro':            '#a3e635',
}

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

function pipColor(v: number) {
  if (v > 0.5)  return 'text-emerald-400'
  if (v < -0.5) return 'text-red-400'
  return 'text-slate-400'
}

function pipBg(v: number) {
  if (v > 0.5)  return 'bg-emerald-500'
  if (v < -0.5) return 'bg-red-500'
  return 'bg-slate-600'
}

function fmt(v: number) {
  const s = v > 0 ? '+' : ''
  return `${s}${v.toFixed(1)}`
}

function fmtPrice(v: number, pipToPrice: number): string {
  if (pipToPrice <= 0.0001) return v.toFixed(4)
  if (pipToPrice <= 0.01)   return v.toFixed(2)
  if (pipToPrice <= 0.1)    return v.toFixed(2)
  return v.toFixed(0)
}

const EVENT_CATEGORIES = [
  { value: 'central_bank',   label: 'Banque Centrale'       },
  { value: 'monetary_shock', label: 'Surprise Macro'        },
  { value: 'geopolitical',   label: 'Géopolitique'          },
  { value: 'trade_policy',   label: 'Commerce / Tarifs'     },
  { value: 'growth_shock',   label: 'Choc Croissance'       },
  { value: 'credit_stress',  label: 'Stress Crédit'         },
  { value: 'commodity',      label: 'Commodités'            },
  { value: 'sentiment',      label: 'Sentiment'             },
  { value: 'technical',      label: 'Technique'             },
  { value: 'unclassified',   label: 'Non classifié'         },
]

function DirectionBadge({ direction, pips }: { direction: string; pips: number }) {
  const icon =
    direction === 'bullish' ? <TrendingUp className="w-3.5 h-3.5"/> :
    direction === 'bearish' ? <TrendingDown className="w-3.5 h-3.5"/> :
    <Minus className="w-3.5 h-3.5"/>
  const cls =
    direction === 'bullish' ? 'text-emerald-400 border-emerald-700/50 bg-emerald-900/30' :
    direction === 'bearish' ? 'text-red-400 border-red-700/50 bg-red-900/30' :
    'text-slate-400 border-slate-700/40 bg-slate-800/50'
  return (
    <span className={clsx('inline-flex items-center gap-1.5 px-2.5 py-1 rounded border text-sm font-semibold', cls)}>
      {icon} {fmt(pips)} pips · {direction.toUpperCase()}
    </span>
  )
}

// ── Regime Badge ──────────────────────────────────────────────────────────────

const REGIME_META: Record<string, { color: string; bg: string; icon: string }> = {
  MONETARY_DOMINANCE: { color: 'text-blue-400',   bg: 'bg-blue-900/30 border-blue-700/40',   icon: '🏦' },
  GEOPOLITICAL_RISK:  { color: 'text-orange-400', bg: 'bg-orange-900/30 border-orange-700/40', icon: '⚠️' },
  CREDIT_STRESS:      { color: 'text-red-400',    bg: 'bg-red-900/30 border-red-700/40',      icon: '🔴' },
  GROWTH_SCARE:       { color: 'text-amber-400',  bg: 'bg-amber-900/30 border-amber-700/40',  icon: '📉' },
  COMMODITY_SHOCK:    { color: 'text-yellow-400', bg: 'bg-yellow-900/30 border-yellow-700/40','icon': '🛢️' },
  BALANCED:           { color: 'text-slate-400',  bg: 'bg-slate-800/50 border-slate-700/40',  icon: '⚖️' },
}

function RegimeBadge({ regime }: { regime: RegimeInfo }) {
  const meta = REGIME_META[regime.regime] || REGIME_META.BALANCED
  const topWeights = Object.entries(regime.weights)
    .filter(([, w]) => w > 1.0)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 3)

  return (
    <div className={clsx('inline-flex flex-col gap-1 px-3 py-2 rounded-lg border text-xs', meta.bg)}>
      <div className="flex items-center gap-1.5">
        <span>{meta.icon}</span>
        <span className={clsx('font-semibold', meta.color)}>{regime.label}</span>
        {regime.dominant_cat && (
          <span className="text-slate-500">· {regime.dominant_cat}</span>
        )}
      </div>
      {topWeights.length > 0 && (
        <div className="flex gap-2 flex-wrap">
          {topWeights.map(([layer, w]) => (
            <span key={layer} className={clsx('font-mono', meta.color)}>
              {layer.replace('layer_', '')} ×{w.toFixed(2)}
            </span>
          ))}
        </div>
      )}
    </div>
  )
}

// ── Node Edit Modal ───────────────────────────────────────────────────────────

function NodeEditModal({ node, instrument, onClose, onSaved }: {
  node: ModelNode; instrument: string; onClose: () => void; onSaved: () => void
}) {
  const [val, setVal]       = useState<string>(node.raw_value !== undefined ? String(node.raw_value) : '')
  const [note, setNote]     = useState(node.override_note || '')
  const [saving, setSaving] = useState(false)
  const [clrg, setClrg]     = useState(false)

  const coeff   = node.coefficient_to_pips ?? 1.0
  const preview = node.node_type === 'input_manual'
    ? (parseFloat(val) || 0) * coeff
    : (parseFloat(val) || 0)

  async function save() {
    setSaving(true)
    try {
      await api.put(`/instrument-models/${instrument}/nodes/${node.id}/override`, {
        value: parseFloat(val) || 0, note
      })
      onSaved(); onClose()
    } finally { setSaving(false) }
  }

  async function clear() {
    setClrg(true)
    try {
      await api.delete(`/instrument-models/${instrument}/nodes/${node.id}/override`)
      onSaved(); onClose()
    } finally { setClrg(false) }
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
      onClick={onClose}>
      <div className="bg-dark-800 border border-slate-700/60 rounded-xl shadow-2xl w-full max-w-md mx-4 p-6"
        onClick={e => e.stopPropagation()}>
        <div className="flex items-start justify-between mb-4">
          <div>
            <div className="text-white font-semibold text-sm">{node.label}</div>
            <div className="text-slate-500 text-xs mt-0.5 leading-relaxed max-w-xs">{node.description}</div>
          </div>
          <button onClick={onClose} className="text-slate-500 hover:text-white ml-2">
            <X className="w-4 h-4"/>
          </button>
        </div>

        <div className="space-y-3">
          <div>
            <label className="text-xs text-slate-400 mb-1 block">
              Valeur ({node.unit})
              {node.node_type === 'input_manual' && coeff !== 0 &&
                <span className="ml-1.5 text-slate-500">× {coeff} = <span className={pipColor(preview)}>{fmt(preview)} pips</span></span>}
              {node.node_type === 'input_event' && <span className="ml-1.5 text-slate-500">niveau structurel (les events s'y ajoutent)</span>}
            </label>
            <input
              type="number" step="any" value={val} onChange={e => setVal(e.target.value)}
              placeholder="0"
              className="w-full bg-dark-700 border border-slate-700/50 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500/60"
            />
          </div>
          <div>
            <label className="text-xs text-slate-400 mb-1 block">Note (optionnel)</label>
            <input
              type="text" value={note} onChange={e => setNote(e.target.value)}
              placeholder="Ex: NFP très fort, marché hawkish…"
              className="w-full bg-dark-700 border border-slate-700/50 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500/60"
            />
          </div>
        </div>

        {val !== '' && (
          <div className={clsx('mt-3 rounded px-3 py-2 text-sm font-semibold',
            preview > 0 ? 'bg-emerald-900/30 text-emerald-400'
            : preview < 0 ? 'bg-red-900/30 text-red-400'
            : 'bg-slate-800 text-slate-400')}>
            Impact estimé : {fmt(preview)} pips
          </div>
        )}

        <div className="flex gap-2 mt-4">
          <button onClick={save} disabled={saving || val === ''}
            className="flex-1 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white text-sm rounded px-3 py-2 font-medium transition-colors">
            {saving ? 'Enregistrement…' : 'Enregistrer'}
          </button>
          {node.source === 'manual' && (
            <button onClick={clear} disabled={clrg}
              className="flex items-center gap-1.5 bg-red-900/40 hover:bg-red-800/50 disabled:opacity-40 text-red-400 text-sm rounded px-3 py-2 transition-colors">
              <Trash2 className="w-3.5 h-3.5"/> {clrg ? '…' : 'Effacer'}
            </button>
          )}
          <button onClick={onClose} className="bg-dark-700 hover:bg-dark-600 text-slate-400 text-sm rounded px-3 py-2 transition-colors">
            Annuler
          </button>
        </div>
      </div>
    </div>
  )
}

// ── DAG View ──────────────────────────────────────────────────────────────────

const DEFAULT_COL_LABELS = ['Events actifs', 'Variables manuelles', 'Couches intermédiaires', 'Résultat net']

function NodeCard({ node, onEdit, eventDetails }: {
  node: ModelNode; onEdit: (n: ModelNode) => void
  eventDetails?: Record<string, EventDetail[]>
}) {
  const meta    = NODE_TYPE_META[node.node_type]
  const v       = node.pip_contribution
  const canEdit = node.node_type === 'input_event' || node.node_type === 'input_manual'
  const isOutput = node.node_type === 'output'

  return (
    <div
      className={clsx(
        'relative rounded border px-2 py-1.5 mb-1 transition-all select-none',
        meta.bg,
        canEdit && 'cursor-pointer hover:border-blue-500/60',
        node.source === 'manual' && 'ring-1 ring-violet-500/25',
        isOutput && 'py-2',
      )}
      onClick={() => canEdit && onEdit(node)}
      title={node.description}
    >
      {node.source === 'manual' && <span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-violet-400"/>}
      {node.source === 'events'  && <span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-sky-400"/>}

      {/* Label — truncated, single line */}
      <div className="text-xs text-slate-300 font-medium truncate pr-3 leading-tight">{node.label}</div>

      {/* Value row — compact, single line */}
      <div className="flex items-center gap-1.5 mt-0.5 flex-wrap">
        {node.node_type === 'input_manual' ? (() => {
          const hasValue = node.source === 'manual' && node.raw_value !== undefined && node.raw_value !== 0
          return hasValue ? (
            <>
              <span className={clsx('text-xs font-mono font-bold', pipColor(v))}>
                {node.raw_value! > 0 ? '+' : ''}{node.raw_value} {node.unit}
              </span>
              <span className={clsx('text-xs font-mono', pipColor(v))}>={fmt(v)}p</span>
            </>
          ) : (
            <span className="text-xs text-slate-600 font-mono">— {node.unit}</span>
          )
        })() : node.node_type === 'input_event' ? (() => {
          const cat     = node.event_category || ''
          const evList  = eventDetails?.[cat] || []
          const maxLf   = evList.length > 0 ? Math.max(...evList.map(e => e.lifecycle_factor)) : 0
          const baseline = node.baseline_value ?? 0
          return (
            <>
              <span className={clsx('text-xs font-mono font-bold', pipColor(v))}>{fmt(v)}p</span>
              {baseline !== 0 && (
                <span className="text-xs text-violet-300/70 font-mono">b:{fmt(baseline)}</span>
              )}
              {evList.length > 0 && (
                <>
                  <div className="w-10 h-0.5 rounded bg-slate-700/60 overflow-hidden">
                    <div className={clsx('h-full rounded', pipBg(v))} style={{width:`${Math.round(maxLf*100)}%`}}/>
                  </div>
                  <span className="text-slate-600 text-xs">{evList.length}ev</span>
                </>
              )}
            </>
          )
        })() : node.node_type === 'intermediate' ? (
          <>
            <span className={clsx('text-xs font-mono font-bold', pipColor(v))}>{fmt(v)}p</span>
            <div className="flex-1 h-0.5 min-w-[20px] rounded bg-slate-700/40 overflow-hidden">
              <div className={clsx('h-full rounded', pipBg(v))} style={{width:`${Math.min(100, Math.abs(v/60)*100)}%`}}/>
            </div>
          </>
        ) : (
          /* output */
          <span className={clsx('text-sm font-mono font-bold', pipColor(v))}>{fmt(v)} pips</span>
        )}
      </div>
    </div>
  )
}

interface EdgeDraw {
  x1: number; y1: number; x2: number; y2: number
  formulaCoeff: number
  displayWeight: number | null   // weight shown on edge line
  fromId: string; toId: string; key: string
  fromType: NodeType
}

function DagView({ nodes, instrument, onEdit, eventDetails, colLabels }: {
  nodes: ModelNode[]; instrument: string; onEdit: (n: ModelNode) => void
  eventDetails: Record<string, EventDetail[]>
  colLabels?: string[]
}) {
  const wrapperRef = useRef<HTMLDivElement>(null)
  const nodeRefs   = useRef<Map<string, HTMLDivElement>>(new Map())
  const [svgEdges, setSvgEdges] = useState<EdgeDraw[]>([])
  const [hoveredId, setHoveredId] = useState<string | null>(null)
  const [svgH, setSvgH] = useState(600)

  // Separate hidden nodes (display_col < 0 = event chocs) from visible nodes
  const hiddenEvents = useMemo(() => nodes.filter(n => (n.display_col ?? 0) < 0), [nodes])
  const visibleNodes = useMemo(() => nodes.filter(n => (n.display_col ?? 0) >= 0), [nodes])

  // Build dynamic columns from visible nodes
  const maxCol = useMemo(() =>
    visibleNodes.reduce((m, n) => Math.max(m, n.display_col ?? 0), 0), [visibleNodes])
  const cols = useMemo(() => {
    const c: ModelNode[][] = Array.from({ length: maxCol + 1 }, () => [])
    for (const n of visibleNodes) c[n.display_col ?? 0].push(n)
    return c
  }, [visibleNodes, maxCol])

  const effectiveColLabels = useMemo(() => {
    if (colLabels && colLabels.length >= cols.length) return colLabels
    // Fall back: fill with defaults then generic labels
    const base = [...DEFAULT_COL_LABELS]
    while (base.length <= maxCol) base.push(`Couche ${base.length}`)
    return base
  }, [colLabels, cols.length, maxCol])

  const nodeMap = useMemo(() => {
    const m = new Map<string, ModelNode>()
    for (const n of nodes) m.set(n.id, n)
    return m
  }, [nodes])

  const connections = useMemo(() => {
    const result: { from: string; to: string; coeff: number }[] = []
    for (const node of nodes) {
      if (!node.formula) continue
      for (const term of node.formula.split('+')) {
        const t = term.trim()
        // Match: optional_sign digit.digit * identifier  (e.g. "0.55*ecb_rate" or "0.04*usd_demand")
        const m1 = t.match(/^(-?[\d.]+)\s*\*\s*(\w+)$/)
        if (m1) {
          result.push({ from: m1[2], to: node.id, coeff: parseFloat(m1[1]) })
        } else if (/^\w+$/.test(t)) {
          result.push({ from: t, to: node.id, coeff: 1.0 })
        }
      }
    }
    return result
  }, [nodes])

  const recompute = useCallback(() => {
    const wrapper = wrapperRef.current
    if (!wrapper) return
    const rect0 = wrapper.getBoundingClientRect()
    const sL    = wrapper.scrollLeft
    const sT    = wrapper.scrollTop
    const edges: EdgeDraw[] = []

    for (const conn of connections) {
      const fromEl = nodeRefs.current.get(conn.from)
      const toEl   = nodeRefs.current.get(conn.to)
      if (!fromEl || !toEl) continue

      const fr       = fromEl.getBoundingClientRect()
      const tr       = toEl.getBoundingClientRect()
      const fromNode = nodeMap.get(conn.from)
      const fromType = fromNode?.node_type ?? 'input_event'

      // Weight to display on edge = formula coefficient (always, if not 1.0)
      const displayWeight: number | null = conn.coeff !== 1.0 ? conn.coeff : null

      edges.push({
        x1: fr.right - rect0.left + sL,
        y1: fr.top + fr.height / 2 - rect0.top + sT,
        x2: tr.left  - rect0.left + sL,
        y2: tr.top + tr.height / 2 - rect0.top + sT,
        formulaCoeff: conn.coeff,
        displayWeight,
        fromId: conn.from,
        toId:   conn.to,
        key:    `${conn.from}->${conn.to}`,
        fromType: fromType as NodeType,
      })
    }
    setSvgEdges(edges)
    setSvgH(wrapper.scrollHeight)
  }, [connections, nodeMap])

  useEffect(() => {
    const t = setTimeout(recompute, 100)
    const ro = new ResizeObserver(() => setTimeout(recompute, 80))
    if (wrapperRef.current) ro.observe(wrapperRef.current)
    return () => { clearTimeout(t); ro.disconnect() }
  }, [recompute])

  function edgeColor(e: EdgeDraw): string {
    if (e.fromType === 'input_manual') return '#a78bfa'
    if (e.fromType === 'intermediate') return '#f59e0b'
    return '#38bdf8'
  }

  return (
    <div ref={wrapperRef} className="relative overflow-x-auto" onScroll={recompute}>
      {/* SVG arêtes — z:5, sous les cartes z:10 */}
      <svg
        className="absolute top-0 left-0 pointer-events-none"
        style={{ width: '100%', height: svgH, zIndex: 5, overflow: 'visible' }}
        aria-hidden="true"
      >
        {svgEdges.map(e => {
          const isHov      = hoveredId === e.fromId || hoveredId === e.toId
          const isLeafEdge = e.fromType === 'input_manual'
          const color      = edgeColor(e)
          const sw         = isHov ? 2.5 : isLeafEdge ? 1 : 1.5
          const opacity    = isHov ? 0.95 : isLeafEdge ? 0.25 : 0.65
          const dx         = Math.min(60, (e.x2 - e.x1) * 0.35)
          const curve      = `M ${e.x1} ${e.y1} C ${e.x1+dx} ${e.y1}, ${e.x2-dx} ${e.y2}, ${e.x2} ${e.y2}`
          const mx         = (e.x1 + e.x2) / 2
          const my         = (e.y1 + e.y2) / 2
          const showWeight = e.displayWeight !== null && (!isLeafEdge || isHov)
          const wLabel     = e.displayWeight !== null
            ? (Math.abs(e.displayWeight) >= 10
                ? `${e.displayWeight > 0 ? '' : '−'}${Math.round(Math.abs(e.displayWeight))}`
                : `${e.displayWeight.toFixed(2)}`)
            : ''

          return (
            <g key={e.key} opacity={opacity}>
              <path d={curve} fill="none" stroke={color} strokeWidth={sw}/>
              <polygon
                points={`${e.x2},${e.y2} ${e.x2-5},${e.y2-2.5} ${e.x2-5},${e.y2+2.5}`}
                fill={color} fillOpacity={0.9}
              />
              {showWeight && (
                <text x={mx} y={my - 5} fill={color} fontSize="8" fontFamily="monospace"
                  textAnchor="middle" dominantBaseline="middle" style={{ userSelect:'none' }}>
                  {wLabel}
                </text>
              )}
            </g>
          )
        })}
      </svg>

      {/* Chocs CT (événements cachés du DAG) */}
      {hiddenEvents.length > 0 && (
        <div className="flex flex-wrap gap-1.5 mb-3 pb-2 border-b border-slate-700/20">
          <span className="text-xs text-slate-600 uppercase tracking-wider self-center mr-1">Chocs CT</span>
          {hiddenEvents.map(ev => {
            const v = ev.pip_contribution
            const hasVal = v !== 0 || ev.baseline_value
            return (
              <div
                key={ev.id}
                className={clsx(
                  'px-2 py-1 rounded border text-xs cursor-pointer transition-colors',
                  hasVal
                    ? 'border-sky-700/50 bg-sky-900/20 text-sky-300'
                    : 'border-slate-700/30 bg-dark-800/30 text-slate-600',
                )}
                onClick={() => onEdit(ev)}
                title={ev.description}
              >
                <span className="font-medium">{ev.label}</span>
                {hasVal && (
                  <span className={clsx('ml-1 font-mono font-bold', pipColor(v))}>
                    {v > 0 ? '+' : ''}{fmt(v)}p
                  </span>
                )}
              </div>
            )
          })}
        </div>
      )}

      {/* Grille dynamique — z:10 */}
      <div
        className="grid gap-x-3"
        style={{
          gridTemplateColumns: `repeat(${cols.length}, minmax(130px, 1fr))`,
          minWidth: `${cols.length * 145}px`,
          position: 'relative',
          zIndex: 10,
        }}
      >
        {effectiveColLabels.slice(0, cols.length).map((lbl, ci) => (
          <div key={ci}>
            <div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 pb-1 border-b border-slate-700/30 truncate">
              {lbl} <span className="font-normal normal-case text-slate-600">({cols[ci]?.length ?? 0})</span>
            </div>
            {(cols[ci] ?? []).map(n => (
              <div
                key={n.id}
                ref={el => { if (el) nodeRefs.current.set(n.id, el); else nodeRefs.current.delete(n.id) }}
                onMouseEnter={() => { setHoveredId(n.id); recompute() }}
                onMouseLeave={() => setHoveredId(null)}
              >
                <NodeCard node={n} onEdit={onEdit} eventDetails={eventDetails}/>
              </div>
            ))}
          </div>
        ))}
      </div>

      {/* Légende */}
      <div className="flex items-center gap-3 mt-3 pt-3 border-t border-slate-700/20 flex-wrap text-xs">
        <div className="flex items-center gap-1 text-violet-400">
          <span className="w-4 h-px bg-violet-400 inline-block"/> Macro → dérivé <span className="text-slate-600 ml-0.5">(w: survol)</span>
        </div>
        <div className="flex items-center gap-1 text-amber-400">
          <span className="w-4 h-px bg-amber-400 inline-block"/> Dérivé → agrégat <span className="text-slate-600 ml-0.5">(w: visible)</span>
        </div>
        <div className="ml-auto flex items-center gap-1 text-slate-500">
          <Edit3 className="w-3 h-3"/> Cliquer nœud pour éditer · <span className="text-sky-400">Chocs CT</span> = chocs court terme additifs
        </div>
      </div>
    </div>
  )
}

// ── Table View ────────────────────────────────────────────────────────────────

type SortKey = 'label' | 'category' | 'pip_contribution' | 'source'

function TableView({ nodes, onEdit }: { nodes: ModelNode[]; onEdit: (n: ModelNode) => void }) {
  const [sort, setSort]         = useState<SortKey>('pip_contribution')
  const [sortDir, setSortDir]   = useState<1 | -1>(-1)
  const [filterType, setFilterType] = useState<string>('all')
  const [filterCat, setFilterCat]   = useState<string>('all')
  const [expanded, setExpanded]     = useState<string | null>(null)

  const categories = useMemo(() => {
    const s = new Set(nodes.map(n => n.category))
    return ['all', ...Array.from(s).sort()]
  }, [nodes])

  const maxAbs = useMemo(() =>
    Math.max(...nodes.map(n => Math.abs(n.pip_contribution)), 1), [nodes])

  const sorted = useMemo(() => {
    const filtered = nodes.filter(n =>
      (filterType === 'all' || n.node_type === filterType) &&
      (filterCat  === 'all' || n.category === filterCat)
    )
    return [...filtered].sort((a, b) => {
      if (sort === 'pip_contribution') return (a.pip_contribution - b.pip_contribution) * sortDir
      const av = sort === 'label' ? a.label : sort === 'category' ? a.category : a.source
      const bv = sort === 'label' ? b.label : sort === 'category' ? b.category : b.source
      return String(av).localeCompare(String(bv)) * sortDir
    })
  }, [nodes, filterType, filterCat, sort, sortDir])

  function toggle(key: SortKey) {
    if (sort === key) setSortDir(d => d === 1 ? -1 : 1)
    else { setSort(key); setSortDir(-1) }
  }

  function SortBtn({ k, label }: { k: SortKey; label: string }) {
    return (
      <button onClick={() => toggle(k)}
        className={clsx('flex items-center gap-1', sort === k ? 'text-blue-400' : 'text-slate-500 hover:text-slate-300')}>
        {label}
        {sort === k && (sortDir === -1 ? <ChevronDown className="w-3 h-3"/> : <ChevronUp className="w-3 h-3"/>)}
      </button>
    )
  }

  const TYPE_OPTS = [
    { value:'all',          label:'Tous' },
    { value:'input_event',  label:'Events' },
    { value:'input_manual', label:'Manuels' },
    { value:'intermediate', label:'Interméd.' },
    { value:'output',       label:'Output' },
  ]

  return (
    <div>
      <div className="flex flex-wrap gap-2 mb-3">
        {TYPE_OPTS.map(o => (
          <button key={o.value} onClick={() => setFilterType(o.value)}
            className={clsx('px-2.5 py-1 rounded text-xs font-medium transition-colors',
              filterType === o.value ? 'bg-blue-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}>
            {o.label}
          </button>
        ))}
        <select value={filterCat} onChange={e => setFilterCat(e.target.value)}
          className="bg-dark-700 border border-slate-700/40 rounded px-2 py-1 text-xs text-slate-400 focus:outline-none ml-2">
          {categories.map(c => <option key={c} value={c}>{c === 'all' ? 'Toutes catégories' : c}</option>)}
        </select>
      </div>

      <div className="rounded-lg border border-slate-700/40 overflow-hidden">
        <table className="w-full text-xs">
          <thead>
            <tr className="bg-dark-700/60 border-b border-slate-700/40 text-slate-500">
              <th className="px-3 py-2 text-left w-8">T</th>
              <th className="px-3 py-2 text-left"><SortBtn k="label" label="Nœud"/></th>
              <th className="px-3 py-2 text-left hidden sm:table-cell"><SortBtn k="category" label="Catégorie"/></th>
              <th className="px-3 py-2 text-right"><SortBtn k="pip_contribution" label="Pips"/></th>
              <th className="px-3 py-2 text-center"><SortBtn k="source" label="Source"/></th>
              <th className="px-3 py-2 text-center w-24">Bar</th>
              <th className="px-3 py-2 text-center w-8"/>
            </tr>
          </thead>
          <tbody>
            {sorted.map(node => {
              const meta    = NODE_TYPE_META[node.node_type]
              const v       = node.pip_contribution
              const isExp   = expanded === node.id
              const canEdit = node.node_type === 'input_event' || node.node_type === 'input_manual'
              return [
                <tr key={node.id}
                  className={clsx('border-b border-slate-700/20 hover:bg-dark-700/20 transition-colors cursor-pointer', isExp && 'bg-dark-700/10')}>
                  <td className="px-3 py-2">
                    <span className={clsx('font-bold text-xs', meta.color)}>
                      {node.node_type === 'input_event' ? 'EV' : node.node_type === 'input_manual' ? 'M' : node.node_type === 'intermediate' ? 'INT' : 'OUT'}
                    </span>
                  </td>
                  <td className="px-3 py-2" onClick={() => setExpanded(isExp ? null : node.id)}>
                    <span className="text-slate-300 font-medium hover:text-white">
                      {node.label}
                      {node.source === 'manual' && <span className="ml-1 text-violet-400">●</span>}
                    </span>
                  </td>
                  <td className="px-3 py-2 text-slate-500 hidden sm:table-cell">{node.category}</td>
                  <td className={clsx('px-3 py-2 text-right font-mono font-bold', pipColor(v))}>{fmt(v)}</td>
                  <td className="px-3 py-2 text-center">
                    <span className="flex items-center justify-center gap-1">
                      <span className={clsx('w-1.5 h-1.5 rounded-full', SOURCE_META[node.source]?.dot || 'bg-slate-600')}/>
                      <span className="text-slate-500">{SOURCE_META[node.source]?.label}</span>
                    </span>
                  </td>
                  <td className="px-3 py-2">
                    <div className="h-1.5 rounded-full bg-slate-700/40 overflow-hidden w-full">
                      <div className={clsx('h-full rounded-full', pipBg(v))}
                        style={{width: `${Math.min(100, Math.abs(v / maxAbs) * 100)}%`}}/>
                    </div>
                  </td>
                  <td className="px-3 py-2 text-center">
                    {canEdit && (
                      <button onClick={() => onEdit(node)} className="text-slate-600 hover:text-blue-400 transition-colors">
                        <Edit3 className="w-3.5 h-3.5"/>
                      </button>
                    )}
                  </td>
                </tr>,
                isExp && (
                  <tr key={`${node.id}-exp`} className="bg-dark-800/40 border-b border-slate-700/10">
                    <td/>
                    <td colSpan={6} className="px-3 py-2 text-xs text-slate-400 space-y-1">
                      <div>{node.description}</div>
                      {node.node_type === 'input_manual' && node.coefficient_to_pips !== undefined && (
                        <div className="text-slate-500">
                          Coeff : {node.coefficient_to_pips} pips/{node.unit}
                          {node.raw_value !== undefined ? ` · ${node.raw_value} ${node.unit}` : ''}
                          {node.pip_linear !== undefined && node.pip_saturated !== undefined && (
                            <span>
                              {' '}→ linéaire <span className="text-slate-400">{fmt(node.pip_linear)}</span>
                              {' '}/ saturé <span className={pipColor(node.pip_saturated)}>{fmt(node.pip_saturated)}</span>
                              {node.saturation_pct !== undefined && node.saturation_pct > 1 && (
                                <span className="text-amber-500/70"> (sat. {node.saturation_pct.toFixed(0)}%)</span>
                              )}
                            </span>
                          )}
                        </div>
                      )}
                      {node.node_type === 'intermediate' && node.formula && (
                        <div className="text-slate-500 font-mono text-xs">{node.formula}</div>
                      )}
                      {node.override_note && <div className="text-violet-400 italic">Note : {node.override_note}</div>}
                      {node.override_set_at && <div className="text-slate-600">Modifié : {node.override_set_at.slice(0,10)}</div>}
                    </td>
                  </tr>
                ),
              ].filter(Boolean)
            })}
          </tbody>
        </table>
        {sorted.length === 0 && (
          <div className="text-center py-6 text-slate-500 text-sm">Aucun nœud pour ce filtre</div>
        )}
      </div>
    </div>
  )
}

// ── Timeline Chart ─────────────────────────────────────────────────────────────

const REGIME_CANVAS_COLORS: Record<string, string> = {
  MONETARY_DOMINANCE: 'rgba(59,130,246,0.07)',
  GEOPOLITICAL_RISK:  'rgba(249,115,22,0.07)',
  CREDIT_STRESS:      'rgba(239,68,68,0.07)',
  GROWTH_SCARE:       'rgba(245,158,11,0.07)',
  COMMODITY_SHOCK:    'rgba(234,179,8,0.07)',
  BALANCED:           'rgba(0,0,0,0)',
}

function newVirtualEvent(): VirtualEventForm {
  return {
    id:             Math.random().toString(36).slice(2),
    date:           new Date().toISOString().slice(0, 10),
    category:       'central_bank',
    pips:           50,
    label:          'Event virtuel',
    absorption_days: 14,
    rise_days:       1,
    plateau_days:    0,
    decay_type:      'exp',
    disabled:        false,
  }
}

function TimelineView({ instrument }: { instrument: string }) {
  const [period, setPeriod]         = useState<Period>('3mo')
  const [startDate, setStartDate]   = useState<string | null>(null)  // override period-based start
  const [data, setData]             = useState<TimelinePoint[]>([])
  const [realPrices, setRealPrices] = useState<PricePoint[]>([])
  const [loading, setLoading]       = useState(false)
  const [showReal, setShowReal]     = useState(true)
  const [showFund, setShowFund]     = useState(true)
  const [showPips, setShowPips]     = useState(false)  // toggle: price vs pips
  const [layerKeys, setLayerKeys]   = useState<string[]>([])
  const [shownLayers, setShownLayers] = useState<Set<string>>(new Set())
  const [showVirtual, setShowVirtual] = useState(false)
  const [virtuals, setVirtuals]     = useState<VirtualEventForm[]>([])
  const [whatifLoading, setWhatifLoading] = useState(false)
  const [whatifData, setWhatifData] = useState<TimelinePoint[] | null>(null)
  const [importingCal, setImportingCal] = useState(false)
  const [backfilling,  setBackfilling]  = useState(false)
  const [calMsg, setCalMsg]             = useState<string | null>(null)
  // What-if filters
  const [filterImpact,   setFilterImpact]   = useState<string[]>(['high', 'medium'])
  const [filterCurrency, setFilterCurrency] = useState<string[]>([])
  const [filterSurprise, setFilterSurprise] = useState(false)
  const [filterMinPips,  setFilterMinPips]  = useState(0)
  const canvasRef                   = useRef<HTMLCanvasElement>(null)
  const [hoverIdx, setHoverIdx]     = useState<number | null>(null)
  const [hoverX,   setHoverX]       = useState<number | null>(null)   // CSS px, for crosshair

  const activeData = whatifData ?? data

  // What-if: events actually sent to simulation (pass filters)
  const availCurrencies = useMemo(() =>
    [...new Set(virtuals.map(v => v.currency).filter(Boolean) as string[])].sort(),
    [virtuals])

  const activeVirtuals = useMemo(() => virtuals.filter(ve => {
    if (ve.disabled) return false
    if (filterImpact.length > 0 && ve.impact && !filterImpact.includes(ve.impact)) return false
    if (filterCurrency.length > 0 && ve.currency && !filterCurrency.includes(ve.currency)) return false
    if (filterSurprise && !ve.has_surprise) return false
    if (filterMinPips > 0 && Math.abs(ve.pips) < filterMinPips) return false
    return true
  }), [virtuals, filterImpact, filterCurrency, filterSurprise, filterMinPips])

  // Load timeline
  useEffect(() => {
    setLoading(true)
    setWhatifData(null)
    const sdParam = startDate ? `&start_date=${startDate}` : ''
    api.get<TimelinePoint[]>(`/instrument-models/${instrument}/timeline?period=${period}${sdParam}`)
      .then(r => {
        const pts = r.data
        setData(pts)
        if (pts.length > 0) {
          const keys = Object.keys(pts[0].nodes).filter(k => k.startsWith('layer_'))
          setLayerKeys(keys)
        }
      })
      .catch(() => setData([]))
      .finally(() => setLoading(false))
  }, [instrument, period, startDate])

  // Load real price history — compute the right period when a custom startDate is set
  useEffect(() => {
    const phPeriod = startDate ? (() => {
      const days = Math.ceil((Date.now() - new Date(startDate).getTime()) / 86_400_000) + 10
      if (days <= 7)   return '5d'
      if (days <= 35)  return '1mo'
      if (days <= 95)  return '3mo'
      if (days <= 190) return '6mo'
      if (days <= 370) return '1y'
      return '2y'
    })() : period
    api.get<{ prices: PricePoint[] }>(`/instrument-models/${instrument}/price-history?period=${phPeriod}`)
      .then(r => setRealPrices(r.data.prices || []))
      .catch(() => setRealPrices([]))
  }, [instrument, period, startDate])

  // Draw canvas
  useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas || activeData.length === 0) return
    const ctx = canvas.getContext('2d')
    if (!ctx) return

    const W = canvas.width  = canvas.offsetWidth
    const H = canvas.height = 300
    ctx.clearRect(0, 0, W, H)

    // Build date→index map for real prices alignment
    const dateToIdx = new Map<string, number>()
    activeData.forEach((pt, i) => dateToIdx.set(pt.date, i))

    // Value extractor depending on mode
    const getV = (pt: TimelinePoint) => showPips ? pt.net_pips : pt.synthetic_price
    const getFundV = (pt: TimelinePoint) => showPips ? pt.structural_pips : pt.fundamental_level

    let minV = Infinity, maxV = -Infinity
    for (const pt of activeData) {
      const v = getV(pt)
      if (v < minV) minV = v
      if (v > maxV) maxV = v
      if (showFund) {
        const f = getFundV(pt)
        if (f < minV) minV = f
        if (f > maxV) maxV = f
      }
    }
    if (showReal && realPrices.length > 0 && !showPips) {
      for (const p of realPrices) {
        if (p.close < minV) minV = p.close
        if (p.close > maxV) maxV = p.close
      }
    }
    for (const k of shownLayers) {
      for (const pt of activeData) {
        const v = pt.nodes[k] ?? 0
        if (v < minV) minV = v
        if (v > maxV) maxV = v
      }
    }

    const pad = Math.max(showPips ? 5 : 0.001, 0.08 * (maxV - minV))
    minV -= pad; maxV += pad

    const mL = 64, mR = 16, mT = 12, mB = 28
    const cW = W - mL - mR, cH = H - mT - mB
    const toX = (i: number) => mL + (i / Math.max(activeData.length - 1, 1)) * cW
    const toY = (v: number) => mT + (1 - (v - minV) / (maxV - minV)) * cH

    // Regime bands
    let bandStart = 0, bandRegime = activeData[0]?.regime || 'BALANCED'
    for (let i = 1; i <= activeData.length; i++) {
      const r = i < activeData.length ? (activeData[i]?.regime || 'BALANCED') : null
      if (r !== bandRegime || i === activeData.length) {
        const color = REGIME_CANVAS_COLORS[bandRegime]
        if (color && color !== 'rgba(0,0,0,0)') {
          ctx.fillStyle = color
          ctx.fillRect(toX(bandStart), mT, toX(Math.max(i - 1, bandStart)) - toX(bandStart) + 1, cH)
        }
        bandStart = i; bandRegime = r || 'BALANCED'
      }
    }

    // Grid
    const nG = 5
    for (let i = 0; i <= nG; i++) {
      const v = minV + (i / nG) * (maxV - minV)
      const y = toY(v)
      ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1
      ctx.beginPath(); ctx.moveTo(mL, y); ctx.lineTo(W - mR, y); ctx.stroke()
      ctx.fillStyle = '#64748b'; ctx.font = '10px sans-serif'; ctx.textAlign = 'right'
      const lbl = showPips ? fmt(v) : v.toFixed(v > 100 ? 1 : 4)
      ctx.fillText(lbl, mL - 4, y + 3.5)
    }

    if (showPips && minV < 0 && maxV > 0) {
      const y0 = toY(0)
      ctx.strokeStyle = '#334155'; ctx.setLineDash([3,3]); ctx.lineWidth = 1
      ctx.beginPath(); ctx.moveTo(mL, y0); ctx.lineTo(W - mR, y0); ctx.stroke()
      ctx.setLineDash([])
    }

    // X labels
    const step = Math.max(1, Math.floor(activeData.length / Math.floor(cW / 60)))
    ctx.fillStyle = '#64748b'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'
    for (let i = 0; i < activeData.length; i += step) {
      ctx.fillText(activeData[i].date.slice(5), toX(i), H - 4)
    }

    // Virtual event markers
    if (virtuals.length > 0) {
      ctx.fillStyle = '#f59e0b'
      for (const ve of virtuals) {
        const idx = dateToIdx.get(ve.date)
        if (idx !== undefined) {
          const x = toX(idx)
          ctx.beginPath()
          ctx.moveTo(x, mT + cH)
          ctx.lineTo(x - 4, mT + cH + 6)
          ctx.lineTo(x + 4, mT + cH + 6)
          ctx.closePath()
          ctx.fill()
        }
      }
    }

    // Real price line (light grey)
    if (showReal && !showPips && realPrices.length > 0) {
      ctx.strokeStyle = 'rgba(148,163,184,0.5)'; ctx.lineWidth = 1.5; ctx.setLineDash([])
      ctx.beginPath()
      let started = false
      for (const p of realPrices) {
        const idx = dateToIdx.get(p.date)
        if (idx !== undefined) {
          const x = toX(idx); const y = toY(p.close)
          if (!started) { ctx.moveTo(x, y); started = true } else { ctx.lineTo(x, y) }
        }
      }
      ctx.stroke()
    }

    // Fundamental level (dashed amber)
    if (showFund) {
      ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 1; ctx.setLineDash([5, 4])
      ctx.beginPath()
      activeData.forEach((pt, i) => {
        const y = toY(getFundV(pt))
        i === 0 ? ctx.moveTo(toX(i), y) : ctx.lineTo(toX(i), y)
      })
      ctx.stroke()
      ctx.setLineDash([])
    }

    // Layer lines (dashed, thin)
    for (const k of shownLayers) {
      const color = CANVAS_COLORS[k] || '#94a3b8'
      ctx.strokeStyle = color; ctx.lineWidth = 1.2; ctx.setLineDash([4, 3])
      ctx.beginPath()
      activeData.forEach((pt, i) => {
        const v = showPips ? (pt.nodes[k] ?? 0) : (pt.synthetic_price + (pt.nodes[k] ?? 0) * (1 / Math.max(activeData.length, 1)))
        i === 0 ? ctx.moveTo(toX(i), toY(showPips ? v : (pt.nodes[k] ?? 0))) : ctx.lineTo(toX(i), toY(showPips ? v : (pt.nodes[k] ?? 0)))
      })
      ctx.stroke()
      ctx.setLineDash([])
    }

    // Synthetic price (main bold line — green/red)
    const lastPt = activeData[activeData.length - 1]
    const lastFirst = activeData[0]
    const trending = getV(lastPt) > getV(lastFirst)
    ctx.strokeStyle = trending ? '#10b981' : '#f87171'; ctx.lineWidth = 2.5; ctx.setLineDash([])
    ctx.beginPath()
    activeData.forEach((pt, i) => {
      i === 0 ? ctx.moveTo(toX(i), toY(getV(pt))) : ctx.lineTo(toX(i), toY(getV(pt)))
    })
    ctx.stroke()

    // What-if overlay (if exists, draw original in grey too)
    if (whatifData) {
      ctx.strokeStyle = '#6366f1'; ctx.lineWidth = 2; ctx.setLineDash([6, 3])
      ctx.beginPath()
      whatifData.forEach((pt, i) => {
        i === 0 ? ctx.moveTo(toX(i), toY(getV(pt))) : ctx.lineTo(toX(i), toY(getV(pt)))
      })
      ctx.stroke()
      ctx.setLineDash([])
    }

  }, [activeData, realPrices, showReal, showFund, showPips, shownLayers, virtuals, whatifData])

  function handleCanvasMouseMove(e: React.MouseEvent<HTMLCanvasElement>) {
    const canvas = canvasRef.current
    if (!canvas || activeData.length === 0) return
    const rect = canvas.getBoundingClientRect()
    const cssX  = e.clientX - rect.left
    const mL = 64, mR = 16
    const cW = rect.width - mL - mR
    const rawIdx = ((cssX - mL) / cW) * (activeData.length - 1)
    const idx = Math.max(0, Math.min(activeData.length - 1, Math.round(rawIdx)))
    setHoverIdx(idx)
    setHoverX(cssX)
  }

  async function runWhatIf() {
    if (activeVirtuals.length === 0) return
    setWhatifLoading(true)
    try {
      const r = await api.post<TimelinePoint[]>(`/instrument-models/${instrument}/timeline-whatif`, {
        period,
        start_date: startDate ?? undefined,
        virtual_events: activeVirtuals.map(ve => ({
          date:            ve.date,
          category:        ve.category,
          pips:            ve.pips,
          label:           ve.label,
          absorption_days: ve.absorption_days,
          rise_days:       ve.rise_days,
          plateau_days:    ve.plateau_days,
          decay_type:      ve.decay_type,
        })),
      })
      setWhatifData(r.data)
    } finally { setWhatifLoading(false) }
  }

  function toggleLayer(k: string) {
    setShownLayers(prev => { const n = new Set(prev); n.has(k) ? n.delete(k) : n.add(k); return n })
  }

  if (loading) return <div className="flex items-center justify-center h-64 text-slate-500 text-sm">Calcul timeline…</div>
  if (!loading && data.length === 0) return <div className="text-center py-8 text-slate-500 text-sm">Aucune donnée pour {instrument}</div>

  return (
    <div className="space-y-3">
      {/* Controls */}
      <div className="flex items-center gap-2 flex-wrap">
        {PERIODS.map(p => (
          <button key={p} onClick={() => { setPeriod(p); setStartDate(null) }}
            className={clsx('px-2 py-1 rounded text-xs font-medium transition-colors',
              period === p && !startDate ? 'bg-blue-600 text-white' : 'bg-dark-700 text-slate-400 hover:text-white')}>
            {p}
          </button>
        ))}
        {/* Custom start date */}
        <div className="flex items-center gap-1 ml-1">
          <span className="text-xs text-slate-500">depuis</span>
          <input
            type="date"
            value={startDate ?? ''}
            max={new Date().toISOString().slice(0, 10)}
            onChange={e => setStartDate(e.target.value || null)}
            className={clsx(
              'bg-dark-700 border rounded px-1.5 py-0.5 text-xs focus:outline-none transition-colors',
              startDate ? 'border-blue-600/50 text-blue-300' : 'border-slate-700/40 text-slate-500'
            )}
          />
          {startDate && (
            <button onClick={() => setStartDate(null)} className="text-slate-500 hover:text-slate-300 text-xs">✕</button>
          )}
        </div>
        <div className="ml-2 flex gap-1.5">
          <button onClick={() => setShowPips(v => !v)}
            className={clsx('px-2 py-1 rounded text-xs border transition-all',
              showPips ? 'border-blue-600/50 text-blue-400 bg-blue-900/20' : 'border-slate-700/40 text-slate-400')}>
            {showPips ? 'Mode pips' : 'Mode prix'}
          </button>
          <button onClick={() => setShowReal(v => !v)}
            className={clsx('flex items-center gap-1 px-2 py-1 rounded text-xs border transition-all',
              showReal ? 'border-slate-600/50 text-slate-300' : 'border-slate-700/30 text-slate-600')}>
            <span className="w-2 h-0.5 bg-slate-400 inline-block"/> Réel
          </button>
          <button onClick={() => setShowFund(v => !v)}
            className={clsx('flex items-center gap-1 px-2 py-1 rounded text-xs border transition-all',
              showFund ? 'border-amber-600/50 text-amber-400' : 'border-slate-700/30 text-slate-600')}>
            <span className="w-2 h-0.5 bg-amber-400 inline-block" style={{borderTop:'1px dashed #f59e0b'}}/> Fondamental
          </button>
          <button onClick={() => setShowVirtual(v => !v)}
            className={clsx('flex items-center gap-1.5 px-2 py-1 rounded text-xs border transition-all',
              showVirtual ? 'border-violet-600/50 text-violet-400 bg-violet-900/20' : 'border-slate-700/30 text-slate-500')}>
            <Zap className="w-3 h-3"/> What-if {activeVirtuals.length > 0 ? `(${activeVirtuals.length}/${virtuals.length})` : virtuals.length > 0 ? `(${virtuals.length})` : ''}
          </button>
        </div>
        <span className="ml-auto text-xs text-slate-500">{activeData.length} jours</span>
      </div>

      {/* Layer toggles */}
      {layerKeys.length > 0 && (
        <div className="flex flex-wrap gap-1.5">
          {layerKeys.map(k => (
            <button key={k} onClick={() => toggleLayer(k)}
              className={clsx('flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium border transition-all',
                shownLayers.has(k) ? 'border-slate-600/50 text-slate-300 bg-slate-800/40' : 'border-slate-700/30 text-slate-600')}>
              <span className={clsx('w-2 h-2 rounded-full', LAYER_COLORS[k] || 'bg-slate-500')}/>
              {k.replace('layer_', '')}
            </button>
          ))}
        </div>
      )}

      {/* Canvas */}
      <div className="rounded-lg bg-dark-900/50 border border-slate-700/30 p-2">
        {/* Canvas wrapper — relative for crosshair overlay */}
        <div className="relative"
          onMouseLeave={() => { setHoverIdx(null); setHoverX(null) }}>
          <canvas ref={canvasRef} className="w-full cursor-crosshair"
            style={{height: 300, display: 'block'}}
            onMouseMove={handleCanvasMouseMove}
          />
          {/* Crosshair vertical line */}
          {hoverX !== null && (
            <div className="absolute top-3 pointer-events-none"
              style={{left: hoverX, bottom: 28, width: 1, background: 'rgba(148,163,184,0.35)'}}/>
          )}
        </div>
        {/* Legend */}
        <div className="flex flex-wrap gap-3 mt-2 px-1">
          <div className="flex items-center gap-1.5 text-xs text-slate-500">
            <span className="w-4 h-0.5 bg-emerald-500 inline-block"/> Prix synthétique
          </div>
          {showReal && (
            <div className="flex items-center gap-1.5 text-xs text-slate-500">
              <span className="w-4 h-0.5 bg-slate-400 inline-block opacity-50"/> Prix réel
            </div>
          )}
          {showFund && (
            <div className="flex items-center gap-1.5 text-xs text-slate-500">
              <span className="w-4 h-0.5 border-t border-dashed border-amber-500 inline-block"/> Fondamental
            </div>
          )}
          {whatifData && (
            <div className="flex items-center gap-1.5 text-xs text-violet-400">
              <span className="w-4 h-0.5 border-t border-dashed border-violet-500 inline-block"/> What-if
              <button onClick={() => setWhatifData(null)} className="ml-1 text-slate-600 hover:text-slate-400">✕</button>
            </div>
          )}
        </div>

        {/* Hover detail panel */}
        {hoverIdx !== null && activeData[hoverIdx] && (() => {
          const pt   = activeData[hoverIdx]
          const prev = hoverIdx > 0 ? activeData[hoverIdx - 1] : null
          const dEvent = Math.round((pt.event_pips - (prev?.event_pips ?? 0)) * 10) / 10

          // Nœuds avec delta significatif vs J-1
          const nodeDeltas = Object.entries(pt.nodes)
            .map(([k, v]) => ({ k, v, d: Math.round((v - (prev?.nodes[k] ?? v)) * 10) / 10 }))
            .filter(({ d }) => Math.abs(d) >= 0.5)
            .sort((a, b) => Math.abs(b.d) - Math.abs(a.d))
            .slice(0, 8)

          return (
            <div className="mt-2 rounded border border-slate-700/30 bg-dark-800/60 px-2.5 py-2 text-xs space-y-1.5">
              {/* Header row */}
              <div className="flex items-center gap-3 flex-wrap">
                <span className="text-slate-400 font-mono font-semibold">{pt.date}</span>
                {!showPips && (
                  <span className="text-white font-medium">{pt.synthetic_price.toFixed(4)}</span>
                )}
                <span className={clsx('font-medium', pt.net_pips >= 0 ? 'text-emerald-400' : 'text-red-400')}>
                  {pt.net_pips >= 0 ? '+' : ''}{pt.net_pips}p
                </span>
                <span className="text-slate-500">struct:{pt.structural_pips}p</span>
                <span className={clsx(pt.event_pips >= 0 ? 'text-blue-400' : 'text-orange-400')}>
                  ev:{pt.event_pips >= 0 ? '+' : ''}{pt.event_pips}p
                  {dEvent !== 0 && (
                    <span className="text-slate-500 ml-0.5">
                      (Δ{dEvent > 0 ? '+' : ''}{dEvent})
                    </span>
                  )}
                </span>
                {pt.regime && pt.regime !== 'BALANCED' && (
                  <span className="text-slate-500 ml-auto">{pt.regime}</span>
                )}
              </div>

              {/* Active events */}
              {pt.active_events && pt.active_events.length > 0 && (
                <div className="space-y-0.5 border-t border-slate-700/30 pt-1.5">
                  {pt.active_events
                    .sort((a, b) => Math.abs(b.contribution) - Math.abs(a.contribution))
                    .map((ev, i) => (
                    <div key={i} className="flex items-center gap-2">
                      <span className={clsx('w-1.5 h-1.5 rounded-full flex-shrink-0',
                        ev.contribution >= 0 ? 'bg-emerald-500' : 'bg-red-500')}/>
                      <span className="flex-1 truncate text-slate-300">{ev.label}</span>
                      <span className="text-slate-600 font-mono">lf:{ev.lifecycle_factor.toFixed(2)}</span>
                      <span className={clsx('font-mono w-14 text-right',
                        ev.contribution >= 0 ? 'text-emerald-400' : 'text-red-400')}>
                        {ev.contribution >= 0 ? '+' : ''}{ev.contribution}p
                      </span>
                    </div>
                  ))}
                </div>
              )}

              {/* Node deltas */}
              {nodeDeltas.length > 0 && (
                <div className="flex flex-wrap gap-x-3 gap-y-0.5 border-t border-slate-700/30 pt-1.5">
                  <span className="text-slate-600 w-full text-xs">Nœuds Δ vs J-1 :</span>
                  {nodeDeltas.map(({ k, v, d }) => (
                    <span key={k} className="text-slate-500">
                      {k.replace('layer_', '').replace(/_/g, ' ')}:
                      <span className="text-slate-300 ml-0.5">{v.toFixed(1)}</span>
                      <span className={clsx('ml-0.5', d > 0 ? 'text-emerald-400' : 'text-red-400')}>
                        ({d > 0 ? '+' : ''}{d})
                      </span>
                    </span>
                  ))}
                </div>
              )}
            </div>
          )
        })()}
      </div>

      {/* Virtual Events Panel */}
      {showVirtual && (
        <div className="rounded-lg border border-violet-700/30 bg-violet-900/10 p-3 space-y-2">
          {/* Header */}
          <div className="flex items-center justify-between gap-2 flex-wrap">
            <div className="flex items-center gap-2">
              <span className="text-xs font-semibold text-violet-400">Events What-if</span>
              {virtuals.length > 0 && (
                <span className="text-xs text-slate-400">
                  {activeVirtuals.length} actif{activeVirtuals.length > 1 ? 's' : ''} / {virtuals.length} total
                </span>
              )}
            </div>
            <div className="flex gap-1.5">
              <button
                disabled={importingCal}
                onClick={async () => {
                  setImportingCal(true)
                  try {
                    const daysBack = startDate
                      ? Math.ceil((Date.now() - new Date(startDate).getTime()) / 86_400_000) + 5
                      : 365
                    const r = await api.get<CalendarEvent[]>(
                      `/instrument-models/${instrument}/calendar-events?days_back=${daysBack}&days_forward=90`
                    )
                    // Déduplique par (date, category) — garde le plus fort en valeur absolue
                    // évite de cumuler NFP + Non-Farm Payrolls qui sont le même release
                    const seen = new Map<string, CalendarEvent>()
                    for (const ev of r.data) {
                      const key = `${ev.date}_${ev.currency}_${ev.category}`
                      const prev = seen.get(key)
                      if (!prev || Math.abs(ev.estimated_pips) > Math.abs(prev.estimated_pips)) {
                        seen.set(key, ev)
                      }
                    }
                    const imported: VirtualEventForm[] = [...seen.values()].map((ev, i) => ({
                      id:              `ff_${ev.date}_${ev.currency}_${i}`,
                      date:            ev.date,
                      category:        ev.category,
                      pips:            ev.estimated_pips,
                      label:           ev.label,
                      absorption_days: 14,
                      rise_days:       1,
                      plateau_days:    0,
                      decay_type:      'exp',
                      currency:        ev.currency,
                      impact:          ev.impact,
                      has_surprise:    ev.has_surprise,
                      actual_value:    ev.actual_value,
                      forecast_value:  ev.forecast_value,
                      event_name:      ev.event_name,
                      routed_node:     ev.routed_node,
                      disabled:        false,
                    }))
                    if (imported.length === 0) {
                      setCalMsg('Aucun event FF calendrier (high/medium) pour cette période')
                    } else {
                      setVirtuals(prev => {
                        const existingIds = new Set(prev.map(v => v.id))
                        const added = imported.filter(x => !existingIds.has(x.id))
                        setCalMsg(`${added.length} event${added.length > 1 ? 's' : ''} importé${added.length > 1 ? 's' : ''}`)
                        return [...prev, ...added]
                      })
                    }
                    setTimeout(() => setCalMsg(null), 3500)
                  } finally { setImportingCal(false) }
                }}
                className="flex items-center gap-1 text-xs text-sky-400 hover:text-sky-300 border border-sky-700/40 rounded px-2 py-0.5 transition-colors disabled:opacity-40">
                <Activity className="w-3 h-3"/> {importingCal ? '…' : 'Import calendrier'}
              </button>
              <button
                disabled={backfilling}
                onClick={async () => {
                  setBackfilling(true)
                  setCalMsg('Backfill en cours (peut prendre 2-3 min)…')
                  try {
                    // Backfill from 15 months ago to today to fill the April 2025 gap
                    const fromDate = new Date()
                    fromDate.setMonth(fromDate.getMonth() - 15)
                    const r = await api.post<{weeks_scraped: number, total_upserted: number, weeks_skipped: number}>(
                      '/instrument-models/ff-calendar/backfill',
                      { from_date: fromDate.toISOString().slice(0, 10) }
                    )
                    setCalMsg(`Backfill OK : ${r.data.total_upserted} events, ${r.data.weeks_scraped} semaines (${r.data.weeks_skipped} déjà remplies)`)
                  } catch (e: any) {
                    setCalMsg(`Erreur backfill: ${e?.response?.data?.detail ?? e?.message}`)
                  } finally {
                    setBackfilling(false)
                    setTimeout(() => setCalMsg(null), 8000)
                  }
                }}
                className="flex items-center gap-1 text-xs text-emerald-400 hover:text-emerald-300 border border-emerald-700/40 rounded px-2 py-0.5 transition-colors disabled:opacity-40"
                title="Scrape ForexFactory pour remplir les données manquantes (avril 2025 → aujourd'hui)">
                <RefreshCw className="w-3 h-3"/> {backfilling ? '…' : 'Sync historique'}
              </button>
              <button onClick={() => {
                  // Guidance event : pousse la courbe depuis le début avec absorption lente
                  const simStart  = data[0]?.date ?? new Date().toISOString().slice(0, 10)
                  const simLen    = Math.max(30, data.length)
                  const bias      = data[0]?.structural_pips ?? 0
                  const guidId    = 'guidance_structural'
                  setVirtuals(prev => {
                    if (prev.some(v => v.id === guidId)) return prev  // déjà présent
                    return [{
                      id:              guidId,
                      date:            simStart,
                      category:        'unclassified',
                      pips:            bias,
                      label:           'Guidance macro',
                      absorption_days: 1,       // quasi-null decay à la fin
                      rise_days:       simLen,  // monte progressivement sur toute la fenêtre
                      plateau_days:    0,
                      decay_type:      'linear',
                    }, ...prev]
                  })
                }}
                className="flex items-center gap-1 text-xs text-amber-400 hover:text-amber-300 border border-amber-700/40 rounded px-2 py-0.5 transition-colors"
                title="Injecte une force directionnelle lente depuis le début de la simulation">
                <TrendingUp className="w-3 h-3"/> Guidance
              </button>
              <button onClick={() => setVirtuals(v => [...v, newVirtualEvent()])}
                className="flex items-center gap-1 text-xs text-violet-400 hover:text-violet-300 border border-violet-700/40 rounded px-2 py-0.5 transition-colors">
                <Plus className="w-3 h-3"/> Manuel
              </button>
            </div>
          </div>

          {calMsg && (
            <div className={clsx('text-xs px-2 py-1 rounded', calMsg.startsWith('Aucun') ? 'text-amber-400 bg-amber-900/20' : 'text-emerald-400 bg-emerald-900/20')}>
              {calMsg}
            </div>
          )}

          {/* Filter bar */}
          {virtuals.length > 0 && (
            <div className="flex flex-wrap gap-2 items-center py-1.5 border-y border-slate-700/30">
              {/* Impact filter */}
              <div className="flex items-center gap-1">
                {(['high', 'medium', 'low'] as const).map(imp => {
                  const active = filterImpact.includes(imp)
                  const cls = imp === 'high' ? 'border-red-700/60 text-red-400 bg-red-900/20' :
                               imp === 'medium' ? 'border-amber-700/60 text-amber-400 bg-amber-900/20' :
                               'border-slate-600/60 text-slate-400 bg-slate-800/30'
                  return (
                    <button key={imp} onClick={() => setFilterImpact(prev =>
                      prev.includes(imp) ? prev.filter(x => x !== imp) : [...prev, imp])}
                      className={clsx('px-1.5 py-0.5 rounded text-xs border transition-all',
                        active ? cls : 'border-slate-700/30 text-slate-600')}>
                      {imp === 'high' ? 'H' : imp === 'medium' ? 'M' : 'L'}
                    </button>
                  )
                })}
              </div>

              {/* Currency filter */}
              {availCurrencies.length > 0 && (
                <div className="flex items-center gap-1">
                  {availCurrencies.map(cur => (
                    <button key={cur} onClick={() => setFilterCurrency(prev =>
                      prev.includes(cur) ? prev.filter(x => x !== cur) : [...prev, cur])}
                      className={clsx('px-1.5 py-0.5 rounded text-xs border transition-all',
                        filterCurrency.includes(cur)
                          ? 'border-sky-700/60 text-sky-400 bg-sky-900/20'
                          : 'border-slate-700/30 text-slate-600')}>
                      {cur}
                    </button>
                  ))}
                </div>
              )}

              {/* Surprise toggle */}
              <button onClick={() => setFilterSurprise(v => !v)}
                className={clsx('px-1.5 py-0.5 rounded text-xs border transition-all',
                  filterSurprise ? 'border-yellow-700/60 text-yellow-400 bg-yellow-900/20' : 'border-slate-700/30 text-slate-600')}>
                ★ surprise
              </button>

              {/* Min pips */}
              <div className="flex items-center gap-1 ml-auto">
                <span className="text-xs text-slate-500">|pip|≥</span>
                <input type="number" min={0} step={5} value={filterMinPips}
                  onChange={e => setFilterMinPips(Math.max(0, parseInt(e.target.value) || 0))}
                  className="w-12 bg-dark-700 border border-slate-700/40 rounded px-1.5 py-0.5 text-xs text-white focus:outline-none text-center"/>
              </div>
            </div>
          )}

          {/* Event list */}
          {virtuals.length === 0 && !calMsg ? (
            <div className="text-xs text-slate-500 text-center py-3">
              Importe le calendrier ou ajoute des events manuels pour simuler leur impact.
            </div>
          ) : (
            <div className="space-y-0.5 max-h-72 overflow-y-auto pr-0.5">
              {virtuals.map((ve, idx) => {
                const isActive = activeVirtuals.some(a => a.id === ve.id)
                const impDot = ve.impact === 'high' ? 'bg-red-500' : ve.impact === 'medium' ? 'bg-amber-500' : 'bg-slate-500'
                const dispDate = ve.date.slice(5)  // MM-DD
                return (
                  <div key={ve.id}
                    className={clsx('flex items-center gap-1.5 rounded px-2 py-1 text-xs transition-all',
                      isActive ? 'bg-dark-800/60' : 'bg-dark-800/20 opacity-40')}>
                    {/* Disable toggle */}
                    <button
                      onClick={() => setVirtuals(v => v.map((x, i) => i === idx ? {...x, disabled: !x.disabled} : x))}
                      className={clsx('flex-shrink-0 transition-colors',
                        ve.disabled ? 'text-slate-600 hover:text-slate-400' : 'text-violet-400 hover:text-violet-300')}
                      title={ve.disabled ? 'Réactiver' : 'Désactiver'}>
                      {ve.disabled ? <EyeOff className="w-3 h-3"/> : <Eye className="w-3 h-3"/>}
                    </button>
                    {/* Impact dot */}
                    <span className={clsx('w-1.5 h-1.5 rounded-full flex-shrink-0', impDot)}/>
                    {/* Currency */}
                    {ve.currency && (
                      <span className="text-slate-500 font-mono w-7 flex-shrink-0">{ve.currency}</span>
                    )}
                    {/* Date */}
                    <span className="text-slate-500 font-mono w-10 flex-shrink-0">{dispDate}</span>
                    {/* Node routing badge */}
                    {ve.routed_node && (
                      <span className={clsx('flex-shrink-0 px-1 rounded border font-mono',
                        ve.routed_node === 'direct'
                          ? 'text-slate-500 border-slate-700/40 text-[9px]'
                          : 'text-violet-400 border-violet-700/40 bg-violet-900/20 text-[9px]'
                      )} title={`Route → nœud : ${ve.routed_node}`}>
                        {ve.routed_node === 'direct' ? '⇢' : ve.routed_node.slice(0, 10)}
                      </span>
                    )}
                    {/* Event name */}
                    <span className="flex-1 text-slate-300 truncate" title={ve.event_name || ve.label}>
                      {(ve.event_name || ve.label).replace(/^\[.*?\]\s*/, '')}
                    </span>
                    {/* Surprise star */}
                    {ve.has_surprise && <span className="text-yellow-400 flex-shrink-0" title="Surprise">★</span>}
                    {/* Actual vs forecast */}
                    {ve.actual_value && (
                      <span className="text-slate-500 hidden sm:inline flex-shrink-0 text-xs">
                        {ve.actual_value}{ve.forecast_value ? `/${ve.forecast_value}` : ''}
                      </span>
                    )}
                    {/* Pips input */}
                    <input type="number" step="any" value={ve.pips}
                      onChange={e => setVirtuals(v => v.map((x, i) => i === idx ? {...x, pips: parseFloat(e.target.value) || 0} : x))}
                      className={clsx(
                        'w-16 bg-dark-700 border rounded px-1.5 py-0.5 text-xs text-right focus:outline-none flex-shrink-0',
                        ve.pips > 0 ? 'text-emerald-400 border-emerald-700/40' :
                        ve.pips < 0 ? 'text-red-400 border-red-700/40' :
                        'text-slate-400 border-slate-700/40'
                      )}/>
                    <span className="text-slate-600 flex-shrink-0">p</span>
                    {/* Delete */}
                    <button onClick={() => setVirtuals(v => v.filter((_, i) => i !== idx))}
                      className="text-slate-600 hover:text-red-400 transition-colors flex-shrink-0 ml-0.5">
                      <X className="w-3 h-3"/>
                    </button>
                  </div>
                )
              })}
            </div>
          )}

          {/* Actions */}
          {virtuals.length > 0 && (
            <div className="flex gap-2 justify-end pt-1">
              <button onClick={() => { setVirtuals([]); setWhatifData(null) }}
                className="text-xs text-slate-500 hover:text-slate-300 transition-colors">
                Effacer tout
              </button>
              <button onClick={runWhatIf} disabled={whatifLoading || activeVirtuals.length === 0}
                className="flex items-center gap-1.5 px-3 py-1.5 bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-white text-xs rounded font-medium transition-colors">
                <Zap className="w-3 h-3"/>
                {whatifLoading ? 'Simulation…' : `Simuler (${activeVirtuals.length})`}
              </button>
            </div>
          )}
        </div>
      )}
    </div>
  )
}

// ── Gauge Sync Panel ──────────────────────────────────────────────────────────

interface GaugeSuggestion {
  node_id: string
  label: string
  value: number
  unit: string
  source: string
  confidence: 'HIGH' | 'MEDIUM' | 'LOW'
  note: string
}

interface GaugeSuggestionsResponse {
  instrument: string
  gauge_date: string
  n_suggestions: number
  suggestions: GaugeSuggestion[]
  gauges_available: string[]
}

const CONF_META: Record<string, { color: string; label: string }> = {
  HIGH:   { color: 'text-emerald-400 bg-emerald-900/20 border-emerald-700/40', label: 'Direct'  },
  MEDIUM: { color: 'text-amber-400 bg-amber-900/20 border-amber-700/40',       label: 'Dérivé'  },
  LOW:    { color: 'text-slate-400 bg-slate-800/40 border-slate-700/30',        label: 'Proxy'   },
}

function SyncPanel({ instrument, onClose, onApplied }: {
  instrument: string; onClose: () => void; onApplied: () => void
}) {
  const [data, setData]         = useState<GaugeSuggestionsResponse | null>(null)
  const [loading, setLoading]   = useState(true)
  const [selected, setSelected] = useState<Set<string>>(new Set())
  const [applying, setApplying] = useState(false)
  const [atDate, setAtDate]     = useState<string>('')

  function fetchSuggestions(date?: string) {
    setLoading(true)
    const qs = date ? `?at_date=${date}` : ''
    api.get<GaugeSuggestionsResponse>(`/instrument-models/${instrument}/gauge-suggestions${qs}`)
      .then(r => {
        setData(r.data)
        const highs = new Set(r.data.suggestions.filter(s => s.confidence === 'HIGH').map(s => s.node_id))
        setSelected(highs)
      })
      .catch(() => setData(null))
      .finally(() => setLoading(false))
  }

  useEffect(() => { fetchSuggestions() }, [instrument])

  function toggleAll(conf: string) {
    if (!data) return
    const ids = data.suggestions.filter(s => s.confidence === conf).map(s => s.node_id)
    const allSelected = ids.every(id => selected.has(id))
    setSelected(prev => {
      const n = new Set(prev)
      ids.forEach(id => allSelected ? n.delete(id) : n.add(id))
      return n
    })
  }

  async function apply() {
    if (!data) return
    setApplying(true)
    try {
      const overrides = data.suggestions
        .filter(s => selected.has(s.node_id))
        .map(s => ({ node_id: s.node_id, value: s.value, note: `[Auto${atDate ? ' @'+atDate : ''}] ${s.note}` }))
      await api.post(`/instrument-models/${instrument}/apply-suggestions`, { overrides })
      onApplied()
      onClose()
    } finally { setApplying(false) }
  }

  const selectedCount = selected.size

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
      onClick={onClose}>
      <div className="bg-dark-800 border border-slate-700/60 rounded-xl shadow-2xl w-full max-w-2xl mx-4 max-h-[85vh] flex flex-col"
        onClick={e => e.stopPropagation()}>

        {/* Header */}
        <div className="flex items-center justify-between p-4 border-b border-slate-700/40">
          <div className="flex-1">
            <div className="text-white font-semibold">Sync Marché — {instrument}</div>
            {data && (
              <div className="text-xs text-slate-500 mt-0.5">
                Snapshot: {data.gauge_date}
                {(data as any).requested_date && (data as any).requested_date !== data.gauge_date && (
                  <span className="text-amber-500/70 ml-1">(demandé: {(data as any).requested_date})</span>
                )}
                · {data.gauges_available.length} indicateurs
              </div>
            )}
            {/* Date picker to initialize at a historical date */}
            <div className="flex items-center gap-2 mt-2">
              <label className="text-xs text-slate-500">Date d'init :</label>
              <input
                type="date"
                value={atDate}
                onChange={e => setAtDate(e.target.value)}
                className="bg-dark-700 border border-slate-700/40 rounded px-2 py-0.5 text-xs text-white focus:outline-none focus:border-blue-500/60"
              />
              <button
                onClick={() => fetchSuggestions(atDate || undefined)}
                disabled={loading}
                className="px-2 py-0.5 bg-blue-600/20 border border-blue-700/40 rounded text-xs text-blue-400 hover:bg-blue-600/30 transition-colors disabled:opacity-40">
                {loading ? '…' : 'Charger'}
              </button>
              {atDate && (
                <button onClick={() => { setAtDate(''); fetchSuggestions() }}
                  className="text-xs text-slate-600 hover:text-slate-400">✕ Réinitialiser</button>
              )}
            </div>
          </div>
          <button onClick={onClose} className="text-slate-500 hover:text-white ml-4">
            <X className="w-4 h-4"/>
          </button>
        </div>

        {/* Content */}
        <div className="flex-1 overflow-y-auto p-4">
          {loading && (
            <div className="text-center py-8 text-slate-500 text-sm">Chargement des suggestions…</div>
          )}
          {!loading && !data && (
            <div className="text-center py-8 text-red-400 text-sm">
              Aucun snapshot de gauges disponible. Actualiser les données marché d'abord.
            </div>
          )}
          {data && (
            <div className="space-y-3">
              {/* Confidence group filters */}
              <div className="flex gap-2 flex-wrap">
                {(['HIGH','MEDIUM','LOW'] as const).map(conf => {
                  const items = data.suggestions.filter(s => s.confidence === conf)
                  const meta = CONF_META[conf]
                  return items.length > 0 ? (
                    <button key={conf} onClick={() => toggleAll(conf)}
                      className={clsx('flex items-center gap-1.5 px-2.5 py-1 rounded border text-xs font-medium transition-colors', meta.color)}>
                      <span>{items.every(s => selected.has(s.node_id)) ? '☑' : '☐'}</span>
                      {meta.label} ({items.length})
                    </button>
                  ) : null
                })}
                <span className="ml-auto text-xs text-slate-500 self-center">
                  {selectedCount} sélectionné{selectedCount > 1 ? 's' : ''}
                </span>
              </div>

              {/* Suggestion rows */}
              <div className="rounded-lg border border-slate-700/40 overflow-hidden">
                <table className="w-full text-xs">
                  <thead>
                    <tr className="bg-dark-700/60 border-b border-slate-700/40 text-slate-500">
                      <th className="px-3 py-2 w-8"/>
                      <th className="px-3 py-2 text-left">Variable</th>
                      <th className="px-3 py-2 text-right">Valeur</th>
                      <th className="px-3 py-2 text-center w-16">Conf.</th>
                      <th className="px-3 py-2 text-left text-slate-600">Détail</th>
                    </tr>
                  </thead>
                  <tbody>
                    {data.suggestions.map(s => {
                      const isSel = selected.has(s.node_id)
                      const meta  = CONF_META[s.confidence]
                      return (
                        <tr key={s.node_id}
                          onClick={() => setSelected(prev => { const n = new Set(prev); isSel ? n.delete(s.node_id) : n.add(s.node_id); return n })}
                          className={clsx('border-b border-slate-700/20 cursor-pointer transition-colors',
                            isSel ? 'bg-blue-900/10' : 'hover:bg-dark-700/20')}>
                          <td className="px-3 py-2 text-center">
                            <div className={clsx('w-3.5 h-3.5 rounded border mx-auto transition-colors',
                              isSel ? 'bg-blue-500 border-blue-400' : 'border-slate-600')}>
                              {isSel && <span className="text-white text-xs flex items-center justify-center h-full leading-none">✓</span>}
                            </div>
                          </td>
                          <td className="px-3 py-2 text-slate-300 font-medium">{s.label}</td>
                          <td className="px-3 py-2 text-right font-mono text-white">
                            {s.value > 0 ? '+' : ''}{s.value} <span className="text-slate-500">{s.unit}</span>
                          </td>
                          <td className="px-3 py-2 text-center">
                            <span className={clsx('px-1.5 py-0.5 rounded border text-xs', meta.color)}>
                              {meta.label}
                            </span>
                          </td>
                          <td className="px-3 py-2 text-slate-500 truncate max-w-[200px]" title={s.note}>
                            {s.note}
                          </td>
                        </tr>
                      )
                    })}
                  </tbody>
                </table>
              </div>

              {/* Warning LOW confidence */}
              {data.suggestions.some(s => s.confidence === 'LOW' && selected.has(s.node_id)) && (
                <div className="flex items-start gap-2 px-3 py-2 rounded bg-amber-900/20 border border-amber-700/30 text-xs text-amber-400">
                  <span className="shrink-0">⚠</span>
                  Certaines valeurs sélectionnées sont des proxies (Conf. Proxy) — revérifier avec les données réelles.
                </div>
              )}
            </div>
          )}
        </div>

        {/* Footer */}
        <div className="flex gap-2 p-4 border-t border-slate-700/40">
          <button onClick={apply} disabled={applying || selectedCount === 0 || !data}
            className="flex-1 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white text-sm rounded-lg px-4 py-2 font-medium transition-colors">
            {applying ? 'Import…' : `Importer ${selectedCount} valeur${selectedCount > 1 ? 's' : ''}`}
          </button>
          <button onClick={onClose} className="bg-dark-700 hover:bg-dark-600 text-slate-400 text-sm rounded-lg px-4 py-2 transition-colors">
            Annuler
          </button>
        </div>
      </div>
    </div>
  )
}

// ── Macro Sync Panel (FF Calendar → node overrides) ───────────────────────────

interface MacroSyncItem {
  node_id: string; node_label: string; macro_key: string; unit: string
  coefficient_to_pips: number
  actual_value: number | null;  actual_date: string | null
  forecast_value: number | null; forecast_date: string | null
  next_event_name: string | null; days_until_forecast: number | null
  current_override: number | null
}

function MacroSyncPanel({ instrument, onClose, onApplied }: {
  instrument: string; onClose: () => void; onApplied: () => void
}) {
  const [items, setItems]       = useState<MacroSyncItem[]>([])
  const [loading, setLoading]   = useState(true)
  // per-node choice: 'actual' | 'forecast' | null (skip)
  const [choices, setChoices]   = useState<Record<string, 'actual' | 'forecast' | null>>({})
  const [applying, setApplying] = useState(false)

  useEffect(() => {
    setLoading(true)
    api.get(`/instrument-models/${instrument}/macro-sync`)
      .then(r => {
        const its: MacroSyncItem[] = r.data.items
        setItems(its)
        // Default: select 'actual' when available
        const init: Record<string, 'actual' | 'forecast' | null> = {}
        for (const it of its) {
          init[it.node_id] = it.actual_value != null ? 'actual' : it.forecast_value != null ? 'forecast' : null
        }
        setChoices(init)
      })
      .catch(() => setItems([]))
      .finally(() => setLoading(false))
  }, [instrument])

  async function apply() {
    setApplying(true)
    try {
      const payload = items.flatMap(it => {
        const choice = choices[it.node_id]
        if (!choice) return []
        const value = choice === 'actual' ? it.actual_value : it.forecast_value
        if (value == null) return []
        const source = choice === 'actual'
          ? `Actual ${it.actual_date ?? ''}`
          : `Forecast ${it.forecast_date ?? ''} — ${it.next_event_name ?? ''}`
        return [{ node_id: it.node_id, value, note: `[FF Calendar] ${source}` }]
      })
      await api.post(`/instrument-models/${instrument}/macro-sync/apply`, { items: payload })
      onApplied()
      onClose()
    } finally { setApplying(false) }
  }

  const applyCount = Object.values(choices).filter(v => v != null).length

  function setAllActual() {
    const n = { ...choices }
    for (const it of items) {
      if (it.actual_value != null) n[it.node_id] = 'actual'
    }
    setChoices(n)
  }
  function setAllForecast() {
    const n = { ...choices }
    for (const it of items) {
      if (it.forecast_value != null) n[it.node_id] = 'forecast'
    }
    setChoices(n)
  }

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
      onClick={onClose}>
      <div className="bg-dark-800 border border-slate-700/60 rounded-xl shadow-2xl w-full max-w-3xl mx-4 max-h-[85vh] flex flex-col"
        onClick={e => e.stopPropagation()}>

        {/* Header */}
        <div className="flex items-center justify-between p-4 border-b border-slate-700/40">
          <div>
            <div className="text-white font-semibold">Sync Marché — {instrument} (FF Calendar)</div>
            <div className="text-xs text-slate-500 mt-0.5">
              Synchronise les nœuds macro avec les dernières valeurs publiées + forecasts prochains événements.
            </div>
          </div>
          <button onClick={onClose} className="text-slate-500 hover:text-white">
            <X className="w-4 h-4"/>
          </button>
        </div>

        {/* Content */}
        <div className="flex-1 overflow-y-auto p-4">
          {loading && (
            <div className="text-center py-10 text-slate-500 text-sm">Lecture FF Calendar…</div>
          )}
          {!loading && items.length === 0 && (
            <div className="text-center py-10 text-slate-500 text-sm">
              Aucun nœud macro-lié trouvé, ou aucune donnée FF Calendar disponible.
            </div>
          )}
          {!loading && items.length > 0 && (
            <div className="space-y-3">
              {/* Quick actions */}
              <div className="flex gap-2 flex-wrap items-center">
                <span className="text-xs text-slate-500">Sélectionner tout :</span>
                <button onClick={setAllActual}
                  className="px-2.5 py-1 rounded border border-violet-700/40 bg-violet-900/20 text-violet-300 text-xs hover:bg-violet-800/30 transition-colors">
                  Valeurs actuelles
                </button>
                <button onClick={setAllForecast}
                  className="px-2.5 py-1 rounded border border-amber-700/40 bg-amber-900/20 text-amber-300 text-xs hover:bg-amber-800/30 transition-colors">
                  Forecasts
                </button>
                <button onClick={() => setChoices(Object.fromEntries(items.map(i => [i.node_id, null])))}
                  className="px-2.5 py-1 rounded border border-slate-700/30 text-slate-500 text-xs hover:text-slate-300 transition-colors">
                  Désélectionner
                </button>
                <span className="ml-auto text-xs text-slate-500">{applyCount} sélectionné{applyCount > 1 ? 's' : ''}</span>
              </div>

              {/* Node rows */}
              <div className="rounded-lg border border-slate-700/40 overflow-hidden">
                <table className="w-full text-xs">
                  <thead>
                    <tr className="bg-dark-700/60 border-b border-slate-700/40 text-slate-500">
                      <th className="px-3 py-2 text-left">Variable</th>
                      <th className="px-3 py-2 text-center w-28">
                        <span className="text-violet-400">Actuel</span>
                      </th>
                      <th className="px-3 py-2 text-center w-40">
                        <span className="text-amber-400">Prochain forecast</span>
                      </th>
                      <th className="px-3 py-2 text-center w-28">Override actuel</th>
                    </tr>
                  </thead>
                  <tbody>
                    {items.map(it => {
                      const ch = choices[it.node_id]
                      return (
                        <tr key={it.node_id}
                          className="border-b border-slate-700/20 hover:bg-dark-700/30 transition-colors">
                          <td className="px-3 py-2.5">
                            <div className="font-medium text-white">{it.node_label}</div>
                            <div className="text-slate-600">{it.macro_key}</div>
                          </td>

                          {/* Actual value cell */}
                          <td className="px-2 py-2.5 text-center">
                            {it.actual_value != null ? (
                              <button
                                onClick={() => setChoices(p => ({ ...p, [it.node_id]: ch === 'actual' ? null : 'actual' }))}
                                className={clsx(
                                  'px-2 py-1.5 rounded border text-xs font-mono w-full transition-colors',
                                  ch === 'actual'
                                    ? 'border-violet-500 bg-violet-900/40 text-violet-200 font-bold'
                                    : 'border-slate-700/40 text-slate-300 hover:border-violet-700/50',
                                )}>
                                {it.actual_value.toFixed(2)} {it.unit}
                                <div className="text-slate-600 font-normal text-xs">{it.actual_date}</div>
                              </button>
                            ) : (
                              <span className="text-slate-700">—</span>
                            )}
                          </td>

                          {/* Forecast cell */}
                          <td className="px-2 py-2.5 text-center">
                            {it.forecast_value != null ? (
                              <button
                                onClick={() => setChoices(p => ({ ...p, [it.node_id]: ch === 'forecast' ? null : 'forecast' }))}
                                className={clsx(
                                  'px-2 py-1.5 rounded border text-xs font-mono w-full transition-colors',
                                  ch === 'forecast'
                                    ? 'border-amber-500 bg-amber-900/30 text-amber-200 font-bold'
                                    : 'border-slate-700/40 text-slate-300 hover:border-amber-700/50',
                                )}>
                                {it.forecast_value.toFixed(2)} {it.unit}
                                <div className="text-slate-600 font-normal text-xs">
                                  {it.forecast_date} ({it.days_until_forecast}j)
                                </div>
                              </button>
                            ) : (
                              <span className="text-slate-700">—</span>
                            )}
                          </td>

                          {/* Current override */}
                          <td className="px-3 py-2.5 text-center">
                            {it.current_override != null ? (
                              <span className="font-mono text-violet-400">
                                {it.current_override.toFixed(2)} {it.unit}
                              </span>
                            ) : (
                              <span className="text-slate-700">—</span>
                            )}
                          </td>
                        </tr>
                      )
                    })}
                  </tbody>
                </table>
              </div>

              <div className="text-xs text-slate-600">
                <span className="text-violet-400">Actuel</span> = dernière valeur publiée dans FF Calendar.&nbsp;
                <span className="text-amber-400">Forecast</span> = estimation du prochain événement.
                Cliquer sur une cellule pour la sélectionner / désélectionner.
              </div>
            </div>
          )}
        </div>

        {/* Footer */}
        <div className="flex gap-2 p-4 border-t border-slate-700/40">
          <button onClick={apply} disabled={applying || applyCount === 0}
            className="flex-1 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white text-sm rounded-lg px-4 py-2 font-medium transition-colors">
            {applying ? 'Import…' : `Appliquer ${applyCount} valeur${applyCount > 1 ? 's' : ''}`}
          </button>
          <button onClick={onClose} className="bg-dark-700 hover:bg-dark-600 text-slate-400 text-sm rounded-lg px-4 py-2 transition-colors">
            Annuler
          </button>
        </div>
      </div>
    </div>
  )
}

// ── Calibration View ──────────────────────────────────────────────────────────

const DECAY_OPTIONS = ['exp', 'linear', 'step'] as const

function CalibrationView({ instrument, eventDetails }: {
  instrument: string
  eventDetails: Record<string, EventDetail[]>
}) {
  const [localCalib, setLocalCalib] = useState<Record<number, EventDetail['calibration']>>({})
  const [saving, setSaving]   = useState<Record<number, boolean>>({})
  const [saved, setSaved]     = useState<Record<number, boolean>>({})

  const allEvents = Object.values(eventDetails).flat()
    .sort((a, b) => b.lifecycle_factor - a.lifecycle_factor)

  if (allEvents.length === 0) {
    return (
      <div className="text-center py-12 text-slate-500 text-sm">
        Aucun event actif actuellement — les paramètres d'arêtes s'affichent ici quand des events sont en cours d'absorption.
      </div>
    )
  }

  function getCalib(ev: EventDetail) {
    return localCalib[ev.analysis_id] ?? ev.calibration
  }

  function setField(id: number, field: keyof EventDetail['calibration'], val: string | number) {
    setLocalCalib(prev => ({
      ...prev,
      [id]: { ...(prev[id] ?? allEvents.find(e => e.analysis_id === id)!.calibration), [field]: val },
    }))
    setSaved(prev => ({ ...prev, [id]: false }))
  }

  async function saveCalib(ev: EventDetail) {
    const calib = getCalib(ev)
    setSaving(prev => ({ ...prev, [ev.analysis_id]: true }))
    try {
      await api.put(`/instrument-models/${instrument}/events/${ev.analysis_id}/calibration`, calib)
      setSaved(prev => ({ ...prev, [ev.analysis_id]: true }))
    } finally {
      setSaving(prev => ({ ...prev, [ev.analysis_id]: false }))
    }
  }

  async function resetCalib(ev: EventDetail) {
    await api.delete(`/instrument-models/${instrument}/events/${ev.analysis_id}/calibration`)
    setLocalCalib(prev => { const n = {...prev}; delete n[ev.analysis_id]; return n })
    setSaved(prev => ({ ...prev, [ev.analysis_id]: false }))
  }

  return (
    <div className="space-y-2">
      <div className="text-xs text-slate-500 mb-3">
        Paramètres de decay (arêtes) des events actifs — modifiez le lifecycle par event indépendamment du template.
      </div>
      {allEvents.map(ev => {
        const cal = getCalib(ev)
        const lf  = ev.lifecycle_factor
        const isSaving = saving[ev.analysis_id]
        const isDirty  = !!localCalib[ev.analysis_id]
        const wasSaved = saved[ev.analysis_id]
        return (
          <div key={ev.analysis_id}
            className="rounded-lg border border-slate-700/30 bg-dark-800/40 p-3 space-y-2">
            {/* Event header */}
            <div className="flex items-center justify-between gap-3 flex-wrap">
              <div className="flex items-center gap-2 min-w-0">
                <span className="text-xs text-slate-500 shrink-0">{ev.start_date}</span>
                <span className="text-xs font-medium text-slate-300 truncate">{ev.title || ev.analysis_id}</span>
                <span className="text-xs text-slate-600 bg-slate-800 rounded px-1.5 py-0.5 shrink-0">{ev.days_since}j</span>
              </div>
              <div className="flex items-center gap-3 shrink-0">
                <span className={clsx('text-xs font-bold font-mono', pipColor(ev.pip_prediction))}>
                  {fmt(ev.pip_prediction)}p
                </span>
                <span className={clsx('text-xs font-mono', pipColor(ev.remaining_pips))}>
                  →{fmt(ev.remaining_pips)}p
                </span>
                {/* Lifecycle bar */}
                <div className="flex items-center gap-1">
                  <div className="w-20 h-1.5 rounded-full bg-slate-700/60 overflow-hidden">
                    <div className={clsx('h-full rounded-full', lf > 0.5 ? 'bg-emerald-500' : lf > 0.2 ? 'bg-amber-500' : 'bg-red-500')}
                      style={{width:`${Math.round(lf*100)}%`}}/>
                  </div>
                  <span className="text-xs text-slate-500 font-mono">{Math.round(lf*100)}%</span>
                </div>
              </div>
            </div>

            {/* Calibration inputs */}
            <div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
              <div>
                <label className="text-xs text-slate-500 block mb-0.5">Absorption (j)</label>
                <input type="number" min={1} max={365} value={cal.absorption_days}
                  onChange={e => setField(ev.analysis_id, 'absorption_days', parseInt(e.target.value)||1)}
                  className="w-full bg-dark-700 border border-slate-700/40 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-blue-500/60"/>
              </div>
              <div>
                <label className="text-xs text-slate-500 block mb-0.5">Rise (j)</label>
                <input type="number" min={0} max={30} value={cal.rise_days}
                  onChange={e => setField(ev.analysis_id, 'rise_days', parseInt(e.target.value)||0)}
                  className="w-full bg-dark-700 border border-slate-700/40 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-blue-500/60"/>
              </div>
              <div>
                <label className="text-xs text-slate-500 block mb-0.5">Plateau (j)</label>
                <input type="number" min={0} max={30} value={cal.plateau_days}
                  onChange={e => setField(ev.analysis_id, 'plateau_days', parseInt(e.target.value)||0)}
                  className="w-full bg-dark-700 border border-slate-700/40 rounded px-2 py-1 text-xs text-white focus:outline-none focus:border-blue-500/60"/>
              </div>
              <div>
                <label className="text-xs text-slate-500 block mb-0.5">Decay type</label>
                <select value={cal.decay_type}
                  onChange={e => setField(ev.analysis_id, 'decay_type', e.target.value)}
                  className="w-full bg-dark-700 border border-slate-700/40 rounded px-2 py-1 text-xs text-white focus:outline-none">
                  {DECAY_OPTIONS.map(d => <option key={d} value={d}>{d}</option>)}
                </select>
              </div>
            </div>

            {/* Actions */}
            <div className="flex items-center gap-2 justify-end">
              {isDirty && !wasSaved && (
                <button onClick={() => resetCalib(ev)}
                  className="text-xs text-slate-600 hover:text-slate-400 transition-colors">
                  ↩ Réinitialiser template
                </button>
              )}
              <button onClick={() => saveCalib(ev)} disabled={isSaving}
                className={clsx('flex items-center gap-1 px-3 py-1 rounded text-xs font-medium transition-colors',
                  wasSaved ? 'bg-emerald-700/40 text-emerald-400 border border-emerald-700/40'
                  : isDirty ? 'bg-blue-600 hover:bg-blue-500 text-white'
                  : 'bg-dark-700 text-slate-500 border border-slate-700/30')}>
                {isSaving ? 'Sauvegarde…' : wasSaved ? '✓ Sauvegardé' : 'Sauvegarder'}
              </button>
            </div>
          </div>
        )
      })}
    </div>
  )
}

// ── MacroConfigView ────────────────────────────────────────────────────────────

const HORIZON_META = {
  ct: { label: 'CT',  color: 'text-sky-400',    bg: 'bg-sky-900/30 border-sky-700/40',     desc: 'Court terme (< 3 mois)' },
  mt: { label: 'MT',  color: 'text-amber-400',  bg: 'bg-amber-900/30 border-amber-700/40', desc: 'Moyen terme (3-12 mois)' },
  lt: { label: 'LT',  color: 'text-violet-400', bg: 'bg-violet-900/30 border-violet-700/40',desc: 'Long terme (> 12 mois)' },
}
const TRAJ_OPTIONS = [
  { value: 'step',   label: 'Choc immédiat (step)' },
  { value: 'linear', label: 'Dérive linéaire'       },
  { value: 'exp',    label: 'Convergence exp.'      },
]

interface AddScenarioForm {
  label: string; horizon: 'ct'|'mt'|'lt'; target_date: string; target_value: string
  confidence: string; trajectory: string; absorption_days: string; notes: string
}
const EMPTY_FORM: AddScenarioForm = {
  label: '', horizon: 'mt', target_date: '', target_value: '', confidence: '0.7',
  trajectory: 'linear', absorption_days: '30', notes: '',
}

function NodeScenarioSection({
  instrument, node, guidance,
}: { instrument: string; node: ModelNode; guidance: MacroGuidanceItem | undefined }) {
  const [scenarios, setScenarios] = useState<NodeScenario[]>([])
  const [expanded, setExpanded]   = useState(false)
  const [addForm, setAddForm]     = useState<AddScenarioForm | null>(null)
  const [saving, setSaving]       = useState(false)

  useEffect(() => {
    if (!expanded) return
    api.get(`/instrument-models/${instrument}/scenarios`, { params: { node_id: node.id } })
      .then(r => setScenarios(r.data))
      .catch(() => {})
  }, [expanded, instrument, node.id])

  async function submitScenario() {
    if (!addForm) return
    setSaving(true)
    try {
      await api.post(`/instrument-models/${instrument}/scenarios`, {
        node_id:        node.id,
        label:          addForm.label || `${node.label} ${addForm.horizon.toUpperCase()}`,
        horizon:        addForm.horizon,
        target_date:    addForm.target_date,
        target_value:   parseFloat(addForm.target_value),
        confidence:     parseFloat(addForm.confidence),
        trajectory:     addForm.trajectory,
        absorption_days: parseInt(addForm.absorption_days, 10),
        notes:          addForm.notes,
      })
      const r = await api.get(`/instrument-models/${instrument}/scenarios`, { params: { node_id: node.id } })
      setScenarios(r.data)
      setAddForm(null)
    } catch {}
    setSaving(false)
  }

  async function deleteScenario(id: number) {
    await api.delete(`/instrument-models/${instrument}/scenarios/${id}`)
    setScenarios(prev => prev.filter(s => s.id !== id))
  }

  const hm  = HORIZON_META
  const mk  = node.macro_key
  const curV = guidance?.current_value
  const nxt  = guidance?.next_event

  return (
    <div className={clsx('rounded-lg border transition-colors',
      (mk || scenarios.length > 0)
        ? 'border-violet-700/30 bg-violet-900/10'
        : 'border-slate-700/20 bg-dark-800/30')}>

      {/* Row header */}
      <div className="flex items-center gap-3 px-3 py-2.5">
        <div className="flex-1 min-w-0">
          <div className="text-sm text-white font-medium truncate">{node.label}</div>
          <div className="flex items-center gap-2 mt-0.5">
            <span className="text-xs text-slate-600">{node.unit} · ×{node.coefficient_to_pips}</span>
            {mk && <span className="text-xs text-violet-400 font-mono">{mk}</span>}
          </div>
        </div>

        {/* Current value from ff_calendar */}
        {curV != null && (
          <div className="text-right shrink-0">
            <div className="text-sm font-bold font-mono text-white">{curV.toFixed(2)}<span className="text-xs text-slate-500 ml-1">{node.unit}</span></div>
            {nxt && (
              <div className="text-xs text-amber-400">
                → {nxt.forecast != null ? nxt.forecast.toFixed(2) : '?'} <span className="text-slate-600">dans {nxt.days_until}j</span>
              </div>
            )}
          </div>
        )}

        {/* Scenario count + expand */}
        <button onClick={() => setExpanded(e => !e)}
          className="flex items-center gap-1.5 shrink-0 text-xs text-slate-500 hover:text-white transition-colors">
          {scenarios.length > 0 && (
            <span className="px-1.5 py-0.5 rounded bg-violet-800/50 text-violet-300 font-mono">
              {scenarios.length}
            </span>
          )}
          {expanded ? <ChevronUp className="w-3.5 h-3.5"/> : <ChevronDown className="w-3.5 h-3.5"/>}
        </button>
      </div>

      {/* Expanded section */}
      {expanded && (
        <div className="border-t border-slate-700/30 px-3 pt-2 pb-3 space-y-2">

          {/* Existing scenarios */}
          {scenarios.map(s => {
            const hMeta = hm[s.horizon] ?? hm.mt
            return (
              <div key={s.id} className={clsx('flex items-center gap-2 px-2 py-1.5 rounded border text-xs', hMeta.bg)}>
                <span className={clsx('font-semibold w-6 shrink-0', hMeta.color)}>{hMeta.label}</span>
                <span className="text-white flex-1 truncate font-medium">{s.label}</span>
                <span className="text-slate-400 shrink-0">{s.target_date}</span>
                <span className="font-mono font-bold text-white shrink-0">{s.target_value.toFixed(2)}</span>
                <span className="text-slate-500 shrink-0">{Math.round(s.confidence*100)}%</span>
                <span className="text-slate-600 shrink-0">{s.trajectory}</span>
                <button onClick={() => deleteScenario(s.id)}
                  className="text-slate-600 hover:text-red-400 transition-colors shrink-0">
                  <Trash2 className="w-3 h-3"/>
                </button>
              </div>
            )
          })}

          {/* Add form */}
          {addForm ? (
            <div className="space-y-2 pt-1">
              <div className="grid grid-cols-3 gap-2">
                {(['ct','mt','lt'] as const).map(h => (
                  <button key={h} onClick={() => setAddForm(f => f && ({...f, horizon: h}))}
                    className={clsx('py-1 rounded border text-xs font-semibold transition-colors', hm[h].bg,
                      addForm.horizon === h ? hm[h].color : 'text-slate-500')}>
                    {hm[h].label} <span className="font-normal opacity-60">{h === 'ct' ? '< 3m' : h === 'mt' ? '3-12m' : '> 1an'}</span>
                  </button>
                ))}
              </div>
              <div className="grid grid-cols-2 gap-2">
                <input type="text" placeholder="Label (ex: Fed surprise cut)"
                  value={addForm.label}
                  onChange={e => setAddForm(f => f && ({...f, label: e.target.value}))}
                  className="col-span-2 text-xs bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-white placeholder-slate-600 focus:outline-none focus:border-violet-500/50"/>
                <div>
                  <div className="text-xs text-slate-600 mb-1">Date cible</div>
                  <input type="date"
                    value={addForm.target_date}
                    onChange={e => setAddForm(f => f && ({...f, target_date: e.target.value}))}
                    className="w-full text-xs bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-white focus:outline-none"/>
                </div>
                <div>
                  <div className="text-xs text-slate-600 mb-1">Valeur cible ({node.unit})</div>
                  <input type="number" step="any"
                    placeholder={curV != null ? curV.toFixed(2) : '0.00'}
                    value={addForm.target_value}
                    onChange={e => setAddForm(f => f && ({...f, target_value: e.target.value}))}
                    className="w-full text-xs bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-white focus:outline-none"/>
                </div>
                <div>
                  <div className="text-xs text-slate-600 mb-1">Confidence</div>
                  <input type="range" min="0" max="1" step="0.05"
                    value={addForm.confidence}
                    onChange={e => setAddForm(f => f && ({...f, confidence: e.target.value}))}
                    className="w-full accent-violet-500"/>
                  <div className="text-xs text-violet-400 text-center">{Math.round(parseFloat(addForm.confidence)*100)}%</div>
                </div>
                <div>
                  <div className="text-xs text-slate-600 mb-1">Trajectoire</div>
                  <select value={addForm.trajectory}
                    onChange={e => setAddForm(f => f && ({...f, trajectory: e.target.value}))}
                    className="w-full text-xs bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-white focus:outline-none">
                    {TRAJ_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
                  </select>
                </div>
              </div>
              <div className="flex gap-2 justify-end">
                <button onClick={() => setAddForm(null)}
                  className="text-xs text-slate-600 hover:text-slate-400 px-3 py-1.5 transition-colors">
                  Annuler
                </button>
                <button onClick={submitScenario} disabled={saving || !addForm.target_date || !addForm.target_value}
                  className="text-xs bg-violet-600 hover:bg-violet-500 disabled:opacity-40 text-white px-3 py-1.5 rounded transition-colors">
                  {saving ? 'Saving…' : '+ Ajouter'}
                </button>
              </div>
            </div>
          ) : (
            <button onClick={() => setAddForm({...EMPTY_FORM})}
              className="flex items-center gap-1 text-xs text-slate-600 hover:text-violet-400 transition-colors">
              <Plus className="w-3 h-3"/> Ajouter un scénario CT/MT/LT
            </button>
          )}
        </div>
      )}
    </div>
  )
}

function MacroConfigView({ instrument, nodes }: { instrument: string; nodes: ModelNode[] }) {
  const [guidance, setGuidance] = useState<MacroGuidanceItem[]>([])

  useEffect(() => {
    api.get(`/instrument-models/${instrument}/macro-guidance`)
      .then(r => setGuidance(r.data))
      .catch(() => {})
  }, [instrument])

  const manualNodes   = nodes.filter(n => n.node_type === 'input_manual')
  const linkedCount   = manualNodes.filter(n => n.macro_key).length
  const scenarioCount = guidance.length  // approximate

  return (
    <div className="space-y-4">
      <div className="flex items-start justify-between gap-4">
        <div>
          <div className="text-sm font-medium text-white">Paramètres macro & scénarios</div>
          <div className="text-xs text-slate-500 mt-0.5">
            Chaque nœud macro-lié reçoit ses valeurs depuis FF Calendar.
            Ajoutez des scénarios CT/MT/LT pour projeter la guidance fondamentale dans le temps.
          </div>
        </div>
        <span className="text-xs text-violet-400 font-mono shrink-0">
          {linkedCount}/{manualNodes.length} liés FF
        </span>
      </div>

      <div className="space-y-1.5">
        {manualNodes.map(node => (
          <NodeScenarioSection
            key={node.id}
            instrument={instrument}
            node={node}
            guidance={guidance.find(g => g.node_id === node.id)}
          />
        ))}
      </div>

      <div className="rounded-lg border border-slate-700/30 bg-dark-800/40 p-3 text-xs text-slate-500 space-y-1">
        <div className="text-white font-medium">Comment fonctionne la guidance ?</div>
        <div>• <span className="text-sky-400">CT</span> — choc/event imminent (ex: réunion Fed dans 3 sem → surprise cut à 3.75%)</div>
        <div>• <span className="text-amber-400">MT</span> — régime macro 3-12 mois (ex: regain inflation → stagnation à 3.80%)</div>
        <div>• <span className="text-violet-400">LT</span> — vision structurelle (ex: cycle de baisse → 2.00% fin 2027)</div>
        <div className="text-slate-600 pt-1">La Timeline interpolera chaque nœud vers sa cible avec la confidence pondérée. Relancez la simulation pour voir la courbe fondamentale évoluer.</div>
      </div>
    </div>
  )
}

// ── Main Page ──────────────────────────────────────────────────────────────────

type ViewMode = 'dag' | 'table' | 'timeline' | 'calibration' | 'params'

export default function InstrumentModels() {
  const [instrument, setInstrument] = useState('EURUSD')
  const [view, setView]             = useState<ViewMode>('dag')
  const [state, setState]           = useState<ModelState | null>(null)
  const [loading, setLoading]       = useState(false)
  const [editNode, setEditNode]     = useState<ModelNode | null>(null)
  const [refreshKey, setRefreshKey] = useState(0)
  const [showSync, setShowSync]     = useState(false)

  const load = useCallback(() => {
    setLoading(true)
    api.get<ModelState>(`/instrument-models/${instrument}`)
      .then(r => setState(r.data))
      .catch(() => setState(null))
      .finally(() => setLoading(false))
  }, [instrument])

  useEffect(() => { load() }, [load, refreshKey])

  function onSaved() { setRefreshKey(k => k + 1) }

  const layerSummary = useMemo(() => {
    if (!state) return []
    return state.nodes
      .filter(n => n.node_type === 'intermediate')
      .map(n => ({
        ...n,
        pct: state.net_pips !== 0
          ? Math.round((n.pip_contribution / Math.abs(state.net_pips)) * 100)
          : 0,
      }))
      .sort((a, b) => Math.abs(b.pip_contribution) - Math.abs(a.pip_contribution))
  }, [state])

  const VIEW_BTNS: { key: ViewMode; Icon: typeof Network; label: string }[] = [
    { key: 'dag',         Icon: Network,   label: 'DAG' },
    { key: 'table',       Icon: Table2,    label: 'Tableau' },
    { key: 'timeline',    Icon: LineChart, label: 'Timeline' },
    { key: 'calibration', Icon: Activity,  label: 'Calibration' },
    { key: 'params',      Icon: Settings2, label: 'Paramètres' },
  ]

  return (
    <div className="p-4 space-y-4">
      {/* Header */}
      <div className="flex items-center justify-between gap-4">
        <div>
          <h1 className="text-xl font-bold text-white">Modèles Instruments</h1>
          <p className="text-sm text-slate-500 mt-0.5">
            Graphes causaux exhaustifs — propagation DAG 3 couches par instrument
          </p>
        </div>
        <div className="flex gap-2">
          <button onClick={() => setShowSync(true)}
            className="flex items-center gap-2 px-3 py-2 bg-blue-600/20 border border-blue-700/40 rounded-lg text-blue-400 hover:bg-blue-600/30 text-sm transition-colors font-medium">
            <Activity className="w-4 h-4"/> Sync Marché
          </button>
          <button onClick={() => setRefreshKey(k => k+1)} disabled={loading}
            className="flex items-center gap-2 px-3 py-2 bg-dark-700 border border-slate-700/40 rounded-lg text-slate-400 hover:text-white text-sm transition-colors disabled:opacity-40">
            <RefreshCw className={clsx('w-4 h-4', loading && 'animate-spin')}/> Actualiser
          </button>
        </div>
      </div>

      {/* Instrument tabs */}
      <div className="flex gap-1.5 flex-wrap">
        {INSTRUMENTS.map(inst => (
          <button key={inst} onClick={() => setInstrument(inst)}
            className={clsx('px-3 py-1.5 rounded-lg text-sm font-medium transition-all border',
              instrument === inst
                ? 'bg-blue-600 border-blue-500 text-white shadow-lg shadow-blue-900/30'
                : 'bg-dark-700 border-slate-700/40 text-slate-400 hover:text-white hover:border-slate-600')}>
            {inst}
          </button>
        ))}
      </div>

      {/* Synthetic Price Banner */}
      {state && (
        <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4 space-y-3">
          {/* Row 1 — synthetic price + direction + regime */}
          <div className="flex items-start justify-between gap-4 flex-wrap">
            <div>
              <div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-1">
                {state.name} — Prix Synthétique
              </div>
              <div className="flex items-baseline gap-3">
                <span className={clsx('text-3xl font-bold font-mono tracking-tight',
                  state.direction === 'bullish' ? 'text-emerald-400'
                  : state.direction === 'bearish' ? 'text-red-400'
                  : 'text-slate-300')}>
                  {fmtPrice(state.synthetic_price, state.pip_to_price)}
                </span>
                <DirectionBadge direction={state.direction} pips={state.net_pips}/>
              </div>
              {/* Structural vs event breakdown */}
              <div className="flex gap-4 mt-2 text-xs">
                <div>
                  <span className="text-slate-500">Fondamental </span>
                  <span className="text-amber-400 font-mono font-semibold">
                    {fmtPrice(state.fundamental_level, state.pip_to_price)}
                  </span>
                  <span className="text-slate-600 ml-1">({fmt(state.structural_pips)}p)</span>
                </div>
                <div>
                  <span className="text-slate-500">Surprises </span>
                  <span className={clsx('font-mono font-semibold', pipColor(state.event_pips))}>
                    {fmt(state.event_pips)}p
                  </span>
                </div>
                <div>
                  <span className="text-slate-600">Intercept </span>
                  <span className="text-slate-500 font-mono">
                    {fmtPrice(state.price_intercept, state.pip_to_price)}
                  </span>
                </div>
              </div>
            </div>

            {/* Layer contributions */}
            {layerSummary.length > 0 && (
              <div className="flex gap-4 flex-wrap">
                {layerSummary.map(l => (
                  <div key={l.id} className="text-center min-w-[72px]">
                    <div className="text-xs text-slate-500 leading-tight mb-0.5">
                      {l.label.replace('▶ ', '').split(' ').slice(0, 2).join(' ')}
                    </div>
                    <div className={clsx('text-sm font-bold font-mono', pipColor(l.pip_contribution))}>{fmt(l.pip_contribution)}</div>
                    <div className="text-xs text-slate-600">{l.pct > 0 ? '+' : ''}{l.pct}%</div>
                  </div>
                ))}
              </div>
            )}

            <div className="flex flex-col gap-2 self-start">
              {state.regime && <RegimeBadge regime={state.regime}/>}
              <div className="text-xs text-slate-600 space-y-0.5">
                <div>{state.nodes.filter(n => n.source === 'events').length} events actifs</div>
                <div>{state.nodes.filter(n => n.source === 'manual').length} overrides manuels</div>
                <div className="text-slate-700">{state.at_date}</div>
              </div>
              {state.nodes.some(n => n.source === 'manual') && (
                <button
                  onClick={async () => {
                    if (!confirm('Effacer tous les overrides manuels ?')) return
                    await api.delete(`/instrument-models/${instrument}/overrides`)
                    setRefreshKey(k => k + 1)
                  }}
                  className="text-xs text-red-500/60 hover:text-red-400 transition-colors text-left">
                  ✕ Reset toutes valeurs
                </button>
              )}
            </div>
          </div>
        </div>
      )}

      {/* View selector */}
      {state && (
        <div className="flex items-center gap-1.5 border-b border-slate-700/30 pb-3">
          {VIEW_BTNS.map(({ key, Icon, label }) => (
            <button key={key} onClick={() => setView(key)}
              className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors',
                view === key
                  ? 'bg-blue-600/20 text-blue-400 border border-blue-700/40'
                  : 'text-slate-500 hover:text-white border border-transparent')}>
              <Icon className="w-4 h-4"/> {label}
            </button>
          ))}
          <span className="ml-auto text-xs text-slate-600">
            {state.nodes.filter(n=>n.node_type==='input_event').length} EV
            · {state.nodes.filter(n=>n.node_type==='input_manual').length} M
            · {state.nodes.filter(n=>n.node_type==='intermediate').length} INT
            · {state.nodes.length} nœuds total
          </span>
        </div>
      )}

      {/* Main content */}
      {state && (
        <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
          {view === 'dag'         && <DagView nodes={state.nodes} instrument={instrument} onEdit={setEditNode} eventDetails={state.event_details || {}} colLabels={state.col_labels}/>}
          {view === 'table'       && <TableView nodes={state.nodes} onEdit={setEditNode}/>}
          {view === 'timeline'    && <TimelineView instrument={instrument}/>}
          {view === 'calibration' && <CalibrationView instrument={instrument} eventDetails={state.event_details || {}}/>}
          {view === 'params'      && <MacroConfigView instrument={instrument} nodes={state.nodes}/>}
        </div>
      )}

      {/* Quick stats */}
      {state && view !== 'timeline' && (
        <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
          {[
            { label: 'Prix synthétique',  value: fmtPrice(state.synthetic_price, state.pip_to_price), sub: state.direction, color: state.direction === 'bullish' ? 'text-emerald-400' : state.direction === 'bearish' ? 'text-red-400' : 'text-white' },
            { label: 'Niveau fondamental',value: fmtPrice(state.fundamental_level, state.pip_to_price), sub: `struct: ${fmt(state.structural_pips)}p`, color: 'text-amber-400' },
            { label: 'Surprises events',  value: `${fmt(state.event_pips)} pips`, sub: `sur ${state.nodes.filter(n=>n.source==='events').length} events actifs`, color: pipColor(state.event_pips) },
            { label: 'Variables manuelles',value: `${state.nodes.filter(n=>n.source==='manual').length} actives`, sub: `sur ${state.nodes.filter(n=>n.node_type==='input_manual').length} disponibles`, color: 'text-violet-400' },
          ].map(card => (
            <div key={card.label} className="rounded-lg border border-slate-700/30 bg-dark-800/50 p-3">
              <div className="text-xs text-slate-500 mb-1">{card.label}</div>
              <div className={clsx('text-lg font-bold font-mono', card.color)}>{card.value}</div>
              <div className="text-xs text-slate-600">{card.sub}</div>
            </div>
          ))}
        </div>
      )}

      {!state && !loading && (
        <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-8 text-center text-slate-500">
          Modèle introuvable pour {instrument}. Vérifier que le backend est démarré.
        </div>
      )}

      {loading && !state && (
        <div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-8 text-center text-slate-500">
          Chargement du modèle…
        </div>
      )}

      {editNode && (
        <NodeEditModal
          node={editNode} instrument={instrument}
          onClose={() => setEditNode(null)} onSaved={onSaved}
        />
      )}

      {showSync && (
        state?.nodes.some(n => n.macro_key)
          ? <MacroSyncPanel
              instrument={instrument}
              onClose={() => setShowSync(false)}
              onApplied={() => { setShowSync(false); setRefreshKey(k => k + 1) }}
            />
          : <SyncPanel
              instrument={instrument}
              onClose={() => setShowSync(false)}
              onApplied={() => { setShowSync(false); setRefreshKey(k => k + 1) }}
            />
      )}
    </div>
  )
}
