/**
 * 차트 비교 검증 페이지 v2 — Chart Validation Dashboard
 *
 * ① 업비트 공식 차트(iframe) vs 로컬 lightweight-charts 시각적 비교
 * ② subscribeCrosshairMove를 통한 실시간 수치 크로스헤어 메트릭 패널
 * ③ 최신 20봉 JSON 스냅샷 추출 → 클립보드 복사
 * ④ LLM 수학적 검증 프롬프트 자동 생성 (팝업 → 복사)
 */
import React, {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import {
  Alert,
  Autocomplete,
  Box,
  Button,
  Chip,
  CircularProgress,
  Collapse,
  Dialog,
  DialogContent,
  DialogTitle,
  Divider,
  FormControl,
  IconButton,
  InputLabel,
  ListSubheader,
  MenuItem,
  Paper,
  Select,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  TextField,
  Tooltip,
  Typography,
  useTheme,
} from '@mui/material';
import {
  AutoAwesome as AiIcon,
  CheckCircle as CheckIcon,
  Close as CloseIcon,
  CompareArrows as CompareIcon,
  ContentCopy as CopyIcon,
  ExpandLess as ExpandLessIcon,
  ExpandMore as ExpandMoreIcon,
  OpenInNew as OpenInNewIcon,
  Refresh as RefreshIcon,
  WifiTethering as WsIcon,
} from '@mui/icons-material';
import {
  ColorType,
  IChartApi,
  ISeriesApi,
  LineStyle,
  MouseEventParams,
  createChart,
} from 'lightweight-charts';
import {
  fetchCandlesOptimized,
  fetchIndicatorsOptimized,
} from '../../services/optimizedCandleApi';
import type { CandleData, IndicatorData } from '../../services/backtestApi';
import { mapWsIndicatorsToChartFormat } from '../../utils/investmentChart/wsIndicatorMap';
import { useMarketDataWebSocket } from '../../hooks/useMarketDataWebSocket';

// ─── 상수 ────────────────────────────────────────────────────────────────────

const INTERVALS = [
  { value: '1m', label: '1분' },
  { value: '3m', label: '3분' },
  { value: '5m', label: '5분' },
  { value: '15m', label: '15분' },
  { value: '30m', label: '30분' },
  { value: '1h', label: '1시간' },
  { value: '4h', label: '4시간' },
  { value: '1d', label: '일봉' },
];

const DEFAULT_MARKET = 'KRW-BTC';
const CANDLE_LIMIT = 200;

const POPULAR_MARKETS = [
  'KRW-BTC', 'KRW-ETH', 'KRW-XRP', 'KRW-SOL', 'KRW-ADA',
  'KRW-DOGE', 'KRW-AVAX', 'KRW-DOT', 'KRW-MATIC', 'KRW-LINK',
  'KRW-TRX', 'KRW-ATOM', 'KRW-SUI', 'KRW-NEAR', 'KRW-APT',
];

const getUpbitChartUrl = (market: string) =>
  `https://upbit.com/exchange?code=CRIX.UPBIT.${market}`;

/** 보조지표 드롭다운 옵션 */
interface IndicatorOption {
  id: string;
  label: string;
  group: 'overlay' | 'subpanel' | 'none';
}
const INDICATOR_OPTIONS: IndicatorOption[] = [
  { id: '',          label: '없음 (캔들만)',          group: 'none' },
  { id: 'ma5',       label: 'MA5  (이동평균 5)',      group: 'overlay' },
  { id: 'ma10',      label: 'MA10 (이동평균 10)',     group: 'overlay' },
  { id: 'ma20',      label: 'MA20 (이동평균 20)',     group: 'overlay' },
  { id: 'ma60',      label: 'MA60 (이동평균 60)',     group: 'overlay' },
  { id: 'bb',        label: '볼린저 밴드 (20,2)',     group: 'overlay' },
  { id: 'macd',      label: 'MACD (12,26,9)',         group: 'subpanel' },
  { id: 'rsi',       label: 'RSI (9)',                group: 'subpanel' },
  { id: 'stoch',     label: 'Stochastic Slow (12,5,5)', group: 'subpanel' },
  { id: 'cci',       label: 'CCI (13)',               group: 'subpanel' },
  { id: 'obv',       label: 'OBV (9)',                group: 'subpanel' },
  { id: 'adx',       label: 'ADX (11)',               group: 'subpanel' },
  { id: 'williamsR', label: 'Williams %R (11)',        group: 'subpanel' },
];

/** 보조지표 ID → TradingView study 이름 */
const TV_STUDY_MAP: Record<string, string> = {
  ma5:       'MASimple@tv-basicstudies',
  ma10:      'MASimple@tv-basicstudies',
  ma20:      'MASimple@tv-basicstudies',
  ma60:      'MASimple@tv-basicstudies',
  bb:        'BB@tv-basicstudies',
  macd:      'MACD@tv-basicstudies',
  rsi:       'RSI@tv-basicstudies',
  stoch:     'Stoch@tv-basicstudies',
  cci:       'CCI@tv-basicstudies',
  obv:       'OBV@tv-basicstudies',
  adx:       'ADX@tv-basicstudies',
  williamsR: 'WilliamsR@tv-basicstudies',
};

/** 업비트 market 코드 → TradingView 심볼 (UPBIT:BTCKRW) */
const toTVSymbol = (market: string): string => {
  // KRW-BTC → UPBIT:BTCKRW
  const [quote, base] = market.split('-');
  return `UPBIT:${base}${quote}`;
};

/** 인터벌 코드 → TradingView interval 파라미터 */
const toTVInterval = (iv: string): string => {
  const map: Record<string, string> = {
    '1m': '1', '3m': '3', '5m': '5', '10m': '10', '15m': '15',
    '30m': '30', '1h': '60', '4h': '240', '1d': 'D', '1w': 'W',
  };
  return map[iv] ?? '15';
};

// ── TV 범위 헬퍼 ─────────────────────────────────────────────────────────────

/** 인터벌 × 봉 수 → TV range preset 문자열 (24/7 크립토 기준) */
function getTVRangeStr(interval: string, count = 200): string {
  const mins: Record<string, number> = {
    '1m': 1, '3m': 3, '5m': 5, '10m': 10, '15m': 15,
    '30m': 30, '1h': 60, '4h': 240, '1d': 1440, '1w': 10080,
  };
  const totalHours = ((mins[interval] ?? 15) * count) / 60;
  if (totalHours <= 12)  return '1D';
  if (totalHours <= 72)  return '5D';
  if (totalHours <= 360) return '1M';
  if (totalHours <= 1080) return '3M';
  return '6M';
}

/** range preset → 초 단위 기간 (24/7 크립토 기준) */
function getTVRangeSecs(range: string): number {
  const map: Record<string, number> = {
    '1D': 86_400, '5D': 432_000, '1M': 2_592_000,
    '3M': 7_776_000, '6M': 15_552_000,
  };
  return map[range] ?? 432_000;
}

/** 현재 시각 기준 TV 예상 visible range { from, to } (Unix 초) */
function calcTVExpectedRange(interval: string, count = 200): { from: number; to: number } {
  const range = getTVRangeStr(interval, count);
  const secs  = getTVRangeSecs(range);
  const now   = Math.floor(Date.now() / 1000);
  return { from: now - secs, to: now };
}

/** TradingView 위젯 iframe URL 생성 */
const getTVWidgetUrl = (market: string, interval: string, isDark: boolean, selectedIndicator: string) => {
  const symbol    = encodeURIComponent(toTVSymbol(market));
  const iv        = toTVInterval(interval);
  const theme     = isDark ? 'dark' : 'light';
  const studyName = TV_STUDY_MAP[selectedIndicator];
  const studies   = studyName ? encodeURIComponent(JSON.stringify([studyName])) : '%5B%5D';
  const range     = getTVRangeStr(interval, 200);
  return `https://s.tradingview.com/widgetembed/?frameElementId=tv_widget`
    + `&symbol=${symbol}&interval=${iv}&theme=${theme}`
    + `&style=1&locale=ko&allow_symbol_change=false`
    + `&save_image=false&hide_top_toolbar=0&hide_legend=0`
    + `&range=${range}`
    + `&studies=${studies}&studies_overrides=%7B%7D`;
};

// ─── 타입 정의 ────────────────────────────────────────────────────────────────

/** 크로스헤어 스냅샷 */
interface CrosshairSnapshot {
  timeLabel: string;
  timeUnix: number;
  candle: { open: number; high: number; low: number; close: number; volume: number } | null;
  indicators: Partial<IndicatorData>;
}

/** 수치 비교 컬럼 */
interface MetricRow {
  label: string;
  field: string;
  color: string;
  warn?: (v: number) => boolean;
  format?: (v: number) => string;
}

const fmt2 = (v: number) => v.toFixed(2);
const fmtInt = (v: number) => Math.round(v).toLocaleString();
const fmtPct = (v: number) => `${v.toFixed(2)}%`;

const METRIC_ROWS: MetricRow[] = [
  // OHLCV (raw upbit data)
  { label: '종가', field: 'close', color: '#6b7280', format: fmtInt },
  { label: '시가', field: 'open', color: '#6b7280', format: fmtInt },
  { label: '고가', field: 'high', color: '#ef4444', format: fmtInt },
  { label: '저가', field: 'low', color: '#3b82f6', format: fmtInt },
  { label: '거래량', field: 'volume', color: '#8b5cf6', format: fmtInt },
  // 계산 지표
  { label: 'MA5', field: 'ma5', color: '#22c55e', format: fmtInt },
  { label: 'MA10', field: 'ma10', color: '#00E676', format: fmtInt },
  { label: 'MA20', field: 'ma20', color: '#2962FF', format: fmtInt },
  { label: 'MA60', field: 'ma60', color: '#00E5FF', format: fmtInt },
  { label: 'BB 상한', field: 'bollingerUpper', color: '#9C27B0', format: fmtInt },
  { label: 'BB 중간', field: 'bollingerMiddle', color: '#9C27B0', format: fmtInt },
  { label: 'BB 하한', field: 'bollingerLower', color: '#9C27B0', format: fmtInt },
  { label: 'RSI', field: 'rsi', color: '#E91E63', format: fmt2, warn: (v) => v > 70 || v < 30 },
  { label: 'MACD', field: 'macdLine', color: '#3b82f6', format: fmt2 },
  { label: 'Signal', field: 'signalLine', color: '#f59e0b', format: fmt2 },
  { label: 'Hist', field: 'macdHistogram', color: '#FF6B35', format: fmt2 },
  { label: '%K', field: 'stochasticK', color: '#3b82f6', format: fmt2, warn: (v) => v > 80 || v < 20 },
  { label: '%D', field: 'stochasticD', color: '#f59e0b', format: fmt2, warn: (v) => v > 80 || v < 20 },
  { label: 'CCI', field: 'cci', color: '#4CAF50', format: fmt2, warn: (v) => v > 100 || v < -100 },
  { label: 'ADX', field: 'adx', color: '#607D8B', format: fmt2, warn: (v) => v > 25 },
  { label: '+DI', field: 'plusDI', color: '#22c55e', format: fmt2 },
  { label: '-DI', field: 'minusDI', color: '#ef4444', format: fmt2 },
  { label: 'OBV', field: 'obv', color: '#795548', format: fmtInt },
  { label: 'W%R', field: 'williamsR', color: '#F44336', format: fmt2, warn: (v) => v > -20 || v < -80 },
];

// ─── 데이터 로드 훅 ───────────────────────────────────────────────────────────

interface LocalChartData {
  candles: CandleData[];
  indicators: IndicatorData[];
  loading: boolean;
  error: string | null;
}

function useLocalChartData(market: string, interval: string) {
  const [data, setData] = useState<LocalChartData>({
    candles: [], indicators: [], loading: false, error: null,
  });

  const fetchData = useCallback(async () => {
    setData((prev) => ({ ...prev, loading: true, error: null }));
    try {
      const candles = await fetchCandlesOptimized({ symbol: market, interval, count: CANDLE_LIMIT });
      const indicators = await fetchIndicatorsOptimized({
        symbol: market, interval, candles,
        maPeriods: [5, 10, 20, 60, 120],
        indicatorSettings: {
          bollingerPeriod: 20, bollingerStdDev: 2.0,
          rsiPeriod: 9, rsiSignalPeriod: 9,
          macdFastPeriod: 12, macdSlowPeriod: 26, macdSignalPeriod: 9,
          stochasticKPeriod: 12, stochasticDPeriod: 5, stochasticSlowing: 5,
          cciPeriod: 13, cciSignalPeriod: 10,
          adxPeriod: 11, dmiPeriod: 14,
          obvPeriod: 9, obvSignalPeriod: 9,
          williamsRPeriod: 11,
        },
      });
      setData({ candles, indicators, loading: false, error: null });
    } catch (err: any) {
      setData((prev) => ({ ...prev, loading: false, error: err?.message || '데이터 로드 실패' }));
    }
  }, [market, interval]);

  useEffect(() => { fetchData(); }, [fetchData]);
  return { ...data, refetch: fetchData };
}

// ─── 유틸: 타임스탬프 → Unix초 ───────────────────────────────────────────────

const toUnixSec = (t: string | number | undefined | null): number => {
  if (t == null) return 0;
  if (typeof t === 'number') return t > 1e10 ? Math.floor(t / 1000) : Math.floor(t);
  if (typeof t !== 'string' || t === '') return 0;
  // 숫자형 문자열 (Unix epoch ms 또는 s) — WS timestamp 직렬화 대응
  const n = Number(t);
  if (Number.isFinite(n) && n > 0) return n > 1e10 ? Math.floor(n / 1000) : Math.floor(n);
  // ISO 8601 문자열
  try {
    const iso = t.endsWith('Z') ? t : t + 'Z';
    const ms = new Date(iso).getTime();
    return isNaN(ms) ? 0 : Math.floor(ms / 1000);
  } catch {
    return 0;
  }
};

// ─── JS Reference Calculator (백엔드 값과 비교할 기준 계산) ─────────────────────

function ema(data: number[], period: number): number[] {
  const k = 2 / (period + 1);
  const result: number[] = [data[0]];
  for (let i = 1; i < data.length; i++) result.push(data[i] * k + result[i - 1] * (1 - k));
  return result;
}

function refMA(closes: number[], period: number): (number | null)[] {
  return closes.map((_, i) => {
    if (i < period - 1) return null;
    return closes.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0) / period;
  });
}

