/* Robindex v2 — app shell: market · create · chat · wallet · earnings · admin.
   Auth-gated, responsive (desktop sidebar / mobile bottom nav), USD pay-per-call. */
const { useState: aS, useRef: aR, useEffect: aE } = React;
const aT = (k) => window.RXI.t(k);

// model price chip: show our metered $/M-out price instead of a multiplier
window.RXB.fmtMult = (v) => "$" + (v >= 10 ? Math.round(v) : v.toFixed(v >= 1 ? 1 : 2)) + "/M";

// ---- v2 real-data adapters (RXAPI.kols / RXAPI.wallet → reg/acct shape) ----
// Followers count: raw int from API → "284K"/"1.2M" string used by sidebar/cards.
const fmtFollowers2 = (n) => {
  n = Number(n || 0);
  if (!isFinite(n) || n < 0) return "0";
  if (n >= 1e6) return (n / 1e6).toFixed(n >= 1e7 ? 0 : 1).replace(/\.0$/, "") + "M";
  if (n >= 1e3) return (n / 1e3).toFixed(n >= 1e4 ? 0 : 1).replace(/\.0$/, "") + "K";
  return String(n);
};
// Map a /api/kols row to the existing reg shape consumed by sidebar/cards/chat.
// Spreads the raw row (preserves status/updatedAt/tweetsTotal/newSince/calls/
// role/handle/accent/tagline/...), then renames display_name→name,
// avatar_url→avatar and formats followers. creator/updater preserved with
// missing `by` defaulted to "community"; claimed preserved (or null).
const mapKol2 = (k) => {
  if (!k || typeof k !== "object") return null;
  const fixBy = (o) => o ? { ...o, by: o.by || "community" } : o;
  return {
    ...k,
    name: k.display_name != null ? k.display_name : k.name,
    avatar: k.avatar_url != null ? k.avatar_url : k.avatar,
    followers: fmtFollowers2(k.followers_count),
    creator: fixBy(k.creator) || { by: "community", cost: 0, cap: 0, earned: 0, you: false },
    updater: fixBy(k.updater),
    claimed: k.claimed || null,
  };
};
const EMPTY_ACCT2 = { credits: 0, rewardBal: 0, ownerBal: 0, ownerGross: 0, freeAskRemaining: 0, referralBonus: 0, ledger: [] };
// Map /api/wallet balances → app-shell acct. ledger is owned by WalletPage/
// EarningsPage (they fetch their own paginated view); app shell only needs
// balances for the sidebar/topbar chips.
const mapAcct2 = (balances) => {
  const b = balances || {};
  return {
    credits: Number(b.cash || 0),
    rewardBal: Number(b.reward || 0),
    ownerBal: Number(b.owner || 0),
    ownerGross: Number(b.ownerGross || 0),
    freeAskRemaining: Number(b.freeAskRemaining || 0),
    referralBonus: Number(b.referral_bonus || 0),
    ledger: [],
  };
};

let _id2 = 1;
const uid2 = () => "c" + _id2++;
const LS2 = {
  get: (k, d) => { try { const v = localStorage.getItem("rx2." + k); return v == null ? d : JSON.parse(v); } catch (e) { return d; } },
  set: (k, v) => { try { localStorage.setItem("rx2." + k, JSON.stringify(v)); } catch (e) {} },
};
function useMedia2(q) {
  const [m, setM] = aS(() => window.matchMedia(q).matches);
  aE(() => { const mq = window.matchMedia(q); const h = () => setM(mq.matches); mq.addEventListener("change", h); return () => mq.removeEventListener("change", h); }, [q]);
  return m;
}
function models2For(lang) {
  const E = window.RXE, mk = E.cfg().markup;
  const ours = E.MODELS2.map((m) => ({
    ...m, mult: E.ourOut(m),
    note: "$" + E.ourIn(m).toFixed(E.ourIn(m) < 1 ? 2 : 1) + " in · $" + E.ourOut(m).toFixed(E.ourOut(m) < 1 ? 2 : 1) + " out /M"
      + (m.tag ? " · " + (m.tag[lang] || m.tag.en) : ""),
  }));
  const custom = ((window.RXB.get() || {}).customModels || []).map((m) => ({ ...m, note: T2("byokFree") }));
  return ours.concat(custom);
}

/* ---------------- Sidebar (desktop) ---------------- */
function Sidebar2({ view, setView, regs, chats, active, onPickPersona, onOpenChat, user, acct, lang }) {
  const { Icon, Avatar } = window.RXC;
  const E = window.RXE;
  const nav = [
    ["market", "layers", T2("navMarket")],
    ["wallet", "wallet", T2("navWallet")],
    ["earnings", "trendUp", T2("navEarnings")],
    ["admin", "shield", T2("navAdmin")],
  ];
  return (
    <aside className="side">
      <button className="brand" onClick={() => setView("market")}>
        <div className="brand-mark"><Icon name="candlestick" size={17} color="var(--on-accent)" /></div>
        <div>
          <div className="brand-name">Robindex <span>Desk</span></div>
          <div className="brand-tag">{T2("brandTag2")}</div>
        </div>
      </button>
      <button className="new-btn" onClick={() => setView("create")}>
        <Icon name="bot" size={15} /> {T2("navCreate")}
      </button>
      <div className="side-nav2">
        {nav.map(([id, ic, lb]) => (
          <button key={id} className={"nav2" + (view === id ? " on" : "")} onClick={() => setView(id)}>
            <Icon name={ic} size={15} />{lb}
            {id === "earnings" && acct.rewardBal > 0 && <span className="n2-badge">+{E.usd(acct.rewardBal, 0)}</span>}
          </button>
        ))}
      </div>
      <div className="side-sec">{aT("personas")}</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
        {regs.filter((r) => r.status === "live").map((r) => (
          <button key={r.id} className={"hist" + (active && active.kolId === r.id ? " on" : "")} onClick={() => onPickPersona(r)}>
            <Avatar kol={kolAdapter(r)} size={18} radius={5} className="hist-av" />
            <span className="hist-t">{r.name}</span>
            {r.claimed ? <span className="hist-sub" title={T2("cardClaimedTag")} aria-label={T2("cardClaimedTag")}><Icon name="check" size={11} /></span> : <span className="live-dot"></span>}
          </button>
        ))}
      </div>
      <div className="side-sec" style={{ marginTop: 8 }}>{aT("recent")}</div>
      <div className="side-scroll">
        {chats.length === 0
          ? <div className="side-empty">{aT("noChats")}</div>
          : chats.map((c) => (
            <button key={c.id} className={"hist" + (active && active.id === c.id ? " on" : "")} onClick={() => onOpenChat(c)}>
              <Avatar kol={c.kol} size={18} radius={5} className="hist-av" />
              <span className="hist-t">{c.title}</span>
            </button>
          ))}
      </div>
      <div className="side-bill">
        <button className="sb-item" onClick={() => setView("wallet")}>
          <Icon name="wallet" size={15} color="var(--accent)" />
          <span className="sb-t">{T2("wCredits")}</span>
          <span className="sb-cr">{E.usd(acct.credits)}</span>
        </button>
        <button className="sb-item" onClick={() => setView("earnings")}>
          <Icon name="sparkles" size={15} color="var(--accent)" />
          <span className="sb-t">{T2("wGift")}</span>
          <span className="sb-cr">{E.usd(acct.rewardBal)}</span>
        </button>
      </div>
      <button className="side-foot" onClick={() => setView("earnings")}>
        <div className="av">{(user && user.email ? user.email[0] : "U").toUpperCase()}</div>
        <div style={{ minWidth: 0, textAlign: "left" }}>
          <div className="nm">{user && user.email ? user.email.split("@")[0] : (lang === "zh" ? "用户" : "User")}</div>
          <div className="sub">{T2("eOwner")}: {E.usd(acct.ownerBal)}</div>
        </div>
        <Icon name="chevronRight" size={15} color="var(--faint)" />
      </button>
    </aside>
  );
}

