// Tempo — Backtester (mode Replay) : chart amCharts 5 Stock Chart
// ─────────────────────────────────────────────────────────────────────────
// Remplace BacktestKLChart (KLineCharts) pour le mode « Replay & trades ».
// amCharts 5 Stock Chart apporte une barre d'outils PRO intégrée : DESSIN
// (trendlines, fibonacci, rectangles, horizontale, texte…), INDICATEURS
// (moyennes, RSI, MACD, Bollinger…) et choix du TYPE DE SÉRIE. On conserve :
// le replay bougie-par-bougie (setAll), les lignes d'ordres (entrée / SL / TP),
// le thème clair/sombre, la locale FR.
//
// CDN à charger AVANT ce fichier (dans cet ordre), voir notes d'intégration :
//   https://cdn.amcharts.com/lib/5/index.js
//   https://cdn.amcharts.com/lib/5/xy.js
//   https://cdn.amcharts.com/lib/5/stock.js
//   https://cdn.amcharts.com/lib/5/themes/Animated.js
//   https://cdn.amcharts.com/lib/5/themes/Dark.js
//   https://cdn.amcharts.com/lib/5/locales/fr_FR.js
// → exposent : am5, am5xy, am5stock, am5themes_Animated, am5themes_Dark,
//   am5locales_fr_FR.
//
// Composant exposé (DROP-IN du chart Replay) :
//   BacktestAMChart({ bars, timeframe, assetLabel, assetId, theme, positions,
//                     pendingOrders, closedTrades, decimals })
//
// Props (moteur v2) :
//   bars          : bougies visibles { time:s, open, high, low, close, volume }
//   timeframe     : '1m'…'1d' → baseInterval (mis à jour SANS recréer le chart)
//   assetLabel    : libellé de l'actif (nom de la série)
//   assetId       : identifiant STABLE de l'actif — quand il change, le chart
//                   est détruit puis recréé proprement (root.dispose + toolbar
//                   vidée). Si absent, assetLabel sert de clé de secours.
//   theme         : 'light' | 'dark' → recréation du chart (thème amCharts)
//   positions     : positions OUVERTES [{id, side, qty, entry, sl, tp}] →
//                   lignes de niveaux (entrée bleue continue, SL rouge, TP vert)
//   pendingOrders : ordres EN ATTENTE [{id, type:'limit'|'stop', side, qty, price}]
//                   → ligne bleue pointillée + label « Limite »/« Stop »
//   closedTrades  : trades clôturés [{side, qty, entry, exit, entryTime,
//                   exitTime, pnl, …}] → marqueurs discrets (triangle à
//                   l'entrée, point à la sortie, tooltip side/qty/P&L)
//   decimals      : décimales de l'actif (numberFormat + labels de niveaux)
// ─────────────────────────────────────────────────────────────────────────

// ── Lecture des CSS vars du thème ─────────────────────────────────────────
function _amCssColor(name, fallback) {
  try {
    const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
    return v || fallback;
  } catch (e) {
    return fallback;
  }
}

// Palette du chart dérivée des CSS vars (re-calculée à chaque (re)thème).
function _amPalette() {
  return {
    up:     _amCssColor('--green', '#16a34a'),
    down:   _amCssColor('--red', '#dc2626'),
    blue:   _amCssColor('--blue', '#3b82f6'),
    text:   _amCssColor('--fg-3', '#64748b'),
    grid:   _amCssColor('--line', '#e2e8f0'),
    bg:     _amCssColor('--bg-card', '#ffffff'),
  };
}

// Convertit nos bougies { time:s } → format amCharts { Date:ms, OHLCV }.
function _amConvert(bars) {
  if (!Array.isArray(bars)) return [];
  const out = [];
  for (const b of bars) {
    if (!b || !Number.isFinite(b.time)) continue;
    out.push({
      Date:   b.time * 1000,
      Open:   b.open,
      High:   b.high,
      Low:    b.low,
      Close:  b.close,
      Volume: b.volume != null ? b.volume : 0,
    });
  }
  return out;
}

