/**
 * 매매 시그널 알림 목록 화면 — 좌: 알림 목록, 우: 매매·호가 (가상매매 우측 패널과 동일)
 */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { Theme, TradeOrderFillRequest } from '../types';
import type { TickerData } from '../hooks/useMarketTicker';
import { useTradeNotification, type TradeNotificationItem } from '../contexts/TradeNotificationContext';
import {
  loadBacktestSettings,
  loadPaperSummary,
  runBacktest,
  type PaperSummaryDto,
  type StrategyDto,
} from '../utils/backendApi';
import { prefetchNotificationStrategies, resolveNotificationStrategy } from '../utils/resolveNotificationStrategy';
import { normalizeMarketCode } from '../utils/strategyHydrate';
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
import { candleTypeToTimeframe } from '../utils/strategyToChartIndicators';
import { normalizeEpochSec } from '../utils/backtestUiUtils';
import { useIndicatorSettings } from '../hooks/useIndicatorSettings';
import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal';
import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal';
import { coerceFiniteNumber } from '../utils/safeFormat';
import {
  TradeSplitOrderPanel,
  TradeOrderbookPanelContent,
  TradeRightPanelTabs,
  TradeRightPanelBody,
} from './trade';
import { useUpbitOrderbook } from '../hooks/useUpbitOrderbook';
import { useUpbitRecentTrades } from '../hooks/useUpbitRecentTrades';
import { useAppSettings } from '../hooks/useAppSettings';
import { readStoredBool, storeBool } from './strategyEditor/usePanelResize';
import '../styles/strategyEditorTheme.css';
import '../styles/virtualTradingDashboard.css';
import '../styles/builderPageShell.css';
import '../styles/paperDashboard.css';
import '../styles/tradeRightPanel.css';
import '../styles/tradeNotificationList.css';
import TradeNotificationListRow from './tradeNotification/TradeNotificationListRow';
import { VirtualGridScroll, VirtualScroll } from './common/VirtualScroll';
import TradeNotificationGridLayoutPicker from './tradeNotification/TradeNotificationGridLayoutPicker';
import FullscreenChartModal from './tradeNotification/FullscreenChartModal';
import {
  loadTradeNotificationGridPreset,
  loadTradeNotificationListLayout,
  loadTradeNotificationCandleFilter,
  loadTradeNotificationListSort,
  saveTradeNotificationCandleFilter,
  saveTradeNotificationGridPreset,
  saveTradeNotificationListLayout,
  saveTradeNotificationListSort,
  TRADE_NOTIFICATION_CANDLE_FILTER_OPTIONS,
  TRADE_NOTIFICATION_READ_FILTER_OPTIONS,
  TRADE_NOTIFICATION_SORT_OPTIONS,
  type TradeNotificationCandleFilter,
  type TradeNotificationListLayout,
  type TradeNotificationListSort,
  type TradeNotificationReadFilter,
} from '../utils/tradeNotificationListLayout';
import { normalizeStartCandleType } from '../utils/strategyStartNodes';
import {
  getTradeNotificationGridPreset,
  type TradeNotificationGridPresetId,
} from '../utils/tradeNotificationGridPresets';
import { sortTradeNotifications } from '../utils/tradeListSort';
import { getKoreanName } from '../utils/marketNameCache';

type RightTab = 'trade' | 'orderbook';

const TNL_RIGHT_OPEN_KEY = 'tnl-right-open';
const REPORT_INDICATOR_TYPES = [
  'RSI', 'MACD', 'CCI', 'SMA', 'EMA', 'IchimokuCloud', 'Stochastic', 'ADX', 'MFI', 'NewPsychological',
] as const;

const IcTrash = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
    <polyline points="3 6 5 6 21 6" />
    <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
    <line x1="10" y1="11" x2="10" y2="17" />
    <line x1="14" y1="11" x2="14" y2="17" />
  </svg>
);

const IcRefresh = () => (
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
    <polyline points="23 4 23 10 17 10" />
    <polyline points="1 20 1 14 7 14" />
    <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
  </svg>
);

interface Props {
  theme: Theme;
  onGoToChart: (market: string) => void;
  tickers?: Map<string, TickerData>;
  defaultMarket?: string;
  paperTradingEnabled?: boolean;
  paperAutoTradeEnabled?: boolean;
  onPaperOrderFilled?: () => void;
}

