/* global React, POOLS, ALLOWLIST, fmt, quote, I, ModeBadge, Mono, Hash, useNetwork */

const { useState, useMemo, useEffect } = React;

// =============================================================
// Local helpers (file-scoped; do NOT import cross-file)
// =============================================================
function SegBtn({ active, onClick, children, disabled, color }) {
  const c = color || "var(--purple)";
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      style={{
        padding: "6px 12px",
        borderRadius: 6,
        border: "1px solid " + (active ? c : "var(--line)"),
        background: active ? c : "var(--surface)",
        color: active ? "var(--ink)" : "var(--ink-2)",
        fontFamily: "var(--font-mono)",
        fontSize: 12,
        fontWeight: 500,
        cursor: disabled ? "not-allowed" : "pointer",
        opacity: disabled ? 0.5 : 1,
      }}
    >
      {children}
    </button>
  );
}

function FieldLabel({ children, hint }) {
  return (
    <label className="muted" style={{
      fontSize: 11,
      fontFamily: "var(--font-mono)",
      textTransform: "uppercase",
      letterSpacing: "0.08em",
      display: "flex",
      justifyContent: "space-between",
      alignItems: "baseline",
      gap: 8,
    }}>
      <span>{children}</span>
      {hint && <span className="dim" style={{ textTransform: "none", letterSpacing: 0 }}>{hint}</span>}
    </label>
  );
}

function InlineError({ children }) {
  return (
    <div style={{
      fontSize: 11.5,
      color: "var(--red)",
      fontFamily: "var(--font-mono)",
      marginTop: 4,
    }}>
      {children}
    </div>
  );
}

// Mock principal validation -- testnet ST..., mainnet SP...
function looksLikePrincipal(s) {
  if (!s) return false;
  return /^S[TP][0-9A-Z]{6,}/.test(s.trim());
}

// =============================================================
// Component 1 -- CreatePool
// =============================================================
const PRESETS = {
  conservative: { virtualStx: 500,  threshold: 2000,  label: "Small (floor)", desc: "Minimum capital required. Steeper curve. ~17% slippage on a 100 STX buy." },
  standard:     { virtualStx: 1000, threshold: 5000,  label: "Standard ⭐", desc: "Industry-standard ratio (5x). Balanced curve. ~9% slippage on a 100 STX buy." },
  aggressive:   { virtualStx: 2000, threshold: 10000, label: "Deep", desc: "More capital, smoother curve, lower slippage. ~5% slippage on a 100 STX buy." },
  custom:       { virtualStx: null, threshold: null,  label: "Custom", desc: "Manual tuning. Validates against guardrails." },
};

function CreatePool({ go, wallet, showError, onTx }) {
  const net = useNetwork();
  const [mode, setMode] = useState("chooser");

  if (mode === "chooser") {
    return (
      <div className="page">
        <div className="hero" style={{ paddingBottom: 8 }}>
          <div className="eyebrow">Create a pool</div>
          <h1 className="h1" style={{ fontSize: 36 }}>Choose pool mode</h1>
          <p className="lead">Two paths to a SIP-010 token paired with native STX. Both deploy a fresh
            template instance and a singleton pool entry.</p>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18, marginTop: 18 }}>
          <ChooserCard
            kind="direct"
            title="Direct pool"
            subtitle="You seed the pool with STX. Standard XYK from day one."
            bullets={[
              "5% fee on every swap (4.5% to fee-receiver, 0.5% to protocol).",
              "swap-and-burn enabled from day one.",
              "Minimum seed: 100 STX.",
            ]}
            cta="Choose direct"
            onChoose={() => setMode("direct")}
          />
          <ChooserCard
            kind="bonding"
            title="Bonding pool"
            subtitle="No seed. Graduates atomically when STX collected crosses the threshold."
            bullets={[
              "1% fee during bonding (0.9% / 0.1%). 5% post-graduation.",
              "Sells capped by accumulated real STX.",
              "swap-and-burn unlocks at graduation.",
            ]}
            cta="Choose bonding"
            onChoose={() => setMode("bonding")}
          />
        </div>
        <MainnetDeployModal/>
      </div>
    );
  }

  return (
    <div className="page">
      <div style={{ marginBottom: 12 }}>
        <button className="btn btn-ghost" onClick={() => setMode("chooser")} style={{ fontSize: 12 }}>
          <span style={{ display: "inline-block", transform: "rotate(180deg)" }}><I.arrow/></span> Choose mode
        </button>
      </div>
      {mode === "direct"  && <DirectForm  wallet={wallet} showError={showError} onTx={onTx}/>}
      {mode === "bonding" && <BondingForm wallet={wallet} showError={showError} onTx={onTx}/>}
      <MainnetDeployModal/>
    </div>
  );
}

