// dashboard-account-wizards.js — extracted verbatim from dashboard.js (classic script,
// shared global scope). Loaded before dashboard.js. Pure move, no logic change.

// ─── Account setup wizards (drawer-based) ──────────────────────────
// One drawer per platform. For X / LinkedIn / CryptoRank the actual
// session capture happens in background.js when the user clicks the
// extension icon — the wizard's job is to instruct + poll for the new
// account to show up in /api/accounts/all.
//
// Polling: snapshots the count BEFORE the wizard starts so detection is
// "count went up by 1" rather than "count > 0" (handles users who already
// have other accounts on the same platform).

const ACCOUNT_WIZARD_POLL_MS = 3000;
const ACCOUNT_WIZARD_TIMEOUT_MS = 5 * 60 * 1000;
const ACCOUNT_KEY_FOR_PLATFORM = {
  twitter: "twitter",
  linkedin: "linkedin",
  telegram: "telegram",
  email: "smtp",
};

let _accountWizardPollHandle = null;

function openAccountSetupWizard(platform, currentCount) {
  // Stop any previous wizard's polling so we don't double-trigger
  if (_accountWizardPollHandle) {
    clearInterval(_accountWizardPollHandle);
    _accountWizardPollHandle = null;
  }

  const meta = {
    twitter: { name: "X / Twitter", emoji: "🐦", origin: "https://x.com/login" },
    linkedin: { name: "LinkedIn", emoji: "💼", origin: "https://www.linkedin.com/login" },
    telegram: { name: "Telegram", emoji: "✈️" },
    email: { name: "Email / SMTP", emoji: "✉️" },
  }[platform];
  if (!meta) return;

  // Per-platform body builder. Returns the drawer's content node.
  let body;
  if (platform === "telegram") body = buildTelegramWizardBody();
  else if (platform === "email") body = buildEmailWizardBody();
  else body = buildCookieSyncWizardBody(platform, meta, currentCount);

  // Wrap body with a header showing the title + help link
  const drawerBody = el("div", { class: "drawer-body" }, [
    el("div", { style: "display: flex; align-items: center; gap: 12px; margin-bottom: 16px;" }, [
      el("div", { style: "font-size: 36px;" }, meta.emoji),
      el("div", {}, [
        el("div", { style: "font-size: 16px; font-weight: 600;" }, `Connect ${meta.name}`),
        el("div", { class: "muted small" }, `Slot ${currentCount + 1} of 5`),
      ]),
    ]),
    body,
  ]);

  openDrawer(`Connect ${meta.name}`, drawerBody);
}

// Cookie-sync wizards (X / LinkedIn / CryptoRank-style) — polls accounts-all
// every 3s after step 2 and auto-advances when the count for this platform
// goes up by ≥1.
function buildCookieSyncWizardBody(platform, meta, beforeCount) {
  const accountKey = ACCOUNT_KEY_FOR_PLATFORM[platform];
  const stepEls = [];
  const pollStartedAt = Date.now();

  // ── Step status badges ──
  const stepBadge = (n, state) =>
    el(
      "span",
      {
        style:
          `display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; font-size: 11px; font-weight: 700; flex-shrink: 0; ` +
          (state === "done"
            ? "background: var(--green); color: white;"
            : state === "active"
              ? "background: var(--accent); color: white;"
              : "background: var(--bg-2); color: var(--text-3);"),
      },
      state === "done" ? "✓" : String(n),
    );

  function step(n, state, title, body) {
    return el(
      "div",
      {
        style:
          "display: flex; gap: 12px; padding: 10px 0; " +
          (state === "active" ? "" : state === "done" ? "opacity: 0.6;" : "opacity: 0.5;"),
      },
      [
        stepBadge(n, state),
        el("div", { style: "flex: 1;" }, [
          el(
            "div",
            { class: "primary-cell", style: "font-size: 13px; margin-bottom: 4px;" },
            title,
          ),
          body,
        ]),
      ],
    );
  }

  // Active step — starts at 1
  let activeStep = 1;

  // Status indicator near the bottom
  const statusLine = el(
    "div",
    {
      class: "muted small",
      style:
        "margin-top: 12px; padding: 8px 10px; background: var(--bg-1); border-radius: var(--radius-sm); text-align: center;",
    },
    "Click the extension icon when you're logged in",
  );

  function refreshSteps() {
    const stepsWrap = el("div", {});
    stepsWrap.appendChild(
      step(
        1,
        activeStep > 1 ? "done" : "active",
        `Open ${meta.name} and sign in`,
        el("div", {}, [
          el(
            "div",
            { class: "muted small", style: "margin-bottom: 6px;" },
            `Use a fresh tab — the extension reads cookies from whichever tab is currently logged in.`,
          ),
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => {
                window.open(meta.origin, "_blank");
                activeStep = Math.max(activeStep, 2);
                rerender();
              },
            },
            `Open ${meta.name} →`,
          ),
        ]),
      ),
    );

    stepsWrap.appendChild(
      step(
        2,
        activeStep > 2 ? "done" : activeStep === 2 ? "active" : "pending",
        "Click the Nexus extension icon",
        el("div", { class: "muted small" }, [
          "In your browser toolbar, click the Nexus Connect icon and hit ",
          el("b", {}, `Sync ${meta.name} session`),
          ". The session cookie gets pulled into the bot.",
        ]),
      ),
    );

    stepsWrap.appendChild(
      step(
        3,
        activeStep > 3 ? "done" : activeStep === 3 ? "active" : "pending",
        "Wait for confirmation",
        el(
          "div",
          { class: "muted small" },
          "We're polling for your new account. It usually shows up in 3–5 seconds. You'll see a green check below when it lands.",
        ),
      ),
    );

    // Replace previous content
    return stepsWrap;
  }

  const stepsContainer = el("div", {});
  function rerender() {
    stepsContainer.innerHTML = "";
    stepsContainer.appendChild(refreshSteps());
    statusLine.textContent =
      activeStep === 1
        ? "Step 1: open the platform login page"
        : activeStep === 2
          ? "Step 2: click extension icon → polling for new account…"
          : "Step 3: complete!";
  }
  rerender();

  // Start polling for new account
  _accountWizardPollHandle = setInterval(async () => {
    if (Date.now() - pollStartedAt > ACCOUNT_WIZARD_TIMEOUT_MS) {
      clearInterval(_accountWizardPollHandle);
      _accountWizardPollHandle = null;
      statusLine.textContent =
        "Polling timed out (5min). Click ↻ Force sync on the page if you've already clicked the extension icon.";
      return;
    }
    try {
      const data = await api("accounts-all");
      const list = data?.[accountKey] || [];
      if (list.length > beforeCount) {
        clearInterval(_accountWizardPollHandle);
        _accountWizardPollHandle = null;
        activeStep = 4; // beyond step 3 = done
        statusLine.textContent = `Connected! Refreshing…`;
        statusLine.style.background = "rgba(34, 197, 94, 0.15)";
        statusLine.style.color = "var(--green)";
        // Update the steps to all-done state
        rerender();
        setTimeout(() => {
          closeDrawer();
          renderAccounts();
        }, 800);
      }
    } catch {
      /* swallow — next tick will retry */
    }
  }, ACCOUNT_WIZARD_POLL_MS);

  // Wrap up
  return el("div", {}, [
    stepsContainer,
    statusLine,
    el("div", { style: "margin-top: 16px; display: flex; gap: 8px;" }, [
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          onclick: () => {
            if (_accountWizardPollHandle) {
              clearInterval(_accountWizardPollHandle);
              _accountWizardPollHandle = null;
            }
            closeDrawer();
          },
        },
        "Cancel",
      ),
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          title: "Force a session sync — useful if the extension's auto-detection didn't fire",
          onclick: async (ev) => {
            await withProgress(ev.currentTarget, async () => {
              const action = platform === "twitter" ? "sync-twitter" : "sync-linkedin";
              let r;
              try {
                r = await api(action);
              } catch (e) {
                toast(`Sync failed: ${e?.message || "unreachable"}`, "err", 5000);
                return;
              }
              if (r?.error) {
                toast(`Sync failed: ${r.error}`, "err", 5000);
                return;
              }
              toast("Sync triggered — checking for new account…", "info");
            });
          },
        },
        `↻ Force sync ${meta.name}`,
      ),
    ]),
  ]);
}