function BottomNav2({ tab, setTab, srcCount, lang }) {
  const { Icon } = window.RXC;
  const items = [
    { id: "market", icon: "layers", label: T2("navMarket") },
    { id: "create", icon: "bot", label: T2("navCreate") },
    { id: "chat", icon: "sparkles", label: lang === "zh" ? "聊天" : "Chat", badge: srcCount },
    { id: "wallet", icon: "wallet", label: T2("navWallet") },
    { id: "earnings", icon: "trendUp", label: T2("navEarnings") },
  ];
  return (
    <nav className="bottom-nav">
      {items.map((it) => (
        <button key={it.id} className={"bn-item" + (tab === it.id ? " on" : "")} onClick={() => setTab(it.id)}>
          <span className="bn-ic"><Icon name={it.icon} size={21} />{it.badge ? <span className="bn-badge">{it.badge}</span> : null}</span>
          <span className="bn-lab">{it.label}</span>
        </button>
      ))}
    </nav>
  );
}

/* ---------------- boot / auth gate (Privy) ---------------- */
// v2 mobile top bar: mirrors app/mobile.jsx MobileTopBar but mounts the
// notification bell inside m-top-actions (next to lang/theme controls).
// MobileTopBar (v1) accepts no children, so we adapt it here the same way
// BottomNav2 adapts BottomNav — keeps the bell inside PrivyProvider + auth gate.
function MobileTopBar2({ kol, lang, setLang, theme, setTheme }) {
  const { Icon, ThemeMenu } = window.RXC;
  return (
    <div className="m-topbar">
      {kol ? (
        <div className="m-kol">
          <window.RXC.Avatar kol={kol} size={30} radius={8} />
          <div style={{ minWidth: 0 }}>
            <div className="m-kol-nm">{kol.display_name}</div>
            <div className="m-kol-role">{kol.role}</div>
          </div>
        </div>
      ) : (
        <div className="m-brand">
          <div className="brand-mark" style={{ width: 26, height: 26, borderRadius: 7 }}><Icon name="candlestick" size={15} color="var(--on-accent)" /></div>
          <div className="brand-name" style={{ fontSize: 14 }}>Robindex <span>Desk</span></div>
        </div>
      )}
      <div className="m-top-actions">
        <NotificationBell2 />
        <window.LangToggle lang={lang} setLang={setLang} />
        <ThemeMenu value={theme} onChange={setTheme} />
      </div>
    </div>
  );
}
function BootMsg2({ kind, lang, detail, onLogout }) {
  const en = lang !== "zh";
  const err = kind === "error";
  const { Icon } = window.RXC;
  const forceClear = () => {
    try {
      localStorage.clear();
      sessionStorage.clear();
      document.cookie.split(";").forEach((c) => {
        document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
      });
      if (navigator.serviceWorker) {
        navigator.serviceWorker.getRegistrations().then((rs) => rs.forEach((r) => r.unregister())).catch(() => {});
      }
      if (window.caches && caches.keys) {
        caches.keys().then((ks) => ks.forEach((k) => caches.delete(k))).catch(() => {});
      }
      if (typeof onLogout === "function") {
        try { onLogout(); } catch (_) {}
      }
    } catch (_) {}
    setTimeout(() => {
      location.reload();
    }, 300);
  };
  return (
    <div className="auth" role={err ? "alert" : "status"} aria-live={err ? "assertive" : "polite"}>
      <div className="auth-bg" />
      <div className="auth-card">
        <div className="auth-logo"><Icon name="candlestick" size={22} color="var(--on-accent)" /></div>
        <h1 className="auth-h">{err ? (en ? "Cannot start Robindex" : "无法启动 Robindex") : (en ? "Loading Robindex" : "正在加载 Robindex")}</h1>
        <p className="auth-sub">{err ? (en ? "Login service is unavailable. Please refresh in a moment." : "登录服务不可用，请稍后刷新重试。") : (en ? "Connecting persona data and login service…" : "正在连接分身数据与登录服务…")}</p>
        {err && detail ? <p className="auth-sub" style={{ fontSize: 12, color: "var(--faint)", wordBreak: "break-all" }}>{String(detail)}</p> : null}
        <div style={{ display: "flex", gap: "10px", marginTop: "20px", justifyContent: "center" }}>
          {err ? <button className="auth-primary" onClick={() => location.reload()}>{en ? "Retry" : "重试"}</button> : null}
          <button className="auth-secondary" style={{ background: "rgba(255,255,255,0.08)", border: "1px solid rgba(255,255,255,0.15)", color: "var(--text)", padding: "10px 16px", borderRadius: "8px", fontSize: "14px", cursor: "pointer" }} onClick={forceClear}>
            {en ? "Clear & Reset" : "清除登录态并重置"}
          </button>
        </div>
      </div>
    </div>
  );
}

