/**
 * ChartSlot
 * 레이아웃 내 개별 차트 슬롯. 자체 심볼/타임프레임/데이터를 관리한다.
 */
import React, {
  useState, useEffect, useLayoutEffect, useCallback, useRef, useMemo,
  useImperativeHandle, forwardRef,
} from 'react';
import ReactDOM from 'react-dom';
import TradingChart from './TradingChart';
import IndicatorContextToolbar from './IndicatorContextToolbar';
import IndicatorSettingsModal from './IndicatorSettingsModal';
import MainChartSettingsModal from './MainChartSettingsModal';
import { MarketSearchPanel } from './MarketSearchPanel';
import { generateBars, formatPrice } from '../utils/dataGenerator';
import { getKoreanName } from '../utils/marketNameCache';
import type { TickerData } from '../hooks/useMarketTicker';
import {
  formatChartLiveChangePct,
  formatChartLivePrice,
  resolveChartLiveQuote,
} from '../utils/chartLiveQuote';
import { DEFAULT_MAIN_CHART_STYLE } from '../utils/storage';
import { enrichIndicatorConfig, getIndicatorDef } from '../utils/indicatorRegistry';
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from '../utils/smaConfig';
import { normalizeIchimokuConfig } from '../utils/ichimokuConfig';
import { isUpbitMarket } from '../utils/upbitApi';
import { useChartRealtimeData, type WsStatus } from '../hooks/useChartRealtimeData';
import { useLiveReceiveFlash } from '../hooks/useLiveReceiveFlash';
import VirtualLiveBadge from './virtual/VirtualLiveBadge';
import type { VirtualLiveStatus } from '../hooks/useVirtualTargetLiveStatus';
import { invalidateMarketCache } from '../utils/requestCache';
import { DISPLAY_COUNT } from '../hooks/useUpbitData';
import { useHistoryLoader, LOAD_MORE_TRIGGER } from '../hooks/useHistoryLoader';
import { useIndicatorSettings } from '../hooks/useIndicatorSettings';
import { markChartIndicatorSaveCommitted } from '../utils/indicatorChartDefaultsSync';
import {
  duplicateIndicatorInList,
  mergeIndicators,
  reorderIndicatorGroup,
  removeIndicatorFromList,
  removeIndicatorTypeFromList,
  splitIndicatorPane,
  replaceIndicatorConfigsFromTypes,
} from '../utils/indicatorPaneMerge';
import { sortIndicatorsByTypeOrder } from '../utils/indicatorListOrder';
import { saveSlot } from '../utils/backendApi';
import type {
  OHLCVBar, ChartType, Theme, Timeframe,
  IndicatorConfig, LegendData, Drawing, ChartMode, MainChartStyle,
} from '../types';
import type { ChartManager } from '../utils/ChartManager';
import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator';

// ── 타임프레임 옵션 ────────────────────────────────────────────────────────
const TF_OPTIONS: Timeframe[] = ['1m','3m','5m','10m','15m','30m','1h','4h','1D','1W','1M'];

function compactKoName(market: string): string {
  const ko = getKoreanName(market);
  if (ko && ko !== market) return ko;
  return market.replace(/^KRW-/, '');
}

function wsToLiveStatus(status: WsStatus): VirtualLiveStatus {
  switch (status) {
    case 'connected': return 'live';
    case 'connecting': return 'connecting';
    case 'disconnected':
    case 'error':
      return 'disconnected';
    default:
      return 'idle';
  }
}

// ── Ws 배지 ───────────────────────────────────────────────────────────────
const WsBadge: React.FC<{ status: WsStatus; isUpbit: boolean }> = ({ status, isUpbit }) => {
  if (!isUpbit) return null;
  const map: Record<WsStatus, { dot: string; label: string }> = {
    connecting:   { dot: 'dot-yellow', label: '연결 중...' },
    connected:    { dot: 'dot-lime',   label: '실시간' },
    disconnected: { dot: 'dot-red',    label: '재연결' },
    error:        { dot: 'dot-red',    label: '오류' },
  };
  const { dot, label } = map[status];
  return (
    <span className={`ws-badge ${dot}`} style={{ fontSize: '10px' }}>
      <span className="ws-dot" />{label}
    </span>
  );
};

let _slotIndCounter = 0;
function newIndId() { return `sind_${++_slotIndCounter}_${Date.now()}`; }

// ── Ref 핸들 (외부에서 슬롯 조작용) ─────────────────────────────────────────
export interface ChartSlotHandle {
  /** 지표 추가 */
  addIndicator: (type: string, fromConfig?: IndicatorConfig) => void;
  /** 지표 일괄 추가 (탭 전체 추가 — 기존 지표 제거 후 탭 지표만 적용) */
  addIndicators: (types: string[]) => void;
  /** 지표 제거 (type 기준) */
  removeIndicatorByType: (type: string) => void;
  /** 보조지표 pane 복사 */
  duplicateIndicator: (sourceId: string) => void;
  /** 현재 활성 심볼 */
  getSymbol: () => string;
  /** 현재 활성 타임프레임 */
  getTimeframe: () => Timeframe;
  /** 현재 활성 지표 목록 */
  getIndicators: () => IndicatorConfig[];
  /** 심볼 변경 */
  setSymbol: (s: string) => void;
  /** 타임프레임 변경 */
  setTimeframe: (tf: Timeframe) => void;
  /** 지표 목록 교체 (모드 전환 시 이전 설정 복원용) */
  setIndicators: (inds: IndicatorConfig[]) => void;
  /** 크로스헤어 자석모드 설정 */
  setCrosshairMagnet: (enabled: boolean) => void;
  /** 가격축 라벨·설명 on/off (캔들·보조지표 동일 값) */
  setSeriesPriceLabelsEnabled: (enabled: boolean) => void;
  setCandleAreaPriceLabelsEnabled: (enabled: boolean) => void;
  setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => void;
  /** 거래량 pane on/off */
  setVolumeVisible: (visible: boolean) => void;
  setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => void;
  /** 차트 시간축·크로스헤어 날짜 형식 */
  setChartTimeFormat: (format: string, timeframe?: Timeframe) => void;
}