// Telegram setup is bot-side (MTProto requires phone + code via the
// Telegram client). We can't do this in-browser. The wizard explains
// the flow + deep-links to /accounts in Telegram.
// Telegram is MTProto (not browser/cookie). This drives the stateful
// /api/accounts/telegram/{start,code,password} login right here: creds → SMS
// code → (2FA) → saved. api_id/api_hash are remembered locally so adding the
// 2nd+ account only needs phone + code.
function buildTelegramWizardBody() {
  const root = el("div", {});
  const status = el("div", {
    class: "muted small",
    style: "margin-top:10px;min-height:16px;line-height:1.5;",
  });
  let loginId = null;
  const INPUT =
    "width:100%;padding:7px 9px;background:#0d0d12;border:1px solid var(--border-soft);border-radius:6px;color:#eee;font-size:13px;box-sizing:border-box;";
  const lbl = (t) =>
    el("label", { style: "font-size:11px;color:#9a9aa8;display:block;margin:8px 0 2px;" }, t);
  let remId = "",
    remHash = "";
  try {
    remId = localStorage.getItem("_tgApiId") || "";
    remHash = localStorage.getItem("_tgApiHash") || "";
  } catch {}

  const render = (node) => {
    root.innerHTML = "";
    root.appendChild(node);
    root.appendChild(status);
  };
  const cancelBtn = () =>
    el(
      "button",
      {
        class: "btn btn-secondary",
        type: "button",
        onclick: async () => {
          if (loginId) {
            try {
              await api("tg-add-cancel", { body: { login_id: loginId } });
            } catch {}
          }
          closeDrawer();
        },
      },
      "Cancel",
    );
  const done = (r) => {
    const prem = r.account && r.account.has_premium;
    toast(
      `Telegram account added${prem ? "" : " — no Premium, outbound locked"}`,
      prem ? "ok" : "err",
      6000,
    );
    closeDrawer();
    navigate("accounts");
  };

  function showCreds() {
    const apiId = el("input", {
      type: "text",
      value: remId,
      placeholder: "API ID (e.g. 1234567)",
      style: INPUT,
    });
    const apiHash = el("input", {
      type: "text",
      value: remHash,
      placeholder: "API Hash (32 hex)",
      style: INPUT,
    });
    const phone = el("input", { type: "text", placeholder: "+14155550100", style: INPUT });
    const label = el("input", {
      type: "text",
      placeholder: "Label (optional, e.g. main)",
      style: INPUT,
    });
    const go = el(
      "button",
      {
        class: "btn btn-primary",
        type: "button",
        onclick: async (e) => {
          const idv = apiId.value.trim(),
            hashv = apiHash.value.trim(),
            ph = phone.value.trim();
          if (!idv || !hashv || !ph) {
            status.textContent = "Fill API ID, API Hash and phone.";
            return;
          }
          await withProgress(e.target, async () => {
            status.textContent = "Connecting to Telegram & sending the code…";
            const r = await api("tg-add-start", {
              body: { api_id: idv, api_hash: hashv, phone: ph, label: label.value.trim() },
            });
            if (r?.error) {
              status.textContent = "⚠ " + r.error;
              return;
            }
            loginId = r.login_id;
            try {
              localStorage.setItem("_tgApiId", idv);
              localStorage.setItem("_tgApiHash", hashv);
            } catch {}
            showCode(ph);
          });
        },
      },
      "Send code →",
    );
    render(
      el("div", {}, [
        el("div", { class: "muted", style: "font-size:12px;line-height:1.6;margin-bottom:6px;" }, [
          "Userbot login over MTProto. Get your API ID + Hash at ",
          el(
            "a",
            {
              href: "https://my.telegram.org/apps",
              target: "_blank",
              style: "color:var(--accent);",
            },
            "my.telegram.org/apps",
          ),
          ". They're remembered, so the next account only needs phone + code.",
        ]),
        lbl("API ID"),
        apiId,
        lbl("API Hash"),
        apiHash,
        lbl("Phone (E.164)"),
        phone,
        lbl("Label (optional)"),
        label,
        el("div", { style: "display:flex;gap:8px;margin-top:12px;" }, [go, cancelBtn()]),
      ]),
    );
  }

  function showCode(phone) {
    const code = el("input", {
      type: "text",
      inputmode: "numeric",
      placeholder: "12345",
      style: INPUT,
      onkeydown: (e) => {
        if (e.key === "Enter") go.click();
      },
    });
    const go = el(
      "button",
      {
        class: "btn btn-primary",
        type: "button",
        onclick: async (e) => {
          const c = code.value.trim();
          if (!c) {
            status.textContent = "Enter the code.";
            return;
          }
          await withProgress(e.target, async () => {
            status.textContent = "Verifying…";
            const r = await api("tg-add-code", { body: { login_id: loginId, code: c } });
            if (r?.error) {
              status.textContent = "⚠ " + r.error;
              if (r.expired) showCreds();
              return;
            }
            if (r.status === "password_needed") {
              show2fa();
              return;
            }
            if (r.status === "success") {
              done(r);
              return;
            }
          });
        },
      },
      "Verify →",
    );
    render(
      el("div", {}, [
        el(
          "div",
          { class: "muted", style: "font-size:12px;margin-bottom:8px;" },
          `Telegram sent a login code to ${phone} — check the Telegram app on that number.`,
        ),
        lbl("Login code"),
        code,
        el("div", { style: "display:flex;gap:8px;margin-top:12px;" }, [go, cancelBtn()]),
      ]),
    );
    setTimeout(() => code.focus(), 50);
  }

  function show2fa() {
    const pw = el("input", {
      type: "password",
      placeholder: "2FA / cloud password",
      style: INPUT,
      onkeydown: (e) => {
        if (e.key === "Enter") go.click();
      },
    });
    const go = el(
      "button",
      {
        class: "btn btn-primary",
        type: "button",
        onclick: async (e) => {
          const p = pw.value;
          if (!p) {
            status.textContent = "Enter your 2FA password.";
            return;
          }
          await withProgress(e.target, async () => {
            status.textContent = "Checking 2FA…";
            const r = await api("tg-add-password", { body: { login_id: loginId, password: p } });
            if (r?.error) {
              status.textContent = "⚠ " + r.error;
              return;
            }
            if (r.status === "success") {
              done(r);
              return;
            }
          });
        },
      },
      "Finish →",
    );
    render(
      el("div", {}, [
        el(
          "div",
          { class: "muted", style: "font-size:12px;margin-bottom:8px;" },
          "This account has two-factor (cloud password) enabled. Enter it to finish.",
        ),
        lbl("2FA password"),
        pw,
        el("div", { style: "display:flex;gap:8px;margin-top:12px;" }, [go, cancelBtn()]),
      ]),
    );
    setTimeout(() => pw.focus(), 50);
  }

  showCreds();
  return root;
}

// Email/SMTP wizard — same form as before but inside a drawer with
// per-field labels + a "Save" button that POSTs to connect-account.
function buildEmailWizardBody() {
  const state = {
    email: "",
    smtpHost: "",
    smtpPort: 587,
    smtpUser: "",
    smtpPass: "",
    displayName: "",
  };

  function field(label, key, opts = {}) {
    return el("div", { style: "margin-bottom: 10px;" }, [
      el("div", { class: "wizard-label" }, label),
      el("input", {
        class: "wizard-input",
        type: opts.type || "text",
        placeholder: opts.placeholder || "",
        value: state[key] || "",
        oninput: (e) => {
          state[key] =
            opts.type === "number" ? parseInt(e.target.value, 10) || 0 : e.target.value.trim();
        },
      }),
      opts.hint ? el("div", { class: "muted small", style: "margin-top: 2px;" }, opts.hint) : null,
    ]);
  }

  // Quick-fill chips for common providers
  function presetBtn(label, host, port) {
    return el(
      "button",
      {
        class: "btn btn-tiny btn-secondary",
        type: "button",
        onclick: () => {
          state.smtpHost = host;
          state.smtpPort = port;
          // Refresh the body so the new values render
          const drawerBody = document.querySelector("#drawer-body");
          if (drawerBody) {
            drawerBody.innerHTML = "";
            drawerBody.appendChild(buildEmailWizardBody());
          }
        },
      },
      label,
    );
  }

  return el("div", {}, [
    el(
      "div",
      { class: "muted", style: "font-size: 13px; margin-bottom: 12px;" },
      "Enter your SMTP credentials. For Gmail, you'll need an App Password (Google Account → Security → App passwords) — your regular password won't work.",
    ),

    el("div", { style: "display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap;" }, [
      el(
        "span",
        { class: "muted small", style: "align-self: center; margin-right: 4px;" },
        "Quick-fill:",
      ),
      presetBtn("Gmail", "smtp.gmail.com", 587),
      presetBtn("Outlook", "smtp-mail.outlook.com", 587),
      presetBtn("Yahoo", "smtp.mail.yahoo.com", 587),
      presetBtn("ProtonMail", "smtp.protonmail.com", 587),
    ]),

    field("Email address", "email", { type: "email", placeholder: "you@gmail.com" }),
    field("Display name (optional)", "displayName", { placeholder: "Jane Doe" }),
    el("div", { class: "row", style: "gap: 8px;" }, [
      el("div", { style: "flex: 3;" }, [
        field("SMTP host", "smtpHost", { placeholder: "smtp.gmail.com" }),
      ]),
      el("div", { style: "flex: 1; min-width: 90px;" }, [
        field("Port", "smtpPort", { type: "number", placeholder: "587" }),
      ]),
    ]),
    field("SMTP username", "smtpUser", {
      placeholder: "you@gmail.com",
      hint: "Usually the same as your email.",
    }),
    field("SMTP password", "smtpPass", {
      type: "password",
      placeholder: "••••••••",
      hint: "App password for Gmail; regular password for most others.",
    }),

    el("div", { style: "display: flex; gap: 8px; margin-top: 16px;" }, [
      el(
        "button",
        {
          class: "btn btn-primary",
          type: "button",
          onclick: async (ev) => {
            if (!state.email || !state.smtpHost || !state.smtpUser || !state.smtpPass) {
              toast("Fill email, host, username, and password", "err");
              return;
            }
            await withProgress(ev.currentTarget, async () => {
              const r = await api("connect-account", {
                body: {
                  platform: "email",
                  email: state.email,
                  smtpHost: state.smtpHost,
                  smtpPort: state.smtpPort,
                  smtpUser: state.smtpUser,
                  smtpPass: state.smtpPass,
                  displayName: state.displayName || undefined,
                },
              }).catch((e) => ({ error: e.message }));
              if (r?.ok || r?.success) {
                toast("SMTP account added", "ok");
                closeDrawer();
                renderAccounts();
              } else toast(`${r?.error || "Failed to save"}`, "err");
            });
          },
        },
        "Save SMTP account",
      ),
      el("button", { class: "btn btn-secondary", type: "button", onclick: closeDrawer }, "Cancel"),
    ]),
  ]);
}

