/* Robindex v2 — Wallet (credits · top-up · ledger), Earnings (reward streams ·
   owner revenue · withdrawal), Claim flow (X verify + owner agreement). */
const { useState: wS, useEffect: wEffect } = React;

/* ---------------- Top-up modal ---------------- */
/* Spec: real async window.RXAPI.openTopup(amount) → { ok, redirected, url }
   | { error }. Amount $5–$10,000. On success the browser is redirected to
   checkout_url — never show a fake local credit arrival, never mutate balance.
   Errors: error/no_checkout_url/redirect_failed/network_error/unauthorized/
   account_banned surfaced via role=alert in zh/en. Submit-guards busy even
   when the call throws. Keep server-aligned channel-fee preview + non-refundable notice. */
function TopupModal({ suggest, onClose }) {
  const { Icon } = window.RXC;
  const E = window.RXE, C = E.cfg();
  const L = (en, zh) => (window.RXI.lang === "zh" ? zh : en);
  const MIN = 5, MAX = 10000;

  const [tab, setTab] = wS("card"); // "card" or "usdc"

  // Card Payment tab states
  const [amt, setAmt] = wS(suggest ? Math.max(MIN, Math.ceil(suggest)) : 25);
  const [busy, setBusy] = wS(false);
  const [err, setErr] = wS(null);
  const presets = [10, 25, 50, 100];
  const fee = E.chFee(amt || 0);
  const valid = amt >= MIN && amt <= MAX;
  const go = async () => {
    if (!valid || busy) return;
    setBusy(true); setErr(null);
    let redirected = false;
    try {
      const r = await window.RXAPI.openTopup(Number(amt));
      if (r && r.error) { setErr(r.error); return; }
      if (!r || !r.ok || !r.redirected) { setErr("no_checkout_url"); return; }
      // openTopup owns location.assign; reaching this line means it accepted the redirect.
      redirected = true;
    } catch (_) {
      setErr("network_error");
    } finally {
      if (!redirected) setBusy(false);
    }
  };
  const errText = err && (() => {
    const zh = window.RXI.lang === "zh";
    const m = {
      unauthorized: zh ? "未登录，请先登录" : "Not signed in",
      account_banned: zh ? "账户已被冻结" : "Account is frozen",
      no_checkout_url: zh ? "未能创建支付链接，请稍后重试" : "Could not create checkout URL, please retry",
      redirect_failed: zh ? "跳转支付页面失败，请重试" : "Failed to redirect to checkout, please retry",
      network_error: zh ? "网络错误，请重试" : "Network error, please retry",
    };
    return m[err] || (zh ? "支付发起失败，请重试" : "Failed to start payment, please retry");
  })();

  // USDC tab states
  const [usdcAddr, setUsdcAddr] = wS(null);
  const [usdcLoading, setUsdcLoading] = wS(false);
  const [usdcError, setUsdcError] = wS(null);
  const [copyFeedback, setCopyFeedback] = wS("");
  const [copied, setCopied] = wS(false);

  wEffect(() => {
    if (tab !== "usdc" || usdcAddr || usdcLoading || usdcError) return;
    let active = true;
    (async () => {
      setUsdcLoading(true);
      setUsdcError(null);
      try {
        const r = await window.RXAPI.usdcAddress();
        if (!active) return;
        if (r && r.error) {
          setUsdcError(r.error);
        } else if (r && r.address) {
          setUsdcAddr(r.address);
        } else {
          setUsdcError("request_failed");
        }
      } catch (e) {
        if (active) setUsdcError(e);
      } finally {
        if (active) setUsdcLoading(false);
      }
    })();
    return () => { active = false; };
  }, [tab, usdcAddr, usdcLoading, usdcError]);

  const doCopy = async () => {
    if (!usdcAddr) return;
    try {
      await navigator.clipboard.writeText(usdcAddr);
      setCopyFeedback(L("Address copied to clipboard", "地址已复制到剪贴板"));
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (_) {
      setCopyFeedback(L("Failed to copy address", "复制地址失败"));
    }
  };

  const makeQrSvg = (address) => {
    if (!address) return "";
    try {
      const qr = window.qrcode(0, 'M');
      qr.addData(address);
      qr.make();
      return qr.createSvgTag({ cellSize: 4, margin: 2 });
    } catch (e) {
      console.error("QR generation failed", e);
      return "";
    }
  };

  return (
    <Modal2 title={T2("wTopup")} icon="wallet" onClose={onClose}>
      <div className="mkt-filters" style={{ marginBottom: 12 }}>
        <button className={"mkt-f" + (tab === "card" ? " on" : "")} onClick={() => setTab("card")}>{T2("wTabCard")}</button>
        <button className={"mkt-f" + (tab === "usdc" ? " on" : "")} onClick={() => setTab("usdc")}>{T2("wTabUSDC")}</button>
      </div>

      {tab === "card" && (
        <>
          <div className="x2-amounts">
            {presets.map((p) => (
              <button key={p} className={"x2-amt" + (amt === p ? " on" : "")} onClick={() => setAmt(p)}>${p}</button>
            ))}
          </div>
          <div className="x2-custom">
            <span style={{ color: "var(--faint)", font: "600 13px var(--mono)" }}>$</span>
            <input type="number" min={MIN} max={MAX} value={amt} onChange={(e) => setAmt(Number(e.target.value))} />
            <span style={{ color: "var(--faint)", font: "400 11px var(--sans)" }}>{T2("wAmount")} · $5–$10,000</span>
          </div>
          <div className="fee-rows" style={{ marginTop: 6 }}>
            <FeeRow subrow title="Credits" value={E.usd(amt || 0)} />
            <FeeRow title={T2("wChannelFee")} sub={T2("crChannelSub")(C.feePct, C.feeFixed)} value={E.usd(fee)} />
            <FeeRow total title={T2("wYouPay")} value={E.usd((amt || 0) + fee)} />
          </div>
          <div className="x2-note x2-danger"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{T2("wNoRefund")}</div>
          {errText && (
            <div className="x2-note x2-danger" role="alert"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{errText}</div>
          )}
          <button className="x2-primary" disabled={!valid || busy} onClick={go}><Icon name="zap" size={14} />{busy ? L("Submitting…", "提交中…") : `${T2("wYouPay")} ${E.usd((amt || 0) + fee)} · ${T2("wPayProvider")}`}</button>
        </>
      )}

      {tab === "usdc" && (
        <>
          {usdcLoading && (
            <div role="status" className="usdc-loading" style={{ padding: "30px 18px", textAlign: "center", color: "var(--dim)" }}>
              <Icon name="refresh" size={16} style={{ animation: "spin 1.5s linear infinite", marginRight: 8, verticalAlign: "middle" }} />
              <span>{T2("wUsdcLoading")}</span>
            </div>
          )}

          {usdcError && (
            <div role="alert" className="usdc-error" style={{ display: "flex", flexDirection: "column", gap: 10, marginTop: 6 }}>
              <div className="x2-note x2-danger" style={{ display: "flex", gap: 8, alignItems: "flex-start", width: "100%" }}>
                <Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />
                <span>{T2("wUsdcUnavailable")}</span>
              </div>
            </div>
          )}

          {!usdcLoading && !usdcError && usdcAddr && (() => {
            const prefix = usdcAddr.slice(0, 6);
            const middle = usdcAddr.slice(6, -4);
            const suffix = usdcAddr.slice(-4);
            return (
              <div className="usdc-content" style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 10 }}>
                {/* 二维码 */}
                <div
                  className="usdc-qr-container"
                  style={{
                    display: "flex",
                    justifyContent: "center",
                    padding: 8,
                    background: "#ffffff",
                    borderRadius: "12px",
                    width: "fit-content",
                    margin: "4px auto 8px",
                    boxShadow: "0 2px 8px rgba(0,0,0,0.06)",
                  }}
                  dangerouslySetInnerHTML={{ __html: makeQrSvg(usdcAddr) }}
                />

                {/* 完整地址与复制 */}
                <div
                  className="x2-custom usdc-address-box"
                  style={{
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "space-between",
                    fontFamily: "var(--mono)",
                    background: "var(--panel-2)",
                    border: "1px solid var(--border)",
                    borderRadius: "var(--r)",
                    padding: "10px 12px",
                  }}
                >
                  <span
                    className="usdc-address-text"
                    aria-label={L(`Address starts with ${prefix} and ends with ${suffix}`, `地址开头是 ${prefix} 结尾是 ${suffix}`)}
                    style={{ userSelect: "all", display: "inline-flex", alignItems: "baseline", letterSpacing: "0.02em", overflow: "visible", whiteSpace: "nowrap" }}
                  >
                    <span className="addr-prefix" style={{ fontSize: "16px", fontWeight: "bold", color: "var(--accent)" }}>{prefix}</span>
                    <span className="addr-middle" style={{ fontSize: "13px", color: "var(--faint)" }}>{middle}</span>
                    <span className="addr-suffix" style={{ fontSize: "16px", fontWeight: "bold", color: "var(--accent)" }}>{suffix}</span>
                  </span>
                  <button onClick={doCopy} className="btn-ghost usdc-copy-btn" style={{ padding: "6px 12px", marginLeft: 8, flexShrink: 0, fontSize: "12px" }}>
                    {copied ? L("Copied!", "已复制!") : L("Copy", "复制")}
                  </button>
                </div>

                {/* aria-live feedback */}
                <div role="status" aria-live="polite" style={{ position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0, 0, 0, 0)", border: 0 }}>
                  {copyFeedback}
                </div>

                {/* 警示块 */}
                <div className="x2-note x2-danger usdc-warning-box" style={{ display: "flex", gap: 8, alignItems: "flex-start", marginTop: 0 }}>
                  <Icon name="shield" size={13} style={{ flex: "none", marginTop: 1, color: "var(--down)" }} />
                  <span className="usdc-warning-text">{T2("wUsdcWarning")}</span>
                </div>

                {/* 费率文案 */}
                <div className="usdc-fee-text" style={{ font: "500 13px var(--sans)", color: "var(--text)", display: "flex", alignItems: "center", gap: 6 }}>
                  <Icon name="zap" size={12} color="var(--accent)" />
                  <span>{T2("wUsdcZeroFee")}</span>
                </div>

                {/* 最低建议额 minSuggested */}
                <div className="usdc-min-suggested" style={{ font: "400 12px var(--sans)", color: "var(--faint)" }}>
                  {T2("wUsdcMinSuggested")(5)}
                </div>
              </div>
            );
          })()}
        </>
      )}
    </Modal2>
  );
}

