import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import Draggable from 'react-draggable';
import {
  Box,
  Chip,
  CircularProgress,
  IconButton,
  Paper,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Typography,
  useTheme,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
import {
  evaluateAlertStrategy,
  getAllAlerts,
  getGlobalConditions,
  type CryptoAlertDto,
} from '../../services/alertApi';
import { getEnabledAlertStrategies } from '../../services/alertStrategyApi';
import { getStrategyRule } from '../../services/strategyRuleApi';
import {
  MultiChartMarketDataProvider,
  useMultiChartMarketData,
  type CandleUpdate,
  type ConnectionStatus,
} from '../../contexts/MultiChartMarketDataContext';
import { chartIntervalToWsTimeframe } from '../../utils/investmentChart/wsCandleHelpers';
import {
  BASE_PRICE_COLUMNS,
  buildFlatCompareMap,
  buildFlatDisplayMap,
  buildStrategyMetricColumns,
  collectIndicatorTypesFromAlertsAndGlobal,
  collectIndicatorTypesFromStrategyRule,
  type StrategyMetricColumn,
} from '../../utils/alert/alertRealtimeStrategyColumns';

const FLASH_LIME = '#CCFF00';
const FLASH_MS = 650;
const DEFAULT_PANEL_W = 960;
const DEFAULT_PANEL_H = 520;
const MIN_PANEL_W = 520;
const MIN_PANEL_H = 280;
const MAX_INSET = 8;
const EVAL_DEBOUNCE_MS = 550;
const MIN_LIVE_CANDLES_FOR_EVAL = 60;

function msFromCandleTimestamp(ts: number): number {
  if (!Number.isFinite(ts)) return Date.now();
  return ts > 1e12 ? ts : ts * 1000;
}

/** 백엔드 `UpbitCandleDto.candleDateTimeKst` 파싱 형식과 동일 */
function formatKstYmdHms(ms: number): string {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone: 'Asia/Seoul',
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  }).formatToParts(new Date(ms));
  const get = (type: Intl.DateTimeFormatPartTypes) => parts.find((p) => p.type === type)?.value ?? '00';
  return `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}:${get('second')}`;
}

function candleUpdatesToLiveUpbitMaps(market: string, candles: CandleUpdate[]): Record<string, unknown>[] {
  return candles.map((c) => {
    const ms = msFromCandleTimestamp(c.timestamp);
    const kst = formatKstYmdHms(ms);
    return {
      market,
      candle_date_time_kst: kst,
      candle_date_time_utc: new Date(ms).toISOString().slice(0, 19),
      opening_price: c.open,
      high_price: c.high,
      low_price: c.low,
      trade_price: c.close,
      timestamp: ms,
      candle_acc_trade_price: null,
      candle_acc_trade_volume: c.volume,
    };
  });
}

function clamp(x: number, y: number, w: number, h: number) {
  if (typeof window === 'undefined') return { x, y };
  const m = 8;
  const maxX = Math.max(m, window.innerWidth - w - m);
  const maxY = Math.max(m, window.innerHeight - h - m);
  return { x: Math.min(Math.max(m, x), maxX), y: Math.min(Math.max(m, y), maxY) };
}

function centerPanel(w: number, h: number) {
  if (typeof window === 'undefined') return { x: 24, y: 80 };
  const m = 8;
  return {
    x: Math.max(m, (window.innerWidth - w) / 2),
    y: Math.max(m, (window.innerHeight - h) / 2),
  };
}

function statusColor(s: ConnectionStatus): 'default' | 'primary' | 'success' | 'error' | 'warning' {
  if (s === 'connected') return 'success';
  if (s === 'connecting') return 'warning';
  if (s === 'error') return 'error';
  return 'default';
}

function statusLabel(s: ConnectionStatus): string {
  if (s === 'connected') return '연결';
  if (s === 'connecting') return '연결중';
  if (s === 'error') return '오류';
  return '끊김';
}

interface StrategyGridRowProps {
  alert: CryptoAlertDto;
  timeframe: string;
  columns: StrategyMetricColumn[];
  /** null이면 전략 평가·행 하이라이트 없음 */
  strategyRuleId: number | null;
  /** 스케줄·평가 API용 봉 간격 (예: 3m) */
  scheduleInterval: string;
}

