// ============================================================================
// App — main dashboard composition (V3 Ops)
// ============================================================================

const { useState, useEffect } = React;

// /coin 스코프(코인 전용 페이지)에서는 심볼 가격포지션 차트를 숨긴다(코인엔 불필요 + 로딩 무거움).
const DASH_IS_COIN = (typeof location !== "undefined")
  && location.pathname.replace(/\/+$/, "").endsWith("/coin");

const GRID_POSITIONS_EMBED_VERSION_PARAM = "v=20260601-grid-viewopts";
const gridPositionsSrc = (accountKey = "") => {
  let apiKey = "";
  try {
    apiKey = (typeof localStorage !== "undefined" && localStorage.getItem("v3ops_api_key")) || "";
  } catch (_) {}
  return `/grid_positions.html?account=${encodeURIComponent(accountKey || "")}&apiKey=${encodeURIComponent(apiKey)}&${GRID_POSITIONS_EMBED_VERSION_PARAM}`;
};

const GridPositionsFrame = ({ accountKey = "", compact = false }) => (
  <iframe
    key={`grid-${accountKey || ""}-${compact ? "compact" : "full"}`}
    title="그리드 포지션 관리"
    src={gridPositionsSrc(accountKey)}
    style={{
      width: "100%",
      height: compact ? "min(720px, calc(100vh - 180px))" : "calc(100vh - 140px)",
      minHeight: compact ? 520 : 0,
      border: "1px solid var(--border, #30363d)",
      borderRadius: "8px",
      background: "#0d1117",
    }}
  />
);

const uniqueSymbols = (items = []) => Array.from(new Set(items.map((x) => x.symbol).filter(Boolean)));
const priceDecimalsFor = (symbol) => symbol === "USDJPY" ? 3 : ["XAUUSD", "NAS100"].includes(symbol) ? 2 : 5;
const POSITION_CHART_TIMEFRAMES = ["5m", "15m", "1h"];
const POSITION_CHART_TIMEFRAME_KEY = "v3ops_chart_timeframe";
const POSITION_CHART_VIEW_STORAGE_KEY = "v3ops_position_chart_viewopts";
const POSITION_CHART_VIEW_DEFAULTS = {
  entry: true,
  sl: true,
  tp: true,
  pending: true,
  filled: true,
  closed: true,
};
const POSITION_CHART_VIEW_OPTIONS = [
  { key: "entry", label: "entry", title: "오픈 진입 마커와 진입가 라인" },
  { key: "sl", label: "SL", title: "오픈 포지션 손절가 라인" },
  { key: "tp", label: "TP", title: "오픈 포지션 익절가 라인" },
  { key: "pending", label: "대기주문", title: "대기 주문 가격 라인" },
  { key: "filled", label: "완료진입", title: "청산된 포지션의 진입 마커" },
  { key: "closed", label: "청산", title: "청산 마커" },
];
const normalizePositionTimeframe = (value) => POSITION_CHART_TIMEFRAMES.includes(value) ? value : "5m";
const readPositionTimeframe = () => {
  try { return normalizePositionTimeframe(localStorage.getItem(POSITION_CHART_TIMEFRAME_KEY) || "5m"); } catch (_) { return "5m"; }
};
const readPositionChartViewOptions = () => {
  try {
    const saved = JSON.parse(localStorage.getItem(POSITION_CHART_VIEW_STORAGE_KEY) || "{}") || {};
    return Object.fromEntries(
      POSITION_CHART_VIEW_OPTIONS.map(({ key }) => [key, saved[key] !== false])
    );
  } catch (_) {
    return { ...POSITION_CHART_VIEW_DEFAULTS };
  }
};
const mergeChartBars = (existing = [], incoming = []) => {
  const byTs = new Map();
  [...existing, ...incoming].forEach((bar) => {
    if (!bar?.ts) return;
    byTs.set(bar.ts, { ...(byTs.get(bar.ts) || {}), ...bar });
  });
  return Array.from(byTs.values()).sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
};
const pendingOrderTypeLabel = (type) => {
  const key = String(type ?? "").toLowerCase().replace(/\s+/g, "_");
  return {
    buy_limit: "BUY LIMIT",
    sell_limit: "SELL LIMIT",
    buy_stop: "BUY STOP",
    sell_stop: "SELL STOP",
    buy_stop_limit: "BUY STOP LIMIT",
    sell_stop_limit: "SELL STOP LIMIT",
    2: "BUY LIMIT",
    3: "SELL LIMIT",
    4: "BUY STOP",
    5: "SELL STOP",
    6: "BUY STOP LIMIT",
    7: "SELL STOP LIMIT",
  }[key] || String(type || "대기").toUpperCase();
};
const fmtOptionalPrice = (value, symbol) => {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? fmt.price(n, priceDecimalsFor(symbol)) : "—";
};