/* ---------------- ledger ---------------- */
/* Wallet & earnings display is fully driven by window.RXAPI (balances, ledger,
   streams, owned, withdraw, top-up, convert) — no mock fallback, no RXS or
   useStore2 reads. The KOL identifier on a ledger row is a handle-like id
   (see app/src/clone.ts payClone: kolId = handle, e.g. 'aleabitoreddit'),
   so we render it directly as @handle without any registry lookup. */
function LedgerRow({ e, lang }) {
  const { Icon } = window.RXC;
  const E = window.RXE;
  const nm = e.kol ? (String(e.kol).startsWith("@") ? e.kol : "@" + e.kol) : "";
  let icon = "zap", title = "", sub = "", tag = null, pos = e.amt > 0;
  if (e.type === "topup") { icon = "wallet"; title = T2("lgTopup"); sub = T2("wYouPay") + " " + E.usd(e.paid) + " · " + T2("wChannelFee") + " " + E.usd(e.fee); }
  else if (e.type === "gift") { icon = "sparkles"; title = (e.note === "distill" ? T2("lgGift") : T2("lgGiftUpd")) + " · " + nm; sub = T2("lgFrom")(e.who); tag = "gift"; }
  else if (e.type === "owner") { icon = "crown"; title = T2("lgOwner") + " · " + nm; sub = T2("lgFrom")(e.who) + " · " + T2("lgGross")(e.gross); tag = "gift"; }
  else if (e.type === "call") { icon = "quote"; title = T2("lgCall") + " · " + nm; sub = (e.q || "") + (e.byok ? " · " + T2("lgByok") : ""); }
  else if (e.type === "create") { icon = "bot"; title = T2("lgCreate") + " · " + nm; sub = T2("lgBalanceNoFee"); }
  else if (e.type === "update") { icon = "refresh"; title = T2("lgUpdate") + " · " + nm; sub = (e.refresh ? "persona refresh · " : "") + T2("lgBalanceNoFee"); }
  else if (e.type === "withdraw") { icon = "send"; title = T2("lgWithdraw"); sub = T2("eYouReceive") + " " + E.usd(e.received) + " · fee " + E.usd(e.fee) + " · " + (e.status || "paid"); }
  return (
    <div className="lg-row">
      <div className="lg-ic"><Icon name={icon} size={14} color={pos ? "var(--accent)" : "var(--dim)"} /></div>
      <div className="lg-main">
        <div className="lg-t">{title}{tag && <span className="lg-tag gift">{lang === "zh" ? "赠金" : "GIFT"}</span>}</div>
        <div className="lg-s">{sub}</div>
        {e.parts && (
          <div className="lg-parts">
            {e.parts.token > 0 && <span>{T2("chTok")} {E.usd(e.parts.token, 3)}</span>}
            {e.parts.kolFee > 0 && <span>{T2("chKol")} {E.usd(e.parts.kolFee)}</span>}
            {e.parts.distill > 0 && <span>{T2("chDistill")} {E.usd(e.parts.distill)}</span>}
            {e.parts.update > 0 && <span>{T2("chUpdate")} {E.usd(e.parts.update)}</span>}
            {e.tokIn && <span>↓{Math.round(e.tokIn / 100) / 10}K ↑{Math.round(e.tokOut / 100) / 10}K tok</span>}
          </div>
        )}
      </div>
      <div style={{ textAlign: "right" }}>
        <div className={"lg-amt " + (pos ? "pos" : "neg")}>{pos ? "+" : ""}{E.usd(e.amt, Math.abs(e.amt) < 0.1 ? 3 : 2)}</div>
        <div className="lg-ts">{E.fmtDT(e.ts).slice(5)}</div>
      </div>
    </div>
  );
}