// ── Props ─────────────────────────────────────────────────────────────────
export interface ChartSlotProps {
  slotIndex:      number;
  theme:          Theme;
  /** 슬롯이 활성(포커스) 상태인지 */
  active:         boolean;
  onActivate:     () => void;
  /** 외부에서 강제 심볼 동기화 */
  forcedSymbol?:  string;
  /** 외부에서 강제 타임프레임 동기화 */
  forcedTf?:      Timeframe;
  /** 마운트 시 1회만 적용되는 초기 심볼 (단일↔다중 모드 전환 시 설정 이전용) */
  initialSymbol?:     string;
  /** 마운트 시 1회만 적용되는 초기 타임프레임 */
  initialTimeframe?:  Timeframe;
  /** 마운트 시 1회만 적용되는 초기 지표 목록 */
  initialIndicators?: IndicatorConfig[];
  /** Crosshair sync: 동기화 시각 (unix seconds) */
  syncTime?:      number;
  onCrosshairTime?: (t: number) => void;
  /** Time sync: 동기화 가시 시간 범위 */
  syncVisibleRange?: { from: number; to: number } | null;
  onVisibleRangeChange?: (r: { from: number; to: number }) => void;
  /** Date range sync: 동기화 가시 논리 범위 */
  syncLogicalRange?: { from: number; to: number } | null;
  onLogicalRangeChange?: (r: { from: number; to: number }) => void;
  /** 돋보기 활성 여부 (App 에서 전달) */
  magnifierEnabled?: boolean;
  /** 돋보기 닫기 콜백 */
  onMagnifierClose?: () => void;
  /** 멀티차트 전체 보기: 이 슬롯을 단일 차트로 확장 */
  onFullView?:    (symbol: string) => void;
  /**
   * 슬롯 내 지표 목록이 바뀔 때마다 호출.
   * App 에서 활성 슬롯의 지표 목록을 IndicatorModal 에 반영하기 위해 사용.
   */
  onIndicatorsChange?: (indicators: IndicatorConfig[]) => void;
  /**
   * 슬롯 내 심볼이 바뀔 때마다 호출.
   * App 에서 activeSlotSymbol(호가창 심볼)을 동기화하기 위해 사용.
   */
  onSymbolChange?: (symbol: string) => void;
  /** 자석모드 상태 (크로스헤어 스냅) */
  magnetMode?: 'off' | 'weak' | 'strong';
  /** 차트 우클릭 → 매수·매도 패널 자동 입력 */
  onTradeOrderRequest?: (req: { market: string; price: number; side: 'buy' | 'sell' }, slotIndex: number) => void;
  /** 캔들 영역 오버레이 지표 가격축 라벨·설명 */
  chartCandleAreaPriceLabels?: boolean;
  /** 하단 보조지표 pane 가격축 라벨·설명 */
  chartSeriesPriceLabels?: boolean;
  /** 차트 하단 거래량 바 표시 */
  chartVolumeVisible?: boolean;
  chartRealtimeSource?: 'BACKEND_STOMP' | 'UPBIT_DIRECT';
  displayTimezone?: string;
  /** 멀티차트 — 가상투자 카드 차트처럼 컴팩트 카드 UI */
  compactMode?: boolean;
  /** 실시간 수신 시 카드 아웃라인 형광 연두 음영 (설정 연동) */
  chartLiveReceiveHighlight?: boolean;
  /** 실시간 차트 화면 표시 여부 (설정 등 다른 메뉴에서 숨김 시 false) */
  chartVisible?: boolean;
  /** pane 구분선 */
  chartPaneSeparator?: ChartPaneSeparatorOptions;
  /** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
  chartCrosshairInfoVisible?: boolean;
  /** 업비트 실시간 티커 맵 (슬롯 심볼별 현재가·등락) */
  marketTickers?: Map<string, TickerData>;
}