// \u2500\u2500 Voice profile drawer (per-X-account AI humanization) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
//
// Lets the user manage the few-shot examples used to humanize AI
// replies for one X account. Three controls:
//   1. Re-fetch from X \u2014 pulls last ~50 tweets via the bot API
//      (twitter:fetch-voice-samples \u2192 social_accounts.voice_samples_json)
//   2. Edit samples manually \u2014 textarea, one sample per line. User
//      can paste tweets from any source (their own, a reference
//      account they want to mimic, hand-curated voice anchors).
//   3. Voice instruction (free text) \u2014 short tone override that's
//      prepended to the system prompt. Stored as voice_instruction.
//
// Save \u2192 PUT /api/social-accounts/:id/voice-profile

async function openVoiceProfileDrawer(account) {
  const handle = `@${account.account_identifier || account.display_name || "?"}`;
  // Loading shell
  const body = el("div", { style: "display:flex;flex-direction:column;gap:14px;" }, [
    el("div", { class: "muted small" }, "Loading voice profile\u2026"),
  ]);
  openDrawer(`\ud83c\udf99 Voice profile \u2014 ${handle}`, body);

  let profile = null;
  try {
    const resp = await api("voice-profile-get", { accountId: account.id });
    profile = resp?.profile || null;
  } catch {}

  body.innerHTML = "";

  // Explainer
  body.appendChild(
    el(
      "div",
      {
        style:
          "padding:10px 12px;background:rgba(99,102,241,0.06);border:1px solid rgba(99,102,241,0.2);border-radius:6px;font-size:12px;color:var(--text-1);line-height:1.5;",
      },
      [
        el(
          "div",
          { style: "color:#a5b4fc;font-weight:600;margin-bottom:4px;" },
          "How voice profiles work",
        ),
        el(
          "div",
          {},
          "Each AI-generated reply for this account uses up to 5 of these samples as few-shot examples. The model anchors to YOUR voice instead of a generic 'crypto market maker' template. Re-fetch pulls your real tweets; manual edits let you paste hand-picked anchors or mimic another account.",
        ),
      ],
    ),
  );

  // Status row
  const fetchedAt = profile?.fetchedAt || null;
  const sampleCount = profile?.samples?.length || 0;
  const source = profile?.source || null;
  body.appendChild(
    el(
      "div",
      {
        style:
          "display:flex;align-items:center;gap:10px;padding:8px 12px;background:rgba(255,255,255,0.02);border:1px solid var(--border);border-radius:6px;font-size:11px;color:var(--text-2);",
      },
      [
        el(
          "span",
          { style: "color:var(--text-1);font-weight:500;" },
          sampleCount > 0
            ? `${sampleCount} sample${sampleCount === 1 ? "" : "s"}`
            : "no samples yet",
        ),
        fetchedAt ? el("span", {}, `\u00b7 last updated ${fmtRel(fetchedAt)}`) : null,
        source
          ? el("span", { style: "font-family:ui-monospace,monospace;" }, `\u00b7 ${source}`)
          : null,
      ].filter(Boolean),
    ),
  );

  // Preset picker — load presets list + render as compact pills.
  // Clicking a preset writes its instruction + samples to the
  // account's profile (overwriting current). User can then edit
  // afterward; the preset is just a starting point.
  const presetsBox = el("div", {});
  body.appendChild(presetsBox);
  (async () => {
    try {
      const r = await api("voice-presets-list");
      const presets = r?.presets || [];
      if (presets.length === 0) return;
      presetsBox.appendChild(
        el(
          "div",
          {
            style:
              "font-size:11px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:5px;",
          },
          "Apply a preset (overwrites current)",
        ),
      );
      const pills = el("div", { style: "display:flex;flex-wrap:wrap;gap:6px;" });
      for (const p of presets) {
        const isActive = profile?.source === `preset:${p.id}`;
        pills.appendChild(
          el(
            "button",
            {
              type: "button",
              style: [
                "padding:5px 10px",
                "background:" + (isActive ? "rgba(99,102,241,0.18)" : "rgba(255,255,255,0.02)"),
                "border:1px solid " + (isActive ? "rgba(99,102,241,0.6)" : "#2a2a2e"),
                "border-radius:14px",
                "color:" + (isActive ? "#a5b4fc" : "#cbd5e1"),
                "font-size:11px",
                "cursor:pointer",
                "font-weight:500",
              ].join(";"),
              title: p.description,
              onclick: async (ev) => {
                if (
                  !confirm(
                    `Apply preset "${p.label}"?\n\n${p.description}\n\nThis OVERWRITES the current samples and instruction. You can edit afterward.`,
                  )
                )
                  return;
                ev.currentTarget.disabled = true;
                const r2 = await api("voice-profile-apply-preset", {
                  accountId: account.id,
                  body: { preset: p.id },
                }).catch((e) => ({ error: e?.message }));
                if (r2?.error) {
                  toast(`Apply failed: ${r2.error}`, "err");
                  ev.currentTarget.disabled = false;
                  return;
                }
                toast(`Preset "${p.label}" applied`, "ok", 2000);
                logTelemetry("accounts", "voice_preset_apply", {
                  accountId: account.id,
                  preset: p.id,
                });
                openVoiceProfileDrawer(account); // reopen with fresh data
              },
            },
            p.label,
          ),
        );
      }
      presetsBox.appendChild(pills);
    } catch {}
  })();

  // Voice instruction (free text override)
  const instructionInput = el(
    "textarea",
    {
      placeholder:
        "Optional voice instruction (e.g. 'be terse and dry', 'no emojis ever', 'use lowercase always'). Prepended to every prompt.",
      style:
        "width:100%;min-height:60px;padding:8px 10px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;color:var(--text-1);font-size:12px;font-family:inherit;resize:vertical;box-sizing:border-box;",
    },
    profile?.instruction || "",
  );
  body.appendChild(
    el("div", {}, [
      el(
        "div",
        {
          style:
            "font-size:11px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:5px;",
        },
        "Voice instruction (optional)",
      ),
      instructionInput,
    ]),
  );

  // Samples editor \u2014 one per line
  const samplesText = (profile?.samples || []).join("\n");
  const samplesInput = el(
    "textarea",
    {
      placeholder:
        "Paste sample tweets / posts here, one per line. Click 'Re-fetch from X' to auto-pull last 50 tweets.",
      style:
        "width:100%;min-height:240px;padding:8px 10px;background:var(--bg-1);border:1px solid var(--border);border-radius:6px;color:var(--text-1);font-size:12px;font-family:ui-monospace,monospace;line-height:1.5;resize:vertical;box-sizing:border-box;",
    },
    samplesText,
  );
  body.appendChild(
    el("div", {}, [
      el("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:5px;" }, [
        el(
          "div",
          {
            style:
              "font-size:11px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:0.04em;flex:1;",
          },
          "Sample tweets (one per line, max 50)",
        ),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            title:
              "Pull last ~50 tweets from this X account and use them as voice samples (overwrites current samples)",
            onclick: async (ev) => {
              if (
                !confirm(
                  `Re-fetch this account's last ~50 tweets and replace the current samples?\n\nUse this when you want fresh anchors. Existing manual edits will be lost.`,
                )
              )
                return;
              ev.currentTarget.disabled = true;
              ev.currentTarget.textContent = "Fetching\u2026";
              const r = await api("voice-profile-refresh", { accountId: account.id }).catch(
                (e) => ({ error: e?.message }),
              );
              if (r?.error) {
                toast(`Refresh failed: ${r.error}`, "err");
                ev.currentTarget.disabled = false;
                ev.currentTarget.textContent = "\u21bb Re-fetch from X";
                return;
              }
              toast(`\u2713 Fetched ${r.fetched} samples`, "ok", 2000);
              logTelemetry("accounts", "voice_refresh", {
                accountId: account.id,
                fetched: r.fetched,
              });
              openVoiceProfileDrawer(account); // reopen with fresh data
            },
          },
          "\u21bb Re-fetch from X",
        ),
        // Phase 3 \u2014 mimic another account's voice. Pastes a handle,
        // fetches THAT user's tweets, stores as our voice samples.
        // Useful when the user wants their bot to sound like e.g. a
        // thoughtful VC (@paulg) instead of a generic operator.
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            title:
              "Scrape another X account's tweets and use them as the voice template for THIS account. Overwrites current samples.",
            onclick: async (ev) => {
              const input = prompt(
                `Mimic which X account?\n\n` +
                  `Paste a handle (@paulg) or URL (https://x.com/paulg). The last 50 tweets from that account will be used as voice samples \u2014 your bot's replies will mimic that voice instead of yours.\n\n` +
                  `Note: only public, non-protected accounts work.`,
                "",
              );
              if (!input) return;
              const handle = String(input)
                .trim()
                .replace(/^@/, "")
                .replace(/^https?:\/\/(?:x|twitter)\.com\//i, "")
                .split(/[\/?#]/)[0];
              if (!handle || !/^[A-Za-z0-9_]{1,30}$/.test(handle)) {
                toast("Invalid handle \u2014 expected @username or x.com/<username>", "err", 4000);
                return;
              }
              if (
                !confirm(
                  `Mimic @${handle}'s voice?\n\nThis OVERWRITES the current samples with @${handle}'s last ~50 tweets. You can edit afterward.`,
                )
              )
                return;
              ev.currentTarget.disabled = true;
              ev.currentTarget.textContent = "Mimicking\u2026";
              const r = await api("voice-profile-mimic", {
                accountId: account.id,
                body: { handle },
              }).catch((e) => ({ error: e?.message }));
              if (r?.error) {
                toast(`Mimic failed: ${r.error}`, "err", 5000);
                ev.currentTarget.disabled = false;
                ev.currentTarget.textContent = "\ud83c\udfad Mimic";
                return;
              }
              toast(`\u2713 Now mimicking @${r.mimicHandle} (${r.fetched} samples)`, "ok", 2500);
              logTelemetry("accounts", "voice_mimic", {
                accountId: account.id,
                mimic: r.mimicHandle,
                fetched: r.fetched,
              });
              openVoiceProfileDrawer(account);
            },
          },
          "\ud83c\udfad Mimic",
        ),
      ]),
      samplesInput,
    ]),
  );

  // ── Voice performance (Phase 4) ─────────────────────────────────
  // Aggregated AI-reply telemetry per voice source. Tells the user
  // "is my voice profile actually working?" by showing sent counts
  // (and reply rates once Phase 4B's reply-back attribution lands).
  const perfMount = el("div", {});
  body.appendChild(perfMount);
  (async () => {
    try {
      const r = await api("voice-profile-stats", { accountId: account.id });
      const stats = r?.stats || [];
      if (stats.length === 0) return; // no telemetry yet — hide section
      perfMount.appendChild(
        el(
          "div",
          {
            style:
              "font-size:11px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:5px;margin-top:6px;",
          },
          "Voice performance",
        ),
      );
      const list = el("div", { style: "display:flex;flex-direction:column;gap:5px;" });
      const total = stats.reduce((s, r2) => s + r2.sent, 0);
      const totalReplied = stats.reduce((s, r2) => s + r2.replied, 0);
      // Sort by sent desc
      const sorted = [...stats].sort((a, b) => b.sent - a.sent);
      for (const row of sorted) {
        const pct = total > 0 ? Math.round((row.sent / total) * 100) : 0;
        const ratePct = row.sent > 0 ? Math.round((row.rate || 0) * 100) : null;
        const isCurrent = profile?.source === row.source;
        list.appendChild(
          el(
            "div",
            {
              style: [
                "padding:6px 10px",
                "background:" + (isCurrent ? "rgba(99,102,241,0.10)" : "rgba(255,255,255,0.02)"),
                "border:1px solid " + (isCurrent ? "rgba(99,102,241,0.35)" : "#26262a"),
                "border-radius:6px",
                "font-size:11px",
                "display:flex",
                "align-items:center",
                "gap:10px",
              ].join(";"),
            },
            [
              el(
                "span",
                {
                  style:
                    "font-family:ui-monospace,monospace;color:" +
                    (isCurrent ? "#a5b4fc" : "#cbd5e1") +
                    ";flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;",
                  title: row.source,
                },
                isCurrent ? `${row.source}` : row.source,
              ),
              el(
                "span",
                {
                  style:
                    "color:#86efac;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums;flex-shrink:0;",
                },
                `${row.sent} sent`,
              ),
              ratePct !== null && row.replied > 0
                ? el(
                    "span",
                    {
                      style:
                        "color:#a78bfa;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums;flex-shrink:0;",
                    },
                    `${row.replied} replied (${ratePct}%)`,
                  )
                : el(
                    "span",
                    {
                      style:
                        "color:var(--text-4);font-family:ui-monospace,monospace;font-size:10px;flex-shrink:0;",
                    },
                    "(reply attribution: phase 4B)",
                  ),
            ],
          ),
        );
      }
      perfMount.appendChild(list);
      perfMount.appendChild(
        el(
          "div",
          {
            style:
              "font-size:10px;color:var(--text-4);margin-top:5px;font-family:ui-monospace,monospace;",
          },
          `Total: ${total} replies generated · ★ marks current profile`,
        ),
      );
    } catch (err) {
      perfMount.appendChild(
        el(
          "div",
          { class: "muted small", style: "font-style:italic;" },
          "Could not load voice performance stats.",
        ),
      );
    }
  })();

  // Save / Cancel footer
  body.appendChild(
    el(
      "div",
      {
        style:
          "display:flex;gap:8px;padding-top:6px;border-top:1px solid var(--border);justify-content:flex-end;",
      },
      [
        el(
          "button",
          { class: "btn btn-secondary", type: "button", onclick: closeDrawer },
          "Cancel",
        ),
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: async (ev) => {
              ev.currentTarget.disabled = true;
              ev.currentTarget.textContent = "Saving\u2026";
              const samples = samplesInput.value
                .split("\n")
                .map((l) => l.trim())
                .filter((l) => l.length > 0);
              const instruction = instructionInput.value.trim();
              const r = await api("voice-profile-set", {
                accountId: account.id,
                body: { samples, instruction, source: "manual" },
              }).catch((e) => ({ error: e?.message }));
              if (r?.error) {
                toast(`Save failed: ${r.error}`, "err");
                ev.currentTarget.disabled = false;
                ev.currentTarget.textContent = "Save";
                return;
              }
              toast(`\u2713 Voice profile saved (${samples.length} samples)`, "ok", 2000);
              logTelemetry("accounts", "voice_save", {
                accountId: account.id,
                samples: samples.length,
              });
              closeDrawer();
            },
          },
          "Save",
        ),
      ],
    ),
  );
}

