// ============ PRIMITIVES ============
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// Locale-safe number-field normalizer. UI number inputs hold raw strings; a user
// in a comma-decimal locale types "0,1", which Number("0,1") turns into NaN — so
// the server would fall back to the engine default (the 0,1 → 5 auto-bundle bug).
// Normalize the comma to a dot on blur/save. Never call Number() here: keep ""
// empty so the server's parseNum default still applies, and don't fight the caret
// mid-typing. The server (lib/parse-num.js) stays the authoritative parser.
function normNum(v) { return String(v ?? "").trim().replace(",", "."); }

// Loads a token's satellite config (launch / initial-buys / dev-autosell /
// priority / automation / dex-listing) and gives back a `save(patch)` that
// PATCHes the server. Pages call this once per kind; the config follows the
// active token through workspace switches.
//
// Server JSON column names are snake_case; UI typically holds camelCase. The
// hook returns the raw server shape — callers map as needed. `save()` takes
// a camelCase patch (the server route converts).
const TOKEN_CFG_API = {
  launch:        { get: "getTokenLaunch",        set: "updateTokenLaunch" },
  initialBuys:   { get: "getTokenInitialBuys",   set: "updateTokenInitialBuys" },
  devAutosell:   { get: "getTokenDevAutosell",   set: "updateTokenDevAutosell" },
  priority:      { get: "getTokenPriority",      set: "updateTokenPriority" },
  automation:    { get: "getTokenAutomation",    set: "updateTokenAutomation" },
  dexListing:    { get: "getTokenDexListing",    set: "updateTokenDexListing" },
};
function useTokenConfig(tokenId, kind) {
  const [config, setConfig] = useState(null);
  const [extra, setExtra] = useState({});
  const [loading, setLoading] = useState(false);
  useEffect(() => {
    if (!tokenId) { setConfig(null); setExtra({}); return; }
    const m = TOKEN_CFG_API[kind];
    if (!m) { console.error("[useTokenConfig] unknown kind", kind); return; }
    setLoading(true);
    Api[m.get](tokenId)
      .then((r) => { setConfig(r.config); setExtra(r); })
      .catch(() => { setConfig(null); setExtra({}); })
      .finally(() => setLoading(false));
  }, [tokenId, kind]);
  const save = async (patch) => {
    const m = TOKEN_CFG_API[kind];
    const resp = await Api[m.set](tokenId, patch);
    setConfig(resp.config);
    setExtra(resp);
    return resp.config;
  };
  return { config, loading, save, extra };
}
window.useTokenConfig = useTokenConfig;

// Generic debounce — used for autosave on text fields without spamming PATCH.
function useDebounced(value, delay = 500) {
  const [v, setV] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setV(value), delay);
    return () => clearTimeout(t);
  }, [value, delay]);
  return v;
}
window.useDebounced = useDebounced;

// ---- Checkbox ----
function Chk({ checked, onChange, indeterminate }) {
  return (
    <span
      className="chk"
      data-checked={indeterminate ? "indeterminate" : checked ? "true" : "false"}
      onClick={(e) => { e.stopPropagation(); onChange && onChange(!checked, e); }}
    >
      {checked && !indeterminate && <I.Check size={11} sw={3} />}
      {indeterminate && <I.MinBar size={11} sw={3} />}
    </span>
  );
}

// ---- Copyable ----
function Copyable({ text, display, onCopy }) {
  return (
    <span
      className="copyable"
      onClick={(e) => {
        e.stopPropagation();
        navigator.clipboard?.writeText?.(text);
        onCopy && onCopy();
      }}
      title="Click to copy"
    >
      <span>{display || text}</span>
      <I.Copy size={11} className="copy-ico" />
    </span>
  );
}

// ---- Status ----
function Status({ kind, children }) {
  return <span className={`status ${kind}`}><span className="dot" />{children}</span>;
}

// ---- Badge ----
function Badge({ tone = "neutral", children }) {
  return <span className={`badge ${tone}`}>{children}</span>;
}

// ---- Tag (soft pill) ----
function Tag({ color, children, icon }) {
  return (
    <span className="tag" style={color ? { background: `color-mix(in oklch, ${color} 16%, transparent)`, color, borderColor: `color-mix(in oklch, ${color} 35%, transparent)` } : undefined}>
      {icon}
      {children}
    </span>
  );
}

