import { useState, useEffect, useRef, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'
import {
  FlaskConical, Library, Zap, BarChart3, RefreshCw, Edit3,
  Sliders, Brain, Search, CheckCircle, XCircle, Minus, AlertCircle,
  Plus, Trash2, Save, Copy, Info, Wand2, ExternalLink, Filter, X,
} from 'lucide-react'
import clsx from 'clsx'

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

export interface CausalNode {
  id: string; label: string
  type: 'macro_event' | 'observable' | 'latent' | 'market_asset' | 'input' | 'intermediate' | 'output'
  x: number; y: number
  formula?: string; unit?: string; instrument?: string; description?: string
}
export interface CausalEdge {
  from: string; to: string
  style: 'solid' | 'dashed'
  type?: string
  strength?: 1 | 2 | 3
  sign?: 'positive' | 'negative' | 'neutral'
  label?: string
  lag_min?: number        // délai avant onset (minutes) — mode intraday 5m
  lag_days?: number       // délai avant onset (jours) — mode journalier
  diffusion_min?: number  // durée jusqu'à absorption complète (minutes)
  decay_days?: number | null // demi-vie (jours), null = shift permanent
}
interface Coefficient { value: number; calibrated: number | null; description: string }
interface InputMapping {
  source: 'user_input' | 'market_watchlist' | 'economic_events' | 'ff_calendar' | 'surprise' | 'actual_value' | 'impact_score_scaled' | 'impact_score_pct'
  key?: string
  field?: string
  unit?: string
  range?: number[]
  normalize?: boolean
}
interface DataSources {
  prices:       { key: string; label: string }[]
  macro_series: { key: string; label: string }[]
  ff_events:    { key: string; label: string }[]
}
export interface GraphJson {
  nodes: CausalNode[]; edges: CausalEdge[]
  coefficients: Record<string, Coefficient>
  instruments: string[]
  input_mapping: Record<string, InputMapping>
}
interface Template {
  id: number; name: string; category: string; sub_type: string
  instruments: string[]; description: string; graph_json: GraphJson
  ai_rationale: string; calibration_json: Record<string, unknown>
  heuristic_ver: number; created_by?: string; created_at?: string
}
interface MarketEvent {
  id: number; name: string; category: string; sub_type: string
  start_date: string; end_date: string | null; level: string; created_at: string | null
  impact_score: number; market_impact: string; affected_assets: string
  actual_value: number | null; expected_value: number | null; surprise_pct: number | null
  analysis_id: number | null; template_id: number | null; activation_score: number | null
}
interface NodeResult { pred: number | null; act: number | null; status: string; label: string }
interface AnalysisResult {
  event: MarketEvent; template_id: number; template_name: string; instrument: string
  inputs: Record<string, number>; node_values: Record<string, number>
  actual_moves: Record<string, number>; yields: Record<string, number | null>
  activation: { score: number | null; nodes: Record<string, NodeResult>; correct: number; total: number }
  drift: { pre_pips: number | null; post_pips: number | null; drift_ratio: number | null; leak: string }
  prices_mode: string; analyzed_at: string; effective_lag_min?: number
}
interface Recommendation {
  template_id: number | null; template_name: string; confidence: number
  rationale: string; coefficient_suggestions: Record<string, number>
  instrument_focus: string[]; notes: string; error?: string
}
interface CalibRow {
  id: number; name: string; category: string
  n_analyses: number; avg_activation: number | null
  calibration: Record<string, { n?: number; avg_pred?: number; avg_actual?: number; coef_ratio?: number }>
}

// ── API ───────────────────────────────────────────────────────────────────────

const api = async (path: string, opts?: RequestInit) => {
  const r = await fetch(path, opts)
  if (!r.ok) { const t = await r.text(); throw new Error(t) }
  return r.json()
}

// ── Visuels ───────────────────────────────────────────────────────────────────

/** Couleur de nœud selon son type sémantique */
const nodeColor = (type: string): string => ({
  macro_event:  '#3b82f6',   // blue  — événements source (NFP, CPI, FOMC)
  observable:   '#06b6d4',   // cyan  — variables marché observables (OIS, 2Y, spreads)
  latent:       '#8b5cf6',   // violet — variables latentes (anticipations, sentiment)
  market_asset: '#10b981',   // green — instruments tradés (EUR/USD, S&P)
  // compat backward
  input:        '#3b82f6',
  intermediate: '#8b5cf6',
  output:       '#10b981',
}[type] ?? '#64748b')

/** Couleur de l'arête selon son signe */
const edgeColor = (sign?: string): string => ({
  positive: '#34d399',   // teal
  negative: '#f87171',   // rouge-rose
  neutral:  '#475569',   // gris ardoise
}[sign ?? 'neutral'] ?? '#475569')

/** Épaisseur selon strength */
const edgeWidth = (s?: number) => s === 1 ? 1 : s === 3 ? 3.5 : 2

/** Format lag minutes → "+5m" / "+2h" */
function fmtLag(min: number): string {
  if (min < 60)   return `+${min}m`
  return `+${(min / 60).toFixed(0)}h`
}
/** Format decay days → "↩3j" */
const fmtDecay = (d: number) => `↩${d}j`

const CAT_LABELS: Record<string, string> = {
  // Template categories
  macro_us:       'Macro US',
  macro_eu:       'Macro EU',
  geopolitical:   'Géopolitique',
  commodity:      'Matières 1ères',
  report:         'Report',
  sentiment:      'Sentiment',
  // Market event categories (aligned)
  event_calendar: 'Calendrier',
  calendar_event: 'Calendrier',
  fundamental:    'Fondamental',
}

// Chip color per category (inline styles to survive Tailwind purge)
const CAT_CHIP: Record<string, { bg: string; color: string; border: string }> = {
  macro_us:       { bg: '#1e3a5f', color: '#93c5fd', border: '#3b82f6' },
  macro_eu:       { bg: '#2d1b69', color: '#c4b5fd', border: '#8b5cf6' },
  event_calendar: { bg: '#0c3535', color: '#67e8f9', border: '#06b6d4' },
  calendar_event: { bg: '#0c3535', color: '#67e8f9', border: '#06b6d4' },
  geopolitical:   { bg: '#422006', color: '#fcd34d', border: '#d97706' },
  commodity:      { bg: '#14352a', color: '#6ee7b7', border: '#10b981' },
  report:         { bg: '#1e293b', color: '#94a3b8', border: '#475569' },
  sentiment:      { bg: '#4a1942', color: '#f9a8d4', border: '#ec4899' },
  fundamental:    { bg: '#1e1b4b', color: '#a5b4fc', border: '#6366f1' },
}
const chipStyle = (cat: string, active: boolean) => {
  const c = CAT_CHIP[cat] ?? { bg: '#1e293b', color: '#94a3b8', border: '#475569' }
  return {
    background: active ? c.bg : 'transparent',
    color: active ? c.color : '#64748b',
    border: `1px solid ${active ? c.border : '#334155'}`,
  }
}
const NODE_TYPES = ['macro_event', 'observable', 'latent', 'market_asset'] as const
const NODE_TYPE_LABELS: Record<string, string> = {
  macro_event: 'Macro event', observable: 'Observable', latent: 'Latent', market_asset: 'Market asset',
}
const fmtPips = (v: number | null | undefined) =>
  v == null ? '—' : `${v > 0 ? '+' : ''}${v.toFixed(0)} pip`

// ── SVG Graph renderer (partagé) ──────────────────────────────────────────────

export function GraphSVG({
  graph, nodeValues, activationNodes, compact = false,
  selectedNodeId, onNodeClick, onBgClick, editorMode = false,
}: {
  graph: GraphJson
  nodeValues?: Record<string, number>
  activationNodes?: Record<string, NodeResult>
  compact?: boolean
  selectedNodeId?: string | null
  onNodeClick?: (id: string, e: React.MouseEvent) => void
  onBgClick?: (e: React.MouseEvent<SVGSVGElement>) => void
  editorMode?: boolean
}) {
  const svgRef = useRef<SVGSVGElement>(null)
  const nodes  = graph.nodes || []
  const edges  = graph.edges || []

  // Scale positions to reduce overlap between nodes and edge labels
  const POS_SCALE = compact ? 1 : 1.55
  const scaledNodes = nodes.map(n => ({ ...n, x: n.x * POS_SCALE, y: n.y * POS_SCALE }))
  const nodeMap = Object.fromEntries(scaledNodes.map(n => [n.id, n]))

  const xs   = scaledNodes.map(n => n.x)
  const ys   = scaledNodes.map(n => n.y)
  const minX = (xs.length ? Math.min(...xs) : 0) - 80
  const minY = (ys.length ? Math.min(...ys) : 0) - 50
  const maxX = (xs.length ? Math.max(...xs) : 400) + 160
  const maxY = (ys.length ? Math.max(...ys) : 400) + 90
  const W = maxX - minX; const H = maxY - minY

  // Nodes larger in editor mode to accommodate type badge
  const NW = compact ? 92 : editorMode ? 140 : 130
  const NH = compact ? 32 : editorMode ? 54 : 48

  const statusColor = (s: string) =>
    s === 'correct' ? '#10b981' : s === 'wrong' ? '#ef4444' : '#64748b'

  function getNodeColor(n: CausalNode) {
    if (activationNodes?.[n.id]) return statusColor(activationNodes[n.id].status)
    return nodeColor(n.type)
  }

  function edgePath(e: CausalEdge) {
    const s = nodeMap[e.from]; const t = nodeMap[e.to]
    if (!s || !t) return ''
    const x1 = s.x; const y1 = s.y + NH / 2
    const x2 = t.x; const y2 = t.y - NH / 2
    const my = (y1 + y2) / 2
    return `M${x1},${y1} C${x1},${my} ${x2},${my} ${x2},${y2}`
  }

  function bezierPt(e: CausalEdge, t0: number): { x: number; y: number } | null {
    const s = nodeMap[e.from]; const t = nodeMap[e.to]
    if (!s || !t) return null
    const x1 = s.x; const y1 = s.y + NH / 2
    const x2 = t.x; const y2 = t.y - NH / 2
    const my = (y1 + y2) / 2
    const dx = x2 - x1; const dy = y2 - y1; const len = Math.sqrt(dx*dx + dy*dy) || 1
    const bx = (1-t0)**3*x1 + 3*(1-t0)**2*t0*x1 + 3*(1-t0)*t0**2*x2 + t0**3*x2
    const by = (1-t0)**3*y1 + 3*(1-t0)**2*t0*my + 3*(1-t0)*t0**2*my + t0**3*y2
    return { x: bx + (-dy/len)*16, y: by + (dx/len)*16 }
  }

  // Label position along bezier path with perpendicular offset to avoid the line
  function edgeLabelPos(e: CausalEdge): { x: number; y: number } | null {
    return bezierPt(e, 0.42)
  }

  function handleSvgClick(e: React.MouseEvent<SVGSVGElement>) {
    if (e.target === svgRef.current || (e.target as SVGElement).tagName === 'svg') {
      onBgClick?.(e)
    }
  }

  return (
    <svg ref={svgRef} viewBox={`${minX} ${minY} ${W} ${H}`}
      style={{ width: '100%', height: compact ? 280 : editorMode ? 620 : 560 }}
      className={clsx('overflow-visible', editorMode && 'cursor-crosshair')}
      onClick={handleSvgClick}>
      <defs>
        {(['positive', 'negative', 'neutral'] as const).map(sign => (
          <marker key={sign} id={`arrow-${sign}`} markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
            <path d="M0,0 L0,6 L8,3 z" fill={edgeColor(sign)} />
          </marker>
        ))}
      </defs>

      {/* Background grid in editor mode */}
      {editorMode && <>
        <defs>
          <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
            <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" strokeWidth="0.5" />
          </pattern>
        </defs>
        <rect x={minX} y={minY} width={W} height={H} fill="url(#grid)" />
      </>}

      {/* Edges */}
      {edges.map((e, i) => {
        const s = nodeMap[e.from]; const t = nodeMap[e.to]
        if (!s || !t) return null
        const sign  = e.sign ?? 'neutral'
        const color = edgeColor(sign)
        const sw    = edgeWidth(e.strength)
        const lpos      = e.label ? edgeLabelPos(e) : null
        const labelText = e.label ?? ''
        const labelW    = labelText.length * 5.8
        const lagTxtMin  = (!compact && e.lag_min)  ? fmtLag(e.lag_min)   : null
        const lagTxtDay  = (!compact && e.lag_days) ? `+${e.lag_days}j`   : null
        const lagTxt     = lagTxtMin && lagTxtDay ? `${lagTxtMin}/${lagTxtDay}` : (lagTxtMin ?? lagTxtDay)
        const decayTxt  = (!compact && e.decay_days != null) ? fmtDecay(e.decay_days) : null
        const lagPos    = lagTxt   ? bezierPt(e, 0.13) : null
        const decayPos  = decayTxt ? bezierPt(e, 0.84) : null
        return (
          <g key={i}>
            <path d={edgePath(e)} fill="none"
              stroke={color} strokeWidth={sw}
              strokeDasharray={e.style === 'dashed' ? '6,3' : undefined}
              markerEnd={`url(#arrow-${sign})`} opacity={0.85} />
            {lpos && (
              <g>
                <rect x={lpos.x - 3} y={lpos.y - 10} width={labelW + 6} height={13}
                  rx={3} fill="#0c1220" opacity={0.88} />
                <text x={lpos.x} y={lpos.y} fill={color} fontSize={9.5} fontWeight="500"
                  letterSpacing="0.2">{labelText}</text>
              </g>
            )}
            {lagPos && lagTxt && (
              <g>
                <rect x={lagPos.x - 3} y={lagPos.y - 10} width={lagTxt.length * 5.8 + 6} height={13}
                  rx={3} fill="#0c1220" opacity={0.88} />
                <text x={lagPos.x} y={lagPos.y} fill="#fbbf24" fontSize={9} fontWeight="600">{lagTxt}</text>
              </g>
            )}
            {decayPos && decayTxt && (
              <g>
                <rect x={decayPos.x - 3} y={decayPos.y - 10} width={decayTxt.length * 5.8 + 6} height={13}
                  rx={3} fill="#0c1220" opacity={0.88} />
                <text x={decayPos.x} y={decayPos.y} fill="#94a3b8" fontSize={9} fontWeight="600">{decayTxt}</text>
              </g>
            )}
          </g>
        )
      })}

      {/* Nodes */}
      {scaledNodes.map(n => {
        const col  = getNodeColor(n)
        const val   = nodeValues?.[n.id]
        const isSel = selectedNodeId === n.id
        const maxLen = editorMode ? 22 : 21
        const lFS    = compact ? 9 : 12
        // vertical layout inside node
        const labelY = compact ? 12 : editorMode ? 17 : 16
        const valY   = compact ? 22 : 34
        const typeY  = NH - 7   // type badge near bottom
        return (
          <g key={n.id} transform={`translate(${n.x - NW / 2},${n.y - NH / 2})`}
            onClick={e => { e.stopPropagation(); onNodeClick?.(n.id, e) }}
            style={{ cursor: editorMode ? 'pointer' : 'default' }}>
            {isSel && (
              <rect width={NW + 8} height={NH + 8} x={-4} y={-4} rx={9}
                fill="none" stroke="#facc15" strokeWidth={2} opacity={0.85} />
            )}
            <rect width={NW} height={NH} rx={7}
              fill={col} fillOpacity={isSel ? 0.32 : 0.16}
              stroke={col} strokeWidth={isSel ? 2 : 1.5} />
            {/* Label */}
            <text x={NW / 2} y={labelY} textAnchor="middle"
              fill="#e2e8f0" fontSize={lFS} fontWeight="600">
              {n.label.length > maxLen ? n.label.slice(0, maxLen - 1) + '…' : n.label}
            </text>
            {/* Value (analyze mode) */}
            {val != null && (
              <text x={NW / 2} y={valY} textAnchor="middle"
                fill={col} fontSize={lFS} fontWeight="700">
                {val > 0 ? '+' : ''}{val.toFixed(1)}{n.unit ? ` ${n.unit}` : ''}
              </text>
            )}
            {/* Type badge (editor only) — inside node at bottom */}
            {editorMode && (
              <>
                <rect x={4} y={typeY - 8} width={NW - 8} height={10} rx={3}
                  fill={col} fillOpacity={0.18} />
                <text x={NW / 2} y={typeY} textAnchor="middle"
                  fill={col} fontSize={8} fontWeight="500" opacity={0.9}>
                  {n.type?.replace(/_/g, ' ')}
                </text>
              </>
            )}
          </g>
        )
      })}
    </svg>
  )
}

// ── Légende des conventions ───────────────────────────────────────────────────

function GraphLegend() {
  return (
    <div className="bg-dark-800/60 border border-slate-700/30 rounded-lg p-3 text-xs space-y-2">
      <div className="text-slate-400 font-semibold uppercase tracking-wider text-xs mb-2">Conventions</div>
      <div className="space-y-1.5">
        {[
          { color: nodeColor('macro_event'),  label: 'Macro event (source)' },
          { color: nodeColor('observable'),   label: 'Observable (OIS, taux…)' },
          { color: nodeColor('latent'),       label: 'Latent (anticipations…)' },
          { color: nodeColor('market_asset'), label: 'Market asset (FX, S&P…)' },
        ].map(({ color, label }) => (
          <div key={label} className="flex items-center gap-2">
            <div className="w-3 h-3 rounded-sm flex-shrink-0" style={{ background: color, opacity: 0.7 }} />
            <span className="text-slate-400">{label}</span>
          </div>
        ))}
      </div>
      <div className="border-t border-slate-700/30 pt-2 space-y-1">
        {[
          { style: 'solid 3px', color: '#34d399',  label: 'Causalité directe forte +' },
          { style: 'solid 1.5px', color: '#f87171', label: 'Effet négatif direct' },
          { style: 'dashed 2px', color: '#94a3b8', label: 'Transmission indirecte' },
        ].map(({ style, color, label }) => (
          <div key={label} className="flex items-center gap-2">
            <div className="w-8 h-0 flex-shrink-0" style={{ borderTop: `${style} ${color}` }} />
            <span className="text-slate-400">{label}</span>
          </div>
        ))}
      </div>
    </div>
  )
}

// ── Graph Patch Grammar ───────────────────────────────────────────────────────
// Syntax: (+|-|~)(node|edge|coef|input|instruments) key=val key="val with spaces"
// sign shortcuts: + positive  - negative  = neutral

interface GrammarOp {
  op: '+' | '-' | '~'
  entity: 'node' | 'edge' | 'coef' | 'input' | 'instruments'
  args: Record<string, string>
  raw: string
}

function parseGrammarArgs(s: string): Record<string, string> {
  const res: Record<string, string> = {}
  const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g
  let m: RegExpExecArray | null
  while ((m = re.exec(s)) !== null) res[m[1]] = m[2] !== undefined ? m[2] : m[3]
  return res
}

function parseGrammar(text: string): { ops: GrammarOp[]; errors: string[] } {
  const ops: GrammarOp[] = []; const errors: string[] = []
  for (const rawLine of text.split('\n')) {
    const line = rawLine.trim()
    if (!line || line.startsWith('#')) continue
    const m = line.match(/^([+\-~])(node|edge|coef|input|instruments)\s*(.*)/i)
    if (!m) { errors.push(`Ligne non reconnue: "${line}"`); continue }
    const entity = m[2].toLowerCase() as GrammarOp['entity']
    const args = entity === 'instruments' ? { list: m[3].trim() } : parseGrammarArgs(m[3])
    ops.push({ op: m[1] as '+' | '-' | '~', entity, args, raw: line })
  }
  return { ops, errors }
}

function resolveSign(s?: string): 'positive' | 'negative' | 'neutral' {
  if (!s) return 'neutral'
  if (s === '+' || s === 'positive') return 'positive'
  if (s === '-' || s === 'negative') return 'negative'
  return 'neutral'
}

function applyGrammarOps(state: EditorState, ops: GrammarOp[]): { state: EditorState; errors: string[] } {
  let s: EditorState = {
    ...state,
    nodes: [...state.nodes], edges: [...state.edges],
    coefficients: { ...state.coefficients }, inputMapping: { ...state.inputMapping },
    instruments: [...state.instruments],
  }
  const errors: string[] = []

  for (const op of ops) {
    try {
      if (op.entity === 'node') {
        const { id, label, type, x, y, formula, unit, instrument } = op.args
        if (op.op === '+') {
          if (!id) { errors.push('+node: id manquant'); continue }
          if (s.nodes.find(n => n.id === id)) { errors.push(`+node: "${id}" existe déjà`); continue }
          const autoY = s.nodes.length ? Math.max(...s.nodes.map(n => n.y)) + 120 : 200
          s.nodes = [...s.nodes, {
            id, label: label ?? id,
            type: (type ?? 'observable') as CausalNode['type'],
            x: x ? +x : 300, y: y ? +y : autoY,
            ...(formula ? { formula } : {}), ...(unit ? { unit } : {}), ...(instrument ? { instrument } : {}),
          }]
        } else if (op.op === '-') {
          const tid = id ?? Object.values(op.args)[0]
          if (!tid) { errors.push('-node: id manquant'); continue }
          s.nodes = s.nodes.filter(n => n.id !== tid)
          s.edges = s.edges.filter(e => e.from !== tid && e.to !== tid)
          const im = { ...s.inputMapping }; delete im[tid]; s.inputMapping = im
        } else {
          if (!id) { errors.push('~node: id manquant'); continue }
          s.nodes = s.nodes.map(n => n.id !== id ? n : {
            ...n,
            ...(label !== undefined ? { label } : {}),
            ...(type !== undefined ? { type: type as CausalNode['type'] } : {}),
            ...(x !== undefined ? { x: +x } : {}),
            ...(y !== undefined ? { y: +y } : {}),
            ...(formula !== undefined ? { formula } : {}),
            ...(unit !== undefined ? { unit } : {}),
            ...(instrument !== undefined ? { instrument } : {}),
          })
        }
      } else if (op.entity === 'edge') {
        const { from, to, sign, s: str, style, label, lag, decay } = op.args
        if (op.op === '+') {
          if (!from || !to) { errors.push('+edge: from/to manquants'); continue }
          s.edges = [...s.edges, {
            from, to, sign: resolveSign(sign),
            strength: (str ? +str : 2) as 1|2|3,
            style: (style ?? 'solid') as 'solid'|'dashed',
            ...(label ? { label } : {}),
            ...(lag !== undefined ? { lag_days: +lag } : {}),
            ...(decay !== undefined ? { decay_days: decay === 'null' ? null : +decay } : {}),
          }]
        } else if (op.op === '-') {
          if (!from || !to) { errors.push('-edge: from/to manquants'); continue }
          s.edges = s.edges.filter(e => !(e.from === from && e.to === to))
        } else {
          if (!from || !to) { errors.push('~edge: from/to manquants'); continue }
          s.edges = s.edges.map(e => (e.from !== from || e.to !== to) ? e : {
            ...e,
            ...(sign !== undefined ? { sign: resolveSign(sign) } : {}),
            ...(str !== undefined ? { strength: +str as 1|2|3 } : {}),
            ...(style !== undefined ? { style: style as 'solid'|'dashed' } : {}),
            ...(label !== undefined ? { label } : {}),
            ...(lag !== undefined ? { lag_days: +lag } : {}),
            ...(decay !== undefined ? { decay_days: decay === 'null' ? null : +decay } : {}),
          })
        }
      } else if (op.entity === 'coef') {
        const { key, value, desc } = op.args
        if (op.op === '+') {
          if (!key || value === undefined) { errors.push('+coef: key/value manquants'); continue }
          s.coefficients = { ...s.coefficients, [key]: { value: +value, calibrated: null, description: desc ?? '' } }
        } else if (op.op === '-') {
          if (!key) { errors.push('-coef: key manquant'); continue }
          const cc = { ...s.coefficients }; delete cc[key]; s.coefficients = cc
        } else {
          if (!key) { errors.push('~coef: key manquant'); continue }
          const ex = s.coefficients[key] ?? { value: 0, calibrated: null, description: '' }
          s.coefficients = { ...s.coefficients, [key]: {
            ...ex,
            ...(value !== undefined ? { value: +value } : {}),
            ...(desc !== undefined ? { description: desc } : {}),
          }}
        }
      } else if (op.entity === 'input') {
        const { node, source, key } = op.args
        if (op.op === '-') {
          if (!node) { errors.push('-input: node manquant'); continue }
          const im = { ...s.inputMapping }; delete im[node]; s.inputMapping = im
        } else {
          if (!node || !source) { errors.push(`${op.op}input: node/source manquants`); continue }
          s.inputMapping = { ...s.inputMapping, [node]: { source: source as InputMapping['source'], ...(key ? { key } : {}) } }
        }
      } else if (op.entity === 'instruments') {
        const list = op.args.list?.split(',').map(x => x.trim()).filter(Boolean)
        if (list?.length) s.instruments = list
      }
    } catch (e) { errors.push(`Erreur "${op.raw}": ${e}`) }
  }
  return { state: s, errors }
}

function graphToGrammar(state: EditorState): string {
  const lines: string[] = []

  if (state.nodes.length) {
    lines.push(`# Nœuds (${state.nodes.length})`)
    for (const n of state.nodes) {
      let l = `+node id=${n.id} label="${n.label}" type=${n.type} x=${n.x} y=${n.y}`
      if (n.instrument) l += ` instrument=${n.instrument}`
      if (n.unit)       l += ` unit=${n.unit}`
      if (n.formula)    l += ` formula="${n.formula}"`
      lines.push(l)
    }
    lines.push('')
  }

  if (state.edges.length) {
    lines.push(`# Arêtes (${state.edges.length})`)
    for (const e of state.edges) {
      const sg = e.sign === 'positive' ? '+' : e.sign === 'negative' ? '-' : '='
      let l = `+edge from=${e.from} to=${e.to} sign=${sg}`
      if (e.strength != null && e.strength !== 2) l += ` s=${e.strength}`
      if (e.style === 'dashed') l += ` style=dashed`
      if (e.label)              l += ` label="${e.label}"`
      if (e.lag_days != null)   l += ` lag=${e.lag_days}`
      if (e.decay_days === null) l += ` decay=null`
      else if (e.decay_days != null) l += ` decay=${e.decay_days}`
      lines.push(l)
    }
    lines.push('')
  }

  const coefs = Object.entries(state.coefficients)
  if (coefs.length) {
    lines.push(`# Coefficients (${coefs.length})`)
    for (const [key, c] of coefs) {
      let l = `+coef key=${key} value=${c.value}`
      if (c.description) l += ` desc="${c.description}"`
      lines.push(l)
    }
    lines.push('')
  }

  const inputs = Object.entries(state.inputMapping)
  if (inputs.length) {
    lines.push(`# Inputs (${inputs.length})`)
    for (const [node, m] of inputs) {
      let l = `~input node=${node} source=${m.source}`
      if (m.key) l += ` key=${m.key}`
      lines.push(l)
    }
    lines.push('')
  }

  if (state.instruments.length)
    lines.push(`~instruments ${state.instruments.join(',')}`)

  return lines.join('\n').trim()
}

const GRAMMAR_REFERENCE = `# Nœuds
+node id=<id> label="<texte>" type=<macro_event|observable|latent|market_asset> [x=<n>] [y=<n>] [instrument=<str>]
~node id=<id>  [label="..."] [type=...] [x=<n>] [y=<n>] [instrument=<str>]
-node id=<id>

# Arêtes  (sign: + positif  - négatif  = neutre)
+edge from=<id> to=<id> [sign=+|-|=] [s=1|2|3] [style=solid|dashed] [label="..."] [lag=<j>] [decay=<j|null>]
~edge from=<id> to=<id>  [...mêmes champs...]
-edge from=<id> to=<id>

# Coefficients
+coef key=<str> value=<float> [desc="..."]
~coef key=<str> [value=<float>] [desc="..."]
-coef key=<str>

# Mapping d'entrée
~input node=<id> source=<surprise|actual_value|user_input|impact_score_scaled> [key=<str>]
-input node=<id>

# Instruments de sortie
~instruments EURUSD,SP500,...`

// ── Tab: Bibliothèque ─────────────────────────────────────────────────────────

function TabLibrary({
  initialTemplateId,
  onOpenEditor,
}: {
  initialTemplateId?: number | null
  onOpenEditor: (id: number) => void
}) {
  const [templates, setTemplates] = useState<Template[]>([])
  const [selected, setSelected]   = useState<Template | null>(null)
  const [catFilter, setCatFilter] = useState('')
  const [subFilter, setSubFilter] = useState('')
  const [instFilter, setInstFilter] = useState('')
  const [dateFrom, setDateFrom]   = useState('')
  const [dateTo, setDateTo]       = useState('')
  const [search, setSearch]       = useState('')
  const [saving, setSaving]         = useState(false)
  const [deleting, setDeleting]     = useState(false)
  const [savingLag, setSavingLag]   = useState(false)
  const [savingTheory, setSavingTheory] = useState(false)
  const [genTheory, setGenTheory]   = useState(false)
  const [editCoefs, setEditCoefs]   = useState<Record<string, number>>({})
  const [editEdgesLag, setEdgesLag] = useState<CausalEdge[]>([])
  const [lagDirty, setLagDirty]   = useState(false)
  const [loading, setLoading]     = useState(true)
  const [apiError, setApiError]   = useState<string | null>(null)
  const [debugInfo, setDebugInfo] = useState<Record<string, unknown> | null>(null)

  useEffect(() => {
    setLoading(true); setApiError(null)
    api('/api/causal-lab/templates')
      .then(data => {
        setTemplates(data)
        if (data.length === 0) api('/api/causal-lab/debug').then(setDebugInfo).catch(() => {})
      })
      .catch(e => setApiError(String(e)))
      .finally(() => setLoading(false))
  }, [])

  // Auto-select template when arriving from a deep-link (?template=ID)
  useEffect(() => {
    if (!initialTemplateId || !templates.length || selected) return
    const t = templates.find(t => t.id === initialTemplateId)
    if (t) selectTemplate(t)
  }, [templates, initialTemplateId])

  function selectTemplate(t: Template) {
    setSelected(t)
    setEditCoefs(Object.fromEntries(
      Object.entries(t.graph_json?.coefficients || {}).map(([k, v]) => [k, v.value])
    ))
    setEdgesLag(JSON.parse(JSON.stringify(t.graph_json?.edges || [])))
    setLagDirty(false)
  }

  async function saveCoefs() {
    if (!selected) return
    setSaving(true)
    try {
      await api(`/api/causal-lab/template/${selected.id}`, {
        method: 'PUT', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ coefficients: editCoefs }),
      })
      const fresh = await api(`/api/causal-lab/template/${selected.id}`)
      setSelected(fresh)
    } finally { setSaving(false) }
  }

  async function saveTemporalParams() {
    if (!selected) return
    setSavingLag(true)
    try {
      const updatedGraph = { ...selected.graph_json, edges: editEdgesLag }
      await api(`/api/causal-lab/template/${selected.id}`, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ graph_json: updatedGraph }),
      })
      const fresh = await api(`/api/causal-lab/template/${selected.id}`)
      setSelected(fresh)
      setEdgesLag(JSON.parse(JSON.stringify(fresh.graph_json?.edges || [])))
      setLagDirty(false)
    } finally { setSavingLag(false) }
  }

  async function generateTheory() {
    if (!selected) return
    setGenTheory(true)
    try {
      await api(`/api/causal-lab/template/${selected.id}/generate-theory`, { method: 'POST' })
      const fresh = await api(`/api/causal-lab/template/${selected.id}`)
      setSelected(fresh)
    } finally { setGenTheory(false) }
  }

  async function saveTheoryParams(updates: Record<string, unknown>) {
    if (!selected) return
    setSavingTheory(true)
    try {
      const newCalib = { ...(selected.calibration_json || {}), ...updates }
      await api(`/api/causal-lab/template/${selected.id}`, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ calibration_json: newCalib }),
      })
      const fresh = await api(`/api/causal-lab/template/${selected.id}`)
      setSelected(fresh)
    } finally { setSavingTheory(false) }
  }

  function updateEdgeLag(i: number, field: 'lag_min' | 'lag_days' | 'diffusion_min' | 'decay_days', val: string) {
    setEdgesLag(prev => prev.map((e, idx) => idx !== i ? e : {
      ...e,
      [field]: val === '' ? (field === 'decay_days' ? null : undefined)
                          : (field === 'decay_days' ? parseFloat(val) : parseInt(val)),
    }))
    setLagDirty(true)
  }

  async function deleteTemplate() {
    if (!selected) return
    if (!window.confirm(`Supprimer "${selected.name}" ? Cette action est irréversible.`)) return
    setDeleting(true)
    try {
      await api(`/api/causal-lab/template/${selected.id}`, { method: 'DELETE' })
      setSelected(null)
      const data = await api(`/api/causal-lab/templates${catFilter ? `?category=${catFilter}` : ''}`)
      setTemplates(data)
    } catch (e: unknown) {
      alert(String(e))
    } finally { setDeleting(false) }
  }

  // Derive filter options from loaded templates
  const cats  = [...new Set(templates.map(t => t.category))].sort()
  const subs  = catFilter
    ? [...new Set(templates.filter(t => t.category === catFilter).map(t => t.sub_type).filter(Boolean))].sort()
    : []
  const insts = [...new Set(templates.flatMap(t => t.instruments))].sort()

  // Client-side filtered list
  const visible = templates.filter(t => {
    if (catFilter  && t.category !== catFilter) return false
    if (subFilter  && t.sub_type !== subFilter)  return false
    if (instFilter && !t.instruments.includes(instFilter)) return false
    if (dateFrom   && t.created_at && t.created_at < dateFrom) return false
    if (dateTo     && t.created_at && t.created_at > dateTo + 'T23:59') return false
    if (search     && !t.name.toLowerCase().includes(search.toLowerCase())
                   && !(t.description ?? '').toLowerCase().includes(search.toLowerCase())) return false
    return true
  })

  return (
    <div className="flex flex-col gap-3">
      {/* ── Filter bar ── */}
      <div className="bg-dark-700/60 rounded-lg border border-slate-700/30 p-3 space-y-2">
        {/* Category chips */}
        <div className="flex flex-wrap gap-1.5 items-center">
          <Filter className="w-3 h-3 text-slate-500 shrink-0" />
          <button
            onClick={() => { setCatFilter(''); setSubFilter('') }}
            className="text-xs px-2.5 py-1 rounded-full border transition-all"
            style={chipStyle('', catFilter === '')}>
            Tout
          </button>
          {cats.map(c => (
            <button key={c}
              onClick={() => { setCatFilter(catFilter === c ? '' : c); setSubFilter('') }}
              className="text-xs px-2.5 py-1 rounded-full border transition-all"
              style={chipStyle(c, catFilter === c)}>
              {CAT_LABELS[c] ?? c}
            </button>
          ))}
        </div>

        {/* Sub-type chips — only if category selected and has sub-types */}
        {subs.length > 0 && (
          <div className="flex flex-wrap gap-1.5 items-center pl-4">
            <span className="text-xs text-slate-600">↳</span>
            <button
              onClick={() => setSubFilter('')}
              className="text-xs px-2 py-0.5 rounded-full border transition-all"
              style={{ background: subFilter === '' ? '#1e293b' : 'transparent', color: subFilter === '' ? '#94a3b8' : '#475569', border: '1px solid ' + (subFilter === '' ? '#475569' : '#1e293b') }}>
              Tous
            </button>
            {subs.map(s => (
              <button key={s}
                onClick={() => setSubFilter(subFilter === s ? '' : s)}
                className="text-xs px-2 py-0.5 rounded-full border transition-all"
                style={{ background: subFilter === s ? '#1e293b' : 'transparent', color: subFilter === s ? '#e2e8f0' : '#475569', border: '1px solid ' + (subFilter === s ? '#64748b' : '#1e293b') }}>
                {s}
              </button>
            ))}
          </div>
        )}

        {/* Instrument + date row */}
        <div className="flex flex-wrap gap-2 items-center">
          {/* Instrument chips */}
          <div className="flex flex-wrap gap-1 items-center">
            <span className="text-xs text-slate-600">Inst:</span>
            {insts.map(i => (
              <button key={i}
                onClick={() => setInstFilter(instFilter === i ? '' : i)}
                className="text-xs px-2 py-0.5 rounded border transition-all"
                style={{ background: instFilter === i ? '#14352a' : 'transparent', color: instFilter === i ? '#6ee7b7' : '#475569', border: '1px solid ' + (instFilter === i ? '#10b981' : '#1e293b') }}>
                {i}
              </button>
            ))}
          </div>
          {/* Date range */}
          <div className="flex items-center gap-1.5 ml-auto">
            <span className="text-xs text-slate-600">Créé:</span>
            <input type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)}
              className="bg-dark-800 border border-slate-700 rounded px-1.5 py-0.5 text-xs text-slate-400 w-32" />
            <span className="text-slate-600 text-xs">→</span>
            <input type="date" value={dateTo} onChange={e => setDateTo(e.target.value)}
              className="bg-dark-800 border border-slate-700 rounded px-1.5 py-0.5 text-xs text-slate-400 w-32" />
            {(dateFrom || dateTo) && (
              <button onClick={() => { setDateFrom(''); setDateTo('') }}
                className="text-slate-600 hover:text-slate-400">
                <X className="w-3 h-3" />
              </button>
            )}
          </div>
        </div>

        {/* Search */}
        <div className="relative">
          <Search className="w-3 h-3 absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-600" />
          <input value={search} onChange={e => setSearch(e.target.value)}
            placeholder="Rechercher par nom / description…"
            className="w-full bg-dark-800 border border-slate-700 rounded pl-7 pr-3 py-1.5 text-xs text-slate-300 placeholder:text-slate-600" />
          {search && (
            <button onClick={() => setSearch('')} className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-600 hover:text-slate-400">
              <X className="w-3 h-3" />
            </button>
          )}
        </div>
      </div>

      {/* ── Main content: list + detail ── */}
      <div className="flex gap-4">
        <div className="w-64 flex-shrink-0">
        {apiError && (
          <div className="mb-2 p-3 bg-red-900/20 border border-red-700/40 rounded text-xs text-red-300 break-all">
            <div className="font-bold mb-1">Erreur API</div>{apiError}
          </div>
        )}
        {debugInfo && (
          <div className="mb-2 p-3 bg-yellow-900/20 border border-yellow-700/40 rounded text-xs text-yellow-200">
            <div className="font-bold mb-1">Diagnostic</div>
            <pre className="whitespace-pre-wrap">{JSON.stringify(debugInfo, null, 2)}</pre>
          </div>
        )}

        {loading
          ? <div className="text-slate-500 text-xs p-4">Chargement…</div>
          : <>
            <div className="text-xs text-slate-600 mb-1.5">{visible.length} graphe{visible.length !== 1 ? 's' : ''}</div>
            <div className="space-y-1 overflow-y-auto max-h-[calc(100vh-400px)]">
              {visible.map(t => (
                <button key={t.id} onClick={() => selectTemplate(t)}
                  className={clsx('w-full text-left px-3 py-2.5 rounded border text-xs transition-all',
                    selected?.id === t.id
                      ? 'bg-blue-900/30 border-blue-500/50 text-blue-200'
                      : 'bg-dark-700 border-slate-700/50 text-slate-300 hover:border-slate-500')}>
                  <div className="font-medium truncate">{t.name}</div>
                  <div className="text-slate-500 mt-0.5 flex items-center gap-1.5 flex-wrap">
                    <span className="px-1.5 py-px rounded-sm text-[10px]"
                      style={chipStyle(t.category, true)}>
                      {CAT_LABELS[t.category] ?? t.category}
                    </span>
                    {t.sub_type && <span className="text-slate-600 text-[10px]">{t.sub_type}</span>}
                    <span className="text-slate-600">{t.instruments.join(', ')}</span>
                  </div>
                </button>
              ))}
            </div>
          </>
        }
        </div>

      {selected ? (
        <div className="flex-1 overflow-y-auto space-y-4 min-w-0 max-h-[calc(100vh-380px)]">
          <div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
            <div className="flex items-start justify-between mb-2">
              <div className="flex-1 min-w-0">
                <h3 className="text-white font-semibold">{selected.name}</h3>
                <p className="text-slate-400 text-xs mt-1">{selected.description}</p>
                {selected.created_at && (
                  <p className="text-slate-600 text-xs mt-1">
                    Créé le {new Date(selected.created_at).toLocaleDateString('fr-FR')}
                    {selected.created_by && ` · par ${selected.created_by}`}
                  </p>
                )}
              </div>
              <div className="flex items-center gap-2 flex-shrink-0 ml-3">
                <button
                  onClick={() => onOpenEditor(selected.id)}
                  title="Modifier dans l'éditeur"
                  className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-violet-800/40 hover:bg-violet-700/50 text-violet-300 border border-violet-700/40 text-xs font-medium transition-all">
                  <ExternalLink className="w-3 h-3" /> Éditer
                </button>
                <span className="text-xs px-2 py-1 rounded bg-purple-900/30 text-purple-300 border border-purple-700/40">
                  v{selected.heuristic_ver}
                </span>
                <button
                  onClick={deleteTemplate}
                  disabled={deleting}
                  title="Supprimer ce template"
                  className="p-1 rounded transition-colors text-slate-500 hover:bg-red-900/30 hover:text-red-400 disabled:opacity-40">
                  <Trash2 className="w-3.5 h-3.5" />
                </button>
              </div>
            </div>
            {selected.ai_rationale && (
              <div className="mt-3 p-3 bg-blue-900/10 border border-blue-800/30 rounded text-xs text-blue-200">
                <Brain className="w-3 h-3 inline mr-1.5 text-blue-400" />
                {selected.ai_rationale}
              </div>
            )}
            <div className="mt-3 flex gap-2 flex-wrap items-center">
              <span className="text-xs px-2 py-0.5 rounded border" style={chipStyle(selected.category, true)}>
                {CAT_LABELS[selected.category] ?? selected.category}
              </span>
              {selected.sub_type && (
                <span className="text-xs px-2 py-0.5 rounded bg-slate-700/40 text-slate-400 border border-slate-600/40">
                  {selected.sub_type}
                </span>
              )}
              {selected.instruments.map(i => (
                <span key={i} className="text-xs px-2 py-0.5 rounded bg-emerald-900/20 text-emerald-400 border border-emerald-700/30">{i}</span>
              ))}
            </div>
          </div>

          <div className="grid grid-cols-[1fr_200px] gap-4">
            <div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
              <h4 className="text-slate-300 text-xs font-semibold mb-3 uppercase tracking-wider">Graphe causal</h4>
              <GraphSVG graph={selected.graph_json} />
            </div>
            <GraphLegend />
          </div>

          <div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
            <h4 className="text-slate-300 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
              <Sliders className="w-3.5 h-3.5" /> Coefficients
            </h4>
            <div className="space-y-2">
              {Object.entries(selected.graph_json?.coefficients || {}).map(([k, c]) => (
                <div key={k} className="grid grid-cols-[1fr_auto] gap-3 items-center">
                  <div>
                    <div className="text-xs text-slate-300 font-medium">{k}</div>
                    <div className="text-xs text-slate-500">{c.description}</div>
                  </div>
                  <input type="number" step="0.001"
                    value={editCoefs[k] ?? c.value}
                    onChange={e => setEditCoefs(p => ({ ...p, [k]: parseFloat(e.target.value) }))}
                    className="w-28 bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 text-right" />
                </div>
              ))}
            </div>
            <button onClick={saveCoefs} disabled={saving}
              className="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 rounded text-xs font-medium text-white">
              {saving ? 'Sauvegarde…' : 'Sauvegarder les coefficients'}
            </button>
          </div>

          {/* Paramètres temporels */}
          {editEdgesLag.some(e => e.from && e.to) && (
            <div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
              <h4 className="text-slate-300 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
                <Zap className="w-3.5 h-3.5 text-yellow-400" /> Paramètres temporels
              </h4>
              <div className="space-y-2">
                <div className="grid grid-cols-[1fr_52px_52px_52px_52px] gap-1.5 text-xs text-slate-500 pb-1 border-b border-slate-700/30">
                  <span>Arête</span>
                  <span className="text-center" title="Délai intraday (mode 5m)">Lag 5m</span>
                  <span className="text-center" title="Délai journalier (mode daily)">Lag j</span>
                  <span className="text-center">Diff. m</span>
                  <span className="text-center">Decay j</span>
                </div>
                {editEdgesLag.map((e, i) => (
                  <div key={i} className="grid grid-cols-[1fr_52px_52px_52px_52px] gap-1.5 items-center">
                    <span className="text-xs font-mono text-slate-400 truncate">{e.from} → {e.to}</span>
                    <input type="number" min={0} step={5}
                      value={e.lag_min ?? ''}
                      onChange={ev => updateEdgeLag(i, 'lag_min', ev.target.value)}
                      placeholder="0"
                      title="Délai en minutes (intraday 5m)"
                      className="w-full bg-dark-800 border border-slate-600 rounded px-1 py-1 text-xs text-slate-200 text-center" />
                    <input type="number" min={0} step={1}
                      value={e.lag_days ?? ''}
                      onChange={ev => updateEdgeLag(i, 'lag_days', ev.target.value)}
                      placeholder="0"
                      title="Délai en jours (mode journalier)"
                      className="w-full bg-dark-800 border border-amber-600/40 rounded px-1 py-1 text-xs text-amber-200 text-center" />
                    <input type="number" min={0} step={15}
                      value={e.diffusion_min ?? ''}
                      onChange={ev => updateEdgeLag(i, 'diffusion_min', ev.target.value)}
                      placeholder="60"
                      className="w-full bg-dark-800 border border-slate-600 rounded px-1 py-1 text-xs text-slate-200 text-center" />
                    <input type="number" min={0} step={1}
                      value={e.decay_days ?? ''}
                      onChange={ev => updateEdgeLag(i, 'decay_days', ev.target.value)}
                      placeholder="∞"
                      className="w-full bg-dark-800 border border-slate-600 rounded px-1 py-1 text-xs text-slate-200 text-center" />
                  </div>
                ))}
              </div>
              <button onClick={saveTemporalParams} disabled={savingLag}
                className={clsx(
                  'mt-4 px-4 py-2 rounded text-xs font-medium text-white disabled:opacity-50 transition-all',
                  lagDirty ? 'bg-amber-600 hover:bg-amber-500 ring-2 ring-amber-400/50 animate-pulse'
                           : 'bg-slate-700 hover:bg-slate-600'
                )}>
                {savingLag ? 'Sauvegarde…' : lagDirty ? '⚠ Sauvegarder les paramètres temporels' : 'Paramètres temporels sauvegardés'}
              </button>
            </div>
          )}

          {/* Paramètres théoriques */}
          <div className="bg-dark-700 rounded-lg p-4 border border-violet-800/30">
            <div className="flex items-center gap-2 mb-3">
              <h4 className="text-slate-300 text-xs font-semibold uppercase tracking-wider flex items-center gap-2 flex-1">
                <span className="text-violet-400">⟁</span> Paramètres théoriques
              </h4>
              <button
                onClick={generateTheory}
                disabled={genTheory}
                className="px-3 py-1 bg-violet-700 hover:bg-violet-600 disabled:opacity-50 rounded text-xs font-medium text-white flex items-center gap-1.5"
              >
                {genTheory ? (
                  <><span className="animate-spin inline-block">↻</span> Génération…</>
                ) : (
                  <><span>✦</span> Générer IA</>
                )}
              </button>
            </div>

            {(() => {
              const calib = selected.calibration_json || {}
              const absorption  = calib.absorption_days as number | undefined
              const halfLife    = calib.half_life_days  as number | undefined
              const lagDays     = calib.lag_days        as number | undefined
              const decay       = calib.decay_type      as string | undefined
              const conf        = calib.confidence      as number | undefined
              const rationale   = calib.theory_rationale as string | undefined
              const genAt       = calib.theory_generated_at as string | undefined
              return (
                <div className="space-y-3">
                  {rationale && (
                    <p className="text-xs text-violet-300 italic border-l-2 border-violet-700/60 pl-2">{rationale}</p>
                  )}
                  <div className="grid grid-cols-3 gap-3">
                    <div>
                      <div className="text-xs text-slate-500 mb-1">Lag (j)</div>
                      <input
                        type="number" min={0} max={30} step={1}
                        value={lagDays ?? ''}
                        placeholder="0"
                        onChange={e => saveTheoryParams({ lag_days: parseInt(e.target.value) || 0 })}
                        className="w-full bg-dark-800 border border-amber-600/40 rounded px-2 py-1 text-xs text-amber-200 text-center"
                      />
                    </div>
                    <div>
                      <div className="text-xs text-slate-500 mb-1">Demi-vie t½ (j)</div>
                      <input
                        type="number" min={1} max={120} step={1}
                        value={halfLife ?? ''}
                        placeholder="—"
                        onChange={e => saveTheoryParams({ half_life_days: parseInt(e.target.value) || 5 })}
                        className="w-full bg-dark-800 border border-violet-600/40 rounded px-2 py-1 text-xs text-violet-200 text-center"
                      />
                    </div>
                    <div>
                      <div className="text-xs text-slate-500 mb-1">Absorption (j)</div>
                      <input
                        type="number" min={1} max={180} step={1}
                        value={absorption ?? ''}
                        placeholder="—"
                        onChange={e => saveTheoryParams({ absorption_days: parseInt(e.target.value) || 7 })}
                        className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 text-center"
                      />
                    </div>
                  </div>
                  <div>
                    <div className="text-xs text-slate-500 mb-1">Type de décroissance</div>
                    <select
                      value={decay ?? 'exp'}
                      onChange={e => saveTheoryParams({ decay_type: e.target.value })}
                      className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200"
                    >
                      <option value="step">step — tout ou rien</option>
                      <option value="linear">linear — déclin linéaire</option>
                      <option value="exp">exp — déclin exponentiel</option>
                    </select>
                  </div>
                  <div className="flex items-center gap-3 text-xs text-slate-500">
                    {conf !== undefined && (
                      <span className="flex items-center gap-1">
                        Confiance IA :
                        <span className={`font-mono font-semibold ${conf >= 0.7 ? 'text-emerald-400' : conf >= 0.4 ? 'text-yellow-400' : 'text-red-400'}`}>
                          {Math.round(conf * 100)}%
                        </span>
                      </span>
                    )}
                    {genAt && (
                      <span className="ml-auto opacity-60">
                        {new Date(genAt).toLocaleDateString('fr-FR')}
                      </span>
                    )}
                    {savingTheory && <span className="text-violet-400 animate-pulse">Sauvegarde…</span>}
                  </div>
                </div>
              )
            })()}
          </div>
        </div>
      ) : (
        <div className="flex-1 flex items-center justify-center text-slate-600 text-sm">
          Sélectionnez un template dans la liste
        </div>
      )}
      </div>{/* end flex list+detail */}
    </div>
  )
}

