import React, { useRef, useEffect, useLayoutEffect, useState, useCallback } from 'react';
import type { MouseEventParams, Time } from 'lightweight-charts';
import type { OHLCVBar, ChartType, Theme, IndicatorConfig, LegendData, Drawing, ChartMode, Timeframe } from '../types';
import { ChartManager } from '../utils/ChartManager';
import { setIndicatorChartContext } from '../utils/indicatorRegistry';
import { DISPLAY_COUNT } from '../hooks/useUpbitData';

/** Upbit 실시간 — 초기 히스토리가 이 개수 미만이면 setData 하지 않음 (종목 전환 직후 1~2봉만 로드되는 깨짐 방지) */
const MIN_BARS_FOR_FULL_RELOAD = Math.min(50, Math.floor(DISPLAY_COUNT / 4));

/** 종목 전환 직후 stale/partial 데이터 차단 — fetch 완료(barsMarket 확정) 후 sparse history도 렌더 허용 */
function canApplyChartBars(
  barData: OHLCVBar[],
  barsMarket: string | null | undefined,
  market: string,
): boolean {
  if (barData.length === 0) return false;
  if (barsMarket === undefined) return true;
  if (barsMarket == null || barsMarket !== market) return false;
  if (!market.startsWith('KRW-')) return true;
  if (barData.length >= MIN_BARS_FOR_FULL_RELOAD) return true;
  return barData.length >= 2;
}
import DrawingCanvas, { hitTestDrawing } from './DrawingCanvas';
import PaneLegend, { type PaneLegendProps } from './PaneLegend';
import ChartHoverToolbar from './ChartHoverToolbar';
import ChartMagnifier from './ChartMagnifier';
import ChartContextMenu from './ChartContextMenu';
import CandlePaneControls from './CandlePaneControls';
import { getKoreanName } from '../utils/marketNameCache';
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';

interface TradingChartProps {
  bars:              OHLCVBar[];
  /** bars 가 현재 market 과 일치할 때만 전달 (없으면 bars.length>0 이면 준비된 것으로 간주) */
  barsMarket?:       string | null;
  /** BB 등 다른 심볼 계산용 */
  market?:           string;
  timeframe?:        Timeframe;
  chartType:         ChartType;
  theme:             Theme;
  mode:              ChartMode;
  indicators:        IndicatorConfig[];
  drawingTool:       string;
  drawings:          Drawing[];
  logScale:          boolean;
  drawingsLocked?:   boolean;
  drawingsVisible?:  boolean;
  onCrosshair:       (data: LegendData | null) => void;
  onManagerReady:    (mgr: ChartManager) => void;
  onAddDrawing:      (d: Drawing) => void;
  /** 시리즈 단일 클릭 → (indicatorId | '__main__' | null, 패인 좌측상단 screen 좌표) */
  onSeriesClick?:    (entryId: string | null, point: { x: number; y: number }) => void;
  /** 시리즈 더블 클릭 → 설정 모달 직접 오픈용 (패인 좌측상단 screen 좌표 포함) */
  onSeriesDoubleClick?: (entryId: string | null, point: { x: number; y: number }) => void;
  /** pane 레전드 레이블 호버 */
  onHoverPaneLegend?:  (id: string, sx: number, sy: number) => void;
  /** pane 레전드 레이블 이탈 */
  onLeavePaneLegend?:  () => void;
  /** 멀티차트 전체 보기: 이 차트를 단일 모드로 확장 (있으면 hover toolbar 버튼에 연결) */
  onFullView?:         () => void;
  /** 현재 선택된 드로잉 ID (핸들 렌더용) */
  selectedDrawingId?:  string | null;
  /** cursor 모드에서 드로잉 단일 클릭 */
  onDrawingClick?:     (id: string | null, screenX: number, screenY: number) => void;
  /** cursor 모드에서 드로잉 더블 클릭 */
  onDrawingDoubleClick?: (id: string, screenX: number, screenY: number) => void;
  /** 돋보기 활성 여부 */
  magnifierEnabled?: boolean;
  /** 돋보기 닫기 콜백 */
  onMagnifierClose?: () => void;
  /**
   * 데이터 로드 완료 콜백 — reloadAll 이 setInitialVisibleRange 까지 마친 뒤 호출.
   * ChartSlot 에서 멀티차트 첫 마운트 시 sync range 를 재적용하는 데 사용.
   */
  onDataLoaded?: () => void;
  /** 보조지표 pane 순서 변경 (드래그 핸들): fromId 를 insertBeforeId 앞으로 이동 (null = 맨 뒤) */
  onReorderIndicators?: (fromId: string, insertBeforeId: string | null) => void;
  /** 보조지표 pane 병합 */
  onMergeIndicators?:   (fromId: string, intoId: string) => void;
  /** 병합 pane 분리 */
  onSplitIndicatorPane?: (hostId: string) => void;
  /** 지표 단독 전체화면 확장 */
  onExpandIndicator?:   (id: string) => void;
  /** 지표 제거 (X 버튼) */
  onRemoveIndicator?:   (id: string) => void;
  /** 보조지표 pane 복사 */
  onDuplicateIndicator?: (id: string) => void;
  /** 현재 단독 전체화면 중인 지표 id */
  focusedIndicatorId?:  string | null;
  /** 전체화면 → 전체 보기 복원 */
  onRestoreIndicators?: () => void;
  /** 우클릭 메뉴에서 매수·매도 선택 시 */
  onTradeOrderRequest?: (req: { market: string; price: number; side: 'buy' | 'sell' }) => void;
  /** 표시 시간대 (IANA) */
  displayTimezone?: string;
}

