import { useRef, useEffect, useCallback, useState } from 'react';
import type {
  GateTargetLevel,
  OHLCVBar,
  PaperTrade,
  SRLevel,
  SRZone,
  TradeSetup,
} from '../../lib/types';
import { formatPrice, formatDate, formatLargeNumber } from '../../lib/format';

interface CandlestickChartProps {
  data: OHLCVBar[];
  srLevels?: SRLevel[];
  zones?: SRZone[];
  gateTargetLevels?: GateTargetLevel[];
  gateTargetLookbackBars?: number;
  gateTargetLoading?: boolean;
  gateTargetError?: boolean;
  showGateTraffic?: boolean;
  onShowGateTrafficChange?: (visible: boolean) => void;
  tradeSetup?: TradeSetup;
  currentPrice?: number;
  /** Paper trades for this ticker — entry/exit arrows on the matching bars. */
  paperTrades?: PaperTrade[];
}

/** YYYY-MM-DD from an ISO timestamp or bare date string. */
function dayKey(value: string): string {
  return value.length >= 10 ? value.slice(0, 10) : value;
}

/**
 * Index of the bar that best represents a paper-trade event time.
 * Prefer the same calendar day; else the first session on/after that day.
 */
function barIndexForEvent(data: OHLCVBar[], timestamp: string): number {
  if (!data.length) return -1;
  const day = dayKey(timestamp);
  const exact = data.findIndex((b) => dayKey(b.date) === day);
  if (exact >= 0) return exact;
  const after = data.findIndex((b) => dayKey(b.date) >= day);
  if (after >= 0) return after;
  // Event is after the last bar we have — pin to the last session.
  if (dayKey(data[data.length - 1].date) < day) return data.length - 1;
  return -1;
}

/** Filled triangle pointing down; tip sits at (x, y). */
function drawEntryArrow(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  color: string,
  size = 7,
) {
  ctx.fillStyle = color;
  ctx.strokeStyle = 'rgba(10, 11, 17, 0.55)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x - size, y - size * 1.35);
  ctx.lineTo(x + size, y - size * 1.35);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}

/** Filled triangle pointing up; tip sits at (x, y). */
function drawExitArrow(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  color: string,
  size = 7,
) {
  ctx.fillStyle = color;
  ctx.strokeStyle = 'rgba(10, 11, 17, 0.55)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x - size, y + size * 1.35);
  ctx.lineTo(x + size, y + size * 1.35);
  ctx.closePath();
  ctx.fill();
  ctx.stroke();
}

/** A horizontal price marker to draw, with an optional band (zone). */
interface SRMarker {
  price: number;
  low: number;
  high: number;
  strength: number;
  role: 'support' | 'resistance';
}

/**
 * Pick only the nearest support below and nearest resistance above the current
 * price — the two levels that actually matter for a trade right now. Keeps the
 * chart readable; the full S/R list lives in the S/R tab.
 */
function nearestSRMarkers(
  srLevels: SRLevel[],
  zones: SRZone[],
  price: number,
): SRMarker[] {
  const raw: SRMarker[] = zones.length
    ? zones.map((z) => ({ price: z.midpoint, low: z.low, high: z.high, strength: z.strength, role: 'support' as const }))
    : srLevels.map((l) => ({ price: l.price_level, low: l.price_level, high: l.price_level, strength: l.strength, role: 'support' as const }));

  const below = raw.filter((m) => m.high <= price).sort((a, b) => (price - a.price) - (price - b.price));
  const above = raw.filter((m) => m.low > price).sort((a, b) => (a.price - price) - (b.price - price));

  const out: SRMarker[] = [];
  if (below[0]) out.push({ ...below[0], role: 'support' });
  if (above[0]) out.push({ ...above[0], role: 'resistance' });
  return out;
}

interface TooltipState {
  visible: boolean;
  x: number;
  y: number;
  bar: OHLCVBar | null;
}

const MIN_VISIBLE_BARS = 10;
const CHART_HEIGHT = 440;
const VOLUME_PANE_HEIGHT = 72;
const PANE_GAP = 18;

type RangePreset = '1M' | '3M' | '6M' | 'YTD' | '1Y' | '3Y' | '5Y' | 'All';
const RANGE_PRESETS: RangePreset[] = ['1M', '3M', '6M', 'YTD', '1Y', '3Y', '5Y', 'All'];
const PRESET_MONTHS: Record<string, number> = { '1M': 1, '3M': 3, '6M': 6, '1Y': 12, '3Y': 36, '5Y': 60 };
const DEFAULT_PRESET: RangePreset = '1Y';

/** First bar index to show for a time-range preset (data is ascending by date). */
function startIndexForPreset(data: OHLCVBar[], preset: RangePreset): number {
  if (preset === 'All' || data.length === 0) return 0;
  const last = new Date(data[data.length - 1].date);
  let cutoff: Date;
  if (preset === 'YTD') {
    cutoff = new Date(last.getFullYear(), 0, 1);
  } else {
    cutoff = new Date(last);
    cutoff.setMonth(cutoff.getMonth() - PRESET_MONTHS[preset]);
  }
  const idx = data.findIndex((b) => new Date(b.date) >= cutoff);
  return idx < 0 ? 0 : idx;
}