const PositionsPage = ({ data, apiStatus }) => {
  const charts = data.symbolCharts || {};
  const [chartTimeframe, setChartTimeframe] = useState(readPositionTimeframe);
  const dataTimeframe = normalizePositionTimeframe(chartTimeframe);
  const accountKey = data.bridge?.accountKey || "MT5";
  const availableTimeframes = charts.timeframes?.length
    ? POSITION_CHART_TIMEFRAMES.filter((tf) => charts.timeframes.includes(tf))
    : POSITION_CHART_TIMEFRAMES;
  const [barCache, setBarCache] = useState({});
  // barCacheRef: fetchFirstPage 가 barCache 의존성 없이 현재 캐시 보유 여부만 읽도록(로딩 표시
  // 판정용). barCache 를 callback deps 에 넣으면 5s 마다 콜백 재생성→mount effect 재발화됨.
  const barCacheRef = React.useRef(barCache);
  barCacheRef.current = barCache;
  const [loadingFirstKey, setLoadingFirstKey] = useState("");
  const [loadingOlderKey, setLoadingOlderKey] = useState("");
  const [exhaustedKeys, setExhaustedKeys] = useState({});
  const [olderError, setOlderError] = useState("");
  // first-page fetch 의 non-Abort 실패. "차트 데이터 없음"(빈 심볼)과 구분해 노출하고, 다음 틱
  // 성공 시 클리어한다(기존 /api/all 배너 노출 대비 회귀 방지). 키 단위로 묶어 심볼 전환 시 stale 미표시.
  const [firstErrorKey, setFirstErrorKey] = useState("");
  const pendingOrders = data.pendingOrders || [];
  const symbols = Array.from(new Set([
    ...(charts.symbols || []),
    ...uniqueSymbols(data.positions || []),
    ...uniqueSymbols(pendingOrders),
  ])).filter(Boolean);
  const preferredSymbol = data.positions?.[0]?.symbol || pendingOrders?.[0]?.symbol;
  const preferred = preferredSymbol && symbols.includes(preferredSymbol)
    ? preferredSymbol
    : symbols.includes("NAS100") ? "NAS100" : symbols[0];
  const [symbol, setSymbol] = useState(preferred || "NAS100");
  const [hoverTicket, setHoverTicket] = useState(null);
  const [selectedTicket, setSelectedTicket] = useState(null);
  const [chartViewOptions, setChartViewOptions] = useState(readPositionChartViewOptions);
  const symbolKey = symbols.join("|");
  const cacheKeyFor = (sym, tf = dataTimeframe) => `${accountKey}|${sym || ""}|${tf || "5m"}`;

  useEffect(() => {
    if (symbols.length && !symbols.includes(symbol)) {
      setSymbol(preferred || symbols[0]);
    }
  }, [symbolKey]);

  useEffect(() => {
    setHoverTicket(null);
    setSelectedTicket(null);
    setOlderError("");
    try { localStorage.setItem("v3ops_selected_symbol", symbol); } catch (_) {}
  }, [symbol, dataTimeframe, accountKey]);

  // ★ lazy-load: 차트 막대는 /api/all 에 더 이상 없다. 현재 (account|symbol|timeframe) 의
  // first-page 를 /api/chart 로 직접 fetch 한다. 트리거 = mount + 선택 변경 + 5s 라이브 틱.
  // latest-wins: 요청 시작 시 selection 스냅샷 + AbortController 로 이전 요청 abort + 응답
  // 적용 전 현재 selection 일치 확인(symbol 포함) → 심볼 바꿔도 옛 데이터 덮어쓰기 방지.
  // MERGE(replace 아님): fresh first-page 를 기존 barCache 에 병합해 페이징한 옛 히스토리 보존.
  const selectionRef = React.useRef({ accountKey, symbol, timeframe: dataTimeframe });
  selectionRef.current = { accountKey, symbol, timeframe: dataTimeframe };
  const firstPageAbortRef = React.useRef(null);
  // in-flight 요청의 key. 5s 틱(fromTick) 경로는 같은-key 요청이 진행 중이면 skip(콜드빌드>5s
  // 일 때 첫 페인트가 매 틱 abort+재시작으로 지연되지 않게). selection 변경은 그대로 abort+재시작.
  const firstPageInFlightKeyRef = React.useRef("");
  const fetchFirstPage = React.useCallback((fromTick = false) => {
    if (DASH_IS_COIN) return; // 코인은 가격 차트 hidden → fetch 안 함
    if (!symbol || !window.V3OPS_LIVE_DATA?.request) return;
    const snap = { accountKey, symbol, timeframe: dataTimeframe };
    const key = `${accountKey}|${symbol}|${dataTimeframe}`;
    // 틱 dedupe: 같은-key first-page 가 이미 in-flight 면 틱 경로는 skip(live-data.js inFlightTick 패턴).
    if (fromTick && firstPageInFlightKeyRef.current === key) return;
    if (firstPageAbortRef.current) {
      try { firstPageAbortRef.current.abort(); } catch (_) {}
    }
    const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
    firstPageAbortRef.current = controller;
    firstPageInFlightKeyRef.current = key;
    const params = new URLSearchParams();
    params.set("symbol", symbol);
    params.set("timeframe", dataTimeframe);
    params.set("limit", "360");
    params.set("includeCurrent", "1");
    if (accountKey) params.set("account", accountKey);
    setLoadingFirstKey((prev) => (barCacheRef.current[key]?.length ? prev : key));
    window.V3OPS_LIVE_DATA.request(`/api/chart?${params.toString()}`, controller ? { signal: controller.signal } : undefined)
      .then((payload) => {
        const cur = selectionRef.current;
        // latest-wins: 응답이 도착했을 때도 현재 selection 과 동일한 경우에만 적용.
        if (cur.accountKey !== snap.accountKey || cur.symbol !== snap.symbol || cur.timeframe !== snap.timeframe) return;
        // 성공 시 이 key 의 first-page 에러 클리어(다음 틱 복구).
        setFirstErrorKey((prev) => (prev === key ? "" : prev));
        const incoming = Array.isArray(payload?.bars) ? payload.bars : [];
        if (!incoming.length) return;
        setBarCache((prev) => ({
          ...prev,
          [key]: mergeChartBars(prev[key] || [], incoming),
        }));
      })
      .catch((err) => {
        if (err?.name === "AbortError") return; // 더 새 요청이 취소함 — 무시
        // non-Abort 실패는 노출(빈 심볼과 구분). 응답 시점 selection 이 그대로일 때만.
        const cur = selectionRef.current;
        if (cur.accountKey === snap.accountKey && cur.symbol === snap.symbol && cur.timeframe === snap.timeframe) {
          setFirstErrorKey(key);
        }
      })
      .finally(() => {
        // 자기 자신이 현재 owner 인 요청만 로딩/in-flight 플래그를 정리(A→B→A 빠른 복귀 시 abort 된
        // 이전 요청의 .finally 가 새 요청 로딩을 조기 클리어해 깜빡이는 것 방지).
        if (firstPageAbortRef.current === controller) {
          firstPageAbortRef.current = null;
          firstPageInFlightKeyRef.current = "";
          setLoadingFirstKey((cur) => (cur === key ? "" : cur));
        }
      });
  }, [accountKey, symbol, dataTimeframe]);

  // mount + (account|symbol|timeframe) 변경 시 즉시 first-page fetch.
  useEffect(() => {
    fetchFirstPage();
    return () => {
      if (firstPageAbortRef.current) {
        try { firstPageAbortRef.current.abort(); } catch (_) {}
        firstPageAbortRef.current = null;
      }
    };
  }, [fetchFirstPage]);

  // 5s 라이브 틱('v3ops:data') 구독 → 현재 심볼 first-page 재fetch(forming bar 갱신).
  useEffect(() => {
    if (DASH_IS_COIN) return;
    const onTick = () => fetchFirstPage(true);
    window.addEventListener("v3ops:data", onTick);
    return () => window.removeEventListener("v3ops:data", onTick);
  }, [fetchFirstPage]);

  const selectTimeframe = (tf) => {
    const next = normalizePositionTimeframe(tf);
    setChartTimeframe(next);
    setOlderError("");
    try { localStorage.setItem(POSITION_CHART_TIMEFRAME_KEY, next); } catch (_) {}
    window.dispatchEvent(new CustomEvent("v3ops:chart-timeframe-change", { detail: { timeframe: next } }));
  };
  const setChartViewOption = (key, checked) => {
    setChartViewOptions((prev) => {
      const next = { ...prev, [key]: checked };
      try { localStorage.setItem(POSITION_CHART_VIEW_STORAGE_KEY, JSON.stringify(next)); } catch (_) {}
      return next;
    });
  };

  const cacheKey = cacheKeyFor(symbol);
  // bars 는 이제 전적으로 barCache(=/api/chart lazy fetch)에서 온다. /api/all 에 더 이상 bars 없음.
  const bars = barCache[cacheKey] || [];
  const isLoadingFirst = loadingFirstKey === cacheKey && !bars.length;
  const hasFirstError = firstErrorKey === cacheKey;
  const loadOlderBars = React.useCallback(() => {
    if (!symbol || !bars.length || !window.V3OPS_LIVE_DATA?.request) return;
    if (loadingOlderKey === cacheKey || exhaustedKeys[cacheKey]) return;
    const oldest = bars[0]?.ts;
    if (!oldest) return;
    const params = new URLSearchParams();
    params.set("symbol", symbol);
    params.set("timeframe", dataTimeframe);
    params.set("to", oldest);
    params.set("limit", "360");
    params.set("includeCurrent", "0");
    if (accountKey) params.set("account", accountKey);
    const currentBars = bars;
    setLoadingOlderKey(cacheKey);
    setOlderError("");
    window.V3OPS_LIVE_DATA.request(`/api/chart?${params.toString()}`)
      .then((payload) => {
        const olderBars = Array.isArray(payload?.bars) ? payload.bars : [];
        setBarCache((prev) => ({
          ...prev,
          [cacheKey]: mergeChartBars(olderBars, prev[cacheKey] || currentBars),
        }));
        if (!payload?.hasMore || olderBars.length === 0) {
          setExhaustedKeys((prev) => ({ ...prev, [cacheKey]: true }));
        }
      })
      .catch((err) => {
        setOlderError(String(err?.message || err));
      })
      .finally(() => {
        setLoadingOlderKey((key) => key === cacheKey ? "" : key);
      });
  }, [accountKey, bars, cacheKey, dataTimeframe, exhaustedKeys, loadingOlderKey, symbol]);
  const openRows = (charts.openPositions || data.positions || []).filter((p) => p.symbol === symbol);
  const pendingRows = pendingOrders.filter((p) => p.symbol === symbol);
  const closedRows = (charts.closedTrades || []).filter((p) => p.symbol === symbol).slice(-80).reverse();
  const latest = bars[bars.length - 1];
  const latestState = latest?.partial ? "진행중" : "완성";
  const isLoadingOlder = loadingOlderKey === cacheKey;
  const activeTicket = hoverTicket ?? selectedTicket;
  const toggleTicket = (ticket) => {
    setSelectedTicket((prev) => String(prev) === String(ticket) ? null : ticket);
  };

  return (
    <>
      <div className="tb-page-head">
        <div>
          <div className="label-caps">포지션 차트</div>
          <h1>심볼별 차트 · 오픈/대기/청산</h1>
          <div className="tb-meta">
            DB market_bars_{dataTimeframe} + MT5 진행봉 기준 · 오픈/청산 마커와 대기 주문 가격선 표시
          </div>
        </div>
        <div className="tb-section-head-right">
          <Badge kind="neutral">타임프레임 {dataTimeframe}</Badge>
          <Badge kind={openRows.length ? "warn" : "success"}>오픈 {openRows.length}건</Badge>
          <Badge kind={pendingRows.length ? "warn" : "success"}>대기 {pendingRows.length}건</Badge>
          <Badge kind="neutral">청산 {closedRows.length}건</Badge>
        </div>
      </div>

      {apiStatus?.ok === false && (
        <div
          className="tb-panel"
          style={{
            borderColor: "var(--warn-bd)",
            background: "color-mix(in oklab, var(--warn) 7%, var(--bg-1))",
          }}
        >
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <Badge kind="warn"><Icon name="alert" size={11} /> API 인증 실패</Badge>
            <span className="tb-meta">
              실제 차트/포지션 데이터를 못 받아서 fallback 화면입니다. 처음 접속 URL에 <span className="mono">?apiKey=...</span>를 붙여 다시 열어주세요.
            </span>
          </div>
        </div>
      )}

      <div className="tb-symbol-tabs tb-chart-symbol-tabs">
        {symbols.map((sym) => (
          <button
            key={sym}
            className={`tb-btn tb-btn-sm ${sym === symbol ? "tb-btn-active" : "tb-btn-secondary"}`}
            onClick={() => setSymbol(sym)}
          >
            {sym}
          </button>
        ))}
      </div>

      <div className="tb-symbol-tabs tb-chart-timeframe-tabs">
        {availableTimeframes.map((tf) => (
          <button
            key={tf}
            className={`tb-btn tb-btn-sm ${tf === dataTimeframe ? "tb-btn-active" : "tb-btn-secondary"}`}
            onClick={() => selectTimeframe(tf)}
          >
            {tf}
          </button>
        ))}
        <span className="tb-meta">
          히스토리 {isLoadingOlder ? "조회 중" : exhaustedKeys[cacheKey] ? "끝" : "대기"}
        </span>
      </div>

      {!DASH_IS_COIN && (
      <div className="tb-panel">
        <div className="tb-panel-head">
          <div>
            <div className="label-caps">{symbol} 가격 차트 · 최근 {bars.length}개 {dataTimeframe}</div>
            <div className="tb-meta" style={{ marginTop: 4 }}>
              최신 {fmt.time(latest?.ts)} · {latestState} · 종가 {latest ? fmt.price(latest.close, symbol === "USDJPY" ? 3 : ["XAUUSD", "NAS100"].includes(symbol) ? 2 : 5) : "—"}
              {hasFirstError ? " · 로딩 실패(재시도 중)" : ""}
              {olderError ? ` · 과거 조회 실패 ${olderError}` : ""}
            </div>
          </div>
          <div className="tb-chart-head-tools">
            <div className="tb-chart-view-options" aria-label="차트 표시 항목">
              <span className="tb-chart-view-title">표시</span>
              {POSITION_CHART_VIEW_OPTIONS.map(({ key, label, title }) => (
                <label key={key} title={title}>
                  <input
                    type="checkbox"
                    checked={chartViewOptions[key] !== false}
                    onChange={(event) => setChartViewOption(key, event.target.checked)}
                  />
                  {label}
                </label>
              ))}
            </div>
            <div className="tb-marker-legend">
              <span><i className="tb-marker-open" /> 오픈 진입 {openRows.length}</span>
              <span><i className="tb-marker-pending" /> 대기 주문 {pendingRows.length}</span>
              <span><i className="tb-marker-closed" /> 청산 {closedRows.length}</span>
              <span><i className="tb-marker-risk" /> TP/SL</span>
            </div>
          </div>
        </div>
        <SymbolPriceChartLW
          symbol={symbol}
          bars={bars}
          loading={isLoadingFirst}
          loadError={hasFirstError}
          openPositions={openRows}
          closedTrades={closedRows}
          pendingOrders={pendingRows}
          timeframe={dataTimeframe}
          height={420}
          focusTicket={activeTicket}
          hasMoreHistory={!exhaustedKeys[cacheKey]}
          loadingHistory={isLoadingOlder}
          onLoadMoreHistory={loadOlderBars}
          viewOptions={chartViewOptions}
          onHoverTicketChange={setHoverTicket}
          onTicketClick={toggleTicket}
        />
      </div>
      )}

      <div>
        <SectionHeader
          title={`${symbol} 오픈 포지션`}
          right={
            <span style={{ display: "flex", gap: 6, alignItems: "center" }}>
              <Badge kind={openRows.length ? "warn" : "success"}>{openRows.length}건</Badge>
              {openRows.length > 0 && (() => {
                const openPnlSum = openRows.reduce((s, r) => s + (Number(r.pnl) || 0), 0);
                return (
                  <Badge kind={openPnlSum >= 0 ? "success" : "danger"}>
                    합계 {fmt.pnl(openPnlSum)}
                  </Badge>
                );
              })()}
            </span>
          }
        />
        <PositionsTable
          rows={openRows}
          activeTicket={activeTicket}
          onHoverTicket={setHoverTicket}
          onLeaveTicket={() => setHoverTicket(null)}
          onToggleTicket={toggleTicket}
        />
      </div>
      <div style={{ marginTop: 16 }}>
        <SectionHeader
          title={`${symbol} 대기 주문`}
          right={<Badge kind={pendingRows.length ? "warn" : "success"}>{pendingRows.length}건</Badge>}
        />
        <PendingOrdersTable rows={pendingRows} />
      </div>
      <div style={{ marginTop: 16 }}>
        <SectionHeader
          title={`${symbol} 최근 청산 포지션`}
          right={
            <span style={{ display: "flex", gap: 6, alignItems: "center" }}>
              <Badge kind="neutral">{closedRows.length}건</Badge>
              {closedRows.length > 0 && (() => {
                const closedPnlSum = closedRows.reduce((s, r) => s + (Number(r.pnl) || 0), 0);
                return (
                  <Badge kind={closedPnlSum >= 0 ? "success" : "danger"}>
                    합계 {fmt.pnl(closedPnlSum)}
                  </Badge>
                );
              })()}
            </span>
          }
        />
        <ClosedTradesTable rows={closedRows} />
      </div>

    </>
  );
};