/* ---------------- Wallet page ---------------- */
function WalletPage2({ lang, onTopup }) {
  const { Icon } = window.RXC;
  const E = window.RXE;
  const [bal, setBal] = wS({ cash: 0, reward: 0, owner: 0, ownerGross: 0, freeAskRemaining: 0 });
  const [referral, setReferral] = wS(null);
  const [copyState, setCopyState] = wS(null);
  const [rows, setRows] = wS([]);
  const [loading, setLoading] = wS(true);
  const [err, setErr] = wS(null);
  const [tick, setTick] = wS(0);
  const [f, setF] = wS("all");
  const filters = [["all", T2("wAll")], ["call", T2("wCalls")], ["gift", T2("wRewards")], ["topup", T2("wTopups")]];

  // adapter: backend row → LedgerRow fields
  const adaptRow = (r) => ({
    id: r.id,
    ts: r.ts,
    type: r.type,
    amt: r.amount,
    kol: r.kol_id,
    who: r.counterparty,
    model: r.model,
    tokIn: r.tok_in,
    tokOut: r.tok_out,
    parts: r.parts ? {
      token: r.parts.token,
      kolFee: r.parts.owner,
      distill: r.parts.distill,
      update: r.parts.update,
    } : undefined,
    paid: r.parts && r.parts.paid,
    fee: r.parts && r.parts.fee,
    received: r.parts && r.parts.received,
    payoutMethod: r.parts && r.parts.payoutMethod,
    note: r.note,
    ref: r.ref,
  });

  wEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      setErr(null);
      try {
        const [res, refRes] = await Promise.all([
          window.RXAPI.wallet({ limit: 60 }),
          window.RXAPI.referralInfo(),
        ]);
        if (cancelled) return;
        if (res && res.error) { setErr(res.error); return; }
        const b = (res && res.balances) || {};
        const lr = (res && res.ledger && res.ledger.rows) || [];
        setBal({
          cash: b.cash || 0,
          reward: b.reward || 0,
          owner: b.owner || 0,
          ownerGross: b.ownerGross || 0,
          freeAskRemaining: b.freeAskRemaining || 0,
        });
        if (refRes && !refRes.error) setReferral(refRes);
        setRows(lr.map(adaptRow));
      } catch (_) {
        if (!cancelled) setErr("network_error");
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lang, tick]);

  const list = rows.filter((e) => {
    if (f === "call") return e.type === "call" || e.type === "create" || e.type === "update";
    if (f === "gift") return e.type === "gift" || e.type === "owner";
    if (f === "topup") return e.type === "topup" || e.type === "withdraw";
    return true;
  }).slice(0, 60);

  const zh = lang === "zh";
  const loadingText = zh ? "加载中…" : "Loading…";
  const errText = err === "network_error"
    ? (zh ? "网络错误，请重试" : "Network error, please retry")
    : (zh ? "加载失败，请重试" : "Failed to load, please retry");
  const retryLabel = zh ? "重试" : "Retry";
  const inviteLink = referral && referral.referral_link
    ? referral.referral_link + "&utm_source=referral&utm_medium=product&utm_campaign=invite"
    : "";
  const copyInvite = async () => {
    if (!inviteLink) return;
    let ok = false;
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(inviteLink); ok = true;
      } else {
        const ta = document.createElement("textarea"); ta.value = inviteLink;
        ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta);
        ta.select(); ok = document.execCommand("copy"); document.body.removeChild(ta);
      }
    } catch (_) {}
    setCopyState(ok ? "ok" : "fail");
    if (ok && window.RXA) window.RXA.track("referral_link_copied", { source: "wallet" });
    setTimeout(() => setCopyState(null), 2200);
  };
  const shareInvite = () => {
    if (!inviteLink) return;
    const text = zh
      ? "我在 Robindex 用带原文出处的 AI 分身做研究。注册后每天可免费咨询，完成首次充值后我们双方都会永久增加每日免费次数。"
      : "I use Robindex AI twins for sourced research. Join for daily free asks; after your first top-up, we both permanently unlock an extra free ask each day.";
    if (window.RXA) window.RXA.track("referral_share_clicked", { source: "wallet", medium: "x" });
    window.open("https://x.com/intent/post?text=" + encodeURIComponent(text + "\n\n" + inviteLink), "_blank", "noopener,noreferrer");
  };

  return (
    <div className="v2-page" data-screen-label="wallet">
      <div className="v2-wrap narrow">
        <div className="v2-head"><h1>{T2("wTitle")}</h1><p>{T2("copyFee")(E.cfg().feePct, E.cfg().feeFixed)}</p></div>
        <div className="w-grid">
          <div className="v2-card w-stat">
            <div className="k"><Icon name="wallet" size={12} />{T2("wCredits")}</div>
            <div className="v">{E.usd(bal.cash)}</div>
            <div className="w-actions"><button className="btn-ask" style={{ flex: "none", padding: "8px 16px" }} onClick={onTopup}><Icon name="plus" size={12} />{T2("wTopup")}</button></div>
          </div>
          <div className="v2-card w-stat">
            <div className="k"><Icon name="sparkles" size={12} />{T2("wGift")}</div>
            <div className="v" style={{ color: "var(--accent)" }}>{E.usd(bal.reward)}</div>
            <div className="s">{T2("wGiftSub")}</div>
          </div>
        </div>
        <div className="v2-card" style={{ padding: 18, marginTop: 12 }}>
          <div style={{ display: "flex", gap: 12, alignItems: "flex-start", flexWrap: "wrap" }}>
            <div style={{ flex: "1 1 260px" }}>
              <div className="k" style={{ color: "var(--accent)", fontWeight: 700 }}><Icon name="gift" size={13} />{zh ? "邀请好友，双方增加每日免费咨询" : "Invite friends — both unlock more daily free asks"}</div>
              <div style={{ marginTop: 7, color: "var(--dim)", font: "400 12.5px/1.55 var(--sans)" }}>
                {zh ? "好友通过你的链接注册并首次充值至少 $5 后，你和好友的每日免费 Flash 咨询额度都永久 +1，最高每天 10 次。" : "When a friend joins through your link and first tops up at least $5, both of you permanently gain +1 daily Flash ask, up to 10/day."}
              </div>
              <div style={{ marginTop: 8, color: "var(--text)", font: "600 12px var(--mono)" }}>
                {zh ? "今日剩余" : "Remaining today"}: {bal.freeAskRemaining} · {zh ? "已转化" : "Converted"}: {referral ? referral.converted_count : 0}
              </div>
            </div>
            <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
              <button className="btn-ask" onClick={copyInvite} disabled={!inviteLink}><Icon name="copy" size={12} />{copyState === "ok" ? (zh ? "已复制" : "Copied") : copyState === "fail" ? (zh ? "复制失败" : "Copy failed") : (zh ? "复制邀请链接" : "Copy invite link")}</button>
              <button className="btn-ghost" onClick={shareInvite} disabled={!inviteLink}><Icon name="xLogo" size={12} />{zh ? "分享到 X" : "Share on X"}</button>
            </div>
          </div>
        </div>
        <div className="v2-sec">{T2("wLedger")}</div>
        <div className="mkt-filters" style={{ marginBottom: 8 }}>
          {filters.map(([id, lb]) => <button key={id} className={"mkt-f" + (f === id ? " on" : "")} onClick={() => setF(id)}>{lb}</button>)}
        </div>
        {err ? (
          <div className="v2-card" style={{ padding: 18 }}>
            <div className="x2-note x2-danger"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{errText}</div>
            <button className="btn-ghost" style={{ marginTop: 10 }} onClick={() => setTick((t) => t + 1)}><Icon name="refresh" size={12} />{retryLabel}</button>
          </div>
        ) : loading ? (
          <div className="v2-card" style={{ padding: 18, textAlign: "center", color: "var(--dim)" }}>{loadingText}</div>
        ) : (
          <div className="v2-card" style={{ padding: "2px 14px" }}>
            <div className="ledger">{list.map((e) => <LedgerRow key={e.id} e={e} lang={lang} />)}</div>
          </div>
        )}
      </div>
    </div>
  );
}