// \u2500\u2500 Lockout-aware account row (X) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
//
// When an X account hits a CAPTCHA / challenge / restricted state,
// the backend's /api/twitter/command wrapper sets lockout_state on
// the social_accounts row + sends a Telegram alert. This UI surfaces
// that state with a red-banner row + "Mark recovered" button so the
// user can manually unlock after solving the challenge on x.com.
function buildLockoutAwareXRow(a) {
  const handle = `@${a.account_identifier || a.display_name || "?"}`;
  const isLocked = !!a.lockout_state || ["suspended", "logged_out"].includes(a.status);
  const lockoutLabel =
    {
      challenge: "\ud83d\udd12 CAPTCHA \u2014 solve on x.com",
      restricted: "\ud83d\udd12 Restricted \u2014 verify on x.com",
      captcha: "\ud83d\udd12 CAPTCHA \u2014 solve on x.com",
      suspended: "\u26d4 Suspended",
      logged_out: "\u26a0\ufe0f Logged out \u2014 re-link cookies",
      manual_pause: "\u23f8 Paused \u2014 flagged by user (X label / soft-restricted)",
    }[a.lockout_state || a.status] || "\ud83d\udd12 Locked";

  if (!isLocked) {
    // Healthy row \u2014 original layout + a quiet "Pause" button (visible
    // on hover) so the user can manually flag an account that X has
    // soft-restricted (e.g. the 'spam / inauthentic behavior' label
    // that doesn't return an HTTP error but means we should stop).
    const pauseBtn = el(
      "button",
      {
        class: "btn btn-tiny btn-secondary",
        type: "button",
        style: "padding:3px 8px;font-size:10px;opacity:0;transition:opacity 120ms;",
        title:
          "Manually pause this account \u2014 useful when X has flagged it without returning an error (soft shadow-ban / 'spam' label)",
        onclick: async (ev) => {
          ev.stopPropagation();
          const reason = prompt(
            `Pause ${handle}? Provide an optional reason (or leave blank).\n\n` +
              `Use this when X has soft-flagged the account (e.g. 'temporarily restricted' / 'may contain spam' label) \u2014 the API doesn't error but you should stop actions.`,
            "X soft-flagged: spam / inauthentic label",
          );
          if (reason === null) return; // cancelled
          const r = await api("social-account-pause", {
            accountId: a.id,
            body: { reason: reason.trim() || "Paused by user" },
          }).catch((e) => ({ error: e?.message }));
          if (r?.error) {
            toast(`Pause failed: ${r.error}`, "err");
            return;
          }
          toast(`\u23f8 ${handle} paused \u2014 actions blocked until resumed`, "ok", 2500);
          logTelemetry("accounts", "manual_pause", { platform: "twitter", accountId: a.id });
          renderAccounts();
        },
      },
      "\u23f8 Pause",
    );
    // Voice profile button \u2014 opens the voice editor drawer so the
    // user can manage few-shot samples that humanize AI replies.
    const voiceBtn = el(
      "button",
      {
        class: "btn btn-tiny btn-secondary",
        type: "button",
        style: "padding:3px 8px;font-size:10px;opacity:0;transition:opacity 120ms;",
        title:
          "Edit AI reply voice profile \u2014 sample tweets used as few-shot examples to make AI replies sound like THIS account",
        onclick: (ev) => {
          ev.stopPropagation();
          openVoiceProfileDrawer(a);
        },
      },
      "\ud83c\udf99 Voice",
    );
    const row = el(
      "div",
      {
        class: "channel-row",
        onmouseover: (e) => {
          const b = e.currentTarget.querySelector(".btn");
          if (b) b.style.opacity = "1";
        },
        onmouseout: (e) => {
          const b = e.currentTarget.querySelector(".btn");
          if (b) b.style.opacity = "0";
        },
      },
      [
        el("div", { class: "channel-icon" }, a.status === "active" ? "\u2705" : "\u23f8"),
        el("div", { class: "channel-name truncate" }, handle),
        el("div", { class: "channel-bar" }, [
          el("div", {
            class: `channel-bar-fill ${(a.health_score || 0) >= 70 ? "healthy" : "degraded"}`,
            style: `width: ${a.health_score || 0}%`,
          }),
        ]),
        el(
          "div",
          { class: "channel-detail" },
          `day ${a.warmup_day || "?"} \u00b7 ${a.outreach_sent_today || 0}/${a.max_daily_outreach || 0} today`,
        ),
        voiceBtn,
        pauseBtn,
      ],
    );
    return row;
  }

  // Locked row \u2014 red border, full-width banner, Mark recovered button.
  const lockedSince = a.locked_at ? fmtRel(a.locked_at) : null;
  return el(
    "div",
    {
      style: [
        "padding:10px 12px",
        "background:rgba(239,68,68,0.06)",
        "border:1px solid rgba(239,68,68,0.3)",
        "border-radius:8px",
        "margin-bottom:6px",
        "display:flex",
        "flex-direction:column",
        "gap:6px",
      ].join(";"),
    },
    [
      // Top row: handle + lock label + Mark recovered button
      el("div", { style: "display:flex;align-items:center;gap:10px;" }, [
        el("span", { style: "font-size:18px;" }, "\ud83d\udd12"),
        el("div", { style: "flex:1;min-width:0;" }, [
          el("div", { style: "font-size:13px;font-weight:600;color:var(--text-1);" }, handle),
          el(
            "div",
            {
              style:
                "font-size:11px;color:var(--red);font-family:ui-monospace,monospace;margin-top:2px;",
            },
            lockoutLabel + (lockedSince ? ` \u00b7 since ${lockedSince}` : ""),
          ),
        ]),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            title: "After you've solved the challenge on x.com, click this to resume actions",
            onclick: async (ev) => {
              if (
                !confirm(
                  `Have you solved the challenge for ${handle} on x.com? This unlocks the account so automations can run again.`,
                )
              )
                return;
              ev.currentTarget.disabled = true;
              ev.currentTarget.textContent = "Unlocking\u2026";
              const r = await api("social-account-unlock", { accountId: a.id }).catch((e) => ({
                error: e?.message,
              }));
              if (r?.error) {
                toast(`Unlock failed: ${r.error}`, "err");
                ev.currentTarget.disabled = false;
                ev.currentTarget.textContent = "Mark recovered";
                return;
              }
              toast(`\u2713 ${handle} unlocked \u2014 actions resumed`, "ok", 2500);
              logTelemetry("accounts", "lockout_unlock", { platform: "twitter", accountId: a.id });
              renderAccounts();
            },
          },
          "Mark recovered",
        ),
      ]),
      // Reason preview (if available \u2014 backend stores up to 280 chars)
      a.lockout_reason
        ? el(
            "div",
            {
              style:
                "font-size:10px;color:var(--text-2);font-family:ui-monospace,monospace;padding:4px 8px;background:rgba(0,0,0,0.2);border-radius:4px;white-space:pre-wrap;",
              title: a.lockout_reason,
            },
            a.lockout_reason.slice(0, 200) + (a.lockout_reason.length > 200 ? "\u2026" : ""),
          )
        : null,
    ],
  );
}

