import { useEffect, useMemo, useRef, useState } from 'react';
import {
  CandlestickSeries,
  ColorType,
  CrosshairMode,
  HistogramSeries,
  type IPrimitivePaneRenderer,
  type IPrimitivePaneView,
  type IChartApi,
  type ISeriesApi,
  type ISeriesPrimitive,
  LineStyle,
  LineSeries,
  type SeriesAttachedParameter,
  type Time,
  createChart,
  type UTCTimestamp,
  type CandlestickData,
  type HistogramData,
  type LineData,
  type WhitespaceData,
} from 'lightweight-charts';
import type { Candle, SeriesPoint } from '../../lib/api';
import { FibRetracementPrimitive, type FibAnchor, type FibRetracement } from './FibRetracementPrimitive';

type Props = {
  candles: Candle[];
  oracle?: SeriesPoint[];
  sma20?: SeriesPoint[];
  ema20?: SeriesPoint[];
  bb20?: { upper: SeriesPoint[]; lower: SeriesPoint[]; mid: SeriesPoint[] };
  showIndicators: boolean;
  showBuild: boolean;
  bucketSeconds: number;
  seriesKey: string;
  priceLines?: Array<{
    id: string;
    title: string;
    price: number | null;
    color: string;
    lineWidth?: number;
    lineStyle?: LineStyle;
    axisLabelVisible?: boolean;
  }>;
  fib?: FibRetracement | null;
  fibOpacity?: number;
  fibSelected?: boolean;
  priceAutoScale?: boolean;
  onReady?: (api: { chart: IChartApi; candles: ISeriesApi<'Candlestick', UTCTimestamp> }) => void;
  onChartClick?: (p: FibAnchor & { target: 'chart' | 'fib' }) => void;
  onChartCrosshairMove?: (p: FibAnchor) => void;
  onPointerEvent?: (p: {
    type: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
    logical: number;
    price: number;
    x: number;
    y: number;
    target: 'chart' | 'fib';
    event: PointerEvent;
  }) => { consume?: boolean; capturePointer?: boolean } | void;
};

type LinePoint = LineData | WhitespaceData;
type BuildSample = { t: number; v: number };

const BUILD_UP_COLOR = '#22c55e';
const BUILD_DOWN_COLOR = '#ef4444';
const BUILD_FLAT_COLOR = '#60a5fa';
const BUILD_UP_SLICE = 'rgba(34,197,94,0.70)';
const BUILD_DOWN_SLICE = 'rgba(239,68,68,0.70)';
const BUILD_FLAT_SLICE = 'rgba(96,165,250,0.70)';

function toTime(t: number): UTCTimestamp {
  return t as UTCTimestamp;
}

function resolveBucketSeconds(bucketSeconds: number, candles: Candle[]): number {
  if (Number.isFinite(bucketSeconds) && bucketSeconds > 0) return bucketSeconds;
  if (candles.length >= 2) {
    const last = candles[candles.length - 1]?.time;
    const prev = candles[candles.length - 2]?.time;
    const delta = typeof last === 'number' && typeof prev === 'number' ? last - prev : 0;
    if (Number.isFinite(delta) && delta > 0) return delta;
  }
  return 60;
}

function samplePriceFromCandles(candles: Candle[]): number | null {
  for (let i = candles.length - 1; i >= 0; i -= 1) {
    const close = candles[i]?.close;
    if (typeof close !== 'number') continue;
    if (!Number.isFinite(close)) continue;
    if (close === 0) continue;
    return close;
  }
  return null;
}

function priceFormatForSample(price: number) {
  const abs = Math.abs(price);
  if (!Number.isFinite(abs) || abs === 0) return { type: 'price' as const, precision: 2, minMove: 0.01 };
  if (abs >= 1000) return { type: 'price' as const, precision: 0, minMove: 1 };
  if (abs >= 1) return { type: 'price' as const, precision: 2, minMove: 0.01 };
  const exponent = Math.floor(Math.log10(abs)); // negative for abs < 1
  const precision = Math.min(8, Math.max(4, -exponent + 3));
  return { type: 'price' as const, precision, minMove: Math.pow(10, -precision) };
}

function toCandleData(candles: Candle[]): CandlestickData[] {
  return candles.map((c) => ({
    time: toTime(c.time),
    open: c.open,
    high: c.high,
    low: c.low,
    close: c.close,
  }));
}

function toVolumeData(candles: Candle[]): HistogramData[] {
  return candles.map((c) => {
    return {
      time: toTime(c.time),
      value: c.volume ?? 0,
      color: 'rgba(148,163,184,0.22)',
    };
  });
}

function toLineSeries(points: SeriesPoint[] | undefined): LinePoint[] {
  if (!points?.length) return [];
  return points.map((p) =>
    p.value == null ? ({ time: toTime(p.time) } as WhitespaceData) : { time: toTime(p.time), value: p.value }
  );
}

function colorForDelta(delta: number): string {
  if (delta > 0) return BUILD_UP_COLOR;
  if (delta < 0) return BUILD_DOWN_COLOR;
  return BUILD_FLAT_COLOR;
}

function sliceColorForDelta(delta: number): string {
  if (delta > 0) return BUILD_UP_SLICE;
  if (delta < 0) return BUILD_DOWN_SLICE;
  return BUILD_FLAT_SLICE;
}

type SliceDir = -1 | 0 | 1;

function dirForDelta(delta: number): SliceDir {
  if (delta > 0) return 1;
  if (delta < 0) return -1;
  return 0;
}

