// TradingView Lightweight Charts wrapper.
// Owns chart lifecycle, data sync, toolbar, trade markers, and loading state.
// Props (read from TerminalVariant):
//   - priceRows:        array of {ts, open, high, low, close, volume} (strings or numbers accepted)
//   - indicatorsByPane: {price: [...], subpane_1: [...], subpane_2: [...]}
//   - trades:           array of trade dicts (see spec §4.6)
//   - fetchStatus:      'idle' | 'loading' | 'slow-loading'
//   - fetchContext:     {mode, symbol, timeframe, rangeLabel, strategy}  — used in the slow-loading overlay
//                       mode 'preview' shows "LOADING BARS · SYM/TF · RANGE"
//                       mode 'run'     shows "RUNNING BACKTEST · STRATEGY"
//   - dateRange:        [startISO, endISO]
//   - onRangeChange:    (newDateRange) => void
//   - theme:            object of color tokens {fg, bg, bg2, line, dim, green, amber, red}
//
// TODO(tech-debt): decompose into:
//   - trading-chart/chart-core.jsx (main chart lifecycle + panes)
//   - trading-chart/chart-toolbar.jsx (RANGE presets + OPTIONS dropdown)
//   - trading-chart/chart-primitives.jsx (VerticalLinePrimitive + related)
//   - trading-chart/chart-loader.jsx (adaptive progress bar + overlay)
// Current file is ~1000 lines; split when any pane type or primitive is added.

// Accept OHLCV as either string (legacy run payload from bootstrap.py) or number
// (new /api/bars payload). LWC needs numbers.
const toNum = (v) => (v === null || v === undefined ? null : typeof v === 'number' ? v : parseFloat(v));

// LWC time format: UNIX seconds for intraday, business-day object for daily.
// We use UNIX seconds uniformly — it's valid for all timeframes.
const toUnixSec = (iso) => Math.floor(new Date(iso).getTime() / 1000);

// Map [{ts, open, high, low, close}] -> LWC candlestick rows.
const mapCandles = (rows) =>
  (rows || [])
    .map((r) => ({
      time: toUnixSec(r.ts),
      open: toNum(r.open),
      high: toNum(r.high),
      low: toNum(r.low),
      close: toNum(r.close),
    }))
    .filter(
      (r) =>
        Number.isFinite(r.time) &&
        Number.isFinite(r.open) &&
        Number.isFinite(r.high) &&
        Number.isFinite(r.low) &&
        Number.isFinite(r.close)
    );

// Heikin-Ashi candle conversion. Each HA candle depends on the previous HA candle's open+close.
const toHeikinAshi = (candles) => {
  const out = [];
  let prev = null;
  for (const c of candles) {
    const haClose = (c.open + c.high + c.low + c.close) / 4;
    const haOpen = prev ? (prev.open + prev.close) / 2 : (c.open + c.close) / 2;
    const haHigh = Math.max(c.high, haOpen, haClose);
    const haLow = Math.min(c.low, haOpen, haClose);
    const ha = { time: c.time, open: haOpen, high: haHigh, low: haLow, close: haClose };
    out.push(ha);
    prev = ha;
  }
  return out;
};

// Map ts-aligned series (indicator values array) -> LWC line series rows, skipping nulls.
const mapIndicatorLine = (rows, values) =>
  (rows || [])
    .map((r, i) => ({ time: toUnixSec(r.ts), value: toNum(values[i]) }))
    .filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value));

// Expand one trade into two events (entry + exit) with label and color.
// Trade payload shape (from bootstrap.py): {id, side, entry_ts, exit_ts, entry, exit, pnl_pct, ...}
// side is serialized as "buy" / "sell" (OrderSide enum values) — treat "buy" as long, "sell" as short.
const expandTradeEvents = (trade) => {
  const isLong = window.ChartUtils.isLongSide(trade.side || 'buy');
  const entryPx = toNum(trade.entry);
  const exitPx = toNum(trade.exit);
  const pnlPct = toNum(trade.pnl_pct);

  const entry = {
    type: 'entry',
    isLong,
    time: toUnixSec(trade.entry_ts),
    price: entryPx,
    label: `${isLong ? 'BUY' : 'SELL'} ${window.ChartUtils.fmt(entryPx, 2)}`,
    color: isLong ? 'green' : 'red',
  };
  const exit = {
    type: 'exit',
    isLong,
    time: toUnixSec(trade.exit_ts),
    price: exitPx,
    // pnl_pct from trade.py is already in percent form (e.g. 34.38 = 34.38%), not a fraction.
    label:
      Number.isFinite(pnlPct)
        ? `${isLong ? 'SELL' : 'BUY'} ${pnlPct >= 0 ? '+' : ''}${window.ChartUtils.fmt(Math.abs(pnlPct), 2)}%`
        : isLong ? 'SELL' : 'BUY',
    color: Number.isFinite(pnlPct) && pnlPct < 0 ? 'red' : 'green',
  };
  return [entry, exit];
};