const ClosedTradesTable = ({ rows }) => (
  <div className="tb-table-wrap">
    <table className="tb-table">
      <thead>
        <tr>
          <th>티켓</th>
          <th>방향</th>
          <th className="r">진입</th>
          <th className="r">청산</th>
          <th className="r">손익</th>
          <th>청산시각</th>
          <th>소스</th>
        </tr>
      </thead>
      <tbody>
        {rows.length ? rows.map((p) => (
          <tr key={`${p.ticket}-${p.closedAt}`}>
            <td className="tb-meta">#{p.ticket}</td>
            <td><Badge kind={p.direction === "short" ? "danger" : "success"} solid>{p.direction === "short" ? "숏" : "롱"}</Badge></td>
            <td className="r">{fmt.price(p.entry, 5)}</td>
            <td className="r">{fmt.price(p.exit, 5)}</td>
            <td className={`r ${fmt.pnlClass(p.pnl)}`}>{fmt.pnl(p.pnl, 2)}</td>
            <td className="tb-meta">{fmt.date(p.closedAt)}</td>
            <td className="tb-meta tb-truncate">{p.source}</td>
          </tr>
        )) : (
          <tr><td colSpan="7" className="tb-meta">최근 청산 포지션이 없습니다.</td></tr>
        )}
      </tbody>
    </table>
  </div>
);