// ── Tab: Editeur de graphe ────────────────────────────────────────────────────

interface EditorState {
  templateId: number | null; name: string; category: string; subType: string
  description: string; instruments: string[]; aiRationale: string
  nodes: CausalNode[]; edges: CausalEdge[]
  coefficients: Record<string, Coefficient>
  inputMapping: Record<string, InputMapping>
}

const BLANK: EditorState = {
  templateId: null, name: 'Nouveau template', category: 'macro_us', subType: '',
  description: '', instruments: ['EURUSD'], aiRationale: '',
  nodes: [], edges: [], coefficients: {}, inputMapping: {},
}

const INSTRUMENTS_LIST = ['EURUSD', 'XAUUSD', 'SP500', 'BRENT', 'US2Y', 'US10Y']

function TabEditor({ initialId }: { initialId?: number | null }) {
  const [templates, setTemplates]   = useState<Template[]>([])
  const [state, setState]           = useState<EditorState>(BLANK)
  const [selNode, setSelNode]       = useState<string | null>(null)
  const [editEdgeIdx, setEditEdge]  = useState<number | null>(null)
  const [addNodeOpen, setAddNode]   = useState(false)
  const [addEdgeOpen, setAddEdge]   = useState(false)
  const [newNode, setNewNode]       = useState<Partial<CausalNode>>({ type: 'observable', x: 200, y: 200 })
  const [newEdge, setNewEdge]       = useState<Partial<CausalEdge>>({ style: 'solid', strength: 2, sign: 'positive' })
  const [editEdge, setEditEdgeVal]  = useState<Partial<CausalEdge>>({})
  const [saving, setSaving]         = useState(false)
  const [msg, setMsg]               = useState<{ ok: boolean; text: string } | null>(null)
  const [dataSources, setDS]        = useState<DataSources>({ prices: [], macro_series: [], ff_events: [] })
  const [grammarText, setGrammarText]   = useState('')
  const [grammarErrors, setGrammarErrors] = useState<string[]>([])
  const [grammarApplied, setGrammarApplied] = useState(0)  // count of applied ops
  const [aiLoading, setAiLoading]       = useState(false)
  const [aiError, setAiError]           = useState<string | null>(null)
  const [showGrammarRef, setShowGrammarRef]     = useState(false)
  const [showGrammarExport, setShowGrammarExport] = useState(false)
  const [copied, setCopied]                     = useState(false)
  const svgRef                      = useRef<SVGSVGElement>(null)

  useEffect(() => {
    api('/api/causal-lab/templates').then(setTemplates)
    api('/api/causal-lab/data-sources').then(setDS).catch(() => {})
  }, [])

  // Load template when arriving from Library "Éditer" button
  useEffect(() => {
    if (!initialId) return
    api(`/api/causal-lab/template/${initialId}`).then(applyTemplateToState).catch(() => {})
  }, [initialId])

  function applyTemplateToState(t: Template) {
    setState({
      templateId: t.id, name: t.name, category: t.category, subType: t.sub_type,
      description: t.description, instruments: t.instruments, aiRationale: t.ai_rationale,
      nodes: JSON.parse(JSON.stringify(t.graph_json.nodes || [])),
      edges: JSON.parse(JSON.stringify(t.graph_json.edges || [])),
      coefficients: JSON.parse(JSON.stringify(t.graph_json.coefficients || {})),
      inputMapping: JSON.parse(JSON.stringify(t.graph_json.input_mapping || {})),
    })
    setSelNode(null)
  }

  function loadTemplate(id: number) {
    const t = templates.find(x => x.id === id)
    if (t) applyTemplateToState(t)
  }

  // Compile natural language → grammar (replaces textarea content)
  async function compileNlToGrammar() {
    if (!grammarText.trim()) return
    setAiLoading(true); setAiError(null)
    try {
      const r = await api(`/api/causal-lab/template/${state.templateId ?? 0}/ai-to-grammar`, {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt: grammarText, current_graph: graphJson }),
      })
      setGrammarText(r.grammar)
      setGrammarErrors([])
      setGrammarApplied(0)
    } catch (e) {
      setAiError(String(e))
    } finally {
      setAiLoading(false)
    }
  }

  // Apply grammar patch directly to editor state (no AI)
  function applyGrammar() {
    const { ops, errors: parseErrors } = parseGrammar(grammarText)
    if (parseErrors.length && ops.length === 0) { setGrammarErrors(parseErrors); return }
    const { state: newState, errors: applyErrors } = applyGrammarOps(state, ops)
    setGrammarErrors([...parseErrors, ...applyErrors])
    setState(newState)
    setGrammarApplied(ops.length - applyErrors.length)
    if (ops.length > 0) setMsg({ ok: true, text: `${ops.length - applyErrors.length}/${ops.length} opérations appliquées.` })
  }

  function resetNew() { setState(BLANK); setSelNode(null) }

  const graphJson: GraphJson = {
    nodes: state.nodes, edges: state.edges,
    coefficients: state.coefficients,
    instruments: state.instruments,
    input_mapping: state.inputMapping,
  }

  // Click SVG background → reposition selected node
  function handleBgClick(e: React.MouseEvent<SVGSVGElement>) {
    if (!selNode) return
    const svg = (e.currentTarget as SVGSVGElement)
    const pt  = svg.createSVGPoint()
    pt.x = e.clientX; pt.y = e.clientY
    const sp = pt.matrixTransform(svg.getScreenCTM()!.inverse())
    setState(s => ({
      ...s, nodes: s.nodes.map(n =>
        n.id === selNode ? { ...n, x: Math.round(sp.x), y: Math.round(sp.y) } : n
      ),
    }))
  }

  function addNode() {
    if (!newNode.id || !newNode.label) return
    const n: CausalNode = {
      id: newNode.id!, label: newNode.label!, type: (newNode.type ?? 'observable') as CausalNode['type'],
      x: newNode.x ?? 200, y: newNode.y ?? 200,
      ...(newNode.formula ? { formula: newNode.formula } : {}),
      ...(newNode.unit    ? { unit: newNode.unit }       : {}),
      ...(newNode.instrument ? { instrument: newNode.instrument } : {}),
    }
    setState(s => ({ ...s, nodes: [...s.nodes, n] }))
    setNewNode({ type: 'observable', x: 200, y: 200 })
    setAddNode(false)
    autoDetectCoefs([...state.nodes, n], state.coefficients)
  }

  function updateNode(id: string, changes: Partial<CausalNode>) {
    setState(s => ({ ...s, nodes: s.nodes.map(n => n.id === id ? { ...n, ...changes } : n) }))
  }

  function removeNode(id: string) {
    setState(s => {
      const im = { ...s.inputMapping }; delete im[id]
      return {
        ...s,
        nodes: s.nodes.filter(n => n.id !== id),
        edges: s.edges.filter(e => e.from !== id && e.to !== id),
        inputMapping: im,
      }
    })
    if (selNode === id) setSelNode(null)
  }

  function addEdge() {
    if (!newEdge.from || !newEdge.to) return
    const e: CausalEdge = {
      from: newEdge.from!, to: newEdge.to!,
      style: newEdge.style ?? 'solid',
      strength: newEdge.strength ?? 2,
      sign: newEdge.sign ?? 'neutral',
      ...(newEdge.label ? { label: newEdge.label } : {}),
    }
    setState(s => ({ ...s, edges: [...s.edges, e] }))
    setNewEdge({ style: 'solid', strength: 2, sign: 'positive' })
    setAddEdge(false)
  }

  function removeEdge(i: number) {
    setState(s => ({ ...s, edges: s.edges.filter((_, idx) => idx !== i) }))
    if (editEdgeIdx === i) setEditEdge(null)
  }

  function startEditEdge(i: number) {
    const e = state.edges[i]
    setEditEdgeVal({ ...e })
    setEditEdge(i)
  }

  function applyEdgeEdit() {
    if (editEdgeIdx === null) return
    setState(s => ({
      ...s,
      edges: s.edges.map((e, i) => i === editEdgeIdx ? { ...e, ...editEdge } : e),
    }))
    setEditEdge(null)
  }

  function autoDetectCoefs(nodes: CausalNode[], existing: Record<string, Coefficient>) {
    const pattern = /\{\{(\w+)\}\}/g
    const found = new Set<string>()
    nodes.forEach(n => { let m; while ((m = pattern.exec(n.formula ?? '')) !== null) found.add(m[1]) })
    const newCoefs = { ...existing }
    found.forEach(k => { if (!(k in newCoefs)) newCoefs[k] = { value: 1.0, calibrated: null, description: '' } })
    setState(s => ({ ...s, coefficients: newCoefs }))
  }

  async function saveAsNew() {
    setSaving(true); setMsg(null)
    try {
      const body = {
        name: state.name, category: state.category, sub_type: state.subType,
        description: state.description, instruments: state.instruments,
        graph_json: graphJson,
        ai_rationale: state.aiRationale,
      }
      const r = await api('/api/causal-lab/template', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      })
      setMsg({ ok: true, text: `Template créé (id=${r.id})` })
      setState(s => ({ ...s, templateId: r.id }))
      api('/api/causal-lab/templates').then(setTemplates)
    } catch (e: unknown) {
      setMsg({ ok: false, text: String(e) })
    } finally { setSaving(false) }
  }

  async function updateExisting() {
    if (!state.templateId) return
    setSaving(true); setMsg(null)
    try {
      await api(`/api/causal-lab/template/${state.templateId}`, {
        method: 'PATCH', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: state.name, category: state.category, sub_type: state.subType,
          description: state.description, instruments: state.instruments,
          graph_json: graphJson,
          ai_rationale: state.aiRationale,
        }),
      })
      setMsg({ ok: true, text: 'Template mis à jour ✓' })
      api('/api/causal-lab/templates').then(setTemplates)
    } catch (e: unknown) {
      setMsg({ ok: false, text: String(e) })
    } finally { setSaving(false) }
  }

  const nodeIds = state.nodes.map(n => n.id)
  const selectedNodeObj = selNode ? state.nodes.find(n => n.id === selNode) : null

  return (
    <div className="flex gap-4">
      {/* Panneau gauche — formulaires */}
      <div className="w-80 flex-shrink-0 space-y-3 overflow-y-auto max-h-[calc(100vh-200px)]">

        {/* Charger existant */}
        <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40 space-y-2">
          <div className="text-xs text-slate-400 font-semibold uppercase tracking-wider">Base de départ</div>
          <div className="flex gap-2">
            <select onChange={e => loadTemplate(Number(e.target.value))} defaultValue=""
              className="flex-1 bg-dark-800 border border-slate-600 rounded px-2 py-1.5 text-xs text-slate-300">
              <option value="">Charger un template existant…</option>
              {templates.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
            </select>
            <button onClick={resetNew} title="Nouveau vide"
              className="shrink-0 px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-xs text-slate-300 whitespace-nowrap">
              ✕ Nouveau
            </button>
          </div>
        </div>

        {/* Métadonnées */}
        <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40 space-y-2">
          <div className="text-xs text-slate-400 font-semibold uppercase tracking-wider">Métadonnées</div>
          {[
            { label: 'Nom', key: 'name' as const },
            { label: 'Description', key: 'description' as const },
          ].map(({ label, key }) => (
            <div key={key}>
              <label className="text-xs text-slate-500">{label}</label>
              <input value={state[key] as string}
                onChange={e => setState(s => ({ ...s, [key]: e.target.value }))}
                className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 mt-0.5" />
            </div>
          ))}
          <div className="grid grid-cols-2 gap-2">
            <div>
              <label className="text-xs text-slate-500">Catégorie</label>
              <select value={state.category}
                onChange={e => setState(s => ({ ...s, category: e.target.value }))}
                className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-300 mt-0.5">
                {Object.entries(CAT_LABELS).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
              </select>
            </div>
            <div>
              <label className="text-xs text-slate-500">Sub-type</label>
              <input value={state.subType}
                onChange={e => setState(s => ({ ...s, subType: e.target.value }))}
                className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 mt-0.5" />
            </div>
          </div>
          <div>
            <label className="text-xs text-slate-500">Instruments</label>
            <div className="flex flex-wrap gap-1 mt-1">
              {INSTRUMENTS_LIST.map(i => (
                <button key={i} onClick={() => setState(s => ({
                  ...s, instruments: s.instruments.includes(i)
                    ? s.instruments.filter(x => x !== i)
                    : [...s.instruments, i],
                }))}
                  className={clsx('text-xs px-2 py-0.5 rounded border',
                    state.instruments.includes(i)
                      ? 'bg-emerald-900/30 text-emerald-400 border-emerald-700/40'
                      : 'bg-dark-800 text-slate-500 border-slate-700')}>
                  {i}
                </button>
              ))}
            </div>
          </div>
        </div>

        {/* Nœuds */}
        <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40">
          <div className="flex items-center justify-between mb-2">
            <span className="text-xs text-slate-400 font-semibold uppercase tracking-wider">
              Nœuds ({state.nodes.length})
            </span>
            <button onClick={() => setAddNode(v => !v)}
              className="flex items-center gap-1 text-xs px-2 py-1 bg-blue-700/40 hover:bg-blue-700 rounded text-blue-300 border border-blue-700/40">
              <Plus className="w-3 h-3" /> Ajouter
            </button>
          </div>

          {addNodeOpen && (
            <div className="mb-2 p-2 bg-dark-800 rounded border border-slate-600 space-y-1.5 text-xs">
              <div className="grid grid-cols-2 gap-1">
                <div>
                  <label className="text-slate-500">ID</label>
                  <input placeholder="ex: ois_pricing"
                    value={newNode.id ?? ''} onChange={e => setNewNode(p => ({ ...p, id: e.target.value }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
                <div>
                  <label className="text-slate-500">Type</label>
                  <select value={newNode.type} onChange={e => setNewNode(p => ({ ...p, type: e.target.value as CausalNode['type'] }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                    {NODE_TYPES.map(t => <option key={t} value={t}>{NODE_TYPE_LABELS[t]}</option>)}
                  </select>
                </div>
              </div>
              <div>
                <label className="text-slate-500">Label</label>
                <input placeholder="ex: Fed OIS / probabilités"
                  value={newNode.label ?? ''} onChange={e => setNewNode(p => ({ ...p, label: e.target.value }))}
                  className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
              </div>
              <div>
                <label className="text-slate-500">Formule (optionnel)</label>
                <input placeholder="ex: input_a * {{coef_x}}"
                  value={newNode.formula ?? ''} onChange={e => setNewNode(p => ({ ...p, formula: e.target.value }))}
                  className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5 font-mono" />
              </div>
              <div className="grid grid-cols-3 gap-1">
                <div>
                  <label className="text-slate-500">Unité</label>
                  <input value={newNode.unit ?? ''} onChange={e => setNewNode(p => ({ ...p, unit: e.target.value }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
                <div>
                  <label className="text-slate-500">X</label>
                  <input type="number" value={newNode.x ?? 200} onChange={e => setNewNode(p => ({ ...p, x: parseInt(e.target.value) }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
                <div>
                  <label className="text-slate-500">Y</label>
                  <input type="number" value={newNode.y ?? 200} onChange={e => setNewNode(p => ({ ...p, y: parseInt(e.target.value) }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
              </div>
              <button onClick={addNode}
                className="w-full py-1 bg-blue-600 hover:bg-blue-500 rounded text-xs text-white">
                + Ajouter le nœud
              </button>
            </div>
          )}

          <div className="space-y-1 max-h-52 overflow-y-auto">
            {state.nodes.map(n => (
              <div key={n.id}
                className={clsx('flex items-center gap-2 px-2 py-1.5 rounded border text-xs cursor-pointer',
                  selNode === n.id
                    ? 'border-yellow-500/60 bg-yellow-900/10 text-yellow-200'
                    : 'border-slate-700/40 bg-dark-800/50 text-slate-300 hover:border-slate-500')}
                onClick={() => setSelNode(selNode === n.id ? null : n.id)}>
                <div className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: nodeColor(n.type) }} />
                <span className="font-mono flex-1 truncate">{n.id}</span>
                <span className="text-slate-500 truncate max-w-[80px]">{n.label}</span>
                <button onClick={e => { e.stopPropagation(); removeNode(n.id) }}
                  className="text-slate-600 hover:text-red-400 flex-shrink-0">
                  <Trash2 className="w-3 h-3" />
                </button>
              </div>
            ))}
            {state.nodes.length === 0 && (
              <div className="text-slate-600 text-xs text-center py-3">Aucun nœud — cliquez "+ Ajouter"</div>
            )}
          </div>

          {selNode && (
            <div className="mt-2 p-2 bg-yellow-900/10 border border-yellow-700/30 rounded text-xs text-yellow-300">
              <Info className="w-3 h-3 inline mr-1" />
              Nœud <strong>{selNode}</strong> sélectionné — cliquez sur le graphe (droite) pour le repositionner
            </div>
          )}
        </div>

        {/* Mapping observable */}
        {selectedNodeObj?.type === 'observable' && selNode && (() => {
          const mapping: InputMapping = state.inputMapping[selNode] || { source: 'user_input' }
          const updateMapping = (changes: Partial<InputMapping>) =>
            setState(s => ({
              ...s,
              inputMapping: { ...s.inputMapping, [selNode]: { ...s.inputMapping[selNode], ...changes } },
            }))
          const clearMapping = () =>
            setState(s => { const m = { ...s.inputMapping }; delete m[selNode]; return { ...s, inputMapping: m } })
          const fieldOptions: Record<string, { key: string; label: string }[]> = {
            market_watchlist: [{ key: 'close', label: 'Prix (close)' }, { key: 'change_pct', label: 'Variation %' }],
            economic_events:  [{ key: 'actual', label: 'Valeur actuelle' }, { key: 'surprise', label: 'Surprise (actual−forecast)' }],
            ff_calendar:      [{ key: 'actual', label: 'Valeur actuelle' }, { key: 'surprise', label: 'Surprise' }],
          }
          const dsKeys = mapping.source === 'market_watchlist' ? dataSources.prices
            : mapping.source === 'economic_events' ? dataSources.macro_series
            : mapping.source === 'ff_calendar' ? dataSources.ff_events : []
          return (
            <div className="bg-dark-700 rounded-lg p-3 border border-cyan-700/40 space-y-2">
              <div className="flex items-center justify-between">
                <span className="text-xs text-cyan-400 font-semibold uppercase tracking-wider">
                  Mapping — {selNode}
                </span>
                {mapping.source && mapping.source !== 'user_input' && (
                  <button onClick={clearMapping} className="text-xs text-slate-500 hover:text-red-400">Effacer</button>
                )}
              </div>
              <div>
                <label className="text-xs text-slate-500">Source</label>
                <select value={mapping.source || 'user_input'}
                  onChange={e => updateMapping({ source: e.target.value as InputMapping['source'], key: '', field: 'close' })}
                  className="w-full mt-0.5 bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-300">
                  <option value="user_input">Manuel (saisie Analyser)</option>
                  <option value="market_watchlist">Prix marché — yfinance</option>
                  <option value="economic_events">Série macro — BD</option>
                  <option value="ff_calendar">Calendrier Forex Factory</option>
                </select>
              </div>
              {mapping.source && mapping.source !== 'user_input' && (<>
                <div>
                  <label className="text-xs text-slate-500">Clé</label>
                  <input list={`ds-${selNode}`}
                    value={mapping.key || ''}
                    onChange={e => updateMapping({ key: e.target.value })}
                    placeholder={
                      mapping.source === 'market_watchlist' ? 'ex: EURUSD' :
                      mapping.source === 'economic_events'  ? 'ex: NFP_US' : 'ex: Non-Farm Payrolls'
                    }
                    className="w-full mt-0.5 bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200" />
                  <datalist id={`ds-${selNode}`}>
                    {dsKeys.map(ds => <option key={ds.key} value={ds.key}>{ds.label}</option>)}
                  </datalist>
                </div>
                <div>
                  <label className="text-xs text-slate-500">Champ</label>
                  <select value={mapping.field || (mapping.source === 'market_watchlist' ? 'close' : 'actual')}
                    onChange={e => updateMapping({ field: e.target.value })}
                    className="w-full mt-0.5 bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-300">
                    {(fieldOptions[mapping.source] || []).map(f => (
                      <option key={f.key} value={f.key}>{f.label}</option>
                    ))}
                  </select>
                </div>
              </>)}
              {/* Normalisation /100 — visible uniquement pour sources surprise */}
              {mapping.source === 'surprise' && (
                <label className="flex items-center gap-2 cursor-pointer mt-1">
                  <input type="checkbox" checked={!!mapping.normalize}
                    onChange={e => updateMapping({ normalize: e.target.checked })}
                    className="accent-cyan-500" />
                  <span className="text-xs text-slate-400">Normaliser ÷100 <span className="text-slate-600">(coefs calibrés en fraction, pas en %)</span></span>
                </label>
              )}
            </div>
          )
        })()}

        {/* Arêtes */}
        <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40">
          <div className="flex items-center justify-between mb-2">
            <span className="text-xs text-slate-400 font-semibold uppercase tracking-wider">
              Arêtes ({state.edges.length})
            </span>
            <button onClick={() => setAddEdge(v => !v)} disabled={state.nodes.length < 2}
              className="flex items-center gap-1 text-xs px-2 py-1 bg-purple-700/40 hover:bg-purple-700 disabled:opacity-40 rounded text-purple-300 border border-purple-700/40">
              <Plus className="w-3 h-3" /> Ajouter
            </button>
          </div>

          {addEdgeOpen && (
            <div className="mb-2 p-2 bg-dark-800 rounded border border-slate-600 space-y-1.5 text-xs">
              <div className="grid grid-cols-2 gap-1">
                <div>
                  <label className="text-slate-500">De</label>
                  <select value={newEdge.from ?? ''} onChange={e => setNewEdge(p => ({ ...p, from: e.target.value }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                    <option value="">— source —</option>
                    {nodeIds.map(id => <option key={id} value={id}>{id}</option>)}
                  </select>
                </div>
                <div>
                  <label className="text-slate-500">Vers</label>
                  <select value={newEdge.to ?? ''} onChange={e => setNewEdge(p => ({ ...p, to: e.target.value }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                    <option value="">— cible —</option>
                    {nodeIds.map(id => <option key={id} value={id}>{id}</option>)}
                  </select>
                </div>
              </div>
              <div className="grid grid-cols-3 gap-1">
                <div>
                  <label className="text-slate-500">Style</label>
                  <select value={newEdge.style} onChange={e => setNewEdge(p => ({ ...p, style: e.target.value as 'solid' | 'dashed' }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                    <option value="solid">Direct ──</option>
                    <option value="dashed">Indirect ╌╌</option>
                  </select>
                </div>
                <div>
                  <label className="text-slate-500">Force</label>
                  <select value={newEdge.strength} onChange={e => setNewEdge(p => ({ ...p, strength: parseInt(e.target.value) as 1|2|3 }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                    <option value={1}>1 — fin</option>
                    <option value={2}>2 — normal</option>
                    <option value={3}>3 — fort</option>
                  </select>
                </div>
                <div>
                  <label className="text-slate-500">Signe</label>
                  <select value={newEdge.sign} onChange={e => setNewEdge(p => ({ ...p, sign: e.target.value as 'positive'|'negative'|'neutral' }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                    <option value="positive">+ positif</option>
                    <option value="negative">− négatif</option>
                    <option value="neutral">∅ neutre</option>
                  </select>
                </div>
              </div>
              <div>
                <label className="text-slate-500">Label</label>
                <input placeholder="ex: canal court terme"
                  value={newEdge.label ?? ''} onChange={e => setNewEdge(p => ({ ...p, label: e.target.value }))}
                  className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
              </div>
              <div className="grid grid-cols-4 gap-1">
                <div>
                  <label className="text-slate-500" title="Délai en minutes (intraday 5m)">Lag (min)</label>
                  <input type="number" min={0} step={5} placeholder="0"
                    value={newEdge.lag_min ?? ''}
                    onChange={e => setNewEdge(p => ({ ...p, lag_min: e.target.value === '' ? undefined : parseInt(e.target.value) }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
                <div>
                  <label className="text-amber-400" title="Délai en jours (mode journalier)">Lag (j)</label>
                  <input type="number" min={0} step={1} placeholder="0"
                    value={newEdge.lag_days ?? ''}
                    onChange={e => setNewEdge(p => ({ ...p, lag_days: e.target.value === '' ? undefined : parseInt(e.target.value) }))}
                    className="w-full bg-dark-700 border border-amber-600/40 rounded px-1.5 py-1 text-amber-200 text-xs mt-0.5" />
                </div>
                <div>
                  <label className="text-slate-500">Diff. (min)</label>
                  <input type="number" min={0} step={15} placeholder="60"
                    value={newEdge.diffusion_min ?? ''}
                    onChange={e => setNewEdge(p => ({ ...p, diffusion_min: e.target.value === '' ? undefined : parseInt(e.target.value) }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
                <div>
                  <label className="text-slate-500">Decay (j)</label>
                  <input type="number" min={0} step={1} placeholder="∞"
                    value={newEdge.decay_days ?? ''}
                    onChange={e => setNewEdge(p => ({ ...p, decay_days: e.target.value === '' ? null : parseFloat(e.target.value) }))}
                    className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                </div>
              </div>
              <button onClick={addEdge}
                className="w-full py-1 bg-purple-600 hover:bg-purple-500 rounded text-xs text-white">
                + Ajouter l'arête
              </button>
            </div>
          )}

          <div className="space-y-1 max-h-52 overflow-y-auto">
            {state.edges.map((e, i) => (
              <div key={i}>
                <div className={clsx('flex items-center gap-2 px-2 py-1.5 rounded border text-xs cursor-pointer',
                  editEdgeIdx === i
                    ? 'border-yellow-500/60 bg-yellow-900/10'
                    : 'border-slate-700/40 bg-dark-800/50 hover:border-slate-500')}
                  onClick={() => editEdgeIdx === i ? setEditEdge(null) : startEditEdge(i)}>
                  <div className="w-8 h-0 flex-shrink-0"
                    style={{ borderTop: `${edgeWidth(e.strength)}px ${e.style === 'dashed' ? 'dashed' : 'solid'} ${edgeColor(e.sign)}` }} />
                  <span className="font-mono text-slate-400 truncate flex-1">
                    {e.from} → {e.to}
                  </span>
                  {e.label && <span className="text-slate-500 truncate max-w-[60px]">{e.label}</span>}
                  <button onClick={ev => { ev.stopPropagation(); removeEdge(i) }}
                    className="text-slate-600 hover:text-red-400 flex-shrink-0">
                    <Trash2 className="w-3 h-3" />
                  </button>
                </div>
                {editEdgeIdx === i && (
                  <div className="mt-1 p-2 bg-dark-800 rounded border border-yellow-700/30 space-y-1.5 text-xs">
                    <div className="grid grid-cols-3 gap-1">
                      <div>
                        <label className="text-slate-500">Style</label>
                        <select value={editEdge.style ?? e.style}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, style: ev.target.value as 'solid'|'dashed' }))}
                          className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                          <option value="solid">Direct ──</option>
                          <option value="dashed">Indirect ╌╌</option>
                        </select>
                      </div>
                      <div>
                        <label className="text-slate-500">Force</label>
                        <select value={editEdge.strength ?? e.strength}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, strength: parseInt(ev.target.value) as 1|2|3 }))}
                          className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                          <option value={1}>1 — fin</option>
                          <option value={2}>2 — normal</option>
                          <option value={3}>3 — fort</option>
                        </select>
                      </div>
                      <div>
                        <label className="text-slate-500">Signe</label>
                        <select value={editEdge.sign ?? e.sign}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, sign: ev.target.value as 'positive'|'negative'|'neutral' }))}
                          className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-300 text-xs mt-0.5">
                          <option value="positive">+ positif</option>
                          <option value="negative">− négatif</option>
                          <option value="neutral">∅ neutre</option>
                        </select>
                      </div>
                    </div>
                    <div>
                      <label className="text-slate-500">Label</label>
                      <input value={editEdge.label ?? e.label ?? ''}
                        onChange={ev => setEditEdgeVal(p => ({ ...p, label: ev.target.value }))}
                        className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                    </div>
                    <div className="grid grid-cols-4 gap-1">
                      <div>
                        <label className="text-slate-500" title="Délai en minutes (intraday 5m)">Lag (min)</label>
                        <input type="number" min={0} step={5}
                          value={editEdge.lag_min ?? e.lag_min ?? ''}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, lag_min: ev.target.value === '' ? undefined : parseInt(ev.target.value) }))}
                          className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                      </div>
                      <div>
                        <label className="text-amber-400" title="Délai en jours (mode journalier)">Lag (j)</label>
                        <input type="number" min={0} step={1}
                          value={editEdge.lag_days ?? e.lag_days ?? ''}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, lag_days: ev.target.value === '' ? undefined : parseInt(ev.target.value) }))}
                          className="w-full bg-dark-700 border border-amber-600/40 rounded px-1.5 py-1 text-amber-200 text-xs mt-0.5" />
                      </div>
                      <div>
                        <label className="text-slate-500">Diff. (min)</label>
                        <input type="number" min={0} step={15}
                          value={editEdge.diffusion_min ?? e.diffusion_min ?? ''}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, diffusion_min: ev.target.value === '' ? undefined : parseInt(ev.target.value) }))}
                          className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                      </div>
                      <div>
                        <label className="text-slate-500">Decay (j)</label>
                        <input type="number" min={0} step={1}
                          value={editEdge.decay_days ?? e.decay_days ?? ''}
                          onChange={ev => setEditEdgeVal(p => ({ ...p, decay_days: ev.target.value === '' ? null : parseFloat(ev.target.value) }))}
                          className="w-full bg-dark-700 border border-slate-600 rounded px-1.5 py-1 text-slate-200 text-xs mt-0.5" />
                      </div>
                    </div>
                    <button onClick={applyEdgeEdit}
                      className="w-full py-1 bg-yellow-700 hover:bg-yellow-600 rounded text-xs text-white">
                      Appliquer
                    </button>
                  </div>
                )}
              </div>
            ))}
            {state.edges.length === 0 && (
              <div className="text-slate-600 text-xs text-center py-3">Aucune arête</div>
            )}
          </div>
        </div>

        {/* Coefficients */}
        <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40">
          <div className="flex items-center justify-between mb-2">
            <span className="text-xs text-slate-400 font-semibold uppercase tracking-wider">
              Coefficients ({Object.keys(state.coefficients).length})
            </span>
            <button onClick={() => autoDetectCoefs(state.nodes, state.coefficients)}
              className="text-xs px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-slate-300">
              Détecter auto
            </button>
          </div>
          <div className="space-y-1.5 max-h-40 overflow-y-auto">
            {Object.entries(state.coefficients).map(([k, c]) => (
              <div key={k} className="flex gap-2 items-center">
                <span className="text-xs text-slate-400 font-mono flex-1 truncate">{k}</span>
                <input type="number" step="0.001" value={c.value}
                  onChange={e => setState(s => ({
                    ...s, coefficients: { ...s.coefficients, [k]: { ...c, value: parseFloat(e.target.value) || 0 } }
                  }))}
                  className="w-20 bg-dark-800 border border-slate-600 rounded px-1.5 py-1 text-xs text-slate-200 text-right" />
                <button onClick={() => setState(s => {
                  const cc = { ...s.coefficients }; delete cc[k]; return { ...s, coefficients: cc }
                })} className="text-slate-600 hover:text-red-400">
                  <Trash2 className="w-3 h-3" />
                </button>
              </div>
            ))}
          </div>
        </div>

        {/* Sauvegarde */}
        <div className="space-y-2">
          {state.templateId && (
            <button onClick={updateExisting} disabled={saving || !state.name}
              className="w-full flex items-center justify-center gap-2 py-2 bg-blue-700 hover:bg-blue-600 disabled:opacity-40 rounded text-xs font-medium text-white">
              <Save className="w-3.5 h-3.5" />
              {saving ? 'Sauvegarde…' : 'Mettre à jour le template'}
            </button>
          )}
          <button onClick={saveAsNew} disabled={saving || !state.name}
            className="w-full flex items-center justify-center gap-2 py-2 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40 rounded text-xs font-medium text-white">
            <Save className="w-3.5 h-3.5" />
            {saving ? 'Sauvegarde…' : 'Créer ce template (copie)'}
          </button>
          {msg && (
            <div className={clsx('p-2 rounded text-xs', msg.ok ? 'bg-emerald-900/20 text-emerald-300' : 'bg-red-900/20 text-red-300')}>
              {msg.text}
            </div>
          )}
        </div>
      </div>

      {/* Panneau droit — SVG live */}
      <div className="flex-1 space-y-3">
        <div className="flex items-center gap-3 mb-1">
          <span className="text-xs text-slate-500">
            {selNode
              ? <span className="text-yellow-400">Cliquez sur le graphe pour repositionner <strong>{selNode}</strong></span>
              : 'Cliquez sur un nœud dans le graphe pour le sélectionner et le repositionner'
            }
          </span>
        </div>
        <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40">
          <GraphSVG
            graph={graphJson}
            editorMode={true}
            selectedNodeId={selNode}
            onNodeClick={(id) => setSelNode(selNode === id ? null : id)}
            onBgClick={handleBgClick}
          />
        </div>
        <GraphLegend />

        {/* Grammar patch panel */}
        <div className="bg-dark-700 rounded-lg p-4 border border-violet-900/40 space-y-3">
          <div className="flex items-center justify-between">
            <h4 className="text-slate-300 text-xs font-semibold uppercase tracking-wider flex items-center gap-2">
              <Wand2 className="w-3.5 h-3.5 text-violet-400" /> Patch Grammaire
            </h4>
            <button onClick={() => setShowGrammarRef(v => !v)}
              className="text-xs text-slate-600 hover:text-slate-400 flex items-center gap-1">
              <Info className="w-3 h-3" /> {showGrammarRef ? 'Masquer' : 'Référence'}
            </button>
          </div>

          {showGrammarRef && (
            <pre className="text-[10px] font-mono text-slate-500 bg-dark-900/60 rounded p-2 overflow-x-auto leading-relaxed whitespace-pre">
              {GRAMMAR_REFERENCE}
            </pre>
          )}

          {/* Export — grammaire courante du graphe */}
          <div className="rounded border border-slate-700/50 overflow-hidden">
            <button
              onClick={() => setShowGrammarExport(v => !v)}
              className="w-full flex items-center justify-between px-3 py-2 bg-dark-800/60 hover:bg-dark-800 text-xs text-slate-400 transition-colors">
              <span className="flex items-center gap-1.5">
                <Copy className="w-3 h-3" />
                Grammaire courante du graphe
                <span className="text-slate-600">
                  ({state.nodes.length} nœuds · {state.edges.length} arêtes)
                </span>
              </span>
              <span className="text-slate-600">{showGrammarExport ? '▲' : '▼'}</span>
            </button>
            {showGrammarExport && (() => {
              const grammar = graphToGrammar(state)
              return (
                <div className="relative">
                  <pre className="text-[10px] font-mono text-slate-400 bg-dark-900/80 p-3 overflow-x-auto max-h-52 leading-relaxed whitespace-pre">
                    {grammar}
                  </pre>
                  <button
                    onClick={() => {
                      navigator.clipboard.writeText(grammar)
                      setCopied(true)
                      setTimeout(() => setCopied(false), 2000)
                    }}
                    className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-[10px] text-slate-300 border border-slate-600 transition-all">
                    {copied ? '✓ Copié' : <><Copy className="w-2.5 h-2.5" /> Copier</>}
                  </button>
                </div>
              )
            })()}
          </div>

          <textarea
            value={grammarText}
            onChange={e => { setGrammarText(e.target.value); setGrammarErrors([]); setGrammarApplied(0) }}
            disabled={aiLoading}
            placeholder={
              "Écrivez la grammaire directement, ou décrivez en français puis cliquez « Compiler »\n\n" +
              "+node id=risk_aversion label=\"Risk Aversion\" type=latent x=450 y=300\n" +
              "+edge from=us_10y to=risk_aversion sign=- lag=2 decay=10\n" +
              "~edge from=ois_pricing to=us_2y lag=3\n" +
              "-edge from=rate_diff to=eurusd_pip"
            }
            rows={7}
            className="w-full bg-dark-900/70 border border-slate-700 rounded px-3 py-2 text-xs font-mono text-slate-200 placeholder:text-slate-600 resize-none focus:border-violet-600 focus:outline-none"
          />

          <div className="flex items-center gap-2 flex-wrap">
            {/* Compile NL → grammar */}
            <button
              onClick={compileNlToGrammar}
              disabled={aiLoading || !grammarText.trim() || !state.templateId}
              title={!state.templateId ? "Sauvegardez d'abord le template" : undefined}
              className="flex items-center gap-1.5 px-3 py-1.5 bg-slate-700 hover:bg-slate-600 disabled:opacity-40 rounded text-xs text-slate-300 border border-slate-600 transition-all">
              {aiLoading
                ? <><RefreshCw className="w-3 h-3 animate-spin" /> Compilation…</>
                : <><Brain className="w-3 h-3 text-violet-400" /> NL → Grammaire</>}
            </button>

            {/* Apply grammar locally */}
            <button
              onClick={applyGrammar}
              disabled={!grammarText.trim() || aiLoading}
              className="flex items-center gap-1.5 px-3 py-1.5 bg-violet-700 hover:bg-violet-600 disabled:opacity-40 rounded text-xs font-medium text-white transition-all">
              <Zap className="w-3 h-3" /> Appliquer
            </button>

            {/* Live parse count */}
            {grammarText.trim() && (() => {
              const { ops } = parseGrammar(grammarText)
              return ops.length > 0 ? (
                <span className="text-xs text-slate-500">{ops.length} opération{ops.length > 1 ? 's' : ''} détectée{ops.length > 1 ? 's' : ''}</span>
              ) : null
            })()}

            {grammarApplied > 0 && (
              <span className="text-xs text-emerald-500">✓ {grammarApplied} appliquée{grammarApplied > 1 ? 's' : ''}</span>
            )}
          </div>

          {/* Errors */}
          {grammarErrors.length > 0 && (
            <div className="p-2 bg-red-900/20 border border-red-700/40 rounded text-xs text-red-300 space-y-0.5">
              {grammarErrors.map((e, i) => <div key={i}>⚠ {e}</div>)}
            </div>
          )}
          {aiError && (
            <div className="p-2 bg-red-900/20 border border-red-700/40 rounded text-xs text-red-300">
              {aiError}
            </div>
          )}

          {/* Save shortcuts — visible right after applying grammar changes */}
          <div className="pt-2 border-t border-slate-700/40 space-y-2">
            <div className="text-xs text-slate-500 uppercase tracking-wider">Sauvegarder</div>
            <div className="flex gap-2">
              {state.templateId && (
                <button onClick={updateExisting} disabled={saving || !state.name}
                  className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-blue-700 hover:bg-blue-600 disabled:opacity-40 rounded text-xs font-medium text-white transition-all">
                  <Save className="w-3.5 h-3.5" />
                  {saving ? 'Sauvegarde…' : 'Mettre à jour'}
                </button>
              )}
              <button onClick={saveAsNew} disabled={saving || !state.name}
                className="flex-1 flex items-center justify-center gap-1.5 py-2 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-40 rounded text-xs font-medium text-white transition-all">
                <Save className="w-3.5 h-3.5" />
                {saving ? 'Sauvegarde…' : 'Créer copie'}
              </button>
            </div>
            {msg && (
              <div className={clsx('p-2 rounded text-xs', msg.ok ? 'bg-emerald-900/20 text-emerald-300' : 'bg-red-900/20 text-red-300')}>
                {msg.text}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}

// ── Tab: Analyser ─────────────────────────────────────────────────────────────

function TabAnalyze({ initialEventId }: { initialEventId?: number | null }) {
  const [events, setEvents]       = useState<MarketEvent[]>([])
  const [templates, setTemplates] = useState<Template[]>([])
  const [selEvent, setSelEvent]   = useState<MarketEvent | null>(null)
  const [selTmpl, setSelTmpl]     = useState<number>(0)
  const [instrument, setInstr]    = useState('EURUSD')
  const [manualInputs, setManual] = useState<Record<string, number>>({})
  const [result, setResult]       = useState<AnalysisResult | null>(null)
  const [rec, setRec]             = useState<Recommendation | null>(null)
  const [loadingRec, setLoadRec]  = useState(false)
  const [loadingAn, setLoadAn]    = useState(false)
  const [q, setQ]                 = useState('')
  const [catF, setCatF]           = useState('')
  const [error, setError]         = useState('')

  useEffect(() => {
    Promise.all([
      api(`/api/causal-lab/market-events?limit=200${catF ? `&category=${catF}` : ''}${q ? `&q=${encodeURIComponent(q)}` : ''}`),
      api('/api/causal-lab/templates'),
    ]).then(([evs, tmps]) => { setEvents(evs); setTemplates(tmps) })
  }, [catF, q])

  useEffect(() => {
    if (!initialEventId || !events.length || selEvent) return
    const found = events.find(e => e.id === initialEventId)
    if (found) setSelEvent(found)
  }, [events, initialEventId])

  const currentTemplate = templates.find(t => t.id === selTmpl)
  const manualNodes = currentTemplate?.graph_json?.input_mapping
    ? Object.entries(currentTemplate.graph_json.input_mapping).filter(([, c]) =>
        !c.source || c.source === 'user_input')
    : []
  const autoNodes = currentTemplate?.graph_json?.input_mapping
    ? Object.entries(currentTemplate.graph_json.input_mapping).filter(([, c]) =>
        c.source && c.source !== 'user_input')
    : []

  async function recommend() {
    if (!selEvent) return
    setLoadRec(true); setRec(null); setError('')
    try {
      const r = await api('/api/causal-lab/recommend', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ market_event_id: selEvent.id }),
      })
      setRec(r.recommendation)
      if (r.recommendation?.template_id) setSelTmpl(r.recommendation.template_id)
    } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)) }
    finally { setLoadRec(false) }
  }

  async function analyze() {
    if (!selEvent || !selTmpl) return
    setLoadAn(true); setResult(null); setError('')
    try {
      const r = await api('/api/causal-lab/analyze', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ market_event_id: selEvent.id, template_id: selTmpl, instrument, inputs: manualInputs, coef_overrides: {} }),
      })
      setResult(r)
    } catch (e: unknown) { setError(e instanceof Error ? e.message : String(e)) }
    finally { setLoadAn(false) }
  }

  return (
    <div className="flex gap-4">
      <div className="w-72 flex-shrink-0 space-y-3">
        <div className="flex gap-2">
          <div className="relative flex-1">
            <Search className="w-3.5 h-3.5 absolute left-2 top-2 text-slate-500" />
            <input value={q} onChange={e => setQ(e.target.value)} placeholder="Rechercher…"
              className="w-full bg-dark-700 border border-slate-700 rounded pl-7 pr-2 py-1.5 text-xs text-slate-300" />
          </div>
          <select value={catF} onChange={e => setCatF(e.target.value)}
            className="bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-xs text-slate-300">
            <option value="">Tous</option>
            <option value="event_calendar">Calendrier éco</option>
            <option value="fundamental">Fondamental</option>
            <option value="geopolitical">Géopolitique</option>
            <option value="report">Report</option>
            <option value="sentiment">Sentiment</option>
            <option value="technical">Technique</option>
          </select>
        </div>

        <div className="space-y-1 overflow-y-auto max-h-60 border border-slate-700/30 rounded-lg p-1">
          {events.length === 0
            ? <div className="text-slate-600 text-xs p-3 text-center">Aucun événement</div>
            : events.map(ev => (
              <button key={ev.id} onClick={() => { setSelEvent(ev); setResult(null); setRec(null) }}
                className={clsx('w-full text-left px-3 py-2 rounded border text-xs transition-all',
                  selEvent?.id === ev.id
                    ? 'bg-blue-900/30 border-blue-500/50 text-blue-200'
                    : 'bg-dark-700 border-slate-700/50 text-slate-300 hover:border-slate-500')}>
                <div className="font-medium truncate">{ev.name}</div>
                <div className="text-slate-500 flex justify-between">
                  <span>{ev.start_date?.slice(0, 10)}</span>
                  {ev.activation_score != null && (
                    <span className={clsx('font-semibold',
                      ev.activation_score >= 0.7 ? 'text-emerald-400' :
                      ev.activation_score >= 0.4 ? 'text-yellow-400' : 'text-red-400')}>
                      {(ev.activation_score * 100).toFixed(0)}%
                    </span>
                  )}
                </div>
              </button>
          ))}
        </div>

        <select value={selTmpl} onChange={e => setSelTmpl(Number(e.target.value))}
          className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-2 text-xs text-slate-300">
          <option value={0}>— Choisir un template —</option>
          {templates.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}
        </select>

        <select value={instrument} onChange={e => setInstr(e.target.value)}
          className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-2 text-xs text-slate-300">
          {['EURUSD', 'XAUUSD', 'SP500', 'BRENT'].map(i => <option key={i} value={i}>{i}</option>)}
        </select>

        {autoNodes.length > 0 && (
          <div className="bg-dark-800/60 rounded p-3 space-y-1.5 border border-cyan-700/20">
            <div className="text-xs text-cyan-500 font-semibold flex items-center gap-1.5">
              <Zap className="w-3 h-3" /> Auto-récupéré depuis BD
            </div>
            {autoNodes.map(([id, cfg]) => (
              <div key={id} className="flex items-center gap-2 text-xs">
                <span className="text-slate-500 flex-1 capitalize">{id.replace(/_/g, ' ')}</span>
                <span className="text-cyan-400/70 text-xs font-mono">{cfg.key ?? cfg.source}</span>
              </div>
            ))}
          </div>
        )}
        {manualNodes.length > 0 && (
          <div className="bg-dark-800 rounded p-3 space-y-2 border border-slate-700/30">
            <div className="text-xs text-slate-400 font-semibold">Inputs manuels</div>
            {manualNodes.map(([id, cfg]) => {
              const range = cfg.range as [number, number] | undefined
              return (
                <div key={id} className="flex items-center gap-2">
                  <span className="text-xs text-slate-400 flex-1 capitalize">{id.replace(/_/g, ' ')}</span>
                  <input type="range" min={range?.[0] ?? -3} max={range?.[1] ?? 3} step={0.5}
                    value={manualInputs[id] ?? 0}
                    onChange={e => setManual(p => ({ ...p, [id]: parseFloat(e.target.value) }))}
                    className="w-24" />
                  <span className="text-xs text-slate-300 w-8 text-right">
                    {(manualInputs[id] ?? 0) > 0 ? '+' : ''}{manualInputs[id] ?? 0}
                  </span>
                </div>
              )
            })}
          </div>
        )}

        <button onClick={recommend} disabled={!selEvent || loadingRec}
          className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-purple-700/50 hover:bg-purple-700 disabled:opacity-40 rounded text-xs font-medium text-purple-200 border border-purple-700/40">
          <Brain className="w-3.5 h-3.5" />
          {loadingRec ? 'GPT-4o analyse…' : 'Recommandation GPT-4o'}
        </button>
        <button onClick={analyze} disabled={!selEvent || !selTmpl || loadingAn}
          className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 rounded text-xs font-medium text-white">
          <Zap className="w-3.5 h-3.5" />
          {loadingAn ? 'Analyse…' : 'Lancer l\'analyse'}
        </button>
        {error && <div className="p-3 bg-red-900/20 border border-red-700/40 rounded text-xs text-red-300">{error}</div>}
      </div>

      <div className="flex-1 space-y-4 min-w-0">
        {rec && (
          <div className="bg-purple-900/10 border border-purple-700/30 rounded-lg p-4">
            <div className="flex items-center gap-2 mb-2">
              <Brain className="w-4 h-4 text-purple-400" />
              <span className="text-sm font-semibold text-purple-200">Recommandation GPT-4o</span>
              {rec.confidence != null && (
                <span className="ml-auto text-xs text-purple-400 font-bold">
                  {(rec.confidence * 100).toFixed(0)}% confiance
                </span>
              )}
            </div>
            {rec.error
              ? <div className="text-red-400 text-xs">{rec.error}</div>
              : <>
                  <div className="text-sm text-white font-medium mb-1">Template : {rec.template_name}</div>
                  <p className="text-xs text-purple-200/80">{rec.rationale}</p>
                  {rec.notes && <p className="text-xs text-slate-400 mt-1">{rec.notes}</p>}
                  {Object.keys(rec.coefficient_suggestions || {}).length > 0 && (
                    <div className="mt-2 flex flex-wrap gap-2">
                      {Object.entries(rec.coefficient_suggestions).map(([k, v]) => (
                        <span key={k} className="text-xs px-2 py-0.5 bg-purple-900/30 text-purple-300 rounded border border-purple-700/30">
                          {k} = {v}
                        </span>
                      ))}
                    </div>
                  )}
                </>
            }
          </div>
        )}

        {result && (
          <>
            {Object.keys(result.inputs).length > 0 && (
              <div className="bg-dark-700 rounded-lg p-3 border border-slate-700/40">
                <div className="text-xs text-slate-500 font-semibold uppercase tracking-wider mb-2">Inputs utilisés</div>
                <div className="flex flex-wrap gap-2">
                  {Object.entries(result.inputs).map(([k, v]) => {
                    const isAuto = autoNodes.some(([id]) => id === k)
                    return (
                      <span key={k} className={clsx('text-xs px-2 py-0.5 rounded border font-mono',
                        isAuto
                          ? 'bg-cyan-900/20 text-cyan-300 border-cyan-700/30'
                          : 'bg-slate-800 text-slate-300 border-slate-700/40')}>
                        {k}: {typeof v === 'number' ? (v > 0 ? '+' : '') + v.toFixed(3) : String(v)}
                        {isAuto && <span className="ml-1 text-cyan-600 text-xs">auto</span>}
                      </span>
                    )
                  })}
                </div>
              </div>
            )}
            <div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
              <div className="flex items-center justify-between mb-3">
                <h3 className="text-white font-semibold text-sm">{result.event.name}</h3>
                <span className={clsx('text-sm font-bold px-3 py-1 rounded',
                  result.activation.score == null          ? 'text-slate-400 bg-slate-800' :
                  result.activation.score >= 0.7           ? 'text-emerald-400 bg-emerald-900/30' :
                  result.activation.score >= 0.4           ? 'text-yellow-400 bg-yellow-900/30' :
                                                             'text-red-400 bg-red-900/30')}>
                  {result.activation.score != null ? `${(result.activation.score * 100).toFixed(0)}% activation` : 'N/A'}
                </span>
              </div>
              <div className="grid grid-cols-4 gap-3 mb-3">
                {[
                  { label: 'Pré-drift', val: fmtPips(result.drift?.pre_pips) },
                  { label: 'Réel post', val: fmtPips(result.drift?.post_pips) },
                  { label: 'Mode prix', val: result.prices_mode },
                  { label: 'Fuite info', val: result.drift?.leak ?? '—' },
                  { label: 'Lag mesuré', val: result.effective_lag_min != null ? `${result.effective_lag_min}min` : '0min' },
                ].map(m => (
                  <div key={m.label} className="bg-dark-800 rounded p-3 text-center">
                    <div className="text-xs text-slate-500">{m.label}</div>
                    <div className="text-sm font-bold text-slate-200 mt-0.5">{m.val}</div>
                  </div>
                ))}
              </div>
              <div className="flex flex-wrap gap-2">
                {Object.entries(result.activation.nodes).map(([id, node]) => {
                  const Icon = node.status === 'correct' ? CheckCircle : node.status === 'wrong' ? XCircle : Minus
                  const col =
                    node.status === 'correct' ? 'text-emerald-400 border-emerald-700/40 bg-emerald-900/10' :
                    node.status === 'wrong'   ? 'text-red-400 border-red-700/40 bg-red-900/10' :
                    'text-slate-500 border-slate-700/40 bg-slate-800/30'
                  return (
                    <div key={id} className={clsx('flex items-center gap-1.5 px-3 py-1.5 rounded border text-xs', col)}>
                      <Icon className="w-3.5 h-3.5" />
                      <span>{node.label}</span>
                      {node.pred != null && (
                        <span className="opacity-70">
                          P:{node.pred > 0 ? '+' : ''}{node.pred.toFixed(1)} / A:{node.act != null ? (node.act > 0 ? '+' : '') + node.act.toFixed(1) : '—'}
                        </span>
                      )}
                    </div>
                  )
                })}
              </div>
            </div>
            <div className="bg-dark-700 rounded-lg p-4 border border-slate-700/40">
              <h4 className="text-slate-300 text-xs font-semibold mb-3 uppercase tracking-wider">Graphe — valeurs calculées</h4>
              {currentTemplate && (
                <GraphSVG graph={currentTemplate.graph_json}
                  nodeValues={result.node_values}
                  activationNodes={result.activation.nodes} />
              )}
            </div>
          </>
        )}

        {(loadingAn || loadingRec) && (
          <div className="flex items-center justify-center h-48 text-slate-500 gap-3">
            <RefreshCw className="w-5 h-5 animate-spin" />
            <span className="text-sm">{loadingAn ? 'Récupération des prix (yfinance)…' : 'GPT-4o en cours…'}</span>
          </div>
        )}
        {!result && !loadingAn && !loadingRec && !rec && (
          <div className="flex flex-col items-center justify-center h-64 text-slate-600 space-y-3">
            <FlaskConical className="w-12 h-12 opacity-30" />
            <div className="text-sm text-center">Sélectionnez un événement, choisissez un template et lancez l'analyse</div>
          </div>
        )}
      </div>
    </div>
  )
}

// ── Tab: Calibration ──────────────────────────────────────────────────────────

function TabCalibration() {
  const [data, setData]       = useState<CalibRow[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    api('/api/causal-lab/calibration').then(setData).finally(() => setLoading(false))
  }, [])

  if (loading) return <div className="text-slate-500 text-sm p-6">Chargement…</div>

  return (
    <div className="space-y-3">
      <div className="text-xs text-slate-500 flex items-center gap-2">
        <AlertCircle className="w-3.5 h-3.5" />
        Statistiques agrégées par template. Le ratio pred/réel converge vers 1.0 quand les coefficients sont bien calibrés.
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-xs">
          <thead>
            <tr className="border-b border-slate-700/50">
              {['Template', 'Catégorie', 'Nb analyses', 'Activation moy.', 'Pred. moy.', 'Réel moy.', 'Ratio'].map(h => (
                <th key={h} className="px-3 py-2 text-left text-slate-500 uppercase tracking-wider font-medium">{h}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {data.map(row => {
              const calib   = row.calibration || {}
              const instKey = Object.keys(calib).find(k => !['n_events', 'avg_activation', 'last_analyzed'].includes(k))
              const instData = instKey ? calib[instKey] : null
              return (
                <tr key={row.id} className="border-b border-slate-800/50 hover:bg-dark-700/30 transition-colors">
                  <td className="px-3 py-2.5 text-slate-200 font-medium">{row.name}</td>
                  <td className="px-3 py-2.5 text-slate-400">{CAT_LABELS[row.category] ?? row.category}</td>
                  <td className="px-3 py-2.5 text-slate-300">{row.n_analyses}</td>
                  <td className="px-3 py-2.5">
                    {row.avg_activation != null
                      ? <span className={clsx('font-bold',
                          row.avg_activation >= 0.7 ? 'text-emerald-400' :
                          row.avg_activation >= 0.4 ? 'text-yellow-400' : 'text-red-400')}>
                          {(row.avg_activation * 100).toFixed(0)}%
                        </span>
                      : <span className="text-slate-600">—</span>}
                  </td>
                  <td className="px-3 py-2.5 text-slate-300">{instData?.avg_pred?.toFixed(1) ?? '—'}</td>
                  <td className="px-3 py-2.5 text-slate-300">{instData?.avg_actual?.toFixed(1) ?? '—'}</td>
                  <td className="px-3 py-2.5">
                    {instData?.coef_ratio != null
                      ? <span className={clsx('font-bold',
                          Math.abs(instData.coef_ratio - 1) < 0.2 ? 'text-emerald-400' :
                          Math.abs(instData.coef_ratio - 1) < 0.5 ? 'text-yellow-400' : 'text-red-400')}>
                          {instData.coef_ratio.toFixed(2)}x
                        </span>
                      : <span className="text-slate-600">—</span>}
                  </td>
                </tr>
              )
            })}
          </tbody>
        </table>
        {data.length === 0 && (
          <div className="text-center py-12 text-slate-600 text-sm">
            Aucune analyse effectuée. Lancez des analyses dans l'onglet "Analyser".
          </div>
        )}
      </div>
    </div>
  )
}

// ── Page principale ───────────────────────────────────────────────────────────

const TABS = [
  { id: 'library', label: 'Bibliothèque', icon: Library      },
  { id: 'editor',  label: 'Editeur',       icon: Edit3        },
  { id: 'analyze', label: 'Analyser',      icon: FlaskConical },
  { id: 'calib',   label: 'Calibration',   icon: BarChart3    },
]

export default function CausalLab() {
  const [searchParams]    = useSearchParams()
  const initialTemplateId = searchParams.get('template') ? parseInt(searchParams.get('template')!) : null
  const initialEventId    = searchParams.get('event')    ? parseInt(searchParams.get('event')!)    : null
  const [tab, setTab]     = useState(initialEventId ? 'analyze' : 'library')
  const [editorInitId, setEditorInitId] = useState<number | null>(null)

  function openInEditor(id: number) {
    setEditorInitId(id)
    setTab('editor')
  }

  return (
    <div className="p-6 min-h-screen">
      <div className="flex items-center gap-3 mb-6">
        <FlaskConical className="w-6 h-6 text-blue-400" />
        <div>
          <h1 className="text-xl font-bold text-white">Lab Causal</h1>
          <p className="text-slate-400 text-xs mt-0.5">
            Bibliothèque · Editeur visuel · Analyse empirique · Calibration IA
          </p>
        </div>
      </div>

      <div className="flex gap-1 mb-6 bg-dark-800 p-1 rounded-lg w-fit border border-slate-700/30">
        {TABS.map(({ id, label, icon: Icon }) => (
          <button key={id} onClick={() => setTab(id)}
            className={clsx('flex items-center gap-2 px-4 py-2 rounded text-sm font-medium transition-all',
              tab === id ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-slate-200')}>
            <Icon className="w-4 h-4" />
            {label}
          </button>
        ))}
      </div>

      <div className="bg-dark-800 rounded-xl p-5 border border-slate-700/30">
        {tab === 'library' && <TabLibrary initialTemplateId={initialTemplateId} onOpenEditor={openInEditor} />}
        {tab === 'editor'  && <TabEditor initialId={editorInitId} />}
        {tab === 'analyze' && <TabAnalyze initialEventId={initialEventId} />}
        {tab === 'calib'   && <TabCalibration />}
      </div>
    </div>
  )
}