// Shared force-sync handler. The old inline handlers swallowed the error
// (`.catch(() => {})`) and then ALWAYS toasted "\u2705 Sync complete" \u2014 so a failed
// sync looked identical to a successful one (the exact "says synced when it
// isn't" class of bug). This verifies the result before claiming success and
// shows an in-flight spinner on the button via withProgress.
// `action` is the sw action name ("sync-twitter" etc); returns {ok}|{error}.
async function _forceSync(ev, action, label) {
  const btn = ev?.currentTarget;
  const run = async () => {
    let r;
    try {
      r = await api(action);
    } catch (e) {
      toast(`${label} sync failed: ${e?.message || "unreachable"}`, "err", 5000);
      return;
    }
    if (r?.error) {
      toast(`${label} sync failed: ${r.error}`, "err", 5000);
      return;
    }
    toast(`\u2705 ${label} synced`, "ok");
    setTimeout(renderAccounts, 800);
  };
  if (btn) await withProgress(btn, run);
  else await run();
}

async function renderAccounts() {
  showLoading("Loading accounts\u2026");
  // Distinguish a real load failure from "you have no accounts". A thrown
  // api() (bot offline / unauthorized) must show an error+retry, never an
  // empty page that reads as "nothing connected".
  let data;
  try {
    data = await api("accounts-all");
  } catch (e) {
    showError(e?.message || "Couldn't reach the bot", renderAccounts);
    return;
  }
  if (data?.error) {
    showError(data.error, renderAccounts);
    return;
  }

  function accountRow(icon, name, detail, barPct) {
    const children = [
      el("div", { class: "channel-icon" }, icon),
      el("div", { class: "channel-name truncate" }, name),
    ];
    if (barPct !== undefined) {
      children.push(
        el("div", { class: "channel-bar" }, [
          el("div", {
            class: `channel-bar-fill ${barPct >= 70 ? "healthy" : "degraded"}`,
            style: `width: ${barPct}%`,
          }),
        ]),
      );
    }
    children.push(el("div", { class: "channel-detail" }, detail));
    return el("div", { class: "channel-row" }, children);
  }

  // Unified per-account controls for EVERY platform (only X had these before):
  // Resync · Pause/Resume · Remove. All go through the owner-scoped
  // /api/accounts/:platform/:id/control endpoint (action-control).
  function _acctActions(platform, a) {
    const s = String(a.status || "").toLowerCase();
    const paused = s === "paused" || s === "cooldown" || !!a.cooldown_until;
    const ctrl = async (ev, action, opts = {}) => {
      if (opts.confirm && !confirm(opts.confirm)) return;
      const btn = ev?.currentTarget;
      const run = async () => {
        const r = await api("account-control", {
          platform,
          accountId: a.id,
          body: { action, ...(opts.body || {}) },
        }).catch((e) => ({ error: e?.message }));
        if (r?.error) {
          toast(`${action} failed: ${r.error}`, "err", 4000);
          return;
        }
        toast(`${action === "remove" ? "removed" : action}`, "ok", 1500);
        setTimeout(renderAccounts, 500);
      };
      if (btn) await withProgress(btn, run);
      else await run();
    };
    const name = a.label || a.identifier || a.account_identifier || a.email || a.phone || a.id;
    const canProxy = ["twitter", "linkedin", "telegram"].includes(platform);
    const canTest = ["twitter", "linkedin"].includes(platform);

    // Inline proxy picker (lazy-loads the owner's proxies on first open).
    const proxyPanel = el("div", {
      style:
        "display:none;margin:4px 0 6px 38px;padding:8px;border:1px solid var(--border-soft);border-radius:6px;background:var(--bg-card);max-width:360px;",
    });
    const openProxy = async () => {
      if (proxyPanel.style.display === "block") {
        proxyPanel.style.display = "none";
        return;
      }
      proxyPanel.style.display = "block";
      proxyPanel.innerHTML = "";
      proxyPanel.appendChild(
        el("div", { class: "muted", style: "font-size:11px;" }, "Loading proxies…"),
      );
      const pr = await api("proxies-list").catch((e) => ({ error: e?.message }));
      proxyPanel.innerHTML = "";
      if (pr?.error) {
        proxyPanel.appendChild(el("div", { class: "muted", style: "font-size:11px;" }, pr.error));
        return;
      }
      const opts = [
        { id: null, label: "Direct (no proxy)" },
        ...(pr.proxies || []).map((p) => ({
          id: p.id,
          label: `${p.name || p.host}:${p.port} ${p.is_active ? "" : "(inactive)"}${p.latency_ms != null ? ` · ${p.latency_ms}ms` : ""}`,
        })),
      ];
      const setProxy = async (proxyId) => {
        const r = await api("account-control", {
          platform,
          accountId: a.id,
          body: { action: "set-proxy", proxy_id: proxyId },
        }).catch((e) => ({ error: e?.message }));
        if (r?.error) {
          toast(`Proxy: ${r.error}`, "err");
          return;
        }
        toast(proxyId ? "Proxy set" : "Proxy cleared (direct)", "ok");
        setTimeout(renderAccounts, 400);
      };
      for (const o of opts) {
        const current = (a.proxy_id || null) === (o.id || null);
        proxyPanel.appendChild(
          el(
            "div",
            {
              style:
                "display:flex;align-items:center;gap:8px;padding:4px 0;cursor:pointer;font-size:12px;",
              onclick: () => setProxy(o.id),
            },
            [
              el("span", { style: `width:14px;color:var(--green);` }, current ? icon("check") : ""),
              el("span", { style: current ? "color:#eee;" : "color:#bbb;" }, o.label),
            ],
          ),
        );
      }
      // Inline "+ Add proxy" so proxies are fully manageable in the dashboard
      // (they were bot-command-only before).
      const PIN =
        "width:100%;padding:5px 7px;margin-bottom:5px;background:#0d0d12;border:1px solid var(--border-soft);border-radius:5px;color:#eee;font-size:12px;box-sizing:border-box;";
      const fName = el("input", { placeholder: "label (optional)", style: PIN });
      const fHost = el("input", { placeholder: "host (e.g. 1.2.3.4)", style: PIN });
      const fPort = el("input", { placeholder: "port", type: "number", style: PIN });
      const fType = el(
        "select",
        { style: PIN },
        ["socks5", "http", "https", "socks4"].map((t) => el("option", { value: t }, t)),
      );
      const fUser = el("input", { placeholder: "username (optional)", style: PIN });
      const fPass = el("input", {
        placeholder: "password (optional)",
        type: "password",
        style: PIN,
      });
      const formBox = el("div", { style: "display:none;margin-top:6px;" }, [
        fName,
        fHost,
        fPort,
        fType,
        fUser,
        fPass,
        el(
          "button",
          {
            class: "btn btn-primary btn-tiny",
            type: "button",
            onclick: async (e) => {
              if (!fHost.value.trim() || !fPort.value.trim()) {
                toast("Host and port required", "err");
                return;
              }
              await withProgress(e.currentTarget, async () => {
                const r = await api("proxy-add", {
                  body: {
                    name: fName.value.trim(),
                    type: fType.value,
                    host: fHost.value.trim(),
                    port: parseInt(fPort.value, 10),
                    username: fUser.value.trim(),
                    password: fPass.value,
                  },
                }).catch((er) => ({ error: er?.message }));
                if (r?.error) {
                  toast(r.error, "err");
                  return;
                }
                toast("Proxy added", "ok");
                proxyPanel.style.display = "none";
                openProxy();
              });
            },
          },
          "Add proxy",
        ),
      ]);
      const addWrap = el(
        "div",
        { style: "margin-top:6px;border-top:1px solid var(--border-soft);padding-top:6px;" },
        [
          el(
            "button",
            {
              class: "btn btn-tiny btn-secondary",
              type: "button",
              onclick: () => {
                formBox.style.display = formBox.style.display === "block" ? "none" : "block";
              },
            },
            "+ Add proxy",
          ),
          formBox,
        ],
      );
      proxyPanel.appendChild(addWrap);
    };

    const bar = el(
      "div",
      { style: "display:flex;gap:6px;flex-wrap:wrap;padding:0 0 6px 38px;margin-top:-2px;" },
      [
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            title: "Re-validate / reconnect this session",
            onclick: (ev) => ctrl(ev, "resync"),
          },
          "↻ Resync",
        ),
        canTest
          ? el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "Live-test this platform's session",
                onclick: async (ev) => {
                  await withProgress(ev.currentTarget, async () => {
                    const r = await api("connections-test", { body: { platform } }).catch((e) => ({
                      error: e?.message,
                    }));
                    if (r?.error) {
                      toast(`Test: ${r.error}`, "err", 4000);
                      return;
                    }
                    toast(
                      `${r.ok ? "connected" : "🔴 " + (r.detail || r.status || "not connected")}${r.identifier ? " · " + r.identifier : ""}`,
                      r.ok ? "ok" : "err",
                      5000,
                    );
                  });
                },
              },
              "Test",
            )
          : null,
        canProxy
          ? el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "Assign a proxy to this account",
                onclick: openProxy,
              },
              "Proxy",
            )
          : null,
        paused
          ? el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                onclick: (ev) => ctrl(ev, "resume"),
              },
              "▶ Resume",
            )
          : el(
              "button",
              {
                class: "btn btn-tiny btn-secondary",
                type: "button",
                title: "Pause this account for 24h",
                onclick: (ev) => ctrl(ev, "pause", { body: { hours: 24 } }),
              },
              "⏸ Pause",
            ),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            style: "color:#e06a6a;",
            title: "Remove this account",
            onclick: (ev) =>
              ctrl(ev, "remove", {
                confirm: `Remove account “${name}”? This deletes its stored session.`,
              }),
          },
          "Remove",
        ),
      ],
    );
    return el("div", {}, [bar, proxyPanel]);
  }
  // Wrap any account row node with the unified action bar.
  const withActions = (node, platform, a) =>
    el("div", { style: "border-bottom:1px solid #1c1c26;" }, [node, _acctActions(platform, a)]);

  // Per-account daily caps (schema-v117). The server is the authority: it clamps
  // to the premium table and re-applies the warmup multiplier, then echoes back
  // the caps that actually apply — so we always re-render from the response
  // rather than from what was typed.
  function tgLimitsBar(a) {
    const overridden = Object.keys(a.limit_overrides || {}).length;
    return el("div", { style: "margin:2px 0 8px 38px;display:flex;gap:8px;align-items:center;" }, [
      el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          style: "font-size:11px;padding:3px 8px;",
          onclick: () => openTgLimitsModal(a),
        },
        `Daily limits${overridden ? ` (${overridden} custom)` : ""}`,
      ),
      el(
        "span",
        { style: "font-size:11px;color:#8b8b9a;" },
        `DM ${a.limits?.dm_send ?? "?"}/day · groups ${a.limits?.group_message ?? "?"}/day` +
          (a.caps?.warmup_multiplier != null && a.caps.warmup_multiplier < 1
            ? ` · warmup ${Math.round(a.caps.warmup_multiplier * 100)}%`
            : ""),
      ),
    ]);
  }

  function openTgLimitsModal(a) {
    // Riskiest first — these are the ones that get Telegram accounts limited.
    const ORDER = [
      ["dm_send", "Direct messages"],
      ["group_message", "Group messages"],
      ["invite", "Invites"],
      ["broadcast", "Broadcasts"],
      ["group_join", "Group joins"],
      ["engage_reply", "Replies"],
      ["group_react", "Reactions"],
      ["forward", "Forwards"],
      ["auto_reply", "Auto-replies"],
      ["group_engage", "Group engage"],
      ["warmer_msg", "Warmer messages"],
      ["scrape", "Scrapes"],
      ["phone_check", "Phone checks"],
    ];
    const limits = a.limits || {};
    const used = a.used_by_type || {};
    const overrides = a.limit_overrides || {};

    const rows = ORDER.filter(([k]) => limits[k] != null).map(([key, label]) => {
      const input = el("input", {
        type: "number",
        min: "1",
        value: String(overrides[key] ?? limits[key] ?? ""),
        style:
          "width:70px;background:var(--bg-1);border:1px solid var(--border-soft);color:var(--text-1);border-radius:5px;padding:4px 6px;font-size:12px;",
      });
      const save = el(
        "button",
        {
          class: "btn btn-secondary",
          type: "button",
          style: "font-size:11px;padding:3px 8px;",
          onclick: (ev) =>
            mutateAction(
              ev,
              "tg-set-limit",
              { body: { account_id: a.id, action_type: key, limit: Number(input.value) } },
              {
                success: `${label}: cap saved`,
                failPrefix: `${label} cap`,
                after: () => renderAccounts(),
              },
            ),
        },
        "Save",
      );
      const reset = overrides[key]
        ? el(
            "button",
            {
              class: "btn btn-secondary",
              type: "button",
              style: "font-size:11px;padding:3px 8px;",
              onclick: (ev) =>
                mutateAction(
                  ev,
                  "tg-set-limit",
                  { body: { account_id: a.id, action_type: key, limit: null } },
                  {
                    success: `${label}: back to default`,
                    failPrefix: `${label} reset`,
                    after: () => renderAccounts(),
                  },
                ),
            },
            "Reset",
          )
        : null;

      return el(
        "div",
        {
          style:
            "display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid #1c1c26;",
        },
        [
          el("div", { style: "flex:1;font-size:12px;" }, [
            el("span", {}, label),
            overrides[key]
              ? el("span", { style: "color:#6ea8ff;font-size:11px;" }, "  · custom")
              : null,
          ]),
          el(
            "span",
            { style: "font-size:11px;color:#8b8b9a;min-width:78px;" },
            `${used[key] || 0} used today`,
          ),
          input,
          save,
          reset,
        ].filter(Boolean),
      );
    });

    openModal({
      title: `Daily limits — ${a.label || a.phone}`,
      subtitle:
        "Per-account caps. A value can only lower a limit or raise it within the built-in maximum, " +
        (a.caps?.warmup_multiplier != null && a.caps.warmup_multiplier < 1
          ? `and warmup (${Math.round(a.caps.warmup_multiplier * 100)}% capacity) still scales it down on top.`
          : "and warmup scaling still applies on top."),
      body: el("div", {}, rows),
      maxWidth: "560px",
    });
  }

  function addAccountHint(icon, title, steps) {
    return el("div", { class: "alert-card warning", style: "margin-top: 8px;" }, [
      el("div", { class: "alert-card-title" }, `${icon} ${title}`),
      el("div", { class: "alert-card-body" }, [
        el(
          "ol",
          { style: "margin: 4px 0 0 16px; line-height: 1.8; font-size: 12px;" },
          steps.map((s) => el("li", {}, s)),
        ),
      ]),
    ]);
  }

  function smtpForm() {
    const wrap = el("div", { style: "margin-top: 12px; display: none;" });
    const fields = [
      { id: "smtp-email", label: "Email address", placeholder: "you@gmail.com", type: "email" },
      { id: "smtp-host", label: "SMTP host", placeholder: "smtp.gmail.com" },
      { id: "smtp-port", label: "SMTP port", placeholder: "587", type: "number", small: true },
      { id: "smtp-user", label: "SMTP username", placeholder: "you@gmail.com" },
      {
        id: "smtp-pass",
        label: "SMTP password / app password",
        placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022",
        type: "password",
      },
    ];
    for (const f of fields) {
      wrap.appendChild(
        el(
          "label",
          {
            style:
              "display: block; font-size: 10px; color: var(--text-4); text-transform: uppercase; letter-spacing: 0.06em; margin-top: 8px;",
          },
          f.label,
        ),
      );
      wrap.appendChild(
        el("input", {
          id: f.id,
          class: "filter-input",
          type: f.type || "text",
          placeholder: f.placeholder || "",
          style: f.small ? "width: 100px; min-width: 100px;" : "min-width: 100%; margin-top: 2px;",
        }),
      );
    }
    wrap.appendChild(
      el(
        "div",
        { style: "margin-top: 4px; font-size: 10px; color: var(--text-4);" },
        "For Gmail, use an App Password (Google Account \u2192 Security \u2192 App passwords).",
      ),
    );
    wrap.appendChild(
      el(
        "button",
        {
          class: "btn btn-primary",
          style: "margin-top: 10px;",
          type: "button",
          onclick: async () => {
            const email = (document.getElementById("smtp-email")?.value || "").trim();
            const host = (document.getElementById("smtp-host")?.value || "").trim();
            const port = parseInt(document.getElementById("smtp-port")?.value) || 587;
            const user = (document.getElementById("smtp-user")?.value || "").trim();
            const pass = (document.getElementById("smtp-pass")?.value || "").trim();
            if (!email || !host || !user || !pass) {
              toast("Fill all fields", "err");
              return;
            }
            toast("Saving SMTP account\u2026", "info");
            const r = await api("connect-account", {
              body: {
                platform: "email",
                email,
                smtpHost: host,
                smtpPort: port,
                smtpUser: user,
                smtpPass: pass,
              },
            }).catch((e) => ({ error: e.message }));
            if (r?.ok || r?.success) {
              toast("\u2705 SMTP account added", "ok");
              renderAccounts();
            } else toast(`\u274c ${r?.error || "Failed"}`, "err");
          },
        },
        "Save SMTP Account",
      ),
    );
    return wrap;
  }

  const tgAccounts = data.telegram || [];
  const xAccounts = data.twitter || [];
  const liAccounts = data.linkedin || [];
  const smtpAccounts = data.smtp || [];

  const _totalConnected =
    tgAccounts.length + xAccounts.length + liAccounts.length + smtpAccounts.length;
  const content = el("div", {}, [
    el("div", { class: "page-header" }, [
      el("div", {}, [
        // Merged surface (2026-05-31): this page is both "connect new" and
        // "manage existing" — the former #connections page now forwards here.
        el("div", { class: "page-title" }, "Accounts & Connections"),
        el(
          "div",
          { class: "page-subtitle" },
          _totalConnected > 0
            ? `${_totalConnected} connected across ${[tgAccounts, xAccounts, liAccounts, smtpAccounts].filter((a) => a.length > 0).length} platforms — connect more below, or manage health & sync.`
            : "Connect your first account below — X, LinkedIn, Telegram or Email — then pick a Goal to put it to work.",
        ),
      ]),
    ]),
    // Account-health summary strip — connected / needs-reconnect / cooldown
    // at a glance (the #1 reason actions "do nothing" is a dead session).
    (() => {
      const all = [...xAccounts, ...liAccounts, ...tgAccounts, ...smtpAccounts];
      if (!all.length) return null;
      let ok = 0,
        bad = 0,
        cd = 0;
      for (const a of all) {
        if (a.lockout_state && a.lockout_state !== "none") {
          bad++;
          continue;
        }
        const s = String(a.status || "").toLowerCase();
        if (s === "active") ok++;
        else if (s === "paused" || s === "cooldown" || a.cooldown_until) cd++;
        else bad++;
      }
      const pill = (t, c) =>
        el("span", { style: `font-size:11px;padding:2px 9px;border-radius:8px;${c}` }, t);
      return el(
        "div",
        { style: "display:flex;gap:8px;align-items:center;margin:0 0 12px;flex-wrap:wrap;" },
        [
          pill(`${ok} connected`, "background:#143d2a;color:var(--green);"),
          bad ? pill(`${bad} needs reconnect`, "background:#3a1a1a;color:var(--red);") : null,
          cd ? pill(`⏸ ${cd} paused / cooldown`, "background:#3a3320;color:#c9a96a;") : null,
        ],
      );
    })(),

    // Goal CTA — carried over from the old Connections page so the
    // connect → activate journey still has its next-step nudge.
    el(
      "div",
      {
        style:
          "margin:4px 0 14px;padding:10px 12px;border:1px solid #2a3a2a;border-radius:8px;background:#0f1a13;display:flex;align-items:center;gap:10px;",
      },
      [
        el(
          "div",
          { style: "flex:1;font-size:12px;color:#bbb;" },
          _totalConnected > 0
            ? "Account connected? Pick a goal and we'll set up the right automations."
            : "Once an account's connected, pick a goal and we'll set up the automations for it.",
        ),
        el(
          "button",
          { class: "btn btn-primary btn-tiny", onclick: () => navigate("objectives") },
          "Pick a goal",
        ),
      ],
    ),

    // Twitter / X
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, `Twitter / X (${xAccounts.length}/5)`),
      ...(xAccounts.length > 0
        ? xAccounts.map((a) => withActions(buildLockoutAwareXRow(a), "twitter", a))
        : []),
      xAccounts.length < 5
        ? addAccountHint(
            "\ud83d\udc26",
            `Add X account (${5 - xAccounts.length} slots remaining)`,
            [
              "Open x.com and log into the account you want to add",
              "Click the Nexus extension icon in your browser toolbar",
              'Click "Sync X Session" \u2014 the cookies are captured automatically',
              "Repeat for each account (use incognito or separate profiles for multiple)",
            ],
          )
        : null,
      xAccounts.length < 5
        ? el("div", { style: "display: flex; gap: 8px; margin-top: 8px;" }, [
            el(
              "button",
              {
                class: "btn btn-primary",
                type: "button",
                onclick: () => openAccountSetupWizard("twitter", xAccounts.length),
              },
              "+ Connect new X account",
            ),
            el(
              "button",
              {
                class: "btn btn-secondary",
                type: "button",
                onclick: (ev) => _forceSync(ev, "sync-twitter", "X"),
              },
              "\u21bb Force sync",
            ),
          ])
        : null,
    ]),

    // LinkedIn
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, `LinkedIn (${liAccounts.length}/5)`),
      ...(liAccounts.length > 0
        ? liAccounts.map((a) =>
            withActions(
              accountRow(
                a.status === "active" ? "\u2705" : "\u274c",
                a.identifier,
                `${a.status} \u00b7 validated ${a.last_validated_at ? fmtDate(a.last_validated_at) : "never"}`,
              ),
              "linkedin",
              a,
            ),
          )
        : []),
      liAccounts.length < 5
        ? addAccountHint(
            "\ud83d\udd17",
            `Add LinkedIn account (${5 - liAccounts.length} slots remaining)`,
            [
              "Open linkedin.com and log into the account you want to add",
              "Click the Nexus extension icon in your browser toolbar",
              'Click "Sync LinkedIn Session" \u2014 the li_at cookie is captured automatically',
              "Sessions expire every ~14 days \u2014 re-sync when status shows expired",
            ],
          )
        : null,
      liAccounts.length < 5
        ? el("div", { style: "display: flex; gap: 8px; margin-top: 8px;" }, [
            el(
              "button",
              {
                class: "btn btn-primary",
                type: "button",
                onclick: () => openAccountSetupWizard("linkedin", liAccounts.length),
              },
              "+ Connect new LinkedIn account",
            ),
            el(
              "button",
              {
                class: "btn btn-secondary",
                type: "button",
                onclick: (ev) => _forceSync(ev, "sync-linkedin", "LinkedIn"),
              },
              "\u21bb Force sync",
            ),
          ])
        : null,
    ]),

    // Telegram
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, `Telegram (${tgAccounts.length}/5)`),
      ...(tgAccounts.length > 0
        ? tgAccounts.map((a) =>
            el("div", {}, [
              withActions(
                accountRow(
                  a.status === "active" ? "\u2705" : "\u274c",
                  a.label || a.phone,
                  `day ${a.warmup_day || "?"} \u00b7 ${a.daily_actions_count || 0} actions \u00b7 ${a.has_telegram_premium ? "Premium" : "Standard"}`,
                  a.health_score || 0,
                ),
                "telegram",
                a,
              ),
              tgLimitsBar(a),
            ]),
          )
        : []),
      tgAccounts.length < 5
        ? addAccountHint(
            "\u2708\ufe0f",
            `Add Telegram account (${5 - tgAccounts.length} slots remaining)`,
            [
              "Get your API ID + Hash at my.telegram.org \u2192 API development tools",
              "Click \u201c+ Connect new Telegram account\u201d below",
              "Enter API ID, API Hash and phone \u2014 then the code Telegram sends (and 2FA if set)",
              "Done \u2014 the userbot connects over MTProto right here, no bot command needed",
            ],
          )
        : null,
      tgAccounts.length < 5
        ? el(
            "button",
            {
              class: "btn btn-primary",
              style: "margin-top: 8px;",
              type: "button",
              onclick: () => openAccountSetupWizard("telegram", tgAccounts.length),
            },
            "+ Connect new Telegram account",
          )
        : null,
    ]),

    // SMTP
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, `Email / SMTP (${smtpAccounts.length}/5)`),
      ...(smtpAccounts.length > 0
        ? smtpAccounts.map((a) =>
            withActions(
              accountRow(
                a.status === "active" ? "\u2705" : "\u274c",
                a.email,
                `${a.smtp_host}:${a.smtp_port || 587}`,
              ),
              "email",
              a,
            ),
          )
        : []),
      smtpAccounts.length < 5
        ? el(
            "button",
            {
              class: "btn btn-primary",
              style: "margin-top: 8px;",
              type: "button",
              onclick: () => openAccountSetupWizard("email", smtpAccounts.length),
            },
            "+ Connect new SMTP account",
          )
        : null,
    ]),

    // ── Data sources (read-only) — these are auto-detected sessions used for
    // scraping, not outreach accounts. Separated so they don't read as
    // first-class sending accounts.
    el(
      "div",
      {
        style:
          "margin:18px 0 6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--text-4);",
      },
      "Data sources (read-only)",
    ),

    // CryptoRank
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "CryptoRank"),
      data.cryptorank
        ? accountRow(
            data.cryptorank.status === "active" ? "\u2705" : "\u274c",
            data.cryptorank.identifier || "Session",
            `${data.cryptorank.status} \u00b7 synced ${data.cryptorank.updated_at ? fmtDate(data.cryptorank.updated_at) : "unknown"}`,
          )
        : el("div", {}, [
            addAccountHint("\ud83d\udcca", "Connect CryptoRank", [
              "Log into cryptorank.io in this browser",
              "The Nexus extension auto-detects your session within ~3 seconds",
              "You'll see a confirmation toast + Telegram DM",
            ]),
            el("div", { style: "display: flex; gap: 8px; margin-top: 8px;" }, [
              el(
                "button",
                {
                  class: "btn btn-secondary",
                  type: "button",
                  onclick: () => window.open("https://cryptorank.io", "_blank"),
                },
                "Open CryptoRank login \u2192",
              ),
              el(
                "button",
                {
                  class: "btn btn-primary",
                  type: "button",
                  onclick: (ev) => _forceSync(ev, "sync-cryptorank", "CryptoRank"),
                },
                "\u21bb Sync now",
              ),
            ]),
          ]),
    ]),

    // Luma
    el("div", { class: "card" }, [
      el("div", { class: "card-title" }, "Luma"),
      data.luma
        ? accountRow(
            data.luma.status === "active" ? "\u2705" : "\u274c",
            data.luma.luma_email || "Session",
            data.luma.status,
          )
        : el("div", {}, [
            addAccountHint("\ud83c\udfab", "Connect Luma", [
              "Open lu.ma and log in with your account",
              "The Nexus extension auto-detects your session, or click Sync below",
            ]),
            el("div", { style: "display: flex; gap: 8px; margin-top: 8px;" }, [
              el(
                "button",
                {
                  class: "btn btn-secondary",
                  type: "button",
                  onclick: () => window.open("https://lu.ma", "_blank"),
                },
                "Open lu.ma \u2192",
              ),
              el(
                "button",
                {
                  class: "btn btn-primary",
                  type: "button",
                  onclick: (ev) => _forceSync(ev, "sync-luma", "Luma"),
                },
                "\u21bb Sync now",
              ),
            ]),
          ]),
    ]),
  ]);
  setContentNode(content);
}