const PendingOrdersTable = ({ rows = [] }) => (
  <div className="tb-table-wrap">
    <table className="tb-table">
      <thead>
        <tr>
          <th>티켓</th>
          <th>종목</th>
          <th>종류</th>
          <th className="r">Lot</th>
          <th className="r">주문가</th>
          <th className="r">현재가</th>
          <th className="r">SL</th>
          <th className="r">TP</th>
          <th>생성</th>
          <th>만료</th>
          <th>소스</th>
          <th>코멘트</th>
        </tr>
      </thead>
      <tbody>
        {rows.length ? rows.map((o) => {
          const tone = o.direction === "short" ? "danger" : o.direction === "long" ? "success" : "neutral";
          const gridParent = o.gridParent || {};
          const virtualParentMeta = gridParent.isVirtual
            ? [
                gridParent.virtualLabel || "가상부모",
                gridParent.registeredAt ? `등록 ${fmt.time(gridParent.registeredAt)}` : "",
                gridParent.lifespanEndAt ? `만료 ${fmt.time(gridParent.lifespanEndAt)}` : "",
              ].filter(Boolean).join(" · ")
            : "";
          return (
            <tr key={o.ticket}>
              <td className="tb-meta">#{o.ticket}</td>
              <td className="tb-strong">{o.symbol}</td>
              <td><Badge kind={tone} solid>{pendingOrderTypeLabel(o.type)}</Badge></td>
              <td className="r">{fmt.num(o.lots, 2)}</td>
              <td className="r">{fmtOptionalPrice(o.price, o.symbol)}</td>
              <td className="r">{fmtOptionalPrice(o.current, o.symbol)}</td>
              <td className="r pnl-neg">{fmtOptionalPrice(o.sl, o.symbol)}</td>
              <td className="r pnl-pos">{fmtOptionalPrice(o.tp, o.symbol)}</td>
              <td className="tb-meta">{fmt.time(o.setupAt)}</td>
              <td className="tb-meta">{o.expirationAt ? fmt.time(o.expirationAt) : "—"}</td>
              <td className="tb-meta" title={o.gridLabel || o.source}>
                <div>{o.gridLabel || o.source}</div>
                {virtualParentMeta ? <div>{virtualParentMeta}</div> : null}
              </td>
              <td className="tb-meta tb-truncate">{o.comment || "—"}</td>
            </tr>
          );
        }) : (
          <tr><td colSpan="12" className="tb-meta">대기 주문이 없습니다.</td></tr>
        )}
      </tbody>
    </table>
  </div>
);

const isUsableAccountKey = (value) => {
  const text = String(value || "").trim();
  return Boolean(text && text !== "—" && text !== "-" && text.toLowerCase() !== "default");
};

const storedDashboardAccount = () => {
  try {
    const stored = localStorage.getItem("v3ops_account") || "";
    return isUsableAccountKey(stored) ? stored : "";
  } catch (_) {
    return "";
  }
};

const preferredLiveDailyAccount = (data, initial) => {
  const candidates = [
    data?.bridge?.accountKey,
    initial?.accountKey,
    storedDashboardAccount(),
  ];
  return candidates.find(isUsableAccountKey) || "";
};