export function CandlestickChart({
  data,
  srLevels = [],
  zones = [],
  gateTargetLevels = [],
  gateTargetLookbackBars = 0,
  gateTargetLoading = false,
  gateTargetError = false,
  showGateTraffic = false,
  onShowGateTrafficChange,
  tradeSetup,
  currentPrice,
  paperTrades = [],
}: CandlestickChartProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const tooltipRef = useRef<HTMLDivElement>(null);
  const tooltipState = useRef<TooltipState>({ visible: false, x: 0, y: 0, bar: null });
  const crosshairRef = useRef<{ x: number; y: number } | null>(null);
  const animFrame = useRef<number>(0);
  const zoomFrame = useRef<number>(0);
  const isPanningRef = useRef<boolean>(false);
  const panStartXRef = useRef<number>(0);

  const [visibleRange, setVisibleRange] = useState<{ start: number; end: number }>({
    start: 0,
    end: data.length,
  });
  const [preset, setPreset] = useState<RangePreset>(DEFAULT_PRESET);

  // Apply the active time-range preset when the data or preset changes (so the
  // default view is a readable window, not the whole multi-year history).
  useEffect(() => {
    setVisibleRange({ start: startIndexForPreset(data, preset), end: data.length });
  }, [data, preset]);

  const draw = useCallback(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container || data.length === 0) return;

    // Clamp visible range to valid bounds
    const start = Math.max(0, visibleRange.start);
    const end = Math.min(data.length, visibleRange.end);
    const visibleData = data.slice(start, end);
    if (visibleData.length === 0) return;

    const dpr = window.devicePixelRatio || 1;
    const rect = container.getBoundingClientRect();
    const W = rect.width;
    const H = CHART_HEIGHT;

    canvas.width = W * dpr;
    canvas.height = H * dpr;
    canvas.style.width = `${W}px`;
    canvas.style.height = `${H}px`;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    ctx.scale(dpr, dpr);
    ctx.clearRect(0, 0, W, H);

    // Current price = explicit prop, else latest close
    const livePrice = currentPrice ?? visibleData[visibleData.length - 1].close;
    // Only the nearest support/resistance are drawn — keep the chart legible
    const markers = nearestSRMarkers(srLevels, zones, livePrice);

    // Margins: trade role labels (Now / Stop / Gate / Entry) outside on the
    // LEFT; pure price ticks on the RIGHT. Support/Resist sit *inside* the
    // plot on their lines, so they don't need exterior gutter space.
    const ml = 56;
    const mr = 58;
    const mt = 12, mb = 32;
    const cw = W - ml - mr;
    const volumeH = VOLUME_PANE_HEIGHT;
    const ch = H - mt - mb - volumeH - PANE_GAP;
    const priceBottom = mt + ch;
    const volumeTop = priceBottom + PANE_GAP;
    const volumeBottom = volumeTop + volumeH;

    // Price range from visible data
    const allPrices = visibleData.flatMap((b) => [b.high, b.low]);
    const srPrices = markers.flatMap((m) => [m.low, m.high]);
    const tradePrices = tradeSetup ? [tradeSetup.entry_price, tradeSetup.stop_loss, tradeSetup.target] : [];
    const allVals = [...allPrices, ...srPrices, ...tradePrices, livePrice];
    const minP = Math.min(...allVals);
    const maxP = Math.max(...allVals);
    const pad = (maxP - minP) * 0.06 || 1;
    const lo = minP - pad;
    const hi = maxP + pad;

    const yScale = (v: number) => mt + ch - ((v - lo) / (hi - lo)) * ch;
    const barW = cw / visibleData.length;
    const candleW = Math.max(barW * 0.65, 1);
    const volumeW = Math.max(barW * 0.65, 1);
    const maxVolume = Math.max(...visibleData.map((b) => Math.max(0, b.volume)), 1);
    const volumeScale = (v: number) => volumeTop + volumeH - (Math.max(0, v) / maxVolume) * volumeH;

    // Grid lines (horizontal) + pure price scale on the RIGHT
    const nTicks = 6;
    ctx.strokeStyle = 'rgba(255,255,255,0.04)';
    ctx.lineWidth = 1;
    ctx.fillStyle = '#6e7484';
    ctx.font = '11px "IBM Plex Mono", ui-monospace, monospace';
    ctx.textAlign = 'left';
    for (let i = 0; i <= nTicks; i++) {
      const v = lo + ((hi - lo) * i) / nTicks;
      const y = yScale(v);
      ctx.beginPath();
      ctx.moveTo(ml, y);
      ctx.lineTo(ml + cw, y);
      ctx.stroke();
      ctx.fillText(formatPrice(v), ml + cw + 6, y + 4);
    }

    // X-axis labels
    ctx.textAlign = 'center';
    const labelInterval = Math.max(Math.floor(visibleData.length / 8), 1);
    for (let i = 0; i < visibleData.length; i += labelInterval) {
      const x = ml + i * barW + barW / 2;
      ctx.fillStyle = '#6e7484';
      ctx.fillText(formatDate(visibleData[i].date), x, H - 6);
    }

    // Volume pane
    ctx.strokeStyle = 'rgba(255,255,255,0.06)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(ml, volumeTop - 9);
    ctx.lineTo(ml + cw, volumeTop - 9);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(ml, volumeBottom);
    ctx.lineTo(ml + cw, volumeBottom);
    ctx.stroke();

    ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
    ctx.fillStyle = '#6e7484';
    ctx.textAlign = 'left';
    ctx.fillText('Volume', ml, volumeTop - 13);
    ctx.textAlign = 'right';
    ctx.fillText(formatLargeNumber(maxVolume), W - 8, volumeTop + 4);

    visibleData.forEach((bar, i) => {
      const x = ml + i * barW + barW / 2;
      const bullish = bar.close >= bar.open;
      const yVolume = volumeScale(bar.volume);
      const hVolume = Math.max(volumeBottom - yVolume, bar.volume > 0 ? 1 : 0);
      ctx.fillStyle = bullish ? 'rgba(47, 157, 178, 0.32)' : 'rgba(227, 106, 88, 0.28)';
      ctx.fillRect(x - volumeW / 2, yVolume, volumeW, hVolume);
    });

    // Gate Target Ladder diagnostic: a right-edge PRICE-traffic profile. It is
    // intentionally one violet channel (not support/resistance colors), and
    // width reflects relative historical bar crossings — never volume.
    const visibleGateLevels = showGateTraffic
      ? gateTargetLevels.filter(
          (level) => level.price_level >= lo && level.price_level <= hi,
        )
      : [];
    const gateProfileMaxWidth = Math.min(cw * 0.24, 160);
    const gateProfileEndX = ml + cw;
    const maxGateTraffic = Math.max(
      ...visibleGateLevels.map((level) => level.traffic_count),
      1,
    );
    const gateProfileRows = visibleGateLevels.map((level) => {
      const width = Math.max(
        3,
        (level.traffic_count / maxGateTraffic) * gateProfileMaxWidth,
      );
      return { level, y: yScale(level.price_level), width };
    });

    if (gateProfileRows.length > 0) {
      ctx.save();
      ctx.strokeStyle = 'rgba(139, 92, 246, 0.22)';
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(gateProfileEndX, mt);
      ctx.lineTo(gateProfileEndX, priceBottom);
      ctx.stroke();

      gateProfileRows.forEach(({ level, y, width }) => {
        const alpha = 0.12 + (level.strength / 100) * 0.24;
        ctx.fillStyle = `rgba(139, 92, 246, ${alpha})`;
        ctx.fillRect(gateProfileEndX - width, y - 1.5, width, 3);
        ctx.fillStyle = 'rgba(196, 181, 253, 0.7)';
        ctx.fillRect(gateProfileEndX - width, y - 1.5, 1, 3);
      });
      if (gateProfileMaxWidth >= 120) {
        ctx.fillStyle = 'rgba(139, 92, 246, 0.78)';
        ctx.font = '9px "IBM Plex Mono", ui-monospace, monospace';
        ctx.textAlign = 'left';
        ctx.fillText(
          'GTL PRICE TRAFFIC · NO VOLUME',
          gateProfileEndX - gateProfileMaxWidth,
          mt + 9,
        );
      }
      ctx.restore();
    }

    // Exterior left-side role labels only; prices live on the right axis.
    // Priority when levels crowd: Now > Stop > Gate > Entry.
    // Best practice: hide Entry when it sits on top of Now — at market, Now
    // *is* the fill; Entry only matters once price has drifted from the scan.
    type LineLabel = {
      y: number;
      text: string;
      color: string;
      weight?: 'normal' | 'bold';
      priority: number;
    };
    const lineLabels: LineLabel[] = [];
    const LABEL_MIN_GAP_PX = 14;
    const nowY = yScale(livePrice);

    // Entry is noise when price hasn't moved off the scan level. ~0.25R or
    // ~12px, whichever is larger, keeps the chart honest without double lines.
    const riskDist = tradeSetup
      ? Math.abs(tradeSetup.entry_price - tradeSetup.stop_loss)
      : 0;
    const entryDriftPx = tradeSetup
      ? Math.abs(yScale(tradeSetup.entry_price) - nowY)
      : 0;
    const entryDriftR = tradeSetup && riskDist > 0
      ? Math.abs(livePrice - tradeSetup.entry_price) / riskDist
      : 0;
    const showEntry = Boolean(
      tradeSetup
      && (entryDriftPx >= 12 || entryDriftR >= 0.25),
    );

    // In-plot S/R labels (drawn after candles so they stay readable). Not
    // exterior margin labels — they ride the line itself, left-aligned.
    type InPlotLabel = { y: number; text: string; color: string; bg: string };
    const inPlotLabels: InPlotLabel[] = [];

    // Nearest support/resistance only (band if it came from a zone).
    // Always draw them — labels live in-plot on the line, so proximity to
    // Now/Stop no longer forces a hide (that was only to free the left gutter).
    markers.forEach((m) => {
      const isSupport = m.role === 'support';
      // Support stays cyan; resistance is a soft coral (warmer than neutral ink,
      // cooler/softer than the stop so the two never read as the same thing).
      const color = isSupport ? '#2f9db2' : '#e89b8c';
      const yMid = yScale(m.price);

      if (m.high > m.low) {
        const yTop = yScale(m.high);
        const rectHeight = Math.max(yScale(m.low) - yTop, 2);
        ctx.fillStyle = isSupport ? 'rgba(47, 157, 178, 0.12)' : 'rgba(232, 155, 140, 0.12)';
        ctx.fillRect(ml, yTop, cw, rectHeight);
      }

      ctx.strokeStyle = color;
      ctx.lineWidth = 1.25;
      ctx.globalAlpha = 0.6;
      ctx.setLineDash([6, 3]);
      ctx.beginPath();
      ctx.moveTo(ml, yMid);
      ctx.lineTo(ml + cw, yMid);
      ctx.stroke();
      ctx.setLineDash([]);
      ctx.globalAlpha = 1;

      inPlotLabels.push({
        y: yMid,
        text: isSupport ? 'Support' : 'Resist',
        color,
        bg: isSupport ? 'rgba(12, 18, 28, 0.78)' : 'rgba(28, 14, 14, 0.78)',
      });
    });

    // Trade setup overlay (drawn before candles so candles render on top)
    if (tradeSetup) {
      const entryY = yScale(tradeSetup.entry_price);
      const stopY = yScale(tradeSetup.stop_loss);
      const targetY = yScale(tradeSetup.target);

      // Stop-loss zone: red semi-transparent rectangle between entry and stop-loss
      const slTop = Math.min(entryY, stopY);
      const slHeight = Math.max(Math.abs(stopY - entryY), 1);
      ctx.fillStyle = 'rgba(227, 106, 88, 0.13)';
      ctx.fillRect(ml, slTop, cw, slHeight);
      // Stop-loss border
      ctx.strokeStyle = 'rgba(227, 106, 88, 0.45)';
      ctx.lineWidth = 1;
      ctx.setLineDash([4, 3]);
      ctx.beginPath();
      ctx.moveTo(ml, stopY);
      ctx.lineTo(ml + cw, stopY);
      ctx.stroke();
      ctx.setLineDash([]);

      // Gate target: a diagnostic marker, not a take-profit zone. Violet ties
      // it to the GTL profile without implying that the trade exits here.
      ctx.strokeStyle = 'rgba(139, 92, 246, 0.65)';
      ctx.lineWidth = 1;
      ctx.setLineDash([2, 3]);
      ctx.beginPath();
      ctx.moveTo(ml, targetY);
      ctx.lineTo(ml + cw, targetY);
      ctx.stroke();
      ctx.setLineDash([]);

      // Entry only when it has drifted from Now — otherwise the two lines
      // stack and fight for the same label slot.
      if (showEntry) {
        ctx.strokeStyle = 'rgba(154, 160, 176, 0.9)';
        ctx.lineWidth = 1.5;
        ctx.setLineDash([6, 4]);
        ctx.beginPath();
        ctx.moveTo(ml, entryY);
        ctx.lineTo(ml + cw, entryY);
        ctx.stroke();
        ctx.setLineDash([]);
        lineLabels.push({
          y: entryY,
          text: 'Entry',
          color: 'rgba(154, 160, 176, 0.95)',
          weight: 'bold',
          priority: 70,
        });
      }

      lineLabels.push(
        { y: stopY, text: 'Stop', color: 'rgba(239, 145, 130, 0.95)', weight: 'bold', priority: 90 },
        { y: targetY, text: 'Gate', color: 'rgba(196, 181, 253, 0.95)', weight: 'bold', priority: 80 },
      );
    }

    // Current price line — the anchor for everything else (drawn on top)
    {
      ctx.strokeStyle = 'rgba(226, 232, 240, 0.9)';
      ctx.lineWidth = 1.25;
      ctx.beginPath();
      ctx.moveTo(ml, nowY);
      ctx.lineTo(ml + cw, nowY);
      ctx.stroke();

      lineLabels.push({
        y: nowY,
        text: 'Now',
        color: 'rgba(226, 232, 240, 0.95)',
        weight: 'bold',
        priority: 100,
      });
    }

    // Left-side role labels. Prefer dropping lower-priority labels over shifting
    // them off their lines (shifted labels look like wrong prices).
    if (lineLabels.length > 0) {
      const byPriority = [...lineLabels].sort((a, b) => b.priority - a.priority);
      const kept: LineLabel[] = [];
      for (const candidate of byPriority) {
        const collides = kept.some(
          (k) => Math.abs(k.y - candidate.y) < LABEL_MIN_GAP_PX,
        );
        if (!collides) kept.push(candidate);
      }
      kept.sort((a, b) => a.y - b.y);

      ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'middle';
      for (const label of kept) {
        ctx.fillStyle = label.color;
        if (label.weight === 'bold') {
          ctx.font = '600 10px "IBM Plex Mono", ui-monospace, monospace';
        } else {
          ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace';
        }
        // Short connector tick from label into the plot edge
        ctx.strokeStyle = label.color;
        ctx.globalAlpha = 0.35;
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(ml - 4, label.y);
        ctx.lineTo(ml, label.y);
        ctx.stroke();
        ctx.globalAlpha = 1;
        ctx.fillText(label.text, ml - 8, label.y);
      }
      ctx.textBaseline = 'alphabetic';
    }

    // Candles
    visibleData.forEach((bar, i) => {
      const x = ml + i * barW + barW / 2;
      const bullish = bar.close >= bar.open;
      const color = bullish ? '#2f9db2' : '#e36a58';

      const yHigh = yScale(bar.high);
      const yLow = yScale(bar.low);
      const yOpen = yScale(bar.open);
      const yClose = yScale(bar.close);

      // Wick
      ctx.strokeStyle = color;
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(x, yHigh);
      ctx.lineTo(x, yLow);
      ctx.stroke();

      // Body
      const bodyTop = Math.min(yOpen, yClose);
      const bodyH = Math.max(Math.abs(yOpen - yClose), 1);
      ctx.fillStyle = color;
      ctx.fillRect(x - candleW / 2, bodyTop, candleW, bodyH);
    });

    // Support / Resist: label rides the line inside the plot, left-aligned.
    // Soft pill so text stays legible over candles without needing the gutter.
    if (inPlotLabels.length > 0) {
      ctx.font = '600 10px "IBM Plex Mono", ui-monospace, monospace';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'middle';
      const padX = 5;
      const padY = 2;
      for (const label of inPlotLabels) {
        const tw = ctx.measureText(label.text).width;
        const bx = ml + 6;
        const by = label.y;
        const bw = tw + padX * 2;
        const bh = 14 + padY;
        ctx.fillStyle = label.bg;
        ctx.beginPath();
        ctx.roundRect(bx, by - bh / 2, bw, bh, 3);
        ctx.fill();
        ctx.fillStyle = label.color;
        ctx.fillText(label.text, bx + padX, by);
      }
      ctx.textBaseline = 'alphabetic';
    }

    // Paper-trade markers: green ↓ entry above the bar at entry price, red ↑
    // exit below the bar at close price (classic execution-chart convention).
    if (paperTrades.length > 0) {
      const ENTRY_GREEN = '#34d399';
      const EXIT_RED = '#f87171';
      for (const pt of paperTrades) {
        const entryAbs = barIndexForEvent(data, pt.opened_at);
        if (entryAbs >= start && entryAbs < end) {
          const local = entryAbs - start;
          const x = ml + local * barW + barW / 2;
          const bar = data[entryAbs];
          // Sit the tip on the entry price, but keep the arrow above the high
          // so it stays readable when entry is mid-body.
          const yPrice = yScale(pt.entry_price);
          const yHigh = yScale(bar.high);
          const y = Math.min(yPrice, yHigh) - 2;
          if (y >= mt - 2 && y <= priceBottom + 4) {
            drawEntryArrow(ctx, x, y, ENTRY_GREEN);
          }
        }

        if (
          pt.status === 'closed'
          && pt.closed_at
          && pt.close_price != null
        ) {
          const closeAbs = barIndexForEvent(data, pt.closed_at);
          if (closeAbs >= start && closeAbs < end) {
            const local = closeAbs - start;
            const x = ml + local * barW + barW / 2;
            const bar = data[closeAbs];
            const yPrice = yScale(pt.close_price);
            const yLow = yScale(bar.low);
            const y = Math.max(yPrice, yLow) + 2;
            if (y >= mt - 4 && y <= priceBottom + 6) {
              drawExitArrow(ctx, x, y, EXIT_RED);
            }
          }
        }
      }
    }

    // Store geometry for hit testing (includes visibleRange offset)
    (canvas as any).__chartMeta = {
      ml,
      mr,
      mt,
      mb,
      cw,
      ch,
      barW,
      lo,
      hi,
      yScale,
      visibleStart: start,
      volumeTop,
      volumeH,
      volumeBottom,
      gateProfile: gateProfileRows.length > 0
        ? {
            startX: gateProfileEndX - gateProfileMaxWidth,
            endX: gateProfileEndX,
            rows: gateProfileRows,
          }
        : null,
    };

    // Size the overlay canvas to match
    const overlay = overlayCanvasRef.current;
    if (overlay) {
      overlay.width = W * dpr;
      overlay.height = H * dpr;
      overlay.style.width = `${W}px`;
      overlay.style.height = `${H}px`;
    }
  }, [
    currentPrice,
    data,
    gateTargetLevels,
    paperTrades,
    showGateTraffic,
    srLevels,
    tradeSetup,
    visibleRange,
    zones,
  ]);

  const drawCrosshair = useCallback(() => {
    const overlay = overlayCanvasRef.current;
    const canvas = canvasRef.current;
    if (!overlay || !canvas) return;

    const dpr = window.devicePixelRatio || 1;
    const ctx = overlay.getContext('2d');
    if (!ctx) return;

    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, overlay.width, overlay.height);

    const pos = crosshairRef.current;
    if (!pos) return;

    const meta = (canvas as any).__chartMeta;
    if (!meta) return;

    const { ml, mt, mb, cw, ch, barW, lo, hi, visibleStart, volumeBottom } = meta;
    const H = overlay.height / dpr;
    const priceBottom = mt + ch;
    const chartBottom = volumeBottom ?? priceBottom;

    // Clamp crosshair to chart area
    const cx = Math.max(ml, Math.min(ml + cw, pos.x));
    const cy = Math.max(mt, Math.min(chartBottom, pos.y));

    // Dashed crosshair lines
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
    ctx.lineWidth = 0.75;
    ctx.setLineDash([4, 3]);

    // Vertical line
    ctx.beginPath();
    ctx.moveTo(cx, mt);
    ctx.lineTo(cx, chartBottom);
    ctx.stroke();

    ctx.setLineDash([]);

    ctx.font = '11px "IBM Plex Mono", ui-monospace, monospace';
    const labelPadX = 5;
    const labelPadY = 3;

    if (cy <= priceBottom) {
      // Horizontal price crosshair only belongs in the price pane.
      ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';
      ctx.lineWidth = 0.75;
      ctx.setLineDash([4, 3]);
      ctx.beginPath();
      ctx.moveTo(ml, cy);
      ctx.lineTo(ml + cw, cy);
      ctx.stroke();
      ctx.setLineDash([]);

      // Price label on y-axis (right side)
      const price = hi - ((cy - mt) / ch) * (hi - lo);
      const priceText = formatPrice(price);
      const priceMetrics = ctx.measureText(priceText);
      const labelW = priceMetrics.width + labelPadX * 2;
      const labelH = 16 + labelPadY * 2;
      const labelX = ml + cw + 2;
      const labelY = cy - labelH / 2;

      ctx.fillStyle = 'rgba(35, 39, 51, 0.95)';
      ctx.beginPath();
      ctx.roundRect(labelX, labelY, labelW, labelH, 3);
      ctx.fill();
      ctx.fillStyle = '#edeef3';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'middle';
      ctx.fillText(priceText, labelX + labelPadX, cy);
    }

    // Date label on x-axis (bottom)
    const localIdx = Math.floor((cx - ml) / barW);
    const absIdx = (visibleStart ?? 0) + localIdx;
    if (absIdx >= 0 && absIdx < data.length) {
      const dateText = formatDate(data[absIdx].date);
      const dateMetrics = ctx.measureText(dateText);
      const dateLabelW = dateMetrics.width + labelPadX * 2;
      const dateLabelH = 16 + labelPadY * 2;
      const dateLabelX = cx - dateLabelW / 2;
      const dateLabelY = H - mb + 2;

      ctx.fillStyle = 'rgba(35, 39, 51, 0.95)';
      ctx.beginPath();
      ctx.roundRect(dateLabelX, dateLabelY, dateLabelW, dateLabelH, 3);
      ctx.fill();
      ctx.fillStyle = '#edeef3';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(dateText, cx, dateLabelY + dateLabelH / 2);
    }
  }, [data]);

  useEffect(() => {
    draw();
    const onResize = () => {
      cancelAnimationFrame(animFrame.current);
      animFrame.current = requestAnimationFrame(() => {
        draw();
        drawCrosshair();
      });
    };
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('resize', onResize);
      cancelAnimationFrame(animFrame.current);
    };
  }, [draw, drawCrosshair]);

  // Prevent default scroll on wheel events over the overlay canvas
  useEffect(() => {
    const overlay = overlayCanvasRef.current;
    if (!overlay) return;
    const preventScroll = (e: WheelEvent) => {
      if (data.length >= MIN_VISIBLE_BARS) {
        e.preventDefault();
      }
    };
    overlay.addEventListener('wheel', preventScroll, { passive: false });
    return () => overlay.removeEventListener('wheel', preventScroll);
  }, [data.length]);

  const handleWheel = useCallback(
    (e: React.WheelEvent<HTMLCanvasElement>) => {
      // Disable zoom if dataset has fewer than 10 bars
      if (data.length < MIN_VISIBLE_BARS) return;

      cancelAnimationFrame(zoomFrame.current);
      zoomFrame.current = requestAnimationFrame(() => {
        const canvas = canvasRef.current;
        if (!canvas) return;

        const meta = (canvas as any).__chartMeta;
        if (!meta) return;

        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left;

        // Determine cursor position as fraction within the visible chart area
        const cursorFraction = Math.max(0, Math.min(1, (mx - meta.ml) / meta.cw));

        setVisibleRange((prev) => {
          const currentWidth = prev.end - prev.start;
          const zoomFactor = 0.1;
          let delta: number;

          if (e.deltaY > 0) {
            // Scroll down → zoom out (widen range)
            delta = Math.max(1, Math.round(currentWidth * zoomFactor));
          } else {
            // Scroll up → zoom in (narrow range)
            delta = -Math.max(1, Math.round(currentWidth * zoomFactor));
          }

          const newWidth = currentWidth + delta;

          // Clamp to min 10 bars, max full dataset
          const clampedWidth = Math.max(MIN_VISIBLE_BARS, Math.min(data.length, newWidth));

          if (clampedWidth === currentWidth) return prev;

          const widthChange = clampedWidth - currentWidth;

          // Distribute the change around the cursor position
          const leftChange = Math.round(widthChange * cursorFraction);
          const rightChange = widthChange - leftChange;

          let newStart = prev.start - leftChange;
          let newEnd = prev.end + rightChange;

          // Clamp to dataset bounds
          if (newStart < 0) {
            newEnd -= newStart;
            newStart = 0;
          }
          if (newEnd > data.length) {
            newStart -= newEnd - data.length;
            newEnd = data.length;
          }
          newStart = Math.max(0, newStart);
          newEnd = Math.min(data.length, newEnd);

          return { start: newStart, end: newEnd };
        });
      });
    },
    [data.length]
  );

  const handleMouseDown = useCallback(
    (e: React.MouseEvent<HTMLCanvasElement>) => {
      // Pan only when zoomed in (visible range < full dataset)
      const currentWidth = visibleRange.end - visibleRange.start;
      if (currentWidth >= data.length) return;

      isPanningRef.current = true;
      panStartXRef.current = e.clientX;
    },
    [data.length, visibleRange]
  );

  const handleMouseMove = useCallback(
    (e: React.MouseEvent<HTMLCanvasElement>) => {
      const canvas = canvasRef.current;
      if (!canvas) return;

      const rect = canvas.getBoundingClientRect();
      const mx = e.clientX - rect.left;
      const my = e.clientY - rect.top;

      // Handle panning
      if (isPanningRef.current) {
        const meta = (canvas as any).__chartMeta;
        if (!meta) return;

        const dx = e.clientX - panStartXRef.current;
        // Convert pixel drag to bar count: negative dx = drag left = shift range right (see later bars)
        const barShift = -Math.round(dx / meta.barW);

        if (barShift !== 0) {
          panStartXRef.current = e.clientX;

          setVisibleRange((prev) => {
            const width = prev.end - prev.start;
            let newStart = prev.start + barShift;
            let newEnd = prev.end + barShift;

            // Clamp to dataset bounds
            if (newStart < 0) {
              newStart = 0;
              newEnd = width;
            }
            if (newEnd > data.length) {
              newEnd = data.length;
              newStart = data.length - width;
            }

            if (newStart === prev.start && newEnd === prev.end) return prev;
            return { start: newStart, end: newEnd };
          });
        }

        // Clear crosshair while panning
        crosshairRef.current = null;
        drawCrosshair();
        return;
      }

      // Update crosshair position and draw
      crosshairRef.current = { x: mx, y: my };
      drawCrosshair();

      // Tooltip logic
      const tip = tooltipRef.current;
      if (!tip || data.length === 0) return;

      const meta = (canvas as any).__chartMeta;
      if (!meta) return;

      const localIdx = Math.floor((mx - meta.ml) / meta.barW);

      // Map local index to the visible data slice
      const visibleStart = meta.visibleStart ?? 0;
      const visibleEnd = Math.min(data.length, visibleStart + Math.round(meta.cw / meta.barW));
      const absIdx = visibleStart + localIdx;

      if (localIdx >= 0 && absIdx < visibleEnd && absIdx < data.length) {
        const bar = data[absIdx];
        tooltipState.current = { visible: true, x: mx, y: my, bar };
        tip.style.display = 'block';
        tip.style.left = `${Math.min(mx + 14, rect.width - 180)}px`;
        tip.style.top = `${Math.max(my - 80, 8)}px`;

        let gateTooltipHtml = '';
        const gateRows = (meta.gateProfile?.rows ?? []) as Array<{
          level: GateTargetLevel;
          y: number;
          width: number;
        }>;
        if (
          meta.gateProfile
          && mx >= meta.gateProfile.startX
          && mx <= meta.gateProfile.endX
        ) {
          const hovered = gateRows
            .filter(
              ({ y, width }) =>
                Math.abs(my - y) <= 5
                && mx >= meta.gateProfile.endX - width - 4,
            )
            .sort((a, b) => Math.abs(my - a.y) - Math.abs(my - b.y))[0];
          if (hovered) {
            const level = hovered.level;
            const sources = (level.sources.length
              ? level.sources
              : [level.detection_method])
              .map((source) => source.replace(/_/g, ' '))
              .join(' + ');
            gateTooltipHtml = `
              <div class="border-t border-violet-400/30 mt-1.5 pt-1.5 text-violet-200 font-medium mb-1">GTL price traffic · not volume</div>
              <div class="grid grid-cols-2 gap-x-3 gap-y-0.5 text-gray-400">
                <span>Price</span><span class="text-right text-violet-200">${formatPrice(level.price_level)}</span>
                <span>Crossings</span><span class="text-right text-gray-200">${level.traffic_count}</span>
                <span>Strength</span><span class="text-right text-gray-200">${level.strength}</span>
                <span>Side</span><span class="text-right text-gray-200">${level.type}</span>
                <span>Source</span><span class="text-right text-gray-200">${sources}</span>
              </div>`;
          }
        }

        // Check if cursor is near trade overlay zone
        let tradeTooltipHtml = '';
        if (tradeSetup && meta.yScale) {
          const entryY = meta.yScale(tradeSetup.entry_price);
          const stopY = meta.yScale(tradeSetup.stop_loss);
          const targetY = meta.yScale(tradeSetup.target);
          const tradeTop = Math.min(entryY, stopY, targetY);
          const tradeBottom = Math.max(entryY, stopY, targetY);
          if (my >= tradeTop - 10 && my <= tradeBottom + 10) {
            tradeTooltipHtml = `
              <div class="border-t border-gray-600 mt-1.5 pt-1.5 text-gray-300 font-medium mb-1">Trade Setup</div>
              <div class="grid grid-cols-2 gap-x-3 gap-y-0.5 text-gray-400">
                <span>Direction</span><span class="text-right text-gray-200">${tradeSetup.direction}</span>
                <span>Entry</span><span class="text-right text-blue-300">${formatPrice(tradeSetup.entry_price)}</span>
                <span>Stop</span><span class="text-right text-red-300">${formatPrice(tradeSetup.stop_loss)}</span>
                <span>Gate target</span><span class="text-right text-violet-200">${formatPrice(tradeSetup.target)}</span>
                <span>R:R</span><span class="text-right text-gray-200">${tradeSetup.rr_ratio.toFixed(2)}</span>
              </div>`;
          }
        }

        tip.innerHTML = `
          <div class="text-gray-300 font-medium mb-1">${formatDate(bar.date)}</div>
          <div class="grid grid-cols-2 gap-x-3 gap-y-0.5 text-gray-400">
            <span>Open</span><span class="text-right text-gray-200">${formatPrice(bar.open)}</span>
            <span>High</span><span class="text-right text-gray-200">${formatPrice(bar.high)}</span>
            <span>Low</span><span class="text-right text-gray-200">${formatPrice(bar.low)}</span>
            <span>Close</span><span class="text-right text-gray-200">${formatPrice(bar.close)}</span>
            <span>Vol</span><span class="text-right text-gray-200" title="${bar.volume.toLocaleString()}">${formatLargeNumber(bar.volume)}</span>
          </div>${gateTooltipHtml}${tradeTooltipHtml}`;
      } else {
        tip.style.display = 'none';
      }
    },
    [data, drawCrosshair, tradeSetup]
  );

  const handleMouseUp = useCallback(() => {
    isPanningRef.current = false;
  }, []);

  const handleMouseLeave = useCallback(() => {
    isPanningRef.current = false;
    crosshairRef.current = null;
    drawCrosshair();
    const tip = tooltipRef.current;
    if (tip) tip.style.display = 'none';
  }, [drawCrosshair]);

  if (data.length === 0) {
    return (
      <div className="flex h-64 items-center justify-center text-gray-500">
        No OHLCV data available
      </div>
    );
  }

  return (
    <div className="w-full">
      <div className="mb-2 flex flex-wrap items-center gap-1">
        {RANGE_PRESETS.map((p) => (
          <button
            key={p}
            type="button"
            onClick={() => {
              // Re-apply the range directly so clicking the active preset still
              // snaps back after a manual wheel-zoom / pan.
              setPreset(p);
              setVisibleRange({ start: startIndexForPreset(data, p), end: data.length });
            }}
            className={`rounded px-2 py-1 text-[11px] font-medium tabular-nums transition-colors ${
              preset === p ? 'bg-white/10 text-blue-300' : 'text-gray-500 hover:text-gray-300'
            }`}
          >
            {p}
          </button>
        ))}
        <span className="ml-1 text-[10px] text-gray-600">scroll to zoom · drag to pan</span>
        <button
          type="button"
          aria-pressed={showGateTraffic}
          onClick={() => onShowGateTrafficChange?.(!showGateTraffic)}
          title={
            'Show the Gate Target Ladder as relative historical price traffic (not volume)'
          }
          className={`ml-auto inline-flex items-center gap-1.5 rounded px-2 py-1 text-[11px] font-medium transition-colors ${
            showGateTraffic
              ? 'bg-violet-400/15 text-violet-200'
              : 'text-gray-500 hover:text-violet-200'
          }`}
        >
          <span
            aria-hidden="true"
            className="h-1.5 w-4 bg-gradient-to-l from-violet-400/80 to-violet-400/10"
          />
          {gateTargetLoading
            ? 'Loading GTL…'
            : gateTargetError
              ? 'GTL unavailable'
              : 'GTL traffic'}
        </button>
      </div>
      <div ref={containerRef} className="relative w-full" style={{ height: CHART_HEIGHT }}>
        <canvas
          ref={canvasRef}
          className="w-full"
          style={{ height: CHART_HEIGHT }}
        />
      <canvas
        ref={overlayCanvasRef}
        className="absolute top-0 left-0 w-full cursor-crosshair"
        style={{ height: CHART_HEIGHT }}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        onMouseLeave={handleMouseLeave}
        onWheel={handleWheel}
      />
        <div
          ref={tooltipRef}
          className="glass absolute pointer-events-none px-3 py-2 text-xs shadow-2xl z-50"
          style={{ display: 'none' }}
        />
      </div>
      {showGateTraffic && gateTargetLevels.length > 0 && (
        <div className="mt-2 flex flex-wrap items-center justify-between gap-x-4 gap-y-1 text-[10px] text-gray-500">
          <span>
            <span className="text-violet-300">GTL price traffic</span>
            {' · '}bar width = relative historical crossings{' · '}not volume
          </span>
          <span className="num text-gray-600">
            {gateTargetLevels.length} proposals
            {gateTargetLookbackBars > 0 ? ` · ${gateTargetLookbackBars} bars` : ''}
          </span>
        </div>
      )}
      {showGateTraffic && !gateTargetLoading && gateTargetLevels.length === 0 && (
        <p className="mt-2 text-[10px] text-gray-600">
          {gateTargetError
            ? 'Gate Target Ladder diagnostic could not be loaded.'
            : 'No Gate Target Ladder proposals are available for this history.'}
        </p>
      )}
    </div>
  );
}