function refRSI(closes: number[], period: number): (number | null)[] {
  if (closes.length < period + 1) return closes.map(() => null);
  const gains: number[] = [], losses: number[] = [];
  for (let i = 1; i < closes.length; i++) {
    const d = closes[i] - closes[i - 1];
    gains.push(Math.max(d, 0)); losses.push(Math.max(-d, 0));
  }
  const result: (number | null)[] = [null];
  let ag = gains.slice(0, period).reduce((a, b) => a + b, 0) / period;
  let al = losses.slice(0, period).reduce((a, b) => a + b, 0) / period;
  for (let i = 0; i < period - 1; i++) result.push(null);
  for (let i = period - 1; i < gains.length; i++) {
    if (i > period - 1) { ag = (ag * (period - 1) + gains[i]) / period; al = (al * (period - 1) + losses[i]) / period; }
    const rs = al === 0 ? Infinity : ag / al;
    result.push(rs === Infinity ? 100 : 100 - 100 / (1 + rs));
  }
  return result;
}

function refMACD(closes: number[], fast = 12, slow = 26, signal = 9) {
  const ef = ema(closes, fast), es = ema(closes, slow);
  const macd = ef.map((v, i) => v - es[i]);
  const sig = ema(macd, signal);
  return macd.map((v, i) => ({ macd: v, signal: sig[i], hist: v - sig[i] }));
}

function refBB(closes: number[], period = 20, mult = 2.0): { upper: number | null; middle: number | null; lower: number | null }[] {
  return closes.map((_, i) => {
    if (i < period - 1) return { upper: null, middle: null, lower: null };
    const slice = closes.slice(i - period + 1, i + 1);
    const mean = slice.reduce((a, b) => a + b, 0) / period;
    const std = Math.sqrt(slice.reduce((a, b) => a + (b - mean) ** 2, 0) / period);
    return { upper: mean + mult * std, middle: mean, lower: mean - mult * std };
  });
}

function refStochK(highs: number[], lows: number[], closes: number[], kPeriod = 12): (number | null)[] {
  return closes.map((_, i) => {
    if (i < kPeriod - 1) return null;
    const h = Math.max(...highs.slice(i - kPeriod + 1, i + 1));
    const l = Math.min(...lows.slice(i - kPeriod + 1, i + 1));
    return h === l ? 50 : ((closes[i] - l) / (h - l)) * 100;
  });
}

function refCCI(highs: number[], lows: number[], closes: number[], period = 13): (number | null)[] {
  return closes.map((_, i) => {
    if (i < period - 1) return null;
    const tps = highs.slice(i - period + 1, i + 1).map((h, j) => (h + lows[i - period + 1 + j] + closes[i - period + 1 + j]) / 3);
    const meanTp = tps.reduce((a, b) => a + b, 0) / period;
    const meanDev = tps.reduce((a, b) => a + Math.abs(b - meanTp), 0) / period;
    return meanDev === 0 ? 0 : (tps[tps.length - 1] - meanTp) / (0.015 * meanDev);
  });
}

/** 선택된 지표의 JS 기준 계산값 배열 반환 */
function computeRefValues(id: string, candles: CandleData[]): (number | null)[] {
  if (candles.length === 0) return [];
  const closes = candles.map((c) => c.close);
  const highs  = candles.map((c) => c.high);
  const lows   = candles.map((c) => c.low);
  switch (id) {
    case 'ma5':  return refMA(closes, 5);
    case 'ma10': return refMA(closes, 10);
    case 'ma20': return refMA(closes, 20);
    case 'ma60': return refMA(closes, 60);
    case 'bb':   return refBB(closes).map((v) => v.middle);
    case 'rsi':  return refRSI(closes, 9);
    case 'macd': return refMACD(closes).map((v) => v.macd);
    case 'stoch': return refStochK(highs, lows, closes, 12);
    case 'cci':   return refCCI(highs, lows, closes, 13);
    default: return candles.map(() => null);
  }
}

/** 백엔드 indicator에서 선택 지표 스칼라값 추출 */
function getBackendValue(id: string, row: IndicatorData): number | null {
  if (!row) return null;
  switch (id) {
    case 'ma5':  return (row as any).ma5 ?? null;
    case 'ma10': return (row as any).ma10 ?? null;
    case 'ma20': return (row as any).ma20 ?? null;
    case 'ma60': return (row as any).ma60 ?? null;
    case 'bb':   return (row as any).bollingerMiddle ?? null;
    case 'rsi':  return (row as any).rsi ?? null;
    case 'macd': return (row as any).macdLine ?? (row as any).macd ?? null;
    case 'stoch': return (row as any).stochasticK ?? (row as any).stochK ?? null;
    case 'cci':   return (row as any).cci ?? (row as any).cci13 ?? null;
    default: return null;
  }
}

// ─── 로컬 차트 컴포넌트 ───────────────────────────────────────────────────────

/** 부모가 LocalChart를 직접 제어할 수 있는 핸들 */
export interface LocalChartHandle {
  /** 지정 unix초 위치로 크로스헤어를 이동시킨다 */
  setCrosshairAtTime: (timeUnix: number) => void;
  /** SubPanels 연결을 위한 IChartApi 반환 */
  getChartRef: () => IChartApi | null;
}

interface LocalChartProps {
  candles: CandleData[];
  indicators: IndicatorData[];
  activeIndicators: Set<string>;
  liveCandle: CandleData | null;
  isDark: boolean;
  onCrosshairMove: (snapshot: CrosshairSnapshot | null) => void;
  /** 크로스헤어 X/Y 비율(0~1)과 unix초 방출 */
  onCrosshairXPct?: (xPct: number | null, yPct: number | null, timeUnix: number | null) => void;
  /** 차트 가시 시간 범위 변경 시 방출 */
  onVisibleRangeChange?: (range: { from: number; to: number } | null) => void;
  /** 서브패널 크로스헤어 동기화용 unix초 (부모 제어) */
  crosshairTimeUnix?: number | null;
  /**
   * 데이터 로드 직후 TV 차트와 동일한 초기 visible range 강제 적용.
   * 설정 시 차트가 이 범위를 보여주도록 scrollTo 실행.
   */
  initialVisibleRange?: { from: number; to: number } | null;
}