const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot({
  slotIndex, theme, active, onActivate,
  forcedSymbol, forcedTf,
  initialSymbol, initialTimeframe, initialIndicators,
  syncTime, onCrosshairTime,
  syncVisibleRange, onVisibleRangeChange,
  syncLogicalRange, onLogicalRangeChange,
  onFullView,
  magnifierEnabled = false,
  onMagnifierClose,
  onIndicatorsChange,
  onSymbolChange,
  magnetMode = 'off',
  onTradeOrderRequest,
  chartCandleAreaPriceLabels = true,
  chartSeriesPriceLabels = true,
  chartVolumeVisible = true,
  chartRealtimeSource = 'BACKEND_STOMP',
  displayTimezone,
  compactMode = false,
  chartLiveReceiveHighlight = true,
  chartVisible = true,
  chartPaneSeparator,
  chartCrosshairInfoVisible = true,
  marketTickers,
}, ref) {
  // ── 전역 지표 파라미터 + 시각 설정 (DB 동기화) ────────────────────────
  const { getParams, saveParams, getVisualConfig, saveVisual } = useIndicatorSettings();

  // ── 자체 상태 ─────────────────────────────────────────────────────────
  // initial* 값이 있으면 우선 적용 (단일↔다중 모드 전환 시 설정 이전용)
  const [symbol,    setSymbol]    = useState(initialSymbol    ?? forcedSymbol ?? 'KRW-BTC');
  const [timeframe, setTimeframe] = useState<Timeframe>(initialTimeframe ?? forcedTf ?? '1D');
  const [chartType] = useState<ChartType>('candlestick');
  const [indicators, setIndicators] = useState<IndicatorConfig[]>(initialIndicators ?? []);
  const [drawings]  = useState<Drawing[]>([]);
  const [legend,    setLegend]    = useState<LegendData | null>(null);
  const [ctxToolbar, setCtxToolbar] = useState<{ entryId: string; screenX: number; screenY: number } | null>(null);
  const [settingsModalId, setSettingsModalId] = useState<string | null>(null);
  const [showMainChartModal, setShowMainChartModal] = useState(false);
  const [mainChartStyle, setMainChartStyle] = useState<MainChartStyle>(DEFAULT_MAIN_CHART_STYLE);
  const [showMarket,    setShowMarket]    = useState(false);
  // MarketSearchPanel 위치: compact=종목명 버튼, 일반=슬롯 전체
  const [slotAnchorRect, setSlotAnchorRect] = useState<DOMRect | null>(null);

  const managerRef   = useRef<ChartManager | null>(null);
  const compactNameRef = useRef<HTMLButtonElement>(null);
  // initialSymbol 도 초기값으로 포함: requestAnimationFrame 이전에 올바른 값 보장
  const symbolRef    = useRef(initialSymbol ?? forcedSymbol ?? 'KRW-BTC');
  const timeframeRef = useRef<Timeframe>(forcedTf ?? '1D');

  // symbol / timeframe 변경 시 ref 동기화 (외부 접근용)
  useEffect(() => { symbolRef.current    = symbol;    }, [symbol]);
  useEffect(() => { timeframeRef.current = timeframe; }, [timeframe]);

  // 심볼 변경 시 부모(App)에 알림 → 호가창 activeSlotSymbol 동기화
  const onSymbolChangeRef = useRef(onSymbolChange);
  onSymbolChangeRef.current = onSymbolChange;
  useEffect(() => {
    onSymbolChangeRef.current?.(symbol);
  }, [symbol]);

  // 지표 목록 변경 시 부모(App)에 알림 → IndicatorModal 의 activeIndicators 갱신
  const onIndicatorsChangeRef = useRef(onIndicatorsChange);
  onIndicatorsChangeRef.current = onIndicatorsChange;
  useEffect(() => {
    onIndicatorsChangeRef.current?.(indicators);
  }, [indicators]);

  // indicators 를 ref 로도 유지 (useImperativeHandle 의 최신 값 접근용)
  const indicatorsRef = useRef<IndicatorConfig[]>(indicators);
  useEffect(() => { indicatorsRef.current = indicators; }, [indicators]);

  // ── 외부 핸들 노출 ─────────────────────────────────────────────────────
  useImperativeHandle(ref, () => ({
    addIndicator: (type: string, fromConfig?: IndicatorConfig) => {
      setIndicators(prev => {
        if (prev.some(i => i.type === type)) return prev;
        const def = getIndicatorDef(type);
        if (!def) return prev;
        let cfg: IndicatorConfig;
        if (fromConfig) {
          cfg = { ...fromConfig, id: newIndId(), type: def.type };
        } else {
          const params = getParams(type, def.defaultParams);
          const { plots, hlines, cloudColors, bandBackground } = getVisualConfig(type, def.plots, def.hlines);
          cfg = {
            id: newIndId(),
            type: def.type,
            params,
            plots,
            hlines,
            ...(cloudColors ? { cloudColors } : {}),
            ...(bandBackground ? { bandBackground } : {}),
          };
        }
        if (type === 'SMA') {
          cfg = normalizeSmaConfig({
            ...cfg,
            plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(),
          });
        } else if (type === 'IchimokuCloud') {
          cfg = normalizeIchimokuConfig(cfg);
        }
        return sortIndicatorsByTypeOrder([...prev, cfg]);
      });
    },
    addIndicators: (types: string[]) => {
      if (!types.length) return;
      setIndicators(
        sortIndicatorsByTypeOrder(
          replaceIndicatorConfigsFromTypes(types, getParams, getVisualConfig, newIndId),
          types,
        ),
      );
    },
    removeIndicatorByType: (type: string) => {
      setIndicators(prev => removeIndicatorTypeFromList(prev, type));
    },
    duplicateIndicator: (sourceId: string) => {
      setIndicators(prev => duplicateIndicatorInList(sourceId, prev, newIndId) ?? prev);
    },
    getSymbol:      () => symbolRef.current,
    getTimeframe:   () => timeframeRef.current,
    getIndicators:  () => indicatorsRef.current,
    setSymbol: (s: string) => {
      pendingRealtimeBarRef.current = null;
      // 새 종목의 캐시를 미리 무효화해 신선한 데이터를 요청하도록 함
      if (isUpbitMarket(s)) invalidateMarketCache(s);
      setSymbol(s);
      setLegend(null);
    },
    setTimeframe: (tf: Timeframe) => { setTimeframe(tf); },
    setIndicators: (inds: IndicatorConfig[]) => { setIndicators(inds); },
    setCrosshairMagnet: (enabled: boolean) => {
      managerRef.current?.setCrosshairMagnet(enabled);
    },
    setSeriesPriceLabelsEnabled: (enabled: boolean) => {
      managerRef.current?.setSeriesPriceLabelsEnabled(enabled);
    },
    setCandleAreaPriceLabelsEnabled: (enabled: boolean) => {
      managerRef.current?.setCandleAreaPriceLabelsEnabled(enabled);
    },
    setIndicatorAreaPriceLabelsEnabled: (enabled: boolean) => {
      managerRef.current?.setIndicatorAreaPriceLabelsEnabled(enabled);
    },
    setVolumeVisible: (visible: boolean) => {
      managerRef.current?.setVolumeVisible(visible);
    },
    setPaneSeparatorOptions: (opts: ChartPaneSeparatorOptions) => {
      managerRef.current?.setPaneSeparatorOptions(opts);
    },
    setChartTimeFormat: (format: string, tf?: Timeframe) => {
      managerRef.current?.setChartTimeFormat(format);
      if (tf) managerRef.current?.setDisplayTimezone(displayTimezone ?? 'Asia/Seoul', tf, format);
    },
  }), [displayTimezone]);

  // 자석모드 변경 → ChartManager 크로스헤어 모드 즉시 반영
  useEffect(() => {
    managerRef.current?.setCrosshairMagnet(magnetMode !== 'off');
  }, [magnetMode]);

  useEffect(() => {
    managerRef.current?.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels);
  }, [chartCandleAreaPriceLabels]);

  useEffect(() => {
    managerRef.current?.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
  }, [chartSeriesPriceLabels]);

  useEffect(() => {
    managerRef.current?.setVolumeVisible(chartVolumeVisible);
  }, [chartVolumeVisible]);

  useEffect(() => {
    if (!chartPaneSeparator) return;
    managerRef.current?.setPaneSeparatorOptions(chartPaneSeparator);
  }, [chartPaneSeparator]);

  // 외부 심볼 동기화
  useEffect(() => {
    if (forcedSymbol && forcedSymbol !== symbol) setSymbol(forcedSymbol);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [forcedSymbol]);

  // 외부 타임프레임 동기화
  useEffect(() => {
    if (forcedTf && forcedTf !== timeframe) setTimeframe(forcedTf);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [forcedTf]);

  // mainChartStyle → ChartManager
  useEffect(() => {
    managerRef.current?.updateMainSeriesStyle(mainChartStyle);
  }, [mainChartStyle]);

  // ── 슬롯 상태 변경 시 DB 자동 저장 (디바운스 2s) ─────────────────────────
  // 멀티슬롯 모드에서 각 슬롯 독립 저장 보장 (App.tsx workspaceState는 슬롯 0만 포함)
  const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  useEffect(() => {
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(() => {
      saveSlot(slotIndex, {
        slotIndex,
        symbol,
        timeframe,
        chartType: 'candlestick',
        theme,
        mode: 'normal',
        logScale: false,
        drawingsLocked: false,
        drawingsVisible: true,
        indicators,
        drawings:    [],
        paneLayout:  null,
        mainChartStyle,
      }).catch(err => console.error('[ChartSlot] saveSlot failed', err));
    }, 2000);
    return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [slotIndex, symbol, timeframe, theme, indicators, mainChartStyle]);

  // ── 동기화 콜백 refs (항상 최신 값을 참조하도록) ─────────────────────────
  const onVisibleRangeChangeRef  = useRef(onVisibleRangeChange);
  const onLogicalRangeChangeRef  = useRef(onLogicalRangeChange);
  onVisibleRangeChangeRef.current  = onVisibleRangeChange;
  onLogicalRangeChangeRef.current  = onLogicalRangeChange;

  // 외부에서 sync 값을 적용하는 중임을 표시 (feedback loop 방지)
  const isSyncingTimeRef    = useRef(false);
  const isSyncingLogicalRef = useRef(false);

  // 데이터 로드 전 수신한 sync range 를 저장해두는 pending ref
  // → onDataLoaded 콜백에서 재적용 (멀티차트 첫 마운트 시 타이밍 문제 해결)
  const pendingSyncVisibleRef = useRef<{ from: number; to: number } | null>(null);
  const pendingSyncLogicalRef = useRef<{ from: number; to: number } | null>(null);
  /** 종목·TF 전환 직후 sync range 재적용 스킵 (이전 종목 뷰포트가 새 차트에 적용되는 것 방지) */
  const skipSyncApplyRef = useRef(false);

  // ── Crosshair sync: syncTime → setCrosshairAtTime ────────────────────
  useEffect(() => {
    if (!syncTime) { managerRef.current?.clearCrosshairPosition(); return; }
    managerRef.current?.setCrosshairAtTime(syncTime);
  }, [syncTime]);

  // ── Time sync: syncVisibleRange 외부 적용 ────────────────────────────
  useEffect(() => {
    if (!syncVisibleRange || skipSyncApplyRef.current) return;
    const mgr = managerRef.current;
    if (!mgr || mgr.getRawBarsLength() === 0) {
      // 데이터 로드 전 → pending 저장 후 onDataLoaded 에서 재적용
      pendingSyncVisibleRef.current = syncVisibleRange;
      return;
    }
    isSyncingTimeRef.current = true;
    mgr.applyVisibleTimeRange(syncVisibleRange.from, syncVisibleRange.to);
    requestAnimationFrame(() => { isSyncingTimeRef.current = false; });
  }, [syncVisibleRange]);

  // ── Date range sync: syncLogicalRange 외부 적용 ──────────────────────
  useEffect(() => {
    if (!syncLogicalRange || skipSyncApplyRef.current) return;
    const mgr = managerRef.current;
    if (!mgr || mgr.getRawBarsLength() === 0) {
      pendingSyncLogicalRef.current = syncLogicalRange;
      return;
    }
    isSyncingLogicalRef.current = true;
    mgr.applyVisibleLogicalRange(syncLogicalRange.from, syncLogicalRange.to);
    requestAnimationFrame(() => { isSyncingLogicalRef.current = false; });
  }, [syncLogicalRange]);

  // 멀티 레이아웃 새로고침 시 차트 빈 화면 방지:
  // CSS Grid 높이 확정 지연에 대비해 여러 시점에 pane 높이를 재적용한다.
  // TradingChart 자체도 applyPaneLayout 자동 재시도를 하지만, 슬롯 레벨에서도 보강.
  // ※ TradingChart를 재마운트(key 변경)하면 managerRef가 초기화되어 MarketSearchPanel 등
  //   다른 상태에 영향을 줄 수 있으므로, 재마운트 대신 resetPaneHeights 재시도만 수행한다.
  const slotContainerRef = useRef<HTMLDivElement | null>(null);
  // 데이터 로딩 완료 후 차트가 여전히 비어 있으면 1회만 재마운트 (data + blank guard)
  const [reloadTick, setReloadTick] = useState(0);
  const reloadTriggeredRef = useRef(false); // 재마운트는 최대 1회
  useLayoutEffect(() => {
    reloadTriggeredRef.current = false;
    chartLiveReadyRef.current = false;
    pendingRealtimeBarRef.current = null;
    pendingSyncVisibleRef.current = null;
    pendingSyncLogicalRef.current = null;
    skipSyncApplyRef.current = true;
  }, [symbol, timeframe]);
  useEffect(() => {
    const checks = [200, 500, 1000, 2000].map(delay =>
      setTimeout(() => {
        const el  = slotContainerRef.current;
        const mgr = managerRef.current;
        if (!el || !mgr) return;
        const h = el.clientHeight;
        if (h <= 0) return;
        if (mgr.hasMainSeries()) {
          // 차트 데이터 있음 → pane 높이만 재적용
          mgr.resetPaneHeights(h);
        } else if (!reloadTriggeredRef.current) {
          // 차트 비어 있고 아직 재시도 안 했음 → 1회만 재마운트
          // (bars 로딩 지연일 수 있으므로, 늦은 타이머에서만 트리거)
          if (delay >= 1000) {
            reloadTriggeredRef.current = true;
            setReloadTick(t => t + 1);
          }
        }
      }, delay)
    );
    return () => checks.forEach(clearTimeout);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // ── 데이터 ────────────────────────────────────────────────────────────
  const useUpbit = isUpbitMarket(symbol);

  // 과거 데이터 추가 로드 (차트를 왼쪽으로 스크롤할 때)
  const { isLoadingMore, loadMoreRef } = useHistoryLoader({
    symbol, timeframe, isUpbit: useUpbit, managerRef,
  });

  const [simBars, setSimBars] = useState<OHLCVBar[]>(() =>
    !isUpbitMarket(symbol) ? generateBars(symbol, timeframe) : []
  );
  useEffect(() => {
    if (useUpbit) return;
    setSimBars(generateBars(symbol, timeframe));
    setLegend(null);
  }, [symbol, timeframe, useUpbit]);

  const pendingRealtimeBarRef = useRef<OHLCVBar | null>(null);
  const barsMarketRef = useRef<string | null>(null);
  const chartLiveReadyRef = useRef(false);

  const handleCandlesReady = useCallback(() => {
    chartLiveReadyRef.current = true;
    const pending = pendingRealtimeBarRef.current;
    const mgr = managerRef.current;
    if (!pending || !mgr || barsMarketRef.current !== symbolRef.current) return;
    pendingRealtimeBarRef.current = null;
    mgr.updateBar(pending);
  }, []);

  const handleTickUpdate = useCallback((bar: OHLCVBar) => {
    if (barsMarketRef.current !== symbolRef.current) return;
    if (!chartLiveReadyRef.current) {
      pendingRealtimeBarRef.current = bar;
      return;
    }
    if (!managerRef.current) {
      pendingRealtimeBarRef.current = bar;
      return;
    }
    managerRef.current.updateBar(bar);
  }, []);
  const handleNewCandle = useCallback((bar: OHLCVBar) => {
    if (barsMarketRef.current !== symbolRef.current) return;
    if (!chartLiveReadyRef.current) {
      pendingRealtimeBarRef.current = bar;
      return;
    }
    if (!managerRef.current) {
      pendingRealtimeBarRef.current = bar;
      return;
    }
    managerRef.current.appendBar(bar);
  }, []);

  const { bars: upbitBars, barsMarket: upbitBarsMarket, latestBar, wsStatus, isLoading } = useChartRealtimeData(
    symbol, timeframe,
    useMemo(() => ({
      onTickUpdate: handleTickUpdate,
      onNewCandle:  handleNewCandle,
      enabled:      useUpbit,
      source:       chartRealtimeSource,
    }), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
  );

  const bars = useUpbit ? upbitBars : simBars;
  const barsMarket = useUpbit ? upbitBarsMarket : symbol;
  barsMarketRef.current = barsMarket;

  const lastKnownBar = useUpbit
    ? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null))
    : (bars.length > 0 ? bars[bars.length - 1] : null);
  const quoteBar = lastKnownBar;
  const slotTicker = marketTickers?.get(symbol) ?? null;
  const liveQuote = useMemo(
    () => resolveChartLiveQuote(slotTicker, quoteBar, bars),
    [slotTicker, quoteBar, bars],
  );

  const receiveSignal = useMemo(() => {
    if (!useUpbit || wsStatus !== 'connected' || !latestBar) return null;
    return `${latestBar.time}|${latestBar.close}|${latestBar.volume}`;
  }, [useUpbit, wsStatus, latestBar]);

  const receiving = useLiveReceiveFlash(receiveSignal, compactMode && useUpbit && wsStatus === 'connected');
  const highlightReceiving = chartLiveReceiveHighlight && receiving;

  // ── 인디케이터 핸들러 ─────────────────────────────────────────────────
  const handleIndicatorSave = useCallback((updated: IndicatorConfig) => {
    setIndicators(prev => prev.map(i => i.id === updated.id ? updated : i));
    setSettingsModalId(null);
    setCtxToolbar(null);
    markChartIndicatorSaveCommitted();
    // 파라미터 변경 DB 저장 → 백엔드 Ta4j 계산에 반영
    saveParams(updated.type, updated.params);
    // 시각 설정(색상·선굵기·수평선) 변경 DB 저장 → 새 지표 추가 시 기본값으로 사용
    saveVisual(
      updated.type,
      updated.plots,
      updated.hlines,
      updated.cloudColors,
      updated.bandBackground,
    );
  }, [saveParams, saveVisual]);

  const handleToggleHidden = useCallback((id: string) => {
    setIndicators(prev => prev.map(i => i.id === id ? { ...i, hidden: !i.hidden } : i));
  }, []);

  const handleRemoveById = useCallback((id: string) => {
    setIndicators(prev => removeIndicatorFromList(prev, id));
    setCtxToolbar(null);
    setSettingsModalId(null);
  }, []);

  const handleReorderIndicators = useCallback((fromId: string, insertBeforeId: string | null) => {
    setIndicators(prev => reorderIndicatorGroup(fromId, insertBeforeId, prev));
  }, []);

  const handleMergeIndicators = useCallback((fromId: string, intoId: string) => {
    setIndicators(prev => mergeIndicators(fromId, intoId, prev));
  }, []);

  const handleSplitIndicatorPane = useCallback((hostId: string) => {
    setIndicators(prev => splitIndicatorPane(hostId, prev));
  }, []);

  const [focusedIndicatorId, setFocusedIndicatorId] = useState<string | null>(null);

  const handleExpandIndicator = useCallback((id: string) => {
    setFocusedIndicatorId(id);
  }, []);

  const handleRestoreIndicators = useCallback(() => {
    setFocusedIndicatorId(null);
  }, []);

  const handleDuplicateIndicator = useCallback((sourceId: string) => {
    setIndicators(prev => duplicateIndicatorInList(sourceId, prev, newIndId) ?? prev);
  }, []);

  const handleRemoveByIdSlot = useCallback((id: string) => {
    setIndicators(prev => removeIndicatorFromList(prev, id));
    setFocusedIndicatorId(prev => prev === id ? null : prev);
    setCtxToolbar(null);
    setSettingsModalId(null);
  }, []);

  /** 단독 전체화면에서 보여줄 지표 목록 (overlay + 선택된 1개) + 타임프레임 필터 */
  const effectiveIndicators = useMemo(() => {
    // 타임프레임 필터: timeframeVisibility[currentTf] === false 이면 제외
    const tfFiltered = indicators.filter(ind =>
      ind.timeframeVisibility?.[timeframe] !== false
    );
    if (!focusedIndicatorId) return tfFiltered;
    return tfFiltered.filter(ind => {
      const def = getIndicatorDef(ind.type);
      return def?.overlay || def?.returnsMarkers || ind.id === focusedIndicatorId;
    });
  }, [focusedIndicatorId, indicators, timeframe]);

  // ── 심볼 선택 ─────────────────────────────────────────────────────────
  const handleSymbolSelect = useCallback((s: string) => {
    if (isUpbitMarket(s)) invalidateMarketCache(s);
    pendingSyncVisibleRef.current = null;
    pendingSyncLogicalRef.current = null;
    skipSyncApplyRef.current = true;
    setSymbol(s);
    setLegend(null);
    setShowMarket(false);
    setSlotAnchorRect(null);
  }, []);

  const openMarketSearch = useCallback((e?: React.MouseEvent) => {
    e?.stopPropagation();
    onActivate();
    if (!showMarket) {
      const anchorEl = compactMode
        ? compactNameRef.current
        : (slotContainerRef.current?.closest('.chart-slot') as HTMLElement | null);
      setSlotAnchorRect(anchorEl?.getBoundingClientRect() ?? null);
    } else {
      setSlotAnchorRect(null);
    }
    setShowMarket(v => !v);
  }, [compactMode, onActivate, showMarket]);

  const displayKoName = compactKoName(symbol);
  const trendUp = compactMode && liveQuote.price != null && liveQuote.isUp;
  const trendDown = compactMode && liveQuote.price != null && !liveQuote.isUp;

  return (
    <div
      className={[
        'chart-slot',
        active ? 'active' : '',
        compactMode ? 'chart-slot--compact' : '',
        highlightReceiving ? 'chart-slot--receiving' : '',
        trendUp ? 'chart-slot--trend-up' : '',
        trendDown ? 'chart-slot--trend-down' : '',
      ].filter(Boolean).join(' ')}
      onClick={onActivate}
    >
      {/* ── 슬롯 헤더 ──────────────────────────────────────────────────────── */}
      <div
        className={`slot-header${compactMode ? ' slot-header--compact' : ''}`}
        onClick={e => e.stopPropagation()}
      >
        {compactMode ? (
          <div className="slot-compact-row">
            <button
              type="button"
              ref={compactNameRef}
              className={`slot-compact-name${showMarket ? ' open' : ''}`}
              title={`${displayKoName} — 종목 변경`}
              aria-expanded={showMarket}
              aria-haspopup="listbox"
              onClick={openMarketSearch}
            >
              <svg width="11" height="11" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" style={{ flexShrink: 0 }} aria-hidden>
                <circle cx="6" cy="6" r="4"/><line x1="9.5" y1="9.5" x2="13" y2="13"/>
              </svg>
              <span className="slot-compact-name-text">{displayKoName}</span>
            </button>

            {liveQuote.price != null && (
              <div className="slot-compact-quote" aria-label="현재가">
                <span
                  className="slot-compact-price"
                  style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}
                >
                  {formatChartLivePrice(liveQuote.price).replace(' KRW', '')}
                </span>
                {liveQuote.changePct != null && (
                  <span
                    className="slot-compact-change"
                    style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}
                  >
                    {formatChartLiveChangePct(liveQuote.changePct)}
                  </span>
                )}
              </div>
            )}

            <label className="slot-compact-tf" onClick={e => e.stopPropagation()}>
              <span className="slot-compact-tf-label">시간봉</span>
              <select
                className="slot-compact-tf-select"
                value={timeframe}
                aria-label="시간봉 선택"
                onChange={e => setTimeframe(e.target.value as Timeframe)}
              >
                {TF_OPTIONS.map(tf => (
                  <option key={tf} value={tf}>{tf}</option>
                ))}
              </select>
            </label>

            <div className="slot-compact-status">
              {isLoading && <span className="slot-loading">로딩…</span>}
              {useUpbit && (
                <VirtualLiveBadge status={wsToLiveStatus(wsStatus)} receiving={receiving} />
              )}
            </div>
          </div>
        ) : (
          <>
            <button
              className={`slot-sym-btn${showMarket ? ' open' : ''}`}
              title="종목 변경"
              onClick={openMarketSearch}
            >
              <svg width="11" height="11" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" style={{ flexShrink: 0 }}>
                <circle cx="6" cy="6" r="4"/><line x1="9.5" y1="9.5" x2="13" y2="13"/>
              </svg>
              {getKoreanName(symbol) !== symbol
                ? <><span className="slot-sym-kr">{getKoreanName(symbol)}</span><span className="slot-sym-code">{symbol}</span></>
                : <span className="slot-sym-kr">{symbol}</span>
              }
            </button>
            <div className="slot-tf-row">
              {TF_OPTIONS.map(tf => (
                <button
                  key={tf}
                  className={`slot-tf-btn${tf === timeframe ? ' active' : ''}`}
                  onClick={() => setTimeframe(tf)}
                >
                  {tf}
                </button>
              ))}
            </div>
            {liveQuote.price != null && (
              <span className="slot-price" style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}>
                {formatChartLivePrice(liveQuote.price).replace(' KRW', '')}
                {liveQuote.changePct != null && (
                  <span className="slot-change" style={{ color: liveQuote.isUp ? 'var(--up)' : 'var(--down)' }}>
                    {formatChartLiveChangePct(liveQuote.changePct)}
                  </span>
                )}
              </span>
            )}
            {useUpbit && <WsBadge status={wsStatus} isUpbit={true} />}
            {isLoading && <span className="slot-loading">로딩...</span>}
          </>
        )}

        {showMarket && ReactDOM.createPortal(
          <MarketSearchPanel
            currentMarket={symbol}
            onSelect={s => { handleSymbolSelect(s); }}
            onClose={() => { setShowMarket(false); setSlotAnchorRect(null); }}
            anchorRect={slotAnchorRect}
            anchorPlacement={compactMode ? 'dropdown' : 'slot'}
          />,
          document.body,
        )}
      </div>

      {/* ── TradingChart ─────────────────────────────────────────────────── */}
      <div
        ref={slotContainerRef}
        className={compactMode ? 'vtd-card-chart-canvas slot-chart-wrap slot-chart-wrap--compact' : 'slot-chart-wrap'}
        style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
      >
        {useUpbit && isLoading && (
          compactMode ? (
            <div className="vtd-card-chart-loading">차트 로딩…</div>
          ) : (
            <div className="chart-loading" style={{ position: 'absolute', zIndex: 10 }}>
              <div className="loading-spinner" />
            </div>
          )
        )}
        {isLoadingMore && (
          <div className="chart-history-loading">
            <div className="loading-spinner" style={{ width: 14, height: 14 }} />
            <span>과거 데이터 로딩...</span>
          </div>
        )}

        <TradingChart
          key={reloadTick}
          chartVisible={chartVisible}
          bars={bars}
          barsMarket={barsMarket}
          market={symbol}
          timeframe={timeframe}
          displayTimezone={displayTimezone}
          chartType={chartType}
          theme={theme}
          mode="chart"
          indicators={effectiveIndicators}
          drawingTool="cursor"
          drawings={drawings}
          logScale={false}
          drawingsLocked={compactMode}
          drawingsVisible={!compactMode}
          showHoverToolbar={!compactMode}
          volumeVisible={chartVolumeVisible}
          candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels}
          indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels}
          paneSeparatorOptions={chartPaneSeparator}
          crosshairInfoVisible={chartCrosshairInfoVisible}
          onCrosshair={data => {
            setLegend(data);
            if (data && onCrosshairTime) onCrosshairTime(data.time ?? 0);
          }}
          onManagerReady={mgr => {
            managerRef.current = mgr;
            const pending = pendingRealtimeBarRef.current;
            if (pending && chartLiveReadyRef.current && barsMarketRef.current === symbolRef.current && bars.length > 0) {
              pendingRealtimeBarRef.current = null;
              const last = bars[bars.length - 1];
              if (last && pending.time === last.time) mgr.updateBar(pending);
              else if (!last || pending.time > last.time) void mgr.appendBar(pending);
              else mgr.updateBar({ ...pending, time: last.time });
            } else {
              pendingRealtimeBarRef.current = null;
            }
            mgr.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels);
            mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels);
            mgr.setVolumeVisible(chartVolumeVisible);
            if (chartPaneSeparator) mgr.setPaneSeparatorOptions(chartPaneSeparator);
            // Time sync: 가시 시간 범위 변화 구독
            mgr.subscribeVisibleTimeRange(r => {
              if (!r || isSyncingTimeRef.current) return;
              onVisibleRangeChangeRef.current?.(r);
            });
            // Date range sync: 가시 논리 범위 변화 구독
            // + 왼쪽 스크롤 감지 → 과거 데이터 추가 로드
            mgr.subscribeVisibleLogicalRange(r => {
              if (!r) return;
              if (!isSyncingLogicalRef.current) {
                onLogicalRangeChangeRef.current?.(r);
              }
              // 가시 범위 왼쪽 끝이 LOAD_MORE_TRIGGER 이내이면 추가 로드
              if (r.from < LOAD_MORE_TRIGGER) {
                loadMoreRef.current();
              }
            });
          }}
          onCandlesReady={handleCandlesReady}
          onDataLoaded={() => {
            // reloadAll(setData + setInitialVisibleRange) 완료 후
            // 첫 마운트 시 pending 상태였던 sync range 를 재적용한다
            const mgr = managerRef.current;
            if (!mgr) return;
            if (skipSyncApplyRef.current) {
              skipSyncApplyRef.current = false;
              const fb = mgr.hasIchimoku() ? 28 : 0;
              mgr.setInitialVisibleRange(DISPLAY_COUNT, fb);
              return;
            }
            if (pendingSyncVisibleRef.current) {
              isSyncingTimeRef.current = true;
              mgr.applyVisibleTimeRange(
                pendingSyncVisibleRef.current.from,
                pendingSyncVisibleRef.current.to,
              );
              requestAnimationFrame(() => { isSyncingTimeRef.current = false; });
              pendingSyncVisibleRef.current = null;
            }
            if (pendingSyncLogicalRef.current) {
              isSyncingLogicalRef.current = true;
              mgr.applyVisibleLogicalRange(
                pendingSyncLogicalRef.current.from,
                pendingSyncLogicalRef.current.to,
              );
              requestAnimationFrame(() => { isSyncingLogicalRef.current = false; });
              pendingSyncLogicalRef.current = null;
            }
          }}
          magnifierEnabled={magnifierEnabled}
          onMagnifierClose={onMagnifierClose}
          onAddDrawing={() => {}}
          onSeriesClick={(entryId, point) => {
            if (!entryId) { setCtxToolbar(null); return; }
            setCtxToolbar({ entryId, screenX: point.x, screenY: point.y });
          }}
          onSeriesDoubleClick={entryId => {
            setCtxToolbar(null);
            if (!entryId) return;
            if (entryId === '__main__') setShowMainChartModal(true);
            else setSettingsModalId(entryId);
          }}
          onHoverPaneLegend={() => {}}
          onLeavePaneLegend={() => {}}
          onTradeOrderRequest={req => onTradeOrderRequest?.(req, slotIndex)}
          onReorderIndicators={handleReorderIndicators}
          onMergeIndicators={handleMergeIndicators}
          onSplitIndicatorPane={handleSplitIndicatorPane}
          onExpandIndicator={handleExpandIndicator}
          onRemoveIndicator={handleRemoveByIdSlot}
          onDuplicateIndicator={handleDuplicateIndicator}
          focusedIndicatorId={focusedIndicatorId}
          onRestoreIndicators={handleRestoreIndicators}
          onToggleIndicatorHidden={handleToggleHidden}
          onOpenIndicatorSettings={id => setSettingsModalId(id)}
          onFullView={onFullView ? () => onFullView(symbolRef.current) : undefined}
        />

        {/* 컨텍스트 툴바 */}
        {ctxToolbar && (() => {
          const cfg = ctxToolbar.entryId !== '__main__'
            ? indicators.find(i => i.id === ctxToolbar.entryId) ?? null
            : null;
          return (
            <IndicatorContextToolbar
              indicatorId={ctxToolbar.entryId}
              config={cfg}
              chartType={chartType}
              screenX={ctxToolbar.screenX}
              screenY={ctxToolbar.screenY}
              onMouseEnterToolbar={() => {}}
              onMouseLeaveToolbar={() => {}}
              onSettings={() => {
                const id = ctxToolbar.entryId;
                setCtxToolbar(null);
                if (id === '__main__') setShowMainChartModal(true);
                else setSettingsModalId(id);
              }}
              onToggleHidden={() => handleToggleHidden(ctxToolbar.entryId)}
              onRemove={() => handleRemoveById(ctxToolbar.entryId)}
              onClose={() => setCtxToolbar(null)}
            />
          );
        })()}

        {/* 메인 차트 설정 모달 */}
        {showMainChartModal && (
          <MainChartSettingsModal
            style={mainChartStyle}
            onSave={style => { setMainChartStyle(style); setShowMainChartModal(false); }}
            onCancel={() => setShowMainChartModal(false)}
          />
        )}

        {/* 지표 설정 모달 */}
        {settingsModalId && settingsModalId !== '__main__' && (() => {
          const cfg = indicators.find(i => i.id === settingsModalId);
          if (!cfg) return null;
          const def = getIndicatorDef(cfg.type);
          const mergedCfg = enrichIndicatorConfig({
            ...cfg,
            params: getParams(cfg.type, def?.defaultParams),
            hlines: cfg.hlines ?? def?.hlines,
          });
          return (
            <IndicatorSettingsModal
              config={mergedCfg}
              chartMarket={symbol}
              onSave={handleIndicatorSave}
              onCancel={() => setSettingsModalId(null)}
            />
          );
        })()}
      </div>
    </div>
  );
});

export default ChartSlot;