// Fenêtre d'affichage façon FX Replay : on n'envoie pas les milliers de bougies
// de l'historique d'un coup (chart écrasé, inutilisable), mais une queue récente,
// et on zoome sur les ~BT_AM_WINDOW dernières — le reste reste pannable/zoomable.
const BT_AM_KEEP = 1500;   // bougies envoyées au chart (contexte pour paner en arrière)
const BT_AM_WINDOW = 160;  // bougies visibles par défaut

function _amSetData(series, dateAxis, bars) {
  if (!series) return;
  const full = _amConvert(bars);
  const data = full.length > BT_AM_KEEP ? full.slice(full.length - BT_AM_KEEP) : full;
  try { series.data.setAll(data); } catch (e) {}
  const applyZoom = () => {
    try {
      const n = data.length;
      if (!dateAxis) return;
      if (n > BT_AM_WINDOW) dateAxis.zoom((n - BT_AM_WINDOW) / n, 1, 0);
      else dateAxis.zoom(0, 1, 0);
    } catch (e) {}
  };
  applyZoom();
  // L'extent de l'axe se recalcule après la validation des données : on ré-applique
  // le zoom au frame suivant pour être sûr que la fenêtre récente est bien cadrée.
  try { setTimeout(applyZoom, 0); } catch (e) {}
}

// timeframe ('5m','15m','30m','1h','1d') → baseInterval amCharts.
function _amBaseInterval(timeframe) {
  switch (timeframe) {
    case '1m':  return { timeUnit: 'minute', count: 1 };
    case '3m':  return { timeUnit: 'minute', count: 3 };
    case '5m':  return { timeUnit: 'minute', count: 5 };
    case '15m': return { timeUnit: 'minute', count: 15 };
    case '30m': return { timeUnit: 'minute', count: 30 };
    case '1h':  return { timeUnit: 'hour',   count: 1 };
    case '4h':  return { timeUnit: 'hour',   count: 4 };
    case '1d':  return { timeUnit: 'day',    count: 1 };
    default:    return { timeUnit: 'minute', count: 1 };
  }
}

// numberFormat amCharts à partir du nombre de décimales de l'actif.
function _amNumberFormat(decimals) {
  const d = Number.isFinite(decimals) ? decimals : 5;
  if (d <= 0) return '#,###';
  return '#,###.' + '0'.repeat(d);
}