const LocalChart = React.forwardRef<LocalChartHandle, LocalChartProps>(function LocalChart({
  candles, indicators, activeIndicators, liveCandle, isDark,
  onCrosshairMove, onCrosshairXPct, onVisibleRangeChange,
  crosshairTimeUnix = null,
  initialVisibleRange = null,
}, ref) {
  const containerRef = useRef<HTMLDivElement>(null);
  const chartRef = useRef<IChartApi | null>(null);
  const candleSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
  const volumeSeriesRef = useRef<ISeriesApi<'Histogram'> | null>(null);
  const maSeriesRef = useRef<Map<string, ISeriesApi<'Line'>>>(new Map());
  const bbSeriesRef = useRef<{ upper: ISeriesApi<'Line'> | null; middle: ISeriesApi<'Line'> | null; lower: ISeriesApi<'Line'> | null; }>({ upper: null, middle: null, lower: null });

  // crosshairMove 에서 사용할 최신 데이터를 ref로 유지
  const candlesRef = useRef(candles);
  candlesRef.current = candles;
  const indicatorsRef = useRef(indicators);
  indicatorsRef.current = indicators;
  const onCrosshairMoveRef = useRef(onCrosshairMove);
  onCrosshairMoveRef.current = onCrosshairMove;
  const onCrosshairXPctRef = useRef(onCrosshairXPct);
  onCrosshairXPctRef.current = onCrosshairXPct;
  const onVisibleRangeChangeRef = useRef(onVisibleRangeChange);
  onVisibleRangeChangeRef.current = onVisibleRangeChange;

  // 외부에서 크로스헤어 위치 제어 + chartRef 노출
  React.useImperativeHandle(ref, () => ({
    getChartRef: () => chartRef.current,
    setCrosshairAtTime: (timeUnix: number) => {
      const chart = chartRef.current;
      const series = candleSeriesRef.current;
      if (!chart || !series) return;
      const cs = candlesRef.current;
      let nearest: CandleData | null = null;
      let minDiff = Infinity;
      for (const c of cs) {
        const ct = toUnixSec(c.time);
        if (ct === 0) continue;
        const d = Math.abs(ct - timeUnix);
        if (d < minDiff) { minDiff = d; nearest = c; }
      }
      try {
        (chart as any).setCrosshairPosition(nearest?.close ?? 0, timeUnix as any, series);
      } catch {}
    },
  }));

  const chartBg = isDark ? '#0e0e1a' : '#ffffff';
  const textColor = isDark ? '#9ca3af' : '#374151';
  const gridColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';

  const toChartCandle = (c: CandleData) => {
    const t = toUnixSec(c.time);
    // time=0이면 현재 봉 직전 시간으로 대체하여 null 반환 제거 (TypeScript 타입 안정성)
    return { time: (t || Math.floor(Date.now() / 1000) - 1) as any, open: c.open, high: c.high, low: c.low, close: c.close };
  };

  const toLinePoint = (d: IndicatorData, value: number | undefined | null) => {
    const t = toUnixSec(d.time);
    if (t === 0 || value == null || isNaN(value)) return null;
    return { time: t as any, value };
  };

  // 메인 차트 초기화 + crosshairMove 구독
  useEffect(() => {
    if (!containerRef.current) return;

    const chart = createChart(containerRef.current, {
      width: containerRef.current.clientWidth,
      height: 360,
      layout: { background: { type: ColorType.Solid, color: chartBg }, textColor },
      grid: { vertLines: { color: gridColor }, horzLines: { color: gridColor } },
      crosshair: { mode: 1 },
      rightPriceScale: { borderColor: gridColor },
      timeScale: { borderColor: gridColor, timeVisible: true, secondsVisible: false },
    });

    chartRef.current = chart;
    candleSeriesRef.current = chart.addCandlestickSeries({
      upColor: '#ef4444', downColor: '#3b82f6',
      borderUpColor: '#ef4444', borderDownColor: '#3b82f6',
      wickUpColor: '#ef4444', wickDownColor: '#3b82f6',
    });

    // 볼륨 히스토그램 시리즈 (우측 보조 스케일)
    volumeSeriesRef.current = chart.addHistogramSeries({
      priceFormat: { type: 'volume' },
      priceScaleId: 'volume',
    });
    chart.priceScale('volume').applyOptions({
      scaleMargins: { top: 0.8, bottom: 0 },
    });

    // ── subscribeCrosshairMove ──────────────────────────────────────
    chart.subscribeCrosshairMove((param: MouseEventParams) => {
      if (!param.time || !param.point) {
        onCrosshairMoveRef.current(null);
        onCrosshairXPctRef.current?.(null, null, null);
        return;
      }

      const unixSec = typeof param.time === 'number' ? param.time : 0;
      if (unixSec === 0) {
        onCrosshairMoveRef.current(null);
        onCrosshairXPctRef.current?.(null, null, null);
        return;
      }

      // X/Y 비율 방출 (TV iframe 크로스헤어 동기화용)
      if (containerRef.current) {
        const chartW = containerRef.current.clientWidth;
        const CHART_H = 360; // 차트 고정 높이 (createChart height 옵션과 일치)

        // X: 픽셀 비율 (부모에서 시간 기반으로 재계산하므로 보조값으로만 사용)
        const xPct = param.point.x / chartW;

        // Y: 가격 기반 정규화 — TV 차트의 가격 스케일에 대응하도록
        //   priceAtTop    = 차트 상단(y=0)의 가격  → 화면 최고가
        //   priceAtBottom = 차트 하단(y=H)의 가격  → 화면 최저가
        //   yPct = (maxPrice - price) / (maxPrice - minPrice), 0=상단, 1=하단
        const series = candleSeriesRef.current;
        const priceAtCross  = series?.coordinateToPrice(param.point.y) ?? null;
        const priceAtTop    = series?.coordinateToPrice(0) ?? null;
        const priceAtBottom = series?.coordinateToPrice(CHART_H) ?? null;

        let yPct: number;
        if (
          priceAtCross  != null &&
          priceAtTop    != null &&
          priceAtBottom != null &&
          priceAtTop !== priceAtBottom
        ) {
          yPct = (priceAtTop - priceAtCross) / (priceAtTop - priceAtBottom);
        } else {
          yPct = param.point.y / CHART_H; // fallback: 픽셀 비율
        }

        onCrosshairXPctRef.current?.(
          Math.max(0, Math.min(1, xPct)),
          Math.max(0, Math.min(1, yPct)),
          unixSec,
        );
      }

      // 가장 가까운 캔들 찾기
      const cs = candlesRef.current;
      let nearestCandle: CandleData | null = null;
      let minDiff = Infinity;
      for (const c of cs) {
        const ct = toUnixSec(c.time);
        if (ct === 0) continue;
        const d = Math.abs(ct - unixSec);
        if (d < minDiff) { minDiff = d; nearestCandle = c; }
      }

      // 가장 가까운 indicator row 찾기
      const inds = indicatorsRef.current;
      let nearestInd: IndicatorData | null = null;
      minDiff = Infinity;
      for (const ind of inds) {
        const it = toUnixSec(ind.time);
        if (it === 0) continue;
        const d = Math.abs(it - unixSec);
        if (d < minDiff) { minDiff = d; nearestInd = ind; }
      }

      const timeLabel = new Date(unixSec * 1000).toLocaleString('ko-KR', {
        month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit',
      });

      onCrosshairMoveRef.current({
        timeLabel,
        timeUnix: unixSec,
        candle: nearestCandle
          ? { open: nearestCandle.open, high: nearestCandle.high, low: nearestCandle.low, close: nearestCandle.close, volume: nearestCandle.volume }
          : null,
        indicators: nearestInd ? { ...nearestInd } : {},
      });
    });
    // ───────────────────────────────────────────────────────────────

    // ── subscribeVisibleTimeRangeChange ────────────────────────────
    chart.timeScale().subscribeVisibleTimeRangeChange((range) => {
      if (range) {
        const from = typeof range.from === 'number' ? range.from : Math.floor(new Date(range.from as string).getTime() / 1000);
        const to = typeof range.to === 'number' ? range.to : Math.floor(new Date(range.to as string).getTime() / 1000);
        onVisibleRangeChangeRef.current?.({ from, to });
      } else {
        onVisibleRangeChangeRef.current?.(null);
      }
    });
    // ───────────────────────────────────────────────────────────────

    const ro = new ResizeObserver((entries) => {
      chart.applyOptions({ width: entries[0].contentRect.width });
    });
    ro.observe(containerRef.current);

    return () => {
      ro.disconnect();
      try { chart.remove(); } catch {}
      chartRef.current = null;
      candleSeriesRef.current = null;
      volumeSeriesRef.current = null;
      maSeriesRef.current.clear();
      bbSeriesRef.current = { upper: null, middle: null, lower: null };
    };
  }, [isDark]); // eslint-disable-line react-hooks/exhaustive-deps

  // 캔들 + 볼륨 데이터 주입
  useEffect(() => {
    if (!candleSeriesRef.current || candles.length === 0) return;
    const data = candles.map(toChartCandle).filter((c) => c.time !== 0) as any[];
    candleSeriesRef.current.setData(data);

    if (volumeSeriesRef.current) {
      const volData = data.map((c: any) => {
        const orig = candles.find((x) => toUnixSec(x.time) === c.time);
        return {
          time: c.time,
          value: orig?.volume ?? 0,
          color: (orig?.close ?? 0) >= (orig?.open ?? 0) ? 'rgba(239,68,68,0.35)' : 'rgba(59,130,246,0.35)',
        };
      });
      volumeSeriesRef.current.setData(volData);
    }

    // 초기 로드 후 TV와 동일한 범위로 맞추거나, 없으면 최신 봉으로 이동
    const ts = chartRef.current?.timeScale();
    if (initialVisibleRange && ts) {
      try {
        ts.setVisibleRange({
          from: initialVisibleRange.from as any,
          to:   initialVisibleRange.to   as any,
        });
      } catch {
        ts.scrollToRealTime();
      }
    } else {
      ts?.scrollToRealTime();
    }
  }, [candles]); // eslint-disable-line react-hooks/exhaustive-deps

  // MA 시리즈
  useEffect(() => {
    if (!chartRef.current || indicators.length === 0) return;
    const chart = chartRef.current;
    const MA_CONFIG = [
      { id: 'ma5', field: 'ma5', color: '#22c55e', label: 'MA5' },
      { id: 'ma10', field: 'ma10', color: '#00E676', label: 'MA10' },
      { id: 'ma20', field: 'ma20', color: '#2962FF', label: 'MA20' },
      { id: 'ma60', field: 'ma60', color: '#00E5FF', label: 'MA60' },
    ];
    MA_CONFIG.forEach(({ id, field, color, label }) => {
      if (!activeIndicators.has(id)) {
        const s = maSeriesRef.current.get(id);
        if (s) { try { chart.removeSeries(s); } catch {} maSeriesRef.current.delete(id); }
        return;
      }
      if (!maSeriesRef.current.has(id)) {
        maSeriesRef.current.set(id, chart.addLineSeries({ color, lineWidth: 1, title: label, lastValueVisible: false, priceLineVisible: false }));
      }
      maSeriesRef.current.get(id)!.setData(indicators.map((d) => toLinePoint(d, d[field])).filter(Boolean) as any[]);
    });
  }, [indicators, activeIndicators]); // eslint-disable-line react-hooks/exhaustive-deps

  // 볼린저 밴드
  useEffect(() => {
    if (!chartRef.current || indicators.length === 0) return;
    const chart = chartRef.current;
    const active = activeIndicators.has('bb');
    if (!active) {
      (['upper', 'middle', 'lower'] as const).forEach((k) => {
        const s = bbSeriesRef.current[k];
        if (s) { try { chart.removeSeries(s); } catch {} bbSeriesRef.current[k] = null; }
      });
      return;
    }
    if (!bbSeriesRef.current.upper) {
      bbSeriesRef.current.upper = chart.addLineSeries({ color: '#9C27B0', lineWidth: 1, lineStyle: LineStyle.Dashed, lastValueVisible: false, priceLineVisible: false, title: 'BB상' });
      bbSeriesRef.current.middle = chart.addLineSeries({ color: '#9C27B0', lineWidth: 1, lastValueVisible: false, priceLineVisible: false, title: 'BB중' });
      bbSeriesRef.current.lower = chart.addLineSeries({ color: '#9C27B0', lineWidth: 1, lineStyle: LineStyle.Dashed, lastValueVisible: false, priceLineVisible: false, title: 'BB하' });
    }
    bbSeriesRef.current.upper!.setData(indicators.map((d) => toLinePoint(d, d.bollingerUpper)).filter(Boolean) as any[]);
    bbSeriesRef.current.middle!.setData(indicators.map((d) => toLinePoint(d, d.bollingerMiddle)).filter(Boolean) as any[]);
    bbSeriesRef.current.lower!.setData(indicators.map((d) => toLinePoint(d, d.bollingerLower)).filter(Boolean) as any[]);
  }, [indicators, activeIndicators]); // eslint-disable-line react-hooks/exhaustive-deps

  // 실시간 마지막 봉 업데이트 (캔들 + 볼륨 + 자동스크롤)
  useEffect(() => {
    if (!candleSeriesRef.current || !liveCandle) return;
    const t = toUnixSec(liveCandle.time);
    if (t === 0) return;
    try {
      candleSeriesRef.current.update({
        time: t as any,
        open:   liveCandle.open,
        high:   liveCandle.high,
        low:    liveCandle.low,
        close:  liveCandle.close,
      });
    } catch {}
    // 볼륨 히스토그램 실시간 업데이트
    if (volumeSeriesRef.current && liveCandle.volume != null) {
      try {
        volumeSeriesRef.current.update({
          time:  t as any,
          value: liveCandle.volume,
          color: liveCandle.close >= liveCandle.open ? 'rgba(239,68,68,0.35)' : 'rgba(59,130,246,0.35)',
        });
      } catch {}
    }
    // 차트를 최신 봉으로 자동 스크롤 (사용자가 직접 드래그하지 않은 경우)
    try { chartRef.current?.timeScale().scrollToRealTime(); } catch {}
  }, [liveCandle]);

  return <div ref={containerRef} style={{ width: '100%' }} />;
});

// ─── TV 크로스헤어 오버레이 ───────────────────────────────────────────────────

interface TvCrosshairOverlayProps {
  /** 로컬 차트에서 방출된 X 비율 — 이미 시간 기반으로 정규화됨 (0~1, null=숨김) */
  xPct: number | null;
  /** 로컬 차트에서 방출된 Y 비율 — 이미 가격 기반으로 정규화됨 (0~1, null=숨김) */
  yPct: number | null;
  /** 수직선 하단에 표시할 시간 레이블 */
  timeLabel: string | null;
  /** 크로스헤어 위치의 스냅샷 수치 */
  snapshot: CrosshairSnapshot | null;
  isDark: boolean;
  /** TV 오버레이 위에서 마우스 이동 시 X 비율 방출 */
  onHoverXPct: (xPct: number | null) => void;
  /**
   * TradingView iframe에서 캔들 차트 영역이 차지하는 위아래 비율.
   * TV 위젯 레이아웃: 상단 툴바 + 캔들 영역 + 지표 패널 + 시간축.
   * 보조지표가 없을 때: { top: 0.06, bottom: 0.93 }
   * 보조지표가 있을 때: { top: 0.06, bottom: 0.65 }  (지표 패널 약 30% 차지)
   */
  tvCandleArea: { top: number; bottom: number };
}

const TvCrosshairOverlay: React.FC<TvCrosshairOverlayProps> = ({
  xPct, yPct, timeLabel, snapshot, isDark, onHoverXPct, tvCandleArea,
}) => {
  const overlayRef = useRef<HTMLDivElement>(null);

  const lineColor = isDark ? 'rgba(255,255,255,0.75)' : 'rgba(0,0,0,0.65)';
  const labelBg   = isDark ? 'rgba(14,14,26,0.93)'   : 'rgba(255,255,255,0.95)';
  const labelFg   = isDark ? '#e5e7eb' : '#111827';
  const valueBg   = isDark ? 'rgba(14,14,26,0.90)'   : 'rgba(255,255,255,0.93)';

  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!overlayRef.current) return;
    const rect = overlayRef.current.getBoundingClientRect();
    onHoverXPct(Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)));
  };

  // ── Y 보정: 가격 정규화 yPct → TV iframe 의 실제 픽셀 위치로 변환 ────────
  // yPct=0 → 로컬 차트 상단 가격 → TV 캔들 영역 상단 (tvCandleArea.top)
  // yPct=1 → 로컬 차트 하단 가격 → TV 캔들 영역 하단 (tvCandleArea.bottom)
  const tvYPct = yPct != null
    ? tvCandleArea.top + yPct * (tvCandleArea.bottom - tvCandleArea.top)
    : null;

  const candle   = snapshot?.candle;
  const fmtPrice = (v: number) => v.toLocaleString('ko-KR');

  return (
    <div
      ref={overlayRef}
      onMouseMove={handleMouseMove}
      onMouseLeave={() => onHoverXPct(null)}
      style={{ position: 'absolute', inset: 0, zIndex: 10, cursor: 'crosshair' }}
    >
      {xPct != null && (
        <>
          {/* ── 수직선 (시간 동기화) ── */}
          <div style={{
            position: 'absolute',
            left: `${xPct * 100}%`, top: 0, bottom: 0,
            width: 1, backgroundColor: lineColor, pointerEvents: 'none',
          }} />

          {/* ── 가로선 (가격 동기화 — TV 캔들 영역에 맞게 Y 보정됨) ── */}
          {tvYPct != null && (
            <>
              <div style={{
                position: 'absolute',
                top: `${tvYPct * 100}%`, left: 0, right: 0,
                height: 1, backgroundColor: lineColor, pointerEvents: 'none',
              }} />
              {/* 가격 레이블 (우측 가장자리) */}
              {candle && (
                <div style={{
                  position: 'absolute',
                  top: `${tvYPct * 100}%`, right: 2,
                  transform: 'translateY(-50%)',
                  backgroundColor: candle.close >= candle.open ? '#ef4444' : '#3b82f6',
                  color: '#fff',
                  fontSize: '0.58rem', padding: '1px 4px', borderRadius: 2,
                  whiteSpace: 'nowrap', pointerEvents: 'none',
                  fontFamily: 'monospace', fontWeight: 600,
                }}>
                  {fmtPrice(candle.close)}
                </div>
              )}
            </>
          )}

          {/* ── 시간 레이블 (수직선 하단) ── */}
          {timeLabel && (
            <div style={{
              position: 'absolute',
              left: `${xPct * 100}%`, bottom: 4,
              transform: 'translateX(-50%)',
              backgroundColor: labelBg, color: labelFg,
              fontSize: '0.6rem', padding: '1px 5px', borderRadius: 3,
              whiteSpace: 'nowrap', pointerEvents: 'none',
              border: `1px solid ${lineColor}`, fontFamily: 'monospace',
            }}>
              {timeLabel}
            </div>
          )}

          {/* ── 좌상단 OHLCV 툴팁 (ChartCrosshairTooltip 위임) ── */}
          {snapshot && (
            <ChartCrosshairTooltip
              snapshot={snapshot}
              isDark={isDark}
              label="TV 기준 (로컬 시각)"
              labelColor={isDark ? '#a5b4fc' : '#4f46e5'}
            />
          )}
        </>
      )}
    </div>
  );
};

// ─── 서브패널 ─────────────────────────────────────────────────────────────────

interface SubPanelConfig {
  id: string;
  label: string;
  build: (chart: IChartApi, indicators: IndicatorData[]) => ISeriesApi<any>[];
}

/** time이 유효한 indicator 행만 필터 + lightweight-charts용 포인트 변환 */
const validRows = (
  rows: IndicatorData[],
  valuePicker: (d: IndicatorData) => number | undefined | null,
) =>
  rows
    .map((d) => {
      const t = toUnixSec(d.time);
      const v = valuePicker(d);
      if (t === 0 || v == null || isNaN(v)) return null;
      return { time: t as any, value: v };
    })
    .filter(Boolean) as { time: any; value: number }[];