const LiveDailyPage = ({ data, apiStatus }) => {
  const initial = data.liveDailyPerformance || {};
  const formatLocalDay = (date) => {
    const y = date.getFullYear();
    const m = String(date.getMonth() + 1).padStart(2, "0");
    const d = String(date.getDate()).padStart(2, "0");
    return `${y}-${m}-${d}`;
  };
  const defaultLiveDailyRange = () => {
    const end = new Date();
    const start = new Date(end);
    start.setMonth(start.getMonth() - 1);
    return {
      start: formatLocalDay(start),
      end: formatLocalDay(end),
    };
  };
  const initialRange = defaultLiveDailyRange();
  const [accountKey, setAccountKey] = useState(() => preferredLiveDailyAccount(data, initial));
  const [tzMode, setTzMode] = useState("utc");
  const [startDate, setStartDate] = useState(initialRange.start);
  const [endDate, setEndDate] = useState(initialRange.end);
  const [selectedDay, setSelectedDay] = useState("");
  const [remote, setRemote] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const [refreshKey, setRefreshKey] = useState(0);
  const view = remote || initial || {};
  const rows = Array.isArray(view.rows) ? view.rows : [];
  const selected = view.selected || rows.find((row) => row.day === view.selectedDay) || rows[0] || null;
  const curve = Array.isArray(view.curve) && view.curve.length ? view.curve : [];
  const latest = view.latest || rows[0] || null;
  const localOffsetMin = -new Date().getTimezoneOffset();
  const chronologicalRows = rows.slice().reverse();
  const rangeFirst = chronologicalRows[0] || null;
  const rangeLast = chronologicalRows[chronologicalRows.length - 1] || null;
  const rangeStartEquity = Number(rangeFirst?.startEquity);
  const rangeEndEquity = Number(rangeLast?.endEquity);
  const hasRangeEquity = Number.isFinite(rangeStartEquity) && Number.isFinite(rangeEndEquity) && rangeStartEquity > 0;
  const rangePnl = chronologicalRows.reduce((sum, row) => sum + (Number(row.pnl) || 0), 0);
  const rangePnlPct = hasRangeEquity ? rangePnl / rangeStartEquity * 100 : 0;
  const rangeDeposit = chronologicalRows.reduce((sum, row) => sum + (Number(row.deposit) || 0), 0);
  const rangeWithdrawal = chronologicalRows.reduce((sum, row) => sum + (Number(row.withdrawal) || 0), 0);
  const rangeEquityDelta = hasRangeEquity ? rangeEndEquity - rangeStartEquity : 0;
  const rangeLabel = rangeFirst && rangeLast ? `${rangeFirst.day} → ${rangeLast.day}` : "선택 구간 없음";

  useEffect(() => {
    const next = preferredLiveDailyAccount(data, initial);
    if (next && next !== accountKey) {
      setAccountKey(next);
      setRemote(null);
      setSelectedDay("");
    }
  }, [initial.accountKey, data.bridge?.accountKey, accountKey]);

  useEffect(() => {
    const onHeaderAccountChange = (event) => {
      const next = event?.detail?.resolvedAccountKey || event?.detail?.accountKey || "";
      if (!next) return;
      setAccountKey(next);
      setRemote(null);
      setSelectedDay("");
      setRefreshKey((v) => v + 1);
    };
    window.addEventListener("v3ops:account-change", onHeaderAccountChange);
    return () => window.removeEventListener("v3ops:account-change", onHeaderAccountChange);
  }, []);

  useEffect(() => {
    if (!window.V3OPS_LIVE_DATA?.request) return;
    const params = new URLSearchParams();
    if (isUsableAccountKey(accountKey)) params.set("accountKey", accountKey);
    params.set("tz", tzMode);
    if (tzMode === "local") params.set("tzOffsetMin", String(localOffsetMin));
    if (startDate) params.set("start", startDate);
    if (endDate) params.set("end", endDate);
    if (!startDate && !endDate) params.set("days", "90");
    if (selectedDay) params.set("day", selectedDay);
    let alive = true;
    setLoading(true);
    setError("");
    window.V3OPS_LIVE_DATA.request(`/api/live-daily?${params.toString()}`)
      .then((payload) => {
        if (!alive) return;
        setRemote(payload);
        const resolvedAccount = payload?.accountKey;
        if (isUsableAccountKey(resolvedAccount) && !isUsableAccountKey(accountKey)) {
          setAccountKey(resolvedAccount);
        }
        if ((payload?.selectedDay || "") !== selectedDay) setSelectedDay(payload?.selectedDay || "");
      })
      .catch((err) => {
        if (!alive) return;
        setError(String(err?.message || err));
      })
      .finally(() => {
        if (alive) setLoading(false);
      });
    return () => { alive = false; };
  }, [accountKey, tzMode, startDate, endDate, selectedDay, refreshKey]);

  const clearRange = () => {
    setStartDate("");
    setEndDate("");
    setSelectedDay("");
  };

  return (
    <>
      <div className="tb-page-head">
        <div>
          <div className="label-caps">일별 라이브 성과</div>
          <h1>계정별 실측 equity 일간 성과</h1>
          <div className="tb-meta">equity 샘플을 DB에 저장하고 날짜 기준을 UTC / 로컬로 전환</div>
        </div>
        <div className="tb-section-head-right">
          <Badge kind={view.ok === false || error ? "warn" : "success"}>{view.tzLabel || (tzMode === "local" ? "LOCAL" : "UTC")}</Badge>
          <Badge kind="neutral">{accountKey}</Badge>
        </div>
      </div>

      {apiStatus?.ok === false && (
        <div className="tb-panel" style={{ borderColor: "var(--warn-bd)", marginBottom: 14 }}>
          <Badge kind="warn"><Icon name="alert" size={11} /> API 연결 확인 필요</Badge>
          <span className="tb-meta" style={{ marginLeft: 8 }}>{apiStatus.error}</span>
        </div>
      )}

      <div className="tb-panel">
        <div className="tb-panel-head">
          <div>
            <div className="label-caps">조회 조건</div>
            <div className="tb-meta" style={{ marginTop: 4 }}>처음에는 최근 1개월을 조회합니다. 시작일/종료일을 바꾸고 조회를 누르세요.</div>
          </div>
          <div className="tb-section-head-right">
            {loading && <Badge kind="info">불러오는 중</Badge>}
            {error && <Badge kind="warn">{error}</Badge>}
            {view.ok === false && <Badge kind="warn">{view.error || "조회 실패"}</Badge>}
            <Badge kind="neutral">DB {view.dbPath ? view.dbPath.split("/").slice(-1)[0] : "trade_history.db"}</Badge>
          </div>
        </div>
        <div className="tb-date-range">
          <label>
            계정
            <span className="tb-account-fixed mono" title={accountKey || "계정 자동 선택"}>
              {accountKey || "계정 자동 선택"}
            </span>
          </label>
          <label>
            시작일
            <input className="tb-date-input" type="date" value={startDate} onChange={(e) => { setStartDate(e.target.value); setSelectedDay(""); }} />
          </label>
          <label>
            종료일
            <input className="tb-date-input" type="date" value={endDate} onChange={(e) => { setEndDate(e.target.value); setSelectedDay(""); }} />
          </label>
          <button className="tb-btn tb-btn-sm tb-btn-active" onClick={() => { setSelectedDay(""); setRefreshKey((v) => v + 1); }}>조회</button>
          <button className="tb-btn tb-btn-sm tb-btn-secondary" onClick={() => { const r = defaultLiveDailyRange(); setStartDate(r.start); setEndDate(r.end); setSelectedDay(""); }}>최근 1개월</button>
          <button className="tb-btn tb-btn-sm tb-btn-secondary" onClick={clearRange}>전체 날짜</button>
          <button
            className={`tb-btn tb-btn-sm ${tzMode === "utc" ? "tb-btn-active" : "tb-btn-secondary"}`}
            onClick={() => { setTzMode("utc"); setSelectedDay(""); }}
          >
            UTC
          </button>
          <button
            className={`tb-btn tb-btn-sm ${tzMode === "local" ? "tb-btn-active" : "tb-btn-secondary"}`}
            onClick={() => { setTzMode("local"); setSelectedDay(""); }}
          >
            로컬
          </button>
        </div>
        <div className="tb-inline-note" style={{ margin: "4px 0 0" }}>
          <strong>조회 방법:</strong> 기본 조회 범위는 최근 1개월입니다. 헤더의 현재 계정과 선택한 시작일/종료일 기준으로 해당 기간의 일별 성과가 표시됩니다. UTC/로컬 버튼은 하루를 끊는 기준 날짜를 바꿉니다.
        </div>
      </div>

      <div className="tb-row-3">
        <Card tone="neutral">
          <div className="label-caps">조회 구간 순손익</div>
          <div className={`tb-kpi-value ${fmt.pnlClass(rangePnl)}`}>{fmt.pnl(rangePnl)}</div>
          <div className="tb-meta">{rangeLabel} · {fmt.pct(rangePnlPct)} · 입금 {fmt.abbr(rangeDeposit, 2)} / 출금 {fmt.abbr(rangeWithdrawal, 2)}</div>
        </Card>
        <Card tone="neutral">
          <div className="label-caps">조회 구간 Equity</div>
          <div className="tb-kpi-value">{fmt.abbr(hasRangeEquity ? rangeEndEquity : 0, 2)}</div>
          <div className="tb-meta">시작 {fmt.abbr(hasRangeEquity ? rangeStartEquity : 0, 2)} · 변동 {fmt.pnl(rangeEquityDelta)} · {rows.length}일</div>
        </Card>
        <Card tone="neutral">
          <div className="label-caps">선택 날짜</div>
          <div className={`tb-kpi-value ${fmt.pnlClass(selected?.pnl)}`}>{fmt.pnl(selected?.pnl || 0)}</div>
          <div className="tb-meta">{selected?.day || latest?.day || "—"} · 입금 {fmt.abbr(selected?.deposit || 0, 2)} / 출금 {fmt.abbr(selected?.withdrawal || 0, 2)}</div>
        </Card>
      </div>

      <div className="tb-row-2-1">
        <div className="tb-panel">
          <div className="tb-panel-head">
            <div>
              <div className="label-caps">조회 구간 equity 곡선</div>
              <div className="tb-meta" style={{ marginTop: 4 }}>{rangeLabel} · {view.tzLabel || "UTC"}</div>
            </div>
            <Badge kind="neutral">샘플 {curve.length}</Badge>
          </div>
          <EquityChart points={curve.length ? curve : [{ i: 0, v: hasRangeEquity ? rangeEndEquity : 0 }]} height={220} />
        </div>
        <div className="tb-panel">
          <div className="tb-panel-head">
            <div className="label-caps">일별 범위</div>
            <Badge kind="neutral">{rows.length}일</Badge>
          </div>
          <div className="tb-table-wrap tb-live-daily-table">
            <table className="tb-table">
              <thead>
                <tr>
                  <th>날짜</th>
                  <th className="r">순손익</th>
                  <th className="r">입금</th>
                  <th className="r">출금</th>
                  <th className="r">%</th>
                  <th className="r">Equity</th>
                </tr>
              </thead>
              <tbody>
                {rows.map((row) => (
                  <tr
                    key={row.day}
                    className={`tb-row-interactive ${row.day === (selectedDay || view.selectedDay) ? "tb-row-focused" : ""}`}
                    onClick={() => setSelectedDay(row.day)}
                  >
                    <td className="mono tb-day-cell">{row.day}</td>
                    <td className={`r mono ${fmt.pnlClass(row.pnl)}`}>{fmt.pnl(row.pnl)}</td>
                    <td className={`r mono ${Number(row.deposit) ? "pnl-pos" : "pnl-flat"}`}>{fmt.abbr(row.deposit || 0, 2)}</td>
                    <td className={`r mono ${Number(row.withdrawal) ? "pnl-neg" : "pnl-flat"}`}>{fmt.abbr(row.withdrawal || 0, 2)}</td>
                    <td className={`r mono ${fmt.pnlClass(row.pnlPct)}`}>{fmt.pct(row.pnlPct)}</td>
                    <td className="r mono">{fmt.abbr(row.endEquity, 2)}</td>
                  </tr>
                ))}
                {!rows.length && (
                  <tr><td colSpan="6" className="tb-meta">저장된 equity 샘플이 없습니다.</td></tr>
                )}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </>
  );
};