function AuthGate2({ privy, lang, theme, setTheme, setLang }) {
  const { Icon, ThemeMenu } = window.RXC;
  const [busy, setBusy] = aS(false);
  const en = lang !== "zh";
  const onLogin = () => {
    if (busy) return;
    if (!privy || typeof privy.login !== "function") {
      console.error("[App2] privy.login not available — is PrivyProvider mounted?");
      return;
    }
    setBusy(true);
    try {
      Promise.resolve(privy.login({ loginMethods: ["email", "google"] }))
        .catch((err) => {
          console.error("[App2] privy.login rejected:", err);
          if (window.RXA) {
            try { window.RXA.track("auth_error", { message: String(err && err.message || err).replace(/[^\x20-\x7e\u4e00-\u9fff]/g, " ").slice(0, 120) }); } catch (_) {}
          }
        })
        .finally(() => setBusy(false));
    } catch (e) {
      console.error("[App2] privy.login error:", e);
      setBusy(false);
    }
  };
  return (
    <div className="auth">
      <div className="auth-bg" />
      <div className="auth-top">
        <window.LangToggle lang={lang} setLang={setLang} />
        <ThemeMenu value={theme} onChange={setTheme} />
      </div>
      <div className="auth-card">
        <div className="auth-logo"><Icon name="candlestick" size={22} color="var(--on-accent)" /></div>
        <h1 className="auth-h">{en ? "Sign in to Robindex" : "登录 Robindex"}</h1>
        <p className="auth-sub">{en ? "Continue with email, Google, or X." : "使用邮箱、Google 或 X 继续。"}</p>
        <button className="auth-primary" onClick={onLogin} disabled={busy}>
          <Icon name="lock" size={14} /> {en ? "Continue" : "继续"}
        </button>
        <p className="auth-terms">
          {en ? "By continuing you agree to our " : "继续即表示您同意我们的"}
          <a href="/terms.html" target="_blank" rel="noopener noreferrer">{en ? "Terms" : "服务条款"}</a>
          {en ? " and " : "与"}
          <a href="/privacy.html" target="_blank" rel="noopener noreferrer">{en ? "Privacy Policy" : "隐私政策"}</a>
          。
        </p>
        <div className="auth-foot">
          <span className="auth-protected">{en ? "Protected by Privy" : "由 Privy 提供保护"}</span>
        </div>
      </div>
    </div>
  );
}

/* ---------------- App ---------------- */
/* Anon CTA Placeholder for locked screens */
function AnonCtaPlaceholder({ titleKey, descKey, lang, onLogin }) {
  const en = lang !== "zh";
  const { Icon } = window.RXC;
  return (
    <div className="anon-cta-placeholder" style={{
      display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
      padding: "80px 20px", textAlign: "center", background: "var(--panel)", borderRadius: "var(--r-lg)",
      border: "1px solid var(--border-soft)", margin: "40px auto", maxWidth: 480
    }}>
      <div style={{ width: 48, height: 48, borderRadius: "50%", background: "var(--accent-ghost)", display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 16 }}>
        <Icon name="lock" size={20} color="var(--accent)" />
      </div>
      <h3 style={{ margin: "0 0 8px 0", font: "600 16px var(--sans)", color: "var(--text)" }}>
        {T2(titleKey)}
      </h3>
      <p style={{ margin: "0 0 20px 0", font: "400 13px var(--sans)", color: "var(--dim)", lineHeight: "1.5" }}>
        {T2(descKey)}
      </p>
      <button className="auth-primary" onClick={onLogin} style={{ maxWidth: 200 }} aria-label={en ? "Sign in to unlock" : "登录以解锁"}>
        <Icon name="lock" size={13} style={{ marginRight: 6 }} />{en ? "Sign In" : "登录"}
      </button>
    </div>
  );
}

const OriginalNotificationBell2 = window.NotificationBell2;

function NotificationBell2(props) {
  const privy = window.PrivySDK && window.PrivySDK.usePrivy ? window.PrivySDK.usePrivy() : null;
  const authenticated = privy && privy.authenticated;
  const en = window.RXI.lang !== "zh";
  const { Icon } = window.RXC;

  if (!authenticated) {
    const handleBellClick = () => {
      if (window.RXA) {
        try { window.RXA.track("auth_modal_opened"); } catch (_) {}
      }
      if (privy && typeof privy.login === "function") {
        window._rxa_auth_in_progress = true;
        Promise.resolve(privy.login({ loginMethods: ["email", "google"] }))
          .then(() => {
            setTimeout(() => {
              if (!privy.authenticated) {
                if (window.RXA) {
                  try { window.RXA.track("auth_cancelled"); } catch (_) {}
                }
                window._rxa_auth_in_progress = false;
              }
            }, 500);
          })
          .catch((err) => {
            if (window.RXA) {
              try { window.RXA.track("auth_error", { message: String(err && err.message || err).replace(/[^\x20-\x7e\u4e00-\u9fff]/g, " ").slice(0, 120) }); } catch (_) {}
            }
            setTimeout(() => {
              if (!privy.authenticated) {
                if (window.RXA) {
                  try { window.RXA.track("auth_cancelled"); } catch (_) {}
                }
                window._rxa_auth_in_progress = false;
              }
            }, 500);
          });
      }
    };
    return (
      <button
        type="button"
        className="topbal"
        onClick={handleBellClick}
        aria-label={en ? "Sign in to view notifications" : "登录以查看通知"}
      >
        <Icon name="lightbulb" size={14} color="var(--faint)" />
      </button>
    );
  }

  if (OriginalNotificationBell2) {
    return <OriginalNotificationBell2 {...props} />;
  }
  return null;
}