const SUB_PANEL_CONFIGS: SubPanelConfig[] = [
  {
    id: 'macd', label: 'MACD (12, 26, 9)',
    build: (chart, indicators) => {
      const hist = chart.addHistogramSeries({ lastValueVisible: false, priceLineVisible: false });
      const macdLine = chart.addLineSeries({ color: '#3b82f6', lineWidth: 1, title: 'MACD', lastValueVisible: false, priceLineVisible: false });
      const signal = chart.addLineSeries({ color: '#f59e0b', lineWidth: 1, title: 'Signal', lastValueVisible: false, priceLineVisible: false });
      const histPts = indicators
        .map((d) => {
          const t = toUnixSec(d.time);
          const v = d.macdHistogram ?? d.histogram;
          if (t === 0 || v == null || isNaN(v)) return null;
          return { time: t as any, value: v, color: v >= 0 ? '#ef4444' : '#3b82f6' };
        })
        .filter(Boolean) as any[];
      hist.setData(histPts);
      macdLine.setData(validRows(indicators, (d) => d.macdLine ?? d.macd));
      signal.setData(validRows(indicators, (d) => d.signalLine ?? d.signal ?? d.macdSignal));
      return [hist, macdLine, signal];
    },
  },
  {
    id: 'rsi', label: 'RSI (9)',
    build: (chart, indicators) => {
      const s = chart.addLineSeries({ color: '#E91E63', lineWidth: 1, title: 'RSI', lastValueVisible: false, priceLineVisible: false });
      s.setData(validRows(indicators, (d) => d.rsi));
      s.createPriceLine({ price: 70, color: '#ef4444', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '70' });
      s.createPriceLine({ price: 50, color: '#6b7280', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false, title: '50' });
      s.createPriceLine({ price: 30, color: '#3b82f6', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '30' });
      return [s];
    },
  },
  {
    id: 'stoch', label: 'Stochastic Slow (12,5,5)',
    build: (chart, indicators) => {
      const k = chart.addLineSeries({ color: '#3b82f6', lineWidth: 1, title: '%K', lastValueVisible: false, priceLineVisible: false });
      const dd = chart.addLineSeries({ color: '#f59e0b', lineWidth: 1, title: '%D', lastValueVisible: false, priceLineVisible: false });
      k.setData(validRows(indicators, (d) => d.stochasticK ?? d.stochK));
      dd.setData(validRows(indicators, (d) => d.stochasticD ?? d.stochD));
      k.createPriceLine({ price: 80, color: '#ef4444', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '80' });
      k.createPriceLine({ price: 20, color: '#3b82f6', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '20' });
      return [k, dd];
    },
  },
  {
    id: 'cci', label: 'CCI (13)',
    build: (chart, indicators) => {
      const s = chart.addLineSeries({ color: '#4CAF50', lineWidth: 1, title: 'CCI', lastValueVisible: false, priceLineVisible: false });
      s.setData(validRows(indicators, (d) => d.cci ?? d.cci13));
      s.createPriceLine({ price: 100, color: '#ef4444', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '+100' });
      s.createPriceLine({ price: 0, color: '#6b7280', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false, title: '0' });
      s.createPriceLine({ price: -100, color: '#3b82f6', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '-100' });
      return [s];
    },
  },
  {
    id: 'obv', label: 'OBV (9)',
    build: (chart, indicators) => {
      const s = chart.addLineSeries({ color: '#795548', lineWidth: 1, title: 'OBV', lastValueVisible: false, priceLineVisible: false });
      const sig = chart.addLineSeries({ color: '#FF9800', lineWidth: 1, lineStyle: LineStyle.Dashed, title: 'OBVSig', lastValueVisible: false, priceLineVisible: false });
      s.setData(validRows(indicators, (d) => d.obv));
      sig.setData(validRows(indicators, (d) => d.obvSignal));
      return [s, sig];
    },
  },
  {
    id: 'adx', label: 'ADX (11)',
    build: (chart, indicators) => {
      const s = chart.addLineSeries({ color: '#607D8B', lineWidth: 1, title: 'ADX', lastValueVisible: false, priceLineVisible: false });
      s.setData(validRows(indicators, (d) => d.adx));
      s.createPriceLine({ price: 25, color: '#ef4444', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '25' });
      s.createPriceLine({ price: 20, color: '#f59e0b', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '20' });
      return [s];
    },
  },
  {
    id: 'williamsR', label: 'Williams %R (11)',
    build: (chart, indicators) => {
      const s = chart.addLineSeries({ color: '#F44336', lineWidth: 1, title: 'W%R', lastValueVisible: false, priceLineVisible: false });
      s.setData(validRows(indicators, (d) => d.williamsR));
      s.createPriceLine({ price: -20, color: '#ef4444', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '-20' });
      s.createPriceLine({ price: -50, color: '#6b7280', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false, title: '-50' });
      s.createPriceLine({ price: -80, color: '#3b82f6', lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: true, title: '-80' });
      return [s];
    },
  },
];

/** 각 서브패널 ID별로 주 시리즈 값을 읽어오는 헬퍼 */
function getSubPanelCrosshairValue(id: string, row: IndicatorData | null): number {
  if (!row) return 0;
  switch (id) {
    case 'macd':      return row.macdLine     ?? (row as any).macd   ?? 0;
    case 'rsi':       return row.rsi          ?? 50;
    case 'stoch':     return row.stochasticK  ?? (row as any).stochK ?? 50;
    case 'cci':       return row.cci          ?? (row as any).cci13  ?? 0;
    case 'obv':       return row.obv          ?? 0;
    case 'adx':       return row.adx          ?? 0;
    case 'williamsR': return row.williamsR    ?? -50;
    default:          return 0;
  }
}

const SubPanels: React.FC<{
  indicators: IndicatorData[];
  activeIndicators: Set<string>;
  isDark: boolean;
  parentChartRef: React.RefObject<IChartApi | null>;
  /** 크로스헤어 unix초 (null = 숨김) — 서브패널 크로스헤어 동기화용 */
  crosshairTimeUnix: number | null;
}> = ({ indicators, activeIndicators, isDark, parentChartRef, crosshairTimeUnix }) => {
  const panelRefs  = useRef<Map<string, HTMLDivElement | null>>(new Map());
  const chartObjsRef = useRef<Map<string, IChartApi>>(new Map());
  /** build() 가 반환한 시리즈 목록 (setCrosshairPosition에 사용) */
  const seriesRefsMap = useRef<Map<string, ISeriesApi<any>[]>>(new Map());

  const chartBg = isDark ? '#0e0e1a' : '#ffffff';
  const textColor = isDark ? '#9ca3af' : '#374151';
  const gridColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';

  const activeSubs = SUB_PANEL_CONFIGS.filter((c) => activeIndicators.has(c.id));
  const activeIdsKey = activeSubs.map((c) => c.id).join(',');

  useEffect(() => {
    activeSubs.forEach((cfg) => {
      const el = panelRefs.current.get(cfg.id);
      if (!el || indicators.length === 0 || chartObjsRef.current.has(cfg.id)) return;

      const chart = createChart(el, {
        width: el.clientWidth, height: 130,
        layout: { background: { type: ColorType.Solid, color: chartBg }, textColor },
        grid: { vertLines: { color: gridColor }, horzLines: { color: gridColor } },
        rightPriceScale: { borderColor: gridColor },
        timeScale: { borderColor: gridColor, timeVisible: true, secondsVisible: false },
        crosshair: { mode: 1 },
      });

      const seriesList = cfg.build(chart, indicators);
      chartObjsRef.current.set(cfg.id, chart);
      seriesRefsMap.current.set(cfg.id, seriesList);

      if (parentChartRef.current) {
        parentChartRef.current.timeScale().subscribeVisibleLogicalRangeChange((r) => {
          if (r) chart.timeScale().setVisibleLogicalRange(r);
        });
        chart.timeScale().subscribeVisibleLogicalRangeChange((r) => {
          if (r && parentChartRef.current) parentChartRef.current.timeScale().setVisibleLogicalRange(r);
        });
      }

      const ro = new ResizeObserver((entries) => {
        chart.applyOptions({ width: entries[0].contentRect.width });
      });
      ro.observe(el);
    });
  }, [activeIdsKey, indicators, isDark]); // eslint-disable-line react-hooks/exhaustive-deps

  // 비활성 서브차트 제거
  useEffect(() => {
    const activeSet = new Set(activeSubs.map((c) => c.id));
    chartObjsRef.current.forEach((chart, id) => {
      if (!activeSet.has(id)) {
        try { chart.remove(); } catch {}
        chartObjsRef.current.delete(id);
        seriesRefsMap.current.delete(id);
      }
    });
  }, [activeIdsKey]); // eslint-disable-line react-hooks/exhaustive-deps

  // 크로스헤어 시간 동기화 (부모 차트 → 서브패널)
  useEffect(() => {
    if (crosshairTimeUnix == null || indicators.length === 0) return;
    // 가장 가까운 indicator row 찾기
    let nearest: IndicatorData | null = null;
    let minDiff = Infinity;
    for (const d of indicators) {
      const t = toUnixSec(d.time);
      if (t === 0) continue;
      const diff = Math.abs(t - crosshairTimeUnix);
      if (diff < minDiff) { minDiff = diff; nearest = d; }
    }
    chartObjsRef.current.forEach((chart, id) => {
      const seriesList = seriesRefsMap.current.get(id);
      if (!seriesList || seriesList.length === 0) return;
      const val = getSubPanelCrosshairValue(id, nearest);
      try {
        (chart as any).setCrosshairPosition(val, crosshairTimeUnix as any, seriesList[0]);
      } catch {}
    });
  }, [crosshairTimeUnix, indicators]); // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <>
      {activeSubs.map((cfg) => (
        <Box key={cfg.id} sx={{ mt: 0.5, borderTop: 1, borderColor: 'divider' }}>
          <Typography variant="caption" color="text.secondary" sx={{ px: 1, fontSize: '0.65rem', fontWeight: 600 }}>
            {cfg.label}
          </Typography>
          <div ref={(el) => { panelRefs.current.set(cfg.id, el); }} style={{ width: '100%' }} />
        </Box>
      ))}
    </>
  );
};

// ─── 크로스헤어 메트릭 패널 ───────────────────────────────────────────────────

const OHLCV_ROWS: MetricRow[] = [
  { label: '시가', field: 'open',   color: '#9ca3af', format: fmtInt },
  { label: '고가', field: 'high',   color: '#ef4444', format: fmtInt },
  { label: '저가', field: 'low',    color: '#3b82f6', format: fmtInt },
  { label: '종가', field: 'close',  color: '#e5e7eb', format: fmtInt },
  { label: '거래량', field: 'volume', color: '#8b5cf6', format: fmtInt },
];

const INDICATOR_ROWS: MetricRow[] = METRIC_ROWS.filter(
  (r) => !['open','high','low','close','volume'].includes(r.field)
);

// ─── 차트 캔버스 위 좌상단 크로스헤어 툴팁 (양쪽 차트 공통) ────────────────────

interface ChartCrosshairTooltipProps {
  snapshot: CrosshairSnapshot | null;
  isDark: boolean;
  /** 헤더 레이블 텍스트 */
  label: string;
  /** 헤더 레이블 색상 */
  labelColor: string;
}

const ChartCrosshairTooltip: React.FC<ChartCrosshairTooltipProps> = ({ snapshot, isDark, label, labelColor }) => {
  if (!snapshot?.candle) return null;

  const bg  = isDark ? 'rgba(13,13,23,0.93)' : 'rgba(255,255,255,0.96)';
  const fg  = isDark ? '#e5e7eb' : '#111827';
  const bdr = isDark ? 'rgba(255,255,255,0.14)' : 'rgba(0,0,0,0.13)';
  const c   = snapshot.candle;
  const isUp = c.close >= c.open;

  return (
    <div style={{
      position: 'absolute', top: 6, left: 6, zIndex: 20,
      backgroundColor: bg, color: fg,
      border: `1px solid ${bdr}`,
      borderRadius: 4, padding: '4px 9px',
      fontSize: '0.62rem', fontFamily: 'monospace', lineHeight: 1.75,
      pointerEvents: 'none',
      boxShadow: '0 2px 8px rgba(0,0,0,0.22)',
      minWidth: 150,
    }}>
      {/* 헤더 */}
      <div style={{ fontWeight: 700, fontSize: '0.58rem', color: labelColor, marginBottom: 2, letterSpacing: '0.01em' }}>
        {label}&nbsp;·&nbsp;{snapshot.timeLabel}
      </div>
      {/* OHLCV */}
      {([
        { l: '시가',   v: c.open,   color: '#9ca3af' },
        { l: '고가',   v: c.high,   color: '#ef4444' },
        { l: '저가',   v: c.low,    color: '#3b82f6' },
        { l: '종가',   v: c.close,  color: isUp ? '#ef4444' : '#3b82f6' },
        { l: '거래량', v: c.volume, color: '#8b5cf6', isVol: true },
      ] as { l: string; v: number; color: string; isVol?: boolean }[]).map(({ l, v, color, isVol }) => (
        <div key={l} style={{ display: 'flex', justifyContent: 'space-between', gap: 14 }}>
          <span style={{ color: '#9ca3af' }}>{l}</span>
          <span style={{ color, fontWeight: 600 }}>
            {isVol ? Math.round(v).toLocaleString() : v.toLocaleString('ko-KR')}
          </span>
        </div>
      ))}
    </div>
  );
};

// ─────────────────────────────────────────────────────────────────────────────