// Full-height dashed vertical line primitive for LWC v5 ISeriesPrimitive API.
// Attached to a series; draws on that series' pane at a given timestamp.
class VerticalLinePrimitive {
  constructor(options) {
    this._options = options; // { time, color, lineWidth, dashed }
    this._chart = null;
    this._paneViews = [new VerticalLinePaneView(this)];
  }
  attached({ chart }) {
    this._chart = chart;
  }
  detached() {
    this._chart = null;
  }
  updateAllViews() {
    for (const v of this._paneViews) v.update();
  }
  paneViews() {
    return this._paneViews;
  }
}

class VerticalLinePaneView {
  constructor(source) {
    this._source = source;
    this._x = null;
    // Cache renderer to avoid allocating a new instance on every repaint (60fps × N primitives).
    this._renderer = new VerticalLineRenderer(source, null);
  }
  update() {
    const src = this._source;
    if (!src._chart) { this._x = null; this._renderer._x = null; return; }
    const ts = src._chart.timeScale();
    this._x = ts.timeToCoordinate(src._options.time);
    this._renderer._x = this._x;
  }
  renderer() {
    return this._renderer;
  }
}

class VerticalLineRenderer {
  constructor(source, x) {
    this._source = source;
    this._x = x;
  }
  draw(target) {
    if (this._x === null) return;
    const opts = this._source._options;
    target.useMediaCoordinateSpace((scope) => {
      const { context: ctx, mediaSize } = scope;
      ctx.save();
      ctx.strokeStyle = opts.color;
      ctx.lineWidth = opts.lineWidth || 1;
      if (opts.dashed) ctx.setLineDash([4, 3]);
      ctx.beginPath();
      ctx.moveTo(this._x, 0);
      ctx.lineTo(this._x, mediaSize.height);
      ctx.stroke();
      ctx.restore();
    });
  }
}

// Mode constants — avoid stringly-typed comparisons throughout this file and variant-terminal.jsx.
// Exposed via window.ChartConstants so variant-terminal.jsx can reference them without duplication.
const CHART_TYPE  = { CANDLE: 'candle', LINE: 'line', HEIKIN: 'heikin' };
const SCALE_MODE  = { LINEAR: 0, LOG: 1 };
const FETCH_STATUS = { IDLE: 'idle', LOADING: 'loading', SLOW: 'slow-loading' };
const FETCH_MODE  = { PREVIEW: 'preview', RUN: 'run' };
window.ChartConstants = { CHART_TYPE, SCALE_MODE, FETCH_STATUS, FETCH_MODE };

const RANGE_PRESETS = [
  { id: '1D', days: 1 },
  { id: '1W', days: 7 },
  { id: '1M', days: 30 },
  { id: '3M', days: 90 },
  { id: '6M', days: 180 },
  { id: '1Y', days: 365 },
];

const TradingChartToolbar = ({
  dateRange,
  onRangeChange,
  theme,
  openOptions,
  onOptionsToggle,
  onFit,
  onReset,
}) => {
  const [startISO, endISO] = dateRange || [null, null];
  const activePreset = React.useMemo(() => {
    if (!startISO || !endISO) return null;
    const ms = new Date(endISO).getTime() - new Date(startISO).getTime();
    const days = Math.round(ms / (1000 * 60 * 60 * 24));
    return RANGE_PRESETS.find((p) => p.days === days)?.id || null;
  }, [startISO, endISO]);

  const applyPreset = (days) => {
    if (!endISO) return;
    const end = new Date(endISO);
    const start = new Date(end);
    start.setUTCDate(start.getUTCDate() - days);
    onRangeChange([start.toISOString().slice(0, 10), endISO]);
  };

  return (
    <div
      className="tc-toolbar"
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 6,
        padding: '4px 8px',
        borderBottom: `1px solid ${theme.line}`,
        background: theme.bg2,
        fontSize: 10,
        fontFamily: 'JetBrains Mono, monospace',
      }}
    >
      <span style={{ color: theme.dim }}>RANGE:</span>
      {RANGE_PRESETS.map((p) => (
        <button
          key={p.id}
          onClick={() => applyPreset(p.days)}
          style={{
            padding: '2px 6px',
            border: `1px solid ${activePreset === p.id ? theme.green : theme.line}`,
            background: 'transparent',
            color: activePreset === p.id ? theme.green : theme.fg,
            fontFamily: 'inherit',
            fontSize: 'inherit',
            cursor: 'pointer',
          }}
        >
          {p.id}
        </button>
      ))}
      <span style={{ flex: 1 }} />
      <button
        onClick={onOptionsToggle}
        style={{
          padding: '2px 8px',
          border: `1px solid ${openOptions ? theme.green : theme.line}`,
          background: 'transparent',
          color: openOptions ? theme.green : theme.fg,
          fontFamily: 'inherit',
          fontSize: 'inherit',
          cursor: 'pointer',
        }}
      >
        ⚙ OPTIONS ▾
      </button>
      <button
        onClick={onFit}
        data-action="fit"
        style={{
          padding: '2px 8px',
          border: `1px solid ${theme.line}`,
          background: 'transparent',
          color: theme.fg,
          fontFamily: 'inherit',
          fontSize: 'inherit',
          cursor: 'pointer',
        }}
      >
        FIT
      </button>
      <button
        onClick={onReset}
        data-action="reset"
        style={{
          padding: '2px 8px',
          border: `1px solid ${theme.line}`,
          background: 'transparent',
          color: theme.fg,
          fontFamily: 'inherit',
          fontSize: 'inherit',
          cursor: 'pointer',
        }}
      >
        RESET
      </button>
    </div>
  );
};