const ENTRY_BLOCK_STAGE_LABELS = {
  detect: "탐지",
  account: "계좌",
  pre_order: "주문전",
  preOrder: "주문전",
  sizing: "사이징",
};

const ENTRY_BLOCK_REASON_LABELS = {
  risk_config_pre_dedup: "risk_config 선차단",
  risk_config: "risk_config",
  alignment_gate: "상위TF 정렬",
  alignment_data_unavailable: "정렬 데이터 없음",
  xau_short_structural_gate: "XAU 구조 차단",
  stale_signal: "신호 지연",
  pattern_not_live: "Live 미허용 패턴",
  pattern_disabled: "비활성 패턴",
  symbol_disabled: "비활성 심볼",
  signal_id_duplicate: "동일 신호 중복",
  market_closed: "장 닫힘",
  wr_gate: "WR 게이트",
  cooldown: "쿨다운",
  chop_gate: "chop",
  signal_filter_pattern_block: "패턴 차단",
  reverse_blocklist: "역방향 차단",
  wet_step_ma20: "step MA20",
  wet_step_alignment0: "step 정렬 0",
  wet_gap_ma60_120_zone: "gap MA60-120",
  wet_inflection_4h_opposite: "inflection 4H 반대",
  long_stable_full_bull: "BTC long 과열(stable+full_bull)",
  long_down_full_bull: "BTC long 피크(down+full_bull)",
  short_down_full_bear: "BTC short 저점(down+full_bear)",
  short_stable_mixed: "BTC short 혼조(stable+mixed)",
  min_sltp_distance: "SL/TP 최소거리",
  v4_block: "V4 overlay",
  below_min_lot: "min_lot 미만",
  below_min_lot_after_step: "step 후 min_lot 미만",
  invalid_lot: "lot invalid",
  invalid_lot_size: "lot invalid",
  lot_zero_skipped: "0 lot",
  final_lot_rejected: "최종 lot 차단",
  equity_unavailable: "equity 없음",
  balance_fetch_failed: "잔고 조회 실패",
  max_positions_reached: "최대 포지션",
  no_raw_signals: "원시 신호 없음",
  insufficient_data: "데이터 부족",
  all_candidates_filtered: "후보 전부 필터",
  alignment_gate_all: "정렬 전부 차단",
  risk_config_all: "risk_config 전부 차단",
  no_final_signal_after_gates: "최종 신호 없음",
  all_final_signals_stale: "최종 신호 지연",
  final_signals: "신호 생성",
};

const entryBlockStageLabel = (stage) => ENTRY_BLOCK_STAGE_LABELS[stage] || stage || "—";
const entryBlockReasonLabel = (reason) => (
  ENTRY_BLOCK_REASON_LABELS[reason] || String(reason || "unknown").replaceAll("_", " ")
);