function buildDeltaSeriesForCandle(candle: Candle, bs: number, samples: BuildSample[] | undefined): LinePoint[] {
  const eps = 1e-3;
  const startT = candle.time + eps;
  const endT = candle.time + bs - eps;
  if (!(endT > startT)) return [];

  const points: BuildSample[] = [{ t: startT, v: 0 }];
  let lastT = startT;
  for (const p of samples || []) {
    let t = p.t;
    if (t <= lastT + eps) t = lastT + eps;
    if (t >= endT) break;
    points.push({ t, v: p.v });
    lastT = t;
  }

  const finalDelta = candle.close - candle.open;
  if (endT > lastT + eps) {
    points.push({ t: endT, v: finalDelta });
  } else if (points.length) {
    points[points.length - 1] = { ...points[points.length - 1]!, v: finalDelta };
  }

  const out: LinePoint[] = [{ time: toTime(candle.time) } as WhitespaceData];
  out.push({ time: toTime(points[0]!.t), value: points[0]!.v } as LineData);

  let lastLineIdx = out.length - 1;
  let lastVal = points[0]!.v;
  for (let i = 1; i < points.length; i += 1) {
    const v = points[i]!.v;
    const prev = out[lastLineIdx] as LineData;
    out[lastLineIdx] = { ...prev, color: colorForDelta(v - lastVal) } as LineData;
    out.push({ time: toTime(points[i]!.t), value: v } as LineData);
    lastLineIdx = out.length - 1;
    lastVal = v;
  }
  return out;
}

type BuildSlicesState = {
  enabled: boolean;
  candles: Candle[];
  bucketSeconds: number;
  samples: Map<number, BuildSample[]>;
  series: ISeriesApi<'Histogram', Time> | null;
  chart: SeriesAttachedParameter<Time>['chart'] | null;
};

class BuildSlicesPaneRenderer implements IPrimitivePaneRenderer {
  private readonly _getState: () => BuildSlicesState;

  constructor(getState: () => BuildSlicesState) {
    this._getState = getState;
  }