const StrategyGridRow: React.FC<StrategyGridRowProps> = ({
  alert,
  timeframe,
  columns,
  strategyRuleId,
  scheduleInterval,
}) => {
  const multi = useMultiChartMarketData();
  const [candle, setCandle] = useState<CandleUpdate | null>(null);
  const [rawInd, setRawInd] = useState<Record<string, number>>({});
  const conn: ConnectionStatus = useMemo(() => {
    if (!multi) return 'disconnected';
    const item = multi.connectionList.find((c) => c.market === alert.symbol && c.timeframe === timeframe);
    return item?.status ?? 'disconnected';
  }, [multi?.connectionList, alert.symbol, timeframe]);
  const prevFlatRef = useRef<Record<string, string>>({});
  const [flash, setFlash] = useState<Set<string>>(() => new Set());
  const [conditionMet, setConditionMet] = useState(false);
  const candlesRef = useRef<CandleUpdate[]>([]);
  const evalTimerRef = useRef<number | null>(null);
  const evalSeqRef = useRef(0);
  const flashTimeoutRef = useRef<number | null>(null);
  const connRef = useRef(conn);
  connRef.current = conn;

  const runEvaluate = useCallback(async () => {
    if (strategyRuleId == null) {
      setConditionMet(false);
      return;
    }
    const buf = candlesRef.current;
    if (buf.length < MIN_LIVE_CANDLES_FOR_EVAL) {
      setConditionMet(false);
      return;
    }
    const seq = ++evalSeqRef.current;
    try {
      const liveCandles = candleUpdatesToLiveUpbitMaps(alert.symbol, buf);
      const res = await evaluateAlertStrategy({
        symbol: alert.symbol,
        strategyRuleId,
        timeInterval: scheduleInterval,
        liveCandles,
        useLiveCandleTail: true,
      });
      if (seq === evalSeqRef.current) setConditionMet(!!res.conditionMet);
    } catch {
      if (seq === evalSeqRef.current) setConditionMet(false);
    }
  }, [alert.symbol, scheduleInterval, strategyRuleId]);

  useEffect(() => {
    if (conn !== 'connected') {
      if (flashTimeoutRef.current != null) {
        window.clearTimeout(flashTimeoutRef.current);
        flashTimeoutRef.current = null;
      }
      setFlash(new Set());
    }
  }, [conn]);

  useEffect(() => {
    if (!multi) return undefined;
    const unsub = multi.subscribe(alert.symbol, timeframe, (data) => {
      const last = data.candles?.[data.candles.length - 1];
      if (!last) return;
      candlesRef.current = data.candles || [];
      setCandle(last);
      setRawInd(data.indicators || {});

      if (strategyRuleId != null && (data.candles?.length ?? 0) >= MIN_LIVE_CANDLES_FOR_EVAL) {
        if (evalTimerRef.current != null) window.clearTimeout(evalTimerRef.current);
        evalTimerRef.current = window.setTimeout(() => {
          evalTimerRef.current = null;
          void runEvaluate();
        }, EVAL_DEBOUNCE_MS);
      } else if (strategyRuleId == null) {
        setConditionMet(false);
      } else {
        setConditionMet(false);
      }

      if (!data.isUpdate) {
        prevFlatRef.current = buildFlatCompareMap(columns, data.indicators || {}, last);
        return;
      }

      const cur = buildFlatCompareMap(columns, data.indicators || {}, last);
      const prev = prevFlatRef.current;
      const changed = new Set<string>();
      for (const k of Object.keys(cur)) {
        if (prev[k] !== undefined && prev[k] !== cur[k]) changed.add(k);
      }
      prevFlatRef.current = { ...cur };
      if (changed.size === 0) return;
      if (connRef.current !== 'connected') return;
      setFlash((s) => {
        const n = new Set(s);
        changed.forEach((k) => n.add(k));
        return n;
      });
      if (flashTimeoutRef.current != null) window.clearTimeout(flashTimeoutRef.current);
      flashTimeoutRef.current = window.setTimeout(() => {
        flashTimeoutRef.current = null;
        setFlash((s) => {
          const n = new Set(s);
          changed.forEach((k) => n.delete(k));
          return n;
        });
      }, FLASH_MS);
    });
    return () => {
      unsub();
      if (flashTimeoutRef.current != null) {
        window.clearTimeout(flashTimeoutRef.current);
        flashTimeoutRef.current = null;
      }
      if (evalTimerRef.current != null) {
        window.clearTimeout(evalTimerRef.current);
        evalTimerRef.current = null;
      }
    };
  }, [multi, alert.symbol, timeframe, columns, strategyRuleId, runEvaluate]);

  const displayMap = useMemo(() => {
    if (!candle) return {} as Record<string, string>;
    return buildFlatDisplayMap(columns, rawInd, candle);
  }, [columns, candle, rawInd]);

  const flashSx = (id: string) =>
    conn === 'connected' && flash.has(id)
      ? {
          color: FLASH_LIME,
          fontWeight: 700,
          textShadow: '0 0 8px rgba(204,255,0,0.45)',
          fontFamily: 'monospace',
          whiteSpace: 'nowrap' as const,
        }
      : { fontFamily: 'monospace', whiteSpace: 'nowrap' as const };

  const rowHighlightSx =
    strategyRuleId != null && conditionMet
      ? {
          bgcolor: 'rgba(204,255,0,0.07)',
          boxShadow: `inset 0 0 0 1.5px ${FLASH_LIME}, 0 0 20px rgba(204,255,0,0.35)`,
        }
      : undefined;

  return (
    <TableRow hover sx={rowHighlightSx}>
      <TableCell sx={{ fontWeight: 600 }}>{alert.symbol}</TableCell>
      <TableCell>{alert.koreanName || '—'}</TableCell>
      <TableCell>
        <Chip size="small" label={statusLabel(conn)} color={statusColor(conn)} variant="outlined" />
      </TableCell>
      {columns.map((col) => (
        <TableCell key={col.id} align="right" sx={{ ...flashSx(col.id), fontSize: '0.75rem' }}>
          {displayMap[col.id] ?? '—'}
        </TableCell>
      ))}
    </TableRow>
  );
};