const EntryBlocksPage = ({ data }) => {
  const blocks = data.entryBlocks || {};
  const totals = blocks.totals || {};
  const bySymbol = blocks.bySymbol || [];
  const byReason = blocks.byReason || [];
  const byStage = blocks.byStage || [];
  const [symbol, setSymbol] = useState("ALL");
  const symbols = ["ALL", ...Array.from(new Set(bySymbol.map((r) => r.symbol).filter(Boolean)))];
  const recent = (blocks.recent || []).filter((row) => symbol === "ALL" || row.symbol === symbol);
  const scanDiagnostics = blocks.scanDiagnostics || {};
  const scanTotals = scanDiagnostics.totals || {};
  const scanRecent = (scanDiagnostics.recent || []).filter((row) => symbol === "ALL" || row.symbol === symbol);
  const dbName = blocks.dbPath ? blocks.dbPath.split("/").slice(-1)[0] : "trade_history.db";

  return (
    <>
      <div className="tb-page-head">
        <div>
          <div className="label-caps">진입 차단</div>
          <h1>진입 전 차단 로그</h1>
          <div className="tb-meta">{data.bridge?.accountDisplay || data.bridge?.accountKey || "계정"} · 최근 {blocks.windowHours || 48}시간 · {dbName}</div>
        </div>
        <Badge kind={totals.total ? "warn" : "success"}>{fmt.num(totals.total || 0, 0)}건</Badge>
      </div>

      <div className="tb-symbol-tabs" style={{ marginBottom: 12 }}>
        {symbols.map((sym) => (
          <button
            key={sym}
            className={`tb-btn tb-btn-sm ${symbol === sym ? "tb-btn-active" : "tb-btn-secondary"}`}
            onClick={() => setSymbol(sym)}
          >
            {sym === "ALL" ? "전체" : sym}
          </button>
        ))}
      </div>

      <div className="tb-row-2-1">
        <div className="tb-panel">
          <div className="tb-panel-head">
            <div className="label-caps">심볼별 차단</div>
            <Badge kind="neutral">NAS · GOLD · BTC</Badge>
          </div>
          <div className="tb-table-wrap">
            <table className="tb-table">
              <thead>
                <tr>
                  <th>심볼</th>
                  <th className="r">전체</th>
                  <th className="r">탐지</th>
                  <th className="r">계좌</th>
                  <th className="r">주문전</th>
                  <th className="r">사이징</th>
                </tr>
              </thead>
              <tbody>
                {bySymbol.map((row) => (
                  <tr key={row.symbol}>
                    <td className="mono" title={row.mt5Symbol || ""}>{row.label || row.symbol}</td>
                    <td className="r mono">{fmt.num(row.total || 0, 0)}</td>
                    <td className="r mono">{fmt.num(row.detect || 0, 0)}</td>
                    <td className="r mono">{fmt.num(row.account || 0, 0)}</td>
                    <td className="r mono">{fmt.num(row.preOrder || 0, 0)}</td>
                    <td className="r mono">{fmt.num(row.sizing || 0, 0)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>

        <div className="tb-stack">
          <div className="tb-panel">
            <div className="tb-panel-head">
              <div className="label-caps">단계별</div>
            </div>
            <div className="tb-table-wrap">
              <table className="tb-table">
                <tbody>
                  {byStage.map((row) => (
                    <tr key={`${row.stage}-${row.stageKey}`}>
                      <td>{entryBlockStageLabel(row.stage)}</td>
                      <td className="r mono">{fmt.num(row.count, 0)}</td>
                    </tr>
                  ))}
                  {!byStage.length && <tr><td className="tb-meta">기록 없음</td><td /></tr>}
                </tbody>
              </table>
            </div>
          </div>
          <div className="tb-panel">
            <div className="tb-panel-head">
              <div className="label-caps">사유 상위</div>
            </div>
            <div className="tb-table-wrap">
              <table className="tb-table">
                <tbody>
                  {byReason.slice(0, 8).map((row) => (
                    <tr key={`${row.reason}-${row.stage}`}>
                      <td title={row.reason}>{entryBlockReasonLabel(row.reason)}</td>
                      <td className="r mono">{fmt.num(row.count, 0)}</td>
                    </tr>
                  ))}
                  {!byReason.length && <tr><td className="tb-meta">기록 없음</td><td /></tr>}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>

      <div style={{ marginTop: 12 }}>
        <SectionHeader title={`신호 미발생 / 탐지 요약 · ${scanRecent.length}건`} />
        <div className="tb-table-wrap">
          <table className="tb-table">
            <thead>
              <tr>
                <th>시간</th>
                <th>심볼</th>
                <th>TF</th>
                <th>봉</th>
                <th className="r">Raw</th>
                <th className="r">Gate</th>
                <th className="r">Final</th>
                <th>사유</th>
                <th>상세</th>
              </tr>
            </thead>
            <tbody>
              {scanRecent.map((row, i) => (
                <tr key={`${row.ts}-${row.symbol}-${row.reason}-${i}`}>
                  <td className="mono tb-meta">{fmt.date(row.ts)}</td>
                  <td className="mono" title={row.mt5Symbol || ""}>{row.symbol}</td>
                  <td className="mono">{row.timeframe || "—"}</td>
                  <td className="mono tb-meta">{fmt.date(row.scanTs)}</td>
                  <td className="r mono">{fmt.num(row.rawCount || 0, 0)}</td>
                  <td className="r mono">{fmt.num(row.afterGatesCount || 0, 0)}</td>
                  <td className="r mono">{fmt.num(row.finalCount || 0, 0)}</td>
                  <td title={row.reason}>{entryBlockReasonLabel(row.reason)}</td>
                  <td className="tb-meta" title={row.detailText || ""}>{row.detailText || "—"}</td>
                </tr>
              ))}
              {!scanRecent.length && (
                <tr><td colSpan="9" className="tb-meta">선택한 조건의 탐지 요약이 없습니다. 다음 새 봉 스캔부터 기록됩니다.</td></tr>
              )}
            </tbody>
          </table>
        </div>
        <div className="tb-meta" style={{ marginTop: 6 }}>
          전체 {fmt.num(scanTotals.total || 0, 0)} · 신호 {fmt.num(scanTotals.final || 0, 0)} · 미발생 {fmt.num(scanTotals.empty || 0, 0)} · 필터/차단 {fmt.num(scanTotals.blocked || 0, 0)}
        </div>
      </div>

      <div style={{ marginTop: 12 }}>
        <SectionHeader title={`최근 차단 · ${recent.length}건`} />
        <div className="tb-table-wrap">
          <table className="tb-table">
            <thead>
              <tr>
                <th>시간</th>
                <th>심볼</th>
                <th>패턴</th>
                <th>방향</th>
                <th>TF</th>
                <th>단계</th>
                <th>사유</th>
                <th className="r">Entry</th>
                <th className="r">Lot</th>
                <th>상세</th>
              </tr>
            </thead>
            <tbody>
              {recent.map((row, i) => (
                <tr key={`${row.ts}-${row.symbol}-${row.reason}-${i}`}>
                  <td className="mono tb-meta">{fmt.date(row.ts)}</td>
                  <td className="mono">{row.symbol}</td>
                  <td>{row.pattern || "—"}</td>
                  <td>{row.direction || "—"}</td>
                  <td className="mono">{row.timeframe || "—"}</td>
                  <td>{entryBlockStageLabel(row.stage)}</td>
                  <td title={row.reason}>{entryBlockReasonLabel(row.reason)}</td>
                  <td className="r mono">{fmt.price(row.entryPrice, priceDecimalsFor(row.symbol))}</td>
                  <td className="r mono">{row.lots == null ? "—" : fmt.num(row.lots, 4)}</td>
                  <td className="tb-meta" title={row.detailText || ""}>{row.detailText || "—"}</td>
                </tr>
              ))}
              {!recent.length && (
                <tr><td colSpan="10" className="tb-meta">선택한 조건의 차단 기록이 없습니다.</td></tr>
              )}
            </tbody>
          </table>
        </div>
      </div>
    </>
  );
};

const ComparePage = ({ data }) => (
  <>
    <div className="tb-page-head">
      <div>
        <div className="label-caps">비교</div>
        <h1>백테스트 vs 라이브 비교</h1>
        <div className="tb-meta">hourly_compare와 브로커 연결 상태</div>
      </div>
    </div>
    <div className="tb-row-2">
      <CompareCard compare={data.hourlyCompare} orphan={data.orphanPositions} />
      <BridgePanel bridge={data.bridge} />
    </div>
    <PositionsTable rows={data.positions} />
  </>
);

const DataPage = ({ data }) => (
  <>
    <div className="tb-page-head">
      <div>
        <div className="label-caps">DB 상태</div>
        <h1>봉 데이터 신선도</h1>
        <div className="tb-meta">TimescaleDB 최신 timestamp 기준</div>
      </div>
      <Badge kind={data.kpi.dbStatus === "ok" ? "success" : data.kpi.dbStatus === "warn" ? "warn" : "danger"}>
        {data.kpi.dbStatus === "ok" ? "정상" : data.kpi.dbStatus === "warn" ? "경고" : "실패"}
      </Badge>
    </div>
    <FreshnessGrid db={data.dbHealth} />
    <div className="tb-row-2">
      <div>
        <SectionHeader title="DB 테이블별 최악 상태" />
        <div className="tb-card"><DBHealth rows={data.dbHealth} /></div>
      </div>
    </div>
    <CronPanel jobs={data.cron} />
  </>
);

const LedgerPage = ({ data }) => (
  <>
    <div className="tb-page-head">
      <div>
        <div className="label-caps">Live 로그</div>
        <h1>최근 라이브 이벤트</h1>
        <div className="tb-meta">trade_ledger.jsonl 최근 이벤트와 warning timeline</div>
      </div>
      <Badge kind="neutral">최근 {data.ledgerEvents.length}건</Badge>
    </div>
    <div className="tb-row-2">
      <WarningTimeline events={data.ledgerEvents} />
      <LedgerFeed events={data.ledgerEvents} />
    </div>
  </>
);

function App() {
  const [active, setActive] = useState("overview");
  const [, setTick] = useState(0);
  const [data, setData] = useState(window.MOCK);
  const [apiStatus, setApiStatus] = useState(window.V3OPS_API_STATUS || { ok: null, error: null, hasApiKey: false });
  const [accountSwitching, setAccountSwitching] = useState(null);
  // 실데이터를 마지막으로 받은 시각(ms). 갱신 실패 시 'mock'인지 'stale real'인지 구분.
  const [lastDataAt, setLastDataAt] = useState(null);

  useEffect(() => {
    const id = setInterval(() => setTick((t) => t + 1), 5000);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    const onData = (event) => {
      const nextData = event.detail || window.MOCK;
      setData(nextData);
      setLastDataAt(Date.now());
      setAccountSwitching((current) => {
        if (!current) return null;
        const resolved = nextData?.bridge?.accountKey || "";
        return !current.accountKey || resolved === current.accountKey ? null : current;
      });
    };
    window.addEventListener("v3ops:data", onData);
    return () => window.removeEventListener("v3ops:data", onData);
  }, []);

  useEffect(() => {
    const onApiStatus = (event) => {
      const nextStatus = event.detail || { ok: null, error: null };
      setApiStatus(nextStatus);
      if (nextStatus.ok === false) setAccountSwitching(null);
    };
    window.addEventListener("v3ops:api-status", onApiStatus);
    if (window.V3OPS_API_STATUS) setApiStatus(window.V3OPS_API_STATUS);
    return () => window.removeEventListener("v3ops:api-status", onApiStatus);
  }, []);

  useEffect(() => {
    const onAccountChange = (event) => {
      const accountKey = event?.detail?.resolvedAccountKey || event?.detail?.accountKey || "";
      setAccountSwitching({ accountKey, startedAt: Date.now() });
    };
    window.addEventListener("v3ops:account-change", onAccountChange);
    return () => window.removeEventListener("v3ops:account-change", onAccountChange);
  }, []);

  return (
    <div className="tb-app">
      <StatusBar bot={data.bot} bridge={data.bridge} />
      <Sidebar active={active} onNav={setActive} />
      <main className="tb-main">
        {apiStatus.ok === false && (
          <div
            className="tb-panel"
            style={{
              borderColor: "var(--warn-bd)",
              background: "color-mix(in oklab, var(--warn) 7%, var(--bg-1))",
              padding: "10px 12px",
            }}
          >
            <div style={{ display: "flex", alignItems: "center", gap: 10, justifyContent: "space-between" }}>
              <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                {lastDataAt ? (
                  <>
                    <Badge kind="warn"><Icon name="alert" size={11} /> 데이터 갱신 실패</Badge>
                    <span className="tb-meta">
                      화면은 마지막 정상 데이터({Math.round((Date.now() - lastDataAt) / 1000)}초 전) 기준입니다 — 실시간 아님. 자동 재시도 중.
                    </span>
                  </>
                ) : (
                  <>
                    <Badge kind="warn"><Icon name="alert" size={11} /> 데이터 미수신</Badge>
                    <span className="tb-meta">
                      아직 실데이터를 못 받았습니다(빈/mock 화면). API 키 인증 실패면 처음 접속을 <span className="mono">?apiKey=...</span> 포함 URL로 열어주세요.
                    </span>
                  </>
                )}
              </div>
              <span className="mono tb-meta">{apiStatus.error}</span>
            </div>
          </div>
        )}

        {active === "overview" && (
          <>
        {/* Overall hero — answers "is everything ok?" in 10 seconds */}
        <OverallHero data={data} />

        {/* KPI cards */}
        <KPIRow data={data} />

        {/* Row: Equity curve (wide) + Bridge / Compare stack */}
        <div className="tb-row-2-1 tb-overview-main-row">
          <div className="tb-panel">
            <div className="tb-panel-head">
              <div>
                <div className="label-caps">현재 MT5 자본곡선 · 60일</div>
                <div className="tb-meta" style={{ marginTop: 4 }}>MT5 /balance equity 기준 · 실시간 포지션 손익 포함</div>
              </div>
              <div className="tb-section-head-right">
                {data.dataQuality?.equityCurve?.source === "live_history" ? (
                  <Badge kind="success" title={data.dataQuality.equityCurve.note}>실측</Badge>
                ) : (
                  <Badge kind="warn" title={data.dataQuality?.equityCurve?.note}>
                    {data.dataQuality?.equityCurve?.label || "미리보기"}
                  </Badge>
                )}
                <Badge kind="neutral">실시간</Badge>
              </div>
            </div>
            <EquityChart points={data.equityCurve} height={180} />
            <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--divider)" }}>
              <div className="label-caps" style={{ marginBottom: 6 }}>낙폭 · %</div>
              <DrawdownChart points={data.equityCurve} height={90} />
            </div>
          </div>
          <div className="tb-stack">
            <BridgePanel bridge={data.bridge} />
            <CompareCard compare={data.hourlyCompare} orphan={data.orphanPositions} />
          </div>
        </div>

        {/* Live PnL bar charts row */}
        {(() => {
          const livePnlQuality = data.dataQuality?.livePnl || {};
          const actualDays = livePnlQuality.actualDays;
          const windowDays = livePnlQuality.windowDays || 60;
          const periodLabel = (actualDays != null && actualDays < windowDays)
            ? `최근 ${actualDays < 1 ? actualDays.toFixed(1) : Math.round(actualDays)}일 (세션 시작 이후)`
            : `최근 ${windowDays}일`;
          return (
            <div className="tb-row-2">
              <div className="tb-panel">
                <div className="tb-panel-head">
                  <div className="label-caps">Live 심볼별 PnL · {periodLabel}</div>
                  <span className="tb-meta">{(data.liveBySymbol || []).length}개 심볼 · {(data.liveBySymbol || []).reduce((s,d)=>s+d.rows,0).toLocaleString()}건</span>
                </div>
                <BarChartH data={data.liveBySymbol || []} valueKey="pnl_usd" labelKey="symbol" height={200} />
              </div>
              <div className="tb-panel">
                <div className="tb-panel-head">
                  <div className="label-caps">Live 소스별 PnL · {periodLabel}</div>
                  <span className="tb-meta">{(data.liveBySource || []).length}개 소스 · {(data.liveBySource || []).reduce((s,d)=>s+d.rows,0).toLocaleString()}건</span>
                </div>
                <BarChartH data={data.liveBySource || []} valueKey="pnl_usd" labelKey="source" height={200} />
              </div>
            </div>
          );
        })()}

        {/* Live positions */}
        <div>
          <SectionHeader
            title={`실시간 포지션 · ${data.positions.length}개 오픈 · 대기 ${(data.pendingOrders || []).length}건`}
            right={<>
              <Badge kind="neutral">PnL <span className={`mono ${fmt.pnlClass(data.positions.reduce((s,p)=>s+p.pnl,0))}`}>{fmt.pnl(data.positions.reduce((s,p)=>s+p.pnl,0))}</span></Badge>
              <button className="tb-btn tb-btn-ghost tb-btn-sm">전체 보기</button>
            </>}
          />
          <PositionsTable rows={data.positions} />
          <div style={{ marginTop: 12 }}>
            <SectionHeader
              title="대기 주문"
              right={<Badge kind={(data.pendingOrders || []).length ? "warn" : "success"}>{(data.pendingOrders || []).length}건</Badge>}
            />
            <PendingOrdersTable rows={data.pendingOrders || []} />
          </div>
        </div>

        {/* Monthly + Exposure */}
        <div className="tb-row-2">
          <div className="tb-panel">
            <div className="tb-panel-head">
              <div className="label-caps">월별 PnL · 6개월</div>
              <Badge kind="success" title={data.dataQuality?.monthlyPnl?.note}>실측</Badge>
            </div>
            <MonthlyBars data={data.monthlyPnl} height={120} />
          </div>
          <div className="tb-panel">
            <div className="tb-panel-head">
              <div className="label-caps">노출도 · 자본 대비</div>
            </div>
            <ExposureBar data={data.exposure} />
          </div>
        </div>

        {/* Data freshness grid + warning timeline */}
        <div className="tb-row-2-1">
          <FreshnessGrid db={data.dbHealth} />
          <WarningTimeline events={data.ledgerEvents} />
        </div>

        {/* Cron / scheduler */}
        <CronPanel jobs={data.cron} />

        {/* DB Health summary (compact, table-level) + Ledger */}
        <div className="tb-row-1-2">
          <div>
            <SectionHeader
              title="DB 상태 · 테이블별"
              right={<Badge kind={data.kpi.dbStatus === "ok" ? "success" : data.kpi.dbStatus === "warn" ? "warn" : "danger"}>{data.kpi.dbStatus === "ok" ? "정상" : data.kpi.dbStatus === "warn" ? "경고" : "실패"}</Badge>}
            />
            <div className="tb-card">
              <DBHealth rows={data.dbHealth} />
            </div>
          </div>
          <div>
            <SectionHeader
              title="최근 Live 로그"
              right={<>
                <Badge kind="neutral">최근 {data.ledgerEvents.length}건</Badge>
                <button className="tb-btn tb-btn-ghost tb-btn-sm"><Icon name="filter" size={11}/> 필터</button>
              </>}
            />
            <LedgerFeed events={data.ledgerEvents} />
          </div>
        </div>

        <div style={{ height: 16 }} />
          </>
        )}

        {active === "positions" && <PositionsPage data={data} apiStatus={apiStatus} />}
        {active === "grid" && <GridPositionsFrame accountKey={(data.bridge && data.bridge.accountKey) || ""} />}
        {active === "liveDaily" && <LiveDailyPage data={data} apiStatus={apiStatus} />}
        {active === "entryBlocks" && <EntryBlocksPage data={data} />}
        {active === "compare" && <ComparePage data={data} />}
        {active === "db" && <DataPage data={data} />}
        {active === "ledger" && <LedgerPage data={data} />}
      </main>
      {accountSwitching && (
        <div className="tb-blocking-overlay" role="alert" aria-live="assertive">
          <div className="tb-blocking-dialog">
            <div className="tb-loading-spinner" />
            <div>
              <div className="tb-blocking-title">계정 전환 중</div>
              <div className="tb-blocking-sub mono">{accountSwitching.accountKey || "선택 계정"}</div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Bar chart valueKey override — needs to handle scientific notation values
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