/* ---------------- Earnings page ---------------- */
/* Spec: on mount/lang change fetch Promise.all([RXAPI.wallet({limit:1}),
   RXAPI.earnings()]). wallet.balances={cash,reward,owner,ownerGross};
   earnings={streams:[{kind,kolId,handle,displayName,status,cost,cap,earned,
   active}], owned:[{kolId,handle,displayName,claimedAt,ownerFee,calls}]}.
   Render streams progress + owned names + real owner/ownerGross. No owned →
   claim CTA. Withdraw gated on real owner < cfg.minWithdraw. Bilingual
   loading(role=status)/error(role=alert)+retry. No setState after unmount.
   Never read RXS/useStore2 accounts or registry; no mock fallback. */
function EarningsPage({ lang, onWithdraw, onClaimOwn }) {
  const { Icon, Avatar } = window.RXC;
  const E = window.RXE, C = E.cfg();
  const [streams, setStreams] = wS([]);
  const [owned, setOwned] = wS([]);
  const [bal, setBal] = wS({ owner: 0, ownerGross: 0 });
  const [loading, setLoading] = wS(true);
  const [err, setErr] = wS(null);
  const [tick, setTick] = wS(0);
  // Per-owned-KOL ownerFee editing state. feeBusy/feeErr keyed by kolId.
  const [feeBusy, setFeeBusy] = wS({});
  const [feeErr, setFeeErr] = wS({});
  // Privy bridge for 401 → clear key (sign out). EarningsPage mounts inside PrivyProvider.
  const privy = window.PrivySDK && window.PrivySDK.usePrivy ? window.PrivySDK.usePrivy() : null;

  wEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      setErr(null);
      try {
        const [wres, eres] = await Promise.all([
          window.RXAPI.wallet({ limit: 1 }),
          window.RXAPI.earnings(),
        ]);
        if (cancelled) return;
        if (wres && wres.error) { setErr(wres.error); return; }
        if (eres && eres.error) { setErr(eres.error); return; }
        const b = (wres && wres.balances) || {};
        setBal({
          owner: b.owner || 0,
          ownerGross: b.ownerGross || 0,
        });
        setStreams((eres && eres.streams) || []);
        setOwned((eres && eres.owned) || []);
      } catch (_) {
        if (!cancelled) setErr("network_error");
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lang, tick]);

  const zh = lang === "zh";
  const loadingText = zh ? "加载中…" : "Loading…";
  const errText = err === "network_error"
    ? (zh ? "网络错误，请重试" : "Network error, please retry")
    : (zh ? "加载失败，请重试" : "Failed to load, please retry");
  const retryLabel = zh ? "重试" : "Retry";
  const L = (en, zhT) => (zh ? zhT : en);
  const feeOpts = [0, 0.01, 0.03, 0.05, 0.10];

  // Async set owner fee for one owned KOL. Per-row busy prevents re-entry;
  // success updates local owned.ownerFee and dispatches rx2:refresh so other
  // pages (chat billing, market) re-pull. 401/unauthorized → privy.logout
  // (clears key). Failure surfaced as bilingual role=alert on that row.
  const setFee = async (kolId, fee) => {
    if (feeBusy[kolId]) return;
    setFeeBusy((m) => ({ ...m, [kolId]: true }));
    setFeeErr((m) => { const n = { ...m }; delete n[kolId]; return n; });
    try {
      const r = await window.RXAPI.setOwnerFee(kolId, fee);
      if (r && (r.status === 401 || r.error === "unauthorized")) {
        if (privy && typeof privy.logout === "function") {
          try { privy.logout(); } catch (_) {}
        }
        setFeeErr((m) => ({ ...m, [kolId]: L("Session expired — please sign in again.", "登录已过期，请重新登录。") }));
        return;
      }
      if (r && r.error) {
        setFeeErr((m) => ({ ...m, [kolId]: L("Update failed (" + r.error + ") — please retry.", "更新失败（" + r.error + "），请重试。") }));
        return;
      }
      if (r && r.ok) {
        setOwned((arr) => arr.map((o) => o.kolId === kolId ? { ...o, ownerFee: fee } : o));
        window.dispatchEvent(new CustomEvent("rx2:refresh"));
        return;
      }
      setFeeErr((m) => ({ ...m, [kolId]: L("No response from server.", "服务器无响应。") }));
    } catch (_) {
      setFeeErr((m) => ({ ...m, [kolId]: L("Network error — please retry.", "网络错误，请重试。") }));
    } finally {
      setFeeBusy((m) => ({ ...m, [kolId]: false }));
    }
  };

  const avatarFor = (s) => ({
    id: s.kolId,
    handle: s.handle,
    display_name: s.displayName,
    avatar_url: "",
    accent: "#5B9DFF",
  });

  return (
    <div className="v2-page" data-screen-label="earnings">
      <div className="v2-wrap narrow">
        <div className="v2-head"><h1>{T2("eTitle")}</h1><p>{T2("crRewardBody")(C.perCallFee, C.rewardMult)}</p></div>
        {err ? (
          <div className="v2-card" style={{ padding: 18 }}>
            <div className="x2-note x2-danger" role="alert"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{errText}</div>
            <button className="btn-ghost" style={{ marginTop: 10 }} onClick={() => setTick((t) => t + 1)}><Icon name="refresh" size={12} />{retryLabel}</button>
          </div>
        ) : loading ? (
          <div className="v2-card" style={{ padding: 18, textAlign: "center", color: "var(--dim)" }} role="status">{loadingText}</div>
        ) : (
          <>
            <div className="v2-sec"><Icon name="sparkles" size={12} />{T2("eRewards")} · {T2("eRewardCap")(C.rewardMult)}</div>
            {streams.map((s) => {
              const pct = s.cap > 0 ? Math.min(100, Math.round((s.earned / s.cap) * 100)) : 0;
              const done = s.earned >= s.cap;
              return (
                <div className="v2-card stream" key={s.kolId + s.kind} style={{ marginBottom: 10 }}>
                  <div className="stream-top">
                    <Avatar kol={avatarFor(s)} size={30} radius={9} />
                    <span className="nm">@{s.handle}</span>
                    <span className="lg-tag gift">{s.kind === "distill" ? T2("lgGift") : T2("lgGiftUpd")}</span>
                    <span className="st" style={{ color: s.status === "distilling" ? "var(--amber)" : done ? "var(--up)" : "var(--accent)" }}>
                      {s.status === "distilling" ? T2("distillingTag") : done ? T2("eStreamDone") : "+" + E.usd(C.perCallFee) + " / call"}
                    </span>
                  </div>
                  <div className="stream-nums">
                    <span className="cur">{E.usd(s.earned)}</span>
                    <span className="cap">/ {E.usd(s.cap)} · {T2("eProgress")}</span>
                    <span className="pct">{pct}%</span>
                  </div>
                  <div className={"rw-bar" + (done ? " done" : "")}><i style={{ width: pct + "%" }}></i></div>
                </div>
              );
            })}
            <div className="v2-sec"><Icon name="crown" size={12} />{T2("eOwner")}</div>
            {owned.length === 0 ? (
              <div className="v2-card" style={{ padding: 18, display: "flex", gap: 12, alignItems: "center" }}>
                <Icon name="xLogo" size={16} color="var(--faint)" />
                <span style={{ font: "400 13px var(--sans)", color: "var(--dim)", flex: 1 }}>{T2("eNoOwner")}</span>
                <button className="btn-ghost" onClick={onClaimOwn}>{T2("clVerify")}</button>
              </div>
            ) : (
              <>
                <div className="v2-card w-stat">
                  <div className="k"><Icon name="crown" size={12} />{owned.map((r) => "@" + r.handle).join(" · ")} · {T2("eOwnerSub")}</div>
                  <div className="v" style={{ color: "var(--up)" }}>{E.usd(bal.owner)}</div>
                  <div className="s">{T2("eGrossLine")(bal.ownerGross)}</div>
                  <div className="w-actions">
                    <button className="btn-ask" style={{ flex: "none", padding: "8px 16px" }} disabled={bal.owner < C.minWithdraw} onClick={onWithdraw}>
                      <Icon name="send" size={12} />{T2("eWithdraw")}
                    </button>
                    <span style={{ font: "400 11.5px var(--sans)", color: "var(--faint)", alignSelf: "center" }}>{T2("eMin")(C.minWithdraw)}</span>
                  </div>
                </div>
                {owned.map((r) => {
                  const fee = r.ownerFee != null ? Number(r.ownerFee) : 0;
                  const net = fee / 2;
                  const busy = !!feeBusy[r.kolId];
                  const rowErr = feeErr[r.kolId] || null;
                  return (
                    <div className="v2-card" key={r.kolId} style={{ marginBottom: 10, padding: "12px 14px" }}>
                      <div className="stream-top" style={{ marginBottom: 8 }}>
                        <Avatar kol={avatarFor(r)} size={26} radius={8} />
                        <span className="nm">@{r.handle}</span>
                        <span className="st" style={{ color: "var(--dim)" }}>
                          {L("Fee", "本人费")} {E.usd(fee)} / {L("call", "次")} · {L("you net 50%", "你净得 50%")} {E.usd(net)}
                        </span>
                      </div>
                      <div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
                        {feeOpts.map((opt) => (
                          <button
                            key={opt}
                            className={"x2-amt" + (fee === opt ? " on" : "")}
                            disabled={busy}
                            onClick={() => setFee(r.kolId, opt)}
                            style={{ padding: "6px 10px", font: "600 12px var(--mono)" }}
                          >${opt.toFixed(2)}</button>
                        ))}
                        {busy && <span style={{ font: "400 11.5px var(--sans)", color: "var(--faint)" }}>{L("Saving…", "保存中…")}</span>}
                      </div>
                      {rowErr && (
                        <div className="x2-note x2-danger" role="alert" style={{ marginTop: 8 }}>
                          <Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{rowErr}
                        </div>
                      )}
                    </div>
                  );
                })}
              </>
            )}
          </>
        )}
      </div>
    </div>
  );
}