const CrosshairMetricsPanel: React.FC<{
  snapshot: CrosshairSnapshot | null;
  market: string;
  isDark: boolean;
  source?: 'local' | 'tv';
}> = ({ snapshot, isDark, source = 'local' }) => {
  const theme = useTheme();

  const getVal = (row: MetricRow, snap: CrosshairSnapshot): number | null => {
    if (['close', 'open', 'high', 'low', 'volume'].includes(row.field)) {
      return snap.candle ? (snap.candle as any)[row.field] ?? null : null;
    }
    const ind = snap.indicators as any;
    if (row.field === 'macdLine')      return ind.macdLine ?? ind.macd ?? null;
    if (row.field === 'signalLine')    return ind.signalLine ?? ind.signal ?? ind.macdSignal ?? null;
    if (row.field === 'macdHistogram') return ind.macdHistogram ?? ind.histogram ?? null;
    if (row.field === 'stochasticK')   return ind.stochasticK ?? ind.stochK ?? null;
    if (row.field === 'stochasticD')   return ind.stochasticD ?? ind.stochD ?? null;
    if (row.field === 'cci')           return ind.cci ?? ind.cci13 ?? null;
    return ind[row.field] ?? null;
  };

  const sectionBg  = isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)';
  const sectionHdr = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)';

  const renderRow = (row: MetricRow) => {
    const val = snapshot ? getVal(row, snapshot) : null;
    const formatted = val != null && !isNaN(val)
      ? (row.format ? row.format(val) : String(val)) : '—';
    const isWarn = val != null && row.warn && row.warn(val);
    return (
      <Box key={row.field} sx={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        px: 1.5, py: 0.3,
        borderBottom: '1px solid', borderColor: 'divider',
        bgcolor: isWarn ? (isDark ? 'rgba(234,179,8,0.08)' : 'rgba(234,179,8,0.05)') : undefined,
      }}>
        <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem' }}>
          {row.label}
        </Typography>
        <Typography variant="caption" sx={{
          fontSize: '0.65rem', fontFamily: 'monospace', fontWeight: val != null ? 600 : 400,
          color: val == null ? 'text.disabled' : isWarn ? theme.palette.warning.main : row.color,
        }}>
          {formatted}
        </Typography>
      </Box>
    );
  };

  return (
    <Paper elevation={0} sx={{
      border: 1, borderColor: 'divider',
      display: 'flex', flexDirection: 'column',
      overflow: 'hidden', height: '100%', minWidth: 160,
    }}>
      {/* ── 헤더 ── */}
      <Box sx={{ px: 1.5, py: 0.75, borderBottom: 1, borderColor: 'divider', bgcolor: sectionBg, flexShrink: 0 }}>
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.25 }}>
          <Typography variant="caption" fontWeight={700} sx={{ fontSize: '0.72rem', flex: 1 }}>
            크로스헤어 수치
          </Typography>
          <Chip label={source === 'tv' ? 'TV 연동' : '로컬'} size="small" sx={{
            fontSize: '0.58rem', height: 16, px: 0,
            bgcolor: source === 'tv' ? '#1d4ed8' : '#15803d', color: '#fff',
          }} />
        </Box>
        {snapshot
          ? <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.6rem' }}>{snapshot.timeLabel}</Typography>
          : <Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.6rem' }}>차트 위에 마우스를 올려주세요</Typography>
        }
      </Box>

      <Box sx={{ flex: 1, overflowY: 'auto' }}>
        {/* ── 섹션 A: 업비트 원시값 (OHLCV) ── */}
        <Box sx={{ px: 1.5, py: 0.4, bgcolor: sectionHdr, borderBottom: 1, borderColor: 'divider' }}>
          <Typography variant="caption" sx={{ fontSize: '0.6rem', fontWeight: 700, color: '#a5b4fc' }}>
            ■ 업비트 원시값 (OHLCV)
          </Typography>
        </Box>
        {OHLCV_ROWS.map(renderRow)}

        {/* ── 섹션 B: 로컬 계산 지표 ── */}
        <Box sx={{ px: 1.5, py: 0.4, bgcolor: sectionHdr, borderBottom: 1, borderColor: 'divider', mt: 0.25 }}>
          <Typography variant="caption" sx={{ fontSize: '0.6rem', fontWeight: 700, color: '#86efac' }}>
            ■ 로컬 계산 지표
          </Typography>
          <Typography variant="caption" sx={{ fontSize: '0.55rem', color: 'text.disabled', ml: 0.5 }}>
            (TV 화면과 직접 비교)
          </Typography>
        </Box>
        {INDICATOR_ROWS.map(renderRow)}
      </Box>
    </Paper>
  );
};

// ─── 수치 검증 테이블 ─────────────────────────────────────────────────────────

// ─── 검증 테이블 (E: JS기준 / F: 로컬백엔드 / Diff 3단계) ───────────────────

/** 0=PASS · 1=미세(amber) · 2=중간(orange) · 3=심각(red) · null=N/A */
type DiffLevel = 0 | 1 | 2 | 3 | null;

interface ValidationRow {
  timeLabel: string;
  close: number;
  volume: number;
  refValue: number | null;
  localValue: number | null;
  diff: number | null;
  diffPct: number | null;
  level: DiffLevel;
}

/** 임계치 배수 기반 3단계 판정 */
function calcDiffLevel(diffPct: number | null, threshold: number): DiffLevel {
  if (diffPct == null) return null;
  if (diffPct <= threshold)          return 0; // PASS
  if (diffPct <= threshold * 5)      return 1; // 미세 차이 (amber)
  if (diffPct <= threshold * 20)     return 2; // 중간 차이 (orange)
  return 3;                                     // 심각 차이 (red)
}

/** 단계별 색상 토큰 */
const DIFF_COLORS: Record<number, { bg: string; bgDark: string; text: string; label: string; chip: string }> = {
  0: { bg: 'transparent',              bgDark: 'transparent',               text: 'inherit', label: 'PASS',    chip: '#15803d' },
  1: { bg: 'rgba(245,158,11,0.13)',    bgDark: 'rgba(245,158,11,0.18)',     text: '#d97706', label: '△ 미세',  chip: '#b45309' },
  2: { bg: 'rgba(249,115,22,0.14)',    bgDark: 'rgba(249,115,22,0.20)',     text: '#ea580c', label: '▲ 중간',  chip: '#c2410c' },
  3: { bg: 'rgba(239,68,68,0.16)',     bgDark: 'rgba(239,68,68,0.22)',      text: '#dc2626', label: '▼ 심각',  chip: '#991b1b' },
};

const ValidationTable: React.FC<{
  candles: CandleData[];
  indicators: IndicatorData[];
  selectedIndicator: string;
  displayCount: number;
  diffThreshold: number;
  isDark: boolean;
}> = ({ candles, indicators, selectedIndicator, displayCount, diffThreshold, isDark }) => {

  const rows = useMemo<ValidationRow[]>(() => {
    if (!selectedIndicator || candles.length === 0) return [];
    const refArr   = computeRefValues(selectedIndicator, candles);
    const lastN    = candles.slice(-displayCount);
    const startIdx = candles.length - lastN.length;

    return lastN.map((c, i) => {
      const globalIdx = startIdx + i;
      const t = c.time ?? '';
      const timeLabel = t ? new Date(t.endsWith('Z') ? t : t + 'Z').toLocaleString('ko-KR', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '—';

      const candleTs = toUnixSec(c.time);
      let nearestInd: IndicatorData | null = null;
      let minD = Infinity;
      for (const ind of indicators) {
        const it = toUnixSec(ind.time);
        if (it === 0) continue;
        const d = Math.abs(it - candleTs);
        if (d < minD) { minD = d; nearestInd = ind; }
      }

      const refValue   = refArr[globalIdx] ?? null;
      const localValue = nearestInd ? getBackendValue(selectedIndicator, nearestInd) : null;
      const diff       = refValue != null && localValue != null ? Math.abs(refValue - localValue) : null;
      const diffPct    = diff != null && refValue != null && refValue !== 0 ? (diff / Math.abs(refValue)) * 100 : null;
      const level      = calcDiffLevel(diffPct, diffThreshold);

      return { timeLabel, close: c.close, volume: c.volume, refValue, localValue, diff, diffPct, level };
    }).reverse();
  }, [candles, indicators, selectedIndicator, displayCount, diffThreshold]);

  const indicatorLabel = INDICATOR_OPTIONS.find((o) => o.id === selectedIndicator)?.label ?? selectedIndicator;
  const isLarge = ['ma5','ma10','ma20','ma60','bb'].includes(selectedIndicator);
  const fmtVal  = (v: number | null) => v == null || isNaN(v) ? '—' : isLarge ? Math.round(v).toLocaleString() : v.toFixed(4);

  /** 단계에 따른 셀 sx — value 셀과 diff/pct 셀 공통 */
  const diffCellSx = (level: DiffLevel, extra?: object) => {
    if (level == null || level === 0) return { fontSize: '0.68rem', fontFamily: 'monospace', ...extra };
    const c = DIFF_COLORS[level];
    return {
      fontSize: '0.68rem', fontFamily: 'monospace',
      bgcolor: isDark ? c.bgDark : c.bg,
      color: c.text,
      fontWeight: level === 3 ? 700 : level === 2 ? 600 : 500,
      ...extra,
    };
  };

  if (!selectedIndicator) return (
    <Box sx={{ p: 2 }}><Typography variant="body2" color="text.secondary">보조지표를 선택하면 검증 테이블이 표시됩니다.</Typography></Box>
  );

  // 단계별 통계 (헤더 요약)
  const stats = rows.reduce((acc, r) => {
    if (r.level === 1) acc.warn++;
    else if (r.level === 2) acc.alert++;
    else if (r.level === 3) acc.fail++;
    else if (r.level === 0) acc.pass++;
    return acc;
  }, { pass: 0, warn: 0, alert: 0, fail: 0 });

  return (
    <>
      {/* 요약 배지 바 */}
      <Box sx={{ px: 1.5, py: 0.5, display: 'flex', gap: 0.75, alignItems: 'center', borderBottom: 1, borderColor: 'divider', flexWrap: 'wrap' }}>
        <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.63rem', mr: 0.5 }}>판정 요약:</Typography>
        <Chip label={`PASS ${stats.pass}`}       size="small" sx={{ fontSize: '0.6rem', height: 18, bgcolor: '#15803d', color: '#fff' }} />
        <Chip label={`△미세 ${stats.warn}`}      size="small" sx={{ fontSize: '0.6rem', height: 18, bgcolor: DIFF_COLORS[1].chip, color: '#fff' }} />
        <Chip label={`▲중간 ${stats.alert}`}     size="small" sx={{ fontSize: '0.6rem', height: 18, bgcolor: DIFF_COLORS[2].chip, color: '#fff' }} />
        <Chip label={`▼심각 ${stats.fail}`}      size="small" sx={{ fontSize: '0.6rem', height: 18, bgcolor: DIFF_COLORS[3].chip, color: '#fff' }} />
        <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.6rem', ml: 'auto' }}>
          기준: ×1={diffThreshold}% / ×5={+(diffThreshold * 5).toPrecision(3)}% / ×20={+(diffThreshold * 20).toPrecision(3)}%
        </Typography>
      </Box>

      <TableContainer sx={{ maxHeight: 340, overflowX: 'auto' }}>
        <Table size="small" stickyHeader>
          <TableHead>
            <TableRow>
              <TableCell sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 90, whiteSpace: 'nowrap' }}>시간</TableCell>
              <TableCell align="right" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 80 }}>종가</TableCell>
              <TableCell align="right" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 70 }}>거래량</TableCell>
              <TableCell align="right" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 90, bgcolor: isDark ? 'rgba(99,102,241,0.14)' : 'rgba(99,102,241,0.08)', color: '#818cf8', whiteSpace: 'nowrap' }}>
                [E] JS기준<br /><span style={{ fontSize: '0.56rem', fontWeight: 400, color: 'inherit' }}>{indicatorLabel}</span>
              </TableCell>
              <TableCell align="right" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 90, bgcolor: isDark ? 'rgba(34,197,94,0.14)' : 'rgba(34,197,94,0.08)', color: '#22c55e', whiteSpace: 'nowrap' }}>
                [F] 로컬백엔드<br /><span style={{ fontSize: '0.56rem', fontWeight: 400, color: 'inherit' }}>{indicatorLabel}</span>
              </TableCell>
              <TableCell align="right" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 72 }}>차이 (절댓값)</TableCell>
              <TableCell align="right" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 65 }}>오차율 %</TableCell>
              <TableCell align="center" sx={{ fontWeight: 700, fontSize: '0.68rem', minWidth: 60 }}>판정</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {rows.length === 0 ? (
              <TableRow><TableCell colSpan={8} align="center" sx={{ py: 2, color: 'text.disabled', fontSize: '0.72rem' }}>데이터 없음</TableCell></TableRow>
            ) : rows.map((row, idx) => {
              const lv = row.level;
              const isNewest = idx === 0;
              const hasError = lv != null && lv > 0;

              // [E] 기준값 셀 bg: 오차가 있으면 단계색, 없으면 기본 파란보라
              const refCellSx = hasError
                ? diffCellSx(lv, { bgcolor: isDark ? DIFF_COLORS[lv!].bgDark : DIFF_COLORS[lv!].bg })
                : { fontSize: '0.68rem', fontFamily: 'monospace', bgcolor: isDark ? 'rgba(99,102,241,0.06)' : 'rgba(99,102,241,0.04)', color: '#818cf8' };
              // [F] 로컬값 셀
              const localCellSx = hasError
                ? diffCellSx(lv, { bgcolor: isDark ? DIFF_COLORS[lv!].bgDark : DIFF_COLORS[lv!].bg })
                : { fontSize: '0.68rem', fontFamily: 'monospace', bgcolor: isDark ? 'rgba(34,197,94,0.06)' : 'rgba(34,197,94,0.04)', color: '#22c55e' };

              return (
                <TableRow key={idx} sx={{ '&:hover': { bgcolor: 'action.hover' }, outline: isNewest ? '1px solid' : 'none', outlineColor: 'primary.main' }}>
                  <TableCell sx={{ fontSize: '0.68rem', whiteSpace: 'nowrap', fontWeight: isNewest ? 700 : 400 }}>{row.timeLabel}</TableCell>
                  <TableCell align="right" sx={{ fontSize: '0.68rem', fontFamily: 'monospace' }}>{Math.round(row.close).toLocaleString()}</TableCell>
                  <TableCell align="right" sx={{ fontSize: '0.68rem', fontFamily: 'monospace' }}>{Math.round(row.volume).toLocaleString()}</TableCell>
                  {/* [E] JS 기준값 */}
                  <TableCell align="right" sx={refCellSx}>{fmtVal(row.refValue)}</TableCell>
                  {/* [F] 로컬 백엔드값 */}
                  <TableCell align="right" sx={localCellSx}>{fmtVal(row.localValue)}</TableCell>
                  {/* 차이 절댓값 */}
                  <TableCell align="right" sx={diffCellSx(lv)}>
                    {row.diff != null ? (isLarge ? Math.round(row.diff).toLocaleString() : row.diff.toFixed(5)) : '—'}
                  </TableCell>
                  {/* 오차율 % */}
                  <TableCell align="right" sx={diffCellSx(lv)}>
                    {row.diffPct != null ? `${row.diffPct.toFixed(4)}%` : '—'}
                  </TableCell>
                  {/* 판정 배지 */}
                  <TableCell align="center">
                    {lv === null && <Chip label="N/A"           size="small" sx={{ fontSize: '0.58rem', height: 17 }} />}
                    {lv === 0    && <Chip label="PASS"          size="small" sx={{ fontSize: '0.58rem', height: 17, bgcolor: DIFF_COLORS[0].chip, color: '#fff' }} />}
                    {lv === 1    && <Chip label={DIFF_COLORS[1].label} size="small" sx={{ fontSize: '0.58rem', height: 17, bgcolor: DIFF_COLORS[1].chip, color: '#fff' }} />}
                    {lv === 2    && <Chip label={DIFF_COLORS[2].label} size="small" sx={{ fontSize: '0.58rem', height: 17, bgcolor: DIFF_COLORS[2].chip, color: '#fff' }} />}
                    {lv === 3    && <Chip label={DIFF_COLORS[3].label} size="small" sx={{ fontSize: '0.58rem', height: 17, bgcolor: DIFF_COLORS[3].chip, color: '#fff' }} />}
                  </TableCell>
                </TableRow>
              );
            })}
          </TableBody>
        </Table>
      </TableContainer>
    </>
  );
};

