// Tempo — Backtester (bar-replay) : PAGE
// ─────────────────────────────────────────────────────────────────────────
// Expose BacktestPage({ state }) (desktop) et BacktestMobile({ state }) (mobile).
// Style FX Replay / TradingView Replay : sélecteur d'actif groupé, boutons TF,
// barre de replay (début / play-pause / vitesses / saut à date / +1 bougie /
// scrubber), chart central, panneau d'ordre v2 (marché/limite/stop, sizing par
// risque, réglages spread/commission), P&L, positions ouvertes (BE / partiel /
// édition SL-TP), ordres en attente, stats de session + courbe d'équité,
// sessions sauvegardées/reprenables, et un JOURNAL DE BACKTEST 100% séparé
// du journal réel (avec export explicite, à la demande, vers ce dernier).
//
// Dépendances (chargées avant ce fichier) :
//   backtest-data.jsx   → BT_ASSETS, BT_CATEGORIES, BT_TIMEFRAMES, loadBtBars, btAsset…
//   backtest-engine.jsx → useBacktest, btSessionStats
//   backtest-amchart.jsx→ BacktestAMChart
//   core.jsx            → Ico, PageHeader, useTheme
//   format-prefs.jsx    → fmtMoneyCur, currencySymbol
//   trades-api.jsx      → bulkInsertTrades (export vers le journal réel)
// ─────────────────────────────────────────────────────────────────────────

// ── Helpers de formatage locaux ─────────────────────────────────────────────
function btCur() { return (typeof window !== 'undefined' && window.__appCurrency) || 'USD'; }
function btMoney(v, signed = false) {
  const n = Number(v) || 0;
  if (typeof fmtMoneyCur === 'function') return fmtMoneyCur(n, btCur(), signed);
  return (signed && n > 0 ? '+' : '') + n.toLocaleString('fr-FR');
}
function btPrice(v, decimals) {
  const n = Number(v);
  if (!Number.isFinite(n)) return '—';
  return n.toLocaleString('fr-FR', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
}
function btR(v) {
  if (v == null || !Number.isFinite(Number(v))) return '—';
  const n = Number(v);
  const s = Math.abs(n).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 2 });
  return (n > 0 ? '+' : n < 0 ? '−' : '') + s + ' R';
}
function btToneColor(v) {
  const n = Number(v) || 0;
  return n > 0 ? 'var(--green)' : n < 0 ? 'var(--red)' : 'var(--fg-2)';
}
function btFmtTime(unix) {
  if (!unix) return '—';
  try {
    return new Date(unix * 1000).toLocaleString('fr-FR', {
      day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit',
    });
  } catch (e) { return '—'; }
}
// Arrondi à 2 décimales (quantités, moitiés de position…).
function btRound2(v) { return Math.round(Number(v) * 100) / 100; }
// Nom de session par défaut : « EUR/USD 1h — 04 juil. 2026 ».
function btDefaultSessionName(assetId, tf) {
  const a = btAsset(assetId);
  const d = new Date().toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' });
  return ((a && a.label) || assetId) + ' ' + tf + ' — ' + d;
}