// ---- Modal ----
function Modal({ open, onClose, title, back, children, footer, width = 460 }) {
  useEffect(() => {
    if (!open) return;
    const on = (e) => e.key === "Escape" && onClose?.();
    window.addEventListener("keydown", on);
    return () => window.removeEventListener("keydown", on);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div className="backdrop" onClick={onClose}>
      <div className="modal" style={{ width }} onClick={(e) => e.stopPropagation()}>
        <div className="modal-header">
          {back && (
            <button className="icon-btn" onClick={back}><I.ArrowL size={14}/></button>
          )}
          <div className="modal-title">{title}</div>
          <button className="icon-btn" onClick={onClose}><I.X size={14}/></button>
        </div>
        <div className="modal-body">{children}</div>
        {footer && <div className="modal-footer">{footer}</div>}
      </div>
    </div>
  );
}

// ---- Toasts ----
const ToastCtx = React.createContext(null);
function ToastProvider({ children }) {
  const [items, setItems] = useState([]);
  const push = useCallback((t) => {
    const id = Math.random().toString(36).slice(2);
    setItems((s) => [...s, { id, ...t }]);
    setTimeout(() => setItems((s) => s.filter((x) => x.id !== id)), t.duration || 3200);
  }, []);
  return (
    <ToastCtx.Provider value={push}>
      {children}
      <div className="toasts">
        {items.map((t) => {
          const Ico = t.kind === "ok" ? I.Check : t.kind === "err" ? I.Alert : t.kind === "warn" ? I.Alert : I.Info;
          return (
            <div key={t.id} className={`toast ${t.kind || ""}`}>
              <Ico className="toast-ico" size={14} />
              <div className="flex1">
                <div className="t-title">{t.title}</div>
                {t.msg && <div className="t-msg">{t.msg}</div>}
              </div>
            </div>
          );
        })}
      </div>
    </ToastCtx.Provider>
  );
}
const useToast = () => React.useContext(ToastCtx);

// ---- Pagination ----
function Pagination({ page, pageCount, total, pageSize, setPage, setPageSize, top }) {
  const start = (page - 1) * pageSize + 1;
  const end = Math.min(total, page * pageSize);
  return (
    <div className={`pagination ${top ? "top" : ""}`}>
      <div className="group">
        <span>Show</span>
        <select
          value={pageSize}
          onChange={(e) => setPageSize(+e.target.value)}
          className="select"
          style={{ width: 66, height: 26, padding: "0 6px", fontSize: 11 }}
        >
          {[10, 20, 50, 100].map((n) => <option key={n} value={n}>{n}</option>)}
        </select>
        <span>per page</span>
      </div>
      <div className="pager">
        <button onClick={() => setPage(Math.max(1, page - 1))} disabled={page <= 1}><I.ChevL size={12}/></button>
        <input
          value={page}
          onChange={(e) => {
            const v = parseInt(e.target.value, 10);
            if (!isNaN(v)) setPage(Math.max(1, Math.min(pageCount, v)));
          }}
        />
        <button onClick={() => setPage(Math.min(pageCount, page + 1))} disabled={page >= pageCount}><I.ChevR size={12}/></button>
      </div>
      <div style={{ textAlign: "right" }}>
        <div>Page {page} of {pageCount}</div>
        <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--fg-4)" }}>
          {start}–{end} of {total}
        </div>
      </div>
    </div>
  );
}

// ---- Confirm dialog ----
function Confirm({ open, title, msg, confirmText = "Confirm", destructive, onConfirm, onClose }) {
  return (
    <Modal
      open={open}
      onClose={onClose}
      title={title}
      footer={
        <>
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className={`btn ${destructive ? "danger" : "primary"}`} onClick={() => { onConfirm(); onClose(); }}>
            {confirmText}
          </button>
        </>
      }
    >
      <div style={{ color: "var(--fg-2)", fontSize: 12, lineHeight: 1.55 }}>{msg}</div>
    </Modal>
  );
}

// ---- Sparkline ----
function Sparkline({ values, width = 80, height = 28, color }) {
  const max = Math.max(...values), min = Math.min(...values);
  const range = max - min || 1;
  const step = width / (values.length - 1);
  const pts = values.map((v, i) => `${(i * step).toFixed(1)},${(height - ((v - min) / range) * height).toFixed(1)}`);
  const path = `M${pts.join(" L")}`;
  const fill = `M0,${height} L${pts.join(" L")} L${width},${height} Z`;
  return (
    <svg width={width} height={height} style={{ display: "block" }}>
      <path d={fill} fill={color || "var(--accent)"} opacity="0.1" />
      <path d={path} stroke={color || "var(--accent)"} fill="none" strokeWidth="1.25" />
    </svg>
  );
}

// ---- Progress ----
function Progress({ value, max = 100 }) {
  return (
    <div className="progress">
      <div style={{ width: `${Math.max(0, Math.min(100, (value / max) * 100))}%` }} />
    </div>
  );
}

// ---- Empty ----
function Empty({ icon, title, desc, action }) {
  return (
    <div className="empty">
      <div className="empty-ico">{icon}</div>
      <h3>{title}</h3>
      {desc && <p>{desc}</p>}
      {action}
    </div>
  );
}