/* ---------------- Withdraw modal ---------------- */
/* Spec (2026-07-11 统一口径, task-054/spec §3.1): only owner balance is
   withdrawable; min $5 gross, flat $1 fee (E.cfg().withdrawFee); future rail is
   native USDC on Arbitrum One only — legacy local/wire tiers removed from UI.
   backend window.RXAPI.withdraw(amount, payoutMethod) → { ok, received, fee,
   payoutMethod } | { error }. Never call window.RXS.withdraw. */

// Front-end gate for the withdraw form. The AUTHORITATIVE switch is the backend
// econ_config.withdraw_enabled flag — while it is off, POST /api/wallet/withdraw
// returns 403 { error:"withdraw_disabled" }. Flip this constant to true to
// render the legacy form active again; do NOT delete that form code.
const WITHDRAW_ENABLED = false;

function WithdrawModal({ onClose }) {
  const { Icon } = window.RXC;
  const E = window.RXE, C = E.cfg();
  const L = (en, zh) => (window.RXI.lang === "zh" ? zh : en);
  // Server-owned balance only; never read useStore2/RXS accounts.
  const [owner, setOwner] = wS(null);
  const [loading, setLoading] = wS(true);
  const [loadErr, setLoadErr] = wS(null);
  const [tick, setTick] = wS(0);
  const [amt, setAmt] = wS(0);
  const [amtInit, setAmtInit] = wS(false);
  const [method, setMethod] = wS("usdc");
  const [busy, setBusy] = wS(false);
  const [err, setErr] = wS(null);
  const [done, setDone] = wS(null);
  // Convert-to-credits flow state (active while WITHDRAW_ENABLED === false).
  const [cvAmt, setCvAmt] = wS(0);
  const [cvAmtInit, setCvAmtInit] = wS(false);
  const [cvBusy, setCvBusy] = wS(false);
  const [cvErr, setCvErr] = wS(null);
  const [cvDone, setCvDone] = wS(null);
  // Privy bridge for 401 → clear key (sign out), mirrors EarningsPage.setFee.
  const privy = window.PrivySDK && window.PrivySDK.usePrivy ? window.PrivySDK.usePrivy() : null;

  wEffect(() => {
    let cancelled = false;
    (async () => {
      setLoading(true);
      setLoadErr(null);
      try {
        const res = await window.RXAPI.wallet({ limit: 1 });
        if (cancelled) return;
        if (res && res.error) { setLoadErr(res.error); return; }
        const b = (res && res.balances) || {};
        const o = b.owner || 0;
        setOwner(o);
        // First time owner arrives, seed amt to its floor.
        if (!amtInit) { setAmt(Math.floor(o)); setAmtInit(true); }
        // First time, seed convert amount to the full owner balance (cents-safe).
        if (!cvAmtInit) { setCvAmt(Math.floor(o * 100) / 100); setCvAmtInit(true); }
      } catch (_) {
        if (!cancelled) setLoadErr("network_error");
      } finally {
        if (!cancelled) setLoading(false);
      }
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [tick]);

  const fee = C.withdrawFee;
  const received = Math.max(0, (amt || 0) - fee);
  const ok = amt >= C.minWithdraw && amt <= (owner || 0) && amt > fee;
  const go = async () => {
    if (!ok || busy) return;
    setBusy(true); setErr(null);
    try {
      const r = await window.RXAPI.withdraw(Number(amt), method);
      if (r && r.error) { setErr(r.error); return; }
      if (r && r.ok) { setDone(r); return; }
      setErr("request_failed");
    } catch (_) {
      setErr("network_error");
    } finally {
      setBusy(false);
    }
  };
  const errText = err && (() => {
    const zh = window.RXI.lang === "zh";
    const m = {
      unauthorized: zh ? "未登录" : "Not signed in",
      below_min: zh ? "低于最低提现额" : "Below minimum withdrawal",
      below_fee: zh ? "提现金额须大于手续费" : "Amount must exceed the fee",
      invalid_method: zh ? "提现通道无效" : "Invalid payout method",
      exceeds_balance: zh ? "超过可用余额" : "Exceeds available balance",
      account_banned: zh ? "账户已被冻结" : "Account is frozen",
      network_error: zh ? "网络错误，请重试" : "Network error, please retry",
      request_failed: zh ? "请求失败，请重试" : "Request failed, please retry",
      withdraw_disabled: T2("wdDisabled"),
    };
    return m[err] || (zh ? "提现失败" : "Withdrawal failed");
  })();

  // Convert owner balance → spendable credits. 401 clears the key (privy.logout),
  // mirroring EarningsPage.setFee. busy resets in finally so exceptions never
  // strand the button (task-004 lesson). Server owns all amount validation.
  const convert = async () => {
    const a = Number(cvAmt);
    if (!(a > 0) || cvBusy) return;
    setCvBusy(true); setCvErr(null); setCvDone(null);
    try {
      const r = await window.RXAPI.ownerConvert(a);
      if (r && (r.status === 401 || r.error === "unauthorized")) {
        if (privy && typeof privy.logout === "function") {
          try { privy.logout(); } catch (_) {}
        }
        setCvErr("unauthorized");
        return;
      }
      if (r && r.error) { setCvErr(r.error); return; }
      if (r && r.ok) {
        setCvDone(r);
        // Refresh wallet balances everywhere (chat billing, market, etc.) and
        // re-pull owner balance inside this modal so the convert cap stays honest.
        window.dispatchEvent(new CustomEvent("rx2:refresh"));
        setTick((t) => t + 1);
        return;
      }
      setCvErr("request_failed");
    } catch (_) {
      setCvErr("network_error");
    } finally {
      setCvBusy(false);
    }
  };
  const cvErrText = cvErr && (() => {
    const zh = window.RXI.lang === "zh";
    const m = {
      unauthorized: zh ? "未登录，请先登录" : "Not signed in",
      invalid_amount: T2("cvErrInvalid"),
      exceeds_balance: T2("cvErrExceeds"),
      account_banned: T2("cvErrBanned"),
      network_error: T2("cvErrNetwork"),
      request_failed: T2("cvErrGeneric"),
    };
    return m[cvErr] || T2("cvErrGeneric");
  })();

  if (done) {
    const chLabel = done.payoutMethod === "usdc" ? "USDC · Arbitrum One"
      : String(done.payoutMethod || "—");
    return (
      <Modal2 title={T2("eWdDone")} icon="send" onClose={onClose}>
        <div className="dst-panel" style={{ padding: "22px 6px" }}>
          <Icon name="check" size={30} color="var(--up)" />
          <h2 style={{ marginTop: 10 }}>{E.usd(done.received)}</h2>
          <p>{T2("eYouReceive")} · {chLabel} · {L("fee", "手续费")} {E.usd(done.fee)}</p>
          <p style={{ color: "var(--faint)", font: "400 12px var(--sans)", marginTop: 4 }}>{L("Processing", "处理中")}</p>
        </div>
        <button className="x2-primary" onClick={onClose}>OK</button>
      </Modal2>
    );
  }
  if (loading) {
    return (
      <Modal2 title={T2("eWithdraw")} icon="send" onClose={onClose}>
        <div className="v2-card" style={{ padding: 18, textAlign: "center", color: "var(--dim)" }} role="status">{L("Loading…", "加载中…")}</div>
      </Modal2>
    );
  }
  if (loadErr) {
    const zh = window.RXI.lang === "zh";
    const loadErrText = loadErr === "network_error"
      ? (zh ? "网络错误，请重试" : "Network error, please retry")
      : (zh ? "加载失败，请重试" : "Failed to load, please retry");
    return (
      <Modal2 title={T2("eWithdraw")} icon="send" onClose={onClose}>
        <div className="x2-note x2-danger" role="alert"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{loadErrText}</div>
        <div style={{ display: "flex", gap: 8, marginTop: 10 }}>
          <button className="btn-ghost" onClick={() => setTick((t) => t + 1)}><Icon name="refresh" size={12} />{zh ? "重试" : "Retry"}</button>
          <button className="btn-ghost" onClick={onClose}>{zh ? "关闭" : "Close"}</button>
        </div>
      </Modal2>
    );
  }
  // 未来唯一提现通道 = Arbitrum One 原生 USDC（D2-final 期间表单整体禁用，仅展示口径）。
  const methods = [
    { id: "usdc", label: "USDC · Arbitrum One", sub: L(`$${C.withdrawFee.toFixed(2)} fee`, `$${C.withdrawFee.toFixed(2)} 手续费`) },
  ];
  return (
    <Modal2 title={T2("eWithdraw")} icon="send" onClose={onClose}>
      {!WITHDRAW_ENABLED && (
        <div role="status" aria-live="polite"
             style={{ display: "flex", gap: 8, padding: "10px 12px", borderRadius: 10,
                      background: "rgba(245, 158, 11, 0.10)",
                      border: "1px solid rgba(245, 158, 11, 0.35)", marginBottom: 12 }}>
          <Icon name="clock" size={14} style={{ flex: "none", marginTop: 1, color: "var(--amber)" }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ font: "700 13px var(--sans)" }}>{T2("wdSoonTitle")}</div>
            <div style={{ font: "400 12px var(--sans)", color: "var(--dim)", marginTop: 2 }}>
              {T2("wdSoonBody")}
            </div>
          </div>
        </div>
      )}

      {!WITHDRAW_ENABLED && (
        <div className="v2-card" style={{ padding: 14, marginBottom: 12, border: "1px solid var(--bd)" }}>
          <div style={{ font: "600 14px var(--sans)", marginBottom: 4, display: "flex", alignItems: "center", gap: 6 }}>
            <Icon name="refresh" size={13} />{T2("cvTitle")}
          </div>
          <div style={{ font: "400 12px var(--sans)", color: "var(--faint)", marginBottom: 8 }}>
            {T2("cvSub")}
          </div>
          <div className="x2-custom">
            <span style={{ color: "var(--faint)", font: "600 13px var(--mono)" }}>$</span>
            <input type="number" min={0.01} max={owner || 0} value={cvAmt}
                   aria-label={T2("cvAmount")}
                   onChange={(e) => { setCvAmt(Number(e.target.value)); setCvDone(null); }} />
            <span style={{ color: "var(--faint)", font: "400 11px var(--sans)" }}>≤ {E.usd(owner || 0)}</span>
            <button className="btn-ghost" onClick={() => setCvAmt(Math.floor((owner || 0) * 100) / 100)}
                    disabled={cvBusy || !owner} aria-label={T2("cvMax")}>
              {T2("cvMax")}
            </button>
          </div>
          {cvDone ? (
            <div role="status" aria-live="polite"
                 style={{ display: "flex", gap: 8, marginTop: 8, padding: "8px 10px", borderRadius: 8,
                          background: "rgba(46, 170, 80, 0.10)", border: "1px solid rgba(46,170,80,0.35)" }}>
              <Icon name="check" size={13} color="var(--up)" style={{ flex: "none", marginTop: 1 }} />
              <span style={{ font: "600 12.5px var(--sans)", color: "var(--up)" }}>{T2("cvDone")}</span>
            </div>
          ) : (
            <button className="x2-primary"
                    disabled={!(cvAmt > 0) || cvBusy}
                    onClick={convert}
                    aria-label={T2("cvCta") + " · $" + cvAmt}
                    style={{ marginTop: 8 }}>
              <Icon name="refresh" size={13} />{cvBusy ? T2("cvConverting") : T2("cvCta")}
            </button>
          )}
          {cvErrText && (
            <div className="x2-note x2-danger" role="alert" style={{ marginTop: 8 }}>
              <Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{cvErrText}
            </div>
          )}
        </div>
      )}

      {/* Legacy withdraw form — disabled while WITHDRAW_ENABLED === false.
          Do not delete; flip the constant to restore when backend
          econ_config.withdraw_enabled opens. */}
      <fieldset disabled={!WITHDRAW_ENABLED}
                style={{ border: 0, padding: 0, margin: 0, opacity: WITHDRAW_ENABLED ? 1 : 0.55 }}>
        <p className="x2-msub">{T2("eOwnerSub")} · {T2("eMin")(C.minWithdraw)}</p>
        <div className="x2-custom">
          <span style={{ color: "var(--faint)", font: "600 13px var(--mono)" }}>$</span>
          <input type="number" min={C.minWithdraw} max={owner || 0} value={amt} onChange={(e) => setAmt(Number(e.target.value))} />
          <span style={{ color: "var(--faint)", font: "400 11px var(--sans)" }}>≤ {E.usd(owner || 0)}</span>
        </div>
        <div role="radiogroup" aria-label={L("Payout method", "提现通道")} style={{ display: "flex", gap: 8, marginTop: 10 }}>
          {methods.map((o) => (
            <label key={o.id} className={"wd-opt" + (method === o.id ? " on" : "")} style={{ flex: 1, display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", border: "1px solid var(--bd)", borderRadius: 10, cursor: "pointer", borderColor: method === o.id ? "var(--accent)" : undefined, background: method === o.id ? "rgba(99,102,241,0.08)" : undefined }}>
              <input type="radio" name="wd-method" value={o.id} checked={method === o.id} onChange={() => setMethod(o.id)} style={{ accentColor: "var(--accent)" }} />
              <span style={{ minWidth: 0 }}>
                <div style={{ font: "600 13px var(--sans)" }}>{o.label}</div>
                <div style={{ font: "400 11px var(--sans)", color: "var(--faint)" }}>{o.sub}</div>
              </span>
            </label>
          ))}
        </div>
        <div className="fee-rows" style={{ marginTop: 6 }}>
          <FeeRow subrow title={T2("eWithdraw")} value={E.usd(amt || 0)} />
          <FeeRow title={T2("wChannelFee")} sub={T2("eWdFee")(fee)} value={"−" + E.usd(fee)} />
          <FeeRow total title={T2("eYouReceive")} value={E.usd(received)} />
        </div>
        {amt > 0 && amt <= fee && (
          <div className="x2-note x2-danger"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{L("Amount must be greater than the fee.", "提现金额须大于手续费。")}</div>
        )}
        {errText && (
          <div className="x2-note x2-danger"><Icon name="shield" size={13} style={{ flex: "none", marginTop: 1 }} />{errText}</div>
        )}
        <button className="x2-primary"
                disabled={!ok || busy || !WITHDRAW_ENABLED}
                aria-label={!WITHDRAW_ENABLED ? T2("wdSoonTitle") : T2("eWdCta")}
                onClick={go}>
          <Icon name="send" size={13} />{busy ? L("Submitting…", "提交中…") : T2("eWdCta")}
        </button>
      </fieldset>
    </Modal2>
  );
}

Object.assign(window, { TopupModal, WalletPage2, EarningsPage, WithdrawModal, LedgerRow });