function fillBothSides(
  market: string,
  price: number,
  setFillBuy: React.Dispatch<React.SetStateAction<TradeOrderFillRequest | null>>,
  setFillSell: React.Dispatch<React.SetStateAction<TradeOrderFillRequest | null>>,
) {
  const seq = Date.now();
  setFillBuy({ market, price, side: 'buy', seq });
  setFillSell({ market, price, side: 'sell', seq: seq + 1 });
}

export const TradeNotificationListPage: React.FC<Props> = ({
  theme,
  onGoToChart,
  tickers,
  defaultMarket = 'KRW-BTC',
  paperTradingEnabled = true,
  paperAutoTradeEnabled = false,
  onPaperOrderFilled,
}) => {
  const {
    allNotifications,
    unreadCount,
    markAsRead,
    markAllAsRead,
    deleteNotification,
    deleteNotifications,
    deleteAllNotifications,
    openDetail,
    refreshHistory,
  } = useTradeNotification();

  const [query, setQuery] = useState('');
  const normalizedQuery = useMemo(() => query.trim().toLowerCase(), [query]);

  const [filter, setFilter] = useState<TradeNotificationReadFilter>('all');
  const [listSort, setListSort] = useState<TradeNotificationListSort>(
    () => loadTradeNotificationListSort(),
  );
  const [candleFilter, setCandleFilter] = useState<TradeNotificationCandleFilter>(
    () => loadTradeNotificationCandleFilter(),
  );
  const [listLayout, setListLayout] = useState<TradeNotificationListLayout>(
    () => loadTradeNotificationListLayout(),
  );
  const [gridPreset, setGridPreset] = useState<TradeNotificationGridPresetId>(
    () => loadTradeNotificationGridPreset(),
  );
  const [strategies, setStrategies] = useState<StrategyDto[]>([]);
  const [selectedMarket, setSelectedMarket] = useState(defaultMarket);
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [reportOpen, setReportOpen] = useState(false);
  const [reportModel, setReportModel] = useState<BacktestAnalysisReportModel | null>(null);
  const [reportLoadingId, setReportLoadingId] = useState<string | null>(null);
  const reportGenRef = useRef(0);
  const [rightTab, setRightTab] = useState<RightTab>('trade');
  const [fillBuy, setFillBuy] = useState<TradeOrderFillRequest | null>(null);
  const [fillSell, setFillSell] = useState<TradeOrderFillRequest | null>(null);
  const [summary, setSummary] = useState<PaperSummaryDto | null>(null);
  const orderAnchorRef = useRef<HTMLDivElement>(null);
  const [refreshing, setRefreshing] = useState(false);
  const [rightOpen, setRightOpen] = useState(() => readStoredBool(TNL_RIGHT_OPEN_KEY, true));
  const [fullscreenItem, setFullscreenItem] = useState<TradeNotificationItem | null>(null);
  const { settings: appSettings } = useAppSettings();
  const { getParams } = useIndicatorSettings();
  const chartLiveReceiveHighlight = appSettings.chartLiveReceiveHighlight ?? true;

  const refreshPaperData = useCallback(async () => {
    try {
      const s = await loadPaperSummary();
      setSummary(s);
    } catch {
      setSummary(null);
    }
    onPaperOrderFilled?.();
  }, [onPaperOrderFilled]);

  useEffect(() => {
    void refreshPaperData();
  }, [refreshPaperData]);

  useEffect(() => {
    let cancelled = false;
    let retryTimer: ReturnType<typeof setTimeout> | null = null;

    const load = () => {
      void prefetchNotificationStrategies().then(list => {
        if (cancelled) return;
        if (list.length > 0) {
          setStrategies(list);
        } else {
          // 백엔드 미준비(Docker 기동 초기)로 빈 목록이 반환된 경우 5초 후 재시도
          retryTimer = setTimeout(load, 5000);
        }
      });
    };

    load();
    return () => {
      cancelled = true;
      if (retryTimer) clearTimeout(retryTimer);
    };
  }, []);

  const tradePrice = useMemo(
    () => coerceFiniteNumber(tickers?.get(selectedMarket)?.tradePrice),
    [tickers, selectedMarket],
  );

  const posQty = useMemo(() => {
    const p = summary?.positions?.find(x => x.symbol === selectedMarket);
    return coerceFiniteNumber(p?.quantity) ?? 0;
  }, [summary?.positions, selectedMarket]);

  const { orderbook, wsStatus: obWsStatus, spread } = useUpbitOrderbook(selectedMarket);
  const { trades: recentTrades, strength: tradeStrength } = useUpbitRecentTrades(selectedMarket);
  const selectedTicker = tickers?.get(selectedMarket);
  const orderbookPrevClose = selectedTicker
    ? (selectedTicker.tradePrice ?? 0) - (selectedTicker.changePrice ?? 0)
    : 0;
  const orderbookTickerInfo = selectedTicker ? {
    tradePrice: selectedTicker.tradePrice,
    changeRate: selectedTicker.changeRate,
    changePrice: selectedTicker.changePrice,
    accTradePrice24: selectedTicker.accTradePrice24,
    accTradeVolume24: selectedTicker.accTradeVolume24,
    highPrice: selectedTicker.highPrice,
    lowPrice: selectedTicker.lowPrice,
    openingPrice: selectedTicker.openingPrice,
  } : undefined;

  const handleSelectMarket = useCallback((market: string, price?: number) => {
    setSelectedMarket(market);
    const p = price ?? coerceFiniteNumber(tickers?.get(market)?.tradePrice);
    if (p != null && p > 0) {
      fillBothSides(market, p, setFillBuy, setFillSell);
    }
    setRightTab('trade');
  }, [tickers]);

  /** 알림 목록 아이템 "매매" 버튼 — 우측 패널 열고 해당 종목 현재가 입력 */
  const handleTradeFromAlert = useCallback((item: TradeNotificationItem) => {
    if (!item.isRead) markAsRead(item.id);
    const price = coerceFiniteNumber(tickers?.get(item.market)?.tradePrice)
      ?? coerceFiniteNumber(item.price);
    handleSelectMarket(item.market, price ?? undefined);
    setRightOpen(true);
    setRightTab('trade');
  }, [markAsRead, tickers, handleSelectMarket]);

  const selectedCount = selectedId ? 1 : 0;

  const handleNotificationSelect = useCallback((item: TradeNotificationItem) => {
    if (!item.isRead) markAsRead(item.id);
    setSelectedId(prev => {
      if (prev === item.id) return null;
      const price = coerceFiniteNumber(item.price);
      if (price != null && price > 0) {
        fillBothSides(item.market, price, setFillBuy, setFillSell);
      } else {
        handleSelectMarket(item.market);
      }
      setRightTab('trade');
      return item.id;
    });
  }, [markAsRead, handleSelectMarket]);

  const handleReportFromAlert = useCallback(async (item: TradeNotificationItem) => {
    if (!item.isRead) markAsRead(item.id);
    if (!item.strategyId && !item.strategyName?.trim()) {
      window.alert('전략 정보가 없어 상세분석 레포트를 생성할 수 없습니다.');
      return;
    }
    if (reportLoadingId) return;

    const gen = ++reportGenRef.current;
    setReportLoadingId(item.id);
    setReportModel(null);
    setReportOpen(false);

    try {
      const { strategy, strategyId: effectiveStrategyId } = await resolveNotificationStrategy(
        item.strategyId,
        item.strategyName,
        strategies,
        item.market,
      );
      if (gen !== reportGenRef.current) return;
      if (!strategy || effectiveStrategyId == null) {
        window.alert('전략을 불러올 수 없습니다.');
        return;
      }

      const market = normalizeMarketCode(item.market);
      const timeframe = candleTypeToTimeframe(item.candleType ?? '1m');
      const toTimeSec = normalizeEpochSec(item.candleTime);
      const indicatorParams = Object.fromEntries(
        REPORT_INDICATOR_TYPES.map(t => [t, getParams(t) as Record<string, unknown>]),
      );
      const { bars, evaluationBarCount } = await fetchBacktestCandleBundle({
        market,
        timeframe,
        toTimeSec,
        buyDsl: strategy.buyCondition,
        sellDsl: strategy.sellCondition,
        indicatorParams,
      });
      if (gen !== reportGenRef.current) return;
      if (bars.length < 10) {
        window.alert('캔들 데이터가 부족해 분석 레포트를 생성할 수 없습니다.');
        return;
      }

      const strategyName = strategy.name || item.strategyName || '전략';
      const btSettings = await loadBacktestSettings();
      const res = await runBacktest({
        strategyId: effectiveStrategyId,
        bars: bars.map(b => ({
          time: b.time,
          open: b.open,
          high: b.high,
          low: b.low,
          close: b.close,
          volume: b.volume,
        })),
        timeframe,
        symbol: market,
        strategyName,
        settings: btSettings,
        indicatorParams,
        evaluationBarCount,
      });
      if (gen !== reportGenRef.current) return;
      if (!res) {
        window.alert('백테스트 분석에 실패했습니다.');
        return;
      }

      const model = buildChartBacktestReportModel({
        analysis: res.analysis ?? null,
        stats: res.stats ?? null,
        signals: res.signals,
        strategyName,
        symbol: market.replace(/^KRW-/, ''),
        timeframe,
        barCount: bars.length,
        createdAt: new Date(item.receivedAt).toISOString(),
      });
      if (!model) {
        window.alert('분석 데이터를 생성할 수 없습니다.');
        return;
      }

      setReportModel(model);
      setReportOpen(true);
    } catch {
      if (gen !== reportGenRef.current) return;
      window.alert('상세분석 레포트 생성 중 오류가 발생했습니다.');
    } finally {
      if (gen === reportGenRef.current) setReportLoadingId(null);
    }
  }, [markAsRead, reportLoadingId, getParams, strategies]);

  const handleDeleteOne = useCallback(async (item: TradeNotificationItem, e: React.MouseEvent) => {
    e.stopPropagation();
    if (!window.confirm('이 알림을 삭제하시겠습니까?')) return;
    await deleteNotification(item.id);
    setSelectedId(prev => (prev === item.id ? null : prev));
  }, [deleteNotification]);

  const handleDeleteSelected = useCallback(async () => {
    if (!selectedId) return;
    if (!window.confirm('선택한 알림을 삭제하시겠습니까?')) return;
    await deleteNotifications([selectedId]);
    setSelectedId(null);
  }, [selectedId, deleteNotifications]);

  const handleDeleteAll = useCallback(async () => {
    if (allNotifications.length === 0) return;
    if (!window.confirm(`알림 ${allNotifications.length}건을 모두 삭제하시겠습니까?`)) return;
    await deleteAllNotifications();
    setSelectedId(null);
  }, [allNotifications.length, deleteAllNotifications]);

  const handleOrderbookRowClick = useCallback((price: number, rowType: 'ask' | 'bid') => {
    setRightTab('trade');
    const side = rowType === 'bid' ? 'buy' : 'sell';
    const req: TradeOrderFillRequest = { market: selectedMarket, price, side, seq: Date.now() };
    if (side === 'buy') setFillBuy(req);
    else setFillSell(req);
  }, [selectedMarket]);

  const filtered = useMemo(() => {
    let items = filter === 'unread'
      ? allNotifications.filter(n => !n.isRead)
      : allNotifications;
    if (candleFilter) {
      items = items.filter(
        n => normalizeStartCandleType(n.candleType) === candleFilter,
      );
    }
    if (normalizedQuery) {
      items = items.filter(n => {
        const mkt = n.market.toLowerCase();
        const sym = n.market.includes('-') ? n.market.split('-')[1].toLowerCase() : mkt;
        const ko = (
          tickers?.get(n.market)?.koreanName ??
          getKoreanName(n.market)
        ).toLowerCase();
        const strat = (n.strategyName ?? '').toLowerCase();
        const sigKo = n.signalType === 'BUY' ? '매수' : '매도';
        return (
          ko.includes(normalizedQuery) ||
          mkt.includes(normalizedQuery) ||
          sym.includes(normalizedQuery) ||
          strat.includes(normalizedQuery) ||
          sigKo.includes(normalizedQuery)
        );
      });
    }
    return items;
  }, [allNotifications, filter, candleFilter, normalizedQuery, tickers]);

  const sorted = useMemo(
    () => sortTradeNotifications(filtered, listSort),
    [filtered, listSort],
  );

  const handleListSortChange = useCallback((sort: TradeNotificationListSort) => {
    setListSort(sort);
    saveTradeNotificationListSort(sort);
  }, []);

  const handleCandleFilterChange = useCallback((value: TradeNotificationCandleFilter) => {
    setCandleFilter(value);
    saveTradeNotificationCandleFilter(value);
  }, []);

  const handleListLayoutChange = useCallback((layout: TradeNotificationListLayout) => {
    setListLayout(layout);
    saveTradeNotificationListLayout(layout);
  }, []);

  const handlePageRefresh = useCallback(async () => {
    if (refreshing) return;
    setRefreshing(true);
    try {
      await Promise.all([
        refreshHistory({ serverOnly: true }),
        refreshPaperData(),
      ]);
    } finally {
      setRefreshing(false);
    }
  }, [refreshing, refreshHistory, refreshPaperData]);

  const handleGridPresetChange = useCallback((preset: TradeNotificationGridPresetId) => {
    setGridPreset(preset);
    saveTradeNotificationGridPreset(preset);
  }, []);

  const toggleRightPanel = useCallback(() => {
    setRightOpen(prev => {
      const next = !prev;
      storeBool(TNL_RIGHT_OPEN_KEY, next);
      return next;
    });
  }, []);

  const isListView = listLayout === 'list';
  const gridCols = getTradeNotificationGridPreset(gridPreset).cols;

  const renderNotificationRow = useCallback((item: TradeNotificationItem) => (
    <TradeNotificationListRow
      key={item.id}
      item={item}
      theme={theme}
      layoutMode={listLayout}
      itemTag="div"
      prefetchedStrategies={strategies}
      isSelected={selectedId === item.id}
      chartsEnabled={isListView}
      chartLiveReceiveHighlight={chartLiveReceiveHighlight}
      onSelect={() => handleNotificationSelect(item)}
      onDelete={e => void handleDeleteOne(item, e)}
      onDetail={() => {
        if (!item.isRead) markAsRead(item.id);
        openDetail(item, { centered: true });
      }}
      onGoToChart={() => {
        markAsRead(item.id);
        setFullscreenItem(item);
      }}
      onTrade={() => handleTradeFromAlert(item)}
      onReport={() => void handleReportFromAlert(item)}
      reportLoading={reportLoadingId === item.id}
      ticker={tickers?.get(normalizeMarketCode(item.market)) ?? tickers?.get(item.market)}
    />
  ), [
    theme,
    listLayout,
    isListView,
    selectedId,
    chartLiveReceiveHighlight,
    handleNotificationSelect,
    handleDeleteOne,
    markAsRead,
    openDetail,
    handleTradeFromAlert,
    handleReportFromAlert,
    reportLoadingId,
    tickers,
    strategies,
  ]);

  return (
    <>
    {fullscreenItem && (
      <FullscreenChartModal
        item={fullscreenItem}
        theme={theme}
        onClose={() => setFullscreenItem(null)}
      />
    )}
    <BacktestAnalysisReportModal
      open={reportOpen}
      onClose={() => setReportOpen(false)}
      model={reportModel}
    />
    <div
      className={[
        'tnl-page',
        'tnl-page--with-right',
        'bps-page--vtd',
        'se-page',
        `se-page--${theme}`,
        'app',
        theme,
        rightOpen ? '' : 'tnl-page--right-collapsed',
      ].filter(Boolean).join(' ')}
    >
      <div className="tnl-body">
          <div className="tnl-main" style={{ containerType: 'inline-size', containerName: 'tnl-main' }}>
          <div className="tnl-header">
            <h1 className="tnl-title">매매 시그널 알림</h1>
            <div className="tnl-toolbar">
              {/* 검색란 */}
              <div className={`tnl-search-box${query ? ' tnl-search-box--active' : ''}`}>
                <svg className="tnl-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
                  <circle cx="11" cy="11" r="8" />
                  <line x1="21" y1="21" x2="16.65" y2="16.65" />
                </svg>
                <input
                  type="text"
                  className="tnl-search-input"
                  placeholder="한글 종목명·코드·전략 검색"
                  value={query}
                  onChange={e => setQuery(e.target.value)}
                  aria-label="알림 목록 검색"
                />
                {query && (
                  <button
                    type="button"
                    className="tnl-search-clear"
                    onClick={() => setQuery('')}
                    aria-label="검색어 초기화"
                  >
                    ✕
                  </button>
                )}
              </div>

              <span className="tnl-chip tnl-chip--warn">미확인 {unreadCount}건</span>
              {selectedCount > 0 && (
                <span className="tnl-chip tnl-chip--selected">선택 {selectedCount}건</span>
              )}
              <div className="tnl-toolbar-selects">
                <select
                  className="tnl-toolbar-select"
                  value={filter}
                  aria-label="표시 범위"
                  onChange={e => setFilter(e.target.value as TradeNotificationReadFilter)}
                >
                  {TRADE_NOTIFICATION_READ_FILTER_OPTIONS.map(o => (
                    <option key={o.value} value={o.value}>{o.label}</option>
                  ))}
                </select>
                <span className="header-toolbar-divider" aria-hidden="true" />
                <select
                  className="tnl-toolbar-select"
                  value={listSort}
                  aria-label="목록 정렬"
                  onChange={e => handleListSortChange(e.target.value as TradeNotificationListSort)}
                >
                  {TRADE_NOTIFICATION_SORT_OPTIONS.map(o => (
                    <option key={o.value} value={o.value}>{o.label}</option>
                  ))}
                </select>
                <span className="header-toolbar-divider" aria-hidden="true" />
                <select
                  className="tnl-toolbar-select"
                  value={candleFilter}
                  aria-label="시간봉 필터"
                  onChange={e => handleCandleFilterChange(e.target.value as TradeNotificationCandleFilter)}
                >
                  <option value="">전체</option>
                  {TRADE_NOTIFICATION_CANDLE_FILTER_OPTIONS.map(o => (
                    <option key={o.value} value={o.value}>{o.label}</option>
                  ))}
                </select>
              </div>
              {unreadCount > 0 && (
                <button type="button" className="tnl-btn tnl-btn--primary" onClick={markAllAsRead}>
                  모두 읽음
                </button>
              )}
              <div className="tnl-layout-toggle vtd-view-toggle" role="group" aria-label="목록 표시 방식">
                <button
                  type="button"
                  className={`vtd-view-toggle-btn${isListView ? ' vtd-view-toggle-btn--on' : ''}`}
                  onClick={() => handleListLayoutChange('list')}
                >
                  목록형
                </button>
                <button
                  type="button"
                  className={`vtd-view-toggle-btn${!isListView ? ' vtd-view-toggle-btn--on' : ''}`}
                  onClick={() => handleListLayoutChange('grid')}
                >
                  그리드형
                </button>
              </div>
              <span className="header-toolbar-divider" aria-hidden="true" />
              <div className="tnl-toolbar-end">
                <button
                  type="button"
                  className={['tnl-icon-btn', refreshing ? 'tnl-icon-btn--refreshing' : ''].filter(Boolean).join(' ')}
                  title="화면 새로고침"
                  aria-label="화면 새로고침"
                  disabled={refreshing}
                  onClick={() => void handlePageRefresh()}
                >
                  <IcRefresh />
                </button>
                <TradeNotificationGridLayoutPicker
                  value={gridPreset}
                  onChange={handleGridPresetChange}
                  disabled={isListView}
                />
                <button
                  type="button"
                  className="tnl-icon-btn"
                  title={selectedCount > 0 ? `선택 알림 삭제 (${selectedCount}건)` : '선택 알림 삭제'}
                  aria-label={selectedCount > 0 ? `선택 알림 ${selectedCount}건 삭제` : '선택 알림 삭제'}
                  disabled={selectedCount === 0}
                  onClick={() => void handleDeleteSelected()}
                >
                  <IcTrash />
                </button>
                <button
                  type="button"
                  className="tnl-icon-btn tnl-icon-btn--danger"
                  title="모두 삭제"
                  aria-label="모두 삭제"
                  disabled={allNotifications.length === 0}
                  onClick={() => void handleDeleteAll()}
                >
                  <IcTrash />
                </button>
              </div>
            </div>
          </div>

          <div className={['tnl-list-wrap', !isListView ? 'tnl-list-wrap--grid' : ''].filter(Boolean).join(' ')}>
            {sorted.length === 0 ? (
              <div className="tnl-empty">
                {normalizedQuery ? `"${query}" 에 해당하는 알림이 없습니다.` : '표시할 알림이 없습니다.'}
              </div>
            ) : isListView ? (
              <VirtualScroll
                className="tnl-list tnl-list--gallery vl-scroll"
                items={sorted}
                estimateSize={384}
                measureDynamic
                overscan={3}
                getItemKey={item => item.id}
                role="list"
                aria-label="매매 시그널 알림 목록"
              >
                {item => renderNotificationRow(item)}
              </VirtualScroll>
            ) : (
              <VirtualGridScroll
                className={[
                  'tnl-list',
                  'tnl-list--grid',
                  'vl-scroll',
                  `tnl-grid--cols-${gridCols}`,
                ].join(' ')}
                style={{ ['--tnl-grid-cols' as string]: gridCols }}
                rowClassName="vl-grid-row tnl-grid-row"
                items={sorted}
                columns={gridCols}
                estimateRowSize={300}
                rowGap={14}
                measureDynamic
                remeasureKey={gridCols}
                overscan={2}
                getItemKey={item => item.id}
                role="list"
                aria-label="매매 시그널 알림 그리드"
              >
                {item => renderNotificationRow(item)}
              </VirtualGridScroll>
            )}
          </div>
        </div>

        <div className={`tnl-side-wrap tnl-side-wrap--right${rightOpen ? ' tnl-side-wrap--open' : ''}`}>
          <button
            type="button"
            className={`bps-panel-handle bps-panel-handle--right${rightOpen ? ' bps-panel-handle--open' : ''}`}
            onClick={toggleRightPanel}
            title={rightOpen ? '매매·호가 패널 닫기' : '매매·호가 패널 열기'}
            aria-expanded={rightOpen}
            aria-label={rightOpen ? '매매·호가 패널 닫기' : '매매·호가 패널 열기'}
          >
            <svg
              width="8"
              height="14"
              viewBox="0 0 8 14"
              fill="none"
              stroke="currentColor"
              strokeWidth="1.8"
              strokeLinecap="round"
              strokeLinejoin="round"
              aria-hidden
            >
              {rightOpen ? (
                <polyline points="2,1 6,7 2,13" />
              ) : (
                <polyline points="6,1 2,7 6,13" />
              )}
            </svg>
          </button>
          <aside
            className={`tnl-right trade-right-panel${rightOpen ? ' tnl-right--open' : ''}`}
            aria-label="매매·호가"
            aria-hidden={!rightOpen}
          >
            <TradeRightPanelTabs
              activeTab={rightTab}
              onTabChange={setRightTab}
              className="tnl-right-tabs"
            />
            <TradeRightPanelBody
              anchorRef={orderAnchorRef}
              bodyClassName="tnl-right-body"
            >
              {rightTab === 'trade' ? (
                <TradeSplitOrderPanel
                  market={selectedMarket}
                  tradePrice={tradePrice}
                  availableKrw={coerceFiniteNumber(summary?.cashBalance) ?? 0}
                  fillBuy={fillBuy}
                  fillSell={fillSell}
                  searchAnchorRef={orderAnchorRef}
                  onMarketSelect={m => handleSelectMarket(m)}
                  paperTradingEnabled={paperTradingEnabled}
                  paperAutoTradeEnabled={paperAutoTradeEnabled}
                  onPaperOrderFilled={() => void refreshPaperData()}
                />
              ) : (
                <TradeOrderbookPanelContent
                  market={selectedMarket}
                  asks={orderbook.asks}
                  bids={orderbook.bids}
                  totalAskSize={orderbook.totalAskSize}
                  totalBidSize={orderbook.totalBidSize}
                  wsStatus={obWsStatus}
                  bestAsk={spread.bestAsk}
                  bestBid={spread.bestBid}
                  spread={spread.spread}
                  spreadPct={spread.pct}
                  prevClose={orderbookPrevClose}
                  tickerInfo={orderbookTickerInfo}
                  recentTrades={recentTrades}
                  tradeStrength={tradeStrength ?? undefined}
                  onRowClick={handleOrderbookRowClick}
                />
              )}
            </TradeRightPanelBody>
          </aside>
        </div>
      </div>
    </div>
    </>
  );
};

export default TradeNotificationListPage;