// ── Fermeture d'un popover au clic extérieur (réutilisé partout) ────────────
function useBtClickOutside(ref, active, onClose) {
  React.useEffect(() => {
    if (!active) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [active]);
}

// ── Segmented control générique (type d'ordre, mode de sizing…) ─────────────
function BtSegmented({ options, value, onChange }) {
  return (
    <div style={{ display: 'flex', gap: 3, background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 9, padding: 3 }}>
      {options.map(o => {
        const on = o.id === value;
        return (
          <button key={o.id} className="tap" onClick={() => onChange(o.id)} style={{
            flex: 1, padding: '6px 8px', borderRadius: 6, border: 'none', cursor: 'pointer',
            background: on ? 'var(--bg-card)' : 'transparent',
            color: on ? 'var(--fg)' : 'var(--fg-3)', fontSize: 11.5, fontWeight: on ? 700 : 500,
            boxShadow: on ? '0 1px 2px rgba(0,0,0,0.08)' : 'none', whiteSpace: 'nowrap',
          }}>{o.label}</button>
        );
      })}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  RÉGLAGES D'EXÉCUTION (spread / commission) — persistés par actif
// ═════════════════════════════════════════════════════════════════════════
// Défauts : spread = champ `spread` de l'actif (catalogue) s'il existe, sinon 0 ;
// commission = 0. Persistés dans localStorage 'tempo:bt:execCfg:{assetId}'.
function btLoadExecCfg(assetId) {
  const a = btAsset(assetId);
  const def = { spread: (a && Number.isFinite(Number(a.spread)) && Number(a.spread) > 0) ? Number(a.spread) : 0, commission: 0 };
  try {
    const raw = localStorage.getItem('tempo:bt:execCfg:' + assetId);
    if (!raw) return def;
    const p = JSON.parse(raw);
    return {
      spread: Number.isFinite(Number(p.spread)) && Number(p.spread) >= 0 ? Number(p.spread) : def.spread,
      commission: Number.isFinite(Number(p.commission)) && Number(p.commission) >= 0 ? Number(p.commission) : 0,
    };
  } catch (e) { return def; }
}
function btStoreExecCfg(assetId, cfg) {
  try { localStorage.setItem('tempo:bt:execCfg:' + assetId, JSON.stringify(cfg)); } catch (e) { /* stockage plein / privé : non bloquant */ }
}

// ═════════════════════════════════════════════════════════════════════════
//  ÉTAT PARTAGÉ DE LA PAGE (chargement bougies + sélection actif/TF + session)
// ═════════════════════════════════════════════════════════════════════════
function useBacktestPage() {
  const [assetId, setAssetIdRaw] = React.useState('EURUSD');
  const [tf, setTfRaw] = React.useState('1h');
  const [bars, setBars] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [loadError, setLoadError] = React.useState(false);

  // epoch : incrémenté par « Nouvelle session » → change resetKey → reset complet
  // du moteur (positions/trades/point de départ aléatoire). Le switch de TF ne
  // touche PAS resetKey : le moteur re-mappe le playhead par le temps et
  // PRÉSERVE positions / ordres / trades — c'est LE point clé du multi-TF.
  const [epoch, setEpoch] = React.useState(0);
  const resetKey = assetId + ':' + epoch;

  // Session Supabase en cours (null = jamais sauvegardée). Permet de METTRE À
  // JOUR la même ligne quand on re-sauvegarde une session reprise.
  const [sessionId, setSessionId] = React.useState(null);
  const [sessionName, setSessionName] = React.useState(null);

  // Réglages d'exécution par actif (spread / commission), synchrones au render.
  const [execCfgMap, setExecCfgMap] = React.useState({});
  const execCfg = React.useMemo(() => execCfgMap[assetId] || btLoadExecCfg(assetId), [execCfgMap, assetId]);
  const setExecCfg = React.useCallback((cfg) => {
    setExecCfgMap(m => ({ ...m, [assetId]: cfg }));
    btStoreExecCfg(assetId, cfg);
  }, [assetId]);

  React.useEffect(() => {
    let live = true;
    setLoading(true);
    setLoadError(false);
    (async () => {
      const data = await loadBtBars(assetId, tf);
      if (!live) return;
      setBars(data);
      setLoading(false);
      setLoadError(data.length === 0);
    })();
    return () => { live = false; };
  }, [assetId, tf]);

  const decimals = btAssetDecimals(assetId);
  const contract = btAssetContract(assetId);
  const bt = useBacktest(bars, {
    startBalance: 100000,
    contract,
    resetKey,
    spread: execCfg.spread,
    commission: execCfg.commission,
    // v2.1 : durée d'une bougie (anti look-ahead au switch de TF) + TF courant
    // (estampillage des trades pour le journal multi-TF).
    barSeconds: ((btTimeframe(tf) || {}).minutes || 0) * 60,
    tf,
  });

  // Switch d'actif / TF : pause IMMÉDIATE de la lecture — sinon le moteur
  // continuerait d'exécuter des fills en aveugle sur les ANCIENNES bougies
  // pendant le chargement (le spinner masque le bouton pause).
  React.useEffect(() => { bt.pause(); }, [assetId, tf]); // bt.pause : callback stable

  // Restauration en attente d'une session reprise : {state, asset, timeframe}.
  // On ne restaure QUE si les bougies chargées correspondent à ce contexte, et
  // toute action qui change le contexte (actif, TF, nouvelle session) l'invalide.
  const pendingRestoreRef = React.useRef(null);

  // Changement d'actif par l'utilisateur : nouvelle session logique
  // (on oublie l'id Supabase repris pour ne pas écraser une autre session).
  const setAssetId = React.useCallback((id) => {
    pendingRestoreRef.current = null; // toute reprise en attente devient caduque
    setAssetIdRaw(id);
    setSessionId(null);
    setSessionName(null);
  }, []);

  // Changement de TF par l'utilisateur : invalide aussi une reprise en attente.
  const setTf = React.useCallback((t) => {
    pendingRestoreRef.current = null;
    setTfRaw(t);
  }, []);

  // « Nouvelle session » : reset complet du moteur via resetKey.
  const newSession = React.useCallback(() => {
    pendingRestoreRef.current = null;
    setEpoch(e => e + 1);
    setSessionId(null);
    setSessionName(null);
  }, []);

  // Reprise d'une session sauvegardée : on recharge actif + TF si besoin,
  // puis on restaure le snapshot une fois les bougies chargées.
  const resumeSession = React.useCallback((row) => {
    if (!row || !row.state) return;
    setSessionId(row.id);
    setSessionName(row.name || null);
    const rowAsset = row.asset || assetId;
    const rowTf = row.timeframe || tf;
    const sameData = rowAsset === assetId && rowTf === tf;
    if (sameData && !loading && bars.length) {
      bt.restoreSnapshot(row.state);
    } else {
      pendingRestoreRef.current = { state: row.state, asset: rowAsset, timeframe: rowTf };
      if (row.asset) setAssetIdRaw(row.asset);
      if (row.timeframe) setTfRaw(row.timeframe);
    }
  }, [assetId, tf, loading, bars, bt]);

  React.useEffect(() => {
    const pr = pendingRestoreRef.current;
    if (!pr) return;
    // Échec de chargement : la reprise en attente est abandonnée.
    if (loadError) { pendingRestoreRef.current = null; return; }
    if (loading || !bars.length) return;
    // Ne restaure que si les bougies chargées sont bien celles de la session
    // (l'utilisateur a pu changer d'actif / TF pendant le chargement).
    pendingRestoreRef.current = null;
    if (pr.asset === assetId && pr.timeframe === tf) bt.restoreSnapshot(pr.state);
  }, [loading, loadError, bars]); // eslint-disable-line react-hooks/exhaustive-deps

  // Markers entrées / sorties à partir des trades clôturés + positions ouvertes.
  const markers = React.useMemo(() => {
    const pal = {
      up: 'rgba(22,163,74,0.95)', down: 'rgba(220,38,38,0.95)', blue: 'rgba(59,130,246,0.95)',
    };
    const exitLabel = { tp: 'TP', sl: 'SL', partial: 'Partiel', be: 'BE' };
    const out = [];
    for (const p of bt.positions) {
      out.push({
        time: p.entryTime,
        position: p.side === 'long' ? 'belowBar' : 'aboveBar',
        color: pal.blue,
        shape: p.side === 'long' ? 'arrowUp' : 'arrowDown',
        text: (p.side === 'long' ? 'Achat' : 'Vente'),
      });
    }
    for (const t of bt.closedTrades) {
      out.push({
        time: t.entryTime,
        position: t.side === 'long' ? 'belowBar' : 'aboveBar',
        color: pal.blue,
        shape: t.side === 'long' ? 'arrowUp' : 'arrowDown',
        text: '',
      });
      out.push({
        time: t.exitTime,
        position: t.side === 'long' ? 'aboveBar' : 'belowBar',
        color: t.pnl >= 0 ? pal.up : pal.down,
        shape: t.side === 'long' ? 'arrowDown' : 'arrowUp',
        text: exitLabel[t.reason] || 'Sortie',
      });
    }
    // setMarkers exige un tri croissant par temps.
    out.sort((a, b) => a.time - b.time);
    return out;
  }, [bt.positions, bt.closedTrades]);

  return {
    assetId, setAssetId, tf, setTf, bars, loading, loadError, decimals, contract, bt, markers,
    execCfg, setExecCfg, newSession, sessionId, setSessionId, sessionName, setSessionName, resumeSession,
    resetKey, // « génération » de contexte (actif + epoch) — garde des sauvegardes async
  };
}

// ═════════════════════════════════════════════════════════════════════════
//  SÉLECTEUR D'ACTIF (groupé Forex / Métaux / Indices)
// ═════════════════════════════════════════════════════════════════════════
function BtAssetSelect({ value, onChange }) {
  const [open, setOpen] = React.useState(false);
  const ref = React.useRef(null);
  const current = btAsset(value);

  useBtClickOutside(ref, open, () => setOpen(false));

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button className="btn tap" onClick={() => setOpen(o => !o)} style={{ minWidth: 168, justifyContent: 'space-between' }}>
        <span style={{ fontWeight: 600 }}>{current ? current.label : '—'}</span>
        <span style={{ display: 'flex', color: 'var(--fg-3)' }}>{Ico.chevDown}</span>
      </button>
      {open && (
        <div className="card" style={{
          position: 'absolute', top: 'calc(100% + 6px)', left: 0, zIndex: 50,
          minWidth: 220, padding: 6, boxShadow: '0 8px 28px rgba(0,0,0,0.14)',
        }}>
          {BT_CATEGORIES.map(cat => (
            <div key={cat}>
              <div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--fg-4)', textTransform: 'uppercase', letterSpacing: '.04em', padding: '8px 10px 4px' }}>{cat}</div>
              {BT_ASSETS.filter(a => a.category === cat).map(a => {
                const on = a.id === value;
                return (
                  <button key={a.id} className="tap" onClick={() => { onChange(a.id); setOpen(false); }} style={{
                    width: '100%', textAlign: 'left', border: 'none', cursor: 'pointer',
                    background: on ? 'var(--blue-soft)' : 'transparent',
                    color: on ? 'var(--blue-600)' : 'var(--fg)',
                    borderRadius: 8, padding: '8px 10px', fontSize: 13, fontWeight: on ? 600 : 500,
                    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                  }}>
                    {a.label}
                    {on && <span style={{ display: 'flex' }}>{Ico.check}</span>}
                  </button>
                );
              })}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── Boutons timeframe ───────────────────────────────────────────────────────
function BtTfButtons({ value, onChange }) {
  return (
    <div style={{ display: 'inline-flex', gap: 3, background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 10, padding: 3 }}>
      {BT_TIMEFRAMES.map(t => {
        const on = t.id === value;
        return (
          <button key={t.id} className="tap" onClick={() => onChange(t.id)} style={{
            padding: '6px 12px', borderRadius: 7, border: 'none', cursor: 'pointer',
            background: on ? 'var(--bg-card)' : 'transparent',
            color: on ? 'var(--fg)' : 'var(--fg-3)',
            fontSize: 12.5, fontWeight: on ? 600 : 500,
            boxShadow: on ? '0 1px 2px rgba(0,0,0,0.08)' : 'none',
          }}>{t.label}</button>
        );
      })}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  BARRE DE REPLAY (début / play-pause / vitesses / saut à date / scrubber)
// ═════════════════════════════════════════════════════════════════════════
const BT_SPEEDS = [1, 2, 5, 10, 30, 60];
const BtReplayIco = {
  start: <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 3v10M13 3L7 8l6 5z"/></svg>,
  play:  <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2.5l9 5.5-9 5.5z"/></svg>,
  pause: <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="3.5" y="3" width="3.2" height="10" rx="1"/><rect x="9.3" y="3" width="3.2" height="10" rx="1"/></svg>,
  step:  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M3 3l6 5-6 5zM12 3v10"/></svg>,
  cal:   <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="12" height="11" rx="2"/><path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3"/></svg>,
};

// Date/heure LOCALES → valeurs d'inputs (YYYY-MM-DD / HH:MM).
function btUnixToYmd(unix) {
  const d = new Date(unix * 1000);
  const p = (n) => String(n).padStart(2, '0');
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
}
function btUnixToHm(unix) {
  const d = new Date(unix * 1000);
  const p = (n) => String(n).padStart(2, '0');
  return p(d.getHours()) + ':' + p(d.getMinutes());
}

// ── Bouton calendrier + popover « Aller à une date » ────────────────────────
function BtJumpButton({ bt, tf, iconBtnStyle }) {
  const [open, setOpen] = React.useState(false);
  const [date, setDate] = React.useState('');
  const [time, setTime] = React.useState('00:00');
  const ref = React.useRef(null);
  useBtClickOutside(ref, open, () => setOpen(false));

  const first = bt.bars && bt.bars.length ? bt.bars[0] : null;
  const last = bt.bars && bt.bars.length ? bt.bars[bt.bars.length - 1] : null;
  const intraday = tf !== '1d'; // en intraday on propose aussi l'heure

  const toggle = () => {
    if (!open && bt.currentBar) {
      // Pré-remplit avec la position courante du replay.
      setDate(btUnixToYmd(bt.currentBar.time));
      setTime(btUnixToHm(bt.currentBar.time));
    }
    setOpen(o => !o);
  };

  const go = () => {
    if (!date || !first || !last) return;
    const d = new Date(date + 'T' + (intraday && time ? time : '00:00'));
    let t = Math.floor(d.getTime() / 1000);
    if (!Number.isFinite(t)) return;
    // Borné à l'historique disponible.
    t = Math.min(Math.max(t, first.time), last.time);
    bt.jumpToTime(t);
    setOpen(false);
  };

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button className="tap" title="Aller à une date" onClick={toggle} disabled={!first} style={iconBtnStyle}>
        {BtReplayIco.cal}
      </button>
      {open && first && last && (
        <div className="card" style={{
          position: 'absolute', bottom: 'calc(100% + 8px)', left: 0, zIndex: 60,
          minWidth: 230, padding: 14, boxShadow: '0 8px 28px rgba(0,0,0,0.14)',
          display: 'flex', flexDirection: 'column', gap: 10,
        }}>
          <div style={{ fontSize: 12.5, fontWeight: 600 }}>Aller à une date</div>
          <input className="input" type="date" value={date}
            min={btUnixToYmd(first.time)} max={btUnixToYmd(last.time)}
            onChange={e => setDate(e.target.value)} />
          {intraday && (
            <input className="input" type="time" value={time} onChange={e => setTime(e.target.value)} />
          )}
          <button className="btn btn-blue tap" onClick={go} style={{ justifyContent: 'center', fontSize: 12.5, fontWeight: 600 }}>
            Sauter
          </button>
          <div style={{ fontSize: 10.5, color: 'var(--fg-4)', lineHeight: 1.45 }}>
            Les SL/TP sont exécutés pendant le saut.
          </div>
        </div>
      )}
    </div>
  );
}

function BtReplayBar({ bt, tf }) {
  const max = Math.max(0, bt.nBars - 1);
  const iconBtn = (extra) => ({
    width: 36, height: 36, borderRadius: 9, border: '1px solid var(--line)',
    background: 'var(--bg-card)', color: 'var(--fg-2)', cursor: 'pointer',
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
    ...extra,
  });
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <button className="tap" title="Aller au début" onClick={bt.goToStart} style={iconBtn()}>{BtReplayIco.start}</button>
        <button className="tap" title={bt.playing ? 'Pause' : 'Lecture'} onClick={() => bt.playing ? bt.pause() : bt.play()}
          style={iconBtn({ width: 42, height: 42, background: 'var(--blue)', color: '#fff', border: '1px solid var(--blue)' })}>
          {bt.playing ? BtReplayIco.pause : BtReplayIco.play}
        </button>
        <button className="tap" title="Bougie suivante" onClick={bt.stepForward} style={iconBtn()}>{BtReplayIco.step}</button>
        <BtJumpButton bt={bt} tf={tf} iconBtnStyle={iconBtn()} />
      </div>

      {/* Presets de vitesse */}
      <div style={{ display: 'inline-flex', gap: 3, background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 9, padding: 3 }}>
        {BT_SPEEDS.map(s => {
          const on = bt.speed === s;
          return (
            <button key={s} className="tap" onClick={() => bt.setSpeed(s)} style={{
              padding: '5px 8px', borderRadius: 6, border: 'none', cursor: 'pointer',
              background: on ? 'var(--bg-card)' : 'transparent',
              color: on ? 'var(--fg)' : 'var(--fg-3)', fontSize: 12, fontWeight: on ? 700 : 500,
              boxShadow: on ? '0 1px 2px rgba(0,0,0,0.08)' : 'none',
            }}>×{s}</button>
          );
        })}
      </div>

      {/* Scrubber */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 180 }}>
        <input type="range" min={0} max={max} value={Math.min(bt.playhead, max)}
          onChange={e => bt.jumpTo(Number(e.target.value))}
          style={{ flex: 1, accentColor: 'var(--blue)', cursor: 'pointer' }} />
        <span className="num" style={{ fontSize: 11.5, color: 'var(--fg-3)', whiteSpace: 'nowrap', minWidth: 92, textAlign: 'right' }}>
          {bt.playhead + 1} / {bt.nBars}
        </span>
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  PANNEAU D'ORDRE v2 (marché / limite / stop + sizing par risque + réglages)
// ═════════════════════════════════════════════════════════════════════════

// Quantité par risque : montantRisque / ((|prixRef − sl| + spreadAdd) × contract),
// arrondie à 2 décimales, minimum 0.01. null si incalculable. spreadAdd
// (optionnel) : spread ajouté à la distance pour un ordre MARCHÉ — l'entrée
// effective (close ± spread) s'éloigne toujours du SL, quel que soit le sens.
function btCalcRiskQty(refPrice, sl, amount, contract, spreadAdd) {
  const p = Number(refPrice), s = Number(sl), a = Number(amount), c = Number(contract);
  if (!Number.isFinite(p) || !Number.isFinite(s) || !Number.isFinite(a) || a <= 0) return null;
  const dist = Math.abs(p - s) + (Number(spreadAdd) > 0 ? Number(spreadAdd) : 0);
  if (!(dist > 0) || !(c > 0)) return null;
  const q = a / (dist * c);
  if (!Number.isFinite(q) || q <= 0) return null;
  return Math.max(0.01, btRound2(q));
}

// ── Popover engrenage : spread + commission ─────────────────────────────────
function BtExecSettings({ execCfg, setExecCfg }) {
  const [open, setOpen] = React.useState(false);
  const [spreadStr, setSpreadStr] = React.useState('');
  const [commStr, setCommStr] = React.useState('');
  const ref = React.useRef(null);
  useBtClickOutside(ref, open, () => setOpen(false));

  const toggle = () => {
    if (!open) {
      // Seed des inputs depuis la config courante à l'ouverture.
      setSpreadStr(String(execCfg.spread));
      setCommStr(String(execCfg.commission));
    }
    setOpen(o => !o);
  };
  const onSpread = (v) => {
    setSpreadStr(v);
    const n = Number(String(v).replace(',', '.'));
    if (Number.isFinite(n) && n >= 0) setExecCfg({ ...execCfg, spread: n });
  };
  const onComm = (v) => {
    setCommStr(v);
    const n = Number(String(v).replace(',', '.'));
    if (Number.isFinite(n) && n >= 0) setExecCfg({ ...execCfg, commission: n });
  };

  const fieldLabel = { fontSize: 11, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.02em', marginBottom: 5, display: 'block' };
  const active = execCfg.spread > 0 || execCfg.commission > 0;

  return (
    <div ref={ref} style={{ position: 'relative', display: 'flex' }}>
      <button className="tap" title="Réglages d'exécution (spread / commission)" onClick={toggle} style={{
        width: 28, height: 28, borderRadius: 8, border: '1px solid var(--line)', cursor: 'pointer',
        background: active ? 'var(--blue-soft)' : 'var(--bg-card)',
        color: active ? 'var(--blue-600)' : 'var(--fg-3)',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
      }}>
        <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="8" cy="8" r="2.4"/>
          <path d="M8 1.6v2.1M8 12.3v2.1M1.6 8h2.1M12.3 8h2.1M3.5 3.5l1.5 1.5M11 11l1.5 1.5M12.5 3.5L11 5M5 11l-1.5 1.5"/>
        </svg>
      </button>
      {open && (
        <div className="card" style={{
          position: 'absolute', top: 'calc(100% + 8px)', right: 0, zIndex: 60,
          width: 230, padding: 14, boxShadow: '0 8px 28px rgba(0,0,0,0.14)',
          display: 'flex', flexDirection: 'column', gap: 12,
        }}>
          <div style={{ fontSize: 12.5, fontWeight: 600 }}>Réglages d'exécution</div>
          <div>
            <label style={fieldLabel}>Spread (unités de prix)</label>
            <input className="input" type="number" min="0" step="any" value={spreadStr} onChange={e => onSpread(e.target.value)} />
          </div>
          <div>
            <label style={fieldLabel}>Commission par clôture ({btCur()})</label>
            <input className="input" type="number" min="0" step="any" value={commStr} onChange={e => onComm(e.target.value)} />
          </div>
          <div style={{ fontSize: 10.5, color: 'var(--fg-4)', lineHeight: 1.5 }}>
            Le spread s'applique à l'entrée des ordres marché uniquement. La commission est déduite à chaque clôture (totale ou partielle).
          </div>
        </div>
      )}
    </div>
  );
}

function BtOrderPanel({ bt, decimals, assetId, execCfg, setExecCfg }) {
  const contract = btAssetContract(assetId);
  const unit = btAssetUnit(assetId);
  const [orderType, setOrderType] = React.useState('market'); // 'market' | 'limit' | 'stop'
  const [price, setPrice] = React.useState('');
  const [sl, setSl] = React.useState('');
  const [tp, setTp] = React.useState('');
  const [sizeMode, setSizeMode] = React.useState('qty');      // 'qty' | 'riskUsd' | 'riskPct'
  const [qty, setQty] = React.useState('1');
  const [riskUsd, setRiskUsd] = React.useState('500');
  const [riskPct, setRiskPct] = React.useState('1');
  const [err, setErr] = React.useState('');

  const spread = Number(execCfg.spread) || 0;
  const slNum = sl === '' ? null : Number(sl);
  const priceNum = price === '' ? null : Number(price);

  // Prix de référence pour l'affichage. Pour un ordre MARCHÉ, la distance au SL
  // intègre le spread (l'entrée effective close ± spread s'éloigne toujours du
  // SL, quel que soit le sens) : quantité et risque réel affichés correspondent
  // exactement à l'ordre réellement exécuté au clic.
  const refDisplay = orderType === 'market' ? bt.currentPrice : priceNum;
  const spreadAdd = orderType === 'market' ? spread : 0;
  const riskAmount = sizeMode === 'riskUsd' ? Number(riskUsd)
    : sizeMode === 'riskPct' ? (bt.balance * Number(riskPct)) / 100
    : null;
  const liveQty = (sizeMode !== 'qty' && slNum != null)
    ? btCalcRiskQty(refDisplay, slNum, riskAmount, contract, spreadAdd)
    : null;
  // Risque réel de la config affichée (qty × distance effective au SL × contrat).
  const shownQty = sizeMode === 'qty' ? Number(qty) : liveQty;
  const riskReal = (Number.isFinite(shownQty) && shownQty > 0 && slNum != null && Number.isFinite(refDisplay))
    ? shownQty * (Math.abs(refDisplay - slNum) + spreadAdd) * contract
    : null;

  const submit = (side) => {
    setErr('');
    if (orderType !== 'market' && (priceNum == null || !Number.isFinite(priceNum))) {
      setErr('Renseigne un prix pour un ordre ' + (orderType === 'limit' ? 'limite' : 'stop') + '.');
      return;
    }
    let q;
    if (sizeMode === 'qty') {
      q = Number(qty);
    } else {
      if (slNum == null || !Number.isFinite(slNum)) {
        setErr('Renseigne un SL pour dimensionner par le risque.');
        return;
      }
      // Distance de risque EFFECTIVE : marché = |close − sl| + spread (identique
      // au récap affiché) ; pending = |prix de l'ordre − sl|.
      q = orderType === 'market'
        ? btCalcRiskQty(bt.currentPrice, slNum, riskAmount, contract, spread)
        : btCalcRiskQty(priceNum, slNum, riskAmount, contract);
      if (q == null) {
        setErr('Impossible de calculer la quantité — vérifie le SL et le montant de risque.');
        return;
      }
    }
    if (!Number.isFinite(q) || q <= 0) { setErr('Quantité invalide.'); return; }
    const res = bt.placeOrder({
      side,
      qty: q,
      type: orderType,
      price: orderType === 'market' ? undefined : priceNum,
      sl: slNum,
      tp: tp === '' ? null : Number(tp),
    });
    if (res && res.error) setErr(res.error);
  };

  const fieldLabel = { fontSize: 11, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.02em', marginBottom: 5, display: 'block' };
  const disabled = bt.nBars === 0;
  const btnLabel = orderType === 'market' ? '' : orderType === 'limit' ? ' LIMITE' : ' STOP';

  return (
    <div className="card" style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 13 }}>
      {/* Prix courant + réglages d'exécution */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 }}>
        <span style={{ fontSize: 12.5, fontWeight: 600, color: 'var(--fg-2)' }}>Prix courant</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <span className="num" style={{ fontSize: 18, fontWeight: 700, letterSpacing: '-.02em' }}>{btPrice(bt.currentPrice, decimals)}</span>
          <BtExecSettings execCfg={execCfg} setExecCfg={setExecCfg} />
        </div>
      </div>

      {/* Type d'ordre */}
      <BtSegmented value={orderType} onChange={(v) => { setOrderType(v); setErr(''); }} options={[
        { id: 'market', label: 'Marché' },
        { id: 'limit', label: 'Limite' },
        { id: 'stop', label: 'Stop' },
      ]} />

      {/* Prix (limite / stop) + SL / TP */}
      <div style={{ display: 'grid', gridTemplateColumns: orderType === 'market' ? '1fr 1fr' : '1fr 1fr 1fr', gap: 10 }}>
        {orderType !== 'market' && (
          <div>
            <label style={fieldLabel}>Prix</label>
            <input className="input" type="number" step="any" placeholder={btPrice(bt.currentPrice, decimals)} value={price} onChange={e => setPrice(e.target.value)} />
          </div>
        )}
        <div>
          <label style={fieldLabel}>SL</label>
          <input className="input" type="number" step="any" placeholder="—" value={sl} onChange={e => setSl(e.target.value)} />
        </div>
        <div>
          <label style={fieldLabel}>TP</label>
          <input className="input" type="number" step="any" placeholder="—" value={tp} onChange={e => setTp(e.target.value)} />
        </div>
      </div>

      {/* Mode de sizing */}
      <BtSegmented value={sizeMode} onChange={(v) => { setSizeMode(v); setErr(''); }} options={[
        { id: 'qty', label: 'Quantité' },
        { id: 'riskUsd', label: 'Risque ' + (typeof currencySymbol === 'function' ? currencySymbol(btCur()) : '$') },
        { id: 'riskPct', label: 'Risque %' },
      ]} />

      {sizeMode === 'qty' ? (
        <div>
          <label style={fieldLabel}>Quantité ({unit}s)</label>
          <input className="input" type="number" min="0" step="any" value={qty} onChange={e => setQty(e.target.value)} />
        </div>
      ) : (
        <div>
          <label style={fieldLabel}>{sizeMode === 'riskUsd' ? 'Risque (' + btCur() + ')' : 'Risque (% du compte)'}</label>
          <input className="input" type="number" min="0" step="any"
            value={sizeMode === 'riskUsd' ? riskUsd : riskPct}
            onChange={e => sizeMode === 'riskUsd' ? setRiskUsd(e.target.value) : setRiskPct(e.target.value)} />
        </div>
      )}

      {/* Récap sizing : qty calculée + risque réel, ou aide si SL manquant */}
      {sizeMode !== 'qty' && slNum == null ? (
        <div style={{ fontSize: 11.5, color: 'var(--fg-3)', background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 8, padding: '8px 10px' }}>
          Renseigne un SL pour calculer la quantité par le risque.
        </div>
      ) : (sizeMode !== 'qty' || riskReal != null) && (
        <div style={{ fontSize: 11.5, color: 'var(--fg-2)', background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 8, padding: '8px 10px', display: 'flex', justifyContent: 'space-between', gap: 8, flexWrap: 'wrap' }}>
          {sizeMode !== 'qty' && (
            <span>Quantité : <b className="num">{liveQty != null ? liveQty.toLocaleString('fr-FR', { maximumFractionDigits: 2 }) : '—'}</b> {unit}{liveQty > 1 ? 's' : ''}</span>
          )}
          <span>Risque réel : <b className="num" style={{ color: 'var(--fg)' }}>{riskReal != null ? btMoney(riskReal) : '—'}</b></span>
        </div>
      )}

      {/* Erreur de validation (locale ou renvoyée par le moteur) */}
      {err && (
        <div style={{ fontSize: 11.5, color: 'var(--red-text)', background: 'var(--red-soft)', borderRadius: 8, padding: '8px 10px' }}>{err}</div>
      )}

      {/* Boutons Achat / Vente */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
        <button className="tap" onClick={() => submit('long')} disabled={disabled} style={{
          padding: '13px 12px', borderRadius: 10, border: 'none', cursor: disabled ? 'default' : 'pointer',
          background: 'var(--green)', color: '#fff', fontSize: 13.5, fontWeight: 700, letterSpacing: '.01em',
          opacity: disabled ? 0.5 : 1,
        }}>ACHAT{btnLabel}</button>
        <button className="tap" onClick={() => submit('short')} disabled={disabled} style={{
          padding: '13px 12px', borderRadius: 10, border: 'none', cursor: disabled ? 'default' : 'pointer',
          background: 'var(--red)', color: '#fff', fontSize: 13.5, fontWeight: 700, letterSpacing: '.01em',
          opacity: disabled ? 0.5 : 1,
        }}>VENTE{btnLabel}</button>
      </div>
    </div>
  );
}

// ── Bandeau compte (solde / équité / P&L réalisé / latent) ──────────────────
function BtAccountStrip({ bt }) {
  const cell = (label, value, color) => (
    <div style={{ flex: 1, padding: '12px 14px', minWidth: 0 }}>
      <div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.02em', marginBottom: 6 }}>{label}</div>
      <div className="num" style={{ fontSize: 15, fontWeight: 700, letterSpacing: '-.015em', color: color || 'var(--fg)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{value}</div>
    </div>
  );
  const sep = <div style={{ width: 1, background: 'var(--line)' }} />;
  return (
    <div className="card" style={{ padding: 0, display: 'flex' }}>
      {cell('Solde', btMoney(bt.balance))}
      {sep}
      {cell('Équité', btMoney(bt.equity))}
      {sep}
      {cell('P&L réalisé', btMoney(bt.realizedPnl, true), btToneColor(bt.realizedPnl))}
      {sep}
      {cell('P&L latent', btMoney(bt.unrealizedPnl, true), btToneColor(bt.unrealizedPnl))}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  POSITIONS OUVERTES v2 (BE / clôture partielle / édition SL-TP)
// ═════════════════════════════════════════════════════════════════════════
function BtPositionRow({ bt, p, decimals, isFirst }) {
  const [editing, setEditing] = React.useState(false);
  const [slV, setSlV] = React.useState('');
  const [tpV, setTpV] = React.useState('');
  const [err, setErr] = React.useState('');

  const dir = p.side === 'long' ? 1 : -1;
  const up = (bt.currentPrice - p.entry) * p.qty * dir * (p.cs > 0 ? p.cs : 1);

  // Moitié de position, arrondie à 2 décimales — désactivée si trop petite.
  const half = btRound2(p.qty / 2);
  const canHalf = half > 0 && half < p.qty;

  const startEdit = () => {
    setSlV(p.sl != null ? String(p.sl) : '');
    setTpV(p.tp != null ? String(p.tp) : '');
    setErr('');
    setEditing(true);
  };
  const applyEdit = () => {
    const res = bt.modifyPosition(p.id, {
      sl: slV === '' ? null : Number(slV),
      tp: tpV === '' ? null : Number(tpV),
    });
    if (res && res.error) { setErr(res.error); return; }
    setErr('');
    setEditing(false);
  };
  const doBe = () => {
    const res = bt.breakEven(p.id);
    if (res && res.error) setErr(res.error); else setErr('');
  };
  const doHalf = () => {
    if (!canHalf) return;
    const res = bt.closePartial(p.id, half);
    if (res && res.error) setErr(res.error); else setErr('');
  };

  const miniBtn = (extra) => ({
    border: '1px solid var(--line)', background: 'var(--bg-card)', color: 'var(--fg-2)',
    borderRadius: 7, padding: '4px 8px', fontSize: 11, fontWeight: 600, cursor: 'pointer',
    ...extra,
  });

  return (
    <div style={{ padding: '11px 14px', borderTop: isFirst ? 'none' : '1px solid var(--line)', display: 'flex', flexDirection: 'column', gap: 8 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
          <span className="pill" style={{
            background: p.side === 'long' ? 'var(--green-soft)' : 'var(--red-soft)',
            color: p.side === 'long' ? 'var(--green-text)' : 'var(--red-text)',
            fontWeight: 700, fontSize: 11,
          }}>{p.side === 'long' ? 'ACHAT' : 'VENTE'}</span>
          <div style={{ minWidth: 0 }}>
            <div className="num" style={{ fontSize: 12.5, fontWeight: 600 }}>{btPrice(p.entry, decimals)} <span style={{ color: 'var(--fg-3)', fontWeight: 500 }}>× {p.qty}</span></div>
            <div style={{ fontSize: 10.5, color: 'var(--fg-4)' }}>
              {p.sl != null ? 'SL ' + btPrice(p.sl, decimals) : 'SL —'} · {p.tp != null ? 'TP ' + btPrice(p.tp, decimals) : 'TP —'}
            </div>
          </div>
        </div>
        <span className="num" style={{ fontSize: 13, fontWeight: 700, color: btToneColor(up), flexShrink: 0 }}>{btMoney(up, true)}</span>
      </div>

      {/* Actions : BE / clôture partielle / édition SL-TP / clôture totale */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
        <button className="tap" title="Ramène le SL au prix d'entrée (break-even)" onClick={doBe} style={miniBtn()}>BE</button>
        <button className="tap" title={canHalf ? 'Clôture la moitié de la position (' + half + ')' : 'Position trop petite pour une clôture partielle'}
          onClick={doHalf} disabled={!canHalf} style={miniBtn({ opacity: canHalf ? 1 : 0.45, cursor: canHalf ? 'pointer' : 'default' })}>½</button>
        <button className="tap" title="Modifier SL / TP" onClick={() => editing ? setEditing(false) : startEdit()}
          style={miniBtn(editing ? { background: 'var(--blue-soft)', color: 'var(--blue-600)', border: '1px solid transparent' } : {})}>SL/TP</button>
        <div style={{ flex: 1 }} />
        <button className="tap" onClick={() => bt.closePosition(p.id)} style={miniBtn({ padding: '4px 10px' })}>Clôturer</button>
      </div>

      {/* Édition inline SL / TP */}
      {editing && (
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <input className="input" type="number" step="any" placeholder="SL" value={slV} onChange={e => setSlV(e.target.value)} style={{ flex: 1, minWidth: 0, padding: '6px 8px', fontSize: 12 }} />
          <input className="input" type="number" step="any" placeholder="TP" value={tpV} onChange={e => setTpV(e.target.value)} style={{ flex: 1, minWidth: 0, padding: '6px 8px', fontSize: 12 }} />
          <button className="tap" onClick={applyEdit} style={miniBtn({ background: 'var(--blue)', color: '#fff', border: '1px solid var(--blue)', padding: '6px 10px' })}>OK</button>
        </div>
      )}
      {err && <div style={{ fontSize: 11, color: 'var(--red-text)' }}>{err}</div>}
    </div>
  );
}

function BtPositionsList({ bt, decimals }) {
  if (!bt.positions.length) {
    return (
      <div className="card" style={{ padding: '18px 16px', textAlign: 'center', color: 'var(--fg-3)', fontSize: 12.5 }}>
        Aucune position ouverte.
      </div>
    );
  }
  return (
    <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 14px', borderBottom: '1px solid var(--line)' }}>
        <span style={{ fontSize: 12.5, fontWeight: 600 }}>Positions ouvertes ({bt.positions.length})</span>
        <button className="tap" onClick={bt.closeAll} style={{
          border: '1px solid var(--line)', background: 'var(--bg-card)', color: 'var(--fg-2)',
          borderRadius: 7, padding: '5px 10px', fontSize: 11.5, fontWeight: 600, cursor: 'pointer',
        }}>Tout clôturer</button>
      </div>
      {bt.positions.map((p, i) => (
        <BtPositionRow key={p.id} bt={bt} p={p} decimals={decimals} isFirst={i === 0} />
      ))}
    </div>
  );
}

// ── Ordres en attente (limite / stop) ───────────────────────────────────────
function BtPendingList({ bt, decimals }) {
  if (!bt.pendingOrders || !bt.pendingOrders.length) return null;
  return (
    <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
      <div style={{ padding: '12px 14px', borderBottom: '1px solid var(--line)', fontSize: 12.5, fontWeight: 600 }}>
        Ordres en attente ({bt.pendingOrders.length})
      </div>
      {bt.pendingOrders.map((o, i) => (
        <div key={o.id} style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10,
          padding: '10px 14px', borderTop: i ? '1px solid var(--line)' : 'none',
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
            <span className="pill" style={{
              background: o.side === 'long' ? 'var(--green-soft)' : 'var(--red-soft)',
              color: o.side === 'long' ? 'var(--green-text)' : 'var(--red-text)',
              fontWeight: 700, fontSize: 10.5,
            }}>{(o.side === 'long' ? 'ACHAT' : 'VENTE') + ' ' + (o.type === 'limit' ? 'LIMITE' : 'STOP')}</span>
            <div style={{ minWidth: 0 }}>
              <div className="num" style={{ fontSize: 12.5, fontWeight: 600 }}>{btPrice(o.price, decimals)} <span style={{ color: 'var(--fg-3)', fontWeight: 500 }}>× {o.qty}</span></div>
              <div style={{ fontSize: 10.5, color: 'var(--fg-4)' }}>
                {o.sl != null ? 'SL ' + btPrice(o.sl, decimals) : 'SL —'} · {o.tp != null ? 'TP ' + btPrice(o.tp, decimals) : 'TP —'}
              </div>
            </div>
          </div>
          <button className="tap" onClick={() => bt.cancelOrder(o.id)} style={{
            border: '1px solid var(--line)', background: 'var(--bg-card)', color: 'var(--fg-2)',
            borderRadius: 7, padding: '5px 10px', fontSize: 11.5, fontWeight: 600, cursor: 'pointer', flexShrink: 0,
          }}>Annuler</button>
        </div>
      ))}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  STATS DE SESSION + COURBE D'ÉQUITÉ (SVG inline pur, pas de lib)
// ═════════════════════════════════════════════════════════════════════════

// Path SVG « lissé » : quadratiques passant par les milieux des segments.
function _btSmoothPath(pts) {
  if (!pts.length) return '';
  if (pts.length < 3) return 'M' + pts.map(p => p[0] + ',' + p[1]).join(' L');
  let d = 'M' + pts[0][0] + ',' + pts[0][1];
  for (let i = 1; i < pts.length - 1; i++) {
    const mx = (pts[i][0] + pts[i + 1][0]) / 2;
    const my = (pts[i][1] + pts[i + 1][1]) / 2;
    d += ' Q' + pts[i][0] + ',' + pts[i][1] + ' ' + mx + ',' + my;
  }
  d += ' L' + pts[pts.length - 1][0] + ',' + pts[pts.length - 1][1];
  return d;
}

function BtEquityCurve({ curve, startBalance }) {
  if (!curve || curve.length < 2) {
    return (
      <div style={{ height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--fg-4)', fontSize: 11.5, background: 'var(--bg-elev)', borderRadius: 10 }}>
        La courbe apparaîtra après ton premier trade clôturé.
      </div>
    );
  }
  const W = 320, H = 120, pad = 6;
  const n = curve.length;
  let min = Infinity, max = -Infinity;
  for (const p of curve) {
    const e = Number(p.equity);
    if (e < min) min = e;
    if (e > max) max = e;
  }
  if (!(max > min)) { max = min + Math.max(1, Math.abs(min) * 0.001); } // courbe plate : évite la division par 0
  // Abscisse par le temps si possible, sinon espacement uniforme (plusieurs
  // clôtures sur la même bougie → temps identiques).
  const t0 = curve[0].time, t1 = curve[n - 1].time;
  const byTime = Number.isFinite(t0) && Number.isFinite(t1) && t1 > t0;
  const x = (p, i) => pad + (byTime ? (p.time - t0) / (t1 - t0) : i / (n - 1)) * (W - 2 * pad);
  const y = (p) => pad + (1 - (p.equity - min) / (max - min)) * (H - 2 * pad);
  const pts = curve.map((p, i) => [Math.round(x(p, i) * 10) / 10, Math.round(y(p) * 10) / 10]);
  const line = _btSmoothPath(pts);
  const area = line + ' L' + pts[pts.length - 1][0] + ',' + (H - 1) + ' L' + pts[0][0] + ',' + (H - 1) + ' Z';
  const net = curve[n - 1].equity - (Number.isFinite(Number(startBalance)) ? Number(startBalance) : curve[0].equity);
  const stroke = net >= 0 ? 'var(--green)' : 'var(--red)';
  const fill = net >= 0 ? 'var(--green-soft)' : 'var(--red-soft)';
  return (
    <svg viewBox={'0 0 ' + W + ' ' + H} preserveAspectRatio="none" style={{ width: '100%', height: 120, display: 'block' }} aria-hidden="true">
      <path d={area} fill={fill} stroke="none" />
      <path d={line} fill="none" stroke={stroke} strokeWidth="1.8" strokeLinejoin="round" strokeLinecap="round" vectorEffect="non-scaling-stroke" />
    </svg>
  );
}

function BtSessionStatsCard({ bt }) {
  const s = React.useMemo(
    () => (typeof btSessionStats === 'function' ? btSessionStats(bt.closedTrades, bt.startBalance) : null),
    [bt.closedTrades, bt.startBalance]
  );

  const pfLabel = (pf) => pf == null ? '—' : !Number.isFinite(pf) ? '∞' : pf.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  // Défensif : accepte un winrate en fraction (0–1) ou en pourcentage (0–100).
  const wrLabel = (wr) => {
    if (wr == null || !Number.isFinite(Number(wr))) return '—';
    const v = Number(wr) <= 1 ? Number(wr) * 100 : Number(wr);
    return v.toLocaleString('fr-FR', { maximumFractionDigits: 1 }) + ' %';
  };
  const ddLabel = (s) => {
    if (!s || s.maxDrawdown == null) return '—';
    const pct = s.maxDrawdownPct != null && Number.isFinite(Number(s.maxDrawdownPct))
      ? ' (' + Number(s.maxDrawdownPct).toLocaleString('fr-FR', { maximumFractionDigits: 1 }) + ' %)'
      : '';
    return '−' + btMoney(Math.abs(s.maxDrawdown)) + pct;
  };

  const row = (label, value, color) => (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
      <span style={{ fontSize: 11, color: 'var(--fg-3)', fontWeight: 500 }}>{label}</span>
      <span className="num" style={{ fontSize: 12, fontWeight: 700, color: color || 'var(--fg)', textAlign: 'right' }}>{value}</span>
    </div>
  );

  return (
    <div className="card" style={{ padding: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
        <span style={{ fontSize: 12.5, fontWeight: 600 }}>Session</span>
        {s && s.n > 0 && (
          <span className="num" style={{ fontSize: 12.5, fontWeight: 700, color: btToneColor(s.netPnl) }}>{btMoney(s.netPnl, true)}</span>
        )}
      </div>

      <BtEquityCurve curve={bt.equityCurve} startBalance={bt.startBalance} />

      {s && s.n > 0 ? (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', columnGap: 18, rowGap: 7 }}>
          {row('Trades', String(s.n) + ' (' + s.wins + 'G / ' + s.losses + 'P)')}
          {row('Winrate', wrLabel(s.winrate), 'var(--blue)')}
          {row('Profit factor', pfLabel(s.profitFactor))}
          {row('R moyen', btR(s.avgR), s.avgR > 0 ? 'var(--green)' : s.avgR < 0 ? 'var(--red)' : undefined)}
          {row('Expectancy', btR(s.expectancyR))}
          {row('Max drawdown', ddLabel(s), s.maxDrawdown > 0 ? 'var(--red)' : undefined)}
          {row('Meilleur', s.best != null ? btMoney(s.best, true) : '—', 'var(--green)')}
          {row('Pire', s.worst != null ? btMoney(s.worst, true) : '—', 'var(--red)')}
        </div>
      ) : (
        <div style={{ fontSize: 11.5, color: 'var(--fg-4)', textAlign: 'center' }}>Aucun trade clôturé pour l'instant.</div>
      )}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  ENREGISTREMENT DE SESSION → Supabase (backtest_sessions / backtest_trades)
// ═════════════════════════════════════════════════════════════════════════
// v2 : inclut le snapshot moteur (colonne jsonb `state`) + un nom lisible.
// Si sessionId est fourni (session reprise), la MÊME ligne est mise à jour :
// les trades précédents de la session sont remplacés par l'état courant.
async function btSaveSession({ assetId, tf, bt, sessionId, name }) {
  if (!window.sb) return { error: { message: 'Hors-ligne — connexion requise.' } };
  // FIGE l'état AVANT tout await : pause de la lecture puis snapshot unique.
  // Sinon le moteur continuerait d'avancer pendant l'aller-retour réseau et les
  // agrégats (trade_count, net_pnl, trades insérés…) divergeraient du snapshot.
  if (typeof bt.pause === 'function') bt.pause();
  const snapshot = typeof bt.getSnapshot === 'function' ? bt.getSnapshot() : null;
  const closed = snapshot ? snapshot.closedTrades : bt.closedTrades;
  const openPos = snapshot ? snapshot.positions : bt.positions;
  const pendings = snapshot ? snapshot.pendingOrders : (bt.pendingOrders || []);
  const realized = snapshot ? (Number(snapshot.realizedPnl) || 0) : bt.realizedPnl;
  const hasContent = closed.length || openPos.length || pendings.length;
  if (!hasContent) return { error: { message: 'Rien à enregistrer : aucun trade, position ou ordre.' } };

  const { data: userData } = await window.sb.auth.getUser();
  const user_id = userData && userData.user && userData.user.id;
  if (!user_id) return { error: { message: 'Tu dois être connecté.' } };

  const netPnl = closed.reduce((s, t) => s + t.pnl, 0);
  const wins = closed.filter(t => t.pnl > 0).length;

  const sessionRow = {
    asset: assetId,
    timeframe: tf,
    start_balance: bt.startBalance,
    end_balance: bt.startBalance + realized,
    net_pnl: netPnl,
    trade_count: closed.length,
    win_count: wins,
    name: name || btDefaultSessionName(assetId, tf),
    state: snapshot,
    updated_at: new Date().toISOString(),
  };

  // 1) crée OU met à jour la session. En cas de mise à jour, on lit d'abord les
  // ids des trades existants — ils ne seront supprimés QU'APRÈS un insert
  // réussi des nouveaux (insert-then-delete : au pire des doublons temporaires,
  // jamais de perte de trades en base sur un échec réseau).
  let session_id = sessionId || null;
  let updated = false;
  let oldIds = [];
  if (session_id) {
    const upd = await window.sb.from('backtest_sessions').update(sessionRow).eq('id', session_id).select().maybeSingle();
    if (upd.error) return { error: upd.error };
    if (upd.data) {
      updated = true;
      const olds = await window.sb.from('backtest_trades').select('id').eq('session_id', session_id);
      if (olds.error) return { error: olds.error, session_id };
      oldIds = (olds.data || []).map(r => r.id);
    } else {
      session_id = null; // la session a été supprimée entre-temps → on en recrée une.
    }
  }
  if (!session_id) {
    const ins = await window.sb.from('backtest_sessions').insert({ user_id, ...sessionRow }).select().single();
    if (ins.error) return { error: ins.error };
    session_id = ins.data.id;
  }

  // 2) insère les trades clôturés rattachés à la session.
  const tradeRows = closed.map(t => ({
    user_id,
    session_id,
    asset: assetId,
    timeframe: t.tf || tf, // TF au moment de l'ouverture du trade (multi-TF), fallback TF courant
    side: t.side,
    qty: t.qty,
    entry_price: t.entry,
    exit_price: t.exit,
    sl: t.sl,
    tp: t.tp,
    pnl: t.pnl,
    r_multiple: t.r,
    reason: t.reason,
    commission: t.commission || 0,
    partial: !!t.partial,
    entry_at: new Date(t.entryTime * 1000).toISOString(),
    exit_at: new Date(t.exitTime * 1000).toISOString(),
  }));
  if (tradeRows.length) {
    const tradesRes = await window.sb.from('backtest_trades').insert(tradeRows);
    // session_id est renvoyé MÊME en erreur : le re-clic passera par le chemin
    // « mise à jour » au lieu de créer une session dupliquée / orpheline.
    if (tradesRes.error) return { error: tradesRes.error, session_id };
  }

  // 3) SEULEMENT après l'insert réussi : purge des anciens trades (mise à jour).
  if (oldIds.length) {
    const del = await window.sb.from('backtest_trades').delete().in('id', oldIds);
    if (del.error) return { error: del.error, session_id }; // doublons temporaires — un re-clic les résorbe
  }
  return { data: { session_id, count: tradeRows.length, updated } };
}

function BtSaveButton({ assetId, tf, bt, sessionId, sessionName, resetKey, onSaved }) {
  const [status, setStatus] = React.useState(null); // null | 'saving' | 'ok' | 'err'
  const [msg, setMsg] = React.useState('');
  const [name, setName] = React.useState('');
  const busyRef = React.useRef(false); // garde anti double-clic (re-entrée)

  // « Génération » du contexte : si l'actif, l'epoch (« Nouvelle session ») ou
  // la session courante change PENDANT l'await de sauvegarde, on n'applique pas
  // onSaved — sinon une nouvelle session vierge serait re-liée à la ligne qui
  // vient d'être enregistrée (et écraserait ses trades au prochain « Mettre à
  // jour la session »).
  const genRef = React.useRef('');
  genRef.current = String(resetKey || '') + '|' + String(sessionId || '');

  // Reprise d'une session nommée → pré-remplit le champ nom.
  React.useEffect(() => { setName(sessionName || ''); }, [sessionId, sessionName]);

  const hasContent = bt.closedTrades.length > 0 || bt.positions.length > 0 || (bt.pendingOrders && bt.pendingOrders.length > 0);
  const disabled = status === 'saving' || !hasContent;

  const save = async () => {
    if (busyRef.current) return;
    busyRef.current = true;
    const gen = genRef.current;
    setStatus('saving'); setMsg('');
    const finalName = name.trim() || btDefaultSessionName(assetId, tf);
    const res = await btSaveSession({ assetId, tf, bt, sessionId, name: finalName });
    busyRef.current = false;
    const sameCtx = genRef.current === gen; // le contexte n'a pas changé pendant l'await
    if (res.error) {
      setStatus('err');
      setMsg((res.error.message) || 'Échec de l\'enregistrement.');
      // Session créée mais trades en échec : on adopte quand même son id pour
      // que le re-clic passe par la mise à jour au lieu de créer un doublon.
      if (sameCtx && res.session_id && res.session_id !== sessionId && onSaved) onSaved(res.session_id, finalName);
    } else {
      setStatus('ok');
      setMsg(res.data.updated
        ? 'Session mise à jour (' + res.data.count + ' trade' + (res.data.count > 1 ? 's' : '') + ').'
        : 'Session enregistrée (' + res.data.count + ' trade' + (res.data.count > 1 ? 's' : '') + ').');
      if (sameCtx && onSaved) onSaved(res.data.session_id, finalName);
    }
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      <input className="input" type="text" value={name} onChange={e => setName(e.target.value)}
        placeholder={btDefaultSessionName(assetId, tf)} title="Nom de la session" style={{ fontSize: 12.5 }} />
      <button className="btn btn-blue tap" onClick={save} disabled={disabled} style={{ justifyContent: 'center', padding: '11px 14px', fontSize: 13, fontWeight: 600, opacity: disabled ? 0.55 : 1 }}>
        {status === 'saving' ? 'Enregistrement…' : sessionId ? 'Mettre à jour la session' : 'Enregistrer la session'}
      </button>
      {msg && (
        <div style={{ fontSize: 11.5, color: status === 'err' ? 'var(--red-text)' : 'var(--green-text)', textAlign: 'center' }}>{msg}</div>
      )}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  SESSIONS SAUVEGARDÉES (liste / reprise / export vers le journal / suppression)
// ═════════════════════════════════════════════════════════════════════════
function useBtSessions(refreshKey) {
  const [data, setData] = React.useState({ loading: true, error: null, sessions: [] });
  const [bump, setBump] = React.useState(0);
  React.useEffect(() => {
    let live = true;
    (async () => {
      if (!window.sb) { setData({ loading: false, error: 'offline', sessions: [] }); return; }
      setData(d => ({ ...d, loading: true, error: null }));
      try {
        const res = await window.sb.from('backtest_sessions').select('*').order('created_at', { ascending: false }).limit(100);
        if (!live) return;
        if (res.error) { setData({ loading: false, error: 'load', sessions: [] }); return; }
        setData({ loading: false, error: null, sessions: res.data || [] });
      } catch (e) {
        if (live) setData({ loading: false, error: 'load', sessions: [] });
      }
    })();
    return () => { live = false; };
  }, [refreshKey, bump]);
  return { ...data, reload: () => setBump(b => b + 1) };
}

// Export d'une session de backtest vers le JOURNAL RÉEL (table trades).
// Action EXPLICITE de l'utilisateur — seule passerelle backtest → journal réel.
// imported_from = 'backtest' + external_id = id du trade de backtest :
// bulkInsertTrades déduplique, un re-export ne crée donc pas de doublons.
async function btExportSessionToJournal(session) {
  if (!window.sb) return { error: { message: 'Hors-ligne — connexion requise.' } };
  if (typeof bulkInsertTrades !== 'function') return { error: { message: 'API du journal indisponible.' } };
  const res = await window.sb.from('backtest_trades').select('*').eq('session_id', session.id).order('exit_at', { ascending: true });
  if (res.error) return { error: res.error };
  const trades = res.data || [];
  if (!trades.length) return { error: { message: 'Aucun trade à exporter dans cette session.' } };
  const rows = trades.map(t => ({
    symbol: t.asset,
    direction: t.side,
    entry: t.entry_price,
    exit_price: t.exit_price,
    lots: t.qty,
    pnl: t.pnl,
    r_multiple: t.r_multiple,
    executed_at: t.exit_at || t.entry_at,
    notes: 'Backtest ' + (t.asset || '') + ' ' + (t.timeframe || ''),
    imported_from: 'backtest',
    external_id: t.id,
  }));
  const ins = await bulkInsertTrades(rows);
  if (ins.error) return { error: ins.error };
  return { data: { count: (ins.data || []).length, skipped: ins.skipped || 0 } };
}

function BtSessionRow({ row, onResume, onDeleted, isFirst }) {
  // mode : null | 'confirmExport' | 'confirmDelete'
  const [mode, setMode] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState(null); // {tone:'ok'|'err', text}
  const busyRef = React.useRef(false);

  const dateLabel = (() => {
    const iso = row.updated_at || row.created_at;
    if (!iso) return '—';
    try { return new Date(iso).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }); }
    catch (e) { return '—'; }
  })();

  const doExport = async () => {
    if (busyRef.current) return;
    busyRef.current = true;
    setBusy(true); setMsg(null);
    const res = await btExportSessionToJournal(row);
    busyRef.current = false;
    setBusy(false); setMode(null);
    if (res.error) setMsg({ tone: 'err', text: res.error.message || 'Échec de l\'export.' });
    else if (res.data.count === 0) setMsg({ tone: 'ok', text: 'Déjà exportée — aucun nouveau trade.' });
    else setMsg({ tone: 'ok', text: res.data.count + ' trade' + (res.data.count > 1 ? 's' : '') + ' exporté' + (res.data.count > 1 ? 's' : '') + ' vers le journal' + (res.data.skipped ? ' (' + res.data.skipped + ' déjà présent' + (res.data.skipped > 1 ? 's' : '') + ')' : '') + '.' });
  };

  const doDelete = async () => {
    if (busyRef.current) return;
    busyRef.current = true;
    setBusy(true); setMsg(null);
    const res = await window.sb.from('backtest_sessions').delete().eq('id', row.id);
    busyRef.current = false;
    setBusy(false); setMode(null);
    if (res.error) setMsg({ tone: 'err', text: res.error.message || 'Échec de la suppression.' });
    else onDeleted();
  };

  const miniBtn = (extra) => ({
    border: '1px solid var(--line)', background: 'var(--bg-card)', color: 'var(--fg-2)',
    borderRadius: 7, padding: '5px 10px', fontSize: 11.5, fontWeight: 600, cursor: 'pointer',
    opacity: busy ? 0.55 : 1,
    ...extra,
  });
  const nTrades = Number(row.trade_count) || 0;

  return (
    <div style={{ padding: '12px 14px', borderTop: isFirst ? 'none' : '1px solid var(--line)', display: 'flex', flexDirection: 'column', gap: 8 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10, flexWrap: 'wrap' }}>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {row.name || ((row.asset || '—') + ' ' + (row.timeframe || ''))}
          </div>
          <div style={{ fontSize: 11, color: 'var(--fg-4)' }}>
            {(row.asset || '—') + ' · ' + (row.timeframe || '—') + ' · ' + nTrades + ' trade' + (nTrades > 1 ? 's' : '') + ' · ' + dateLabel}
          </div>
        </div>
        <span className="num" style={{ fontSize: 13.5, fontWeight: 700, color: btToneColor(row.net_pnl), flexShrink: 0 }}>{btMoney(row.net_pnl, true)}</span>
      </div>

      {mode === 'confirmExport' ? (
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 11.5, color: 'var(--fg-2)' }}>Exporter {nTrades} trade{nTrades > 1 ? 's' : ''} vers le journal réel ?</span>
          <button className="tap" onClick={doExport} disabled={busy} style={miniBtn({ background: 'var(--blue)', color: '#fff', border: '1px solid var(--blue)' })}>{busy ? 'Export…' : 'Confirmer'}</button>
          <button className="tap" onClick={() => setMode(null)} disabled={busy} style={miniBtn()}>Annuler</button>
        </div>
      ) : mode === 'confirmDelete' ? (
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <span style={{ fontSize: 11.5, color: 'var(--fg-2)' }}>Supprimer cette session et ses trades de backtest ?</span>
          <button className="tap" onClick={doDelete} disabled={busy} style={miniBtn({ background: 'var(--red)', color: '#fff', border: '1px solid var(--red)' })}>{busy ? 'Suppression…' : 'Supprimer'}</button>
          <button className="tap" onClick={() => setMode(null)} disabled={busy} style={miniBtn()}>Annuler</button>
        </div>
      ) : (
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
          <button className="tap" onClick={() => onResume(row)} disabled={!row.state}
            title={row.state ? 'Recharge l\'actif / TF et restaure positions, ordres et trades' : 'Session enregistrée sans snapshot (ancienne version) — reprise impossible'}
            style={miniBtn(row.state ? { background: 'var(--blue-soft)', color: 'var(--blue-600)', border: '1px solid transparent' } : { opacity: 0.45, cursor: 'default' })}>
            Reprendre
          </button>
          <button className="tap" onClick={() => { setMsg(null); setMode('confirmExport'); }} disabled={!nTrades}
            style={miniBtn(!nTrades ? { opacity: 0.45, cursor: 'default' } : {})}>Exporter vers le journal</button>
          <div style={{ flex: 1 }} />
          <button className="tap" onClick={() => { setMsg(null); setMode('confirmDelete'); }} style={miniBtn({ color: 'var(--red-text)' })}>Supprimer</button>
        </div>
      )}

      {msg && <div style={{ fontSize: 11.5, color: msg.tone === 'err' ? 'var(--red-text)' : 'var(--green-text)' }}>{msg.text}</div>}
    </div>
  );
}

function BtSessionsPanel({ refreshKey, onResume }) {
  const { loading, error, sessions, reload } = useBtSessions(refreshKey);

  if (loading) {
    return <div className="card" style={{ padding: 32, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>Chargement des sessions…</div>;
  }
  if (error) {
    return (
      <div className="card" style={{ padding: 32, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
        <span>Impossible de charger les sessions de backtest.</span>
        <button className="btn tap" onClick={reload} style={{ fontSize: 12.5 }}>Réessayer</button>
      </div>
    );
  }
  if (!sessions.length) {
    return (
      <div className="card" style={{ padding: 36, textAlign: 'center' }}>
        <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-2)', marginBottom: 6 }}>Aucune session enregistrée</div>
        <div style={{ fontSize: 12.5, color: 'var(--fg-3)', maxWidth: 380, margin: '0 auto', lineHeight: 1.5 }}>
          Depuis l'onglet Backtest, enregistre une session pour la retrouver ici, la reprendre plus tard ou l'exporter vers ton journal réel.
        </div>
      </div>
    );
  }
  return (
    <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
      <div style={{ padding: '12px 14px', borderBottom: '1px solid var(--line)', fontSize: 12.5, fontWeight: 600 }}>
        Sessions enregistrées ({sessions.length})
      </div>
      {sessions.map((s, i) => (
        <BtSessionRow key={s.id} row={s} isFirst={i === 0} onResume={onResume} onDeleted={reload} />
      ))}
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  JOURNAL DE BACKTEST (lecture backtest_trades + stats locales)
//  100% indépendant du journal réel (table 'trades').
// ═════════════════════════════════════════════════════════════════════════
function useBacktestJournal(refreshKey) {
  const [data, setData] = React.useState({ loading: true, error: null, trades: [] });
  const [bump, setBump] = React.useState(0); // reload manuel (bouton « Réessayer »)
  React.useEffect(() => {
    let live = true;
    (async () => {
      if (!window.sb) { setData({ loading: false, error: 'offline', trades: [] }); return; }
      setData(d => ({ ...d, loading: true, error: null }));
      try {
        const res = await window.sb.from('backtest_trades').select('*').order('exit_at', { ascending: false }).limit(500);
        if (!live) return;
        if (res.error) { setData({ loading: false, error: 'load', trades: [] }); return; }
        setData({ loading: false, error: null, trades: res.data || [] });
      } catch (e) {
        if (live) setData({ loading: false, error: 'load', trades: [] });
      }
    })();
    return () => { live = false; };
  }, [refreshKey, bump]);
  return { ...data, reload: () => setBump(b => b + 1) };
}

function btJournalStats(trades) {
  const n = trades.length;
  if (!n) return { n: 0, winRate: 0, netPnl: 0, profitFactor: null, avgR: 0, hasR: false };
  let wins = 0, gross = 0, lossGross = 0, netPnl = 0, rSum = 0, rCount = 0;
  for (const t of trades) {
    const p = Number(t.pnl) || 0;
    netPnl += p;
    if (p > 0) { wins++; gross += p; } else if (p < 0) { lossGross += -p; }
    const r = Number(t.r_multiple);
    if (t.r_multiple != null && Number.isFinite(r)) { rSum += r; rCount++; }
  }
  const profitFactor = lossGross > 0 ? gross / lossGross : (gross > 0 ? Infinity : null);
  return {
    n, winRate: (wins / n) * 100, netPnl, profitFactor,
    avgR: rCount ? rSum / rCount : 0, hasR: rCount > 0,
  };
}

function BtJournal({ refreshKey, decimals }) {
  const { loading, error, trades, reload } = useBacktestJournal(refreshKey);
  const stats = React.useMemo(() => btJournalStats(trades), [trades]);

  if (loading) {
    return <div className="card" style={{ padding: 32, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13 }}>Chargement du journal de backtest…</div>;
  }
  if (error) {
    return (
      <div className="card" style={{ padding: 32, textAlign: 'center', color: 'var(--fg-3)', fontSize: 13, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
        <span>Impossible de charger le journal de backtest.</span>
        <button className="btn tap" onClick={reload} style={{ fontSize: 12.5 }}>Réessayer</button>
      </div>
    );
  }
  if (!trades.length) {
    return (
      <div className="card" style={{ padding: 36, textAlign: 'center' }}>
        <div style={{ color: 'var(--fg-4)', marginBottom: 10, display: 'flex', justifyContent: 'center', transform: 'scale(1.6)' }}>{Ico.trades}</div>
        <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--fg-2)', marginBottom: 6 }}>Aucun trade de backtest</div>
        <div style={{ fontSize: 12.5, color: 'var(--fg-3)', maxWidth: 360, margin: '0 auto', lineHeight: 1.5 }}>
          Rejoue un scénario, clôture des trades, puis enregistre la session pour les retrouver ici.
        </div>
      </div>
    );
  }

  const pf = stats.profitFactor;
  const pfLabel = pf == null ? '—' : !Number.isFinite(pf) ? '∞' : pf.toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

  const Kpi = ({ label, value, color }) => (
    <div className="card" style={{ padding: '14px 16px' }}>
      <div style={{ fontSize: 10.5, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.02em', marginBottom: 8 }}>{label}</div>
      <div className="num" style={{ fontSize: 20, fontWeight: 700, letterSpacing: '-.02em', color: color || 'var(--fg)' }}>{value}</div>
    </div>
  );

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 12 }}>
        <Kpi label="Trades" value={String(stats.n)} />
        <Kpi label="Winrate" value={stats.winRate.toLocaleString('fr-FR', { maximumFractionDigits: 1 }) + ' %'} color="var(--blue)" />
        <Kpi label="P&L net" value={btMoney(stats.netPnl, true)} color={btToneColor(stats.netPnl)} />
        <Kpi label="Profit factor" value={pfLabel} color="var(--blue)" />
        <Kpi label="R moyen" value={stats.hasR ? btR(stats.avgR) : '—'} color={stats.avgR > 0 ? 'var(--green)' : stats.avgR < 0 ? 'var(--red)' : 'var(--fg)'} />
      </div>

      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        <div style={{ display: 'grid', gridTemplateColumns: '90px 1fr 1fr 1fr 1fr 80px 80px', gap: 0, padding: '10px 14px', borderBottom: '1px solid var(--line)', fontSize: 10.5, fontWeight: 600, color: 'var(--fg-3)', textTransform: 'uppercase', letterSpacing: '.02em' }}>
          <span>Sens</span><span>Actif</span><span>Date</span><span>Entrée</span><span>Sortie</span><span style={{ textAlign: 'right' }}>R</span><span style={{ textAlign: 'right' }}>P&L</span>
        </div>
        <div className="scroll" style={{ maxHeight: 420, overflow: 'auto' }}>
          {trades.map((t, i) => (
            <div key={t.id || i} style={{ display: 'grid', gridTemplateColumns: '90px 1fr 1fr 1fr 1fr 80px 80px', gap: 0, padding: '10px 14px', borderTop: i ? '1px solid var(--line)' : 'none', alignItems: 'center', fontSize: 12 }}>
              <span className="pill" style={{
                background: t.side === 'long' ? 'var(--green-soft)' : 'var(--red-soft)',
                color: t.side === 'long' ? 'var(--green-text)' : 'var(--red-text)',
                fontWeight: 700, fontSize: 10, width: 'fit-content',
              }}>{t.side === 'long' ? 'ACHAT' : 'VENTE'}</span>
              <span style={{ fontWeight: 600 }}>{t.asset} <span style={{ color: 'var(--fg-4)', fontWeight: 500 }}>{t.timeframe}</span>{t.partial ? <span className="pill" style={{ marginLeft: 6, fontSize: 9.5, background: 'var(--blue-soft)', color: 'var(--blue-600)', fontWeight: 700 }}>½</span> : null}</span>
              {/* Date de sortie du trade (bougie simulée) — repère de session indispensable au journal. */}
              <span className="num" style={{ color: 'var(--fg-2)' }}>{t.exit_at ? btFmtTime(new Date(t.exit_at).getTime() / 1000) : '—'}</span>
              <span className="num" style={{ color: 'var(--fg-2)' }}>{btPrice(t.entry_price, btAssetDecimals(t.asset))}</span>
              <span className="num" style={{ color: 'var(--fg-2)' }}>{btPrice(t.exit_price, btAssetDecimals(t.asset))}</span>
              <span className="num" style={{ textAlign: 'right', color: 'var(--fg-2)' }}>{btR(t.r_multiple)}</span>
              <span className="num" style={{ textAlign: 'right', fontWeight: 700, color: btToneColor(t.pnl) }}>{btMoney(t.pnl, true)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Onglets Backtest / Sessions / Journal ───────────────────────────────────
function BtTabs({ tab, setTab }) {
  const tabs = [
    { id: 'replay', label: 'Backtest' },
    { id: 'sessions', label: 'Sessions' },
    { id: 'journal', label: 'Journal' },
  ];
  return (
    <div style={{ display: 'inline-flex', gap: 4, background: 'var(--bg-elev)', border: '1px solid var(--line)', borderRadius: 11, padding: 3 }}>
      {tabs.map(t => {
        const on = t.id === tab;
        return (
          <button key={t.id} className="tap" onClick={() => setTab(t.id)} style={{
            padding: '7px 15px', borderRadius: 8, border: 'none', cursor: 'pointer',
            background: on ? 'var(--bg-card)' : 'transparent', color: on ? 'var(--fg)' : 'var(--fg-3)',
            fontSize: 12.5, fontWeight: on ? 600 : 500, boxShadow: on ? '0 1px 3px rgba(0,0,0,0.08)' : 'none',
          }}>{t.label}</button>
        );
      })}
    </div>
  );
}

// ── Spinner de chargement des bougies ───────────────────────────────────────
function BtLoading({ label }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14, height: '100%', minHeight: 280, color: 'var(--fg-3)' }}>
      <div style={{ width: 30, height: 30, borderRadius: '50%', border: '3px solid var(--line)', borderTopColor: 'var(--blue)', animation: 'bt-spin .8s linear infinite' }} />
      <span style={{ fontSize: 13 }}>{label || 'Chargement des bougies…'}</span>
      <style>{'@keyframes bt-spin{to{transform:rotate(360deg)}}'}</style>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  PAGE DESKTOP
// ═════════════════════════════════════════════════════════════════════════
function BacktestPage({ state }) {
  const page = useBacktestPage();
  const { assetId, setAssetId, tf, setTf, loading, loadError, decimals, bt, markers } = page;
  const { theme } = (typeof useTheme === 'function') ? useTheme() : { theme: 'light' };
  const [tab, setTab] = React.useState('replay');
  const [journalKey, setJournalKey] = React.useState(0);
  const [sessionsKey, setSessionsKey] = React.useState(0);
  // Outils de dessin du chart Replay (KLineCharts).

  const onSaved = (session_id, name) => {
    page.setSessionId(session_id);
    page.setSessionName(name);
    setJournalKey(k => k + 1);
    setSessionsKey(k => k + 1);
  };

  return (
    <div style={{ width: '100%', height: '100%', overflow: 'auto', display: 'flex', flexDirection: 'column' }} className="scroll">
      <PageHeader title="Backtester" />
      <div style={{ padding: '18px 24px 40px', flex: 1, maxWidth: 1200, width: '100%', margin: '0 auto' }}>
        {/* Barre haute : actif + (TF + nouvelle session en mode replay) + onglets */}
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12, marginBottom: 16 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
            <BtAssetSelect value={assetId} onChange={setAssetId} />
            {tab === 'replay' && <BtTfButtons value={tf} onChange={setTf} />}
            {tab === 'replay' && (
              <button className="btn tap" onClick={page.newSession} title="Réinitialise le compte et repart d'un point de départ aléatoire dans l'historique">
                <span style={{ display: 'flex' }}>{Ico.refresh}</span> Nouvelle session
              </button>
            )}
          </div>
          <BtTabs tab={tab} setTab={setTab} />
        </div>

        {tab === 'replay' ? (
          <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 340px', gap: 16, alignItems: 'start' }}>
            {/* Colonne gauche : chart + replay */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
              <div className="card" style={{ padding: 12 }}>
                <div style={{ position: 'relative', height: '60vh', minHeight: 360 }}>
                  {loading
                    ? <BtLoading />
                    : loadError
                      ? <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--fg-3)', fontSize: 13 }}>Données indisponibles pour cet actif / timeframe.</div>
                      : <BacktestAMChart bars={bt.visibleBars} timeframe={tf}
                                         assetId={assetId}
                                         assetLabel={(btAsset(assetId) || {}).label || assetId}
                                         theme={theme} positions={bt.positions}
                                         pendingOrders={bt.pendingOrders}
                                         closedTrades={bt.closedTrades} decimals={decimals} />}
                </div>
              </div>
              {!loading && !loadError && (
                <div className="card" style={{ padding: 14 }}>
                  <BtReplayBar bt={bt} tf={tf} />
                </div>
              )}
            </div>

            {/* Colonne droite : compte + ordre + attente + positions + stats + save */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
              <BtAccountStrip bt={bt} />
              <BtOrderPanel bt={bt} decimals={decimals} assetId={assetId} execCfg={page.execCfg} setExecCfg={page.setExecCfg} />
              <BtPendingList bt={bt} decimals={decimals} />
              <BtPositionsList bt={bt} decimals={decimals} />
              <BtSessionStatsCard bt={bt} />
              <BtSaveButton assetId={assetId} tf={tf} bt={bt} sessionId={page.sessionId} sessionName={page.sessionName} resetKey={page.resetKey} onSaved={onSaved} />
            </div>
          </div>
        ) : tab === 'sessions' ? (
          <BtSessionsPanel refreshKey={sessionsKey} onResume={(row) => { page.resumeSession(row); setTab('replay'); }} />
        ) : (
          <BtJournal refreshKey={journalKey} decimals={decimals} />
        )}
      </div>
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════════════
//  PAGE MOBILE (version simplifiée mais fonctionnelle)
// ═════════════════════════════════════════════════════════════════════════
function BacktestMobile({ state }) {
  const page = useBacktestPage();
  const { assetId, setAssetId, tf, setTf, loading, loadError, decimals, bt, markers } = page;
  const { theme } = (typeof useTheme === 'function') ? useTheme() : { theme: 'light' };
  const [tab, setTab] = React.useState('replay');
  const [journalKey, setJournalKey] = React.useState(0);
  const [sessionsKey, setSessionsKey] = React.useState(0);
  // Outils de dessin du chart Replay (KLineCharts).

  const onSaved = (session_id, name) => {
    page.setSessionId(session_id);
    page.setSessionName(name);
    setJournalKey(k => k + 1);
    setSessionsKey(k => k + 1);
  };

  return (
    <div style={{ padding: '14px 16px 28px' }}>
      <h1 style={{ fontSize: 24, fontWeight: 700, letterSpacing: '-.025em', margin: '0 0 14px' }}>Backtester</h1>

      <div style={{ display: 'flex', gap: 8, marginBottom: 12, overflowX: 'auto' }} className="scroll">
        <BtTabs tab={tab} setTab={setTab} />
      </div>

      {tab === 'replay' ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            <BtAssetSelect value={assetId} onChange={setAssetId} />
            <button className="btn tap" onClick={page.newSession} title="Nouvelle session"><span style={{ display: 'flex' }}>{Ico.refresh}</span></button>
          </div>
          <div style={{ overflowX: 'auto' }} className="scroll"><BtTfButtons value={tf} onChange={setTf} /></div>

          <div className="card" style={{ padding: 8 }}>
            <div style={{ position: 'relative', height: '42vh', minHeight: 260 }}>
              {loading
                ? <BtLoading />
                : loadError
                  ? <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--fg-3)', fontSize: 12.5 }}>Données indisponibles.</div>
                  : <BacktestAMChart bars={bt.visibleBars} timeframe={tf}
                                     assetId={assetId}
                                     assetLabel={(btAsset(assetId) || {}).label || assetId}
                                     theme={theme} positions={bt.positions}
                                     pendingOrders={bt.pendingOrders}
                                     closedTrades={bt.closedTrades} decimals={decimals} />}
            </div>
          </div>

          {!loading && !loadError && (
            <>
              <div className="card" style={{ padding: 12 }}><BtReplayBar bt={bt} tf={tf} /></div>
              <BtAccountStrip bt={bt} />
              <BtOrderPanel bt={bt} decimals={decimals} assetId={assetId} execCfg={page.execCfg} setExecCfg={page.setExecCfg} />
              <BtPendingList bt={bt} decimals={decimals} />
              <BtPositionsList bt={bt} decimals={decimals} />
              <BtSessionStatsCard bt={bt} />
              <BtSaveButton assetId={assetId} tf={tf} bt={bt} sessionId={page.sessionId} sessionName={page.sessionName} resetKey={page.resetKey} onSaved={onSaved} />
            </>
          )}
        </div>
      ) : tab === 'sessions' ? (
        <BtSessionsPanel refreshKey={sessionsKey} onResume={(row) => { page.resumeSession(row); setTab('replay'); }} />
      ) : (
        <BtJournal refreshKey={journalKey} decimals={decimals} />
      )}
    </div>
  );
}

Object.assign(window, { BacktestPage, BacktestMobile });