const TradingChart = (props) => {
  const { dateRange, onRangeChange, theme } = props;
  const [openOptions, setOpenOptions] = React.useState(false);
  const [chartType, setChartType] = React.useState(CHART_TYPE.CANDLE); // candle | line | heikin
  const [scaleMode, setScaleMode] = React.useState(SCALE_MODE.LINEAR); // 0 Normal/Linear, 1 Log
  const [showVolume, setShowVolume] = React.useState(false);
  const [seriesVisibility, setSeriesVisibility] = React.useState({
    supertrend: true,
    crsi_fast: true,
    crsi_slow: true,
  });
  const [priceSeriesVersion, setPriceSeriesVersion] = React.useState(0);

  const mainContainerRef = React.useRef(null);
  const mainChartRef = React.useRef(null);
  const priceSeriesRef = React.useRef({ kind: null, series: null });
  const supertrendSeriesRef = React.useRef(null);
  const crsiFastSeriesRef = React.useRef(null);
  const crsiSlowSeriesRef = React.useRef(null);
  const crsiSlowLbSeriesRef = React.useRef(null);
  const crsiSlowUbSeriesRef = React.useRef(null);
  const crsiFastLbSeriesRef = React.useRef(null);
  const crsiFastUbSeriesRef = React.useRef(null);
  const crsiSignalsMarkersRef = React.useRef(null);

  const volumeSeriesRef = React.useRef(null);

  // v5 API: createSeriesMarkers returns a handle with setMarkers() and detach(); no series.setMarkers().
  const priceMarkersHandleRef = React.useRef(null);
  // Tracks { targetSeries, primitive } pairs for cleanup on re-render / unmount.
  const verticalPrimitivesRef = React.useRef([]);

  // Install (or swap) the price series for the given chart type.
  const installPriceSeries = (kind) => {
    const chart = mainChartRef.current;
    if (!chart) return;
    if (priceSeriesRef.current.series) {
      try { chart.removeSeries(priceSeriesRef.current.series); } catch (e) { console.warn('installPriceSeries: removeSeries failed:', e); }
      priceSeriesRef.current = { kind: null, series: null };
    }
    let s;
    if (kind === CHART_TYPE.CANDLE || kind === CHART_TYPE.HEIKIN) {
      s = chart.addSeries(window.LightweightCharts.CandlestickSeries, {
        upColor: theme.green,
        downColor: theme.red,
        borderUpColor: theme.green,
        borderDownColor: theme.red,
        wickUpColor: theme.green,
        wickDownColor: theme.red,
      });
    } else {
      s = chart.addSeries(window.LightweightCharts.LineSeries, {
        color: theme.green,
        lineWidth: 1,
        priceLineVisible: false,
        lastValueVisible: true,
      });
    }
    priceSeriesRef.current = { kind, series: s };
  };

  // Push price data for the currently installed series kind.
  const pushPriceData = () => {
    const { kind, series } = priceSeriesRef.current;
    if (!series) return;
    if (kind === CHART_TYPE.CANDLE) {
      series.setData(mapCandles(props.priceRows));
    } else if (kind === CHART_TYPE.HEIKIN) {
      series.setData(toHeikinAshi(mapCandles(props.priceRows)));
    } else {
      series.setData(
        (props.priceRows || [])
          .map((r) => ({ time: toUnixSec(r.ts), value: toNum(r.close) }))
          .filter((p) => Number.isFinite(p.time) && Number.isFinite(p.value))
      );
    }
  };

  // Create main chart on mount.
  React.useEffect(() => {
    const el = mainContainerRef.current;
    if (!el || !window.LightweightCharts) return;

    const chart = window.LightweightCharts.createChart(el, {
      layout: {
        background: { color: theme.bg },
        textColor: theme.fg,
        fontFamily: 'JetBrains Mono, monospace',
        fontSize: 11,
      },
      grid: {
        vertLines: { color: theme.line, style: 1 },
        horzLines: { color: theme.line, style: 1 },
      },
      rightPriceScale: { borderColor: theme.line },
      timeScale: { borderColor: theme.line, timeVisible: true, secondsVisible: false },
      crosshair: { mode: 1 }, // Magnet
      autoSize: true,
    });
    mainChartRef.current = chart;

    installPriceSeries(CHART_TYPE.CANDLE);

    supertrendSeriesRef.current = chart.addSeries(window.LightweightCharts.LineSeries, {
      color: theme.amber,
      lineWidth: 1,
      priceLineVisible: false,
      lastValueVisible: false,
    });

    // Pane 1: cRSI 0-200 pane. Both main lines in white (fg), red bands for cRSI1
    // (0-100, slow series), orange bands for cRSI2 (100-200, fast series).
    // addPane() with no args (v5 API expects optional PaneOptions, not int).
    chart.addPane();

    // Main lines — white so they stand out on the colored band background.
    crsiFastSeriesRef.current = chart.addSeries(
      window.LightweightCharts.LineSeries,
      { color: theme.fg, lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false },
      1,
    );
    crsiSlowSeriesRef.current = chart.addSeries(
      window.LightweightCharts.LineSeries,
      { color: theme.fg, lineWidth: 1.5, priceLineVisible: false, lastValueVisible: false },
      1,
    );

    // Red bands around cRSI1 slow series (0-100 scale).
    crsiSlowLbSeriesRef.current = chart.addSeries(
      window.LightweightCharts.LineSeries,
      { color: '#f32121', lineWidth: 1, priceLineVisible: false, lastValueVisible: false },
      1,
    );
    crsiSlowUbSeriesRef.current = chart.addSeries(
      window.LightweightCharts.LineSeries,
      { color: '#f32121', lineWidth: 1, priceLineVisible: false, lastValueVisible: false },
      1,
    );

    // Orange bands around cRSI2 fast series (100-200 scale, offset +100).
    crsiFastLbSeriesRef.current = chart.addSeries(
      window.LightweightCharts.LineSeries,
      { color: '#ffa500', lineWidth: 1, priceLineVisible: false, lastValueVisible: false },
      1,
    );
    crsiFastUbSeriesRef.current = chart.addSeries(
      window.LightweightCharts.LineSeries,
      { color: '#ffa500', lineWidth: 1, priceLineVisible: false, lastValueVisible: false },
      1,
    );

    // Approximate 65/35 split. Stretch factor is relative; sum is normalized by LWC.
    const panes = chart.panes();
    if (panes.length >= 2) {
      panes[0].setStretchFactor(65);
      panes[1].setStretchFactor(35);
    }

    // Lock cRSI pane Y-axis to exactly 0-200 (same trick Pine uses: force the
    // range regardless of data that may spike outside during warmup/edge cases).
    // Every series in pane 1 returns the same price range so the union equals
    // that range — no series can push the scale wider.
    const force0to200 = () => ({ priceRange: { minValue: 0, maxValue: 200 } });
    for (const ref of [
      crsiFastSeriesRef,
      crsiSlowSeriesRef,
      crsiSlowLbSeriesRef,
      crsiSlowUbSeriesRef,
      crsiFastLbSeriesRef,
      crsiFastUbSeriesRef,
    ]) {
      ref.current?.applyOptions({ autoscaleInfoProvider: force0to200 });
    }

    return () => {
      chart.remove();
      mainChartRef.current = null;
      priceSeriesRef.current = { kind: null, series: null };
      supertrendSeriesRef.current = null;
      crsiFastSeriesRef.current = null;
      crsiSlowSeriesRef.current = null;
      crsiSlowLbSeriesRef.current = null;
      crsiSlowUbSeriesRef.current = null;
      crsiFastLbSeriesRef.current = null;
      crsiFastUbSeriesRef.current = null;
      crsiSignalsMarkersRef.current = null;
      volumeSeriesRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // create once; theme changes handled via options update below

  // Swap price series when chart type changes.
  React.useEffect(() => {
    if (!mainChartRef.current) return;
    installPriceSeries(chartType);
    // Re-bind data for the new series kind.
    pushPriceData();
    // Bump the version so markers effect re-runs and re-attaches markers to new series.
    setPriceSeriesVersion((v) => v + 1);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [chartType]);

  // Apply theme to live chart when it changes.
  React.useEffect(() => {
    const chart = mainChartRef.current;
    if (!chart) return;
    chart.applyOptions({
      layout: { background: { color: theme.bg }, textColor: theme.fg },
      grid: {
        vertLines: { color: theme.line },
        horzLines: { color: theme.line },
      },
      rightPriceScale: { borderColor: theme.line },
      timeScale: { borderColor: theme.line },
    });
    const { kind, series: priceSeries } = priceSeriesRef.current;
    if (priceSeries) {
      if (kind === CHART_TYPE.CANDLE || kind === CHART_TYPE.HEIKIN) {
        priceSeries.applyOptions({
          upColor: theme.green,
          downColor: theme.red,
          borderUpColor: theme.green,
          borderDownColor: theme.red,
          wickUpColor: theme.green,
          wickDownColor: theme.red,
        });
      } else {
        priceSeries.applyOptions({ color: theme.green });
      }
    }
    supertrendSeriesRef.current?.applyOptions({ color: theme.amber });
    // cRSI main lines track theme foreground for visibility in both dark/bright modes.
    // Band series keep their fixed red/orange colors per the Pine Script spec.
    crsiFastSeriesRef.current?.applyOptions({ color: theme.fg });
    crsiSlowSeriesRef.current?.applyOptions({ color: theme.fg });

  }, [theme.bg, theme.fg, theme.line, theme.green, theme.red, theme.amber]);

  // Push price + Supertrend data whenever inputs change.
  React.useEffect(() => {
    const { series: priceSeries } = priceSeriesRef.current;
    const supertrendSeries = supertrendSeriesRef.current;
    if (!priceSeries || !supertrendSeries) return;

    pushPriceData();

    const supertrend = (props.indicatorsByPane?.price || []).find((i) => i.name === 'supertrend');
    // Bicolor Supertrend: green when line is below price (uptrend, line = lower band),
    // red when line is above price (downtrend, line = upper band). LWC v5 LineData
    // supports per-point `color` that overrides the series default for that segment.
    if (supertrend && supertrend.values && props.priceRows && props.priceRows.length) {
      const points = [];
      for (let i = 0; i < props.priceRows.length; i++) {
        const row = props.priceRows[i];
        const stVal = toNum(supertrend.values[i]);
        if (!Number.isFinite(stVal)) continue;
        const close = toNum(row.close);
        if (!Number.isFinite(close)) continue;
        points.push({
          time: toUnixSec(row.ts),
          value: stVal,
          color: stVal < close ? theme.green : theme.red,
        });
      }
      supertrendSeries.setData(points);
    } else {
      supertrendSeries.setData([]);
    }

    const subpane1 = props.indicatorsByPane?.subpane_1 || [];
    const crsiFast = subpane1.find((i) => i.name === 'crsi_fast');
    const crsiSlow = subpane1.find((i) => i.name === 'crsi_slow');
    crsiFastSeriesRef.current?.setData(
      crsiFast && crsiFast.values ? mapIndicatorLine(props.priceRows, crsiFast.values) : []
    );
    crsiSlowSeriesRef.current?.setData(
      crsiSlow && crsiSlow.values ? mapIndicatorLine(props.priceRows, crsiSlow.values) : []
    );

    // cRSI band series — fixed red/orange per Pine Script (no theme recolor).
    const slowLb = subpane1.find((i) => i.name === 'crsi_slow_lb');
    const slowUb = subpane1.find((i) => i.name === 'crsi_slow_ub');
    const fastLb = subpane1.find((i) => i.name === 'crsi_fast_lb');
    const fastUb = subpane1.find((i) => i.name === 'crsi_fast_ub');
    crsiSlowLbSeriesRef.current?.setData(
      slowLb?.values ? mapIndicatorLine(props.priceRows, slowLb.values) : []
    );
    crsiSlowUbSeriesRef.current?.setData(
      slowUb?.values ? mapIndicatorLine(props.priceRows, slowUb.values) : []
    );
    crsiFastLbSeriesRef.current?.setData(
      fastLb?.values ? mapIndicatorLine(props.priceRows, fastLb.values) : []
    );
    crsiFastUbSeriesRef.current?.setData(
      fastUb?.values ? mapIndicatorLine(props.priceRows, fastUb.values) : []
    );

    if (props.priceRows && props.priceRows.length > 0) {
      mainChartRef.current?.timeScale().fitContent();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [props.priceRows, props.indicatorsByPane, theme.green, theme.red]);

  // Render BOTH/SLOW signal markers on the cRSI slow series (subpane_1).
  // White arrow = BOTH (both series fired), maroon arrow = SLOW (slow series only).
  React.useEffect(() => {
    const crsiSlowSeries = crsiSlowSeriesRef.current;
    if (!crsiSlowSeries) return;

    // Detach previous marker handle before re-rendering.
    if (crsiSignalsMarkersRef.current) {
      try {
        crsiSignalsMarkersRef.current.detach();
      } catch (_) {
        // ignore — series may have been removed
      }
      crsiSignalsMarkersRef.current = null;
    }

    const signals = (props.indicatorsByPane?.subpane_1 || []).find(
      (i) => i.name === 'crsi_signals',
    );
    const raw = signals?.markers || [];
    if (!raw.length) return;

    const lwc = raw.map((m) => {
      const isLong = m.kind.endsWith('_long');
      const isBoth = m.kind.startsWith('both');
      return {
        time: toUnixSec(m.ts),
        position: isLong ? 'belowBar' : 'aboveBar',
        shape: isLong ? 'arrowUp' : 'arrowDown',
        // White for BOTH (visible on both dark/bright), maroon for SLOW-only signals.
        color: isBoth ? '#ffffff' : '#8b0000',
        text: isBoth ? 'BOTH' : 'SLOW',
      };
    });
    crsiSignalsMarkersRef.current = window.LightweightCharts.createSeriesMarkers(
      crsiSlowSeries,
      lwc,
    );
  }, [props.indicatorsByPane]);

  // Apply scale mode (linear/log) to pane 0 only (cRSI oscillator pane should stay linear).
  React.useEffect(() => {
    const chart = mainChartRef.current;
    if (!chart) return;
    const panes = chart.panes();
    // LWC v5: prefer per-pane priceScale; fall back to chart-level if API not available.
    if (panes[0] && typeof panes[0].priceScale === 'function') {
      panes[0].priceScale('right').applyOptions({ mode: scaleMode });
    } else {
      // Fallback: applies to all panes — acceptable until LWC v5 per-pane API is confirmed.
      // TODO(follow-up): confirm panes()[i].priceScale availability on runtime LWC version.
      chart.priceScale('right').applyOptions({ mode: scaleMode });
    }
  }, [scaleMode]);

  // Toggle volume histogram pane and refresh data when priceRows change while volume is on.
  React.useEffect(() => {
    const chart = mainChartRef.current;
    if (!chart) return;
    if (showVolume) {
      if (!volumeSeriesRef.current) {
        // addPane() with no args (v5 API takes optional PaneOptions, not an integer index).
        if (chart.panes().length < 3) chart.addPane();
        volumeSeriesRef.current = chart.addSeries(
          window.LightweightCharts.HistogramSeries,
          {
            color: theme.dim,
            priceFormat: { type: 'volume' },
            priceLineVisible: false,
            lastValueVisible: false,
          },
          2,
        );
        const panes = chart.panes();
        if (panes[2]) panes[2].setStretchFactor(20);
      }
      // Push/refresh data whether just created or pre-existing (handles priceRows changes).
      volumeSeriesRef.current.setData(
        (props.priceRows || []).map((r) => ({
          time: toUnixSec(r.ts),
          value: toNum(r.volume) || 0,
          color:
            toNum(r.close) >= toNum(r.open)
              ? `${theme.green}66`
              : `${theme.red}66`,
        }))
      );
    } else if (volumeSeriesRef.current) {
      try { chart.removeSeries(volumeSeriesRef.current); } catch (e) { console.warn('volume: removeSeries failed:', e); }
      volumeSeriesRef.current = null;
    }
  }, [showVolume, props.priceRows, theme.dim, theme.green, theme.red]);

  // Toggle series visibility (supertrend, cRSI fast/slow).
  React.useEffect(() => {
    supertrendSeriesRef.current?.applyOptions({ visible: seriesVisibility.supertrend });
    crsiFastSeriesRef.current?.applyOptions({ visible: seriesVisibility.crsi_fast });
    crsiSlowSeriesRef.current?.applyOptions({ visible: seriesVisibility.crsi_slow });
  }, [
    seriesVisibility.supertrend,
    seriesVisibility.crsi_fast,
    seriesVisibility.crsi_slow,
  ]);

  // Bind trade markers: full-height dashed vertical lines via LWC v5 ISeriesPrimitive API,
  // plus arrow markers on the price series via v5 createSeriesMarkers.
  React.useEffect(() => {
    const priceSeries = priceSeriesRef.current?.series;
    if (!mainChartRef.current || !priceSeries) return;

    // --- Detach previous vertical primitives ---
    for (const { targetSeries, primitive } of verticalPrimitivesRef.current) {
      try {
        targetSeries.detachPrimitive(primitive);
      } catch (e) {
        console.warn('trade markers: detachPrimitive failed:', e);
      }
    }
    verticalPrimitivesRef.current = [];

    // --- Detach previous arrow markers ---
    if (priceMarkersHandleRef.current) {
      try { priceMarkersHandleRef.current.detach(); }
      catch (e) { console.warn('trade markers: detach failed:', e); }
      priceMarkersHandleRef.current = null;
    }

    const trades = props.trades || [];
    if (!trades.length) return;

    const events = trades.flatMap(expandTradeEvents);

    // --- Attach vertical primitives (one per trade event per pane) ---
    // One representative series per pane is sufficient — the primitive draws on that pane's canvas.
    const verticalTargets = [
      { series: priceSeries, opacity: 1 },
      { series: crsiFastSeriesRef.current, opacity: 0.6 },
    ].filter((t) => t.series != null);

    for (const ev of events) {
      const baseColor = ev.color === 'green' ? theme.green : theme.red;
      for (const { series, opacity } of verticalTargets) {
        // Append hex alpha suffix for attenuated panes (60% opacity ≈ 0x99).
        const color = opacity < 1 ? `${baseColor}99` : baseColor;
        const primitive = new VerticalLinePrimitive({
          time: ev.time,
          color,
          lineWidth: 1,
          dashed: true,
        });
        try {
          series.attachPrimitive(primitive);
          verticalPrimitivesRef.current.push({ targetSeries: series, primitive });
        } catch (e) {
          console.warn('trade markers: attachPrimitive failed:', e);
        }
      }
    }

    // --- Arrow markers on price series (unchanged) ---
    const arrows = events.map((ev) => ({
      time: ev.time,
      position:
        ev.type === 'entry'
          ? ev.isLong ? 'belowBar' : 'aboveBar'
          : ev.isLong ? 'aboveBar' : 'belowBar',
      color: ev.color === 'green' ? theme.green : theme.red,
      shape:
        ev.type === 'entry'
          ? ev.isLong ? 'arrowUp' : 'arrowDown'
          : ev.isLong ? 'arrowDown' : 'arrowUp',
      text: ev.label,
    }));
    priceMarkersHandleRef.current = window.LightweightCharts.createSeriesMarkers(priceSeries, arrows);
  }, [props.trades, theme.green, theme.red, priceSeriesVersion]);

  return (
    <div
      className="tc-root"
      style={{
        display: 'grid',
        gridTemplateColumns: '1fr',
        gridTemplateRows: 'auto auto 1fr',
        gridTemplateAreas: '"toolbar" "options" "main"',
        gap: 0,
        height: '100%',
        minHeight: 0,
      }}
    >
      <div style={{ gridArea: 'toolbar' }}>
        <TradingChartToolbar
          dateRange={dateRange}
          onRangeChange={onRangeChange}
          theme={theme}
          openOptions={openOptions}
          onOptionsToggle={() => setOpenOptions((v) => !v)}
          onFit={() => mainChartRef.current?.timeScale().fitContent()}
          onReset={() => {
            mainChartRef.current?.timeScale().resetTimeScale();
            mainChartRef.current?.timeScale().fitContent();
          }}
        />
      </div>
      {openOptions && (
        <div
          style={{
            gridArea: 'options',
            padding: '6px 8px',
            background: theme.bg2,
            borderBottom: `1px solid ${theme.line}`,
            display: 'flex',
            gap: 18,
            flexWrap: 'wrap',
            fontFamily: 'JetBrains Mono, monospace',
            fontSize: 10,
            color: theme.fg,
          }}
        >
          <div>
            <span style={{ color: theme.dim }}>TYPE:</span>{' '}
            {[CHART_TYPE.CANDLE, CHART_TYPE.LINE, CHART_TYPE.HEIKIN].map((t) => (
              <button
                key={t}
                onClick={() => setChartType(t)}
                style={{
                  marginLeft: 4,
                  padding: '1px 6px',
                  border: `1px solid ${chartType === t ? theme.green : theme.line}`,
                  background: 'transparent',
                  color: chartType === t ? theme.green : theme.fg,
                  fontFamily: 'inherit',
                  fontSize: 'inherit',
                  cursor: 'pointer',
                }}
              >
                {t}
              </button>
            ))}
          </div>
          <div>
            <span style={{ color: theme.dim }}>SCALE:</span>{' '}
            {[{ id: SCALE_MODE.LINEAR, label: 'LIN' }, { id: SCALE_MODE.LOG, label: 'LOG' }].map((o) => (
              <button
                key={o.id}
                onClick={() => setScaleMode(o.id)}
                style={{
                  marginLeft: 4,
                  padding: '1px 6px',
                  border: `1px solid ${scaleMode === o.id ? theme.green : theme.line}`,
                  background: 'transparent',
                  color: scaleMode === o.id ? theme.green : theme.fg,
                  fontFamily: 'inherit',
                  fontSize: 'inherit',
                  cursor: 'pointer',
                }}
              >
                {o.label}
              </button>
            ))}
          </div>
          <div>
            <span style={{ color: theme.dim }}>VOLUME:</span>{' '}
            <button
              onClick={() => setShowVolume((v) => !v)}
              style={{
                marginLeft: 4,
                padding: '1px 6px',
                border: `1px solid ${showVolume ? theme.green : theme.line}`,
                background: 'transparent',
                color: showVolume ? theme.green : theme.fg,
                fontFamily: 'inherit',
                fontSize: 'inherit',
                cursor: 'pointer',
              }}
            >
              {showVolume ? 'ON' : 'OFF'}
            </button>
          </div>
          <div>
            <span style={{ color: theme.dim }}>SHOW:</span>{' '}
            {[
              { id: 'supertrend', label: 'ST' },
              { id: 'crsi_fast', label: 'cRSI F' },
              { id: 'crsi_slow', label: 'cRSI S' },
            ].map((o) => (
              <button
                key={o.id}
                onClick={() => setSeriesVisibility((prev) => ({ ...prev, [o.id]: !prev[o.id] }))}
                style={{
                  marginLeft: 4,
                  padding: '1px 6px',
                  border: `1px solid ${seriesVisibility[o.id] ? theme.green : theme.line}`,
                  background: 'transparent',
                  color: seriesVisibility[o.id] ? theme.green : theme.fg,
                  fontFamily: 'inherit',
                  fontSize: 'inherit',
                  cursor: 'pointer',
                }}
              >
                {o.label}
              </button>
            ))}
          </div>
        </div>
      )}
      <div
        style={{
          gridArea: 'main',
          borderRight: `1px solid ${theme.line}`,
          position: 'relative',
          minHeight: 400,
          background: theme.bg,
        }}
      >
        <div ref={mainContainerRef} style={{ position: 'absolute', inset: 0 }} />
        {props.fetchStatus !== FETCH_STATUS.IDLE && (
          <div
            style={{
              position: 'absolute',
              left: 0,
              right: 0,
              top: 0,
              height: 2,
              overflow: 'hidden',
              pointerEvents: 'none',
              zIndex: 5,
            }}
          >
            <div
              style={{
                width: '30%',
                height: '100%',
                background: `linear-gradient(90deg, transparent, ${theme.green}, transparent)`,
                animation: 'tc-progress 1.2s linear infinite',
              }}
            />
          </div>
        )}
        {props.fetchStatus === FETCH_STATUS.SLOW && (
          <div
            style={{
              position: 'absolute',
              inset: 0,
              background: 'rgba(5,7,10,0.4)',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              pointerEvents: 'none',
              zIndex: 10,
            }}
          >
            <div
              style={{
                padding: '6px 14px',
                border: `1px solid ${theme.green}`,
                background: theme.bg,
                color: theme.green,
                fontFamily: 'JetBrains Mono, monospace',
                fontSize: 11,
                display: 'flex',
                gap: 10,
                alignItems: 'center',
              }}
            >
              <div
                style={{
                  width: 10,
                  height: 10,
                  border: `2px solid ${theme.green}`,
                  borderTopColor: 'transparent',
                  borderRadius: '50%',
                  animation: 'tc-spin 1s linear infinite',
                }}
              />
              {props.fetchContext?.mode === FETCH_MODE.RUN
                ? `RUNNING BACKTEST · ${props.fetchContext?.strategy || ''}`
                : `LOADING BARS · ${props.fetchContext?.symbol || ''} / ${props.fetchContext?.timeframe || ''}${props.fetchContext?.rangeLabel ? ` · ${props.fetchContext.rangeLabel}` : ''}`}
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

window.TradingChart = TradingChart;

if (!document.getElementById('tc-keyframes')) {
  const style = document.createElement('style');
  style.id = 'tc-keyframes';
  style.textContent = `
    @keyframes tc-progress {
      0%   { transform: translateX(-100%); }
      100% { transform: translateX(400%); }
    }
    @keyframes tc-spin {
      to { transform: rotate(360deg); }
    }
  `;
  document.head.appendChild(style);
}