function BacktestAMChart({ bars, timeframe, assetLabel, assetId, theme, positions, pendingOrders, closedTrades, decimals }) {
  const chartDivRef = React.useRef(null);
  const toolbarDivRef = React.useRef(null);

  // Références persistantes vers les objets amCharts utiles aux mises à jour.
  const rootRef = React.useRef(null);
  const valueSeriesRef = React.useRef(null);
  const valueAxisRef = React.useRef(null);
  const dateAxisRef = React.useRef(null);
  const markerSeriesRef = React.useRef(null); // série « fantôme » des marqueurs de trades
  const orderRangesRef = React.useRef([]); // axisRanges des lignes de niveaux
  const lastBarsRef = React.useRef(null);

  const amMissing = typeof window === 'undefined' || !window.am5 || !window.am5stock || !window.am5xy;

  // Clé de recréation liée à l'ACTIF : changer d'actif détruit/recrée le chart
  // (dessins, indicateurs et zoom repartent de zéro — c'est voulu, l'échelle de
  // prix et l'historique n'ont plus rien à voir). assetLabel en secours.
  const assetKey = assetId != null ? assetId : assetLabel;

  // ── Création / re-création du chart (montage + thème + changement d'actif) ─
  React.useEffect(() => {
    if (amMissing || !chartDivRef.current || !toolbarDivRef.current) return;
    const toolbarEl = toolbarDivRef.current; // capturé pour un cleanup fiable
    const am5 = window.am5, am5xy = window.am5xy, am5stock = window.am5stock;
    const pal = _amPalette();
    const isDark = theme === 'dark' ||
      (typeof document !== 'undefined' && document.documentElement.getAttribute('data-theme') === 'dark');

    const root = am5.Root.new(chartDivRef.current);
    rootRef.current = root;
    try { root._logo && root._logo.dispose(); } catch (e) {}

    const themes = [window.am5themes_Animated.new(root)];
    if (isDark && window.am5themes_Dark) themes.push(window.am5themes_Dark.new(root));
    root.setThemes(themes);
    if (window.am5locales_fr_FR) root.locale = window.am5locales_fr_FR;

    const stockChart = root.container.children.push(am5stock.StockChart.new(root, {}));

    const mainPanel = stockChart.panels.push(am5stock.StockPanel.new(root, {
      wheelY: 'zoomX', panX: true, panY: true,
    }));

    const valueAxis = mainPanel.yAxes.push(am5xy.ValueAxis.new(root, {
      renderer: am5xy.AxisRendererY.new(root, { pan: 'zoom' }),
      tooltip: am5.Tooltip.new(root, {}),
      numberFormat: _amNumberFormat(decimals),
      extraTooltipPrecision: 2,
    }));
    valueAxisRef.current = valueAxis;

    const dateAxis = mainPanel.xAxes.push(am5xy.GaplessDateAxis.new(root, {
      baseInterval: _amBaseInterval(timeframe),
      renderer: am5xy.AxisRendererX.new(root, { pan: 'zoom', minorGridEnabled: true }),
      tooltip: am5.Tooltip.new(root, {}),
    }));
    dateAxisRef.current = dateAxis;

    const valueSeries = mainPanel.series.push(am5xy.CandlestickSeries.new(root, {
      name: assetLabel || 'Actif',
      clustered: false,
      valueXField: 'Date',
      valueYField: 'Close',
      highValueYField: 'High',
      lowValueYField: 'Low',
      openValueYField: 'Open',
      calculateAggregates: true,
      xAxis: dateAxis,
      yAxis: valueAxis,
      legendValueText: 'O {openValueY} H {highValueY} L {lowValueY} C {valueY}',
    }));
    valueSeriesRef.current = valueSeries;

    // Couleurs bougies hausse/baisse depuis les CSS vars du thème.
    try {
      const upC = am5.color(pal.up), downC = am5.color(pal.down);
      valueSeries.columns.template.states.create('riseFromOpen', { fill: upC, stroke: upC });
      valueSeries.columns.template.states.create('dropFromOpen', { fill: downC, stroke: downC });
    } catch (e) {}

    stockChart.set('stockSeries', valueSeries);

    // Série « fantôme » des marqueurs de trades : LineSeries invisible
    // (strokeOpacity 0) dont les bullets matérialisent les entrées (triangle
    // vert long / rouge short) et les sorties (point cerclé) des trades clôturés.
    const markerSeries = mainPanel.series.push(am5xy.LineSeries.new(root, {
      xAxis: dateAxis, yAxis: valueAxis,
      valueXField: 'Date', valueYField: 'Value',
      stroke: am5.color(pal.blue),
    }));
    try { markerSeries.strokes.template.setAll({ strokeOpacity: 0 }); } catch (e) {}
    markerSeries.bullets.push(function (r, s, dataItem) {
      const ctx = (dataItem && dataItem.dataContext) || {};
      let sprite;
      try {
        const color = am5.color(ctx.color || pal.blue);
        sprite = ctx.kind === 'entry'
          // Entrée : triangle pointé vers le haut (long) ou le bas (short).
          ? am5.Triangle.new(root, {
              width: 9, height: 8, fill: color,
              rotation: ctx.dir === 'down' ? 180 : 0,
              tooltipText: ctx.tip || '',
            })
          // Sortie : point discret cerclé de la couleur du P&L.
          : am5.Circle.new(root, {
              radius: 3.5, fill: am5.color(pal.bg),
              stroke: color, strokeWidth: 1.5,
              tooltipText: ctx.tip || '',
            });
      } catch (e) { return undefined; }
      return am5.Bullet.new(root, { sprite });
    });
    markerSeriesRef.current = markerSeries;

    const valueLegend = mainPanel.plotContainer.children.push(am5stock.StockLegend.new(root, { stockChart }));
    valueLegend.data.setAll([valueSeries]);

    // Curseur (crosshair) en mode 'none' → le glissement PANE le chart (pas de zoom-sélection).
    mainPanel.set('cursor', am5xy.XYCursor.new(root, {
      behavior: 'none', yAxis: valueAxis, xAxis: dateAxis, snapToSeries: [valueSeries],
    }));

    // Barre de défilement horizontale (façon TradingView) : navigation fiable dans
    // le temps — on la glisse pour se déplacer, on tire ses bords pour zoomer.
    try {
      mainPanel.set('scrollbarX', am5.Scrollbar.new(root, { orientation: 'horizontal' }));
    } catch (e) {}

    // Barre d'outils PRO : indicateurs + dessin + type de série.
    am5stock.StockToolbar.new(root, {
      container: toolbarDivRef.current,
      stockChart,
      controls: [
        am5stock.IndicatorControl.new(root, { stockChart, legend: valueLegend }),
        am5stock.DrawingControl.new(root, { stockChart }),
      ],
    });

    // Données initiales : fenêtre récente (pas toutes les bougies) + niveaux + marqueurs.
    const initBars = lastBarsRef.current != null ? lastBarsRef.current : bars;
    _amSetData(valueSeries, dateAxis, initBars);
    _amDrawLevels(valueAxis, root, orderRangesRef, positions, pendingOrders, decimals);
    _amUpdateTradeMarkers(markerSeries, initBars, closedTrades, decimals);

    return () => {
      // root.dispose() libère TOUT ce qui a été créé sur ce root : chart,
      // séries (bougies + marqueurs), axisRanges, curseur, scrollbar et toolbar.
      try { root.dispose(); } catch (e) {}
      // amCharts injecte le DOM de la StockToolbar dans le conteneur externe :
      // on le vide à la main pour éviter l'empilement de toolbars au
      // re-thème / changement d'actif.
      try { toolbarEl.innerHTML = ''; } catch (e) {}
      rootRef.current = null;
      valueSeriesRef.current = null;
      valueAxisRef.current = null;
      dateAxisRef.current = null;
      markerSeriesRef.current = null;
      orderRangesRef.current = []; // les ranges appartiennent au root disposé
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [amMissing, theme, assetKey]);

  // ── baseInterval quand le timeframe change (sans re-créer le chart) ───────
  React.useEffect(() => {
    const series = valueSeriesRef.current;
    if (!series) return;
    try {
      const dateAxis = series.get('xAxis');
      if (dateAxis) dateAxis.set('baseInterval', _amBaseInterval(timeframe));
    } catch (e) {}
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [timeframe]);

  // ── numberFormat quand l'actif (decimals) change ──────────────────────────
  React.useEffect(() => {
    const axis = valueAxisRef.current;
    if (!axis) return;
    try { axis.set('numberFormat', _amNumberFormat(decimals)); } catch (e) {}
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [decimals]);

  // ── Données du replay (visibleBars) : setAll à chaque changement ──────────
  React.useEffect(() => {
    lastBarsRef.current = bars;
    const series = valueSeriesRef.current;
    if (!series) return;
    _amSetData(series, dateAxisRef.current, bars);
    // La fenêtre envoyée au chart a bougé → on re-filtre les marqueurs visibles.
    _amUpdateTradeMarkers(markerSeriesRef.current, bars, closedTrades, decimals);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [bars]);

  // ── Lignes de niveaux (entrée / SL / TP / ordres en attente) ──────────────
  // Recréées au diff via axisRanges, SANS recréer le chart.
  React.useEffect(() => {
    if (!valueAxisRef.current || !rootRef.current) return;
    _amDrawLevels(valueAxisRef.current, rootRef.current, orderRangesRef, positions, pendingOrders, decimals);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [positions, pendingOrders, decimals]);

  // ── Marqueurs de trades : mis à jour quand un trade se clôture ────────────
  // (une clôture manuelle ne fait pas forcément avancer le playhead → l'effet
  // « bars » ci-dessus ne suffit pas).
  React.useEffect(() => {
    _amUpdateTradeMarkers(markerSeriesRef.current, lastBarsRef.current, closedTrades, decimals);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [closedTrades, decimals]);

  if (amMissing) {
    return (
      <div style={{
        width: '100%', height: '100%', minHeight: 280,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        background: 'var(--bg-soft)', borderRadius: 12, border: '1px solid var(--line)',
        color: 'var(--fg-3)', fontSize: 13,
      }}>
        Chart en cours de chargement…
      </div>
    );
  }

  return (
    <div style={{ width: '100%', height: '100%', minHeight: 280, display: 'flex', flexDirection: 'column' }}>
      {/* Barre d'outils amCharts (dessin + indicateurs + type de série) */}
      <div ref={toolbarDivRef} style={{ width: '100%' }} />
      {/* Chart */}
      <div ref={chartDivRef} style={{ width: '100%', flex: 1, minHeight: 240 }} />
    </div>
  );
}

// ── Format prix commun (labels de niveaux + tooltips de marqueurs) ────────
function _amFmtPrice(v, decimals) {
  const n = Number(v);
  if (!isFinite(n)) return '';
  try { return n.toFixed(decimals != null ? decimals : 5); } catch (e) { return String(v); }
}

// ── Lignes de niveaux (axisRanges) ────────────────────────────────────────
// Supprime les ranges précédentes (dispose → pas de fuite amCharts), puis
// recrée une ligne horizontale par niveau :
//   • entrée de position : bleu CONTINU, label side + qty
//   • SL : rouge tireté / TP : vert tireté
//   • ordre en attente  : bleu POINTILLÉ, label « Limite »/« Stop »
// NE PAS confondre avec les dessins utilisateur (gérés par DrawingControl).
function _amDrawLevels(valueAxis, root, orderRangesRef, positions, pendingOrders, decimals) {
  if (!valueAxis || !root) return;
  const am5 = window.am5;
  const pal = _amPalette();

  // Supprime les anciennes ranges de niveaux (refs gardées dans orderRangesRef).
  const prev = orderRangesRef.current || [];
  for (const r of prev) {
    try { valueAxis.axisRanges.removeValue(r); } catch (e) {}
    try { r.get('grid') && r.get('grid').dispose(); } catch (e) {}
    try { r.get('label') && r.get('label').dispose(); } catch (e) {}
    try { r.dispose(); } catch (e) {}
  }
  orderRangesRef.current = [];

  const fmt = (v) => _amFmtPrice(v, decimals);

  // dash : null → ligne continue, sinon tableau strokeDasharray.
  const addLine = (price, hex, label, dash) => {
    if (price == null || !isFinite(Number(price))) return;
    let range;
    try {
      range = valueAxis.createAxisRange(valueAxis.makeDataItem({ value: Number(price) }));
    } catch (e) { return; }
    if (!range) return;
    try {
      const color = am5.color(hex);
      const grid = range.get('grid');
      if (grid) {
        const gset = { stroke: color, strokeWidth: 1.5, strokeOpacity: 1, location: 1 };
        if (dash) gset.strokeDasharray = dash;
        grid.setAll(gset);
      }
      const lbl = range.get('label');
      if (lbl) {
        lbl.setAll({
          text: label, inside: true, centerY: am5.p50,
          fontSize: 11, fill: am5.color('#ffffff'),
          background: am5.RoundedRectangle.new(root, {
            fill: color, fillOpacity: 1,
          }),
          paddingLeft: 5, paddingRight: 5, paddingTop: 2, paddingBottom: 2,
        });
      }
    } catch (e) {}
    orderRangesRef.current.push(range);
  };

  // Positions ouvertes : entrée (continue) + SL/TP (tiretés).
  const list = Array.isArray(positions) ? positions : [];
  list.forEach((pos, i) => {
    const tag = list.length > 1 ? ' #' + (i + 1) : '';
    const sideTxt = pos.side === 'long' ? 'Achat' : 'Vente';
    addLine(pos.entry, pal.blue, sideTxt + ' ' + pos.qty + tag + ' ' + fmt(pos.entry), null);
    if (pos.sl != null) addLine(pos.sl, pal.down, 'SL' + tag + ' ' + fmt(pos.sl), [3, 3]);
    if (pos.tp != null) addLine(pos.tp, pal.up, 'TP' + tag + ' ' + fmt(pos.tp), [3, 3]);
  });

  // Ordres en attente : bleu pointillé, label type + side + qty.
  const pend = Array.isArray(pendingOrders) ? pendingOrders : [];
  pend.forEach((o) => {
    if (!o) return;
    const typeTxt = o.type === 'stop' ? 'Stop' : 'Limite';
    const sideTxt = o.side === 'long' ? 'Achat' : 'Vente';
    addLine(o.price, pal.blue, typeTxt + ' ' + sideTxt + ' ' + o.qty + ' ' + fmt(o.price), [2, 4]);
  });
}

// ── Marqueurs de trades (bullets sur la série fantôme) ────────────────────
// Alimente la série des marqueurs avec les entrées/sorties des trades CLÔTURÉS
// dont le timestamp tombe dans la fenêtre de données envoyée au chart (même
// slice que _amSetData, cf. BT_AM_KEEP) — les Date correspondent aux bougies
// existantes, donc aucun point parasite sur le GaplessDateAxis.
// Performance : seuls les BT_AM_MAX_TRADES derniers trades sont dessinés
// (2 marqueurs par trade → ~200 max), le surplus est ignoré.
const BT_AM_MAX_TRADES = 100;

function _amUpdateTradeMarkers(markerSeries, bars, closedTrades, decimals) {
  if (!markerSeries) return;
  const trades = Array.isArray(closedTrades) ? closedTrades : [];
  const src = Array.isArray(bars) ? bars : [];
  const windowBars = src.length > BT_AM_KEEP ? src.slice(src.length - BT_AM_KEEP) : src;
  if (!windowBars.length || !trades.length) {
    try { markerSeries.data.setAll([]); } catch (e) {}
    return;
  }
  const firstMs = windowBars[0].time * 1000;
  const lastMs = windowBars[windowBars.length - 1].time * 1000;
  const pal = _amPalette();
  const recent = trades.length > BT_AM_MAX_TRADES ? trades.slice(trades.length - BT_AM_MAX_TRADES) : trades;

  const data = [];
  for (const t of recent) {
    if (!t) continue;
    const sideTxt = t.side === 'long' ? 'Achat' : 'Vente';
    const pnl = Number(t.pnl);
    const pnlTxt = isFinite(pnl) ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) : '—';
    const entryMs = Number(t.entryTime) * 1000;
    const exitMs = Number(t.exitTime) * 1000;
    // Entrée : triangle (haut = long / bas = short), couleur du side.
    if (isFinite(entryMs) && entryMs >= firstMs && entryMs <= lastMs && isFinite(Number(t.entry))) {
      data.push({
        Date: entryMs, Value: Number(t.entry), kind: 'entry',
        dir: t.side === 'long' ? 'up' : 'down',
        color: t.side === 'long' ? pal.up : pal.down,
        tip: 'Entrée ' + sideTxt + ' ' + t.qty + '\n' + _amFmtPrice(t.entry, decimals),
      });
    }
    // Sortie : point cerclé, couleur du P&L.
    if (isFinite(exitMs) && exitMs >= firstMs && exitMs <= lastMs && isFinite(Number(t.exit))) {
      data.push({
        Date: exitMs, Value: Number(t.exit), kind: 'exit',
        color: (isFinite(pnl) && pnl < 0) ? pal.down : pal.up,
        tip: 'Sortie ' + sideTxt + ' ' + t.qty + '\nP&L ' + pnlTxt,
      });
    }
  }
  // amCharts exige des données triées par Date sur un DateAxis.
  data.sort((a, b) => a.Date - b.Date);
  try { markerSeries.data.setAll(data); } catch (e) {}
}

Object.assign(window, { BacktestAMChart });