/* ---------------- App ---------------- */
function App2() {
  // Synchronous escape hatch: ?clear=true or ?logout=true in URL clears all client-side auth data and reloads.
  try {
    const params = new URLSearchParams(window.location.search);
    if (params.get("clear") === "true" || params.get("logout") === "true") {
      localStorage.clear();
      sessionStorage.clear();
      document.cookie.split(";").forEach((c) => {
        document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
      });
      if (navigator.serviceWorker) {
        navigator.serviceWorker.getRegistrations().then((rs) => rs.forEach((r) => r.unregister())).catch(() => {});
      }
      if (window.caches && caches.keys) {
        caches.keys().then((ks) => ks.forEach((k) => caches.delete(k))).catch(() => {});
      }
      const cleanUrl = window.location.origin + window.location.pathname;
      setTimeout(() => { window.location.replace(cleanUrl); }, 300);
      return null;
    }
  } catch (_) {}

  const [theme, setTheme] = aS(() => LS2.get("theme", "aurora"));
  const [lang, setLangState] = aS(() => window.RXI.lang);
  const privy = window.PrivySDK.usePrivy();
  // user is derived solely from Privy — never persisted to LS, never faked.
  const user = privy.authenticated && privy.user ? {
    email: (privy.user.email && privy.user.email.address)
      || (privy.user.google && privy.user.google.email)
      || (privy.user.twitter && privy.user.twitter.subject)
      || "trader@robindex.ai",
    method: privy.user.google ? "google" : privy.user.twitter ? "twitter" : "email",
  } : null;
  const [view, setView] = aS("market");
  const [model, setModel] = aS(() => LS2.get("model", "dspro"));
  const [effort, setEffort] = aS("high");
  const [chats, setChats] = aS([]);
  const [active, setActive] = aS(null);
  const [messages, setMessages] = aS([]);
  const [sources, setSources] = aS([]);
  const [railTab, setRailTab] = aS("persona");
  const [highlight, setHighlight] = aS(null);
  const [mSub, setMSub] = aS("ask");
  // modals
  const [topup, setTopup] = aS(null);       // { suggest } | null
  const [updReg, setUpdReg] = aS(null);
  const [claimReg, setClaimReg] = aS(null);
  const [wd, setWd] = aS(false);
  const [insuff, setInsuff] = aS(null);     // { need, bal }
  const [addModel, setAddModel] = aS(false);
  const [ansViewSlug, setAnsViewSlug] = aS(null);
  const threadRef = aR(null);
  const chatAbortRef = aR(null);
  const mobile = useMedia2("(max-width: 760px)");
  // Real data state (no useStore2 / mock fallback). regs/acct come from
  // RXAPI.kols() + RXAPI.wallet() once Privy is authenticated; loading/error
  // drive a BootMsg2 gate at the authenticated-app position.
  const [regs, setRegs] = aS([]);
  const [acct, setAcct] = aS(EMPTY_ACCT2);
  const [loading, setLoading] = aS(true);
  const [error, setError] = aS(null);
  const [tick, setTick] = aS(0);
  const E = window.RXE;

  const [pendingAction, setPendingAction] = aS(null);

  aE(() => {
    try {
      const code = new URLSearchParams(window.location.search).get("ref");
      if (code && /^[a-z0-9]{8}$/i.test(code)) {
        localStorage.setItem("rx2.referral_code", code.toLowerCase());
        if (window.RXA) window.RXA.track("referral_landed", { source: "invite_link" });
      }
    } catch (_) {}
  }, []);

  const triggerLogin = () => {
    if (!privy || typeof privy.login !== "function") {
      console.error("[App2] privy.login not available — is PrivyProvider mounted?");
      return;
    }
    window._rxa_auth_in_progress = true;
    if (window.RXA) {
      try { window.RXA.track("auth_modal_opened"); } catch (_) {}
    }
    try {
      Promise.resolve(privy.login({ loginMethods: ["email", "google"] }))
        .then(() => {
          setTimeout(() => {
            if (!privy.authenticated) {
              if (window.RXA) {
                try { window.RXA.track("auth_cancelled"); } catch (_) {}
              }
              window._rxa_auth_in_progress = false;
            }
          }, 500);
        })
        .catch((err) => {
          if (window.RXA) {
            try { window.RXA.track("auth_error", { message: String(err && err.message || err).replace(/[^\x20-\x7e\u4e00-\u9fff]/g, " ").slice(0, 120) }); } catch (_) {}
          }
          setTimeout(() => {
            if (!privy.authenticated) {
              if (window.RXA) {
                try { window.RXA.track("auth_cancelled"); } catch (_) {}
              }
              window._rxa_auth_in_progress = false;
            }
          }, 500);
        });
    } catch (e) {
      console.error("[App2] privy.login error:", e);
    }
  };

  const requireAuth = (type, data) => {
    const action = { type, data };
    setPendingAction(action);
    try {
      sessionStorage.setItem("rx_pending_action", JSON.stringify(action));
    } catch (_) {}
    triggerLogin();
  };

  // Intercept RXAPI methods that require authentication
  aE(() => {
    const originalCloneCreate = window.RXAPI.cloneCreate;
    window.RXAPI.cloneCreate = async (quoteId) => {
      if (!privy.authenticated) {
        requireAuth("clone_create", { quoteId });
        return { error: "unauthorized" };
      }
      return originalCloneCreate(quoteId);
    };

    const originalUpdateEstimate = window.RXAPI.updateEstimate;
    window.RXAPI.updateEstimate = async (kolId) => {
      if (!privy.authenticated) {
        requireAuth("update_estimate", { kolId });
        return { error: "unauthorized" };
      }
      return originalUpdateEstimate(kolId);
    };

    const originalUpdatePay = window.RXAPI.updatePay;
    window.RXAPI.updatePay = async (kolId, quoteId) => {
      if (!privy.authenticated) {
        requireAuth("update_pay", { kolId, quoteId });
        return { error: "unauthorized" };
      }
      return originalUpdatePay(kolId, quoteId);
    };

    const originalClaimStart = window.RXAPI.claimStart;
    window.RXAPI.claimStart = async (kolId) => {
      if (!privy.authenticated) {
        requireAuth("claim_start", { kolId });
        return { error: "unauthorized" };
      }
      return originalClaimStart(kolId);
    };

    const originalClaimConfirm = window.RXAPI.claimConfirm;
    window.RXAPI.claimConfirm = async (kolId, fee) => {
      if (!privy.authenticated) {
        requireAuth("claim_confirm", { kolId, fee });
        return { error: "unauthorized" };
      }
      return originalClaimConfirm(kolId, fee);
    };

    return () => {
      window.RXAPI.cloneCreate = originalCloneCreate;
      window.RXAPI.updateEstimate = originalUpdateEstimate;
      window.RXAPI.updatePay = originalUpdatePay;
      window.RXAPI.claimStart = originalClaimStart;
      window.RXAPI.claimConfirm = originalClaimConfirm;
    };
  }, [privy.authenticated]);

  const changeView = (v) => {
    if ((v === "wallet" || v === "earnings" || v === "admin") && !privy.authenticated) {
      requireAuth(v === "wallet" ? "wallet_view" : v === "earnings" ? "earnings_view" : "admin_view", null);
      return;
    }
    setView(v);
  };

  const openTopupModal = (suggest) => {
    if (!privy.authenticated) {
      requireAuth("topup", { suggest });
      return;
    }
    setTopup({ suggest });
  };

  const openClaimModal = (reg) => {
    if (!privy.authenticated) {
      requireAuth("claim", { regId: reg.id });
      return;
    }
    setClaimReg(reg);
  };

  const openUpdateModal = (reg) => {
    if (!privy.authenticated) {
      requireAuth("update", { regId: reg.id });
      return;
    }
    setUpdReg(reg);
  };

  const openAddModelModal = () => {
    if (!privy.authenticated) {
      requireAuth("byok", null);
      return;
    }
    setAddModel(true);
  };

  aE(() => { document.documentElement.setAttribute("data-theme", theme); LS2.set("theme", theme); }, [theme]);
  aE(() => { LS2.set("model", model); }, [model]);
  const abortChatStream = () => {
    if (chatAbortRef.current) {
      chatAbortRef.current.abort();
      chatAbortRef.current = null;
    }
  };
  aE(() => () => abortChatStream(), []);
  aE(() => { if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight; }, [messages]);

  // Publish Privy access token to window.RXAUTH for store2-api.js / chat2.jsx.
  // The __privyV2Bridge marker ensures we only clean up our own bridge on
  // logout/unmount — never a future owner's.
  if (privy.authenticated && typeof privy.getAccessToken === "function") {
    window.RXAUTH = { __privyV2Bridge: true, getToken: () => privy.getAccessToken() };
  } else if (window.RXAUTH && window.RXAUTH.__privyV2Bridge) {
    delete window.RXAUTH;
  }
  aE(() => {
    return () => {
      if (window.RXAUTH && window.RXAUTH.__privyV2Bridge) {
        delete window.RXAUTH;
      }
    };
  }, []);
  // Record signup acceptance once Privy is authenticated AND the RXAUTH bridge
  // is wired. Placed after the bridge effect so window.RXAUTH is guaranteed to
  // exist when this runs. Backend is idempotent, so re-calling per auth cycle
  // is safe; failures are non-blocking.
  aE(() => {
    if (!privy.authenticated) return;
    if (!window.RXAUTH || !window.RXAUTH.__privyV2Bridge) return;
    if (!window.RXAPI || typeof window.RXAPI.acceptSignup !== "function") return;
    (async () => {
      try {
        await window.RXAPI.acceptSignup();
        const code = localStorage.getItem("rx2.referral_code");
        if (code && window.RXAPI.attributeReferral) {
          const result = await window.RXAPI.attributeReferral(code);
          if (result && (result.success || result.error === "already_attributed" || result.error === "self_referral")) {
            localStorage.removeItem("rx2.referral_code");
          }
        }
      } catch (e) {
        console.warn("[App2] signup/referral attribution failed:", e);
      }
    })();
  }, [privy.authenticated]);

  // Once Privy is authenticated, parallel-fetch KOLs + wallet (limit:60) and
  // map to reg/acct shape. Re-fires on tick (rx2:refresh event). Unmount-safe
  // via `cancelled` flag. 401 → privy.logout; never falls back to mock.
  aE(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);
    (async () => {
      if (privy.authenticated) {
        const [kolsRes, walRes] = await Promise.all([
          window.RXAPI.kols(),
          window.RXAPI.wallet({ limit: 60 }),
        ]);
        if (cancelled) return;
        const unauth = (r) => r && (r.status === 401 || r.error === "unauthorized");
        if (unauth(kolsRes) || unauth(walRes)) {
          if (privy && typeof privy.logout === "function") {
            try { privy.logout(); } catch (e) { console.warn("[App2] privy.logout failed:", e); }
          }
          setError("unauthorized");
          setLoading(false);
          return;
        }
        if (kolsRes && kolsRes.error) { setError(kolsRes.error); setLoading(false); return; }
        if (walRes && walRes.error) { setError(walRes.error); setLoading(false); return; }
        const kolsList = (kolsRes && Array.isArray(kolsRes.kols)) ? kolsRes.kols : [];
        setRegs(kolsList.map(mapKol2).filter(Boolean));
        setAcct(mapAcct2(walRes && walRes.balances));
        setLoading(false);
      } else {
        const kolsRes = await window.RXAPI.kols();
        if (cancelled) return;
        if (kolsRes && kolsRes.error) { setError(kolsRes.error); setLoading(false); return; }
        const kolsList = (kolsRes && Array.isArray(kolsRes.kols)) ? kolsRes.kols : [];
        setRegs(kolsList.map(mapKol2).filter(Boolean));
        setAcct(EMPTY_ACCT2);
        setLoading(false);
      }
    })();
    return () => { cancelled = true; };
  }, [privy.authenticated, tick]);

  // Restore draft/pending actions on login success
  aE(() => {
    if (privy.authenticated) {
      if (window._rxa_auth_in_progress) {
        if (window.RXA) {
          try { window.RXA.track("auth_completed"); } catch (_) {}
        }
        window._rxa_auth_in_progress = false;
      }
      let saved = pendingAction;
      if (!saved) {
        try {
          const str = sessionStorage.getItem("rx_pending_action");
          if (str) saved = JSON.parse(str);
        } catch (_) {}
      }
      if (saved) {
        const { type, data } = saved;
        if (type === "chat_send" && data) {
          if (data.kolId) {
            const targetReg = regs.find(r => r.id === data.kolId);
            if (!targetReg && regs.length === 0) {
              return;
            }
            if (targetReg) {
              const existingChat = chats.find(c => c.kolId === data.kolId);
              if (existingChat) {
                setActive(existingChat);
              } else {
                const kol = window.kolFull(targetReg, lang);
                const chat = { id: uid2(), kolId: targetReg.id, kol, title: kol.display_name + (lang === "zh" ? " · 新会话" : " · new chat") };
                setChats(c => [chat, ...c]);
                setActive(chat);
              }
              changeView("chat");
            }
          }
          if (data.model) {
            setModel(data.model);
          }
          if (data.effort) {
            setEffort(data.effort);
          }
          if (data.question && data.kolId) {
            sessionStorage.setItem("rx_composer_draft_" + data.kolId, data.question);
          }
        } else if (type === "topup" && data) {
          setTopup({ suggest: data.suggest });
        } else if (type === "clone_create" && data) {
          changeView("create");
        } else if (type === "update" && data) {
          const targetReg = regs.find(r => r.id === data.regId);
          if (!targetReg && regs.length === 0) return;
          if (targetReg) setUpdReg(targetReg);
        } else if (type === "claim" && data) {
          const targetReg = regs.find(r => r.id === data.regId);
          if (!targetReg && regs.length === 0) return;
          if (targetReg) setClaimReg(targetReg);
        } else if (type === "wallet_view") {
          changeView("wallet");
        } else if (type === "earnings_view") {
          changeView("earnings");
        } else if (type === "admin_view") {
          changeView("admin");
        } else if (type === "byok") {
          setAddModel(true);
        }
        setPendingAction(null);
        try {
          sessionStorage.removeItem("rx_pending_action");
        } catch (_) {}
      }
    }
  }, [privy.authenticated, regs]);

  // External refresh signal (e.g. after create/update/claim/withdraw) → bump
  // tick, which re-fires the fetch effect above.
  aE(() => {
    const onRefresh = () => setTick((t) => t + 1);
    window.addEventListener("rx2:refresh", onRefresh);
    return () => window.removeEventListener("rx2:refresh", onRefresh);
  }, []);

  // ?a=<slug> deep-link boot: open AnswerViewerModal then clean param.
  aE(() => {
    const p = new URLSearchParams(window.location.search);
    const s = p.get("a");
    if (s && window.AnswerViewerModal && !ansViewSlug) {
      const slug = s.trim();
      if (slug) {
        setAnsViewSlug(slug);
        try {
          const u = new URL(window.location.href);
          u.searchParams.delete("a");
          window.history.replaceState(null, "", u.toString());
        } catch (_) {}
      }
    }
  }, []);

  // ?k=<handle> deep-link boot: find KOL by handle, open chat, then clean param.
  aE(() => {
    const p = new URLSearchParams(window.location.search);
    const h = p.get("k");
    if (h && regs.length > 0) {
      const handle = h.trim().toLowerCase();
      if (handle) {
        const target = regs.find((r) => String(r.handle || "").toLowerCase() === handle);
        if (target) {
          openPersona(target);
          try {
            const u = new URL(window.location.href);
            u.searchParams.delete("k");
            window.history.replaceState(null, "", u.toString());
          } catch (_) {}
        }
      }
    }
  }, [regs]);

  const setLang = (l) => { window.RXI.set(l); setLangState(l); document.documentElement.setAttribute("lang", l === "zh" ? "zh" : "en"); };

  const models = models2For(lang);
  const curModel = models.find((m) => m.id === model) || models[0];
  const activeReg = active ? (regs.find((r) => r.id === active.kolId) || null) : null;
  const activeKol = activeReg ? window.kolFull(activeReg, lang) : null;

  const patchKMsg = (kId, patch) => {
    setMessages((m) => m.map((x) => {
      if (x.id !== kId) return x;
      const next = { ...x, ...patch };
      if (patch.resp) next.resp = { ...x.resp, ...patch.resp };
      return next;
    }));
  };

  const openPersona = (reg) => {
    if (reg.status !== "live") { changeView("create"); return; }
    abortChatStream();
    const kol = window.kolFull(reg, lang);
    const chat = { id: uid2(), kolId: reg.id, kol, title: kol.display_name + (lang === "zh" ? " · 新会话" : " · new chat") };
    setChats((c) => [chat, ...c]);
    setActive(chat); changeView("chat"); setMSub("ask");
    setMessages([]); setSources([]); setHighlight(null); setRailTab("persona");
  };
  const openChat = (c) => { abortChatStream(); setActive(c); changeView("chat"); setMSub("ask"); };

  const ask = async (question) => {
    if (!activeKol || !active) return;
    if (window.RXA) {
      try { window.RXA.track("ask_clicked"); } catch (_) {}
    }
    if (!privy.authenticated) {
      requireAuth("chat_send", { question, model, effort, kolId: activeKol?.id });
      return;
    }
    abortChatStream();
    const ac = new AbortController();
    chatAbortRef.current = ac;

    const userMsg = { id: uid2(), role: "u", text: question };
    const kId = uid2();
    const kMsg = {
      id: kId, role: "k", kol: activeKol, resp: window.chat2EmptyResp(lang),
      phase: 0, done: false, usedModel: curModel, bill: null, error: null, cancelled: false,
    };
    setMessages((m) => [...m, userMsg, kMsg]);

    const ownerFee = Number(activeReg && activeReg.claimed && activeReg.claimed.fee) || 0;
    const useFreeAsk = acct.freeAskRemaining > 0 && ownerFee <= 0 && !curModel.byok;
    const generationStartedAt = Date.now();
    if (window.RXA) window.RXA.track("answer_generation_started", { kol_id: activeKol.id, model: useFreeAsk ? "dsflash" : curModel.id });

    const result = await window.streamChat2({
      kolId: activeKol.id,
      message: question,
      model: curModel.id,
      conversationId: active.id,
      signal: ac.signal,
      useFreeAsk,
      onEvent: (type, data) => {
        if (type === "progress") {
          patchKMsg(kId, { phase: data.phase, progressText: data.text });
        } else if (type === "tool_call") {
          patchKMsg(kId, { resp: { toolCalls: data.toolCalls } });
        } else if (type === "delta" || type === "final_answer") {
          patchKMsg(kId, { resp: { answerMd: data.answerMd } });
        } else if (type === "bill") {
          patchKMsg(kId, { bill: data });
        } else if (type === "meta" && data.final && data.citations) {
          const cites = window.chat2AdaptCitations(data.citations, lang);
          setSources(cites);
          setRailTab("sources");
          setHighlight(null);
        } else if (type === "error") {
          patchKMsg(kId, { error: data.message || (lang === "zh" ? "请求失败" : "Request failed") });
        } else if (type === "done") {
          patchKMsg(kId, { done: true, progressText: null });
        }
      },
    });

    if (chatAbortRef.current === ac) chatAbortRef.current = null;
    if (useFreeAsk) setTick((n) => n + 1);

    if (result.aborted) {
      patchKMsg(kId, { cancelled: true, progressText: null });
      return;
    }
    if (result.status === 401 || result.error === "unauthorized") {
      patchKMsg(kId, { error: lang === "zh" ? "登录已过期，请重新登录" : "Session expired — please sign in again" });
      if (privy && typeof privy.logout === "function") { try { privy.logout(); } catch (e) { console.warn("[App2] privy.logout failed:", e); } }
      return;
    }
    if (result.error === "quota_exhausted") {
      if (window.RXA) window.RXA.track("free_ask_limit_reached", { kol_id: activeKol.id });
      patchKMsg(kId, { error: lang === "zh" ? "今日免费次数已用完" : "Today's free asks are used up" });
      return;
    }
    if (result.status === 402 || result.error === "insufficient") {
      setInsuff({ need: result.need, bal: result.balance });
      patchKMsg(kId, { error: lang === "zh" ? "余额不足" : "Insufficient credits" });
      return;
    }
    if (result.ok && window.RXA) {
      window.RXA.track("answer_generation_completed", { kol_id: activeKol.id, model: useFreeAsk ? "dsflash" : curModel.id, duration_ms: Date.now() - generationStartedAt });
      if (useFreeAsk) window.RXA.track("free_ask_consumed", { kol_id: activeKol.id, model: "dsflash" });
    }
    if (result.error) {
      patchKMsg(kId, { error: result.message || result.error });
      return;
    }
    if (result.ok) {
      if (result.conversationId && result.conversationId !== active.id) {
        setActive((c) => c ? { ...c, id: result.conversationId } : c);
        setChats((list) => list.map((c) => c.id === active.id ? { ...c, id: result.conversationId } : c));
      }
      if (!result.incomplete) patchKMsg(kId, { done: true, progressText: null });
      else patchKMsg(kId, { done: false, error: lang === "zh" ? "连接中断，请重试" : "Connection interrupted — please retry", progressText: null });
      if (result.sources) {
        const cites = window.chat2AdaptCitations(result.sources, lang);
        setSources(cites);
        setRailTab("sources");
        setHighlight(null);
      }
      await window.refreshChat2Wallet();
    }
  };

  const onCite = (ref) => { setRailTab("sources"); setHighlight(ref); if (mobile) setMSub("sources"); };

  // Loading guard: while Privy is still initializing, show a boot screen so
  // we never flash the app or the auth gate at an unauthenticated user.
  if (!privy.ready) return <BootMsg2 kind="loading" lang={lang} onLogout={() => { try { privy.logout(); } catch (_) {} }} />;

  // Authenticated-app data gate: while initial data is loading or errored with
  // no cached regs, show BootMsg2/retry at this position. Never falls back to
  // mock. On background refresh (tick) with cached regs, the UI stays put.
  if (loading && regs.length === 0) return <BootMsg2 kind="loading" lang={lang} onLogout={() => { try { privy.logout(); } catch (_) {} }} />;
  if (error && regs.length === 0) return <BootMsg2 kind="error" lang={lang} detail={error} onLogout={() => { try { privy.logout(); } catch (_) {} }} />;

  const modals = (
    <React.Fragment>
      {topup && <TopupModal suggest={topup.suggest} onClose={() => setTopup(null)} />}
      {updReg && <UpdateModal reg={updReg} onClose={() => setUpdReg(null)} onTopup={(gap) => { setUpdReg(null); openTopupModal(gap); }} />}
      {claimReg && <ClaimModal reg={claimReg} lang={lang} onClose={() => setClaimReg(null)} />}
      {wd && <WithdrawModal onClose={() => setWd(false)} />}
      {insuff && <InsuffModal need={insuff.need} bal={insuff.bal} onClose={() => setInsuff(null)} onTopup={() => { setInsuff(null); openTopupModal(insuff.need); }} onByok={() => { setInsuff(null); openAddModelModal(); }} />}
      {addModel && <window.AddModelModal onClose={() => setAddModel(false)} onSaved={(m) => { setAddModel(false); setModel(m.id); }} />}
      {ansViewSlug && <AnswerViewerModal slug={ansViewSlug} onClose={() => setAnsViewSlug(null)} />}
    </React.Fragment>
  );

  const pageFor = (v) => {
    if ((v === "wallet" || v === "earnings" || v === "admin") && !privy.authenticated) {
      const titleKey = v === "wallet" ? "anonWalletTitle" : v === "earnings" ? "anonEarningsTitle" : "anonWalletTitle";
      const descKey = v === "wallet" ? "anonWalletDesc" : v === "earnings" ? "anonEarningsDesc" : "anonWalletDesc";
      return (
        <AnonCtaPlaceholder
          titleKey={titleKey}
          descKey={descKey}
          lang={lang}
          onLogin={triggerLogin}
        />
      );
    }
    if (v === "market") return <MarketPage lang={lang} regs={regs} onAsk={openPersona} onCreate={() => changeView("create")} onClaim={openClaimModal} onUpdate={openUpdateModal} />;
    if (v === "create") return <CreatePage lang={lang} onOpenPersona={openPersona} onTopup={(gap) => openTopupModal(gap)} />;
    if (v === "wallet") return <WalletPage2 lang={lang} onTopup={() => openTopupModal(null)} />;
    if (v === "earnings") return <EarningsPage lang={lang} onWithdraw={() => setWd(true)} onClaimOwn={() => changeView("market")} />;
    if (v === "admin") return <AdminPage lang={lang} />;
    return null;
  };

  /* ---------------- mobile ---------------- */
  if (mobile) {
    const tab = view === "chat" ? "chat" : view === "admin" ? "earnings" : view;
    let body;
    if (view === "chat" && activeKol) {
      body = (
        <div className="m-body chat-body">
          <div className="m-subtabs">
            <button className={"tab" + (mSub === "ask" ? " on" : "")} onClick={() => setMSub("ask")}>
              <window.RXC.Icon name="sparkles" size={13} />{aT("tabAsk")}
            </button>
            <button className={"tab" + (mSub === "sources" ? " on" : "")} onClick={() => setMSub("sources")}>
              <window.RXC.Icon name="quote" size={13} />{aT("railSources")}{sources.length > 0 ? " · " + sources.length : ""}
            </button>
          </div>
          {mSub === "ask"
            ? <ChatArea2 kol={activeKol} reg={activeReg} messages={messages} model={curModel} onCite={onCite} onAsk={ask} threadRef={threadRef} models={models} modelId={model} setModel={setModel} effort={effort} setEffort={setEffort} onAddModel={openAddModelModal} />
            : <Rail2 kol={activeKol} reg={activeReg} sources={sources} railTab={railTab} setRailTab={setRailTab} highlight={highlight} mobile={true} />}
        </div>
      );
    } else if (view === "chat") {
      body = (
        <div className="m-body">
          <div className="m-nochat">
            <window.RXC.Icon name="sparkles" size={26} color="var(--ghost)" />
            <p>{aT("selectPersona")}</p>
            <button className="auth-primary" style={{ maxWidth: 220 }} onClick={() => changeView("market")}>{T2("navMarket")}</button>
          </div>
        </div>
      );
    } else {
      body = <div className="m-body scrollable">{pageFor(view)}</div>;
    }
    return (
      <div className="app mobile">
        <MobileTopBar2 kol={view === "chat" ? activeKol : null} lang={lang} setLang={setLang} theme={theme} setTheme={setTheme} />
        {body}
        <BottomNav2 tab={tab} setTab={changeView} srcCount={sources.length} lang={lang} />
        {modals}
      </div>
    );
  }

  /* ---------------- desktop ---------------- */
  const viewMeta = {
    market: ["layers", T2("navMarket"), T2("deskSub2")],
    create: ["bot", T2("navCreate"), T2("crOnce")],
    wallet: ["wallet", T2("wTitle"), T2("wCredits")],
    earnings: ["trendUp", T2("eTitle"), T2("eOwnerSub")],
    admin: ["shield", T2("adTitle"), T2("adSub")],
  };
  const vm = viewMeta[view] || viewMeta.market;
  return (
    <div className="app">
      <Sidebar2 view={view} setView={changeView} regs={regs} chats={chats} active={active} onPickPersona={openPersona} onOpenChat={openChat} user={user} acct={acct} lang={lang} />
      <div className="main">
        <div className="topbar">
          {view === "chat" && activeKol ? (
            <div className="kol-id">
              <window.RXC.Avatar kol={activeKol} size={34} radius={9} className="face" />
              <div className="meta">
                <div className="nm">{activeKol.display_name} <span className="live-pill"><span className="dot"></span>{aT("online")}</span> <ClaimBadge claimed={activeReg && activeReg.claimed} /></div>
                <div className="role">{activeKol.role}</div>
              </div>
            </div>
          ) : (
            <div className="kol-id">
              <div className="brand-mark" style={{ width: 34, height: 34, borderRadius: 9 }}><window.RXC.Icon name={vm[0]} size={18} color="var(--on-accent)" /></div>
              <div className="meta">
                <div className="nm">{vm[1]}</div>
                <div className="role">{vm[2]}</div>
              </div>
            </div>
          )}
          <div className="spacer"></div>
          <button className="topbal" onClick={() => openTopupModal(null)} title={T2("wTopup")} aria-label={`${T2("wTopup")} · ${E.usd(acct.credits + acct.rewardBal)}`}>
            <window.RXC.Icon name="wallet" size={13} color="var(--accent)" />
            {E.usd(acct.credits + acct.rewardBal)}
            <span className="plus" aria-hidden="true">+</span>
          </button>
          <NotificationBell2 />
          <window.LangToggle lang={lang} setLang={setLang} />
          <window.RXC.ThemeMenu value={theme} onChange={setTheme} />
        </div>
        {view === "chat" && activeKol ? (
          <div className="center">
            <ChatArea2 kol={activeKol} reg={activeReg} messages={messages} model={curModel} onCite={onCite} onAsk={ask} threadRef={threadRef} models={models} modelId={model} setModel={setModel} effort={effort} setEffort={setEffort} onAddModel={openAddModelModal} />
            <Rail2 kol={activeKol} reg={activeReg} sources={sources} railTab={railTab} setRailTab={setRailTab} highlight={highlight} />
          </div>
        ) : pageFor(view)}
      </div>
      {modals}
    </div>
  );
}

