/* global React, Jersey, useT, useLang, PlayerName, CountryName, Ball, Bnb */
const { useState, useMemo, useEffect, useRef } = React;

// ============ Chainlink-style line chart ============
// Single blue line with gradient area fill, no per-direction red/green coloring,
// hover crosshair with price tooltip, breathing dot at latest price.
// When there is no historical data (single placeholder candle), expand the
// data into a horizontal baseline so the chart visually fills the frame.
function CandleChart({ candles, height = 320 }) {
  // ResizeObserver-driven viewBox: keep coordinate-system width fixed at 920,
  // but adjust the viewBox height so its aspect matches the actual container.
  // This lets the SVG visually fill any container size (no empty space below)
  // without preserveAspectRatio="none" distortion.
  const wrapRef = useRef(null);
  const [vbh, setVbh] = useState(height);
  useEffect(() => {
    if (!wrapRef.current || typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver(entries => {
      for (const e of entries) {
        const cw = e.contentRect.width;
        const ch = e.contentRect.height;
        if (cw > 0 && ch > 0) setVbh(Math.max(220, Math.round(920 * ch / cw)));
      }
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  const w = 920, h = vbh;
  const padT = 16, padR = 76, padB = 22, padL = 16;
  const innerW = w - padL - padR;
  const innerH = h - padT - padB;

  // If we only have one data point, expand to a flat baseline across the width
  // so the chart looks intentional (Chainlink "no recent movement" state).
  let closes = candles.map(c => c.c);
  if (closes.length < 2) {
    const single = closes[0] ?? 0;
    closes = new Array(48).fill(single);
  }
  const max = Math.max(...closes);
  const min = Math.min(...closes);
  const pad = Math.max((max - min) * 0.10, max * 0.05, 1e-12);
  const yMax = max + pad;
  const yMin = Math.max(0, min - pad);
  const range = yMax - yMin || 1;

  const y = v => padT + (1 - (v - yMin) / range) * innerH;
  const x = i => padL + (closes.length > 1 ? (i * innerW) / (closes.length - 1) : innerW / 2);

  const fmtY = v => (window.fmtUsdPrice ? window.fmtUsdPrice(v) : "$" + (v * 612).toFixed(6));

  const grid = [];
  for (let i = 0; i <= 4; i++) {
    const v = yMin + (range * i) / 4;
    grid.push({ y: y(v), label: fmtY(v) });
  }

  const linePoints = closes.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
  const areaPoints = `${padL},${y(yMin).toFixed(1)} ${linePoints} ${(padL + innerW).toFixed(1)},${y(yMin).toFixed(1)}`;

  const last = closes[closes.length - 1];
  const first = closes[0];
  const isUp = last >= first;
  // Chainlink-style single blue accent regardless of direction
  const lineColor = "#3b82f6";
  const lastX = x(closes.length - 1);
  const lastY = y(last);

  // Hover crosshair state
  const svgRef = useRef(null);
  const [hover, setHover] = useState(null); // { i, x, y, price }

  function onMove(e) {
    if (!svgRef.current || closes.length < 2) return;
    const rect = svgRef.current.getBoundingClientRect();
    const px = ((e.clientX - rect.left) / rect.width) * w;
    if (px < padL || px > w - padR) { setHover(null); return; }
    const i = Math.round(((px - padL) / innerW) * (closes.length - 1));
    const clamped = Math.max(0, Math.min(closes.length - 1, i));
    setHover({ i: clamped, x: x(clamped), y: y(closes[clamped]), price: closes[clamped] });
  }

  return (
    <div ref={wrapRef} className="candle-wrap">
    <svg
      ref={svgRef}
      viewBox={`0 0 ${w} ${h}`}
      preserveAspectRatio="none"
      className="candle-chart"
      onMouseMove={onMove}
      onMouseLeave={() => setHover(null)}
    >
      <defs>
        <linearGradient id="tv-area-grad" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%"   stopColor={lineColor} stopOpacity="0.28" />
          <stop offset="60%"  stopColor={lineColor} stopOpacity="0.08" />
          <stop offset="100%" stopColor={lineColor} stopOpacity="0" />
        </linearGradient>
      </defs>

      {/* horizontal grid + USD axis */}
      {grid.map((g, i) => (
        <g key={i}>
          <line x1={padL} x2={w - padR} y1={g.y} y2={g.y} stroke="rgba(255,255,255,0.04)" />
          <text x={w - padR + 8} y={g.y + 3} fill="#6b6b6b" fontSize="10" fontFamily="Inter, sans-serif">{g.label}</text>
        </g>
      ))}

      {/* area fill */}
      <polygon points={areaPoints} fill="url(#tv-area-grad)" />

      {/* main line */}
      <polyline points={linePoints} fill="none" stroke={lineColor} strokeWidth="1.8" strokeLinejoin="round" strokeLinecap="round" />

      {/* breathing dot at latest price */}
      <circle cx={lastX} cy={lastY} r="5" fill={lineColor} opacity="0.35">
        <animate attributeName="r" values="5;10;5" dur="2.4s" repeatCount="indefinite" />
        <animate attributeName="opacity" values="0.35;0;0.35" dur="2.4s" repeatCount="indefinite" />
      </circle>
      <circle cx={lastX} cy={lastY} r="3.2" fill="#ffffff" stroke={lineColor} strokeWidth="2" />

      {/* hover crosshair */}
      {hover && (
        <g pointerEvents="none">
          <line x1={hover.x} x2={hover.x} y1={padT} y2={h - padB} stroke="rgba(255,255,255,0.18)" strokeDasharray="3 4" />
          <line x1={padL}    x2={w - padR} y1={hover.y} y2={hover.y} stroke="rgba(255,255,255,0.18)" strokeDasharray="3 4" />
          <circle cx={hover.x} cy={hover.y} r="3.2" fill="#ffffff" stroke={lineColor} strokeWidth="2" />
          <rect x={Math.min(w - padR - 80, Math.max(padL, hover.x + 8))} y={hover.y - 24} width="74" height="18" fill="#0b0e11" stroke={lineColor} strokeWidth="1" rx="3" />
          <text x={Math.min(w - padR - 80, Math.max(padL, hover.x + 8)) + 6} y={hover.y - 11} fill="#fff" fontSize="11" fontWeight="600" fontFamily="Inter, sans-serif">{fmtY(hover.price)}</text>
        </g>
      )}
    </svg>
    </div>
  );
}

// ============ Skeleton — shown while on-chain history is being fetched ============
// Bloomberg-terminal vibe: faint grid, ghost waveform, sweeping highlight bar,
// LED indicator + monospace status line. No "Loading..." spinner — anything
// labeled "Loading" feels generic; this should feel like a live wire warming up.
function CandleChartSkeleton({ height = 320, lang }) {
  const wrapRef = useRef(null);
  const [vbh, setVbh] = useState(height);
  useEffect(() => {
    if (!wrapRef.current || typeof ResizeObserver === "undefined") return;
    const ro = new ResizeObserver(entries => {
      for (const e of entries) {
        const cw = e.contentRect.width;
        const ch = e.contentRect.height;
        if (cw > 0 && ch > 0) setVbh(Math.max(220, Math.round(920 * ch / cw)));
      }
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);
  const w = 920, h = vbh;
  const padT = 16, padR = 76, padB = 22, padL = 16;
  const innerW = w - padL - padR;
  const innerH = h - padT - padB;
  // Stable wave path so the skeleton doesn't twitch on re-render
  const wave = [...Array(48)].map((_, i) => {
    const x = padL + (i * innerW / 47);
    const y = padT + innerH * 0.55 +
              Math.sin(i * 0.42) * innerH * 0.10 +
              Math.sin(i * 1.13 + 1.7) * innerH * 0.04;
    return `${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(" ");

  return (
    <div ref={wrapRef} className="candle-wrap">
    <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className="candle-chart">
      <defs>
        <linearGradient id="tv-skel-sweep" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"   stopColor="#3b82f6" stopOpacity="0" />
          <stop offset="50%"  stopColor="#3b82f6" stopOpacity="0.22" />
          <stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
        </linearGradient>
        <linearGradient id="tv-skel-area" x1="0" x2="0" y1="0" y2="1">
          <stop offset="0%"   stopColor="#3b82f6" stopOpacity="0.10" />
          <stop offset="100%" stopColor="#3b82f6" stopOpacity="0" />
        </linearGradient>
        <clipPath id="tv-skel-clip"><rect x={padL} y={padT} width={innerW} height={innerH} /></clipPath>
      </defs>

      {/* faint grid (matches real chart) */}
      {[0, 1, 2, 3, 4].map(i => (
        <line key={i}
          x1={padL} x2={w - padR}
          y1={padT + i * (innerH / 4)} y2={padT + i * (innerH / 4)}
          stroke="rgba(255,255,255,0.04)" />
      ))}

      {/* ghost waveform + sweep */}
      <g clipPath="url(#tv-skel-clip)">
        <polygon
          points={`${padL},${(padT + innerH).toFixed(1)} ${wave} ${(padL + innerW).toFixed(1)},${(padT + innerH).toFixed(1)}`}
          fill="url(#tv-skel-area)"
        />
        <polyline
          points={wave}
          fill="none"
          stroke="rgba(59,130,246,0.32)"
          strokeWidth="1.4"
          strokeLinejoin="round"
          strokeLinecap="round"
          strokeDasharray="2 3"
        />
        <rect
          x={padL} y={padT} width={innerW * 0.38} height={innerH}
          fill="url(#tv-skel-sweep)"
        >
          <animateTransform
            attributeName="transform"
            type="translate"
            from={`${-innerW * 0.4} 0`}
            to={`${innerW} 0`}
            dur="2.6s"
            repeatCount="indefinite"
          />
        </rect>
      </g>

      {/* top-left LED + status */}
      <g>
        <circle cx={padL + 8} cy={padT + 8} r="3.2" fill="#3b82f6">
          <animate attributeName="opacity" values="0.25;1;0.25" dur="1.4s" repeatCount="indefinite" />
        </circle>
        <text
          x={padL + 20} y={padT + 12}
          fill="#7d8590" fontSize="10"
          fontFamily="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
          letterSpacing="0.12em"
        >
          {lang === "zh" ? "STREAMING · 抓取链上记录 · 7D" : "STREAMING · ON-CHAIN HISTORY · 7D"}
        </text>
      </g>

      {/* bottom-right scanning index */}
      <g>
        <text
          x={w - padR - 6} y={h - padB + 14}
          fill="#5b6470" fontSize="9"
          fontFamily="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
          letterSpacing="0.10em" textAnchor="end"
        >
          {lang === "zh" ? "扫描中 · BSC 主网" : "SCANNING · BSC MAINNET"}
        </text>
      </g>
    </svg>
    </div>
  );
}

// ============ Volume bars ============
function VolumeBars({ candles, height = 56 }) {
  const w = 920, h = height;
  const padR = 64, padL = 12, padT = 4, padB = 4;
  const innerW = w - padL - padR, innerH = h - padT - padB;
  const max = Math.max(...candles.map(c => c.v)) || 1;
  const cw = innerW / candles.length;
  const bodyW = Math.max(2, cw * 0.66);
  return (
    <svg viewBox={`0 0 ${w} ${h}`} className="volume-chart">
      {candles.map((c, i) => {
        const up = c.c >= c.o;
        const x = padL + i * cw + cw / 2;
        const bh = (c.v / max) * innerH;
        return <rect key={i} x={x - bodyW / 2} y={h - padB - bh} width={bodyW} height={bh} fill={up ? "#0ecb81" : "#f6465d"} opacity="0.5" />;
      })}
    </svg>
  );
}

// ============ Live Trade Stream — REAL on-chain Buy/Sell events ============
function LiveTradeStream({ player, midPrice, trades }) {
  // `trades` is already loaded by TradeView from the curve's Buy/Sell events.
  // We format the rows here and listen for fresh events via getLogs polling.
  const feed = useMemo(() => {
    if (!trades || trades.length === 0) return [];
    // Most recent first, cap at 26
    return trades.slice().reverse().slice(0, 26).map(tr => ({
      side: tr.side,
      price: tr.p,
      qty: Math.round(tr.v / Math.max(tr.p, 1e-12)),
      wallet: tr.user || "",
      time: tr.t ? new Date(tr.t * 1000).toISOString().slice(11, 19) : "",
      block: tr.block,
    }));
  }, [trades]);

  return (
    <div className="rt">
      <div className="rt-head">
        <span>实时成交 · On-chain</span>
        <span className="live-dot" style={{display:'inline-block', marginLeft:8, verticalAlign:'middle'}}></span>
      </div>
      <div className="rt-cols dex">
        <span>价格</span>
        <span>数量</span>
        <span>钱包</span>
        <span>时间</span>
      </div>
      <div className="rt-rows">
        {feed.length === 0 && (
          <div className="rt-empty">尚无成交 · No trades yet</div>
        )}
        {feed.map((t, i) => (
          <div key={t.block + '-' + i} className={`rt-row dex ${t.side}`}>
            <span className="mono">{window.fmtUsdPrice ? window.fmtUsdPrice(t.price) : "$" + (t.price * 612).toFixed(4)}</span>
            <span className="mono">{t.qty.toLocaleString()}</span>
            <span className="mono dim">{t.wallet ? t.wallet.slice(0, 6) + "…" + t.wallet.slice(-4) : '—'}</span>
            <span className="mono dim">{t.time}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

// ============ Curve State Panel ============
function CurveStatePanel({ player, midPrice }) {
  const t = useT();
  // From PlayerCurve.sol: real silver reserve grows toward graduationThreshold (7.5 BNB)
  const realSilver = (player.progressBps / 10000) * 7.5;
  const virtualSilver = 3.0;
  const virtualToken  = 1_000_000;
  const tokensIssued  = virtualToken * (1 - virtualSilver / (virtualSilver + realSilver));

  return (
    <div className="curve-panel">
      <div className="cp-head">{t("cp.title")} · <span className="dim">PlayerCurve.sol</span></div>
      <div className="cp-grid">
        <div className="cp-stat">
          <div className="lab">{t("cp.realRes")}</div>
          <div className="val mono">{realSilver.toFixed(3)} BNB</div>
        </div>
        <div className="cp-stat">
          <div className="lab">{t("cp.virtRes")}</div>
          <div className="val mono dim">{virtualSilver.toFixed(2)} BNB</div>
        </div>
        <div className="cp-stat">
          <div className="lab">{t("cp.issued")}</div>
          <div className="val mono">{(tokensIssued / 1000).toFixed(1)}K</div>
        </div>
        <div className="cp-stat">
          <div className="lab">{t("cp.threshold")}</div>
          <div className="val mono">7.500 BNB</div>
        </div>
      </div>
      <div className="cp-bar-wrap">
        <div className="cp-bar-label">
          <span>{t("cp.progress")}</span>
          <span className="mono" style={{color:'#f0c245'}}>{(player.progressBps/100).toFixed(2)}%</span>
        </div>
        <div className="cp-bar"><div className="cp-fill" style={{width: (player.progressBps/100) + '%'}}></div></div>
      </div>
    </div>
  );
}

// ============ DEX Swap Panel ============
function SwapPanel({ player, midPrice, onTraded }) {
  const t = useT();
  const [side, setSide] = useState("buy");
  const [payAmount, setPayAmount] = useState("");
  const [slippage, setSlippage] = useState(0.5);
  const [pctSel, setPctSel] = useState(0);

  // Real on-chain state
  const [account, setAccount] = useState(null);
  // Track BigInt wei alongside formatted Number — MAX/% must use exact wei to
  // avoid `toFixed`-induced rounding that pushes parseEther past actual balance.
  const [silverBalanceWei, setSilverBalanceWei] = useState(0n);
  const [tokenBalanceWei, setTokenBalanceWei] = useState(0n);
  const [silverBalance, setSilverBalance] = useState(0);
  const [tokenBalance, setTokenBalance] = useState(0);
  // When a % button is clicked we capture the exact wei amount; cleared on manual edit.
  const [payWei, setPayWei] = useState(null);
  const [chainOut, setChainOut] = useState(0);
  const [busy, setBusy] = useState(false);
  const [status, setStatus] = useState("");

  const pay = parseFloat(payAmount) || 0;

  // Subscribe to wallet account changes
  useEffect(() => {
    if (!window.GB) return;
    const cur = window.GB.account();
    if (cur) setAccount(cur);
    const unsub = window.GB.onAccountChange((a) => setAccount(a));
    return unsub;
  }, []);

  // Poll balances when connected
  useEffect(() => {
    if (!account || !window.GB) {
      setSilverBalance(0); setSilverBalanceWei(0n);
      setTokenBalance(0); setTokenBalanceWei(0n);
      return;
    }
    let cancelled = false;
    const load = async () => {
      try {
        const v = window.GB.vault(false);
        const sb = await v.silverBalls(account);
        if (!cancelled) {
          setSilverBalanceWei(sb);
          setSilverBalance(Number(window.GB.formatEther(sb)));
        }
        if (player.token) {
          const tk = window.GB.playerToken(player.token, false);
          const tb = await tk.balanceOf(account);
          if (!cancelled) {
            setTokenBalanceWei(tb);
            setTokenBalance(Number(window.GB.formatEther(tb)));
          }
        }
      } catch (e) { /* noop */ }
    };
    load();
    const id = setInterval(load, 8000);
    return () => { cancelled = true; clearInterval(id); };
  }, [account, player.token]);

  // Quote receive amount from chain on input change
  useEffect(() => {
    if (!pay || !player.curve || !window.GB) { setChainOut(0); return; }
    let cancelled = false;
    const fetchQuote = async () => {
      try {
        const curve = window.GB.playerCurve(player.curve, false);
        // Prefer exact wei captured by MAX/% to match what the tx will send.
        const amtIn = payWei !== null ? payWei : window.GB.parseEther(String(pay));
        const q = side === 'buy' ? await curve.quoteBuy(amtIn) : await curve.quoteSell(amtIn);
        if (!cancelled) setChainOut(Number(window.GB.formatEther(q[0])));
      } catch (e) { if (!cancelled) setChainOut(0); }
    };
    const t = setTimeout(fetchQuote, 250);
    return () => { cancelled = true; clearTimeout(t); };
  }, [pay, side, player.curve, payWei]);

  // Bonding curve fallback math (when wallet not connected, no chain quote)
  const realSilver = (player.progressBps / 10000) * 7.5;
  const virtualSilver = 3.0;
  const virtualToken  = 1_000_000;
  const TAX_BPS = 500;

  let priceImpactPct = 0, rate = 0, fallbackReceive = 0;
  if (side === "buy" && pay > 0) {
    const burn = pay * TAX_BPS / 10000;
    const net  = pay - burn;
    const sReserve = virtualSilver + realSilver;
    const tReserve = virtualToken * (1 - tokensIssuedFromReserves(virtualSilver, virtualToken, realSilver));
    fallbackReceive = (tReserve * net) / (sReserve + net);
    const spotPrice = sReserve / tReserve;
    const execPrice = pay / Math.max(fallbackReceive, 1e-9);
    priceImpactPct = ((execPrice - spotPrice) / spotPrice) * 100;
  } else if (side === "sell" && pay > 0) {
    const sReserve = virtualSilver + realSilver;
    const tIssued = tokensIssuedFromReserves(virtualSilver, virtualToken, realSilver) * virtualToken;
    const tReserve = virtualToken - tIssued;
    const grossOut = (sReserve * pay) / (tReserve + pay);
    const burn = grossOut * TAX_BPS / 10000;
    fallbackReceive = grossOut - burn;
    const spotPrice = sReserve / tReserve;
    const execPrice = fallbackReceive / Math.max(pay, 1e-9);
    priceImpactPct = ((spotPrice - execPrice) / spotPrice) * 100;
  }

  // Prefer chain quote when available
  const receive = chainOut > 0 ? chainOut : fallbackReceive;
  rate = pay > 0 ? receive / pay : 0;
  const minReceive = receive * (1 - slippage / 100);
  const balance = side === "buy" ? silverBalance : tokenBalance;

  const doSwap = async () => {
    if (!window.GB) return;
    if (!account) {
      setBusy(true);
      setStatus(t("sp.connecting") || "Connecting...");
      try { await window.GB.connectWallet(); } catch (e) {}
      setBusy(false);
      setStatus("");
      return;
    }
    if (!pay || pay > balance) return;
    setBusy(true);
    try {
      const curve = window.GB.playerCurve(player.curve, true);
      // Use exact wei from MAX/% selection; fall back to parseEther for typed input.
      let amtIn = payWei !== null ? payWei : window.GB.parseEther(String(pay));
      // Safety clamp: never try to spend more than the user actually holds. This
      // catches any parseEther rounding (toFixed in display can round UP past
      // the raw wei balance) that would otherwise revert as "insufficient".
      const balWei = side === 'buy' ? silverBalanceWei : tokenBalanceWei;
      if (amtIn > balWei) amtIn = balWei;
      const minOut = window.GB.parseEther(String(Math.max(0, minReceive).toFixed(18)));
      if (side === 'sell') {
        // Approve player token if needed
        const tok = window.GB.playerToken(player.token, true);
        const allow = await tok.allowance(account, player.curve);
        if (allow < amtIn) {
          setStatus(t("sp.approving") || "Approving...");
          const ap = await tok.approve(player.curve, (1n << 256n) - 1n);
          await ap.wait();
        }
      }
      setStatus(t("sp.confirming") || "Confirm in wallet...");
      const tx = side === 'buy' ? await curve.buy(amtIn, minOut) : await curve.sell(amtIn, minOut);
      setStatus(t("sp.pending") || "Pending...");
      await tx.wait();
      setStatus(t("sp.done") || "Done!");
      setPayAmount("");
      setPayWei(null);
      setPctSel(0);
      // Bump parent's trade-history revision so the chart + live feed refresh.
      if (typeof onTraded === 'function') onTraded();
      setTimeout(() => setStatus(""), 2500);
    } catch (e) {
      console.error("Swap failed:", e);
      const msg = e?.shortMessage || e?.reason || e?.message || "Tx failed";
      setStatus("Error: " + String(msg).slice(0, 80));
      setTimeout(() => setStatus(""), 5000);
    } finally {
      setBusy(false);
    }
  };

  const setByPct = (p) => {
    setPctSel(p);
    const bw = side === 'buy' ? silverBalanceWei : tokenBalanceWei;
    if (bw === 0n) { setPayWei(0n); setPayAmount(""); return; }
    // BigInt math — no floating-point rounding can push us past balance.
    const wei = (bw * BigInt(p)) / 100n;
    setPayWei(wei);
    // Pretty-print: trim trailing zeros, cap decimals so the input isn't a wall of digits.
    const s = window.GB.formatEther(wei);
    const [intPart, decPart] = s.split('.');
    const maxDec = side === 'buy' ? 6 : 4;
    const trimmed = decPart ? decPart.slice(0, maxDec).replace(/0+$/, '') : '';
    setPayAmount(trimmed ? `${intPart}.${trimmed}` : intPart);
  };

  return (
    <div className="swap-panel">
      <div className="sp-head">
        <div className="sp-title">{t("sp.title")}</div>
        <div className="sp-slippage">
          <span className="dim">{t("sp.slippage")}</span>
          {[0.1, 0.5, 1.0].map(s => (
            <button key={s} className={slippage === s ? 'active' : ''} onClick={() => setSlippage(s)}>{s}%</button>
          ))}
        </div>
      </div>

      {/* From field */}
      <div className="sp-field">
        <div className="sp-field-top">
          <span className="dim">{t("sp.pay")}</span>
          <span className="dim mono">{t("sp.balance")} {(side === 'buy' ? silverBalance : tokenBalance).toFixed(side === 'buy' ? 2 : 2)}</span>
        </div>
        <div className="sp-field-mid">
          <input value={payAmount} onChange={e => { setPayAmount(e.target.value); setPayWei(null); setPctSel(0); }} placeholder="0.0" className="mono" />
          <div className="sp-asset">
            {side === 'buy' 
              ? <span className="sp-silver"><Ball variant="silver" size={20} /> SILVER</span>
              : <span className="sp-token"><Jersey kit={player.kit} number={player.number} size={22} /> ${player.symbol}</span>
            }
          </div>
        </div>
        <div className="sp-pcts">
          {[25, 50, 75, 100].map(p => (
            <button key={p} className={pctSel === p ? 'active' : ''} onClick={() => setByPct(p)}>
              {p === 100 ? 'MAX' : p + '%'}
            </button>
          ))}
        </div>
      </div>

      {/* Flip button */}
      <div className="sp-flip-row">
        <button className="sp-flip" onClick={() => { setSide(side === 'buy' ? 'sell' : 'buy'); setPayAmount(""); setPayWei(null); setPctSel(0); }}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M7 4v16M7 20l-4-4M7 4l-4 4M17 20V4M17 4l4 4M17 20l4-4"/></svg>
        </button>
      </div>

      {/* To field */}
      <div className="sp-field">
        <div className="sp-field-top">
          <span className="dim">{t("sp.receive")}</span>
          <span className="dim mono">{receive > 0 ? '≈ ' + (() => {
            // Both legs have approximately equal USD value (modulo 5% tax). The
            // SILVER leg has a fixed 1000:1 BNB ratio, so derive USD from there
            // — avoids any drift from midPrice / priceBNB unit inconsistencies.
            const silverSide = side === 'buy' ? pay : receive;
            const usd = silverSide * (window.BNB_USD || 612) / 1000;
            if (usd === 0) return "$0";
            if (usd < 0.01) return "$" + usd.toFixed(4);
            return "$" + usd.toFixed(2);
          })() : ''}</span>
        </div>
        <div className="sp-field-mid">
          <input value={receive ? receive.toFixed(side === 'buy' ? 2 : 2) : ""} readOnly placeholder="0.0" className="mono" />
          <div className="sp-asset">
            {side === 'buy'
              ? <span className="sp-token"><Jersey kit={player.kit} number={player.number} size={22} /> ${player.symbol}</span>
              : <span className="sp-silver"><Ball variant="silver" size={20} /> SILVER</span>
            }
          </div>
        </div>
      </div>

      {/* Quote details */}
      {pay > 0 && (
        <div className="sp-quote">
          <div className="sp-quote-row"><span>{t("sp.rate")}</span><span className="mono">1 {side === 'buy' ? 'SILVER' : '$' + player.symbol} = {rate.toFixed(side === 'buy' ? 2 : 6)} {side === 'buy' ? '$' + player.symbol : 'SILVER'}</span></div>
          <div className="sp-quote-row"><span>{t("sp.impact")}</span><span className={`mono ${priceImpactPct > 3 ? 'down' : ''}`} style={priceImpactPct <= 3 ? {color:'#0ecb81'} : {}}>{priceImpactPct.toFixed(2)}%</span></div>
          <div className="sp-quote-row"><span>{t("sp.minRecv")}</span><span className="mono">{minReceive.toFixed(2)} {side === 'buy' ? '$' + player.symbol : 'SILVER'}</span></div>
          <div className="sp-quote-row"><span>{t("sp.tax")}</span><span className="mono" style={{color:'var(--yellow)'}}>{t("sp.taxAction")}</span></div>
          <div className="sp-quote-row"><span>{t("sp.route")}</span><span className="mono dim">PlayerCurve → Vault</span></div>
        </div>
      )}

      <button
        className={`sp-submit ${side}`}
        disabled={busy || (account && (!pay || pay > balance))}
        onClick={doSwap}
      >
        {busy && status ? status :
         !account ? (t("sp.connect") || "Connect Wallet") :
         !pay ? t("sp.enterAmount") :
         pay > balance ? t("sp.insufficient") :
         side === 'buy' ? `${t("sp.buy")} $${player.symbol}` : `${t("sp.sell")} $${player.symbol}`}
      </button>
      {status && !busy && <div className="sp-status mono">{status}</div>}
    </div>
  );
}

// helper: tokensIssued fraction
function tokensIssuedFromReserves(vSilver, vToken, realSilver) {
  // tokens issued = vToken - vToken * (vSilver / (vSilver + realSilver))
  // returns fraction issued, 0..~0.95
  return realSilver / (vSilver + realSilver);
}

// ============ Player Info side panel ============
function PlayerInfoPanel({ player, isLeader }) {
  const t = useT();
  return (
    <div className="info-panel">
      <div className="info-row">
        <span>{t("ip.contract")}</span>
        <span className="mono dim">0x71f3...{(player.id * 137).toString(16).slice(-4)}A</span>
      </div>
      <div className="info-row">
        <span>{t("ip.goals")}</span>
        <span className="mono" style={{display:'inline-flex',alignItems:'center',gap:6,color: 'var(--yellow)', fontWeight: 700}}>{player.goals} <Ball variant="silver" size={14} /></span>
      </div>
      <div className="info-row">
        <span>{t("ip.rank")}</span>
        <span className="mono">#{[...window.PLAYERS].sort((a,b) => b.goals - a.goals).findIndex(p => p.id === player.id) + 1}</span>
      </div>
      <div className="info-row">
        <span>{t("ip.holders")}</span>
        <span className="mono">{player.holders.toLocaleString()}</span>
      </div>
      <div className="info-row">
        <span>{t("ip.mcap")}</span>
        <span className="mono">{player.marketCapBNB.toFixed(1)} BNB</span>
      </div>
      <div className="info-row">
        <span>{t("ip.prog")}</span>
        <span className="mono" style={{color: 'var(--yellow)'}}>{(player.progressBps / 100).toFixed(1)}%</span>
      </div>
      {isLeader && (
        <div className="info-mvp-tag">{t("ip.mvpNote")}</div>
      )}
    </div>
  );
}

// Convert PlayerCurve Buy/Sell events into time-bucketed OHLCV candles.
// Each event already carries the post-trade price implied by (silverIn/tokensOut)
// or (silverNet/tokensIn). We bucket by `bucketSec` seconds.
function buildCandles(trades, bucketSec, bucketCount) {
  if (!trades || trades.length === 0) return [];
  const now = Math.floor(Date.now() / 1000);
  const start = now - bucketSec * bucketCount;
  const buckets = new Array(bucketCount).fill(null);
  for (const tr of trades) {
    if (tr.t < start) continue;
    const idx = Math.min(bucketCount - 1, Math.floor((tr.t - start) / bucketSec));
    if (idx < 0) continue;
    if (!buckets[idx]) {
      buckets[idx] = { t: idx, o: tr.p, h: tr.p, l: tr.p, c: tr.p, v: tr.v };
    } else {
      const b = buckets[idx];
      b.c = tr.p;
      if (tr.p > b.h) b.h = tr.p;
      if (tr.p < b.l) b.l = tr.p;
      b.v += tr.v;
    }
  }
  // Forward-fill empty buckets with the previous close (flat line, no fake volume)
  let lastClose = trades[0].p;
  const out = [];
  for (let i = 0; i < bucketCount; i++) {
    if (buckets[i]) { out.push(buckets[i]); lastClose = buckets[i].c; }
    else            { out.push({ t: i, o: lastClose, h: lastClose, l: lastClose, c: lastClose, v: 0 }); }
  }
  return out;
}

// ============ Main Trade View ============
function TradeView({ player, onBack, lang, setLang }) {
  const t = useT();
  const [tf, setTf] = useState("1m");
  // Each timeframe = (seconds per bucket, number of buckets)
  const TF_CONFIG = {
    "1m":  { sec: 60,       count: 90 },
    "5m":  { sec: 5*60,     count: 90 },
    "15m": { sec: 15*60,    count: 90 },
    "1h":  { sec: 60*60,    count: 72 },
    "4h":  { sec: 4*60*60,  count: 60 },
    "1d":  { sec: 24*60*60, count: 30 },
    "1w":  { sec: 7*24*60*60, count: 16 },
  };
  const isGraduated = player.progressBps >= 10000;

  // Fetch real Buy/Sell events from PlayerCurve. Triggers on player.curve change
  // OR when a fresh swap is completed via the SwapPanel (tradesRev bumps).
  // trades[] is sorted by block; each entry: { t: blockTimestamp, p: priceBNB, v: volumeBNB }
  const [trades, setTrades] = useState([]);
  const [tradesLoading, setTradesLoading] = useState(true);
  const [tradesRev, setTradesRev] = useState(0);

  useEffect(() => {
    if (!window.GB || !player.curve) { setTrades([]); setTradesLoading(false); return; }
    let cancelled = false;
    setTradesLoading(true);
    (async () => {
      try {
        const curve = window.GB.playerCurve(player.curve, false);
        const provider = window.GB.readProvider();
        // Pull head block + its timestamp once. We approximate every event's
        // timestamp from its block number (BSC = 3s/block, deterministic), so
        // we don't have to round-trip getBlock for every event block — that
        // was the main latency hit, not getLogs.
        const head = await provider.getBlockNumber();
        const headBlock = await provider.getBlock(head);
        const headTs = headBlock ? Number(headBlock.timestamp) : Math.floor(Date.now() / 1000);
        // Pull last ~7 days on BSC (~201,600 blocks at 3s/block).
        const TOTAL_BLOCKS = 200_000;
        const from = Math.max(0, head - TOTAL_BLOCKS);
        let buyEvts = [], sellEvts = [];
        // Fast path — paid dRPC handles 200k blocks per getLogs. Try one shot.
        try {
          [buyEvts, sellEvts] = await Promise.all([
            curve.queryFilter(curve.filters.Buy(),  from, head),
            curve.queryFilter(curve.filters.Sell(), from, head),
          ]);
        } catch (bigErr) {
          // Fallback for capped public RPCs — chunk into 25k windows w/ 10 concurrent.
          console.warn("Big-range getLogs failed, falling back to chunked:", bigErr?.message);
          buyEvts = []; sellEvts = [];
          const CHUNK = 25_000;
          const POOL = 10;
          const chunks = [];
          for (let a = from; a <= head; a += CHUNK + 1) {
            chunks.push([a, Math.min(head, a + CHUNK)]);
          }
          for (let i = 0; i < chunks.length; i += POOL) {
            if (cancelled) return;
            const slice = chunks.slice(i, i + POOL);
            const results = await Promise.all(slice.flatMap(([a, b]) => [
              curve.queryFilter(curve.filters.Buy(),  a, b).catch(() => []),
              curve.queryFilter(curve.filters.Sell(), a, b).catch(() => []),
            ]));
            for (let j = 0; j < slice.length; j++) {
              buyEvts.push(...results[j * 2]);
              sellEvts.push(...results[j * 2 + 1]);
            }
          }
        }
        // Approximate block timestamps from BSC's ~3s block time — saves N
        // sequential getBlock RTTs (which dominated total load time).
        const blockTs = {};
        const allEvts = [...buyEvts, ...sellEvts];
        for (const e of allEvts) {
          if (blockTs[e.blockNumber] === undefined) {
            blockTs[e.blockNumber] = headTs - (head - e.blockNumber) * 3;
          }
        }
        const fe = window.GB.formatEther;
        // SILVER:BNB ratio is fixed at 1000:1 via the vault emission multiplier.
        // Convert silver amounts to BNB so `p` (price) and `v` (volume) live in
        // the same BNB-per-token unit system as player.priceBNB and fmtUsdPrice.
        const buys = buyEvts.map(e => {
          const silverInBNB = Number(fe(e.args.silverIn)) / 1000;
          const tokensOut = Number(fe(e.args.tokensOut));
          return tokensOut > 0 ? {
            t: blockTs[e.blockNumber], block: e.blockNumber,
            p: silverInBNB / tokensOut, v: silverInBNB, side: "buy",
            user: e.args.user,
          } : null;
        }).filter(Boolean);
        const sells = sellEvts.map(e => {
          const silverNetBNB = Number(fe(e.args.silverNet)) / 1000;
          const tokensIn = Number(fe(e.args.tokensIn));
          return tokensIn > 0 ? {
            t: blockTs[e.blockNumber], block: e.blockNumber,
            p: silverNetBNB / tokensIn, v: silverNetBNB, side: "sell",
            user: e.args.user,
          } : null;
        }).filter(Boolean);
        const merged = [...buys, ...sells].sort((a, b) => a.block - b.block);
        if (!cancelled) { setTrades(merged); setTradesLoading(false); }
      } catch (e) {
        console.error("Failed to load trade history:", e);
        if (!cancelled) { setTrades([]); setTradesLoading(false); }
      }
    })();
    return () => { cancelled = true; };
  }, [player.curve, tradesRev]);

  // Live push via WebSocket — Alchemy WSS streams every new Buy/Sell the
  // moment it's mined, so the chart and trade feed update in real-time without
  // burning RPC credits on polling. Fall back to a 30s poll if WS isn't available.
  useEffect(() => {
    if (!player.curve || !window.GB) return;
    const wsCurve = window.GB.playerCurveWs ? window.GB.playerCurveWs(player.curve) : null;
    if (!wsCurve) {
      const id = setInterval(() => setTradesRev(r => r + 1), 30000);
      return () => clearInterval(id);
    }
    const fe = window.GB.formatEther;
    const onBuy = (user, silverIn, tokensOut, burned, ev) => {
      const log = ev?.log || ev;
      const silverInBNB = Number(fe(silverIn)) / 1000;
      const tokensOutNum = Number(fe(tokensOut));
      if (tokensOutNum <= 0) return;
      const tr = {
        t: Math.floor(Date.now() / 1000),
        block: log?.blockNumber || 0,
        p: silverInBNB / tokensOutNum,
        v: silverInBNB,
        side: "buy",
        user,
      };
      setTrades(prev => prev.some(x => x.block === tr.block && x.user === tr.user && x.side === tr.side)
        ? prev
        : [...prev, tr].sort((a, b) => a.block - b.block));
    };
    const onSell = (user, tokensIn, silverNet, burned, ev) => {
      const log = ev?.log || ev;
      const silverNetBNB = Number(fe(silverNet)) / 1000;
      const tokensInNum = Number(fe(tokensIn));
      if (tokensInNum <= 0) return;
      const tr = {
        t: Math.floor(Date.now() / 1000),
        block: log?.blockNumber || 0,
        p: silverNetBNB / tokensInNum,
        v: silverNetBNB,
        side: "sell",
        user,
      };
      setTrades(prev => prev.some(x => x.block === tr.block && x.user === tr.user && x.side === tr.side)
        ? prev
        : [...prev, tr].sort((a, b) => a.block - b.block));
    };
    wsCurve.on("Buy", onBuy);
    wsCurve.on("Sell", onSell);
    return () => {
      try { wsCurve.off("Buy", onBuy); wsCurve.off("Sell", onSell); } catch (e) {}
    };
  }, [player.curve]);

  const config = TF_CONFIG[tf];
  const candles = useMemo(() => {
    const built = buildCandles(trades, config.sec, config.count);
    return built.length > 0 ? built : [{ t: 0, o: player.priceBNB || 0.00001, h: player.priceBNB || 0.00001, l: player.priceBNB || 0.00001, c: player.priceBNB || 0.00001, v: 0 }];
  }, [trades, config.sec, config.count, player.priceBNB]);

  // Live price: prefer real last-trade price, fall back to chain-derived price
  const [midPrice, setMidPrice] = useState(() => candles[candles.length - 1].c);
  const [delta, setDelta] = useState(0);

  useEffect(() => {
    setMidPrice(candles[candles.length - 1].c);
  }, [player.id, candles]);

  // Bump on every gb:stats24h event so the market list re-renders with the
  // fresh change% / volume values whenever loadStats24h finishes another curve.
  const [statsRev, setStatsRev] = useState(0);
  useEffect(() => {
    let pending = null;
    const onUpdate = () => {
      if (pending) return;
      pending = setTimeout(() => { pending = null; setStatsRev(r => r + 1); }, 200);
    };
    window.addEventListener('gb:stats24h', onUpdate);
    return () => {
      window.removeEventListener('gb:stats24h', onUpdate);
      if (pending) clearTimeout(pending);
    };
  }, []);

  const allPlayers = window.PLAYERS || [];
  const sortedByGoals = [...allPlayers].sort((a, b) => (b.goals || 0) - (a.goals || 0));
  const isLeader = sortedByGoals[0] && sortedByGoals[0].id === player.id;
  const rank = sortedByGoals.findIndex(p => p.id === player.id) + 1;

  // Market list tab — was non-functional cosmetic buttons; now drives sort/filter.
  const [mktTab, setMktTab] = useState("all");
  const marketRows = useMemo(() => {
    const get = (p) => (window.gbDeriveStats24h ? window.gbDeriveStats24h(p) : { change24: 0, volume24: 0 });
    const pr = (p) => p.priority || 0;
    let arr = allPlayers.map(p => ({ ...p, ...get(p) }));
    if (mktTab === "all") {
      arr.sort((a, b) => (b.goals - a.goals) || (pr(b) - pr(a)) || (a.id - b.id));
    } else if (mktTab === "hot") {
      arr.sort((a, b) => (b.volume24 - a.volume24) || (b.goals - a.goals) || (pr(b) - pr(a)));
    } else if (mktTab === "gainers") {
      arr.sort((a, b) => (b.change24 - a.change24) || (pr(b) - pr(a)));
    } else if (mktTab === "scored") {
      arr = arr.filter(p => (p.goals || 0) > 0).sort((a, b) => b.goals - a.goals);
    }
    return arr;
    // `statsRev` is included so this memo recomputes when fresh stats arrive.
  }, [allPlayers, mktTab, statsRev]);

  const change24h = candles[0].o > 0 ? ((midPrice - candles[0].o) / candles[0].o) * 100 : 0;
  const high24 = Math.max(...candles.map(c => c.h));
  const low24  = Math.min(...candles.map(c => c.l));
  const volume24 = candles.reduce((a, c) => a + c.v, 0);
  const hasTrades = trades.length > 0;

  return (
    <div className="trade-view">
      {/* Top bar — pair info */}
      <div className="tv-bar">
        <button className="tv-back" onClick={onBack}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M19 12H5M11 6l-6 6 6 6"/></svg>
          {t("tv.back")}
        </button>

        <div className="tv-pair">
          <Jersey kit={player.kit} number={player.number} size={48} />
          <div>
            <div className="tv-pair-name">
              ${player.symbol} <span className="dim">/ SILVER</span>
              {isLeader && <span className="tv-mvp">{t("tv.mvp")}</span>}
            </div>
            <div className="tv-pair-sub"><PlayerName p={player} both={false} /> · <CountryName p={player} /> · #{rank} {t("tv.race")}</div>
          </div>
        </div>

        <div style={{display:'flex', alignItems:'center', gap: 18}}>
          <div className="tv-stats">
          <div className="tv-stat">
            <div className="lab">{t("tv.lastPrice")}</div>
            <div className={`val mono ${delta >= 0 ? 'up' : 'down'}`}>{window.fmtUsdPrice ? window.fmtUsdPrice(midPrice) : "$" + (midPrice * 612).toFixed(4)}</div>
            <div className="sub mono dim">{midPrice < 0.0001 ? midPrice.toExponential(2) : midPrice.toFixed(6)} BNB</div>
          </div>
          <div className="tv-stat">
            <div className="lab">{t("tv.change24")}</div>
            <div className={`val mono ${change24h >= 0 ? 'up' : 'down'}`}>{change24h >= 0 ? '+' : ''}{change24h.toFixed(2)}%</div>
          </div>
          <div className="tv-stat">
            <div className="lab">{t("tv.high24")}</div>
            <div className="val mono">{window.fmtUsdPrice ? window.fmtUsdPrice(high24) : "$" + (high24 * 612).toFixed(4)}</div>
          </div>
          <div className="tv-stat">
            <div className="lab">{t("tv.low24")}</div>
            <div className="val mono">{window.fmtUsdPrice ? window.fmtUsdPrice(low24) : "$" + (low24 * 612).toFixed(4)}</div>
          </div>
          <div className="tv-stat">
            <div className="lab">{t("tv.vol24")}</div>
            <div className="val mono">{(volume24 / 10000).toFixed(2)}K BNB</div>
          </div>
          <div className="tv-stat">
            <div className="lab">{t("tv.goals")}</div>
            <div className="val mono" style={{color: 'var(--yellow)'}}>{player.goals}</div>
            {player.goalDelta > 0 && <div className="sub" style={{color: '#0ecb81'}}>+{player.goalDelta} {t("tv.thisMatch")}</div>}
          </div>
          </div>

          <div className="b-lang">
            <button className={lang === "zh" ? "active" : ""} onClick={() => setLang("zh")}>中</button>
            <button className={lang === "en" ? "active" : ""} onClick={() => setLang("en")}>EN</button>
          </div>
        </div>
      </div>

      {/* Main grid: chart + book + book-right column */}
      <div className="tv-grid">
        <div className="tv-chart-col">
          <div className="tv-card">
            <div className="tv-chart-head">
              <div className="tv-tf">
                {["1m","5m","15m","1h","4h","1d","1w"].map((tk) => (
                  <button key={tk} className={tk === tf ? 'active' : ''} onClick={() => setTf(tk)}>{tk}</button>
                ))}
              </div>
              <div style={{display:'flex', alignItems:'center', gap:12}}>
                {isGraduated
                  ? <span className="tv-curve-badge graduated">{t("tv.outer")}</span>
                  : <span className="tv-curve-badge">{t("tv.inner")}</span>
                }
                <span className="tv-indicators dim">
                  <span style={{color: 'var(--yellow)'}}>● MA(12) {candles[candles.length-1].c.toFixed(6)}</span>
                </span>
              </div>
            </div>
            {isGraduated ? (
              <iframe
                src={`https://dexscreener.com/bsc/${'0x' + player.symbol.toLowerCase().padEnd(40, '0').slice(0, 40)}?embed=1&theme=dark&trades=0&info=0`}
                style={{width:'100%', height:'400px', border:'none', background:'var(--bg)'}}
                title="DexScreener"
              />
            ) : tradesLoading ? (
              <div className="chart-frame" style={{position:'relative'}}>
                <CandleChartSkeleton height={320} lang={lang} />
              </div>
            ) : (
              <div className="chart-frame" style={{position:'relative'}}>
                <CandleChart candles={candles} height={320} />
                {hasTrades && <VolumeBars candles={candles} height={56} />}
                {!hasTrades && (
                  <div className="tv-chart-empty-tag mono">
                    <span className="tv-chart-empty-led"></span>
                    {lang === "zh" ? "首次成交待发生 · 当前 " : "AWAITING FIRST PRINT · "}
                    {window.fmtUsdPrice ? window.fmtUsdPrice(player.priceBNB) : "$" + (player.priceBNB * 612).toFixed(6)}
                  </div>
                )}
              </div>
            )}
          </div>

          <div className="tv-card">
            <CurveStatePanel player={player} midPrice={midPrice} />
          </div>
        </div>

        <div className="tv-side">
          <div className="tv-card">
            <SwapPanel player={player} midPrice={midPrice} onTraded={() => setTradesRev(r => r + 1)} />
          </div>
          <div className="tv-card" style={{flex: 1, minHeight: 0}}>
            <LiveTradeStream player={player} midPrice={midPrice} trades={trades} />
          </div>
          <div className="tv-card">
            <PlayerInfoPanel player={player} isLeader={isLeader} />
          </div>
        </div>
      </div>

      {/* Market list — all players */}
      <div className="tv-market">
        <div className="tv-market-head">
          <div className="tv-market-tabs">
            <button className={mktTab === "all" ? "active" : ""} onClick={() => setMktTab("all")}>全部市场</button>
            <button className={mktTab === "hot" ? "active" : ""} onClick={() => setMktTab("hot")}>🔥 热门</button>
            <button className={mktTab === "gainers" ? "active" : ""} onClick={() => setMktTab("gainers")}>📈 涨幅榜</button>
            <button className={mktTab === "scored" ? "active" : ""} onClick={() => setMktTab("scored")}><Ball variant="silver" size={12} /> 已进球</button>
          </div>
        </div>
        <table className="tv-market-table">
          <thead>
            <tr>
              <th>代币</th>
              <th>球员</th>
              <th>最新价</th>
              <th>24h 涨跌</th>
              <th>市值</th>
              <th>进球</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody>
            {marketRows.map((p) => {
              const ch = p.change24 || 0;
              const px = p.displayPrice || p.lastPrice || p.priceBNB;
              return (
                <tr key={p.id} className={p.id === player.id ? 'active' : ''} onClick={() => p.id !== player.id && onBack(p)}>
                  <td>
                    <div className="tv-pair-sm">
                      <Jersey kit={p.kit} number={p.number} size={28} />
                      <span className="mono">${p.symbol}</span>
                    </div>
                  </td>
                  <td>{p.name}</td>
                  <td className="mono">{window.fmtUsdPrice ? window.fmtUsdPrice(px) : "$" + (px * 612).toFixed(4)}</td>
                  <td className="mono" style={{color: ch > 0 ? '#0ecb81' : ch < 0 ? '#f6465d' : '#7d8590'}}>
                    {ch > 0 ? '+' : ''}{ch.toFixed(2)}%
                  </td>
                  <td className="mono">{p.marketCapBNB.toFixed(1)} BNB</td>
                  <td><span className="mono" style={{color: 'var(--gold)', fontWeight: 700}}>{p.goals}</span></td>
                  <td><button className="tv-trade-btn">交易</button></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

window.TradeView = TradeView;