// ─── LLM 프롬프트 생성기 ─────────────────────────────────────────────────────

function buildLLMPrompt(
  market: string,
  interval: string,
  snapshot20: any[],
  crosshair: CrosshairSnapshot | null,
): string {
  const dataStr = JSON.stringify(snapshot20, null, 2);
  const crosshairStr = crosshair ? JSON.stringify({
    time: crosshair.timeLabel,
    candle: crosshair.candle,
    indicators: crosshair.indicators,
  }, null, 2) : 'null';

  return `당신은 금융공학 및 기술적 지표 전문가입니다. 
아래 데이터는 업비트 ${market} 종목의 ${interval} 봉 캔들 데이터와 백엔드에서 계산된 보조지표 수치입니다.
이 데이터를 기반으로 다음 항목을 정밀 분석해주세요.

═══ 분석 요청 항목 ═══
1. 각 지표(RSI, MACD, 스토캐스틱, 볼린저밴드, CCI, ADX)의 계산 수식이 표준 방식과 일치하는지 검증
2. 인접 봉 간 수치 연속성 확인 (이상 급변 또는 null 구간 탐지)
3. 오차율이 0.1% 이상으로 추정되는 경우, 부동소수점 처리 또는 Smoothing 방식 차이 추론
4. 수식 교정 제안 (개선 필요 항목)
5. 전체 지표 일치율(%) 종합 평가

═══ 계산 공식 정보 ═══
- MA: 단순이동평균(SMA), 기간별 종가 합계/기간
- RSI: Wilder's Smoothing 방식, 기간=9, 과매수70/과매도30
- MACD: EMA(12) - EMA(26), Signal=EMA(MACD,9), Histogram=MACD-Signal
- 볼린저밴드: SMA(20) ± 2σ (표준편차)
- 스토캐스틱: Slow %K=SMA(RawK,5), %D=SMA(%K,5), 기간=12
- CCI: (TP - SMA_TP) / (0.015 × 평균편차), 기간=13
- ADX: Wilder Smoothing, 기간=11
- OBV: 누적 거래량(상승봉+, 하락봉-), 신호선=SMA(9)
- Williams %R: (Max고-종가)/(Max고-Min저) × -100, 기간=11

═══ 현재 크로스헤어 지점 스냅샷 ═══
${crosshairStr}

═══ 최근 20봉 지표 데이터 (JSON) ═══
${dataStr}

위 데이터를 분석하여 각 지표별 수식 무결성과 수치 정확도를 상세히 보고해주세요.
특히 RSI의 Wilder Smoothing 시드값 처리, MACD의 EMA 초기화 방식, 스토캐스틱의 Slowing 적용 여부를 중점 검토해주세요.`;
}

// ─── JSON 스냅샷 빌더 ─────────────────────────────────────────────────────────

function buildSnapshot20(candles: CandleData[], indicators: IndicatorData[]): any[] {
  const last20 = [...indicators].slice(-20).reverse();
  return last20.map((ind) => {
    const indT = toUnixSec(ind.time);
    const matchCandle = indT > 0
      ? candles.find((c) => { const ct = toUnixSec(c.time); return ct > 0 && Math.abs(ct - indT) < 120; })
      : undefined;
    return {
      timestamp: ind.time,
      candle: matchCandle
        ? { open: matchCandle.open, high: matchCandle.high, low: matchCandle.low, close: matchCandle.close, volume: matchCandle.volume }
        : null,
      indicators: {
        ma5: ind.ma5, ma10: ind.ma10, ma20: ind.ma20, ma60: ind.ma60,
        bollingerUpper: ind.bollingerUpper, bollingerMiddle: ind.bollingerMiddle, bollingerLower: ind.bollingerLower,
        rsi: ind.rsi,
        macd: { line: ind.macdLine ?? ind.macd, signal: ind.signalLine ?? ind.signal ?? ind.macdSignal, histogram: ind.macdHistogram ?? ind.histogram },
        stochastic: { k: ind.stochasticK ?? ind.stochK, d: ind.stochasticD ?? ind.stochD },
        cci: ind.cci ?? ind.cci13,
        adx: ind.adx, plusDI: ind.plusDI, minusDI: ind.minusDI,
        obv: ind.obv, obvSignal: ind.obvSignal,
        williamsR: ind.williamsR,
      },
      formula_notes: {
        rsi_method: 'Wilder\'s Smoothing (period=9)',
        macd_method: 'EMA(12)-EMA(26), Signal=EMA(9)',
        stoch_method: 'Slow: RawK→SMA(5)→%K, %D=SMA(%K,5)',
        cci_method: '(TP-SMA)/0.015×MeanDev, period=13',
        adx_method: 'Wilder Smoothing, period=11',
      },
    };
  });
}

// ─── 메인 페이지 ─────────────────────────────────────────────────────────────