function ChooserCard({ kind, title, subtitle, bullets, cta, onChoose }) {
  const isBonding = kind === "bonding";
  return (
    <div className="card" style={{
      padding: 22,
      background: isBonding ? "linear-gradient(180deg, rgba(249,115,22,0.08) 0%, transparent 100%)" : "var(--surface)",
      borderColor: isBonding ? "rgba(249,115,22,0.30)" : "var(--line)",
      display: "flex",
      flexDirection: "column",
      gap: 14,
    }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
        <ModeBadge mode={kind}/>
      </div>
      <h2 style={{ margin: 0, fontSize: 22, fontWeight: 600 }}>{title}</h2>
      <p className="muted" style={{ margin: 0, fontSize: 13 }}>{subtitle}</p>
      <ul style={{ margin: 0, paddingLeft: 18, fontSize: 13, color: "var(--ink-2)", display: "flex", flexDirection: "column", gap: 6 }}>
        {bullets.map((b, i) => <li key={i}>{b}</li>)}
      </ul>
      <div style={{ marginTop: "auto" }}>
        <button
          className={isBonding ? "btn" : "btn btn-primary"}
          style={isBonding ? { background: "var(--orange)", color: "var(--ink)", borderColor: "transparent" } : null}
          onClick={onChoose}
        >
          {cta} <I.arrow/>
        </button>
      </div>
    </div>
  );
}

// -------------------------------------------------------------
// DirectForm
// -------------------------------------------------------------
function DirectForm({ wallet, showError, onTx }) {
  const net = useNetwork();
  const [name, setName] = useState("");
  const [symbol, setSymbol] = useState("");
  const [supply, setSupply] = useState(1_000_000_000);
  const [uri, setUri] = useState("");
  const [stxSeed, setStxSeed] = useState(100);
  // Prefer fullAddress when available (real wallet); fall back to truncated
  // form (mock wallets in the tweaks panel set only `address`). Using the
  // truncated form here would fail looksLikePrincipal validation.
  const [feeReceiver, setFeeReceiver] = useState(
    (wallet && (wallet.fullAddress || wallet.address)) || ""
  );
  // useState() seed only runs on first mount, so if the form rendered before
  // the wallet finished connecting (common: user lands on /create then clicks
  // Connect, or async stx_getAddresses resolves after this hook ran) the
  // field stays empty forever. Re-seed when an address becomes available,
  // but only if the user has NOT typed anything yet (don't clobber edits).
  const walletAddr = (wallet && (wallet.fullAddress || wallet.address)) || "";
  useEffect(() => {
    if (walletAddr && !feeReceiver) setFeeReceiver(walletAddr);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [walletAddr]);

  const errs = {
    stxSeed:  !(stxSeed >= 100) ? "Minimum 100 STX." : null,
    supply:
      !(supply > 0)        ? "Supply must be > 0." :
      supply < MIN_SUPPLY  ? `Min supply: 100M tokens (industry norm: 1B). Pool with fewer is economically unviable.` :
      supply > MAX_SUPPLY  ? `Max supply: ${MAX_SUPPLY.toLocaleString()} tokens. Above this is absurd / overflow risk.` :
      null,
    name:     name.length > 32 ? "Max 32 chars." : null,
    symbol:   symbol.length > 10 ? "Max 10 chars." : null,
    feeReceiver: feeReceiver && !looksLikePrincipal(feeReceiver) ? "Invalid principal." : null,
  };
  const hasErr = Object.values(errs).some(Boolean) || !name || !symbol || !feeReceiver;

  const initialPrice = supply > 0 ? stxSeed / supply : 0;

  const submit = () => {
    if (!wallet || !wallet.connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (hasErr) return showError("Fix validation errors before submitting.");
    onTx({
      summary: "Create direct pool",
      detail: name + " · " + symbol + " · seed " + fmt.stx(stxSeed) + " STX",
    });
  };

  return (
    <div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 18 }}>
      <div className="card" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
        <div className="eyebrow">Direct pool</div>

        <div className="field">
          <FieldLabel hint={`${name.length}/32`}>Name</FieldLabel>
          <input className="input" placeholder="My Token" value={name} onChange={e => setName(e.target.value)}/>
          {errs.name && <InlineError>{errs.name}</InlineError>}
        </div>

        <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr", gap: 10 }}>
          <div className="field">
            <FieldLabel hint={`${symbol.length}/10`}>Symbol</FieldLabel>
            <input className="input mono" placeholder="MYT" value={symbol} onChange={e => setSymbol(e.target.value.toUpperCase())}/>
            {errs.symbol && <InlineError>{errs.symbol}</InlineError>}
          </div>
          <div className="field">
            <FieldLabel hint="locked">Decimals</FieldLabel>
            <input
              className="input mono"
              type="number"
              value={TOKEN_DECIMALS}
              readOnly
              title="Locked to 6 by the frontend (STX-native convention). See research/m3-followups.md."
              style={{ opacity: 0.6, cursor: "not-allowed" }}
            />
          </div>
          <div className="field">
            <FieldLabel hint={fmt.big(supply)}>Supply</FieldLabel>
            <input className="input mono" type="number" value={Number.isFinite(supply) ? supply : ""} onChange={e => setSupply(parseFloat(e.target.value))}/>
          </div>
        </div>

        <div className="field">
          <FieldLabel hint="optional">Token URI</FieldLabel>
          <input className="input mono" placeholder="https://..." value={uri} onChange={e => setUri(e.target.value)}/>
        </div>

        <div className="field">
          <FieldLabel hint="min 100 STX">STX seed</FieldLabel>
          <input className="input mono" type="number" min={100} value={Number.isFinite(stxSeed) ? stxSeed : ""} onChange={e => setStxSeed(parseFloat(e.target.value))}/>
          {errs.stxSeed && <InlineError>{errs.stxSeed}</InlineError>}
        </div>

        <div className="field">
          <FieldLabel>Fee receiver</FieldLabel>
          <input className="input mono" placeholder="ST..." value={feeReceiver} onChange={e => setFeeReceiver(e.target.value)}/>
          {errs.feeReceiver && <InlineError>{errs.feeReceiver}</InlineError>}
        </div>

        {/* Supply min/max -- block. Callout style matches BondingForm. */}
        {errs.supply && (
          <div style={{
            padding: 10,
            border: "1px solid var(--red)",
            borderRadius: 8,
            background: "rgba(239,68,68,0.08)",
            color: "var(--red)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>{errs.supply}</div>
          </div>
        )}

        {/* Supply soft-warn -- below 1B industry norm but allowed. */}
        {!errs.supply && supply >= MIN_SUPPLY && supply < SOFT_WARN_SUPPLY && (
          <div style={{
            padding: 10,
            border: "1px solid var(--yellow)",
            borderRadius: 8,
            background: "rgba(245,158,11,0.08)",
            color: "var(--yellow)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>
              Supply {supply.toLocaleString()} below industry standard of 1B tokens.
              Smaller supplies may signal niche/test tokens. Consider increasing to 1B.
            </div>
          </div>
        )}

        <button
          className="btn btn-primary"
          style={{ width: "100%", justifyContent: "center", padding: "10px 14px", marginTop: 6 }}
          disabled={!!(wallet && wallet.connected) && hasErr}
          onClick={submit}
        >
          {wallet && wallet.connected ? "Sign and create pool" : "Connect wallet"}
        </button>
      </div>

      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 12 }}>Preview</div>
        <div className="kv"><span className="k">Mode</span><span className="v"><ModeBadge mode="direct"/></span></div>
        <div className="kv"><span className="k">Fee schedule</span><span className="v" style={{ fontSize: 12 }}>5% (450 + 50 bps)</span></div>
        <div className="kv"><span className="k">Total to pay</span><span className="v"><Mono>{fmt.stx(stxSeed)} STX</Mono></span></div>
        <div className="kv"><span className="k">Initial price</span>
          <span className="v"><Mono>{initialPrice > 0 ? initialPrice.toExponential(3) : "--"}</Mono> <span className="dim" style={{ fontSize: 11 }}>STX/{symbol || "TKN"}</span></span>
        </div>

        <div className="hr"/>
        <div className="eyebrow" style={{ marginBottom: 8, fontSize: 10.5 }}>Slippage @ buy size</div>
        {[10, 100, 1000].map((dx) => {
          const slip = stxSeed > 0 ? (dx / stxSeed) * 100 : 0;
          const color = slip >= 20 ? "var(--red)" : slip >= 10 ? "var(--orange)" : "var(--green)";
          return (
            <div className="kv" key={dx}>
              <span className="k"><Mono>{dx} STX</Mono> buy</span>
              <span className="v mono" style={{ color }}>{stxSeed > 0 ? slip.toFixed(2) + "%" : "--"}</span>
            </div>
          );
        })}

        <div className="hr"/>
        <div className="eyebrow" style={{ marginBottom: 8, fontSize: 10.5 }}>Curve sketch</div>
        <DirectCurveSketch stxSeed={stxSeed} supply={supply}/>
        <div className="muted mono" style={{ fontSize: 10.5, marginTop: 4, display: "flex", justifyContent: "space-between" }}>
          <span>0 STX</span><span>{stxSeed > 0 ? fmt.stx(stxSeed * 5) + " STX" : "-- STX"}</span>
        </div>

        <div className="hr"/>
        <div className="muted" style={{ fontSize: 11.5 }}>
          A new instance of <Mono>{net.tokenTemplateName}</Mono> is deployed and registered against
          <Mono> {net.singletonName}</Mono>. The token's source hash must match <Mono>APPROVED_TOKEN_HASH</Mono>.
        </div>
      </div>
    </div>
  );
}

// -------------------------------------------------------------
// BondingForm  (with PRESETS + ratio-floor validation + curve sketch)
// -------------------------------------------------------------

// Locked at 6 in the UI for v1: matches STX-native conventions (Velar /
// STX.City). The token template's `initialize` accepts arbitrary decimals,
// but exposing the choice to deployers adds confusion without a clear
// economic upside, so we ship a single value. See research/m3-followups.md.
const TOKEN_DECIMALS = 6;
// Precomputed: 10n ** 6n. Babel-standalone's `**` transpilation calls Math.pow,
// which throws on BigInt operands. Hardcoded literal sidesteps the transform.
const TOKEN_DECIMALS_POW = 1_000_000n;

// Minimum fraction of supply that must be released through the curve
// before graduation, in basis points. Anchored to the ecosystem norm:
// STX.City releases ~78.7% (~7870 bps), pump.fun ~79.31% (~7931 bps).
// See /tmp/bonding-curve-degenerate-survey-report.md. We pick a more
// permissive 60% floor so the UI rejects the obvious degenerate-curve
// footgun (virtual reserves so high that ~0 tokens are sold pre-grad)
// while still allowing flatter curves than the typical launchpad. The
// singleton has no upper bound on virtual-stx (only a floor), so this
// is a frontend-only guardrail until M3 ships ERR_DEGENERATE_CURVE.
const MIN_TOKENS_RELEASED_BPS = 6000;

// Anchored to research/graduation-virtual-ratio-survey (Obsidian vault).
// STX.City n=20 mainnet pools all use ratio = graduation/virtual = 5x
// (19/20 exactly 5.0, one at 4.99). Soft warn over 5x (off-norm but
// allowed); hard block over 10x (2x slack, kills the 5e27 footgun).
// The hard block matches the proposed M3 contract cap so frontend and
// contract share a single source of truth -- see research/m3-followups.md.
const RATIO_SOFT_WARN = 5;
const RATIO_HARD_BLOCK = 10;
// Absolute cap on graduation threshold (defense in depth: prevents
// astronomical thresholds even at "valid" ratios). 10M STX matches the
// proposed M3 MAX_GRADUATION_STX = u10000000000000.
const MAX_GRADUATION_STX = 10_000_000;

// Anchor: industry norm = 1B tokens (pump.fun, STX.City). 100M is 10% of
// that -- lenient lower bound, blocks economically unviable supplies.
// M3 contract enforcement tracked en research/m3-followups.md / PD-08.
const MIN_SUPPLY = 100_000_000;

// Industry social norm: 1B tokens (pump.fun, moonshot, sunpump). Below this
// is allowed but flagged. Anchor: research/supply-minimum-survey.md.
const SOFT_WARN_SUPPLY = 1_000_000_000;

// Hard ceiling: 1e15 whole tokens. With decimals=6 atomic = 1e21 (well below
// uint128 / Clarity uint range). Defense against overflow + absurd inputs.
// M3 contract enforcement tracked en research/m3-followups.md / PD-08.
const MAX_SUPPLY = 1_000_000_000_000_000;  // 1e15

// Locked supply for non-custom presets: 1B = industry standard
// (pump.fun / STX.City / sunpump). Custom mode keeps it editable.
const PRESET_LOCKED_SUPPLY = 1_000_000_000;

function BondingForm({ wallet, showError, onTx }) {
  const net = useNetwork();
  const [preset, setPreset] = useState("standard");
  const presetCfg = PRESETS[preset];
  const presetLocked = preset !== "custom";

  const [virtualStx, setVirtualStx]                 = useState(presetCfg.virtualStx || 1000);
  const [graduationThreshold, setGraduationThreshold] = useState(presetCfg.threshold || 5000);

  const [name, setName] = useState("");
  const [symbol, setSymbol] = useState("");
  // decimals is locked at TOKEN_DECIMALS (6) -- no state, displayed read-only
  // so the user sees the value being used.
  const [supply, setSupply] = useState(1_000_000_000);
  const [uri, setUri] = useState("");
  // Prefer fullAddress (real wallet); fall back to truncated for mock wallets.
  const [feeReceiver, setFeeReceiver] = useState(
    (wallet && (wallet.fullAddress || wallet.address)) || ""
  );
  // See DirectForm: useState() seed runs once; re-seed when wallet address
  // appears, unless the user has already typed something into the field.
  const walletAddr = (wallet && (wallet.fullAddress || wallet.address)) || "";
  useEffect(() => {
    if (walletAddr && !feeReceiver) setFeeReceiver(walletAddr);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [walletAddr]);

  // Two-stage submit lifecycle for the deploy → create-pool flow.
  // stage:  "" | "deploying" | "waiting-deploy" | "creating-pool"
  const [stage, setStage] = useState("");
  const submitting = stage !== "";

  // When preset changes, pre-fill the (now disabled) inputs.
  // Supply also locks to 1B for any non-custom preset (industry standard).
  useEffect(() => {
    if (preset !== "custom") {
      setVirtualStx(PRESETS[preset].virtualStx);
      setGraduationThreshold(PRESETS[preset].threshold);
      setSupply(PRESET_LOCKED_SUPPLY);
    }
  }, [preset]);

  // Validations -- inline u-codes. Negated comparisons (`!(x >= MIN)`) catch
  // NaN that lands in state when a numeric input is cleared.
  const errs = {
    virtualStx:  !(virtualStx          >= 500)  ? "Virtual STX min 500." : null,
    threshold:   !(graduationThreshold >= 2000) ? "Threshold min 2000." : null,
    supply:
      !(supply > 0)        ? "Supply must be > 0." :
      supply < MIN_SUPPLY  ? `Min supply: 100M tokens (industry norm: 1B). Pool with fewer is economically unviable.` :
      supply > MAX_SUPPLY  ? `Max supply: ${MAX_SUPPLY.toLocaleString()} tokens. Above this is absurd / overflow risk.` :
      null,
    name:        name.length > 32 ? "Max 32 chars." : null,
    symbol:      symbol.length > 10 ? "Max 10 chars." : null,
    feeReceiver: feeReceiver && !looksLikePrincipal(feeReceiver) ? "Invalid principal." : null,
  };

  // Ratio floor: virtualStxMicro * 200_000_000 >= supplyMicro.
  // Skip when either field is blank/NaN: errs.virtualStx / errs.supply
  // already cover the empty case, and showing "ratio violated" while the
  // user is mid-edit is noise.
  const ratioOK = useMemo(() => {
    if (!Number.isFinite(virtualStx) || !Number.isFinite(supply)) return true;
    const vSMicro = virtualStx * 1e6;
    const sMicro  = supply * Math.pow(10, TOKEN_DECIMALS);
    return vSMicro * 2e8 >= sMicro;
  }, [virtualStx, supply]);

  // Live preview math
  const initialPrice = supply > 0 ? virtualStx / supply : 0;

  // Tokens released at graduation in display units (XYK math:
  // supply * gradThr / (vS + gradThr)). Convert to atomic for the floor
  // check -- 1e6 atomic = 1.0 token at TOKEN_DECIMALS=6.
  const tokensReleasedAtGrad = supply > 0 && (virtualStx + graduationThreshold) > 0
    ? supply * (graduationThreshold / (virtualStx + graduationThreshold))
    : 0;
  const tokensAtGradAtomic = Math.floor(tokensReleasedAtGrad * Math.pow(10, TOKEN_DECIMALS));
  // Floor expressed as a fraction of supply. Below this the curve is
  // degenerate -- virtual reserve dominates and <60% of supply lands
  // with real buyers pre-grad. Compare in BigInt so we don't lose
  // precision for large supplies.
  const supplyAtomic = supply > 0
    ? BigInt(Math.floor(Number(supply))) * TOKEN_DECIMALS_POW
    : 0n;
  const minTokensAtGrad = (supplyAtomic * BigInt(MIN_TOKENS_RELEASED_BPS)) / 10000n;
  const curveDegenerate = supply > 0 && BigInt(tokensAtGradAtomic) < minTokensAtGrad;
  // Runtime % released, surfaced in the warning text.
  const releasedPct = supply > 0 && tokensReleasedAtGrad > 0
    ? (tokensReleasedAtGrad / supply) * 100
    : 0;

  // graduation/virtual ratio guardrail. Soft warn vs STX.City norm;
  // hard block matches proposed M3 contract cap. See top-of-file
  // RATIO_SOFT_WARN / RATIO_HARD_BLOCK / MAX_GRADUATION_STX comment.
  const ratio = virtualStx > 0 ? graduationThreshold / virtualStx : 0;
  const ratioOverWarn = ratio > RATIO_SOFT_WARN;
  const ratioOverBlock = ratio > RATIO_HARD_BLOCK;
  const graduationOverCap = graduationThreshold > MAX_GRADUATION_STX;
  const ratioInvalid = ratioOverBlock || graduationOverCap;

  const hasErr =
    Object.values(errs).some(Boolean) ||
    !name || !symbol || !feeReceiver ||
    !ratioOK || curveDegenerate || ratioInvalid;

  // 100 STX test buy via quote()
  const testQuote = useMemo(() => {
    if (!(supply > 0) || !(virtualStx >= 500)) return null;
    const mockPool = {
      virtualStx,
      bondedStxCollected: 0,
      virtualToken: supply,
      bondedTokensSold: 0,
    };
    return quote({ pool: mockPool, mode: "bonding", dir: "buy", amountIn: 100 });
  }, [virtualStx, supply]);

  // Two-tx flow:
  //   tx1) contract-publish a new instance of restricted-token-template-v6
  //        under {wallet}.{tokenName}. Source bytes are fetched from the
  //        already-deployed template (which hashes to APPROVED_TOKEN_HASH),
  //        so re-deploy preserves the hash invariant -- no rewrite logic.
  //   wait) poll Hiro until tx1.tx_status === "success".
  //   tx2)  call lp-singleton-v6.create-pool-bonding(token, name, symbol,
  //         decimals, supply, uri, virtual-stx, graduation-threshold,
  //         fee-receiver). Bonding pools take no STX seed -> empty PCs.
  //
  // Both tx's surface as toasts via onTx(...) and reuse useTxTracker for
  // their lifecycle (pending -> success/error). The form button is
  // disabled across both stages so the user can't double-broadcast.
  const submit = async () => {
    if (!wallet || !wallet.connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (hasErr) return showError("Fix validation errors before submitting.");

    const userAddress = wallet.fullAddress;
    if (!userAddress) {
      return showError("Wallet missing fullAddress -- reconnect.");
    }

    // Contract-name slug from symbol. Stacks names: must start with a letter,
    // contain only [a-zA-Z0-9-_!?+<>=/*]; we conservatively allow [a-z0-9-]
    // and cap at 40 chars. Symbol is uppercased in the form, so lowercase here.
    const tokenName = symbol.trim().toLowerCase()
      .replace(/[^a-z0-9-]/g, "-")
      .replace(/^-+|-+$/g, "")
      .slice(0, 40);
    if (!tokenName || !/^[a-z]/.test(tokenName)) {
      return showError("Symbol must start with a letter and contain only a-z, 0-9, hyphens.");
    }

    const tokenPrincipal = `${userAddress}.${tokenName}`;

    // uint conversions: form units are display units; chain wants base units.
    // decimals is locked at TOKEN_DECIMALS (6) so 10^6 is small enough to fit
    // safely in Number, but use BigInt for symmetry with virtualStx/grad.
    const supplyBase     = BigInt(Math.floor(Number(supply))) * TOKEN_DECIMALS_POW;
    const virtualStxUstx = BigInt(Math.floor(Number(virtualStx) * 1_000_000));
    const graduationUstx = BigInt(Math.floor(Number(graduationThreshold) * 1_000_000));

    // Local tracker so catch() can label the failure correctly. React's
    // `stage` state is captured at the closure's render and won't reflect
    // intra-submit transitions.
    let currentStage = "deploying";
    setStage(currentStage);
    try {
      // Layer C guardrail: bail before the wallet sees the tx if its
      // active network does not match the app. Cancel and reconnect both
      // throw; quietly exit so finally below resets stage.
      try {
        await window.assertWalletReady(net, wallet);
      } catch (_) {
        return;
      }

      // Mainnet confirm gate (Q6). Fires AFTER validation but BEFORE the
      // first wallet popup; user cancel rejects with a recognizable error
      // so the form returns to editable state without a noisy error toast.
      if (net.id === "mainnet") {
        try {
          await showMainnetDeployModal({
            tokenName,
            feeReceiver,
            tokenTemplateName: net.tokenTemplateName,
            singletonShort:    net.singletonName,
            deployFeeMicroStx: net.deployFeeMicroStx,
          });
        } catch (_) {
          // user cancelled -- clean exit; finally below resets stage.
          return;
        }
      }

      // ---- tx1: deploy a fresh token instance ----
      const source = await window.HiroApi.fetchTokenTemplateSource(net);
      // Path A: build tx in-page + sign-via-wallet + broadcast ourselves.
      // signAndDeployContract (Connect 8 stx_deployContract) has a builder bug
      // that corrupts the contract name on chain; see api.jsx notes.
      const deploy = await window.HiroApi.buildAndSignDeployContract({
        name:    tokenName,
        source,
        cfg:     net,
        network: net.id,
        clarityVersion: 4,
      });

      onTx({
        summary: `Deploy token ${tokenName}`,
        detail:  `${name} - ${symbol} - ${tokenPrincipal}`,
        txid:    deploy.txid,
      });

      currentStage = "waiting-deploy";
      setStage(currentStage);
      await window.HiroApi.waitForTx(deploy.txid, net);

      // ---- tx2: register the pool against the freshly-deployed token ----
      currentStage = "creating-pool";
      setStage(currentStage);
      const pool = await window.HiroApi.signAndBroadcast({
        contractAddress: net.deployer,
        contractName:    net.singletonName,
        fnName:          "create-pool-bonding",
        fnArgs: [
          { kind: "principal",     value: tokenPrincipal },
          { kind: "string-ascii",  value: name.slice(0, 32) },
          { kind: "string-ascii",  value: symbol.slice(0, 32) },
          { kind: "uint",          value: BigInt(TOKEN_DECIMALS) },
          { kind: "uint",          value: supplyBase },
          { kind: "optional-utf8", value: uri || null },
          { kind: "uint",          value: virtualStxUstx },
          { kind: "uint",          value: graduationUstx },
          { kind: "principal",     value: feeReceiver },
        ],
        // Bonding pools don't pull STX. "allow" + empty PCs is fine for the
        // demo; a strict deny-mode equivalent would still have no transfers
        // to gate. M3 will add proper PCs for direct mode (where the seed
        // does transfer).
        postConditions:    [],
        postConditionMode: "allow",
        cfg:               net,
      });

      onTx({
        summary: `Create bonding pool ${symbol}`,
        detail:  `${tokenPrincipal} · vS ${virtualStx} / thr ${graduationThreshold}`,
        txid:    pool.txid,
      });
    } catch (e) {
      const stageLabel =
        currentStage === "deploying"      ? "deploy"             :
        currentStage === "waiting-deploy" ? "deploy confirm"     :
        currentStage === "creating-pool"  ? "create-pool-bonding" :
                                            "submit";
      showError(`${stageLabel} failed: ${e?.message || String(e)}`);
    } finally {
      setStage("");
    }
  };

  const buttonLabel =
    !wallet || !wallet.connected ? "Connect wallet" :
    stage === "deploying"        ? "Confirm deploy in wallet…" :
    stage === "waiting-deploy"   ? "Waiting for deploy to confirm…" :
    stage === "creating-pool"    ? "Confirm create-pool in wallet…" :
                                   "Sign and create bonding pool";

  return (
    <div style={{ display: "grid", gridTemplateColumns: "1.4fr 1fr", gap: 18 }}>
      <div className="card" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
        <div className="eyebrow">Bonding pool</div>

        {/* Preset chips */}
        <div>
          <FieldLabel>Curve preset</FieldLabel>
          <div style={{ display: "flex", gap: 6, flexWrap: "wrap", marginTop: 6 }}>
            {Object.keys(PRESETS).map(k => (
              <SegBtn key={k} active={preset === k} onClick={() => setPreset(k)} color="var(--orange)">
                {PRESETS[k].label}
              </SegBtn>
            ))}
          </div>
          <div className="muted" style={{ fontSize: 11.5, marginTop: 6 }}>{presetCfg.desc}</div>
        </div>

        {/* Curve params: compact summary in preset mode, full inputs in custom */}
        {preset !== "custom" ? (
          <div
            className="mono"
            style={{
              fontSize: 12,
              padding: "8px 10px",
              background: "rgba(255,255,255,0.02)",
              border: "1px solid rgba(255,255,255,0.08)",
              borderRadius: 6,
              lineHeight: 1.5,
            }}
          >
            <span style={{ color: "var(--muted)", fontSize: 10.5, letterSpacing: 1, textTransform: "uppercase", marginRight: 8 }}>
              Curve
            </span>
            <strong>{virtualStx} STX</strong> seed
            {" → "}
            <strong>{graduationThreshold} STX</strong> graduation target
            <span className="muted" style={{ marginLeft: 8 }}>
              ({ratio.toFixed(0)}x ratio · ~{releasedPct.toFixed(0)}% supply released pre-grad)
            </span>
          </div>
        ) : (
          <>
            <div className="muted" style={{ fontSize: 11.5, lineHeight: 1.45 }}>
              Bonding pools start empty -- buyers fill them by buying.{" "}
              <strong>virtual-stx</strong> shapes the curve (synthetic, you don't pay it).{" "}
              <strong>graduation-threshold</strong> is the real STX target buyers must
              collectively deposit before the pool graduates to AMM mode.
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
              <div className="field">
                <FieldLabel hint="curve seed (STX), min 500">virtual-stx</FieldLabel>
                <input
                  className="input mono"
                  type="number"
                  value={Number.isFinite(virtualStx) ? virtualStx : ""}
                  onChange={e => setVirtualStx(parseFloat(e.target.value))}
                />
                {errs.virtualStx && <InlineError>{errs.virtualStx}</InlineError>}
                {curveDegenerate && !errs.virtualStx && (
                  <InlineError>
                    Curve too flat: only {releasedPct.toFixed(1)}% of supply released at graduation.
                    Established launchpads typically release ~79%. Increase virtual-stx or
                    graduation-threshold ratio to fix.
                  </InlineError>
                )}
              </div>
              <div className="field">
                <FieldLabel hint="STX target, min 2000">graduation-threshold</FieldLabel>
                <input
                  className="input mono"
                  type="number"
                  value={Number.isFinite(graduationThreshold) ? graduationThreshold : ""}
                  onChange={e => setGraduationThreshold(parseFloat(e.target.value))}
                />
                {errs.threshold && <InlineError>{errs.threshold}</InlineError>}
              </div>
            </div>
          </>
        )}

        {/* Common token fields */}
        <div className="field">
          <FieldLabel hint={`${name.length}/32`}>Name</FieldLabel>
          <input className="input" placeholder="My Token" value={name} onChange={e => setName(e.target.value)}/>
          {errs.name && <InlineError>{errs.name}</InlineError>}
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr", gap: 10 }}>
          <div className="field">
            <FieldLabel hint={`${symbol.length}/10`}>Symbol</FieldLabel>
            <input className="input mono" placeholder="MYT" value={symbol} onChange={e => setSymbol(e.target.value.toUpperCase())}/>
            {errs.symbol && <InlineError>{errs.symbol}</InlineError>}
          </div>
          <div className="field">
            <FieldLabel hint="locked">Decimals</FieldLabel>
            <input
              className="input mono"
              type="number"
              value={TOKEN_DECIMALS}
              readOnly
              title="Locked to 6 by the frontend (STX-native convention). See research/m3-followups.md."
              style={{ opacity: 0.6, cursor: "not-allowed" }}
            />
          </div>
          <div className="field">
            <FieldLabel hint={presetLocked ? "locked to 1B" : fmt.big(supply)}>Supply</FieldLabel>
            <input
              className="input mono"
              type="number"
              disabled={presetLocked}
              value={Number.isFinite(supply) ? supply : ""}
              onChange={e => setSupply(parseFloat(e.target.value))}
              title={presetLocked ? "Locked to 1B for presets. Switch to Custom to edit." : undefined}
              style={presetLocked ? { opacity: 0.6, cursor: "not-allowed" } : undefined}
            />
            {presetLocked && (
              <span className="muted" style={{ fontSize: 11, marginTop: 4, display: "block" }}>
                Locked to 1B for presets. Switch to Custom to edit.
              </span>
            )}
          </div>
        </div>
        <div className="field">
          <FieldLabel hint="optional">Token URI</FieldLabel>
          <input className="input mono" placeholder="https://..." value={uri} onChange={e => setUri(e.target.value)}/>
        </div>
        <div className="field">
          <FieldLabel>Fee receiver</FieldLabel>
          <input className="input mono" placeholder="ST..." value={feeReceiver} onChange={e => setFeeReceiver(e.target.value)}/>
          {errs.feeReceiver && <InlineError>{errs.feeReceiver}</InlineError>}
        </div>

        {/* Ratio floor warning -- only loud when violated */}
        {!ratioOK && (
          <div style={{
            padding: 10,
            border: "1px solid var(--orange)",
            borderRadius: 8,
            background: "rgba(249,115,22,0.08)",
            color: "var(--orange)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>
              <strong>Ratio floor violated</strong>: increase virtual-stx or reduce supply.
              Pool would revert at create time.
            </div>
          </div>
        )}

        {/* Graduation/virtual ratio HARD BLOCK -- mirrors M3 contract cap.
            Disables submit (via ratioInvalid in hasErr). */}
        {ratioInvalid && (
          <div style={{
            padding: 10,
            border: "1px solid var(--red)",
            borderRadius: 8,
            background: "rgba(239,68,68,0.08)",
            color: "var(--red)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>
              {ratioOverBlock ? (
                <>
                  <strong>Ratio {ratio.toFixed(1)}x exceeds max {RATIO_HARD_BLOCK}x.</strong>{" "}
                  Pool will likely never graduate. The standard ratio used by similar launchpads is {RATIO_SOFT_WARN}x; our presets use {RATIO_SOFT_WARN}x.
                </>
              ) : (
                <>
                  <strong>Graduation threshold &gt; {MAX_GRADUATION_STX.toLocaleString()} STX max.</strong>{" "}
                  Reduce or use preset.
                </>
              )}
            </div>
          </div>
        )}

        {/* Soft warn -- ratio off-norm but still within hard cap. Does NOT
            disable submit; informational only. */}
        {ratioOverWarn && !ratioInvalid && (
          <div style={{
            padding: 10,
            border: "1px solid var(--yellow)",
            borderRadius: 8,
            background: "rgba(245,158,11,0.08)",
            color: "var(--yellow)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>
              Ratio {ratio.toFixed(1)}x exceeds the standard {RATIO_SOFT_WARN}x ratio used by similar launchpads. Pool may take
              longer to graduate. (Soft warning)
            </div>
          </div>
        )}

        {/* Supply min/max -- block. Same callout style as ratio errors. */}
        {errs.supply && (
          <div style={{
            padding: 10,
            border: "1px solid var(--red)",
            borderRadius: 8,
            background: "rgba(239,68,68,0.08)",
            color: "var(--red)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>{errs.supply}</div>
          </div>
        )}

        {/* Supply soft-warn -- below 1B industry norm but allowed. */}
        {!errs.supply && supply >= MIN_SUPPLY && supply < SOFT_WARN_SUPPLY && (
          <div style={{
            padding: 10,
            border: "1px solid var(--yellow)",
            borderRadius: 8,
            background: "rgba(245,158,11,0.08)",
            color: "var(--yellow)",
            fontSize: 12,
            fontFamily: "var(--font-mono)",
            display: "flex",
            gap: 8,
            alignItems: "flex-start",
          }}>
            <I.warn/>
            <div>
              Supply {supply.toLocaleString()} below industry standard of 1B tokens.
              Smaller supplies may signal niche/test tokens. Consider increasing to 1B.
            </div>
          </div>
        )}

        <button
          className="btn btn-primary"
          style={{
            width: "100%",
            justifyContent: "center",
            padding: "10px 14px",
            background: "var(--orange)",
            borderColor: "transparent",
          }}
          disabled={!!(wallet && wallet.connected) && (hasErr || submitting)}
          onClick={submit}
          title={
            ratioOverBlock
              ? `Ratio ${ratio.toFixed(1)}x exceeds max ${RATIO_HARD_BLOCK}x. Pool will likely never graduate.`
              : graduationOverCap
              ? `Graduation threshold > ${MAX_GRADUATION_STX.toLocaleString()} STX max. Reduce or use preset.`
              : curveDegenerate
              ? "Curve releases <60% of supply pre-graduation. Adjust virtual-stx/graduation-threshold ratio."
              : !ratioOK
              ? "Ratio floor violated. Increase virtual-stx or reduce supply."
              : ""
          }
        >
          {buttonLabel}
        </button>
      </div>

      {/* Preview */}
      <div className="card">
        <div className="eyebrow" style={{ marginBottom: 12 }}>Preview</div>
        <div className="kv"><span className="k">Mode</span><span className="v"><ModeBadge mode="bonding"/></span></div>
        <div className="kv"><span className="k">Initial spot price</span>
          <span className="v"><Mono>{initialPrice > 0 ? initialPrice.toExponential(3) : "--"}</Mono> <span className="dim" style={{ fontSize: 11 }}>STX/{symbol || "TKN"}</span></span>
        </div>
        <div className="kv"><span className="k">Tokens at graduation</span>
          <span className="v"><Mono>&asymp; {fmt.big(tokensReleasedAtGrad)}</Mono> <span className="dim" style={{ fontSize: 11 }}>{symbol || "TKN"}</span></span>
        </div>
        <div className="kv"><span className="k">100 STX test buy slip</span>
          <span className="v mono" style={{
            color: !testQuote ? "var(--ink)"
              : testQuote.priceImpact >= 20 ? "var(--red)"
              : testQuote.priceImpact >= 10 ? "var(--orange)"
              : "var(--green)",
          }}>
            {testQuote ? testQuote.priceImpact.toFixed(2) + "%" : "--"}
          </span>
        </div>
        <div className="kv"><span className="k">Fee</span>
          <span className="v" style={{ fontSize: 11.5 }}>1% bonding (90 + 10 bps), 5% post-grad</span>
        </div>

        <div className="hr"/>
        <div className="eyebrow" style={{ marginBottom: 8, fontSize: 10.5 }}>Curve sketch</div>
        <BondingCurveSketch virtualStx={virtualStx} threshold={graduationThreshold} supply={supply}/>
        <div className="muted mono" style={{ fontSize: 10.5, marginTop: 4, display: "flex", justifyContent: "space-between" }}>
          <span>0 STX</span><span>{fmt.stx(graduationThreshold)} STX (grad.)</span>
        </div>
      </div>
    </div>
  );
}

// XYK (constant-product) sketch for the direct preview. Mirrors
// BondingCurveSketch styling: tokens-remaining-in-pool over a sweep of
// cumulative STX buys (0..5*stxSeed). Pure visual aid, not a quote.
function DirectCurveSketch({ stxSeed, supply }) {
  const w = 220, h = 90, pad = 8;

  const points = useMemo(() => {
    if (!(supply > 0) || !(stxSeed > 0)) return [];
    const k = stxSeed * supply;
    const xmax = stxSeed * 5;
    const pts = [];
    for (let i = 0; i <= 40; i++) {
      const stxIn = (xmax / 40) * i;
      const tokensRemaining = k / (stxSeed + stxIn);
      pts.push([stxIn, tokensRemaining]);
    }
    return pts;
  }, [stxSeed, supply]);

  if (points.length === 0) {
    return <div className="muted mono" style={{ fontSize: 11 }}>(invalid params)</div>;
  }

  const ymax = points[0][1] || 1;
  const xmax = stxSeed * 5;
  const path = "M " + points.map(([x, y]) => {
    const sx = pad + (x / xmax) * (w - 2 * pad);
    const sy = h - pad - (y / ymax) * (h - 2 * pad);
    return sx.toFixed(1) + "," + sy.toFixed(1);
  }).join(" L ");

  // fresh pool marker: stxIn=0 -> top-left of curve
  const markerX = pad;
  const markerY = h - pad - (h - 2 * pad);

  return (
    <svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
      <path d={path} stroke="var(--orange)" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
      <circle cx={markerX} cy={markerY} r="3.5" fill="var(--orange)" stroke="var(--surface)" strokeWidth="1.5"/>
    </svg>
  );
}

function BondingCurveSketch({ virtualStx, threshold, supply }) {
  const w = 220, h = 90, pad = 8;

  const points = useMemo(() => {
    if (!(supply > 0) || !(virtualStx > 0) || !(threshold > 0)) return [];
    const pts = [];
    for (let i = 0; i <= 40; i++) {
      const stxCollected = (threshold / 40) * i;
      const tokensRemaining = (virtualStx + threshold) * supply / (virtualStx + stxCollected) - supply;
      pts.push([stxCollected, tokensRemaining]);
    }
    return pts;
  }, [virtualStx, threshold, supply]);

  if (points.length === 0) {
    return <div className="muted mono" style={{ fontSize: 11 }}>(invalid params)</div>;
  }

  const ymax = points[0][1] || 1;
  const xmax = threshold;
  const path = "M " + points.map(([x, y]) => {
    const sx = pad + (x / xmax) * (w - 2 * pad);
    const sy = h - pad - (y / ymax) * (h - 2 * pad);
    return sx.toFixed(1) + "," + sy.toFixed(1);
  }).join(" L ");

  // current state marker (fresh pool: stxCollected = 0 -> top-left)
  const markerX = pad;
  const markerY = h - pad - (h - 2 * pad);

  return (
    <svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
      <line x1={w - pad} x2={w - pad} y1={pad} y2={h - pad} stroke="var(--orange)" strokeDasharray="3 3" strokeWidth="1" opacity="0.5"/>
      <path d={path} stroke="var(--orange)" strokeWidth="1.6" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
      <circle cx={markerX} cy={markerY} r="3.5" fill="var(--orange)" stroke="var(--surface)" strokeWidth="1.5"/>
    </svg>
  );
}

// =============================================================
// Component 2 -- AllowlistManager
// =============================================================
const MOCK_SIGNERS = [
  { address: "ST7ADMIN…1",   label: "Signer 1" },
  { address: "ST7MULTI…ADMIN", label: "Signer 2 of 3" },  // matches main.jsx multisig wallet
  { address: "ST7ADMIN…3",   label: "Signer 3" },
];

function AllowlistManager({ go, wallet, showError, onTx }) {
  const net = useNetwork();
  const isMultisig = wallet && wallet.kind === "multisig";
  const [tab, setTab] = useState("principals");

  return (
    <div className="page">
      <div className="hero" style={{ paddingBottom: 8 }}>
        <div className="eyebrow">Token administration</div>
        <h1 className="h1" style={{ fontSize: 32 }}>Allowlist manager</h1>
        <p className="lead">
          Add or remove principals and code-hashes from the token allowlist. Mutations require 2-of-3
          multisig signatures (Asigna).
        </p>
      </div>

      <div className="card" style={{ marginTop: 12 }}>
        <div className="eyebrow" style={{ marginBottom: 10 }}>Multisig status</div>
        <div className="kv"><span className="k">Multisig</span><span className="v">2-of-3 (Asigna)</span></div>
        {MOCK_SIGNERS.map((s, i) => (
          <div key={i} className="kv">
            <span className="k">{s.label}</span>
            <span className="v"><Mono>{s.address}</Mono></span>
          </div>
        ))}
        <div className="hr"/>
        <div className="kv">
          <span className="k">You</span>
          <span className="v" style={{ color: isMultisig ? "var(--green)" : "var(--ink-3)" }}>
            {isMultisig ? "Signer 2 of 3 -- can propose & confirm" : "Read-only -- connect a multisig wallet to propose"}
          </span>
        </div>
      </div>

      {/* Tabs */}
      <div style={{ display: "flex", gap: 4, marginTop: 22, borderBottom: "1px solid var(--line)" }}>
        {[
          { id: "principals", label: "Principals" },
          { id: "hashes",     label: "Code hashes" },
          { id: "pending",    label: `Pending (${(ALLOWLIST.pending || []).length})` },
        ].map(it => (
          <button
            key={it.id}
            onClick={() => setTab(it.id)}
            style={{
              padding: "10px 14px",
              fontSize: 13,
              fontWeight: 500,
              border: 0,
              background: "transparent",
              color: tab === it.id ? "var(--ink)" : "var(--ink-2)",
              borderBottom: tab === it.id ? "2px solid var(--purple)" : "2px solid transparent",
              marginBottom: -1,
              cursor: "pointer",
            }}
          >{it.label}</button>
        ))}
      </div>

      <div style={{ marginTop: 18 }}>
        {tab === "principals" && <PrincipalsTab wallet={wallet} isMultisig={isMultisig} showError={showError} onTx={onTx}/>}
        {tab === "hashes"     && <HashesTab     wallet={wallet} isMultisig={isMultisig} showError={showError} onTx={onTx}/>}
        {tab === "pending"    && <PendingTab    wallet={wallet} isMultisig={isMultisig} showError={showError} onTx={onTx}/>}
      </div>
    </div>
  );
}

function PrincipalsTab({ wallet, isMultisig, showError, onTx }) {
  const [newP, setNewP] = useState("");
  const [note, setNote] = useState("");
  const connected = !!(wallet && wallet.connected);
  const propose = (action, p) => {
    if (!connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (!isMultisig) return showError("Connect a multisig wallet to propose.");
    if (action === "add" && !looksLikePrincipal(p)) return showError("Invalid principal format.");
    onTx({
      summary: "Propose " + action + " principal",
      detail: p,
    });
    if (action === "add") { setNewP(""); setNote(""); }
  };
  return (
    <>
      <div className="card" style={{ padding: 0 }}>
        <PrincipalRow head/>
        {(ALLOWLIST.principals || []).map((p, i) => (
          <PrincipalRow
            key={i}
            principal={p.p}
            note={p.note}
            added={p.added}
            wallet={wallet}
            isMultisig={isMultisig}
            onRemove={() => propose("remove", p.p)}
            last={i === ALLOWLIST.principals.length - 1}
          />
        ))}
      </div>

      <div className="card" style={{ marginTop: 18 }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Add principal</div>
        <div className="field" style={{ marginBottom: 10 }}>
          <FieldLabel>Principal</FieldLabel>
          <input className="input mono" placeholder="ST..." value={newP} onChange={e => setNewP(e.target.value)}/>
        </div>
        <div className="field" style={{ marginBottom: 10 }}>
          <FieldLabel hint="optional">Note</FieldLabel>
          <input className="input" placeholder="What is this principal?" value={note} onChange={e => setNote(e.target.value)}/>
        </div>
        <button
          className="btn btn-primary"
          disabled={connected && (!isMultisig || !newP)}
          onClick={() => propose("add", newP)}
        >
          Propose add
        </button>
      </div>
    </>
  );
}

function PrincipalRow({ head, principal, note, added, wallet, isMultisig, onRemove, last }) {
  const headStyle = {
    display: "grid",
    gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1.5fr) auto auto",
    gap: 12,
    padding: "10px 16px",
    fontFamily: "var(--font-mono)",
    fontSize: 10.5,
    textTransform: "uppercase",
    letterSpacing: "0.08em",
    color: "var(--ink-3)",
    borderBottom: "1px solid var(--line)",
    background: "var(--surface-2)",
  };
  if (head) {
    return (
      <div style={headStyle}>
        <span>Principal</span>
        <span>Note</span>
        <span>Added</span>
        <span style={{ textAlign: "right" }}>Action</span>
      </div>
    );
  }
  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1.5fr) auto auto",
      gap: 12,
      padding: "12px 16px",
      alignItems: "center",
      borderBottom: last ? "0" : "1px solid var(--line)",
    }}>
      <span style={{ minWidth: 0 }}><Hash>{principal}</Hash></span>
      <span style={{ fontSize: 12, color: "var(--ink-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{note}</span>
      <span className="muted mono" style={{ fontSize: 11 }}>{added}</span>
      <button
        className="btn btn-danger"
        style={{ padding: "4px 10px", fontSize: 11.5 }}
        disabled={!!(wallet && wallet.connected) && !isMultisig}
        onClick={onRemove}
      >Remove</button>
    </div>
  );
}

function HashesTab({ wallet, isMultisig, showError, onTx }) {
  const [src, setSrc]       = useState("");
  // Mock hash: deterministic but obviously fake; real impl would call sha512/256.
  const computed = useMemo(() => {
    if (!src) return "";
    let h = 0;
    for (let i = 0; i < src.length; i++) { h = ((h << 5) - h) + src.charCodeAt(i); h |= 0; }
    const hex = Math.abs(h).toString(16).padStart(8, "0");
    return "0x" + hex.repeat(8).slice(0, 32) + "…" + hex.repeat(2).slice(0, 4);
  }, [src]);

  const connected = !!(wallet && wallet.connected);
  const propose = () => {
    if (!connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (!isMultisig) return showError("Connect a multisig wallet to propose.");
    if (!src.trim()) return showError("Paste a contract source first.");
    onTx({ summary: "Propose add code-hash", detail: computed });
    setSrc("");
  };
  const onRemoveHash = (h) => {
    if (!connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (!isMultisig) return showError("Connect a multisig wallet to propose.");
    onTx({ summary: "Propose remove code-hash", detail: h });
  };

  return (
    <>
      <div className="card" style={{ padding: 0 }}>
        <HashRow head/>
        {(ALLOWLIST.hashes || []).map((h, i) => (
          <HashRow
            key={i}
            hash={h.h}
            note={h.note}
            added={h.added}
            wallet={wallet}
            isMultisig={isMultisig}
            onRemove={() => onRemoveHash(h.h)}
            last={i === ALLOWLIST.hashes.length - 1}
          />
        ))}
      </div>

      <div className="card" style={{ marginTop: 18 }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Add code-hash</div>
        <div className="field" style={{ marginBottom: 10 }}>
          <FieldLabel hint="paste contract source -- hash computed inline">Source</FieldLabel>
          <textarea
            className="input mono"
            rows={6}
            placeholder=";; Paste a Clarity contract source here..."
            value={src}
            onChange={e => setSrc(e.target.value)}
            style={{ resize: "vertical", fontSize: 12 }}
          />
        </div>
        {computed && (
          <div className="kv" style={{ marginBottom: 10 }}>
            <span className="k">SHA-512/256 (mock)</span>
            <span className="v"><Mono>{computed}</Mono></span>
          </div>
        )}
        <button
          className="btn btn-primary"
          disabled={connected && (!isMultisig || !computed)}
          onClick={propose}
        >Propose add</button>
      </div>
    </>
  );
}

function HashRow({ head, hash, note, added, wallet, isMultisig, onRemove, last }) {
  const cols = "minmax(0, 2fr) minmax(0, 1.5fr) auto auto";
  if (head) {
    return (
      <div style={{
        display: "grid",
        gridTemplateColumns: cols,
        gap: 12,
        padding: "10px 16px",
        fontFamily: "var(--font-mono)",
        fontSize: 10.5,
        textTransform: "uppercase",
        letterSpacing: "0.08em",
        color: "var(--ink-3)",
        borderBottom: "1px solid var(--line)",
        background: "var(--surface-2)",
      }}>
        <span>Hash</span><span>Note</span><span>Added</span><span style={{ textAlign: "right" }}>Action</span>
      </div>
    );
  }
  return (
    <div style={{
      display: "grid",
      gridTemplateColumns: cols,
      gap: 12,
      padding: "12px 16px",
      alignItems: "center",
      borderBottom: last ? "0" : "1px solid var(--line)",
    }}>
      <span style={{ minWidth: 0 }}><Hash>{hash}</Hash></span>
      <span style={{ fontSize: 12, color: "var(--ink-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{note}</span>
      <span className="muted mono" style={{ fontSize: 11 }}>{added}</span>
      <button className="btn btn-danger" style={{ padding: "4px 10px", fontSize: 11.5 }} disabled={!!(wallet && wallet.connected) && !isMultisig} onClick={onRemove}>Remove</button>
    </div>
  );
}

function PendingTab({ wallet, isMultisig, showError, onTx }) {
  const items = ALLOWLIST.pending || [];
  if (items.length === 0) {
    return (
      <div className="card" style={{ textAlign: "center", padding: 32 }}>
        <p className="muted">No pending proposals.</p>
      </div>
    );
  }
  const connected = !!(wallet && wallet.connected);
  const confirm = (it) => {
    if (!connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (!isMultisig) return showError("Connect a multisig wallet to confirm.");
    onTx({ summary: "Confirm proposal", detail: `${it.action} ${it.kind} ${it.value}` });
  };
  const reject = (it) => {
    if (!connected) {
      if (wallet && typeof wallet.connect === "function") return wallet.connect();
      return showError("Connect a wallet first.");
    }
    if (!isMultisig) return showError("Connect a multisig wallet to reject.");
    onTx({ summary: "Reject proposal", detail: `${it.action} ${it.kind} ${it.value}` });
  };
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
      {items.map((it, i) => (
        <div key={i} className="card">
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 12, flexWrap: "wrap" }}>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div className="eyebrow" style={{ marginBottom: 6 }}>{it.action} {it.kind}</div>
              <div style={{ fontSize: 14 }}><Mono>{it.value}</Mono></div>
              <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>Proposed by {it.proposer}</div>
            </div>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="btn btn-primary" disabled={connected && !isMultisig} onClick={() => confirm(it)}>Confirm</button>
              <button className="btn btn-danger"  disabled={connected && !isMultisig} onClick={() => reject(it)}>Reject</button>
            </div>
          </div>
          <div className="hr"/>
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <span className="muted mono" style={{ fontSize: 11.5 }}>
              Confirmed {it.confirmedBy} / {it.required}
            </span>
            <div style={{ flex: 1, height: 4, borderRadius: 999, background: "var(--surface-2)", overflow: "hidden" }}>
              <div style={{
                width: ((it.confirmedBy / it.required) * 100) + "%",
                height: "100%",
                background: "var(--purple)",
              }}/>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
}

// =============================================================
// Component 3 -- Emergency
// =============================================================
function Emergency({ go, wallet, showError, onTx }) {
  const net = useNetwork();
  const connected = !!(wallet && wallet.connected);
  const isAdmin = wallet && wallet.kind === "multisig";
  const [singletonPaused, setSingletonPaused] = useState(false);
  const [poolStates, setPoolStates] = useState(() => {
    const m = {};
    POOLS.forEach(p => { m[p.id] = p.active; });
    return m;
  });
  const [pendingAdmin, setPendingAdmin] = useState("");

  const requireConnect = () => {
    if (wallet && typeof wallet.connect === "function") return wallet.connect();
    return showError("Connect a wallet first.");
  };

  const togglePause = () => {
    if (!connected) return requireConnect();
    if (!isAdmin) return showError("Read-only -- protocol-admin wallet required.");
    const next = !singletonPaused;
    setSingletonPaused(next);
    onTx({ summary: "Set paused: " + next, detail: next ? "Singleton frozen" : "Singleton resumed" });
  };

  const togglePool = (p) => {
    if (!connected) return requireConnect();
    if (!isAdmin) return showError("Read-only -- protocol-admin wallet required.");
    const next = !poolStates[p.id];
    setPoolStates(prev => ({ ...prev, [p.id]: next }));
    onTx({ summary: "Toggle pool active", detail: p.name + " -> " + (next ? "active" : "frozen") });
  };

  const proposeAdmin = () => {
    if (!connected) return requireConnect();
    if (!isAdmin) return showError("Read-only -- protocol-admin wallet required.");
    if (!looksLikePrincipal(pendingAdmin)) return showError("Invalid principal.");
    onTx({ summary: "Set pending admin", detail: pendingAdmin });
    setPendingAdmin("");
  };

  const acceptAdmin = () => {
    if (!connected) return requireConnect();
    return showError("Only the pending admin can accept.");
  };

  return (
    <div className="page">
      <div className="hero" style={{ paddingBottom: 8 }}>
        <div className="eyebrow">Protocol admin</div>
        <h1 className="h1" style={{ fontSize: 32 }}>Emergency</h1>
        <p className="lead">Singleton-wide kill switch, per-pool freeze, and two-step admin rotation.</p>
      </div>

      {!isAdmin && (
        <div style={{
          padding: 12,
          border: "1px solid var(--yellow)",
          borderRadius: 8,
          background: "rgba(245, 158, 11, 0.08)",
          color: "var(--yellow)",
          fontSize: 13,
          fontFamily: "var(--font-mono)",
          margin: "12px 0",
          display: "flex",
          gap: 10,
          alignItems: "flex-start",
        }}>
          <I.warn/>
          <div>
            <strong>Read-only</strong> -- protocol-admin wallet required. Connect a multisig wallet to enable
            destructive actions.
          </div>
        </div>
      )}

      {/* Section 1: Global pause */}
      <div className="card" style={{ marginTop: 12 }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Global pause</div>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
          <div>
            <div style={{ fontSize: 18, fontWeight: 600, color: singletonPaused ? "var(--red)" : "var(--green)" }}>
              Singleton: {singletonPaused ? "PAUSED" : "ACTIVE"}
            </div>
            <div className="muted" style={{ fontSize: 12, marginTop: 4 }}>
              Pauses all swap functions globally. Reversible.
            </div>
          </div>
          <button
            className={singletonPaused ? "btn btn-primary" : "btn btn-danger"}
            disabled={connected && !isAdmin}
            onClick={togglePause}
          >
            {singletonPaused ? "Resume singleton" : "Pause singleton"}
          </button>
        </div>
        <div className="hr"/>
        <div className="muted mono" style={{ fontSize: 11.5, color: "var(--orange)" }}>
          <I.warn/> Pausing the singleton freezes ALL pools -- use only in incident response.
        </div>
      </div>

      {/* Section 2: Per-pool freeze */}
      <div className="card" style={{ marginTop: 18, padding: 0 }}>
        <div style={{
          padding: "12px 16px",
          borderBottom: "1px solid var(--line)",
          background: "var(--surface-2)",
        }}>
          <div className="eyebrow">Per-pool freeze</div>
        </div>
        {POOLS.map((p, i) => {
          const active = poolStates[p.id];
          return (
            <div key={p.id} style={{
              display: "grid",
              gridTemplateColumns: "minmax(0, 1.5fr) auto auto auto",
              gap: 12,
              padding: "12px 16px",
              alignItems: "center",
              borderBottom: i === POOLS.length - 1 ? "0" : "1px solid var(--line)",
            }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontWeight: 600, fontSize: 13, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</div>
                <span className="mono dim" style={{ fontSize: 11 }}>{p.symbol}</span>
              </div>
              <ModeBadge mode={p.mode} paused={!active}/>
              <span className="mono" style={{ fontSize: 12, color: active ? "var(--green)" : "var(--red)" }}>
                {active ? "active" : "frozen"}
              </span>
              <button
                className={active ? "btn btn-danger" : "btn"}
                style={{ padding: "4px 10px", fontSize: 11.5 }}
                disabled={connected && !isAdmin}
                onClick={() => togglePool(p)}
              >
                {active ? "Freeze" : "Unfreeze"}
              </button>
            </div>
          );
        })}
      </div>

      {/* Section 3: Admin rotation */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 18, marginTop: 18 }}>
        <div className="card">
          <div className="eyebrow" style={{ marginBottom: 12 }}>Current admin</div>
          <div className="kv"><span className="k">Principal</span>
            <span className="v"><Mono>{wallet && wallet.kind === "multisig" ? wallet.address : "ST7MULTI…ADMIN"}</Mono></span>
          </div>
          <div className="kv"><span className="k">Type</span>
            <span className="v">multisig 2-of-3</span>
          </div>
        </div>
        <div className="card">
          <div className="eyebrow" style={{ marginBottom: 12 }}>Pending admin</div>
          <div className="kv"><span className="k">Principal</span>
            <span className="v dim">none</span>
          </div>
          <div className="muted" style={{ fontSize: 11.5 }}>
            Two-step rotation: propose, then the new admin must accept.
          </div>
        </div>
      </div>

      <div className="card" style={{ marginTop: 18 }}>
        <div className="eyebrow" style={{ marginBottom: 12 }}>Propose new admin</div>
        <div style={{ display: "flex", gap: 8, alignItems: "stretch" }}>
          <input
            className="input mono"
            placeholder="ST..."
            value={pendingAdmin}
            onChange={e => setPendingAdmin(e.target.value)}
            style={{ flex: 1 }}
          />
          <button className="btn btn-primary" disabled={connected && (!isAdmin || !pendingAdmin)} onClick={proposeAdmin}>Set pending admin</button>
          <button className="btn" disabled={connected} title="Only the pending admin can accept." onClick={acceptAdmin}>Accept admin</button>
        </div>
      </div>
    </div>
  );
}

// =============================================================
// Mainnet-deploy confirm modal (Q6)
// =============================================================
//
// One modal lives at the bottom of every CreatePool render so any submit
// path can pop it. Communication is via module-scoped state + a tiny pub/sub
// pair: showMainnetDeployModal returns a Promise that resolves when the user
// clicks Continue and rejects when they Cancel.
//
// Why module-scoped instead of a React Portal / Context: the cancel/continue
// hooks have to bridge back to an awaiting Promise inside BondingForm.submit.
// A module-scoped Promise pair keeps the call site (`await showMainnetDeployModal(opts)`)
// boring -- no extra context, no extra ref-callback wiring through CreatePool.
let _mnModalSetter = null;
let _mnResolve     = null;
let _mnReject      = null;

function showMainnetDeployModal(opts) {
  return new Promise((resolve, reject) => {
    if (!_mnModalSetter) {
      reject(new Error("MainnetDeployModal not mounted"));
      return;
    }
    _mnResolve = resolve;
    _mnReject  = reject;
    _mnModalSetter({ open: true, opts });
  });
}

function MainnetDeployModal() {
  const [state, setState] = useState({ open: false, opts: null });

  useEffect(() => {
    _mnModalSetter = setState;
    return () => {
      _mnModalSetter = null;
      // If the modal unmounts while pending (e.g. user navigates away),
      // reject the awaiting Promise so the form's submit() exits cleanly.
      if (_mnReject) {
        const r = _mnReject;
        _mnReject = _mnResolve = null;
        r(new Error("User cancelled mainnet deploy"));
      }
    };
  }, []);

  if (!state.open || !state.opts) return null;
  const o = state.opts;

  const close = () => setState({ open: false, opts: null });
  const onCancel = () => {
    close();
    const r = _mnReject;
    _mnResolve = _mnReject = null;
    if (r) r(new Error("User cancelled mainnet deploy"));
  };
  const onContinue = () => {
    close();
    const r = _mnResolve;
    _mnResolve = _mnReject = null;
    if (r) r();
  };

  const feeStx = (o.deployFeeMicroStx / 1_000_000).toFixed(6);

  return (
    <div
      role="dialog"
      aria-modal="true"
      style={{
        position: "fixed", inset: 0,
        background: "rgba(0,0,0,0.65)",
        display: "flex", alignItems: "center", justifyContent: "center",
        zIndex: 200, padding: 20,
      }}
      onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
    >
      <div className="card" style={{ maxWidth: 560, width: "100%", padding: 22 }}>
        <h2 style={{ marginTop: 0, marginBottom: 4, fontSize: 18, fontWeight: 600 }}>
          Deploy on Mainnet
        </h2>
        <div className="hr" style={{ margin: "8px 0 14px" }}/>
        <p style={{ fontSize: 13, lineHeight: 1.55, margin: 0 }}>
          You are about to deploy a token contract on Stacks Mainnet. This will
          broadcast TWO signed transactions:
        </p>
        <ol style={{ fontSize: 13, lineHeight: 1.55, paddingLeft: 20, marginTop: 10 }}>
          <li style={{ marginBottom: 8 }}>
            Deploy <Mono>{o.tokenTemplateName}</Mono> instance:{" "}
            <Mono>{o.tokenName}</Mono>
            <br/>
            Deploy fee: <Mono>{feeStx} STX ({o.deployFeeMicroStx} uSTX)</Mono>
          </li>
          <li>
            create-pool-bonding on <Mono>{o.singletonShort}</Mono>
            <br/>
            Standard wallet-set fee at sign time.
          </li>
        </ol>
        <p style={{ fontSize: 13, lineHeight: 1.55, marginTop: 12 }}>
          Mainnet fees are paid in real STX. Double-check the fee-receiver
          address (<Mono>{o.feeReceiver}</Mono>) before signing.
        </p>
        <div style={{ display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 }}>
          <button className="btn" onClick={onCancel}>Cancel</button>
          <button className="btn btn-primary" onClick={onContinue}>Continue to wallet</button>
        </div>
      </div>
    </div>
  );
}

// =============================================================
// Export
// =============================================================
Object.assign(window, { CreatePool, AllowlistManager, Emergency });