  draw(target: any) {
    const { enabled, candles, bucketSeconds, samples, series, chart } = this._getState();
    if (!enabled) return;
    if (!candles.length || !series || !chart) return;

    const bs = resolveBucketSeconds(bucketSeconds, candles);
    const lastCandleTime = candles[candles.length - 1]?.time ?? null;

    const yBase = series.priceToCoordinate(0);
    if (yBase == null) return;

    const xs = candles.map((c) => chart.timeScale().timeToCoordinate(toTime(c.time)));

    target.useBitmapCoordinateSpace(({ context, bitmapSize, horizontalPixelRatio, verticalPixelRatio }: any) => {
      const yBottomPx = Math.round(yBase * verticalPixelRatio);
      const lastIdx = xs.length - 1;

      for (let i = 0; i < candles.length; i += 1) {
        const x = xs[i];
        if (x == null) continue;
        if (!Number.isFinite(x)) continue;

        const c = candles[i]!;

        const start = c.time;
        const end = start + bs;
        const isCurrent = lastCandleTime != null && c.time === lastCandleTime;
        const now = Date.now() / 1000;
        const progressT = isCurrent ? Math.min(end, Math.max(start, now)) : end;

        let spacing = 0;
        const prevX = i > 0 ? xs[i - 1] : null;
        const nextX = i < lastIdx ? xs[i + 1] : null;
        if (prevX != null && Number.isFinite(prevX)) spacing = x - prevX;
        if (nextX != null && Number.isFinite(nextX)) {
          const s2 = nextX - x;
          spacing = spacing > 0 ? Math.min(spacing, s2) : s2;
        }
        if (!(spacing > 0)) spacing = 6;

        const barWidthCss = Math.max(1, spacing * 0.9);
        const barWidthPx = Math.max(1, Math.round(barWidthCss * horizontalPixelRatio));
        const xCenterPx = Math.round(x * horizontalPixelRatio);
        const xLeftPx = Math.round(xCenterPx - barWidthPx / 2);

        const volumeValue = typeof c.volume === 'number' && Number.isFinite(c.volume) ? c.volume : 0;
        if (!(volumeValue > 0)) continue;
        const yTop = series.priceToCoordinate(volumeValue);
        if (yTop == null) continue;
        const yTopPx = Math.round(yTop * verticalPixelRatio);
        if (!(yBottomPx > yTopPx)) continue;
        const barHeightPx = yBottomPx - yTopPx;

        const x0 = Math.max(0, Math.min(bitmapSize.width, xLeftPx));
        const x1 = Math.max(0, Math.min(bitmapSize.width, xLeftPx + barWidthPx));
        const w = x1 - x0;
        if (!(w > 0)) continue;

        // Prefer server-provided `flowRows`: brick-by-brick direction per time slice inside the candle.
        // Fallback to `flow` (3 shares) or net candle direction.
        const rowsFromApi = Array.isArray((c as any).flowRows) ? ((c as any).flowRows as any[]) : null;
        const rowDirs = rowsFromApi?.length ? rowsFromApi : null;

        if (rowDirs) {
          const rows = Math.max(1, rowDirs.length);
          const progressRows = Math.max(0, Math.min(rows, Math.ceil(((progressT - start) / bs) * rows)));
          const movesFromApi = Array.isArray((c as any).flowMoves) ? ((c as any).flowMoves as any[]) : null;
          const rowMoves = movesFromApi && movesFromApi.length === rows ? movesFromApi : null;
          let maxMove = 0;
          if (rowMoves) {
            for (let r = 0; r < progressRows; r += 1) {
              const v = Number(rowMoves[r]);
              if (Number.isFinite(v) && v > maxMove) maxMove = v;
            }
          }

          const sepPx = 1; // black separator line between steps
          const sepColor = 'rgba(0,0,0,0.75)';

          // Blue (flat) bricks have a constant pixel height (unit).
          // Up/down bricks are scaled by |Δ| in their dt.
          const minNonFlatPx = 1;
          let unitFlatPx = 2;

          let flatCount = 0;
          let nonFlatCount = 0;
          for (let r = 0; r < progressRows; r += 1) {
            const dirRaw = rowDirs[r];
            const dir = dirRaw > 0 ? 1 : dirRaw < 0 ? -1 : 0;
            if (dir === 0) flatCount += 1;
            else nonFlatCount += 1;
          }

          const sepTotal = Math.max(0, progressRows - 1) * sepPx;
          if (flatCount > 0) {
            const maxUnit = Math.floor((barHeightPx - sepTotal - nonFlatCount * minNonFlatPx) / flatCount);
            unitFlatPx = Math.max(1, Math.min(unitFlatPx, maxUnit));
          } else {
            unitFlatPx = 0;
          }

          let nonFlatAvailable = barHeightPx - sepTotal - flatCount * unitFlatPx;
          if (!Number.isFinite(nonFlatAvailable) || nonFlatAvailable < 0) nonFlatAvailable = 0;

          const baseNonFlat = nonFlatCount * minNonFlatPx;
          let extra = nonFlatAvailable - baseNonFlat;
          if (!Number.isFinite(extra) || extra < 0) extra = 0;

          let sumMove = 0;
          if (nonFlatCount > 0) {
            for (let r = 0; r < progressRows; r += 1) {
              const dirRaw = rowDirs[r];
              const dir = dirRaw > 0 ? 1 : dirRaw < 0 ? -1 : 0;
              if (dir === 0) continue;
              const mvRaw = rowMoves ? Number(rowMoves[r]) : 0;
              const mv = Number.isFinite(mvRaw) ? Math.max(0, mvRaw) : 0;
              sumMove += mv;
            }
          }

          if (!(flatCount + nonFlatCount > 0) || barHeightPx <= sepTotal) {
            continue;
          }

          // Stack bricks bottom-up (earliest slices at the bottom).
          let usedExtra = 0;
          let nonFlatSeen = 0;
          let y = yBottomPx;
          for (let r = 0; r < progressRows; r += 1) {
            const dirRaw = rowDirs[r];
            const dir = dirRaw > 0 ? 1 : dirRaw < 0 ? -1 : 0;
            const isLast = r === progressRows - 1;

            let h = 0;
            if (dir === 0) {
              h = unitFlatPx;
            } else {
              nonFlatSeen += 1;
              const mvRaw = rowMoves ? Number(rowMoves[r]) : 0;
              const mv = Number.isFinite(mvRaw) ? Math.max(0, mvRaw) : 0;
              const share = sumMove > 0 ? mv / sumMove : 1 / nonFlatCount;
              const wantExtra = Math.floor(extra * share);
              const isLastNonFlat = nonFlatSeen === nonFlatCount;
              const add = isLastNonFlat ? Math.max(0, extra - usedExtra) : Math.max(0, wantExtra);
              usedExtra += add;
              h = minNonFlatPx + add;
            }

            if (h <= 0) continue;
            y -= h;
            context.fillStyle = sliceColorForDelta(dir);
            context.fillRect(x0, y, w, h);

            if (!isLast) {
              y -= sepPx;
              context.fillStyle = sepColor;
              context.fillRect(x0, y, w, sepPx);
            }
          }
          continue;
        }

        const f: any = (c as any).flow;
        let upShare = typeof f?.up === 'number' ? f.up : Number(f?.up);
        let downShare = typeof f?.down === 'number' ? f.down : Number(f?.down);
        let flatShare = typeof f?.flat === 'number' ? f.flat : Number(f?.flat);

        if (!Number.isFinite(upShare)) upShare = 0;
        if (!Number.isFinite(downShare)) downShare = 0;
        if (!Number.isFinite(flatShare)) flatShare = 0;
        upShare = Math.max(0, upShare);
        downShare = Math.max(0, downShare);
        flatShare = Math.max(0, flatShare);

        let sum = upShare + downShare + flatShare;
        if (!(sum > 0)) {
          const overallDir = dirForDelta(c.close - c.open);
          upShare = overallDir > 0 ? 1 : 0;
          downShare = overallDir < 0 ? 1 : 0;
          flatShare = overallDir === 0 ? 1 : 0;
          sum = 1;
        }

        upShare /= sum;
        downShare /= sum;
        flatShare /= sum;

        const downH = Math.floor(barHeightPx * downShare);
        const flatH = Math.floor(barHeightPx * flatShare);
        const upH = Math.max(0, barHeightPx - downH - flatH);

        let y = yBottomPx;

        if (downH > 0) {
          context.fillStyle = sliceColorForDelta(-1);
          context.fillRect(x0, y - downH, w, downH);
          y -= downH;
        }
        if (flatH > 0) {
          context.fillStyle = sliceColorForDelta(0);
          context.fillRect(x0, y - flatH, w, flatH);
          y -= flatH;
        }
        if (upH > 0) {
          context.fillStyle = sliceColorForDelta(1);
          context.fillRect(x0, y - upH, w, upH);
        }
      }
    });
  }
}

class BuildSlicesPaneView implements IPrimitivePaneView {
  private readonly _renderer: BuildSlicesPaneRenderer;

  constructor(getState: () => BuildSlicesState) {
    this._renderer = new BuildSlicesPaneRenderer(getState);
  }

  zOrder() {
    return 'top';
  }

  renderer() {
    return this._renderer;
  }
}

class BuildSlicesPrimitive implements ISeriesPrimitive<Time> {
  private _param: SeriesAttachedParameter<Time> | null = null;
  private _series: ISeriesApi<'Histogram', Time> | null = null;
  private _enabled = true;
  private _candles: Candle[] = [];
  private _bucketSeconds = 0;
  private _samples: Map<number, BuildSample[]> = new Map();
  private readonly _paneView: BuildSlicesPaneView;
  private readonly _paneViews: readonly IPrimitivePaneView[];