/* ---------------- Root wrapper: fetch /api/config, mount PrivyProvider ---------------- */
function App2Wrapper() {
  const [appId, setAppId] = aS(null);   // null = loading, "" = error, string = ok
  const [err, setErr] = aS(null);
  aE(() => {
    let cancelled = false;
    fetch("/api/config")
      .then((r) => { if (!r.ok) throw new Error("config status " + r.status); return r.json(); })
      .then((j) => {
        if (cancelled) return;
        if (!j || !j.privyAppId) { setErr("missing privyAppId in /api/config"); return; }
        setAppId(j.privyAppId);
      })
      .catch((e) => { if (!cancelled) setErr(String((e && e.message) || e)); });
    return () => { cancelled = true; };
  }, []);
  if (err) return <BootMsg2 kind="error" lang={window.RXI.lang} detail={err} />;
  if (!appId) return <BootMsg2 kind="loading" lang={window.RXI.lang} />;
  const { PrivyProvider } = window.PrivySDK;
  if (!PrivyProvider) return <BootMsg2 kind="error" lang={window.RXI.lang} detail="window.PrivySDK.PrivyProvider missing" />;
  return (
    <PrivyProvider appId={appId} config={{ loginMethods: ["email", "google"] }}>
      <App2 />
    </PrivyProvider>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App2Wrapper />);