Object.assign(window, { Chk, Copyable, Status, Badge, Tag, Modal, ToastProvider, ToastCtx, useToast, Pagination, Confirm, Sparkline, Progress, Empty, LockedBanner, GroupPicker });

// ---- GroupPicker ----
// Reusable wallet-group selector. Loads groups via Api.listGroups() and renders
// a `.select` button + a fixed-positioned popover list (color dot + wallet
// count), styled like the folder rail of the wallet picker. Shared here so both
// the Initial-buys tab and the dev-autosell card can pick a group.
//   value    — selected group id, or null
//   onChange — called with the picked group id
function GroupPicker({ value, onChange, placeholder = "Select group…" }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const [groups, setGroups] = useState([]);
  const [loading, setLoading] = useState(true);
  const btnRef = useRef(null);

  useEffect(() => {
    Api.listGroups()
      .then((g) => setGroups(g.groups || []))
      .catch(() => {})
      .finally(() => setLoading(false));
  }, []);

  useEffect(() => {
    if (!open) return;
    const update = () => {
      if (!btnRef.current) return;
      const r = btnRef.current.getBoundingClientRect();
      setPos({ left: r.left, top: r.bottom + 4, width: r.width });
    };
    update();
    const onDoc = (e) => {
      if (btnRef.current?.contains(e.target)) return;
      if (e.target.closest && e.target.closest('[data-gpicker-pop="1"]')) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    window.addEventListener("scroll", update, true);
    window.addEventListener("resize", update);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      window.removeEventListener("scroll", update, true);
      window.removeEventListener("resize", update);
    };
  }, [open]);

  const selected = groups.find((g) => g.id === value) || null;

  return (
    <div className="gpicker">
      <button
        ref={btnRef}
        type="button"
        className="select"
        style={{ justifyContent: "space-between", width: "100%", cursor: "pointer" }}
        onClick={(e) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }}
      >
        {selected ? (
          <span className="row gap-8" style={{ minWidth: 0 }}>
            <span style={{ width: 8, height: 8, borderRadius: "50%", background: selected.color || "var(--fg-3)", flexShrink: 0 }}/>
            <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{selected.name}</span>
          </span>
        ) : (
          <span className="muted">{placeholder}</span>
        )}
        <I.ChevD size={12} className="muted"/>
      </button>
      {open && pos && (
        <div
          data-gpicker-pop="1"
          style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 9999, width: Math.max(200, pos.width), background: "var(--bg-1)", border: "1px solid var(--line-2)", borderRadius: 8, boxShadow: "0 12px 40px oklch(0% 0 0 / 0.4)", overflow: "hidden", maxHeight: 300, display: "flex", flexDirection: "column" }}
        >
          <div style={{ flex: 1, overflowY: "auto", padding: 4 }}>
            {groups.length === 0 && (
              <div className="muted" style={{ padding: 16, fontSize: 12, textAlign: "center" }}>{loading ? "Loading…" : "No groups"}</div>
            )}
            {groups.map((g) => {
              const isSel = g.id === value;
              return (
                <div
                  key={g.id}
                  onClick={() => { onChange(g.id); setOpen(false); }}
                  style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, padding: "7px 10px", borderRadius: 5, cursor: "pointer", fontSize: 12, color: isSel ? "var(--fg-0)" : "var(--fg-1)", background: isSel ? "var(--bg-3)" : "transparent" }}
                >
                  <span style={{ display: "inline-flex", alignItems: "center", gap: 8, minWidth: 0 }}>
                    <span style={{ width: 8, height: 8, borderRadius: "50%", background: g.color || "var(--fg-3)", flexShrink: 0 }}/>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{g.name}</span>
                  </span>
                  <span className="muted mono" style={{ fontSize: 10 }}>{g.count} wallet{g.count === 1 ? "" : "s"}</span>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

window.GroupPicker = GroupPicker;

// ---- CurvePoolPicker ----
// Dropdown selector for curve pools (collections of VOL/MC curves).
function CurvePoolPicker({ value, onChange, placeholder = "Select curve pool…" }) {
  const [open, setOpen] = useState(false);
  const [pos, setPos] = useState(null);
  const [pools, setPools] = useState([]);
  const [loading, setLoading] = useState(true);
  const btnRef = useRef(null);

  useEffect(() => {
    Api.listCurvePools()
      .then((d) => setPools(d.pools || []))
      .catch(() => {})
      .finally(() => setLoading(false));
  }, []);

  useEffect(() => {
    if (!open) return;
    const update = () => {
      if (!btnRef.current) return;
      const r = btnRef.current.getBoundingClientRect();
      setPos({ left: r.left, top: r.bottom + 4, width: r.width });
    };
    update();
    const onDoc = (e) => {
      if (btnRef.current?.contains(e.target)) return;
      if (e.target.closest && e.target.closest('[data-cppicker-pop="1"]')) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    window.addEventListener("scroll", update, true);
    window.addEventListener("resize", update);
    return () => {
      document.removeEventListener("mousedown", onDoc);
      window.removeEventListener("scroll", update, true);
      window.removeEventListener("resize", update);
    };
  }, [open]);

  const selected = pools.find((p) => p.id === value) || null;

  return (
    <div className="gpicker">
      <button
        ref={btnRef}
        type="button"
        className="select"
        style={{ justifyContent: "space-between", width: "100%", cursor: "pointer" }}
        onClick={(e) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }}
      >
        {selected ? (
          <span className="row gap-8" style={{ minWidth: 0 }}>
            <span style={{ width: 8, height: 8, borderRadius: "50%", background: selected.color || "var(--fg-3)", flexShrink: 0 }}/>
            <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{selected.name}</span>
            <span className="muted mono" style={{ fontSize: 10 }}>{selected.count}</span>
          </span>
        ) : (
          <span className="muted">{placeholder}</span>
        )}
        <I.ChevD size={12} className="muted"/>
      </button>
      {open && pos && (
        <div
          data-cppicker-pop="1"
          style={{ position: "fixed", top: pos.top, left: pos.left, zIndex: 9999, width: Math.max(200, pos.width), background: "var(--bg-1)", border: "1px solid var(--line-2)", borderRadius: 8, boxShadow: "0 12px 40px oklch(0% 0 0 / 0.4)", overflow: "hidden", maxHeight: 300, display: "flex", flexDirection: "column" }}
        >
          <div style={{ flex: 1, overflowY: "auto", padding: 4 }}>
            {pools.length === 0 && (
              <div className="muted" style={{ padding: 16, fontSize: 12, textAlign: "center" }}>{loading ? "Loading…" : "No pools"}</div>
            )}
            {/* None option */}
            <div
              onClick={() => { onChange(null); setOpen(false); }}
              style={{ display: "flex", alignItems: "center", gap: 8, padding: "7px 10px", borderRadius: 5, cursor: "pointer", fontSize: 12, color: !value ? "var(--fg-0)" : "var(--fg-2)", background: !value ? "var(--bg-3)" : "transparent" }}
            >
              <span className="muted">None (use preset/template)</span>
            </div>
            {pools.map((p) => {
              const isSel = p.id === value;
              return (
                <div
                  key={p.id}
                  onClick={() => { onChange(p.id); setOpen(false); }}
                  style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, padding: "7px 10px", borderRadius: 5, cursor: "pointer", fontSize: 12, color: isSel ? "var(--fg-0)" : "var(--fg-1)", background: isSel ? "var(--bg-3)" : "transparent" }}
                >
                  <span style={{ display: "inline-flex", alignItems: "center", gap: 8, minWidth: 0 }}>
                    <span style={{ width: 8, height: 8, borderRadius: "50%", background: p.color || "var(--fg-3)", flexShrink: 0 }}/>
                    <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</span>
                  </span>
                  <span className="muted mono" style={{ fontSize: 10 }}>{p.count} curve{p.count === 1 ? "" : "s"}</span>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}
window.CurvePoolPicker = CurvePoolPicker;

function LockedBanner({ ws, onSwitchLive }) {
  const phase = ws?.phase;
  const isActive = phase === "active";
  const isBundled = phase === "bundled";
  const tone = isActive ? "warn" : "ok";
  const title = isActive ? "Token is live — configuration locked" : "Token bundled — configuration locked";
  const msg = isActive
    ? "This workspace has deployed a token. Launch settings, initial buys, volume bot rules and workspace settings become read-only to prevent mid-flight changes."
    : "Bundle executed. Historical configuration is preserved as an audit record and cannot be modified.";
  const dot = tone === "warn" ? "var(--warn)" : "var(--ok)";
  return (
    <div style={{
      display: "flex", alignItems: "flex-start", gap: 12,
      padding: "11px 14px", marginBottom: 14,
      background: "var(--bg-2)",
      border: "1px solid var(--line-1)",
      borderLeft: `2px solid ${dot}`,
      borderRadius: 6,
    }}>
      <span style={{ width: 6, height: 6, borderRadius: "50%", background: dot, marginTop: 6, flexShrink: 0, boxShadow: `0 0 0 4px color-mix(in oklch, ${dot} 20%, transparent)` }} />
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12, fontWeight: 600, color: "var(--fg-0)" }}>
          <I.Lock size={11}/> {title}
        </div>
        <div style={{ fontSize: 11.5, color: "var(--fg-2)", marginTop: 3, lineHeight: 1.55 }}>{msg}</div>
      </div>
      {isActive && onSwitchLive && (
        <button className="btn" onClick={onSwitchLive} style={{ flexShrink: 0 }}>
          <I.ArrowUpRight size={11}/> Open Live
        </button>
      )}
    </div>
  );
}