const TradingChart: React.FC<TradingChartProps> = ({
  bars, barsMarket, market = '', timeframe = '1D', chartType, theme, mode, indicators, drawingTool, drawings,
  logScale, drawingsLocked = false, drawingsVisible = true,
  onCrosshair, onManagerReady, onAddDrawing,
  onSeriesClick, onSeriesDoubleClick,
  onHoverPaneLegend, onLeavePaneLegend,
  onFullView,
  selectedDrawingId,
  onDrawingClick,
  onDrawingDoubleClick,
  magnifierEnabled = false,
  onMagnifierClose,
  onDataLoaded,
  onReorderIndicators,
  onMergeIndicators,
  onSplitIndicatorPane,
  onExpandIndicator,
  onRemoveIndicator,
  onDuplicateIndicator,
  focusedIndicatorId,
  onRestoreIndicators,
  onTradeOrderRequest,
  displayTimezone = DEFAULT_DISPLAY_TIMEZONE,
}) => {
  const containerRef  = useRef<HTMLDivElement>(null);
  const wrapperRef    = useRef<HTMLDivElement>(null);   // 스크롤 래퍼
  const managerRef    = useRef<ChartManager | null>(null);
  const barsRef       = useRef<OHLCVBar[]>([]);
  // 드로잉 최신 참조 (캡처 리스너에서 closure 없이 접근)
  const drawingsRef         = useRef<Drawing[]>(drawings);
  const drawingToolRef      = useRef<string>(drawingTool);
  const drawingsVisibleRef  = useRef<boolean>(drawingsVisible);
  const drawingsLockedRef   = useRef<boolean>(drawingsLocked);
  const modeRef             = useRef<ChartMode>(mode);
  const onDrawingClickRef      = useRef(onDrawingClick);
  const onDrawingDoubleClickRef = useRef(onDrawingDoubleClick);

  const canPanRef = useRef(false);
  const panStateRef = useRef({
    active: false,
    lastX: 0,
    lastY: 0,
    moved: false,
    /** 0=캔들 pane — 세로 패닝 허용 */
    originPaneIndex: 0,
  });
  const suppressClickRef = useRef(false);
  const seriesDblSuppressRef = useRef(false);
  const toggleCandleOnlyRef = useRef<() => void>(() => {});
  const candleOnlyModeRef = useRef(false);
  const lastCrosshairPriceRef = useRef<number | null>(null);
  const onTradeOrderRequestRef = useRef(onTradeOrderRequest);
  onTradeOrderRequestRef.current = onTradeOrderRequest;
  const onSeriesDoubleClickRef = useRef(onSeriesDoubleClick);
  onSeriesDoubleClickRef.current = onSeriesDoubleClick;
  const marketRef = useRef(market);
  marketRef.current = market;
  const barsMarketRef = useRef(barsMarket);
  barsMarketRef.current = barsMarket;

  const [ctxMenu, setCtxMenu] = useState<{
    x: number; y: number; price: number;
  } | null>(null);

  // 드로잉 더블클릭 감지용
  const lastDrawingClickRef = useRef({ id: '', time: 0 });
  const drawingSingleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // 직전 값 추적 (불필요한 재실행 방지)
  const prevBarsKey     = useRef<string>('');
  const prevIndKey      = useRef<string>('');
  const prevSortedPKRef = useRef<string>('');   // 순서 무관 paramKey (reorder 감지용)
  const prevMarket      = useRef<string>(market);
  const prevMarketTf    = useRef<Timeframe>(timeframe);
  const prevChartType   = useRef<ChartType>(chartType);
  const prevTheme       = useRef<Theme>(theme);
  const prevLogScale    = useRef<boolean>(logScale);
  const indicatorsRef   = useRef(indicators);
  const recoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
  /** 캔들 pane 전체보기 (오버레이 지표 유지, 하단 보조지표 pane 숨김) */
  const [candleOnlyMode, setCandleOnlyMode] = useState(false);

  useEffect(() => {
    if (barsMarket === market) {
      barsRef.current = bars;
    } else {
      barsRef.current = [];
    }
  }, [bars, barsMarket, market]);

  useEffect(() => {
    indicatorsRef.current = indicators;
  }, [indicators]);

  const toggleCandleOnly = useCallback(() => {
    setCandleOnlyMode(v => !v);
  }, []);

  useEffect(() => { toggleCandleOnlyRef.current = toggleCandleOnly; }, [toggleCandleOnly]);
  useEffect(() => { candleOnlyModeRef.current = candleOnlyMode; }, [candleOnlyMode]);
  const prevCandleOnlyRef = useRef(false);

  // 최신 값을 ref에 동기화 (캡처 리스너 closure 탈출)
  useEffect(() => {
    setIndicatorChartContext(market, timeframe);
  }, [market, timeframe]);

  useEffect(() => { drawingsRef.current          = drawings; },         [drawings]);
  useEffect(() => { drawingToolRef.current        = drawingTool; },     [drawingTool]);
  useEffect(() => { drawingsVisibleRef.current    = drawingsVisible; },  [drawingsVisible]);
  useEffect(() => { drawingsLockedRef.current     = drawingsLocked; },   [drawingsLocked]);
  useEffect(() => { modeRef.current               = mode; },            [mode]);
  useEffect(() => { onDrawingClickRef.current     = onDrawingClick; },  [onDrawingClick]);
  useEffect(() => { onDrawingDoubleClickRef.current = onDrawingDoubleClick; }, [onDrawingDoubleClick]);
  useEffect(() => {
    canPanRef.current = drawingTool === 'cursor';
    const el = containerRef.current;
    if (el && !panStateRef.current.active) {
      el.style.cursor = canPanRef.current ? 'grab' : '';
      el.style.touchAction = canPanRef.current ? 'none' : '';
    }
  }, [mode, drawingTool]);

  /**
   * pane 높이 재배분 + 스크롤 컨테이너 확장
   *
   * wrapper.clientHeight 를 정확한 가용 높이로 ChartManager 에 전달합니다.
   * CSS Grid 레이아웃 초기화 지연으로 wrapperH=0 이 될 수 있으므로,
   * retryCount 를 통해 최대 6회 자동 재시도합니다 (100→200→300ms…)
   */
  const applyPaneLayout = useCallback((mgr: ChartManager, retryCount = 0) => {
    const wrapper   = wrapperRef.current;
    const container = containerRef.current;
    if (!wrapper || !container) return;

    const wrapperH = wrapper.clientHeight;
    if (wrapperH <= 0) {
      // CSS Grid 높이가 아직 계산되지 않은 경우: 지연 후 재시도 (최대 6회)
      if (retryCount < 6) {
        setTimeout(() => applyPaneLayout(mgr, retryCount + 1), 100 * (retryCount + 1));
      }
      return;
    }

    let required: number;
    if (candleOnlyModeRef.current) {
      mgr.applyCandleOnlyLayout(true, wrapperH);
      required = wrapperH;
    } else {
      if (mgr.isCandleOnlyLayout()) {
        mgr.restoreFromCandleFullscreen(wrapperH);
      }
      required = mgr.resetPaneHeights(wrapperH);
    }

    if (required > wrapperH + 4) {
      container.style.height = `${required}px`;
    } else {
      container.style.height = '';
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /** 캔들 확대 보기 → 원복 직후 가격·시간축·pane 비율 정상화 */
  useEffect(() => {
    const mgr = managerRef.current;
    if (!mgr || !chartMgr) return;
    const wasOnly = prevCandleOnlyRef.current;
    prevCandleOnlyRef.current = candleOnlyMode;
    if (!wasOnly && candleOnlyMode) {
      applyPaneLayout(mgr);
    } else if (wasOnly && !candleOnlyMode && mgr.hasMainSeries()) {
      const runRestore = () => {
        const m = managerRef.current;
        if (!m?.hasMainSeries()) return;
        const wrapperH = wrapperRef.current?.clientHeight ?? 0;
        m.restoreFromCandleFullscreen(wrapperH);
        applyPaneLayout(m);
        const futureBars = m.hasIchimoku() ? 28 : 0;
        m.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
      };
      requestAnimationFrame(() => requestAnimationFrame(runRestore));
      [200, 500, 1000].forEach(delay => setTimeout(runRestore, delay));
    }
  }, [candleOnlyMode, chartMgr, applyPaneLayout]);

  // ── 전체 재로드 (데이터 + 인디케이터) ─────────────────────────────────────
  const reloadSafetyTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
  const reloadGenRef = useRef(0);
  const reloadInFlightRef = useRef(false);
  const reloadCoalesceRef = useRef(false);
  const reloadRetryRef = useRef(0);
  const reloadAllRef = useRef<(
    mgr: ChartManager,
    newBars: OHLCVBar[],
    ct: ChartType,
    th: Theme,
    ls: boolean,
    inds: IndicatorConfig[],
  ) => Promise<void>>(async () => {});

  const queueFullReload = useCallback(() => {
    if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
    recoveryTimerRef.current = window.setTimeout(() => {
      recoveryTimerRef.current = null;
      const m = managerRef.current;
      const barData = barsRef.current;
      const inds = indicatorsRef.current;
      if (!m || barData.length === 0) return;
      if (!canApplyChartBars(barData, barsMarket, market)) return;
      prevBarsKey.current = '';
      reloadAllRef.current?.(
        m, barData,
        prevChartType.current, prevTheme.current, prevLogScale.current,
        inds,
      );
    }, 80);
  }, [market, barsMarket]);

  const queueIndicatorRecovery = useCallback(() => {
    if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
    recoveryTimerRef.current = window.setTimeout(() => {
      recoveryTimerRef.current = null;
      const m = managerRef.current;
      const barData = barsRef.current;
      const inds = indicatorsRef.current;
      if (!m || barData.length === 0) return;
      if (!canApplyChartBars(barData, barsMarket, market)) return;

      const expected = countExpectedVisibleIndicators(inds);
      if (expected === 0) return;
      if (m.getLoadedIndicatorCount() >= expected) return;

      prevBarsKey.current = '';
      const ct = prevChartType.current;
      const th = prevTheme.current;
      const ls = prevLogScale.current;

      if (m.hasMainSeries()) {
        void m.reloadIndicatorsOnly(inds).then(() => {
          if (m.getLoadedIndicatorCount() >= expected) {
            prevBarsKey.current = barsKey(barData, market);
            prevIndKey.current = indKey(inds);
            applyPaneLayout(m);
            return;
          }
          reloadAllRef.current?.(m, barData, ct, th, ls, inds);
        });
        return;
      }
      reloadAllRef.current?.(m, barData, ct, th, ls, inds);
    }, 120);
  }, [market, barsMarket, applyPaneLayout]);

  /** setData 직후 캔들·볼륨 pane 레이아웃 확정 (지표 로드 중단 시에도 빈 화면 방지) */
  const commitCandleLayout = useCallback((mgr: ChartManager) => {
    requestAnimationFrame(() => {
      applyPaneLayout(mgr);
      requestAnimationFrame(() => {
        const futureBars = mgr.hasIchimoku() ? 28 : 0;
        mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
      });
    });
  }, [applyPaneLayout]);

  const reloadAll = useCallback(async (
    mgr: ChartManager,
    newBars: OHLCVBar[],
    ct: ChartType,
    th: Theme,
    ls: boolean,
    inds: IndicatorConfig[],
  ) => {
    if (newBars.length === 0) return;

    if (reloadInFlightRef.current) {
      reloadCoalesceRef.current = true;
      barsRef.current = newBars;
      return;
    }

    reloadInFlightRef.current = true;

    try {
      do {
        reloadCoalesceRef.current = false;
        const gen = reloadGenRef.current;
        const barData = barsRef.current.length > 0 ? barsRef.current : newBars;
        if (barData.length === 0) break;

        reloadSafetyTimers.current.forEach(clearTimeout);
        reloadSafetyTimers.current = [];

        barsRef.current = barData;

        mgr.setData(barData, ct, th);
        if (gen !== reloadGenRef.current) {
          prevBarsKey.current = '';
          break;
        }
        mgr.setTheme(th);
        mgr.setLogScale(ls);
        mgr.setDisplayTimezone(displayTimezone, timeframe);
        commitCandleLayout(mgr);

        let stale = false;
        for (const ind of inds) {
          if (gen !== reloadGenRef.current) {
            prevBarsKey.current = '';
            stale = true;
            break;
          }
          await mgr.addIndicator(ind);
        }
        if (stale) break;

        if (gen !== reloadGenRef.current) {
          prevBarsKey.current = '';
          break;
        }

        const expected = countExpectedVisibleIndicators(inds);
        const loaded = mgr.getLoadedIndicatorCount();

        if (expected > 0 && loaded < expected) {
          prevBarsKey.current = '';
          if (reloadRetryRef.current < 4 && mgr.hasMainSeries()) {
            reloadRetryRef.current += 1;
            await mgr.reloadIndicatorsOnly(inds);
            if (gen !== reloadGenRef.current) break;
          }
          if (mgr.getLoadedIndicatorCount() < expected) {
            commitCandleLayout(mgr);
            if (reloadRetryRef.current < 6) {
              reloadRetryRef.current += 1;
              queueIndicatorRecovery();
            }
            break;
          }
        }

        reloadRetryRef.current = 0;
        prevChartType.current = ct;
        prevTheme.current     = th;
        prevLogScale.current  = ls;
        prevBarsKey.current   = barsKey(barData, market);
        prevIndKey.current    = indKey(inds);
        prevSortedPKRef.current = sortedParamKey(inds);

        await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
          applyPaneLayout(mgr);
          requestAnimationFrame(() => {
            const futureBars = mgr.hasIchimoku() ? 28 : 0;
            mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
            resolve();
          });
        })));

        if (gen !== reloadGenRef.current) {
          prevBarsKey.current = '';
          break;
        }

        onDataLoaded?.();

        reloadSafetyTimers.current = [300, 700, 1400].map(delay =>
          setTimeout(() => {
            const m = managerRef.current;
            if (!m || !m.hasMainSeries()) return;
            const expectedLater = countExpectedVisibleIndicators(indicatorsRef.current);
            if (expectedLater > 0 && m.getLoadedIndicatorCount() < expectedLater) {
              prevBarsKey.current = '';
              queueIndicatorRecovery();
              return;
            }
            const w = wrapperRef.current;
            if (!w || w.clientHeight <= 0) return;
            m.resetPaneHeights(w.clientHeight);
            const fb = m.hasIchimoku() ? 28 : 0;
            m.setInitialVisibleRange(DISPLAY_COUNT, fb);
            if (delay === 1400) onDataLoaded?.();
          }, delay)
        );
      } while (reloadCoalesceRef.current);
    } finally {
      reloadInFlightRef.current = false;
      if (reloadCoalesceRef.current) {
        reloadCoalesceRef.current = false;
        queueFullReload();
      }
    }
  }, [applyPaneLayout, commitCandleLayout, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);

  useEffect(() => {
    reloadAllRef.current = reloadAll;
  }, [reloadAll]);

  // ── 차트 초기화 (마운트 시 1회) ──────────────────────────────────────────
  useEffect(() => {
    if (!containerRef.current) return;

    const mgr = new ChartManager(containerRef.current, theme);
    managerRef.current = mgr;
    setChartMgr(mgr);
    onManagerReady(mgr);

    const unsub = mgr.subscribeCrosshair((p: MouseEventParams<Time>) => {
      if (p.point?.y != null) {
        const yPrice = mgr.yToPrice(p.point.y);
        if (yPrice != null && yPrice > 0) lastCrosshairPriceRef.current = yPrice;
      }
      if (!p.time) { onCrosshair(null); return; }
      const bar = mgr.getBarAtTime(p.time as number)
               ?? barsRef.current.find(b => b.time === p.time);
      if (bar) {
        const crosshairPrice = p.point?.y != null ? mgr.yToPrice(p.point.y) ?? undefined : undefined;
        const indicatorValues = mgr.getIndicatorValuesFromParams(p);
        onCrosshair({
          time: p.time as number,
          open: bar.open, high: bar.high, low: bar.low,
          close: bar.close, volume: bar.volume,
          crosshairPrice: crosshairPrice ?? undefined,
          indicatorValues,
        });
      }
    });

    const container = containerRef.current!;

    // ── 드로잉 클릭/더블클릭 감지 (capture phase → 인디케이터 클릭보다 우선) ──────
    // cursor 모드 + trading + visible + !locked 일 때만 처리
    const handleDrawingCapture = (e: MouseEvent) => {
      if (suppressClickRef.current) {
        suppressClickRef.current = false;
        e.stopImmediatePropagation();
        return;
      }
      if (
        modeRef.current !== 'trading'      ||
        drawingToolRef.current !== 'cursor'||
        !drawingsVisibleRef.current        ||
        drawingsLockedRef.current
      ) return;

      const rect  = container.getBoundingClientRect();
      const cx    = e.clientX - rect.left;
      const cy    = e.clientY - rect.top;
      const cssW  = container.clientWidth;
      const cssH  = container.clientHeight;
      const m     = managerRef.current;
      if (!m) return;

      // 역순(나중에 그린 것 우선)으로 hit-test
      const ds = drawingsRef.current;
      for (let i = ds.length - 1; i >= 0; i--) {
        if (hitTestDrawing(cx, cy, ds[i], m, cssW, cssH)) {
          const hitId = ds[i].id;
          // 인디케이터 컨텍스트 툴바가 열리지 않도록 이벤트 전파 차단
          e.stopImmediatePropagation();

          const now = Date.now();
          const isDouble =
            now - lastDrawingClickRef.current.time < 400 &&
            lastDrawingClickRef.current.id === hitId;
          lastDrawingClickRef.current = { id: hitId, time: now };

          if (isDouble) {
            if (drawingSingleTimerRef.current) {
              clearTimeout(drawingSingleTimerRef.current);
              drawingSingleTimerRef.current = null;
            }
            onDrawingDoubleClickRef.current?.(hitId, e.clientX, e.clientY);
          } else {
            if (drawingSingleTimerRef.current) clearTimeout(drawingSingleTimerRef.current);
            drawingSingleTimerRef.current = setTimeout(() => {
              drawingSingleTimerRef.current = null;
              onDrawingClickRef.current?.(hitId, e.clientX, e.clientY);
            }, 220);
          }
          return;
        }
      }
      // 빈 공간 클릭 → 선택 해제
      if (
        modeRef.current === 'trading' &&
        drawingToolRef.current === 'cursor'
      ) {
        onDrawingClickRef.current?.(null, e.clientX, e.clientY);
      }
    };

    // capture:true → subscribeSeriesClick(bubble) 보다 먼저 실행
    container.addEventListener('click', handleDrawingCapture, { capture: true });

    const onContextMenuCapture = (e: MouseEvent) => {
      if (!onTradeOrderRequestRef.current || !marketRef.current) return;
      const m = managerRef.current;
      if (!m) return;
      const rect = container.getBoundingClientRect();
      const chartX = e.clientX - rect.left;
      const chartY = e.clientY - rect.top;
      const price = m.getTradePriceAtChartPoint(chartX, chartY);
      if (price == null) return;
      e.preventDefault();
      e.stopPropagation();
      setCtxMenu({ x: e.clientX, y: e.clientY, price });
    };
    container.addEventListener('contextmenu', onContextMenuCapture, { capture: true });

    const onPanPointerDown = (e: PointerEvent) => {
      if (!canPanRef.current || e.button !== 0) return;
      const rect = container.getBoundingClientRect();
      const chartX = e.clientX - rect.left;
      const chartY = e.clientY - rect.top;
      if (mgr.isOnPriceAxis(chartX, chartY) || mgr.isOnTimeAxis(chartY)) return;
      const originPaneIndex = mgr.getPaneIndexAtChartY(chartY);
      try {
        container.setPointerCapture(e.pointerId);
      } catch {
        /* ignore */
      }
      panStateRef.current = {
        active: true,
        lastX: e.clientX,
        lastY: e.clientY,
        moved: false,
        originPaneIndex,
      };
      container.style.cursor = 'grabbing';
    };

    const onPanPointerMove = (e: PointerEvent) => {
      if (!panStateRef.current.active) return;
      const dx = e.clientX - panStateRef.current.lastX;
      const dy = e.clientY - panStateRef.current.lastY;
      panStateRef.current.lastX = e.clientX;
      panStateRef.current.lastY = e.clientY;
      const allowVertical = panStateRef.current.originPaneIndex === 0;
      if (!panStateRef.current.moved) {
        const hitY = allowVertical && Math.abs(dy) > 2;
        if (Math.abs(dx) > 2 || hitY) panStateRef.current.moved = true;
      }
      if (!panStateRef.current.moved) return;
      if (e.cancelable) e.preventDefault();
      managerRef.current?.panByPixelDelta(dx, dy, {
        allowVerticalPan: allowVertical,
      });
    };

    const onPanPointerUp = () => {
      if (!panStateRef.current.active) return;
      if (panStateRef.current.moved) suppressClickRef.current = true;
      panStateRef.current.active = false;
      container.style.cursor = canPanRef.current ? 'grab' : '';
    };

    container.addEventListener('pointerdown', onPanPointerDown);
    window.addEventListener('pointermove', onPanPointerMove);
    window.addEventListener('pointerup', onPanPointerUp);
    window.addEventListener('pointercancel', onPanPointerUp);

    const onWheelCapture = (e: WheelEvent) => {
      const m = managerRef.current;
      if (!m) return;
      const rect = container.getBoundingClientRect();
      const chartX = e.clientX - rect.left;
      const chartY = e.clientY - rect.top;
      if (!m.isCandlePanePriceAxis(chartX, chartY)) return;

      e.preventDefault();
      e.stopPropagation();
      m.zoomMainPriceByWheel(e.deltaY, chartY);
    };
    container.addEventListener('wheel', onWheelCapture, { capture: true, passive: false });

    // subscribeSeriesClick 은 이제 native DOM 이벤트 기반.
    // point 는 ChartManager 내부에서 chart-container 좌표로 계산된 screen 좌표.
    /** 캔들 pane: 시리즈 위 → 설정 팝업, 빈 공간 → 전체보기/원복 토글 */
    const onCandlePaneDblClick = (e: MouseEvent) => {
      const m = managerRef.current;
      if (!m) return;
      const rect = container.getBoundingClientRect();
      const chartX = e.clientX - rect.left;
      const chartY = e.clientY - rect.top;

      const entryId = m.resolveEntryAtChartPoint(chartX, chartY);
      if (entryId && entryId !== '__main__') {
        e.preventDefault();
        e.stopImmediatePropagation();
        suppressClickRef.current = true;
        seriesDblSuppressRef.current = true;
        onSeriesDoubleClickRef.current?.(entryId, { x: e.clientX, y: e.clientY });
        return;
      }

      if (!m.isInCandlePlotArea(chartX, chartY)) return;

      e.preventDefault();
      e.stopImmediatePropagation();
      suppressClickRef.current = true;
      seriesDblSuppressRef.current = true;

      if (m.isClickOnMainSeries(chartX, chartY)) {
        onSeriesDoubleClickRef.current?.('__main__', { x: e.clientX, y: e.clientY });
      } else {
        toggleCandleOnlyRef.current();
      }
    };
    container.addEventListener('dblclick', onCandlePaneDblClick, { capture: true });

    const unsubClick = mgr.subscribeSeriesClick(
      (entryId, point) => { onSeriesClick?.(entryId, point); },
      (entryId, point) => {
        if (seriesDblSuppressRef.current) {
          seriesDblSuppressRef.current = false;
          return;
        }
        // 캔들 pane 더블클릭은 dblclick 캡처에서 처리 (시리즈/빈공간 구분)
        if (entryId === '__main__') return;
        onSeriesDoubleClick?.(entryId, point);
      },
    );

    // autoSize:true 를 쓰면 LWC 가 내부 ResizeObserver 로 자동 크기 조절.
    // 이 외부 ResizeObserver 는 applyPaneLayout 재트리거 + 데이터 재로드 보장용.
    let prevW = 0, prevH = 0;
    const ro = new ResizeObserver(([entry]) => {
      const { width, height } = entry.contentRect;
      if (width === prevW && height === prevH) return;
      // 이전 크기가 0이었다가 유효해진 경우 (= display:none → visible 전환)
      const wasHidden = prevW <= 0 || prevH <= 0;
      prevW = width; prevH = height;
      if (width <= 0 || height <= 0) return;

      const m = managerRef.current;
      if (!m) return;

      // 컨테이너가 처음으로 유효한 크기를 가질 때:
      // 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드
      if (barsRef.current.length > 0 && !m.hasMainSeries()) {
        const bm = barsMarketRef.current;
        if (!canApplyChartBars(barsRef.current, bm, marketRef.current)) return;
        void reloadAll(
          m,
          barsRef.current,
          prevChartType.current,
          prevTheme.current,
          prevLogScale.current,
          indicatorsRef.current,
        );
      } else {
        applyPaneLayout(m);
        // 멀티→단일 레이아웃 전환처럼 숨겨졌다가 다시 표시될 때
        // setInitialVisibleRange 를 재실행해 가시 범위(캔들 분포)를 정상화
        if (wasHidden && m.hasMainSeries()) {
          requestAnimationFrame(() => {
            const fb = m.hasIchimoku() ? 28 : 0;
            m.setInitialVisibleRange(DISPLAY_COUNT, fb);
          });
        }
      }
    });
    ro.observe(containerRef.current);

    const bootstrapIfReady = () => {
      const m = managerRef.current;
      if (!m || m.hasMainSeries()) return;
      const data = barsRef.current;
      if (!canApplyChartBars(data, barsMarketRef.current, marketRef.current)) return;
      void reloadAllRef.current(
        m,
        data,
        prevChartType.current,
        prevTheme.current,
        prevLogScale.current,
        indicatorsRef.current,
      );
    };
    requestAnimationFrame(() => requestAnimationFrame(bootstrapIfReady));

    return () => {
      unsub();
      unsubClick();
      container.removeEventListener('click', handleDrawingCapture, { capture: true });
      container.removeEventListener('contextmenu', onContextMenuCapture, { capture: true });
      container.removeEventListener('dblclick', onCandlePaneDblClick, { capture: true });
      container.removeEventListener('pointerdown', onPanPointerDown);
      container.removeEventListener('wheel', onWheelCapture, { capture: true });
      window.removeEventListener('pointermove', onPanPointerMove);
      window.removeEventListener('pointerup', onPanPointerUp);
      window.removeEventListener('pointercancel', onPanPointerUp);
      if (drawingSingleTimerRef.current) {
        clearTimeout(drawingSingleTimerRef.current);
        drawingSingleTimerRef.current = null;
      }
      reloadSafetyTimers.current.forEach(clearTimeout);
      reloadSafetyTimers.current = [];
      if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
      ro.disconnect();
      managerRef.current?.destroy();
      managerRef.current = null;
      setChartMgr(null);
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    managerRef.current?.setDisplayTimezone(displayTimezone, timeframe);
  }, [displayTimezone, timeframe]);

  // 종목·타임프레임 변경 시 barsKey 캐시만 초기화 (reloadAll 은 bars effect 에서 1회만)
  useEffect(() => {
    if (prevMarket.current === market && prevMarketTf.current === timeframe) return;
    prevMarket.current = market;
    prevMarketTf.current = timeframe;
    prevBarsKey.current = '';
    prevIndKey.current = '';
    barsRef.current = [];
    reloadRetryRef.current = 0;
    reloadGenRef.current += 1;
    reloadInFlightRef.current = false;
    reloadCoalesceRef.current = false;
    reloadSafetyTimers.current.forEach(clearTimeout);
    reloadSafetyTimers.current = [];
    managerRef.current?.cancelPendingIndicatorUpdates();
  }, [market, timeframe]);

  // ── 데이터/인디케이터 동기화 ─────────────────────────────────────────────
  useEffect(() => {
    const mgr = managerRef.current;
    if (!mgr || !chartMgr || bars.length === 0) return;
    if (!canApplyChartBars(bars, barsMarket, market)) return;

    // manager 준비 전 bars 가 먼저 도착한 경우(로그인 직후 등) 메인 시리즈 누락 방지
    if (!mgr.hasMainSeries()) {
      void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
      return;
    }

    const bk = barsKey(bars, market);
    const ik = indKey(indicators);
    const barsChanged = bk !== prevBarsKey.current;
    const indChanged  = ik !== prevIndKey.current;
    const ctChanged   = chartType !== prevChartType.current;
    const thChanged   = theme     !== prevTheme.current;
    const lsChanged   = logScale  !== prevLogScale.current;
    const expectIndicators = countExpectedVisibleIndicators(indicators);
    const loadedIndicators = mgr.getLoadedIndicatorCount();
    const indicatorsIncomplete = expectIndicators > 0
      && mgr.hasMainSeries()
      && loadedIndicators < expectIndicators;

    if (!barsChanged && !indChanged && !ctChanged && !thChanged && !lsChanged && !indicatorsIncomplete) return;

    if (barsChanged || ctChanged || thChanged || indicatorsIncomplete) {
      // 데이터·차트 형식 변경 또는 지표 누락 → 전체 재로드
      void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
    } else if (lsChanged) {
      prevLogScale.current = logScale;
      mgr.setLogScale(logScale);
    } else if (indChanged) {
      const prevPK = prevIndKey.current.split('@@')[0] ?? '';
      const currPK = paramKey(indicators);
      const prevSK = prevIndKey.current.split('@@')[1] ?? '';
      const currSK = styleKey(indicators);

      const needsRecalc  = prevPK !== currPK;
      // 같은 지표 집합인데 순서만 바뀐 경우 (reorder-only)
      const isReorderOnly = needsRecalc
        && sortedParamKey(indicators) === prevSortedPKRef.current
        && currSK === prevSK;

      if (isReorderOnly) {
        // ── pane 순서 변경만: 메인 차트를 건드리지 않고 지표만 재배치 ──────────
        // 1) 지표 영역만 커버 (순간 숨김) → 2) 지표만 재추가 → 3) 서서히 공개
        prevSortedPKRef.current = sortedParamKey(indicators);
        prevIndKey.current      = ik;

        const containerEl = containerRef.current;
        // 지표 영역 위에 불투명 커버 div 삽입
        let cover: HTMLDivElement | null = null;
        if (containerEl) {
          cover = document.createElement('div');
          cover.style.cssText = [
            'position:absolute', 'inset:0', 'z-index:900',
            'pointer-events:none', 'opacity:1',
            'background:var(--bg,#131722)',
          ].join(';');
          containerEl.parentElement?.appendChild(cover);
        }

        mgr.reloadIndicatorsOnly(indicators).then(() => {
          if (candleOnlyModeRef.current) {
            mgr.applyCandleOnlyLayout(true, wrapperRef.current?.clientHeight);
          }
          applyPaneLayout(mgr);
          requestAnimationFrame(() => requestAnimationFrame(() => {
            if (cover) {
              cover.style.transition = 'opacity 0.25s ease';
              cover.style.opacity    = '0';
              setTimeout(() => { cover?.remove(); }, 300);
            }
          }));
        });
      } else if (needsRecalc) {
        // ── 지표 추가·제거·파라미터 변경 → 지표만 재로드 (메인 캔들 유지) ──
        // reloadIndicatorsOnly 는 메인/볼륨 시리즈를 건드리지 않으므로
        // 차트 위치 초기화·깜빡임 없이 지표만 갱신된다.
        prevSortedPKRef.current = sortedParamKey(indicators);
        prevIndKey.current      = ik;

        // 현재 scroll/zoom 위치를 저장해 reload 후 복원
        const savedLR = mgr.getVisibleLogicalRange();

        mgr.reloadIndicatorsOnly(indicators).then(() => {
          if (candleOnlyModeRef.current) {
            mgr.applyCandleOnlyLayout(true, wrapperRef.current?.clientHeight);
          } else if (mgr.isCandleOnlyLayout()) {
            mgr.restoreFromCandleFullscreen(wrapperRef.current?.clientHeight);
          }
          applyPaneLayout(mgr);
          requestAnimationFrame(() => requestAnimationFrame(() => {
            if (savedLR) mgr.applyVisibleLogicalRange(savedLR.from, savedLR.to);
          }));
        });
      } else if (prevSK !== currSK) {
        // ── 스타일만 변경 (색상·선폭·plotVisibility) → in-place 업데이트 ────
        // 시리즈를 제거·재생성하지 않으므로 pane 재번호 없음
        for (const ind of indicators) {
          mgr.applyIndicatorStyle(ind);
        }
        prevIndKey.current = ik;
      }
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators]);

  // 데이터는 준비됐으나 메인 시리즈가 없는 빈 차트 자동 복구 (종목 전환·레이아웃 전환 직후)
  useEffect(() => {
    const mgr = managerRef.current;
    if (!mgr || !chartMgr || bars.length === 0) return;
    if (!canApplyChartBars(bars, barsMarket, market)) return;
    if (mgr.hasMainSeries()) return;

    const timers = [0, 80, 200, 500].map(delay =>
      window.setTimeout(() => {
        const m = managerRef.current;
        if (!m || m.hasMainSeries()) return;
        const data = barsRef.current;
        if (!canApplyChartBars(data, barsMarketRef.current, marketRef.current)) return;
        void reloadAllRef.current(
          m,
          data,
          prevChartType.current,
          prevTheme.current,
          prevLogScale.current,
          indicatorsRef.current,
        );
      }, delay),
    );

    return () => { timers.forEach(clearTimeout); };
  }, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators]);

  // 종목 전환 직후 reloadAll 이 중단되어 캔들만 남은 경우 자동 복구
  useEffect(() => {
    const mgr = managerRef.current;
    if (!mgr || !chartMgr || bars.length === 0) return;
    if (!canApplyChartBars(bars, barsMarket, market)) return;

    const expected = countExpectedVisibleIndicators(indicators);
    if (expected === 0) return;
    if (mgr.getLoadedIndicatorCount() >= expected) return;

    const timers = [120, 400, 900].map(delay =>
      window.setTimeout(() => {
        const m = managerRef.current;
        if (!m || !m.hasMainSeries()) return;
        if (!canApplyChartBars(bars, barsMarket, market)) return;
        const exp = countExpectedVisibleIndicators(indicatorsRef.current);
        if (exp === 0 || m.getLoadedIndicatorCount() >= exp) return;
        prevBarsKey.current = '';
        queueIndicatorRecovery();
      }, delay),
    );

    return () => { timers.forEach(clearTimeout); };
  }, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, queueIndicatorRecovery]);

  // cursor 모드에서 드로잉 위에 있으면 pointer 커서로 변경
  const handleWrapperMouseMove = useCallback((e: React.PointerEvent) => {
    if (panStateRef.current.active) return;
    if (canPanRef.current) {
      const container = containerRef.current;
      if (!container) return;
      if (
        modeRef.current === 'trading' &&
        drawingToolRef.current === 'cursor' &&
        drawingsVisibleRef.current &&
        !drawingsLockedRef.current
      ) {
        const rect = container.getBoundingClientRect();
        const cx = e.clientX - rect.left;
        const cy = e.clientY - rect.top;
        const m = managerRef.current;
        if (m) {
          const isHit = drawingsRef.current.some(d =>
            hitTestDrawing(cx, cy, d, m, container.clientWidth, container.clientHeight),
          );
          container.style.cursor = isHit ? 'pointer' : 'grab';
          return;
        }
      }
      container.style.cursor = 'grab';
      return;
    }
    if (
      modeRef.current !== 'trading'        ||
      drawingToolRef.current !== 'cursor'  ||
      !drawingsVisibleRef.current          ||
      drawingsLockedRef.current
    ) {
      if (containerRef.current) containerRef.current.style.cursor = '';
      return;
    }
    const container = containerRef.current;
    if (!container) return;
    const rect = container.getBoundingClientRect();
    const cx = e.clientX - rect.left;
    const cy = e.clientY - rect.top;
    const cssW = container.clientWidth;
    const cssH = container.clientHeight;
    const m = managerRef.current;
    if (!m) return;

    const ds = drawingsRef.current;
    const isHit = ds.some(d => hitTestDrawing(cx, cy, d, m, cssW, cssH));
    container.style.cursor = isHit ? 'pointer' : '';
  }, []);

  const fmtCtxPrice = (p: number) =>
    p >= 1000 ? Math.round(p).toLocaleString('ko-KR') : p.toFixed(p >= 1 ? 2 : 6);

  const closeCtxAndRequest = useCallback((side: 'buy' | 'sell') => {
    if (!ctxMenu || !market) return;
    onTradeOrderRequestRef.current?.({ market, price: ctxMenu.price, side });
    setCtxMenu(null);
  }, [ctxMenu, market]);

  return (
    // wrapperRef: 스크롤 영역 (overflow-y: auto → App.css .tv-chart-wrap)
    <div
      ref={wrapperRef}
      className="tv-chart-wrap"
      style={{ flex: 1, position: 'relative', minHeight: 0 }}
      onPointerMove={handleWrapperMouseMove}
    >
      <div
        ref={containerRef}
        className="chart-container"
      />
      {ctxMenu && (
        <ChartContextMenu
          x={ctxMenu.x}
          y={ctxMenu.y}
          marketLabel={getKoreanName(market) || market.replace(/^KRW-/, '')}
          priceLabel={`${fmtCtxPrice(ctxMenu.price)} KRW`}
          onBuy={() => closeCtxAndRequest('buy')}
          onSell={() => closeCtxAndRequest('sell')}
          onClose={() => setCtxMenu(null)}
        />
      )}
      {chartMgr && (
        <DrawingCanvas
          manager={chartMgr}
          activeTool={drawingTool}
          drawings={drawings}
          onAddDrawing={onAddDrawing}
          onZoom={(x0, x1) => chartMgr.zoomToXRange(x0, x1)}
          theme={theme}
          enabled={mode === 'trading' || drawingTool === 'zoom' || drawingTool === 'magnifier'}
          visible={drawingsVisible}
          locked={drawingsLocked}
          selectedDrawingId={selectedDrawingId}
        />
      )}
      {/* PaneLegend: containerRef 를 상태에 저장해 React 렌더와 동기화 */}
      {chartMgr && (
        <CandlePaneControlsPortal
          manager={chartMgr}
          getContainer={() => containerRef.current}
          candleOnly={candleOnlyMode}
          onExpand={() => setCandleOnlyMode(true)}
          onRestore={() => setCandleOnlyMode(false)}
        />
      )}
      {chartMgr && !candleOnlyMode && (
        <PaneLegendPortal
          manager={chartMgr}
          indicators={indicators}
          getContainer={() => containerRef.current}
          onHoverName={(id, sx, sy) => onHoverPaneLegend?.(id, sx, sy)}
          onLeaveName={() => onLeavePaneLegend?.()}
          onDoubleClick={(id) => onSeriesDoubleClick?.(id, { x: 0, y: 0 })}
          onReorder={onReorderIndicators}
          onMerge={onMergeIndicators}
          onSplit={onSplitIndicatorPane}
          onExpand={onExpandIndicator}
          onRemove={onRemoveIndicator}
          onDuplicate={onDuplicateIndicator}
          focusedId={focusedIndicatorId}
          onRestore={onRestoreIndicators}
        />
      )}

      {/* 마우스 오버 시 표시되는 플로팅 줌/스크롤 툴바 */}
      {chartMgr && (
        <ChartHoverToolbar
          onZoomOut={()      => managerRef.current?.zoomOut()}
          onZoomIn={()       => managerRef.current?.zoomIn()}
          onFit={()          => managerRef.current?.fitContent()}
          onScrollLeft={()   => managerRef.current?.scrollLeft()}
          onScrollRight={()  => managerRef.current?.scrollRight()}
          onFullView={onFullView}
        />
      )}

      {/* 돋보기 오버레이 */}
      <ChartMagnifier
        containerRef={containerRef}
        wrapperRef={wrapperRef}
        enabled={magnifierEnabled}
        onClose={onMagnifierClose ?? (() => {})}
      />
    </div>
  );
};