const ChartValidationPage: React.FC = () => {
  const theme = useTheme();
  const isDark = theme.palette.mode === 'dark';

  const [market, setMarket] = useState(DEFAULT_MARKET);
  const [interval, setInterval] = useState('15m');
  // 단일 보조지표 선택 ('' = 캔들만)
  const [selectedIndicator, setSelectedIndicator] = useState('rsi');
  const [tableExpanded, setTableExpanded] = useState(true);
  const [tableRows, setTableRows] = useState(20);
  const [diffThreshold, setDiffThreshold] = useState(0.1); // %
  const [iframeKey, setIframeKey] = useState(0);
  const [liveCandle, setLiveCandle] = useState<CandleData | null>(null);
  const [liveIndicator, setLiveIndicator] = useState<Record<string, unknown> | null>(null);

  // ── 크로스헤어 동기화 상태 ────────────────────────────────────────────
  /** 로컬 차트에서 subscribeCrosshairMove로 얻은 실제 크로스헤어 스냅샷 */
  const [localCrosshairSnapshot, setLocalCrosshairSnapshot] = useState<CrosshairSnapshot | null>(null);
  /**
   * TV 오버레이 크로스헤어 위치에 해당하는 스냅샷.
   * - 로컬 크로스헤어 이동 시: TV 크로스헤어가 가리키는 시간의 캔들 데이터
   * - TV 오버레이 호버 시: TV 마우스 위치 시간의 캔들 데이터
   * 두 스냅샷을 독립적으로 유지해 각 차트가 자신의 크로스헤어 수치를 표시하도록 함.
   */
  const [tvCrosshairSnapshot, setTvCrosshairSnapshot] = useState<CrosshairSnapshot | null>(null);
  // 하위호환용 파생 계산 (LLM 프롬프트 등에서 참조)
  const crosshairSnapshot = localCrosshairSnapshot ?? tvCrosshairSnapshot;
  /** 로컬 차트 크로스헤어 X 비율 (TV 오버레이 동기화용) */
  const [tvCrosshairXPct, setTvCrosshairXPct] = useState<number | null>(null);
  /** 로컬 차트 크로스헤어 Y 비율 (TV 오버레이 가로선용) */
  const [tvCrosshairYPct, setTvCrosshairYPct] = useState<number | null>(null);
  /** TV 오버레이에 표시할 시간 레이블 */
  const [tvCrosshairTimeLabel, setTvCrosshairTimeLabel] = useState<string | null>(null);
  /** 로컬 차트의 현재 가시 시간 범위 (스크롤/줌 추적) */
  const localVisibleRangeRef = useRef<{ from: number; to: number } | null>(null);
  /**
   * TV 차트의 예상 가시 시간 범위 (URL range 파라미터로 추정).
   * - TV iframe은 overlay 가 마우스 이벤트를 차단해 사용자가 스크롤 불가 → 범위 고정
   * - interval/market 변경 또는 명시적 새로고침 시 재계산
   */
  const tvCurrentRangeRef = useRef<{ from: number; to: number }>(calcTVExpectedRange(interval));
  /** 크로스헤어 소스: 'local' | 'tv' */
  const [crosshairSource, setCrosshairSource] = useState<'local' | 'tv'>('local');
  /** LocalChart 핸들 (setCrosshairAtTime / getChartRef 호출용) */
  const localChartRef = useRef<LocalChartHandle>(null);
  /** SubPanels 연결용 — LocalChart의 IChartApi를 감싸는 동적 ref */
  const subPanelParentRef = useMemo<React.RefObject<IChartApi | null>>(() => ({
    get current() { return localChartRef.current?.getChartRef() ?? null; },
  } as React.RefObject<IChartApi | null>), []);

  // LLM 프롬프트 다이얼로그 (수동 복사용)
  const [promptDialogOpen, setPromptDialogOpen] = useState(false);
  const [generatedPrompt, setGeneratedPrompt] = useState('');
  const [promptCopied, setPromptCopied] = useState(false);

  // AI 직접 검증 다이얼로그
  const [aiDialogOpen, setAiDialogOpen] = useState(false);
  const [aiLoading, setAiLoading] = useState(false);
  const [aiResponse, setAiResponse] = useState('');
  const [aiError, setAiError] = useState<string | null>(null);
  const [aiResponseCopied, setAiResponseCopied] = useState(false);

  // JSON 복사 상태
  const [jsonCopied, setJsonCopied] = useState(false);

  const { candles, indicators, loading, error, refetch } = useLocalChartData(market, interval);

  // WebSocket 실시간
  const { status: connectionStatus } = useMarketDataWebSocket({
    market,
    timeframe: interval,
    enabled: true,
    onData: ({ candles: wsCandles, indicators: wsIndicators }) => {
      if (wsCandles && wsCandles.length > 0) {
        const last = wsCandles[wsCandles.length - 1] as any;
        // time 필드 우선, 없으면 timestamp. 숫자형 그대로 유지하여 toUnixSec가 정확히 처리하도록
        const rawTime = last.time ?? last.timestamp;
        setLiveCandle({
          time: typeof rawTime === 'number' ? String(rawTime) : String(rawTime ?? ''),
          open:   Number(last.open),
          high:   Number(last.high),
          low:    Number(last.low),
          close:  Number(last.close),
          volume: Number(last.volume ?? 0),
        });
      }
      if (wsIndicators) {
        setLiveIndicator(mapWsIndicatorsToChartFormat(wsIndicators as any));
      }
    },
  });

  // ── 로컬 차트 → TV 오버레이 동기화 ──────────────────────────────────
  const handleCrosshairXPct = useCallback((xPct: number | null, yPct: number | null, timeUnix: number | null) => {
    // ── X: TV 차트의 알려진 범위(tvCurrentRangeRef) 기준으로 정확히 계산 ──
    // TV iframe overlay가 마우스 이벤트를 차단하므로 TV는 항상 초기 range를 유지.
    // localVisibleRangeRef 대신 tvCurrentRangeRef를 사용해야 정확한 위치 산출.
    if (timeUnix != null) {
      const { from, to } = tvCurrentRangeRef.current;
      const span = to - from;
      if (span > 0) {
        const timePct = (timeUnix - from) / span;
        setTvCrosshairXPct(Math.max(0, Math.min(1, timePct)));
      } else {
        setTvCrosshairXPct(xPct);
      }
    } else {
      setTvCrosshairXPct(null);
    }

    // ── Y: LocalChart에서 이미 가격 정규화된 값을 그대로 사용 ──────────
    setTvCrosshairYPct(yPct);

    if (timeUnix != null) {
      const label = new Date(timeUnix * 1000).toLocaleString('ko-KR', {
        month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
      });
      setTvCrosshairTimeLabel(label);
    } else {
      setTvCrosshairTimeLabel(null);
    }
  }, []);

  const handleVisibleRangeChange = useCallback((range: { from: number; to: number } | null) => {
    localVisibleRangeRef.current = range;
  }, []);

  // ── 캔들 배열에서 특정 시각에 가장 가까운 캔들 스냅샷 생성 (공통 헬퍼) ─
  const buildSnapshotAt = useCallback((timeUnix: number): CrosshairSnapshot => {
    let nearestCandle: CandleData | null = null;
    let nearestInd: IndicatorData | null = null;
    let minDiff = Infinity;
    for (const c of candles) {
      const ct = toUnixSec(c.time);
      if (ct === 0) continue;
      const d = Math.abs(ct - timeUnix);
      if (d < minDiff) { minDiff = d; nearestCandle = c; }
    }
    minDiff = Infinity;
    for (const ind of indicators) {
      const it = toUnixSec(ind.time);
      if (it === 0) continue;
      const d = Math.abs(it - timeUnix);
      if (d < minDiff) { minDiff = d; nearestInd = ind; }
    }
    const timeLabel = new Date(timeUnix * 1000).toLocaleString('ko-KR', {
      month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
    });
    return {
      timeLabel, timeUnix,
      candle: nearestCandle
        ? { open: nearestCandle.open, high: nearestCandle.high, low: nearestCandle.low, close: nearestCandle.close, volume: nearestCandle.volume }
        : null,
      indicators: nearestInd ? { ...nearestInd } : {},
    };
  }, [candles, indicators]);

  // ── 로컬 차트 crosshair 이동 → localCrosshairSnapshot 갱신 ───────────
  const handleLocalCrosshairMove = useCallback((snap: CrosshairSnapshot | null) => {
    setCrosshairSource('local');
    setLocalCrosshairSnapshot(snap);
    // TV 툴팁도 동일 시간의 캔들로 독립 갱신 (TV 크로스헤어가 가리키는 시각)
    if (snap != null) {
      setTvCrosshairSnapshot(buildSnapshotAt(snap.timeUnix));
    } else {
      setTvCrosshairSnapshot(null);
    }
  }, [buildSnapshotAt]);

  // ── TV 오버레이 호버 → 로컬 차트 역방향 동기화 ───────────────────────
  const handleTvOverlayHoverXPct = useCallback((xPct: number | null) => {
    if (xPct == null) {
      setCrosshairSource('local');
      return;
    }
    setCrosshairSource('tv');
    // TV의 알려진 범위로 xPct → timeUnix 역산 (localVisibleRange 아님)
    const range = tvCurrentRangeRef.current;
    const timeUnix = Math.round(range.from + xPct * (range.to - range.from));
    if (!timeUnix) return;

    // 로컬 차트 크로스헤어를 해당 시간으로 이동 (setCrosshairAtTime은 subscribeCrosshairMove를 발화하지 않음)
    localChartRef.current?.setCrosshairAtTime(timeUnix);

    // TV 툴팁: TV 크로스헤어가 가리키는 시간의 캔들
    setTvCrosshairSnapshot(buildSnapshotAt(timeUnix));
    // 로컬 툴팁: 로컬 차트가 동일 시간으로 이동했으므로 같은 시간의 캔들
    setLocalCrosshairSnapshot(buildSnapshotAt(timeUnix));
  }, [buildSnapshotAt]);

  // 선택된 단일 지표로부터 activeIndicators Set 파생
  const activeIndicators = useMemo(
    () => new Set(selectedIndicator ? [selectedIndicator] : [] as string[]),
    [selectedIndicator],
  );

  // interval/market 변경 시 TV 예상 범위 재계산
  useEffect(() => {
    tvCurrentRangeRef.current = calcTVExpectedRange(interval);
  }, [interval, market]);

  const handleRefresh = () => {
    refetch();
    setIframeKey((k) => k + 1);
    tvCurrentRangeRef.current = calcTVExpectedRange(interval);
    setLocalCrosshairSnapshot(null);
    setTvCrosshairSnapshot(null);
  };

  // JSON 스냅샷 복사
  const handleCopyJson = useCallback(async () => {
    const snap = buildSnapshot20(candles, indicators);
    const json = JSON.stringify(snap, null, 2);
    await navigator.clipboard.writeText(json);
    setJsonCopied(true);
    setTimeout(() => setJsonCopied(false), 2500);
  }, [candles, indicators]);

  // LLM 프롬프트 생성 & 다이얼로그 오픈
  const handleGeneratePrompt = useCallback(() => {
    const snap20 = buildSnapshot20(candles, indicators);
    const prompt = buildLLMPrompt(market, interval, snap20, crosshairSnapshot);
    setGeneratedPrompt(prompt);
    setPromptDialogOpen(true);
    setPromptCopied(false);
  }, [candles, indicators, market, interval, crosshairSnapshot]);

  const handleCopyPrompt = async () => {
    await navigator.clipboard.writeText(generatedPrompt);
    setPromptCopied(true);
    setTimeout(() => setPromptCopied(false), 2500);
  };

  // ── AI 직접 검증: exdev.co.kr:8800/v1/chat/completions 호출 ───────────
  const handleAiVerify = useCallback(async () => {
    const snap20 = buildSnapshot20(candles, indicators);
    const prompt = buildLLMPrompt(market, interval, snap20, crosshairSnapshot);

    setAiDialogOpen(true);
    setAiLoading(true);
    setAiResponse('');
    setAiError(null);
    setAiResponseCopied(false);

    try {
      const res = await fetch('http://exdev.co.kr:8800/v1/chat/completions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          model: 'gpt-4o',
          messages: [{ role: 'user', content: prompt }],
          temperature: 0.2,
          stream: false,
        }),
      });

      if (!res.ok) {
        const text = await res.text().catch(() => '');
        throw new Error(`HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}`);
      }

      const json = await res.json();
      const content: string =
        json?.choices?.[0]?.message?.content ??
        json?.choices?.[0]?.text ??
        JSON.stringify(json, null, 2);

      setAiResponse(content);
    } catch (err: any) {
      setAiError(err?.message ?? '알 수 없는 오류');
    } finally {
      setAiLoading(false);
    }
  }, [candles, indicators, market, interval, crosshairSnapshot]);

  const handleCopyAiResponse = async () => {
    await navigator.clipboard.writeText(aiResponse);
    setAiResponseCopied(true);
    setTimeout(() => setAiResponseCopied(false), 2500);
  };

  const wsColor = connectionStatus === 'connected' ? '#22c55e' : connectionStatus === 'connecting' ? '#eab308' : '#ef4444';
  const wsLabel = connectionStatus === 'connected' ? '실시간 연결' : connectionStatus === 'connecting' ? '연결 중...' : '연결 끊김';

  return (
    <Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 1 }}>

      {/* ── 컨트롤 바 ──────────────────────────── */}
      <Paper elevation={0} sx={{ p: 1.5, border: 1, borderColor: 'divider', flexShrink: 0 }}>
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap' }}>
          {/* 종목 */}
          <Autocomplete
            size="small"
            options={POPULAR_MARKETS}
            value={market}
            onChange={(_, v) => v && setMarket(v)}
            freeSolo
            sx={{ width: 148 }}
            renderInput={(params) => <TextField {...params} label="종목" />}
          />
          {/* 봉 */}
          <Box sx={{ display: 'flex', gap: 0.5 }}>
            {INTERVALS.map((iv) => (
              <Button
                key={iv.value}
                size="small"
                variant={interval === iv.value ? 'contained' : 'outlined'}
                onClick={() => setInterval(iv.value)}
                sx={{ minWidth: 44, px: 0.5, fontSize: '0.7rem' }}
              >
                {iv.label}
              </Button>
            ))}
          </Box>

          <Divider orientation="vertical" flexItem />

          {/* 보조지표 드롭다운 (단일 선택) */}
          <FormControl size="small" sx={{ minWidth: 200 }}>
            <InputLabel sx={{ fontSize: '0.75rem' }}>보조지표</InputLabel>
            <Select
              value={selectedIndicator}
              label="보조지표"
              onChange={(e) => setSelectedIndicator(e.target.value)}
              sx={{ fontSize: '0.75rem' }}
            >
              <MenuItem value="" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
                없음 (캔들만)
              </MenuItem>
              <ListSubheader sx={{ fontSize: '0.65rem', lineHeight: '26px' }}>── 오버레이 (메인 차트)</ListSubheader>
              {INDICATOR_OPTIONS.filter((o) => o.group === 'overlay').map((o) => (
                <MenuItem key={o.id} value={o.id} sx={{ fontSize: '0.75rem' }}>{o.label}</MenuItem>
              ))}
              <ListSubheader sx={{ fontSize: '0.65rem', lineHeight: '26px' }}>── 서브패널 (하단 차트)</ListSubheader>
              {INDICATOR_OPTIONS.filter((o) => o.group === 'subpanel').map((o) => (
                <MenuItem key={o.id} value={o.id} sx={{ fontSize: '0.75rem' }}>{o.label}</MenuItem>
              ))}
            </Select>
          </FormControl>

          <Divider orientation="vertical" flexItem />

          {/* WS 상태 */}
          <Chip
            icon={<WsIcon sx={{ fontSize: 13, color: wsColor + ' !important' }} />}
            label={wsLabel}
            size="small"
            variant="outlined"
            sx={{ fontSize: '0.65rem', borderColor: wsColor, color: wsColor }}
          />

          {/* 버튼들 */}
          <Tooltip title="데이터 새로고침">
            <IconButton size="small" onClick={handleRefresh} disabled={loading}>
              {loading ? <CircularProgress size={17} /> : <RefreshIcon fontSize="small" />}
            </IconButton>
          </Tooltip>
          <Tooltip title="업비트 거래소 새 탭에서 열기">
            <IconButton size="small" onClick={() => window.open(getUpbitChartUrl(market), '_blank')}>
              <OpenInNewIcon fontSize="small" />
            </IconButton>
          </Tooltip>

          <Divider orientation="vertical" flexItem />

          {/* JSON 복사 */}
          <Tooltip title="최근 20봉 지표값 JSON을 클립보드에 복사">
            <Button
              size="small"
              variant="outlined"
              startIcon={jsonCopied ? <CheckIcon sx={{ fontSize: 14 }} /> : <CopyIcon sx={{ fontSize: 14 }} />}
              onClick={handleCopyJson}
              disabled={indicators.length === 0}
              color={jsonCopied ? 'success' : 'inherit'}
              sx={{ fontSize: '0.7rem', whiteSpace: 'nowrap' }}
            >
              {jsonCopied ? 'JSON 복사됨!' : 'JSON 복사'}
            </Button>
          </Tooltip>

          {/* AI 검증 (직접 API 호출) */}
          <Tooltip title="exdev.co.kr:8800 LLM API로 지표 수식 검증 요청">
            <Button
              size="small"
              variant="contained"
              color="secondary"
              startIcon={<AiIcon sx={{ fontSize: 14 }} />}
              onClick={handleAiVerify}
              disabled={indicators.length === 0 || aiLoading}
              sx={{ fontSize: '0.7rem', whiteSpace: 'nowrap' }}
            >
              {aiLoading ? 'AI 분석 중…' : 'AI 검증'}
            </Button>
          </Tooltip>
        </Box>
        {error && <Alert severity="error" sx={{ mt: 1, py: 0.5, fontSize: '0.74rem' }}>{error}</Alert>}
      </Paper>

      {/* ══════════════════════════════════════════════════════════════════ */}
      {/* ── 2×2 비교 그리드 (캔들 행 + 보조지표 행) ──────────────────── */}
      {/* ══════════════════════════════════════════════════════════════════ */}
      <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minHeight: 0, overflow: 'hidden' }}>

        {/* ── Row 1: 캔들 차트 비교 ([A] 업비트 vs [B] 로컬) ─────────── */}
        <Box sx={{ display: 'flex', gap: 1, flex: 3, minHeight: 0, overflow: 'hidden' }}>

          {/* [A] 업비트 캔들 차트 (보조지표 없음) */}
          <Paper elevation={0} sx={{ flex: '0 0 calc(50% - 4px)', border: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
            <Box sx={{ px: 1.5, py: 0.5, borderBottom: 1, borderColor: 'divider', display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, bgcolor: isDark ? 'rgba(99,102,241,0.08)' : 'rgba(99,102,241,0.04)' }}>
              <CompareIcon sx={{ fontSize: 13, color: '#818cf8' }} />
              <Typography variant="caption" fontWeight={700} sx={{ fontSize: '0.72rem', color: '#818cf8', flex: 1 }}>
                업비트 캔들차트 [A]
              </Typography>
              <Chip label={toTVSymbol(market)} size="small" sx={{ fontSize: '0.58rem', height: 16 }} />
            </Box>
            <Box sx={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
              <iframe
                key={`tv-candle-${iframeKey}-${market}-${interval}`}
                src={getTVWidgetUrl(market, interval, isDark, '')}
                title="업비트 캔들차트"
                style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none' }}
                allow="fullscreen"
                referrerPolicy="no-referrer-when-downgrade"
              />
              <TvCrosshairOverlay
                xPct={tvCrosshairXPct}
                yPct={tvCrosshairYPct}
                timeLabel={tvCrosshairTimeLabel}
                snapshot={tvCrosshairSnapshot}
                isDark={isDark}
                onHoverXPct={handleTvOverlayHoverXPct}
                tvCandleArea={{ top: 0.06, bottom: 0.93 }}
              />
            </Box>
          </Paper>

          {/* [B] 로컬 캔들 차트 */}
          <Paper elevation={0} sx={{ flex: '0 0 calc(50% - 4px)', border: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
            <Box sx={{ px: 1.5, py: 0.5, borderBottom: 1, borderColor: 'divider', display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, bgcolor: isDark ? 'rgba(34,197,94,0.08)' : 'rgba(34,197,94,0.04)' }}>
              <ShowChartIcon sx={{ color: '#22c55e', fontSize: 13 }} />
              <Typography variant="caption" fontWeight={700} sx={{ fontSize: '0.72rem', color: '#22c55e', flex: 1 }}>
                로컬 캔들차트 [B]
              </Typography>
              <Chip label={`${candles.length}봉`} size="small" variant="outlined" sx={{ fontSize: '0.58rem', height: 16 }} />
              {liveCandle && <Chip label={`LIVE ${liveCandle.close?.toLocaleString()}`} size="small" sx={{ fontSize: '0.58rem', height: 16, bgcolor: '#22c55e', color: 'white' }} />}
            </Box>
            <Box sx={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
              {loading
                ? <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}><CircularProgress size={28} /></Box>
                : candles.length > 0
                  ? (
                    <Box sx={{ position: 'relative', height: '100%' }}>
                      <LocalChart
                        ref={localChartRef}
                        candles={candles} indicators={indicators}
                        activeIndicators={activeIndicators}
                        liveCandle={liveCandle} isDark={isDark}
                        onCrosshairMove={handleLocalCrosshairMove}
                        onCrosshairXPct={handleCrosshairXPct}
                        onVisibleRangeChange={handleVisibleRangeChange}
                        crosshairTimeUnix={crosshairSnapshot?.timeUnix ?? null}
                        initialVisibleRange={tvCurrentRangeRef.current}
                      />
                      <ChartCrosshairTooltip
                        snapshot={localCrosshairSnapshot}
                        isDark={isDark}
                        label="로컬 차트"
                        labelColor="#22c55e"
                      />
                    </Box>
                  )
                  : <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}><Typography color="text.secondary">데이터 없음</Typography></Box>
              }
            </Box>
          </Paper>
        </Box>

        {/* ── Row 2: 보조지표 비교 ([C] 업비트 vs [D] 로컬) — 지표 선택 시만 표시 ── */}
        {selectedIndicator && (
          <Box sx={{ display: 'flex', gap: 1, flex: 2, minHeight: 0, overflow: 'hidden' }}>

            {/* [C] 업비트 보조지표 (TV iframe with study) */}
            <Paper elevation={0} sx={{ flex: '0 0 calc(50% - 4px)', border: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
              <Box sx={{ px: 1.5, py: 0.5, borderBottom: 1, borderColor: 'divider', display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, bgcolor: isDark ? 'rgba(99,102,241,0.06)' : 'rgba(99,102,241,0.03)' }}>
                <CompareIcon sx={{ fontSize: 13, color: '#a5b4fc' }} />
                <Typography variant="caption" fontWeight={700} sx={{ fontSize: '0.72rem', color: '#a5b4fc', flex: 1 }}>
                  업비트 보조지표 [C] — {INDICATOR_OPTIONS.find((o) => o.id === selectedIndicator)?.label}
                </Typography>
              </Box>
              <Box sx={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
                <iframe
                  key={`tv-indicator-${iframeKey}-${market}-${interval}-${selectedIndicator}`}
                  src={getTVWidgetUrl(market, interval, isDark, selectedIndicator)}
                  title="업비트 보조지표"
                  style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none' }}
                  allow="fullscreen"
                  referrerPolicy="no-referrer-when-downgrade"
                />
              </Box>
            </Paper>

            {/* [D] 로컬 보조지표 */}
            <Paper elevation={0} sx={{ flex: '0 0 calc(50% - 4px)', border: 1, borderColor: 'divider', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
              <Box sx={{ px: 1.5, py: 0.5, borderBottom: 1, borderColor: 'divider', display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0, bgcolor: isDark ? 'rgba(34,197,94,0.06)' : 'rgba(34,197,94,0.03)' }}>
                <ShowChartIcon sx={{ color: '#4ade80', fontSize: 13 }} />
                <Typography variant="caption" fontWeight={700} sx={{ fontSize: '0.72rem', color: '#4ade80', flex: 1 }}>
                  로컬 보조지표 [D] — {INDICATOR_OPTIONS.find((o) => o.id === selectedIndicator)?.label}
                </Typography>
              </Box>
              <Box sx={{ flex: 1, overflow: 'hidden' }}>
                <SubPanels
                  indicators={indicators}
                  activeIndicators={activeIndicators}
                  isDark={isDark}
                  parentChartRef={subPanelParentRef}
                  crosshairTimeUnix={crosshairSnapshot?.timeUnix ?? null}
                />
              </Box>
            </Paper>
          </Box>
        )}
      </Box>

      {/* ── 검증 테이블 [E/F] — 하단 보이기/숨기기 패널 ──────────────────── */}
      <Paper
        elevation={0}
        sx={{
          border: 1, borderColor: 'divider', flexShrink: 0,
          borderRadius: 1,
          overflow: 'hidden',
        }}
      >
        {/* 토글 헤더 */}
        <Box
          sx={{ px: 1.5, py: 0.7, display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', userSelect: 'none', bgcolor: isDark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.02)' }}
          onClick={() => setTableExpanded((v) => !v)}
        >
          <Typography variant="caption" fontWeight={700} sx={{ fontSize: '0.76rem', flex: 1 }}>
            📊 데이터 검증 테이블
            <Typography component="span" variant="caption" color="text.secondary" sx={{ ml: 1, fontSize: '0.64rem' }}>
              [E] JS기준값 vs [F] 로컬백엔드 · 최근 {tableRows}봉 · 임계치 {diffThreshold}%
            </Typography>
          </Typography>
          {/* 임계치 */}
          <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.65rem', mr: 0.5 }}>임계치:</Typography>
          <Select size="small" value={diffThreshold} onChange={(e) => setDiffThreshold(Number(e.target.value))} onClick={(e) => e.stopPropagation()} sx={{ fontSize: '0.7rem', height: 22, mr: 0.5 }}>
            {[0.001, 0.01, 0.1, 0.5, 1.0].map((v) => <MenuItem key={v} value={v} sx={{ fontSize: '0.7rem' }}>{v}%</MenuItem>)}
          </Select>
          {/* 봉 수 */}
          <Select size="small" value={tableRows} onChange={(e) => setTableRows(Number(e.target.value))} onClick={(e) => e.stopPropagation()} sx={{ fontSize: '0.7rem', height: 22, mr: 0.5 }}>
            {[10, 20, 30, 50, 100].map((n) => <MenuItem key={n} value={n} sx={{ fontSize: '0.7rem' }}>{n}봉</MenuItem>)}
          </Select>
          <Chip
            label={tableExpanded ? '숨기기 ▲' : '펼치기 ▼'}
            size="small"
            variant="outlined"
            sx={{ fontSize: '0.62rem', height: 20, cursor: 'pointer' }}
          />
        </Box>
        <Collapse in={tableExpanded}>
          <Divider />
          {loading
            ? <Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}><CircularProgress size={22} /></Box>
            : <ValidationTable
                candles={candles} indicators={indicators}
                selectedIndicator={selectedIndicator}
                displayCount={tableRows}
                diffThreshold={diffThreshold}
                isDark={isDark}
              />
          }
        </Collapse>
      </Paper>

      {/* ── LLM 프롬프트 다이얼로그 (수동 복사용) ─────────────── */}
      <Dialog
        open={promptDialogOpen}
        onClose={() => setPromptDialogOpen(false)}
        maxWidth="md"
        fullWidth
        PaperProps={{ sx: { maxHeight: '80vh' } }}
      >
        <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1.5 }}>
          <AiIcon color="secondary" />
          <Typography variant="subtitle1" fontWeight={700} sx={{ flex: 1 }}>
            AI 수식 검증 프롬프트
          </Typography>
          <Tooltip title={promptCopied ? '복사됨!' : '프롬프트 전체 복사'}>
            <Button
              size="small"
              variant={promptCopied ? 'contained' : 'outlined'}
              color={promptCopied ? 'success' : 'primary'}
              startIcon={promptCopied ? <CheckIcon /> : <CopyIcon />}
              onClick={handleCopyPrompt}
              sx={{ mr: 1 }}
            >
              {promptCopied ? '복사됨!' : '전체 복사'}
            </Button>
          </Tooltip>
          <IconButton size="small" onClick={() => setPromptDialogOpen(false)}>
            <CloseIcon fontSize="small" />
          </IconButton>
        </DialogTitle>
        <DialogContent dividers sx={{ p: 0 }}>
          <Alert severity="info" sx={{ borderRadius: 0, fontSize: '0.76rem', py: 0.5 }}>
            위 프롬프트를 Claude / ChatGPT 등 LLM에 붙여넣기 하면 지표 수식 무결성 검증 보고서를 받을 수 있습니다.
          </Alert>
          <Box
            component="pre"
            sx={{
              m: 0, p: 2,
              fontSize: '0.73rem', lineHeight: 1.7,
              fontFamily: 'monospace',
              whiteSpace: 'pre-wrap', wordBreak: 'break-word',
              overflowY: 'auto',
              bgcolor: isDark ? '#0e0e1a' : '#f8f9fa',
              color: isDark ? '#d1d5db' : '#1f2937',
              maxHeight: '55vh',
            }}
          >
            {generatedPrompt}
          </Box>
        </DialogContent>
      </Dialog>

      {/* ── AI 검증 응답 다이얼로그 ─────────────── */}
      <Dialog
        open={aiDialogOpen}
        onClose={() => { if (!aiLoading) setAiDialogOpen(false); }}
        maxWidth="md"
        fullWidth
        PaperProps={{ sx: { maxHeight: '85vh' } }}
      >
        <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1.5 }}>
          <AiIcon color="secondary" />
          <Typography variant="subtitle1" fontWeight={700} sx={{ flex: 1 }}>
            AI 검증 결과
            <Typography component="span" variant="caption" color="text.secondary" sx={{ ml: 1 }}>
              exdev.co.kr:8800
            </Typography>
          </Typography>
          {aiResponse && (
            <Tooltip title={aiResponseCopied ? '복사됨!' : '응답 전체 복사'}>
              <Button
                size="small"
                variant={aiResponseCopied ? 'contained' : 'outlined'}
                color={aiResponseCopied ? 'success' : 'primary'}
                startIcon={aiResponseCopied ? <CheckIcon /> : <CopyIcon />}
                onClick={handleCopyAiResponse}
                sx={{ mr: 1 }}
              >
                {aiResponseCopied ? '복사됨!' : '전체 복사'}
              </Button>
            </Tooltip>
          )}
          <IconButton size="small" onClick={() => { if (!aiLoading) setAiDialogOpen(false); }}>
            <CloseIcon fontSize="small" />
          </IconButton>
        </DialogTitle>

        <DialogContent dividers sx={{ p: 0 }}>
          {/* 로딩 */}
          {aiLoading && (
            <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', py: 8, gap: 2 }}>
              <CircularProgress size={40} color="secondary" />
              <Typography variant="body2" color="text.secondary">
                AI 지표 검증 분석 중… (데이터 전송 후 응답 대기)
              </Typography>
            </Box>
          )}

          {/* 에러 */}
          {!aiLoading && aiError && (
            <Alert severity="error" sx={{ m: 2 }}>
              <Typography variant="body2" fontWeight={700}>API 요청 실패</Typography>
              <Typography variant="caption" sx={{ fontFamily: 'monospace', display: 'block', mt: 0.5 }}>
                {aiError}
              </Typography>
            </Alert>
          )}

          {/* 응답 본문 */}
          {!aiLoading && aiResponse && (
            <Box
              component="pre"
              sx={{
                m: 0, p: 2.5,
                fontSize: '0.78rem', lineHeight: 1.85,
                fontFamily: '"Noto Sans KR", sans-serif',
                whiteSpace: 'pre-wrap', wordBreak: 'break-word',
                overflowY: 'auto',
                bgcolor: isDark ? '#0e0e1a' : '#f8f9fa',
                color: isDark ? '#e2e8f0' : '#1e293b',
                maxHeight: '65vh',
              }}
            >
              {aiResponse}
            </Box>
          )}
        </DialogContent>
      </Dialog>
    </Box>
  );
};

// 인라인 SVG 아이콘 (MUI import 충돌 방지)
const ShowChartIcon: React.FC<{ sx?: any; color?: string }> = ({ sx, color = 'currentColor' }) => (
  <svg viewBox="0 0 24 24" fill={color} style={{ width: 15, height: 15, ...sx }}>
    <path d="M3.5 18.49l6-6.01 4 4L22 6.92l-1.41-1.41-7.09 7.97-4-4L2 16.99z" />
  </svg>
);

export default ChartValidationPage;