  constructor() {
    this._paneView = new BuildSlicesPaneView(() => ({
      enabled: this._enabled,
      candles: this._candles,
      bucketSeconds: this._bucketSeconds,
      samples: this._samples,
      series: this._series,
      chart: this._param?.chart ?? null,
    }));
    this._paneViews = [this._paneView];
  }

  attached(param: SeriesAttachedParameter<Time>) {
    this._param = param;
    this._series = param.series as ISeriesApi<'Histogram', Time>;
  }

  detached() {
    this._param = null;
    this._series = null;
  }

  paneViews() {
    return this._paneViews;
  }

  setEnabled(next: boolean) {
    this._enabled = Boolean(next);
    this._param?.requestUpdate();
  }

  setData(next: { candles: Candle[]; bucketSeconds: number; samples: Map<number, BuildSample[]> }) {
    this._candles = Array.isArray(next.candles) ? next.candles : [];
    this._bucketSeconds = Number.isFinite(next.bucketSeconds) ? next.bucketSeconds : 0;
    this._samples = next.samples;
    this._param?.requestUpdate();
  }
}

export default function TradingChart({
  candles,
  oracle,
  sma20,
  ema20,
  bb20,
  showIndicators,
  showBuild,
  bucketSeconds,
  seriesKey,
  priceLines,
  fib,
  fibOpacity = 1,
  fibSelected = false,
  priceAutoScale = true,
  onReady,
  onChartClick,
  onChartCrosshairMove,
  onPointerEvent,
}: Props) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<IChartApi | null>(null);
  const fibPrimitiveRef = useRef<FibRetracementPrimitive | null>(null);
  const fibRef = useRef<FibRetracement | null>(fib ?? null);
  const fibOpacityRef = useRef<number>(fibOpacity);
  const priceAutoScaleRef = useRef<boolean>(priceAutoScale);
  const prevPriceAutoScaleRef = useRef<boolean>(priceAutoScale);
  const showBuildRef = useRef<boolean>(showBuild);
  const onReadyRef = useRef<Props['onReady']>(onReady);
  const onChartClickRef = useRef<Props['onChartClick']>(onChartClick);
  const onChartCrosshairMoveRef = useRef<Props['onChartCrosshairMove']>(onChartCrosshairMove);
  const onPointerEventRef = useRef<Props['onPointerEvent']>(onPointerEvent);
  const capturedOverlayPointerRef = useRef<number | null>(null);
  const buildSlicesPrimitiveRef = useRef<BuildSlicesPrimitive | null>(null);
  const buildSamplesRef = useRef<Map<number, BuildSample[]>>(new Map());
  const buildKeyRef = useRef<string | null>(null);
  const lastBuildCandleStartRef = useRef<number | null>(null);
  const hoverCandleTimeRef = useRef<number | null>(null);
  const [hoverCandleTime, setHoverCandleTime] = useState<number | null>(null);
  const priceLinesRef = useRef<Map<string, any>>(new Map());
  const seriesRef = useRef<{
    candles?: ISeriesApi<'Candlestick'>;
    volume?: ISeriesApi<'Histogram'>;
    buildHover?: ISeriesApi<'Line'>;
    oracle?: ISeriesApi<'Line'>;
    sma20?: ISeriesApi<'Line'>;
    ema20?: ISeriesApi<'Line'>;
    bbUpper?: ISeriesApi<'Line'>;
    bbLower?: ISeriesApi<'Line'>;
    bbMid?: ISeriesApi<'Line'>;
  }>({});

  const candleData = useMemo(() => toCandleData(candles), [candles]);
  const volumeData = useMemo(() => toVolumeData(candles), [candles]);
  const oracleData = useMemo(() => toLineSeries(oracle), [oracle]);
  const smaData = useMemo(() => toLineSeries(sma20), [sma20]);
  const emaData = useMemo(() => toLineSeries(ema20), [ema20]);
  const bbUpper = useMemo(() => toLineSeries(bb20?.upper), [bb20?.upper]);
  const bbLower = useMemo(() => toLineSeries(bb20?.lower), [bb20?.lower]);
  const bbMid = useMemo(() => toLineSeries(bb20?.mid), [bb20?.mid]);
  const priceFormat = useMemo(() => {
    const sample = samplePriceFromCandles(candles);
    return sample == null ? { type: 'price' as const, precision: 2, minMove: 0.01 } : priceFormatForSample(sample);
  }, [candles]);

  useEffect(() => {
    onReadyRef.current = onReady;
  }, [onReady]);

  useEffect(() => {
    onChartClickRef.current = onChartClick;
  }, [onChartClick]);

  useEffect(() => {
    onChartCrosshairMoveRef.current = onChartCrosshairMove;
  }, [onChartCrosshairMove]);

  useEffect(() => {
    onPointerEventRef.current = onPointerEvent;
  }, [onPointerEvent]);

  useEffect(() => {
    fibRef.current = fib ?? null;
  }, [fib]);

  useEffect(() => {
    fibOpacityRef.current = fibOpacity;
    fibPrimitiveRef.current?.setOpacity(fibOpacity);
  }, [fibOpacity]);

  useEffect(() => {
    priceAutoScaleRef.current = priceAutoScale;
  }, [priceAutoScale]);

  useEffect(() => {
    showBuildRef.current = showBuild;
    if (!showBuild && (hoverCandleTimeRef.current != null || hoverCandleTime != null)) {
      hoverCandleTimeRef.current = null;
      setHoverCandleTime(null);
    }
  }, [showBuild, hoverCandleTime]);

  useEffect(() => {
    if (!containerRef.current) return;
    if (chartRef.current) return;

    const chart = createChart(containerRef.current, {
      layout: {
        background: { type: ColorType.Solid, color: 'rgba(0,0,0,0)' },
        textColor: '#e6e9ef',
        fontFamily:
          'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif',
      },
      grid: {
        vertLines: { color: 'rgba(255,255,255,0.06)' },
        horzLines: { color: 'rgba(255,255,255,0.06)' },
      },
      crosshair: {
        mode: CrosshairMode.Normal,
        vertLine: { color: 'rgba(255,255,255,0.18)', style: LineStyle.Dashed },
        horzLine: { color: 'rgba(255,255,255,0.18)', style: LineStyle.Dashed },
      },
      rightPriceScale: { borderColor: 'rgba(255,255,255,0.08)' },
      timeScale: { borderColor: 'rgba(255,255,255,0.08)', timeVisible: true, secondsVisible: false },
      handleScale: { mouseWheel: true, pinch: true },
      handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: true },
    });
    chartRef.current = chart;

    const candleSeries = chart.addSeries(CandlestickSeries, {
      upColor: '#22c55e',
      downColor: '#ef4444',
      borderVisible: false,
      wickUpColor: '#22c55e',
      wickDownColor: '#ef4444',
      priceFormat,
    });

    const fibPrimitive = new FibRetracementPrimitive();
    candleSeries.attachPrimitive(fibPrimitive);
    fibPrimitiveRef.current = fibPrimitive;
    fibPrimitive.setFib(fib ?? null);
    fibPrimitive.setOpacity(fibOpacityRef.current);

    const volumeSeries = chart.addSeries(HistogramSeries, {
      priceFormat: { type: 'volume' },
      priceScaleId: '',
      color: 'rgba(255,255,255,0.15)',
    });
    volumeSeries.priceScale().applyOptions({
      scaleMargins: { top: 0.88, bottom: 0 },
    });

    const buildSlicesPrimitive = new BuildSlicesPrimitive();
    volumeSeries.attachPrimitive(buildSlicesPrimitive);
    buildSlicesPrimitiveRef.current = buildSlicesPrimitive;
    buildSlicesPrimitive.setEnabled(showBuildRef.current);

    const buildHoverSeries = chart.addSeries(LineSeries, {
      color: BUILD_FLAT_COLOR,
      lineWidth: 2,
      priceFormat,
      priceScaleId: 'build',
      lastValueVisible: false,
      priceLineVisible: false,
      crosshairMarkerVisible: false,
    });
    buildHoverSeries.priceScale().applyOptions({
      scaleMargins: { top: 0.72, bottom: 0.12 },
      visible: false,
      borderVisible: false,
    });

    const oracleSeries = chart.addSeries(LineSeries, {
      color: 'rgba(251,146,60,0.9)',
      lineWidth: 1,
      lineStyle: LineStyle.Dotted,
      priceFormat,
    });

    const smaSeries = chart.addSeries(LineSeries, { color: 'rgba(248,113,113,0.9)', lineWidth: 1, priceFormat });
    const emaSeries = chart.addSeries(LineSeries, { color: 'rgba(52,211,153,0.9)', lineWidth: 1, priceFormat });
    const bbUpperSeries = chart.addSeries(LineSeries, { color: 'rgba(250,204,21,0.6)', lineWidth: 1, priceFormat });
    const bbLowerSeries = chart.addSeries(LineSeries, { color: 'rgba(163,163,163,0.6)', lineWidth: 1, priceFormat });
    const bbMidSeries = chart.addSeries(LineSeries, {
      color: 'rgba(250,204,21,0.35)',
      lineWidth: 1,
      lineStyle: LineStyle.Dashed,
      priceFormat,
    });

    seriesRef.current = {
      candles: candleSeries,
      volume: volumeSeries,
      buildHover: buildHoverSeries,
      oracle: oracleSeries,
      sma20: smaSeries,
      ema20: emaSeries,
      bbUpper: bbUpperSeries,
      bbLower: bbLowerSeries,
      bbMid: bbMidSeries,
    };

    onReadyRef.current?.({ chart, candles: candleSeries as ISeriesApi<'Candlestick', UTCTimestamp> });

    const onClick = (param: any) => {
      if (!param?.point) return;
      const logical = param.logical ?? chart.timeScale().coordinateToLogical(param.point.x);
      if (logical == null) return;
      const price = candleSeries.coordinateToPrice(param.point.y);
      if (price == null) return;

      const currentFib = fibRef.current;
      let target: 'chart' | 'fib' = 'chart';
      if (currentFib) {
        const x1 = chart.timeScale().logicalToCoordinate(currentFib.a.logical as any);
        const x2 = chart.timeScale().logicalToCoordinate(currentFib.b.logical as any);
        if (x1 != null && x2 != null) {
          const tol = 6;
          const left = Math.min(x1, x2) - tol;
          const right = Math.max(x1, x2) + tol;
          const p0 = currentFib.a.price;
          const delta = currentFib.b.price - p0;
          const yRatioMin = 0;
          const yRatioMax = 4.236;
          const pMin = Math.min(p0 + delta * yRatioMin, p0 + delta * yRatioMax);
          const pMax = Math.max(p0 + delta * yRatioMin, p0 + delta * yRatioMax);
          const y1 = candleSeries.priceToCoordinate(pMin);
          const y2 = candleSeries.priceToCoordinate(pMax);
          if (y1 != null && y2 != null) {
            const top = Math.min(y1, y2) - tol;
            const bottom = Math.max(y1, y2) + tol;
            if (param.point.x >= left && param.point.x <= right && param.point.y >= top && param.point.y <= bottom) {
              target = 'fib';
            }
          }
        }
      }

      onChartClickRef.current?.({ logical: Number(logical), price: Number(price), target });
    };
    chart.subscribeClick(onClick);

    const onCrosshairMove = (param: any) => {
      if (!param?.point) {
        if (showBuildRef.current && hoverCandleTimeRef.current != null) {
          hoverCandleTimeRef.current = null;
          setHoverCandleTime(null);
        }
        return;
      }

      if (showBuildRef.current) {
        const t = typeof param?.time === 'number' ? Number(param.time) : null;
        const next = t != null && Number.isFinite(t) ? t : null;
        if (hoverCandleTimeRef.current !== next) {
          hoverCandleTimeRef.current = next;
          setHoverCandleTime(next);
        }
      }

      const logical = param.logical ?? chart.timeScale().coordinateToLogical(param.point.x);
      if (logical == null) return;
      const price = candleSeries.coordinateToPrice(param.point.y);
      if (price == null) return;
      onChartCrosshairMoveRef.current?.({ logical: Number(logical), price: Number(price) });
    };
    chart.subscribeCrosshairMove(onCrosshairMove);

    const container = containerRef.current;
    const toAnchorFromPoint = (x: number, y: number): FibAnchor | null => {
      const logical = chart.timeScale().coordinateToLogical(x);
      if (logical == null) return null;
      const price = candleSeries.coordinateToPrice(y);
      if (price == null) return null;
      return { logical: Number(logical), price: Number(price) };
    };

    const isOverFib = (x: number, y: number, currentFib: FibRetracement): boolean => {
      const x1 = chart.timeScale().logicalToCoordinate(currentFib.a.logical as any);
      const x2 = chart.timeScale().logicalToCoordinate(currentFib.b.logical as any);
      if (x1 == null || x2 == null) return false;

      const tol = 6;
      const left = Math.min(x1, x2) - tol;
      const right = Math.max(x1, x2) + tol;

      const p0 = currentFib.a.price;
      const delta = currentFib.b.price - p0;
      const yRatioMin = 0;
      const yRatioMax = 4.236;
      const pMin = Math.min(p0 + delta * yRatioMin, p0 + delta * yRatioMax);
      const pMax = Math.max(p0 + delta * yRatioMin, p0 + delta * yRatioMax);
      const y1 = candleSeries.priceToCoordinate(pMin);
      const y2 = candleSeries.priceToCoordinate(pMax);
      if (y1 == null || y2 == null) return false;

      const top = Math.min(y1, y2) - tol;
      const bottom = Math.max(y1, y2) + tol;
      return x >= left && x <= right && y >= top && y <= bottom;
    };

    const onOverlayPointer = (e: PointerEvent) => {
      if (!containerRef.current) return;
      const rect = containerRef.current.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const anchor = toAnchorFromPoint(x, y);
      if (!anchor) return;

      const currentFib = fibRef.current;
      const target: 'chart' | 'fib' = currentFib && isOverFib(x, y, currentFib) ? 'fib' : 'chart';
      const type =
        e.type === 'pointerdown'
          ? ('pointerdown' as const)
          : e.type === 'pointermove'
            ? ('pointermove' as const)
            : e.type === 'pointerup'
              ? ('pointerup' as const)
              : ('pointercancel' as const);

      const decision = onPointerEventRef.current?.({
        type,
        logical: anchor.logical,
        price: anchor.price,
        x,
        y,
        target,
        event: e,
      });

      if (decision?.capturePointer) {
        capturedOverlayPointerRef.current = e.pointerId;
        try {
          containerRef.current.setPointerCapture(e.pointerId);
        } catch {
          // ignore
        }
      }

      if (decision?.consume) {
        e.preventDefault();
        e.stopImmediatePropagation();
      }

      if (type === 'pointerup' || type === 'pointercancel') {
        if (capturedOverlayPointerRef.current === e.pointerId) {
          capturedOverlayPointerRef.current = null;
          try {
            containerRef.current.releasePointerCapture(e.pointerId);
          } catch {
            // ignore
          }
        }
      }
    };
    container?.addEventListener('pointerdown', onOverlayPointer, { capture: true });
    container?.addEventListener('pointermove', onOverlayPointer, { capture: true });
    container?.addEventListener('pointerup', onOverlayPointer, { capture: true });
    container?.addEventListener('pointercancel', onOverlayPointer, { capture: true });

    const onWheel = (e: WheelEvent) => {
      if (!e.ctrlKey) return;
      if (!containerRef.current) return;
      e.preventDefault();
      e.stopPropagation();
      if (priceAutoScaleRef.current) return;

      const rect = containerRef.current.getBoundingClientRect();
      const y = e.clientY - rect.top;
      const pivotPrice = candleSeries.coordinateToPrice(y);
      if (pivotPrice == null) return;

      const ps = candleSeries.priceScale();
      const range = ps.getVisibleRange();
      let low: number;
      let high: number;
      if (range) {
        low = Math.min(range.from, range.to);
        high = Math.max(range.from, range.to);
      } else {
        const top = candleSeries.coordinateToPrice(0);
        const bottom = candleSeries.coordinateToPrice(rect.height);
        if (top == null || bottom == null) return;
        low = Math.min(top, bottom);
        high = Math.max(top, bottom);
      }
      const span = high - low;
      if (!(span > 0)) return;

      const zoomFactor = Math.exp(e.deltaY * 0.002);
      if (!Number.isFinite(zoomFactor) || zoomFactor <= 0) return;
      const t = Math.min(1, Math.max(0, (pivotPrice - low) / span));
      const minSpan = span * 1e-6;
      const maxSpan = span * 1e6;
      const nextSpan = Math.min(maxSpan, Math.max(minSpan, span * zoomFactor));
      const nextLow = pivotPrice - t * nextSpan;
      const nextHigh = nextLow + nextSpan;

      ps.setAutoScale(false);
      ps.setVisibleRange({ from: nextLow, to: nextHigh });
    };
    container?.addEventListener('wheel', onWheel, { passive: false, capture: true });

    let pan: { pointerId: number; startPrice: number; startRange: { from: number; to: number } } | null = null;
    const onPointerDown = (e: PointerEvent) => {
      if (e.button !== 0) return;
      if (!e.ctrlKey) return;
      if (priceAutoScaleRef.current) return;
      if (!containerRef.current) return;

      const rect = containerRef.current.getBoundingClientRect();
      const y = e.clientY - rect.top;
      const startPrice = candleSeries.coordinateToPrice(y);
      if (startPrice == null) return;

      const ps = candleSeries.priceScale();
      const range = ps.getVisibleRange();
      let from: number;
      let to: number;
      if (range) {
        from = range.from;
        to = range.to;
      } else {
        const top = candleSeries.coordinateToPrice(0);
        const bottom = candleSeries.coordinateToPrice(rect.height);
        if (top == null || bottom == null) return;
        from = Math.min(top, bottom);
        to = Math.max(top, bottom);
      }
      if (!(to > from)) return;

      pan = { pointerId: e.pointerId, startPrice, startRange: { from, to } };
      try {
        containerRef.current.setPointerCapture(e.pointerId);
      } catch {
        // ignore
      }
      e.preventDefault();
      e.stopPropagation();
    };

    const onPointerMove = (e: PointerEvent) => {
      if (!pan) return;
      if (pan.pointerId !== e.pointerId) return;
      if (!containerRef.current) return;
      if (!e.ctrlKey || priceAutoScaleRef.current) {
        pan = null;
        return;
      }

      const rect = containerRef.current.getBoundingClientRect();
      const y = e.clientY - rect.top;
      const currentPrice = candleSeries.coordinateToPrice(y);
      if (currentPrice == null) return;
      const delta = pan.startPrice - currentPrice;

      const ps = candleSeries.priceScale();
      ps.setAutoScale(false);
      ps.setVisibleRange({ from: pan.startRange.from + delta, to: pan.startRange.to + delta });

      e.preventDefault();
      e.stopPropagation();
    };

    const stopPan = (e: PointerEvent) => {
      if (!pan) return;
      if (pan.pointerId !== e.pointerId) return;
      pan = null;
      try {
        containerRef.current?.releasePointerCapture(e.pointerId);
      } catch {
        // ignore
      }
    };

    container?.addEventListener('pointerdown', onPointerDown, { capture: true });
    container?.addEventListener('pointermove', onPointerMove, { capture: true });
    container?.addEventListener('pointerup', stopPan, { capture: true });
    container?.addEventListener('pointercancel', stopPan, { capture: true });

    const ro = new ResizeObserver(() => {
      if (!containerRef.current) return;
      const { width, height } = containerRef.current.getBoundingClientRect();
      chart.applyOptions({ width: Math.floor(width), height: Math.floor(height) });
    });
    ro.observe(containerRef.current);

    return () => {
      chart.unsubscribeClick(onClick);
      chart.unsubscribeCrosshairMove(onCrosshairMove);
      container?.removeEventListener('pointerdown', onOverlayPointer, { capture: true });
      container?.removeEventListener('pointermove', onOverlayPointer, { capture: true });
      container?.removeEventListener('pointerup', onOverlayPointer, { capture: true });
      container?.removeEventListener('pointercancel', onOverlayPointer, { capture: true });
      container?.removeEventListener('wheel', onWheel, { capture: true });
      container?.removeEventListener('pointerdown', onPointerDown, { capture: true });
      container?.removeEventListener('pointermove', onPointerMove, { capture: true });
      container?.removeEventListener('pointerup', stopPan, { capture: true });
      container?.removeEventListener('pointercancel', stopPan, { capture: true });
      ro.disconnect();
      if (fibPrimitiveRef.current) {
        candleSeries.detachPrimitive(fibPrimitiveRef.current);
        fibPrimitiveRef.current = null;
      }
      if (buildSlicesPrimitiveRef.current) {
        volumeSeries.detachPrimitive(buildSlicesPrimitiveRef.current);
        buildSlicesPrimitiveRef.current = null;
      }
      const lines = priceLinesRef.current;
      for (const line of Array.from(lines.values())) {
        try {
          candleSeries.removePriceLine(line);
        } catch {
          // ignore
        }
      }
      lines.clear();
      chart.remove();
      chartRef.current = null;
      seriesRef.current = {};
    };
  }, []);

  useEffect(() => {
    const candlesSeries = seriesRef.current.candles;
    if (!candlesSeries) return;

    const desired = (priceLines || []).filter((l) => l.price != null && Number.isFinite(l.price));
    const desiredIds = new Set(desired.map((l) => l.id));

    const map = priceLinesRef.current;
    for (const [id, line] of Array.from(map.entries())) {
      if (desiredIds.has(id)) continue;
      try {
        candlesSeries.removePriceLine(line);
      } catch {
        // ignore
      }
      map.delete(id);
    }

    for (const spec of desired) {
      const opts: any = {
        price: spec.price,
        color: spec.color,
        title: spec.title,
        lineWidth: spec.lineWidth ?? 1,
        lineStyle: spec.lineStyle ?? LineStyle.Dotted,
        axisLabelVisible: spec.axisLabelVisible ?? true,
      };

      const existing = map.get(spec.id);
      if (!existing) {
        try {
          const created = candlesSeries.createPriceLine(opts);
          map.set(spec.id, created);
        } catch {
          // ignore
        }
        continue;
      }

      try {
        existing.applyOptions(opts);
      } catch {
        // ignore
      }
    }
  }, [priceLines]);

  useEffect(() => {
    const s = seriesRef.current;
    if (!s.candles || !s.volume || !s.buildHover) return;
    s.candles.setData(candleData);
    s.volume.setData(volumeData);
    s.oracle?.setData(oracleData);
    s.sma20?.setData(smaData);
    s.ema20?.setData(emaData);
    s.bbUpper?.setData(bbUpper);
    s.bbLower?.setData(bbLower);
    s.bbMid?.setData(bbMid);

    const bs = resolveBucketSeconds(bucketSeconds, candles);
    const eps = 1e-3;
    const maxPointsPerCandle = 600;
    const minStep = Math.max(0.5, bs / maxPointsPerCandle);
    const map = buildSamplesRef.current;

    if (buildKeyRef.current !== seriesKey) {
      map.clear();
      buildKeyRef.current = seriesKey;
      lastBuildCandleStartRef.current = null;
    }

    const visibleStarts = new Set(candles.map((c) => c.time));
    for (const start of Array.from(map.keys())) {
      if (!visibleStarts.has(start)) map.delete(start);
    }

    const last = candles[candles.length - 1];
    if (last) {
      const prevStart = lastBuildCandleStartRef.current;
      if (prevStart != null && prevStart !== last.time) {
        const prevCandle = candles.find((c) => c.time === prevStart);
        if (prevCandle) {
          const endT = prevStart + bs - eps;
          const finalDelta = prevCandle.close - prevCandle.open;
          const list = map.get(prevStart) ?? [];
          const lastT = list.length ? list[list.length - 1]!.t : -Infinity;
          if (endT > lastT + eps) {
            list.push({ t: endT, v: finalDelta });
          } else if (list.length) {
            list[list.length - 1] = { t: lastT, v: finalDelta };
          }
          map.set(prevStart, list);
        }
      }

      const start = last.time;
      const endT = start + bs - eps;
      const delta = last.close - last.open;
      const nowT = Date.now() / 1000;
      const tClamped = Math.min(endT, Math.max(start + eps, nowT));
      const list = map.get(start) ?? [];
      if (list.length) {
        const lastPt = list[list.length - 1]!;
        if (tClamped - lastPt.t < minStep) {
          list[list.length - 1] = { t: tClamped, v: delta };
        } else {
          const t = Math.min(endT, Math.max(lastPt.t + eps, tClamped));
          if (t > lastPt.t) list.push({ t, v: delta });
        }
      } else {
        list.push({ t: tClamped, v: delta });
      }
      map.set(start, list);
      lastBuildCandleStartRef.current = start;
    }

    const buildPrimitive = buildSlicesPrimitiveRef.current;
    buildPrimitive?.setData({ candles, bucketSeconds: bs, samples: map });
    buildPrimitive?.setEnabled(showBuild);

    if (showBuild) {
      const hoverTime = hoverCandleTime;
      const hoverCandle = hoverTime == null ? null : candles.find((c) => c.time === hoverTime);
      const hoverData = hoverCandle ? buildDeltaSeriesForCandle(hoverCandle, bs, map.get(hoverCandle.time)) : [];

      if (hoverData.length) {
        s.buildHover.applyOptions({ visible: true });
        s.buildHover.setData(hoverData);
      } else {
        s.buildHover.applyOptions({ visible: false });
        s.buildHover.setData([]);
      }
    } else {
      s.buildHover.applyOptions({ visible: false });
      s.buildHover.setData([]);
    }

    s.sma20?.applyOptions({ visible: showIndicators });
    s.ema20?.applyOptions({ visible: showIndicators });
    s.bbUpper?.applyOptions({ visible: showIndicators });
    s.bbLower?.applyOptions({ visible: showIndicators });
    s.bbMid?.applyOptions({ visible: showIndicators });
  }, [
    candleData,
    volumeData,
    oracleData,
    smaData,
    emaData,
    bbUpper,
    bbLower,
    bbMid,
    showIndicators,
    showBuild,
    candles,
    bucketSeconds,
    seriesKey,
    hoverCandleTime,
  ]);

  useEffect(() => {
    const s = seriesRef.current;
    if (!s.candles) return;
    s.candles.applyOptions({ priceFormat });
    s.buildHover?.applyOptions({ priceFormat });
    s.oracle?.applyOptions({ priceFormat });
    s.sma20?.applyOptions({ priceFormat });
    s.ema20?.applyOptions({ priceFormat });
    s.bbUpper?.applyOptions({ priceFormat });
    s.bbLower?.applyOptions({ priceFormat });
    s.bbMid?.applyOptions({ priceFormat });
  }, [priceFormat]);

  useEffect(() => {
    fibPrimitiveRef.current?.setFib(fib ?? null);
  }, [fib]);

  useEffect(() => {
    fibPrimitiveRef.current?.setSelected(Boolean(fibSelected));
  }, [fibSelected]);

  useEffect(() => {
    const candlesSeries = seriesRef.current.candles;
    if (!candlesSeries) return;
    const ps = candlesSeries.priceScale();
    const prev = prevPriceAutoScaleRef.current;
    prevPriceAutoScaleRef.current = priceAutoScale;

    if (priceAutoScale) {
      ps.setAutoScale(true);
      return;
    }

    ps.setAutoScale(false);
    if (!prev) return;
    if (!containerRef.current) return;

    const rect = containerRef.current.getBoundingClientRect();
    const top = candlesSeries.coordinateToPrice(0);
    const bottom = candlesSeries.coordinateToPrice(rect.height);
    if (top == null || bottom == null) return;
    const from = Math.min(top, bottom);
    const to = Math.max(top, bottom);
    if (!(to > from)) return;
    ps.setVisibleRange({ from, to });
  }, [priceAutoScale]);

  return <div className="tradingChart" ref={containerRef} />;
}