const CandlePaneControlsPortal: React.FC<{
  manager: ChartManager;
  getContainer: () => HTMLElement | null;
  candleOnly: boolean;
  onExpand: () => void;
  onRestore: () => void;
}> = ({ manager, getContainer, candleOnly, onExpand, onRestore }) => {
  const [el, setEl] = useState<HTMLElement | null>(null);
  useEffect(() => {
    const c = getContainer();
    if (c) { setEl(c); return; }
    const tid = setTimeout(() => setEl(getContainer()), 100);
    return () => clearTimeout(tid);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  if (!el) return null;
  return (
    <CandlePaneControls
      manager={manager}
      containerEl={el}
      candleOnly={candleOnly}
      onExpand={onExpand}
      onRestore={onRestore}
    />
  );
};

// ── containerEl 을 안전하게 주입하는 래퍼 ───────────────────────────────────
const PaneLegendPortal: React.FC<
  Omit<PaneLegendProps, 'containerEl'> & { getContainer: () => HTMLElement | null }
> = ({ getContainer, ...rest }) => {
  const [el, setEl] = useState<HTMLElement | null>(null);

  useEffect(() => {
    // 마운트 직후 containerEl 확보
    const container = getContainer();
    if (container) { setEl(container); return; }
    // 혹시 null 이면 짧게 재시도
    const tid = setTimeout(() => setEl(getContainer()), 100);
    return () => clearTimeout(tid);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  if (!el) return null;
  return <PaneLegend {...rest} containerEl={el} />;
};

// ── 변경 감지용 키 생성 헬퍼 ────────────────────────────────────────────────
function countExpectedVisibleIndicators(inds: IndicatorConfig[]): number {
  return inds.filter(i => i.hidden !== true).length;
}

function barsKey(bars: OHLCVBar[], market = ''): string {
  if (bars.length === 0) return '';
  const last = bars[bars.length - 1];
  // last.close 제외 — 틱마다 full reload 방지 (실시간은 updateBar/appendBar 경로)
  return `${market}:${bars.length}:${bars[0].time}:${last.time}`;
}

/**
 * 재계산이 필요한 변경 키 (params·플롯 구성·추가·제거) — 배열 순서 기준
 * 이 키가 바뀌면 시리즈를 재생성해야 한다.
 */
function paramKey(inds: IndicatorConfig[]): string {
  return inds.map(i =>
    `${i.id}|${JSON.stringify(i.params)}|${(i.plots ?? []).map(p => p.id).sort().join(',')}|${i.mergedWith ?? ''}`
  ).join(';');
}

/**
 * 순서 무관 파라미터 키 — id 기준으로 정렬
 * paramKey 값은 같지만 배열 순서만 다른 경우를 감지하는 데 사용.
 */
function sortedParamKey(inds: IndicatorConfig[]): string {
  return [...inds]
    .sort((a, b) => a.id.localeCompare(b.id))
    .map(i => `${i.id}|${JSON.stringify(i.params)}|${(i.plots ?? []).map(p => p.id).sort().join(',')}|${i.mergedWith ?? ''}`)
    .join(';');
}

/**
 * 스타일만 포함하는 키 (색상·선폭·plotVisibility)
 * 이 키만 바뀌면 시리즈를 재생성하지 않고 in-place 업데이트.
 */
function styleKey(inds: IndicatorConfig[]): string {
  return inds.map(i =>
    `${i.id}|${i.hidden ? '1' : '0'}|${i.lastValueVisible === false ? '0' : '1'}|${(i.plots ?? []).map(p => `${p.color ?? ''}:${p.lineWidth ?? 1}:${p.lineStyle ?? 'solid'}`).join(',')}|${JSON.stringify(i.plotVisibility ?? {})}|${JSON.stringify((i.hlines ?? []).map(h => `${h.price}:${h.color}:${h.visible ?? true}:${h.lineStyle ?? 'dashed'}:${h.lineWidth ?? 1}`))}|${JSON.stringify(i.cloudColors ?? {})}`
  ).join(';');
}

/** 전체 변경 감지 키 (두 키를 합침) */
function indKey(inds: IndicatorConfig[]): string {
  return paramKey(inds) + '@@' + styleKey(inds);
}

export default TradingChart;