interface InnerGridProps {
  alerts: CryptoAlertDto[];
  timeframe: string;
  metricColumns: StrategyMetricColumn[];
  /** 동일 심볼이 여러 전략 블록에 있을 때 React key 구분 */
  rowKeyPrefix: string;
  strategyRuleId: number | null;
  scheduleInterval: string;
}

const InnerStrategyGrid: React.FC<InnerGridProps> = ({
  alerts,
  timeframe,
  metricColumns,
  rowKeyPrefix,
  strategyRuleId,
  scheduleInterval,
}) => {
  const allColumns = useMemo(() => [...BASE_PRICE_COLUMNS, ...metricColumns], [metricColumns]);

  if (alerts.length === 0) {
    return (
      <Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
        활성화된 알림 종목이 없습니다. 알림 대상 종목에서 종목을 켜 주세요.
      </Typography>
    );
  }

  return (
    <TableContainer sx={{ flex: 1, minHeight: 0, maxHeight: '100%' }}>
      <Table size="small" stickyHeader>
        <TableHead>
          <TableRow>
            <TableCell sx={{ fontWeight: 700 }}>심볼</TableCell>
            <TableCell sx={{ fontWeight: 700 }}>종목명</TableCell>
            <TableCell sx={{ fontWeight: 700 }}>WS</TableCell>
            {allColumns.map((c) => (
              <TableCell key={c.id} align="right" sx={{ fontWeight: 700, fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
                {c.label}
              </TableCell>
            ))}
          </TableRow>
        </TableHead>
        <TableBody>
          {alerts.map((a) => (
            <StrategyGridRow
              key={`${rowKeyPrefix}:${a.symbol}`}
              alert={a}
              timeframe={timeframe}
              columns={allColumns}
              strategyRuleId={strategyRuleId}
              scheduleInterval={scheduleInterval}
            />
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
};

export type AlertStrategySection = {
  key: string;
  title: string;
  metricColumns: StrategyMetricColumn[];
  /** 공통 블록(전략 없음)은 null */
  strategyRuleId: number | null;
};

export interface AlertRealtimeStrategyPopupProps {
  anchorEl: HTMLElement | null;
  onClose: () => void;
  /** 스케줄 설정의 평가 시간봉 (예: 5m, 1h) — 멀티차트 WS와 동일 */
  timeInterval: string;
  /** 스케줄러 마지막 전략 체크 실행 시각 (ISO 문자열·Jackson 배열 등) */
  lastStrategyRunTime?: unknown;
  /** 스케줄러 다음 전략 체크 예정 시각 */
  nextStrategyRunTime?: unknown;
}

/** 백엔드 LocalDateTime JSON (ISO 또는 [y,m,d,h,mi,s,nano]) 파싱 */
function parseScheduleDateTime(v: unknown): Date | null {
  if (v == null || v === '') return null;
  if (typeof v === 'number' && Number.isFinite(v)) {
    return new Date(v > 1e12 ? v : v * 1000);
  }
  if (typeof v === 'string') {
    const d = new Date(v);
    return Number.isFinite(d.getTime()) ? d : null;
  }
  if (Array.isArray(v) && v.length >= 3) {
    const y = Number(v[0]);
    const mo = Number(v[1]);
    const day = Number(v[2]);
    const h = v.length > 3 ? Number(v[3]) : 0;
    const mi = v.length > 4 ? Number(v[4]) : 0;
    const s = v.length > 5 ? Number(v[5]) : 0;
    const nano = v.length > 6 ? Number(v[6]) : 0;
    const ms = Number.isFinite(nano) ? Math.floor(nano / 1e6) : 0;
    if (![y, mo, day].every((x) => Number.isFinite(x))) return null;
    return new Date(
      y,
      (Number.isFinite(mo) ? mo : 1) - 1,
      Number.isFinite(day) ? day : 1,
      Number.isFinite(h) ? h : 0,
      Number.isFinite(mi) ? mi : 0,
      Number.isFinite(s) ? s : 0,
      ms,
    );
  }
  return null;
}

function formatScheduleInstant(v: unknown): string {
  const d = parseScheduleDateTime(v);
  return d && Number.isFinite(d.getTime()) ? d.toLocaleString('ko-KR') : '—';
}

const AlertRealtimeStrategyPopupShell: React.FC<{
  children: React.ReactNode;
  anchorEl: HTMLElement;
  onClose: () => void;
  isDark: boolean;
  lastStrategyRunTime?: unknown;
  nextStrategyRunTime?: unknown;
}> = ({ children, anchorEl, onClose, isDark, lastStrategyRunTime, nextStrategyRunTime }) => {
  const nodeRef = useRef<HTMLDivElement>(null);
  const [dragPos, setDragPos] = useState({ x: 24, y: 80 });
  const [panelW, setPanelW] = useState(DEFAULT_PANEL_W);
  const [panelH, setPanelH] = useState(DEFAULT_PANEL_H);
  const [isMaximized, setIsMaximized] = useState(false);
  const preMaxRef = useRef<{ x: number; y: number; w: number; h: number } | null>(null);
  const resizeListenersRef = useRef<{ move: (e: MouseEvent) => void; up: () => void } | null>(null);

  useLayoutEffect(() => {
    setDragPos(centerPanel(panelW, panelH));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [anchorEl]);

  const onDrag = useCallback(
    (_: unknown, data: { x: number; y: number }) => {
      setDragPos(clamp(data.x, data.y, panelW, panelH));
    },
    [panelW, panelH],
  );

  const toggleMaximize = useCallback(() => {
    if (typeof window === 'undefined') return;
    if (!isMaximized) {
      preMaxRef.current = { x: dragPos.x, y: dragPos.y, w: panelW, h: panelH };
      setIsMaximized(true);
      return;
    }
    const saved = preMaxRef.current;
    setIsMaximized(false);
    if (saved) {
      setPanelW(saved.w);
      setPanelH(saved.h);
      setDragPos(clamp(saved.x, saved.y, saved.w, saved.h));
    }
    preMaxRef.current = null;
  }, [isMaximized, dragPos.x, dragPos.y, panelW, panelH]);

  const onResizeStart = useCallback(
    (e: React.MouseEvent) => {
      if (isMaximized) return;
      e.preventDefault();
      e.stopPropagation();
      const sx = e.clientX;
      const sy = e.clientY;
      const sw = panelW;
      const sh = panelH;
      const onMove = (ev: MouseEvent) => {
        if (typeof window === 'undefined') return;
        const maxW = window.innerWidth - MAX_INSET * 2;
        const maxH = window.innerHeight - MAX_INSET * 2;
        const nw = Math.min(maxW, Math.max(MIN_PANEL_W, sw + (ev.clientX - sx)));
        const nh = Math.min(maxH, Math.max(MIN_PANEL_H, sh + (ev.clientY - sy)));
        setPanelW(nw);
        setPanelH(nh);
        setDragPos((p) => clamp(p.x, p.y, nw, nh));
      };
      const onUp = () => {
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
        resizeListenersRef.current = null;
      };
      resizeListenersRef.current = { move: onMove, up: onUp };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
    },
    [isMaximized, panelW, panelH],
  );

  useEffect(() => {
    return () => {
      const h = resizeListenersRef.current;
      if (h) {
        window.removeEventListener('mousemove', h.move);
        window.removeEventListener('mouseup', h.up);
        resizeListenersRef.current = null;
      }
    };
  }, []);

  const shell = (
    <Box
      ref={nodeRef}
      sx={{
        position: 'fixed',
        left: isMaximized ? MAX_INSET : 0,
        top: isMaximized ? MAX_INSET : 0,
        zIndex: (t) => t.zIndex.modal + 30,
        width: isMaximized ? `calc(100vw - ${MAX_INSET * 2}px)` : panelW,
        height: isMaximized ? `calc(100vh - ${MAX_INSET * 2}px)` : panelH,
        pointerEvents: 'auto',
        boxSizing: 'border-box',
      }}
    >
      <Paper
        elevation={0}
        sx={{
          width: '100%',
          height: '100%',
          overflow: 'hidden',
          display: 'flex',
          flexDirection: 'column',
          position: 'relative',
          borderRadius: '12px',
          border: '1px solid rgba(63, 126, 245, 0.35)',
          bgcolor: '#1e222d',
          boxShadow: '0 0 0 1px rgba(63,126,245,0.08), 0 20px 60px rgba(0,0,0,0.65), 0 0 40px rgba(63,126,245,0.06)',
        }}
      >
        <Box
          sx={{
            display: 'flex',
            alignItems: 'center',
            gap: 1,
            px: 1.75,
            py: 1.25,
            flexShrink: 0,
            borderBottom: '1px solid rgba(122,162,247,0.12)',
            background: 'linear-gradient(180deg, rgba(63,126,245,0.14) 0%, rgba(63,126,245,0.04) 28px, #1f2335 100%)',
          }}
        >
          <Box
            className="rt-strategy-drag-handle"
            sx={{
              display: 'flex',
              alignItems: 'center',
              gap: 1,
              flex: 1,
              minWidth: 0,
              cursor: isMaximized ? 'default' : 'grab',
              userSelect: 'none',
              '&:active': { cursor: isMaximized ? 'default' : 'grabbing' },
            }}
          >
            <Box
              component="span"
              sx={{
                px: 1.25,
                py: 0.35,
                borderRadius: '20px',
                fontSize: '11px',
                fontWeight: 800,
                color: '#fff',
                letterSpacing: '1.2px',
                bgcolor: '#3f7ef5',
                flexShrink: 0,
              }}
            >
              LIVE
            </Box>
            <Typography variant="subtitle2" sx={{ fontWeight: 700, flexShrink: 0, color: '#c0caf5', fontSize: '14px' }}>
              STRATEGY DATA
              <Typography component="span" sx={{ fontSize: '12px', fontWeight: 400, color: '#9aa5ce', ml: 0.5 }}>
                (실시간 전략 데이터)
              </Typography>
            </Typography>
            <Typography
              variant="caption"
              color="text.secondary"
              component="span"
              title={`마지막 전략체크: ${formatScheduleInstant(lastStrategyRunTime)} · 다음 실행: ${formatScheduleInstant(nextStrategyRunTime)}`}
              sx={{
                ml: 'auto',
                pl: 1,
                pr: 0.5,
                textAlign: 'right',
                fontSize: { xs: '0.65rem', sm: '0.72rem' },
                lineHeight: 1.35,
                minWidth: 0,
                maxWidth: { xs: 'min(62vw, 340px)', sm: 'min(58vw, 520px)' },
                whiteSpace: 'nowrap',
                overflow: 'hidden',
                textOverflow: 'ellipsis',
              }}
            >
              마지막 전략체크: {formatScheduleInstant(lastStrategyRunTime)} · 다음 실행:{' '}
              {formatScheduleInstant(nextStrategyRunTime)}
            </Typography>
          </Box>
          <IconButton
            className="rt-strategy-no-drag"
            size="small"
            onClick={toggleMaximize}
            aria-label={isMaximized ? '이전 크기로' : '최대화'}
            sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}
          >
            {isMaximized ? <FullscreenExitIcon fontSize="small" /> : <FullscreenIcon fontSize="small" />}
          </IconButton>
          <IconButton
            className="rt-strategy-no-drag"
            size="small"
            onClick={onClose}
            aria-label="닫기"
            sx={{ color: '#6272a4', '&:hover': { color: '#c0caf5', bgcolor: 'rgba(255,255,255,0.07)' } }}
          >
            <CloseIcon fontSize="small" />
          </IconButton>
        </Box>
        <Box sx={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>{children}</Box>
        {!isMaximized && (
          <Box
            className="rt-strategy-no-drag"
            role="presentation"
            aria-hidden
            onMouseDown={onResizeStart}
            sx={{
              position: 'absolute',
              right: 0,
              bottom: 0,
              width: 22,
              height: 22,
              cursor: 'nwse-resize',
              zIndex: 2,
              background: isDark
                ? 'linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.12) 50%)'
                : 'linear-gradient(135deg, transparent 50%, rgba(0,0,0,0.08) 50%)',
            }}
          />
        )}
      </Paper>
    </Box>
  );

  if (isMaximized) {
    return createPortal(shell, document.body);
  }

  return createPortal(
    <Draggable
      nodeRef={nodeRef}
      handle=".rt-strategy-drag-handle"
      position={dragPos}
      onDrag={onDrag}
      bounds="body"
      cancel=".rt-strategy-no-drag"
    >
      {shell}
    </Draggable>,
    document.body,
  );
};

const AlertRealtimeStrategyPopup: React.FC<AlertRealtimeStrategyPopupProps> = ({
  anchorEl,
  onClose,
  timeInterval,
  lastStrategyRunTime,
  nextStrategyRunTime,
}) => {
  const theme = useTheme();
  const isDark = theme.palette.mode === 'dark';
  const wsTf = useMemo(() => chartIntervalToWsTimeframe(timeInterval || '1h'), [timeInterval]);
  const scheduleInterval = wsTf;

  const [loading, setLoading] = useState(true);
  const [loadError, setLoadError] = useState<string | null>(null);
  const [enabledAlerts, setEnabledAlerts] = useState<CryptoAlertDto[]>([]);
  const [strategySections, setStrategySections] = useState<AlertStrategySection[]>([]);

  useEffect(() => {
    if (!anchorEl) return undefined;
    let cancelled = false;
    (async () => {
      setLoading(true);
      setLoadError(null);
      try {
        const [alertsAll, globals, strats] = await Promise.all([
          getAllAlerts(),
          getGlobalConditions(),
          getEnabledAlertStrategies().catch(() => []),
        ]);
        if (cancelled) return;
        const enabled = alertsAll.filter((a) => a.enabled);
        setEnabledAlerts(enabled);

        const baseTypes = collectIndicatorTypesFromAlertsAndGlobal(enabled, globals || []);
        const sections: AlertStrategySection[] = [];

        if (!strats.length) {
          sections.push({
            key: 'no-strategy',
            title: '공통 (활성 알림 전략 없음)',
            metricColumns: buildStrategyMetricColumns(new Set(Array.from(baseTypes))),
            strategyRuleId: null,
          });
        } else {
          for (const st of strats) {
            if (cancelled) return;
            const merged = new Set<string>(Array.from(baseTypes));
            const rid = st.strategyRuleId;
            if (rid != null) {
              try {
                const rule = await getStrategyRule(rid);
                if (cancelled) return;
                collectIndicatorTypesFromStrategyRule(rule).forEach((t) => merged.add(t));
              } catch {
                /* 전략 상세 없으면 공통 지표만 */
              }
            }
            const title = st.strategyRuleName?.trim() || `전략 #${rid ?? st.id ?? ''}`;
            const key = st.id != null ? `as-${st.id}` : `sr-${rid}-${sections.length}`;
            sections.push({
              key,
              title,
              metricColumns: buildStrategyMetricColumns(merged),
              strategyRuleId: rid ?? null,
            });
          }
        }

        if (cancelled) return;
        setStrategySections(sections);
      } catch (e: any) {
        if (!cancelled) setLoadError(e?.message || '데이터를 불러오지 못했습니다.');
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [anchorEl]);

  const subscriptions = useMemo(
    () => enabledAlerts.map((a) => ({ market: a.symbol, timeframe: wsTf, koreanName: a.koreanName })),
    [enabledAlerts, wsTf],
  );

  if (!anchorEl || typeof document === 'undefined' || !document.body) {
    return null;
  }

  const body = (
    <AlertRealtimeStrategyPopupShell
      anchorEl={anchorEl}
      onClose={onClose}
      isDark={isDark}
      lastStrategyRunTime={lastStrategyRunTime}
      nextStrategyRunTime={nextStrategyRunTime}
    >
      <Box sx={{ px: 1.5, pt: 1, flexShrink: 0 }}>
        <Typography variant="caption" color="text.secondary">
          평가 시간봉: <strong>{timeInterval || '1h'}</strong> (WS: {wsTf}) · 스케줄러가 사용하는 봉과 동일한 스트림으로 갱신됩니다.
        </Typography>
      </Box>
      {loading ? (
        <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
          <CircularProgress size={32} />
        </Box>
      ) : loadError ? (
        <Typography color="error" sx={{ p: 2 }}>
          {loadError}
        </Typography>
      ) : enabledAlerts.length === 0 ? (
        <Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
          활성화된 알림 종목이 없습니다. 알림 대상 종목에서 종목을 켜 주세요.
        </Typography>
      ) : (
        <MultiChartMarketDataProvider subscriptions={subscriptions}>
          <Box
            sx={{
              flex: 1,
              minHeight: 0,
              overflow: 'auto',
              display: 'flex',
              flexDirection: 'column',
              gap: 2,
              py: 1,
              px: 0.5,
            }}
          >
            {strategySections.map((sec) => (
              <Paper
                key={sec.key}
                variant="outlined"
                sx={{
                  display: 'flex',
                  flexDirection: 'column',
                  minHeight: 0,
                  flex: strategySections.length > 1 ? '1 1 auto' : '1 1 0',
                  overflow: 'hidden',
                  borderColor: 'divider',
                }}
              >
                <Box
                  sx={{
                    px: 1.25,
                    py: 0.75,
                    borderBottom: 1,
                    borderColor: 'divider',
                    bgcolor: (t) => (t.palette.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)'),
                  }}
                >
                  <Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
                    {strategySections.length > 1 ? `전략 그룹: ${sec.title}` : `전략: ${sec.title}`}
                  </Typography>
                </Box>
                <Box sx={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
                  <InnerStrategyGrid
                    alerts={enabledAlerts}
                    timeframe={wsTf}
                    metricColumns={sec.metricColumns}
                    rowKeyPrefix={sec.key}
                    strategyRuleId={sec.strategyRuleId}
                    scheduleInterval={scheduleInterval}
                  />
                </Box>
              </Paper>
            ))}
          </Box>
        </MultiChartMarketDataProvider>
      )}
    </AlertRealtimeStrategyPopupShell>
  );

  return body;
};

export default AlertRealtimeStrategyPopup;
