// dashboard-templates-picker.js — extracted verbatim from dashboard.js (classic script,
// shared global scope). Loaded by dashboard.html before dashboard.js so its
// top-level declarations are defined when init() runs. Pure move, no logic change.
// ── Templates picker modal (Phase 6) ────────────────────────────────

let _composerTemplates = null;
let _composerTemplateJtbds = null;
let _composerTemplateTracks = null;

async function _ensureCompositionTemplates() {
  if (_composerTemplates)
    return {
      templates: _composerTemplates,
      jtbds: _composerTemplateJtbds,
      tracks: _composerTemplateTracks,
    };
  const r = await api("composition-templates").catch(() => null);
  if (r && Array.isArray(r.templates)) {
    _composerTemplates = r.templates;
    _composerTemplateJtbds = r.jtbds || [];
    _composerTemplateTracks = r.tracks || [];
  }
  return {
    templates: _composerTemplates || [],
    jtbds: _composerTemplateJtbds || [],
    tracks: _composerTemplateTracks || [],
  };
}

/**
 * Phase ZC — Test-all canvas session.
 *
 * Session state lives on `dashboardState.testAllSession` (created on start,
 * cleared on cancel/complete). Survives navigate() because dashboardState is
 * module-scope.
 *
 * Flow:
 *   1. startTestAllCanvasSession(comps) — initializes session, navigates to
 *      first comp's canvas
 *   2. renderWorkflowCanvas detects the session, auto-fires Test on 3 ~1.5s
 *      after first paint, awaits completion, calls advanceTestAllSession
 *   3. advanceTestAllSession records the result + navigates to next comp's
 *      canvas (or to /workflows/test-all-results when done)
 *   4. _renderTestAllSessionBar renders a fixed-position progress bar across
 *      every page (re-injected on every setContentNode via runAfterRender hook)
 */
function startTestAllCanvasSession(comps) {
  if (!Array.isArray(comps) || comps.length === 0) return;
  dashboardState.testAllSession = {
    queue: comps.map((c) => ({ id: c.id, name: c.name, _raw: c })),
    currentIndex: 0,
    status: "running",
    results: [],
    startedAt: new Date().toISOString(),
    finishedAt: null,
  };
  _renderTestAllSessionBar();
  // Navigate to first comp's canvas
  _navigateToSessionCurrent();
}

function _navigateToSessionCurrent() {
  const s = dashboardState.testAllSession;
  if (!s || s.status !== "running") return;
  const comp = s.queue[s.currentIndex];
  if (!comp) return;
  // Stash draft so the canvas renders this composition (mirrors the standard
  // composer-card click flow used elsewhere).
  dashboardState.composerDraft = comp._raw || comp;
  s._intraNav = true;
  navigate("workflows/canvas");
  s._intraNav = false;
}

function advanceTestAllSession(result) {
  const s = dashboardState.testAllSession;
  if (!s) return;
  // Record this comp's result (defensive — accept partial result objects)
  s.results.push({
    compositionId: result?.compositionId ?? s.queue[s.currentIndex]?.id,
    name: result?.name ?? s.queue[s.currentIndex]?.name,
    status: result?.status || "fail",
    summary: result?.summary || "no summary",
    durationMs: result?.durationMs ?? null,
    runId: result?.runId ?? null,
  });
  if (s.status !== "running") {
    // User cancelled or paused mid-run; do nothing — bar reflects state
    _renderTestAllSessionBar();
    return;
  }
  s.currentIndex++;
  if (s.currentIndex >= s.queue.length) {
    _finishTestAllSession();
  } else {
    _renderTestAllSessionBar();
    _navigateToSessionCurrent();
  }
}

function _finishTestAllSession() {
  const s = dashboardState.testAllSession;
  if (!s) return;
  s.status = "complete";
  s.finishedAt = new Date().toISOString();
  _renderTestAllSessionBar();
  s._intraNav = true;
  navigate("workflows/test-all-results");
  s._intraNav = false;
}

function cancelTestAllSession() {
  const s = dashboardState.testAllSession;
  if (!s) return;
  s.status = "cancelled";
  s.finishedAt = new Date().toISOString();
  _renderTestAllSessionBar();
  s._intraNav = true;
  navigate("workflows/test-all-results");
  s._intraNav = false;
}

function pauseTestAllSession() {
  const s = dashboardState.testAllSession;
  if (!s || s.status !== "running") return;
  s.status = "paused";
  _renderTestAllSessionBar();
}

function resumeTestAllSession() {
  const s = dashboardState.testAllSession;
  if (!s || s.status !== "paused") return;
  s.status = "running";
  _renderTestAllSessionBar();
  // If we're already on the right canvas, the renderWorkflowCanvas hook will
  // fire on the next render — but we may need to re-navigate to trigger it.
  _navigateToSessionCurrent();
}

/**
 * Renders / updates / removes the persistent top-bar based on
 * dashboardState.testAllSession state. Idempotent — safe to call repeatedly.
 * Mounted once at document.body level so it persists across navigation.
 */
function _renderTestAllSessionBar() {
  const existing = document.getElementById("test-all-session-bar");
  const s = dashboardState.testAllSession;
  // No active session, or completed/cancelled — remove bar
  if (!s || s.status === "complete" || s.status === "cancelled") {
    if (existing) existing.remove();
    return;
  }
  const isPaused = s.status === "paused";
  const comp = s.queue[s.currentIndex];
  const progress = `${s.currentIndex + 1} of ${s.queue.length}`;
  const bg = isPaused ? "#3a2a0a" : "#1a2a3a";
  const accent = isPaused ? "#daaa5a" : "#5adada";

  const bar = existing || document.createElement("div");
  bar.id = "test-all-session-bar";
  bar.setAttribute(
    "style",
    [
      "position:fixed",
      "top:0",
      "left:0",
      "right:0",
      "z-index:9998",
      `background:${bg}`,
      `border-bottom:2px solid ${accent}`,
      "padding:8px 16px",
      "display:flex",
      "align-items:center",
      "gap:12px",
      "font-size:12px",
      "color:var(--text-1)",
      "box-shadow:0 2px 12px rgba(0,0,0,0.4)",
    ].join(";"),
  );
  bar.innerHTML = "";

  const icon = document.createElement("span");
  icon.style.fontSize = "16px";
  icon.textContent = isPaused ? "⏸" : "🧪";
  bar.appendChild(icon);

  const status = document.createElement("span");
  status.style.fontWeight = "600";
  status.style.color = accent;
  status.textContent = isPaused ? `Paused at ${progress}` : `Testing ${progress}`;
  bar.appendChild(status);

  const sep = document.createElement("span");
  sep.style.color = "var(--text-4)";
  sep.textContent = "·";
  bar.appendChild(sep);

  const compName = document.createElement("span");
  compName.style.color = "#ccc";
  compName.style.maxWidth = "400px";
  compName.style.overflow = "hidden";
  compName.style.textOverflow = "ellipsis";
  compName.style.whiteSpace = "nowrap";
  compName.textContent = `"${comp?.name || "?"}"`;
  bar.appendChild(compName);

  // Spacer
  const spacer = document.createElement("span");
  spacer.style.flex = "1";
  bar.appendChild(spacer);

  // Mini progress badge: pass / fail / skipped counts so far
  const counts = s.results.reduce((a, r) => {
    a[r.status] = (a[r.status] || 0) + 1;
    return a;
  }, {});
  const countsEl = document.createElement("span");
  countsEl.style.fontSize = "11px";
  countsEl.style.color = "var(--text-2)";
  countsEl.textContent = `${counts.pass || 0} ✅ · ${counts.fail || 0} ❌ · ${counts.skipped || 0} ↷`;
  bar.appendChild(countsEl);

  // Pause / Resume button
  const pauseBtn = document.createElement("button");
  pauseBtn.style.cssText =
    "padding:4px 10px;background:rgba(255,255,255,0.06);border:1px solid #444;color:var(--text-2);border-radius:4px;cursor:pointer;font-size:11px;";
  pauseBtn.textContent = isPaused ? "▶ Resume" : "⏸ Pause";
  pauseBtn.onclick = () => (isPaused ? resumeTestAllSession() : pauseTestAllSession());
  bar.appendChild(pauseBtn);

  // Cancel button
  const cancelBtn = document.createElement("button");
  cancelBtn.style.cssText =
    "padding:4px 10px;background:rgba(8,9,11,0.3);border:1px solid rgba(248,113,113,0.38);color:var(--red);border-radius:var(--radius-sm);cursor:pointer;font-size:11px;";
  cancelBtn.textContent = "⏹ Cancel";
  cancelBtn.onclick = () => {
    if (confirm("Cancel test session? Compositions already run will appear in the results.")) {
      cancelTestAllSession();
    }
  };
  bar.appendChild(cancelBtn);

  if (!existing) document.body.appendChild(bar);
}

/**
 * Phase ZB — "Test all my workflows" modal.
 *
 * Click "▶ Test all my workflows" on Active workflows page → this opens.
 * Lists every saved composition in card form. Click "Run all" → backend
 * iterates them serially, dry-running each one. Per-composition card
 * transitions: pending → running → pass/fail/skipped. Each card is
 * expandable to show the per-play log streamed from runComposition's
 * preview output.
 *
 * The user sees their workflows actually execute (in dry-run, no real
 * sends), step by step.
 */
let _testAllPollTimer = null;
function _stopTestAllPoll() {
  if (_testAllPollTimer) {
    clearInterval(_testAllPollTimer);
    _testAllPollTimer = null;
  }
}

/**
 * Phase ZE2 — Pre-flight configuration wizard for test-all with templates.
 *
 * For each unactivated template, lets the user fill in real values:
 *   - param_placeholders → typed inputs (string/number/array/boolean)
 *   - special string keys (`tag`, `proposal_template`) → smart pickers
 *   - channels[].account_id === null → account dropdown (filtered by platform)
 *
 * User can also Skip a template entirely (won't be tested).
 *
 * Resolves to:
 *   { configs: { <tplId>: { skip?, params?, channelAccounts? } } }
 *   or null if cancelled.
 *
 * @param {Array} templates — pre-made template objects (id, name, description, body, param_placeholders, ...)
 */
/**
 * Phase ZG — Persisted wizard memory. Stores per-template last-used values
 * in localStorage so re-testing the same template pre-fills instantly. The
 * key is namespaced by template id; an opt-out is to never call _saveWizardConfig.
 */
const WIZARD_MEMORY_KEY = "nexus.wizard.lastConfigs";
function _loadWizardMemory() {
  try {
    return JSON.parse(localStorage.getItem(WIZARD_MEMORY_KEY) || "{}");
  } catch {
    return {};
  }
}
function _saveWizardConfig(templateId, config) {
  try {
    const mem = _loadWizardMemory();
    mem[templateId] = { ...config, lastUsedAt: new Date().toISOString() };
    localStorage.setItem(WIZARD_MEMORY_KEY, JSON.stringify(mem));
  } catch {}
}
function _getSavedWizardConfig(templateId) {
  return _loadWizardMemory()[templateId] || null;
}

/**
 * Phase ZG — Compute smart-default values for a placeholder based on the
 * picker data. Returns the value to pre-fill (or null/empty if no sensible
 * default). The wizard applies these on open so first-time tests need
 * minimal typing.
 *
 * Order of precedence (handled by caller):
 *   1. Saved value from localStorage (highest)
 *   2. Smart default from this function
 *   3. Empty
 */
function _smartDefaultForPlaceholder(p, pickerData) {
  if (!p?.key) return "";
  const t = String(p.type || "string").toLowerCase();

  // proposal_template → service_intro is the more general default
  if (p.key === "proposal_template") return "service_intro";

  // tag-like keys → most-used tag
  const isTagKey =
    p.key === "tag" ||
    /(_|^)tag$/.test(p.key) ||
    p.key === "audience_tag" ||
    p.key === "cohort_tag" ||
    p.key === "conference_tag" ||
    p.key === "warm_tag" ||
    p.key === "follow_tag";
  if (isTagKey && t === "string" && pickerData?.tags?.length > 0) {
    return pickerData.tags[0]; // tags are sorted by count DESC server-side
  }

  // sequence_id → first available sequence
  if (/sequence_id$/.test(p.key) && pickerData?.sequences?.length > 0) {
    return pickerData.sequences[0].id;
  }

  // company / company_slug → first company
  if (
    (p.key === "company" || /^(watched_)?company(_slug)?$/.test(p.key)) &&
    t === "string" &&
    pickerData?.companies?.length > 0
  ) {
    const c = pickerData.companies[0];
    return c.slug || c.name?.toLowerCase().replace(/\s+/g, "-") || String(c.id);
  }

  // competitor handle → first tracked competitor
  if (
    (p.key === "competitor_handle" || p.key === "target_handle") &&
    pickerData?.competitors?.length > 0
  ) {
    const c = pickerData.competitors[0];
    return c.handle || c.twitter_handle || c.username || c.name || "";
  }

  // composition_id → first composition
  if (/composition_id$/.test(p.key) && pickerData?.compositions?.length > 0) {
    return pickerData.compositions[0].id;
  }

  // boolean → true (most templates assume opt-in defaults)
  if (t === "boolean") return true;

  // number → 1 as a safe placeholder
  if (t === "number") return 1;

  // No smart default for free-text fields (DM bodies, custom URLs, etc.)
  return "";
}

/**
 * Phase ZG — Pick a sensible default account_id per channel platform.
 * Returns the first non-locked-out account, or null if none exist.
 */
function _smartDefaultAccountFor(platform, accounts) {
  const pool = accounts[platform] || [];
  if (pool.length === 0) return null;
  // Prefer non-locked / healthy accounts
  const healthy = pool.find(
    (a) => !a.lockout_state || a.lockout_state === "ok" || a.status === "active",
  );
  return (healthy || pool[0]).id;
}

async function _openTemplateConfigWizard(templates) {
  // Pre-load every data source the wizard's pickers might need, in parallel.
  // Each fetch is best-effort — failures fall back to empty arrays + the
  // affected fields render as text inputs with "set up first" guidance.
  const [accountsResp, tagsResp, sequencesResp, companiesResp, competitorsResp, compositionsResp] =
    await Promise.all([
      api("accounts-all").catch(() => ({})),
      api("crm-contacts-tags").catch(() => ({ tags: [] })),
      api("email-sequences-list").catch(() => ({ sequences: [] })),
      api("crm-companies-list").catch(() => ({ companies: [] })),
      api("competitors-list").catch(() => ({ competitors: [] })),
      api("compositions-list").catch(() => ({ compositions: [] })),
    ]);
  const accounts = {
    telegram: accountsResp?.telegram || [],
    twitter: accountsResp?.twitter || [],
    linkedin: accountsResp?.linkedin || [],
    email: accountsResp?.smtp || [],
  };
  const pickerData = {
    tags: (tagsResp?.tags || []).map((t) => t.tag || t).filter(Boolean),
    sequences: sequencesResp?.sequences || [],
    companies: companiesResp?.companies || [],
    competitors: competitorsResp?.competitors || [],
    compositions: compositionsResp?.compositions || [],
  };
  // Hardcoded proposal-template choices (matches workflow/proposal-templates/ files)
  const PROPOSAL_TEMPLATES = ["service_intro", "investment_pitch"];

  return new Promise((resolve) => {
    const overlay = el("div", {
      style:
        "position: fixed; inset: 0; background: rgba(0,0,0,0.75); z-index: 10001; display: flex; align-items: center; justify-content: center;",
      onclick: (ev) => {
        if (ev.target === ev.currentTarget) {
          overlay.remove();
          resolve(null);
        }
      },
    });
    const modal = el("div", {
      style:
        "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 760px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column;",
    });
    const header = el(
      "div",
      {
        class: "row",
        style:
          "padding: 14px 18px; border-bottom: 1px solid var(--border); justify-content: space-between; align-items: center;",
      },
      [
        el("div", {}, [
          el("div", { style: "font-weight: 600; font-size: 16px;" }, "Configure templates to test"),
          el(
            "div",
            { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
            `Fill in what each template needs (tag, account, message body…) so the dry-run can target real contacts. Skip the ones you don't care about.`,
          ),
        ]),
        el(
          "button",
          {
            class: "btn btn-tiny btn-secondary",
            type: "button",
            onclick: () => {
              overlay.remove();
              resolve(null);
            },
          },
          "✕",
        ),
      ],
    );
    modal.appendChild(header);

    // Counter row (live-updates as user toggles skip/fills) + auto-fill helpers
    const counterRow = el("div", {
      class: "row",
      style:
        "padding: 8px 18px; font-size: 11px; background: var(--bg-0); border-bottom: 1px solid var(--border); align-items: center; gap: 12px;",
    });
    const counterEl = el("div", { class: "muted", style: "flex: 1;" }, "");
    counterRow.appendChild(counterEl);
    // Phase ZG — show whether ANY of these templates have saved values, with
    // a link to clear them so the user can start fresh.
    const hasAnySaved = templates.some((t) => _getSavedWizardConfig(t.id));
    if (hasAnySaved) {
      counterRow.appendChild(
        el(
          "span",
          {
            style: "color: var(--green); font-size: 10px;",
            title:
              "Some fields are pre-filled with the values you used last time you tested these templates.",
          },
          "pre-filled from your last test",
        ),
      );
      counterRow.appendChild(
        el(
          "a",
          {
            href: "javascript:void(0)",
            style:
              "color: var(--text-3); font-size: 10px; text-decoration: underline; cursor: pointer;",
            title:
              "Forget the saved values for the templates in this wizard. Smart defaults still apply.",
            onclick: () => {
              if (
                !confirm(
                  `Forget saved values for ${templates.length} template${templates.length === 1 ? "" : "s"}? Smart defaults will still be auto-picked.`,
                )
              )
                return;
              try {
                const mem = _loadWizardMemory();
                for (const t of templates) delete mem[t.id];
                localStorage.setItem(WIZARD_MEMORY_KEY, JSON.stringify(mem));
              } catch {}
              // Re-render the wizard so inputs reset
              overlay.remove();
              _openTemplateConfigWizard(templates).then((r) => resolve(r));
            },
          },
          "forget",
        ),
      );
    }
    modal.appendChild(counterRow);

    const body = el("div", {
      style:
        "padding: 12px 18px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 10px;",
    });
    modal.appendChild(body);

    // Per-template card state. Each card mutates its own entry.
    const cardStates = templates.map((tpl) => ({
      tpl,
      skip: false,
      params: {}, // key → string|number|bool|array
      channelAccounts: [], // index → { platform, account_id }
      _isValid: () => true,
    }));

    function refreshCounter() {
      const skipped = cardStates.filter((s) => s.skip).length;
      const configured = cardStates.length - skipped;
      const ready = cardStates.filter((s) => !s.skip && s._isValid()).length;
      counterEl.textContent = `${configured} to test · ${skipped} skipped · ${ready}/${configured} fully configured`;
      continueBtn.disabled = ready < configured; // need all non-skipped to be valid
      continueBtn.textContent =
        ready === configured && configured > 0
          ? `▶ Continue with ${configured} test${configured === 1 ? "" : "s"}`
          : `Fill required fields (${configured - ready} left)`;
      if (configured === 0) {
        continueBtn.disabled = false;
        continueBtn.textContent = `▶ Continue (0 templates)`;
      }
    }

    // Build one card per template. `requestNavigate(route)` is passed so
    // guidance action buttons (e.g., "Connect LinkedIn →") can leave the
    // wizard entirely. The caller (test-all modal) reads { navigateTo } and
    // closes its own overlay before navigating, so the user lands on the
    // setup page without any modals on top.
    const requestNavigate = (route) => {
      overlay.remove();
      resolve({ navigateTo: route });
    };
    for (const state of cardStates) {
      body.appendChild(
        _buildWizardTemplateCard(state, {
          accounts,
          pickerData,
          PROPOSAL_TEMPLATES,
          onChange: refreshCounter,
          requestNavigate,
        }),
      );
    }

    // Footer with Continue + Cancel
    const continueBtn = el("button", { class: "btn btn-primary", type: "button" }, "▶ Continue");
    const skipAllBtn = el(
      "button",
      { class: "btn btn-secondary btn-tiny", type: "button" },
      "Skip all",
    );
    const fillAllBtn = el(
      "button",
      { class: "btn btn-secondary btn-tiny", type: "button" },
      "Configure all",
    );
    const cancelBtn = el("button", { class: "btn btn-secondary", type: "button" }, "Cancel");

    skipAllBtn.onclick = () => {
      for (const s of cardStates) {
        s.skip = true;
        const cb = s._skipCheckbox;
        if (cb) cb.checked = true;
        if (s._formEl) s._formEl.style.display = "none";
      }
      refreshCounter();
    };
    fillAllBtn.onclick = () => {
      for (const s of cardStates) {
        s.skip = false;
        const cb = s._skipCheckbox;
        if (cb) cb.checked = false;
        if (s._formEl) s._formEl.style.display = "";
      }
      refreshCounter();
    };
    cancelBtn.onclick = () => {
      overlay.remove();
      resolve(null);
    };
    continueBtn.onclick = () => {
      const configs = {};
      for (const s of cardStates) {
        if (s.skip) {
          configs[s.tpl.id] = { skip: true };
        } else {
          configs[s.tpl.id] = {
            params: s.params,
            channelAccounts: s.channelAccounts,
          };
          // Phase ZG — remember these values so re-testing pre-fills instantly
          _saveWizardConfig(s.tpl.id, configs[s.tpl.id]);
        }
      }
      overlay.remove();
      resolve({ configs });
    };

    const footer = el(
      "div",
      {
        class: "row",
        style:
          "padding: 10px 18px; border-top: 1px solid var(--border); gap: 8px; align-items: center;",
      },
      [skipAllBtn, fillAllBtn, el("div", { style: "flex: 1;" }), cancelBtn, continueBtn],
    );
    modal.appendChild(footer);

    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    refreshCounter();
  });
}

/**
 * Build one card in the wizard. Mutates `state` as the user fills inputs.
 *
 * Card layout:
 *   header (icon + name + Skip toggle)
 *   ─────────────────────────────────
 *   requirements summary chips ("Needs: tag · LinkedIn account · DM body")
 *   collapsible runbook checklist (if template has one)
 *   per-placeholder typed inputs
 *   per-channel account pickers (or guidance card if account missing)
 *   tag-empty CRM guidance (if `tag` placeholder + 0 known tags)
 */
function _buildWizardTemplateCard(state, ctx) {
  const { tpl } = state;
  const { accounts, pickerData, PROPOSAL_TEMPLATES, onChange, requestNavigate } = ctx;
  // Backward-compat alias used in a few places below.
  const knownTags = pickerData?.tags || [];
  const placeholders = Array.isArray(tpl.param_placeholders) ? tpl.param_placeholders : [];
  // Channels needing an account-id pick. Two sub-cases:
  //   - accounts[platform] has at least one → show dropdown
  //   - accounts[platform] is empty → show "Connect X" guidance card
  const channels = Array.isArray(tpl.body?.channels) ? tpl.body.channels : [];
  const channelsNeedingAccount = channels
    .map((ch, i) => ({ ch, i }))
    .filter(({ ch }) => ch && ch.account_id == null);

  // Phase ZG — load saved values from previous test of this template
  const saved = _getSavedWizardConfig(tpl.id);

  // Initialize state.params with saved or smart defaults so the inputs
  // render pre-filled. User can override before clicking Continue.
  state.params = {};
  for (const p of placeholders) {
    const savedVal = saved?.params?.[p.key];
    if (savedVal != null && savedVal !== "") {
      state.params[p.key] = savedVal;
    } else {
      const smart = _smartDefaultForPlaceholder(p, pickerData);
      if (smart !== "" && smart != null) state.params[p.key] = smart;
    }
  }

  // Initialize state.channelAccounts with saved or smart-default account picks
  state.channelAccounts = channels.map((ch, idx) => {
    if (ch && ch.account_id != null) return { platform: ch.platform, account_id: ch.account_id };
    const savedPick = saved?.channelAccounts?.[idx];
    if (savedPick && savedPick.account_id != null) {
      // Verify the saved account still exists; otherwise fall through to smart default
      const stillExists = (accounts[ch?.platform] || []).some((a) => a.id === savedPick.account_id);
      if (stillExists) return { platform: ch.platform, account_id: savedPick.account_id };
    }
    const smartId = _smartDefaultAccountFor(ch?.platform, accounts);
    return { platform: ch?.platform || null, account_id: smartId };
  });

  const card = el("div", {
    style: "border: 1px solid var(--border); border-radius: 6px; background: var(--bg-0);",
  });

  // Header row: name + skip toggle
  const skipCheckbox = el("input", {
    type: "checkbox",
    id: `wiz-skip-${tpl.id}`,
    style: "margin-right: 6px;",
  });
  state._skipCheckbox = skipCheckbox;
  const skipLabel = el(
    "label",
    {
      for: `wiz-skip-${tpl.id}`,
      style:
        "font-size: 11px; color: var(--text-2); user-select: none; cursor: pointer; display: flex; align-items: center;",
    },
    [skipCheckbox, "Skip"],
  );
  const headerRow = el(
    "div",
    {
      class: "row",
      style:
        "padding: 10px 12px; align-items: center; gap: 10px; border-bottom: 1px solid var(--border);",
    },
    [
      el("div", { style: "font-size: 18px;" }, icon(icon.for(tpl.icon) || "clipboard")),
      el("div", { style: "flex: 1; min-width: 0;" }, [
        el(
          "div",
          {
            style:
              "font-size: 13px; font-weight: 500; color: var(--text-2); overflow: hidden; text-overflow: ellipsis;",
          },
          tpl.name || tpl.id,
        ),
        el(
          "div",
          {
            class: "muted",
            style:
              "font-size: 10px; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;",
          },
          tpl.description || "",
        ),
      ]),
      skipLabel,
    ],
  );
  card.appendChild(headerRow);

  // Form body — collapsible based on skip state
  const formEl = el("div", {
    style: "padding: 10px 12px; display: flex; flex-direction: column; gap: 10px;",
  });
  state._formEl = formEl;

  // ── Requirements summary chips ────────────────────────────────────
  // A scannable "this template needs: X, Y, Z" header so the user knows
  // up front what they're being asked for.
  const requirementChips = [];
  for (const p of placeholders) {
    if (!p.required) continue;
    requirementChips.push(p.label || p.key);
  }
  for (const { ch } of channelsNeedingAccount) {
    requirementChips.push(`${_platformLabel(ch.platform)} account`);
  }
  if (requirementChips.length > 0) {
    formEl.appendChild(
      el(
        "div",
        {
          style:
            "display: flex; flex-wrap: wrap; gap: 4px; align-items: center; font-size: 10px; color: var(--text-3);",
        },
        [
          el("span", { style: "color: var(--text-4);" }, "Needs:"),
          ...requirementChips.map((c) =>
            el(
              "span",
              {
                style:
                  "padding: 2px 6px; background: var(--bg-2); border: 1px solid #2a2a2a; border-radius: 10px; color: var(--text-2);",
              },
              c,
            ),
          ),
        ],
      ),
    );
  }

  // ── Runbook checklist (collapsible) ───────────────────────────────
  // Templates declare `runbook: [{ check, required }]` — pre-flight gotchas
  // the operator should know before running. Show as a collapsible section.
  const runbook = Array.isArray(tpl.runbook) ? tpl.runbook : [];
  if (runbook.length > 0) {
    const checklistWrap = el("details", {
      style: "background: #0d0d0d; border: 1px solid #1f1f1f; border-radius: 4px; padding: 0;",
    });
    const reqCount = runbook.filter((r) => r.required).length;
    const summary = el(
      "summary",
      {
        style:
          "padding: 6px 10px; cursor: pointer; font-size: 11px; color: var(--text-2); user-select: none; list-style: none;",
      },
      [
        el("span", { style: "color: var(--text-3); margin-right: 6px;" }, icon("clipboard")),
        el(
          "span",
          {},
          `Setup checklist (${runbook.length} item${runbook.length === 1 ? "" : "s"}${reqCount > 0 ? `, ${reqCount} required` : ""}) — click to expand`,
        ),
      ],
    );
    checklistWrap.appendChild(summary);
    const list = el(
      "ul",
      {
        style:
          "margin: 0; padding: 4px 12px 8px 26px; font-size: 11px; color: var(--text-2); line-height: 1.55;",
      },
      runbook.map((r) =>
        el(
          "li",
          {
            style: r.required ? "color: var(--text-2);" : "color: var(--text-3);",
          },
          [
            r.required
              ? el("span", { style: "color: var(--red); margin-right: 4px;" }, "•")
              : el("span", { style: "color: var(--text-4); margin-right: 4px;" }, "·"),
            r.check,
          ],
        ),
      ),
    );
    checklistWrap.appendChild(list);
    formEl.appendChild(checklistWrap);
  }

  if (placeholders.length === 0 && channelsNeedingAccount.length === 0) {
    formEl.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 11px;" },
        "No configuration needed — will run with the template's defaults.",
      ),
    );
  }

  // Inputs for placeholders — dispatcher picks the right control per
  // placeholder key/type so users can CLICK known values instead of typing.
  for (const p of placeholders) {
    formEl.appendChild(
      _renderPlaceholderInput(p, state, {
        tpl,
        pickerData,
        PROPOSAL_TEMPLATES,
        onChange,
        requestNavigate,
      }),
    );
  }

  // ── Channel-account pickers + missing-account guidance ──────────
  // For each channel needing an account, either show a dropdown of the
  // user's connected accounts, OR a guidance card with a "Connect X →"
  // button that closes the wizard and deep-links to the Accounts page.
  if (channelsNeedingAccount.length > 0) {
    formEl.appendChild(
      el(
        "div",
        {
          style:
            "font-size: 11px; color: var(--text-2); margin-top: 4px; padding-top: 8px; border-top: 1px dashed var(--border);",
        },
        "Pick which account to use for each channel:",
      ),
    );
  }
  // Track which channel indexes have NO account available — they auto-skip
  // (the user's only option is to skip this template or connect an account).
  const channelsMissingAccount = [];
  for (const { ch, i } of channelsNeedingAccount) {
    const platformAccounts = accounts[ch.platform] || [];
    const wrap = el("div", { style: "display: flex; flex-direction: column; gap: 4px;" });
    const label = el("div", { style: "font-size: 11px; color: var(--text-2);" }, [
      `${_platformLabel(ch.platform)} account`,
      el("span", { style: "color: var(--red); margin-left: 4px;" }, "*"),
    ]);
    wrap.appendChild(label);
    if (platformAccounts.length === 0) {
      // No accounts of this platform → guidance card with action button
      channelsMissingAccount.push({ platform: ch.platform, channelIndex: i });
      state.channelAccounts[i] = { platform: ch.platform, account_id: null, _missing: true };
      wrap.appendChild(
        _buildSetupGuidanceCard({
          title: `No ${_platformLabel(ch.platform)} account connected yet`,
          body: `This template needs a ${_platformLabel(ch.platform)} account to send/engage from. Connect one to test it — or skip this template and come back later.`,
          actions: [
            {
              label: `Connect ${_platformLabel(ch.platform)} →`,
              primary: true,
              onclick: () => requestNavigate("accounts"),
            },
            {
              label: "Skip this template",
              onclick: () => {
                state.skip = true;
                if (state._skipCheckbox) state._skipCheckbox.checked = true;
                if (state._formEl) state._formEl.style.display = "none";
                onChange();
              },
            },
          ],
        }),
      );
    } else {
      const select = el(
        "select",
        {
          style:
            "width: 100%; padding: 6px 8px; background: var(--bg-0); border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-1); font-size: 12px;",
        },
        [
          el("option", { value: "" }, `— pick a ${_platformLabel(ch.platform)} account —`),
          ...platformAccounts.map((a) =>
            el("option", { value: String(a.id) }, _accountLabel(ch.platform, a)),
          ),
        ],
      );
      // Phase ZG — pre-fill the dropdown with the smart-default account
      // chosen during card init (saved value or first available).
      const initialAccountId = state.channelAccounts[i]?.account_id;
      if (initialAccountId != null) select.value = String(initialAccountId);
      // Mark the label as pre-filled when applicable
      if (initialAccountId != null) {
        label.appendChild(
          el(
            "span",
            {
              style: "color: var(--green); margin-left: 6px; font-size: 9px;",
              title: "Auto-filled",
            },
            "pre-filled",
          ),
        );
      }
      select.onchange = () => {
        const id = select.value ? Number(select.value) : null;
        state.channelAccounts[i] = { platform: ch.platform, account_id: id };
        onChange();
      };
      wrap.appendChild(select);
    }
    formEl.appendChild(wrap);
  }

  // (The per-placeholder dispatcher now handles "no tags yet" inline as a
  // small hint under the tag input — no separate CRM-empty card needed.)

  // Validity check: required placeholders + channel accounts must be filled.
  // Channels with NO connected account are tracked in channelsMissingAccount
  // and don't gate validity — the user is expected to skip those templates.
  // To keep the Continue button honest, mark templates with missing accounts
  // as invalid unless the user explicitly skipped them.
  state._isValid = () => {
    if (channelsMissingAccount.length > 0) return false;
    for (const p of placeholders) {
      if (!p.required) continue;
      const v = state.params[p.key];
      if (v == null || v === "" || (typeof v === "string" && !v.trim())) return false;
    }
    for (const { i } of channelsNeedingAccount) {
      const pick = state.channelAccounts[i];
      if (!pick || pick.account_id == null) return false;
    }
    return true;
  };

  // Skip toggle behavior
  skipCheckbox.onchange = () => {
    state.skip = skipCheckbox.checked;
    formEl.style.display = state.skip ? "none" : "";
    onChange();
  };

  card.appendChild(formEl);
  return card;
}

/**
 * Inline guidance card with action buttons. Used by the wizard to surface
 * "you need to set up X first" with clear next steps + deep-links instead
 * of dead-end red error text.
 *
 * @param {object} opts
 * @param {string} opts.title
 * @param {string} opts.body
 * @param {"warning"|"info"} [opts.severity="warning"]
 * @param {Array<{label, onclick, primary?: boolean}>} opts.actions
 */
function _buildSetupGuidanceCard({ title, body, severity = "warning", actions = [] }) {
  const palette =
    severity === "info"
      ? { bg: "#1a2030", border: "#2a3a55", titleColor: "#aaccff" }
      : { bg: "#2a1a10", border: "#5a3a20", titleColor: "#faa080" };
  const card = el(
    "div",
    {
      style: `padding: 10px 12px; background: ${palette.bg}; border: 1px solid ${palette.border}; border-left: 3px solid ${palette.titleColor}; border-radius: 4px; display: flex; flex-direction: column; gap: 8px;`,
    },
    [
      el("div", { style: `font-size: 12px; font-weight: 500; color: ${palette.titleColor};` }, [
        severity === "info" ? "ℹ " : "⚠ ",
        title,
      ]),
      el("div", { style: "font-size: 11px; color: var(--text-2); line-height: 1.55;" }, body),
    ],
  );
  if (actions.length > 0) {
    const actionRow = el(
      "div",
      { class: "row", style: "gap: 6px; flex-wrap: wrap;" },
      actions.map((a) =>
        el(
          "button",
          {
            type: "button",
            class: a.primary ? "btn btn-primary btn-tiny" : "btn btn-secondary btn-tiny",
            onclick: a.onclick,
          },
          a.label,
        ),
      ),
    );
    card.appendChild(actionRow);
  }
  return card;
}

/**
 * Wizard input dispatcher — picks the most click-friendly control for a
 * given placeholder. Order of precedence (first match wins):
 *
 *   1. boolean type        → checkbox
 *   2. proposal_template   → select (hardcoded 2 choices)
 *   3. tag-like keys       → select of existing CRM tags
 *                              (with "+ use a different tag" toggle to text)
 *   4. *_sequence_id       → select of email sequences
 *   5. company / *_slug    → select of companies
 *   6. competitor_handle   → select of competitor handles
 *   7. *_composition_id    → select of user's compositions
 *   8. number type         → number input
 *   9. array type          → text input (comma-separated)
 *   0. fallback            → text input (e.g. DM bodies)
 *
 * For pickable types where data is empty (e.g. "no email sequences yet"),
 * the dispatcher falls through to a text input AND attaches a small
 * "[set up X →]" guidance line so the user knows what's missing.
 */
function _renderPlaceholderInput(p, state, ctx) {
  const { tpl, pickerData, PROPOSAL_TEMPLATES, onChange, requestNavigate } = ctx;
  const tagsList = pickerData?.tags || [];
  const sequencesList = pickerData?.sequences || [];
  const companiesList = pickerData?.companies || [];
  const competitorsList = pickerData?.competitors || [];
  const compositionsList = pickerData?.compositions || [];

  const wrap = el("div", { style: "display: flex; flex-direction: column; gap: 4px;" });
  const t = String(p.type || "string").toLowerCase();
  // Phase ZG — initial value comes from state.params (which the card init
  // pre-filled with saved values or smart defaults). When pre-filled, the
  // user sees the input already populated instead of starting blank.
  const initialValue = state.params[p.key];
  const hasInitial = initialValue != null && initialValue !== "";

  const labelEl = el(
    "div",
    { style: "font-size: 11px; color: var(--text-2);" },
    [
      p.label || p.key,
      p.required ? el("span", { style: "color: var(--red); margin-left: 4px;" }, "*") : null,
      p.type
        ? el("span", { class: "muted", style: "margin-left: 6px; font-size: 10px;" }, `(${p.type})`)
        : null,
      hasInitial
        ? el(
            "span",
            {
              style: "color: var(--green); margin-left: 6px; font-size: 9px;",
              title: "Auto-filled",
            },
            "pre-filled",
          )
        : null,
    ].filter(Boolean),
  );
  wrap.appendChild(labelEl);

  const inputStyle =
    "width: 100%; padding: 6px 8px; background: var(--bg-0); border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-1); font-size: 12px;";

  // Helper — record value + refresh wizard state
  const set = (v) => {
    state.params[p.key] = v;
    onChange();
  };

  // Helper — emit a "no X yet → set up" hint below an input when a
  // pickable data source is empty.
  const emptySourceHint = (label, route) =>
    el(
      "div",
      {
        style:
          "font-size: 10px; color: var(--text-3); margin-top: 2px; display: flex; align-items: center; gap: 6px;",
      },
      [
        `No ${label} yet —`,
        el(
          "a",
          {
            href: "javascript:void(0)",
            style: "color: #8aaafa; cursor: pointer; text-decoration: underline;",
            onclick: () => requestNavigate(route),
          },
          `set up ${label} →`,
        ),
      ],
    );

  // Small helper for selects: select the option matching `val` (or "" if none)
  const selectInitial = (selectEl, val) => {
    if (val == null) return;
    const s = String(val);
    for (const opt of selectEl.options) {
      if (opt.value === s) {
        selectEl.value = s;
        return;
      }
    }
  };

  // 1. boolean
  if (t === "boolean") {
    const input = el("input", { type: "checkbox" });
    if (hasInitial) input.checked = !!initialValue;
    input.onchange = () => set(!!input.checked);
    wrap.appendChild(input);
    return wrap;
  }

  // 2. proposal_template — small fixed list
  if (p.key === "proposal_template") {
    const select = el("select", { style: inputStyle }, [
      el("option", { value: "" }, "— pick a proposal template —"),
      ...PROPOSAL_TEMPLATES.map((id) => el("option", { value: id }, id)),
    ]);
    selectInitial(select, initialValue);
    select.onchange = () => set(select.value);
    wrap.appendChild(select);
    return wrap;
  }

  // 3. tag-like keys — select of existing tags + "use a different tag" toggle
  const isTagKey =
    p.key === "tag" ||
    /(_|^)tag$/.test(p.key) ||
    p.key === "audience_tag" ||
    p.key === "cohort_tag" ||
    p.key === "conference_tag" ||
    p.key === "warm_tag" ||
    p.key === "follow_tag";
  if (isTagKey && t === "string") {
    if (tagsList.length === 0) {
      // No tags exist yet — text input + CRM setup hint
      const input = el("input", {
        type: "text",
        placeholder: "e.g. founders-cohort-q2",
        style: inputStyle,
      });
      if (hasInitial) input.value = String(initialValue);
      input.oninput = () => set(input.value.trim());
      wrap.appendChild(input);
      wrap.appendChild(emptySourceHint("CRM tags", "crm"));
      return wrap;
    }
    // Tags exist → select with "+ use a different tag" option
    const select = el("select", { style: inputStyle }, [
      el("option", { value: "" }, `— pick a tag (${tagsList.length} available) —`),
      ...tagsList.slice(0, 100).map((tag) => el("option", { value: tag }, tag)),
      el("option", { value: "__custom__" }, "+ use a different tag (type below)"),
    ]);
    const customInput = el("input", {
      type: "text",
      placeholder: "Type tag name…",
      style: `${inputStyle} display: none; margin-top: 4px;`,
    });
    // Initial value: if it matches an existing tag, select it; else use custom mode
    if (hasInitial) {
      const initStr = String(initialValue);
      if (tagsList.includes(initStr)) {
        select.value = initStr;
      } else {
        select.value = "__custom__";
        customInput.style.display = "";
        customInput.value = initStr;
      }
    }
    select.onchange = () => {
      if (select.value === "__custom__") {
        customInput.style.display = "";
        customInput.focus();
        set(customInput.value.trim() || "");
      } else {
        customInput.style.display = "none";
        set(select.value);
      }
    };
    customInput.oninput = () => set(customInput.value.trim());
    wrap.appendChild(select);
    wrap.appendChild(customInput);
    return wrap;
  }

  // 4. email sequence id — select of user's sequences
  const isSequenceKey = /sequence_id$/.test(p.key);
  if (isSequenceKey) {
    if (sequencesList.length === 0) {
      const input = el("input", { type: "number", style: inputStyle });
      if (hasInitial) input.value = String(initialValue);
      input.oninput = () => set(input.value);
      wrap.appendChild(input);
      wrap.appendChild(emptySourceHint("email sequences", "email/sequences"));
      return wrap;
    }
    const select = el("select", { style: inputStyle }, [
      el("option", { value: "" }, `— pick a sequence (${sequencesList.length} available) —`),
      ...sequencesList.map((s) =>
        el(
          "option",
          { value: String(s.id) },
          `#${s.id} ${s.name || "(unnamed)"} ${s.status ? `[${s.status}]` : ""}`,
        ),
      ),
    ]);
    selectInitial(select, initialValue);
    select.onchange = () => set(select.value ? Number(select.value) : "");
    wrap.appendChild(select);
    return wrap;
  }

  // 5. company slug / company key — select of CRM companies
  const isCompanyKey = p.key === "company" || /^(watched_)?company(_slug)?$/.test(p.key);
  if (isCompanyKey && t === "string") {
    if (companiesList.length === 0) {
      const input = el("input", {
        type: "text",
        placeholder: p.label || "e.g. stripe",
        style: inputStyle,
      });
      if (hasInitial) input.value = String(initialValue);
      input.oninput = () => set(input.value.trim());
      wrap.appendChild(input);
      wrap.appendChild(emptySourceHint("companies", "crm/companies"));
      return wrap;
    }
    const slugFor = (c) => c.slug || c.name?.toLowerCase().replace(/\s+/g, "-") || String(c.id);
    const select = el("select", { style: inputStyle }, [
      el("option", { value: "" }, `— pick a company (${companiesList.length} tracked) —`),
      ...companiesList
        .slice(0, 200)
        .map((c) =>
          el(
            "option",
            { value: slugFor(c) },
            `${c.name || slugFor(c)}${c.website ? ` (${c.website})` : ""}`,
          ),
        ),
      el("option", { value: "__custom__" }, "+ use a different slug (type below)"),
    ]);
    const customInput = el("input", {
      type: "text",
      placeholder: "Type slug…",
      style: `${inputStyle} display: none; margin-top: 4px;`,
    });
    if (hasInitial) {
      const initStr = String(initialValue);
      const known = companiesList.some((c) => slugFor(c) === initStr);
      if (known) select.value = initStr;
      else {
        select.value = "__custom__";
        customInput.style.display = "";
        customInput.value = initStr;
      }
    }
    select.onchange = () => {
      if (select.value === "__custom__") {
        customInput.style.display = "";
        customInput.focus();
        set(customInput.value.trim() || "");
      } else {
        customInput.style.display = "none";
        set(select.value);
      }
    };
    customInput.oninput = () => set(customInput.value.trim());
    wrap.appendChild(select);
    wrap.appendChild(customInput);
    return wrap;
  }

  // 6. competitor handle — select of tracked competitors
  if (p.key === "competitor_handle" || p.key === "target_handle") {
    if (competitorsList.length === 0) {
      const input = el("input", {
        type: "text",
        placeholder: p.label || "e.g. competitor",
        style: inputStyle,
      });
      if (hasInitial) input.value = String(initialValue);
      input.oninput = () => set(input.value.trim());
      wrap.appendChild(input);
      wrap.appendChild(emptySourceHint("competitors", "competitors"));
      return wrap;
    }
    const handleFor = (c) => c.handle || c.twitter_handle || c.username || c.name || "";
    const select = el("select", { style: inputStyle }, [
      el("option", { value: "" }, `— pick a competitor (${competitorsList.length} tracked) —`),
      ...competitorsList.map((c) => {
        const handle = handleFor(c);
        const display =
          c.name && handle && c.name !== handle
            ? `${c.name} (@${handle})`
            : handle
              ? `@${handle}`
              : c.name || "?";
        return el("option", { value: handle }, display);
      }),
      el("option", { value: "__custom__" }, "+ use a different handle (type below)"),
    ]);
    const customInput = el("input", {
      type: "text",
      placeholder: "Type @handle…",
      style: `${inputStyle} display: none; margin-top: 4px;`,
    });
    if (hasInitial) {
      const initStr = String(initialValue);
      const known = competitorsList.some((c) => handleFor(c) === initStr);
      if (known) select.value = initStr;
      else {
        select.value = "__custom__";
        customInput.style.display = "";
        customInput.value = initStr;
      }
    }
    select.onchange = () => {
      if (select.value === "__custom__") {
        customInput.style.display = "";
        customInput.focus();
        set(customInput.value.trim() || "");
      } else {
        customInput.style.display = "none";
        set(select.value);
      }
    };
    customInput.oninput = () => set(customInput.value.trim());
    wrap.appendChild(select);
    wrap.appendChild(customInput);
    return wrap;
  }

  // 7. composition id — select of user's compositions
  if (/composition_id$/.test(p.key)) {
    if (compositionsList.length === 0) {
      const input = el("input", { type: "number", style: inputStyle });
      if (hasInitial) input.value = String(initialValue);
      input.oninput = () => set(input.value);
      wrap.appendChild(input);
      wrap.appendChild(emptySourceHint("compositions", "workflows"));
      return wrap;
    }
    const select = el("select", { style: inputStyle }, [
      el("option", { value: "" }, `— pick a composition (${compositionsList.length} available) —`),
      ...compositionsList.map((c) =>
        el(
          "option",
          { value: String(c.id) },
          `#${c.id} ${c.name || "(unnamed)"} ${c.status ? `[${c.status}]` : ""}`,
        ),
      ),
    ]);
    selectInitial(select, initialValue);
    select.onchange = () => set(select.value ? Number(select.value) : "");
    wrap.appendChild(select);
    return wrap;
  }

  // 8. number type
  if (t === "number") {
    const input = el("input", { type: "number", style: inputStyle });
    if (hasInitial) input.value = String(initialValue);
    input.oninput = () => set(input.value);
    wrap.appendChild(input);
    return wrap;
  }

  // 9. array type — comma-separated text
  if (t === "array") {
    const input = el("input", {
      type: "text",
      placeholder: "comma-separated values",
      style: inputStyle,
    });
    if (hasInitial)
      input.value = Array.isArray(initialValue) ? initialValue.join(", ") : String(initialValue);
    input.oninput = () => set(input.value);
    wrap.appendChild(input);
    return wrap;
  }

  // 0. fallback — text input (custom message bodies, URLs, etc.)
  const input = el("input", { type: "text", placeholder: p.label || "", style: inputStyle });
  if (hasInitial) input.value = String(initialValue);
  input.oninput = () => set(input.value.trim());
  wrap.appendChild(input);
  return wrap;
}

function _platformLabel(p) {
  return (
    { telegram: "Telegram", twitter: "Twitter/X", linkedin: "LinkedIn", email: "Email" }[p] || p
  );
}

function _accountLabel(platform, a) {
  if (platform === "telegram")
    return `${a.label || a.phone || "#" + a.id} ${a.status ? `(${a.status})` : ""}`;
  if (platform === "twitter")
    return `@${a.account_identifier || "acct" + a.id} ${a.status ? `(${a.status})` : ""}`;
  if (platform === "linkedin")
    return `${a.identifier || "session#" + a.id} ${a.status ? `(${a.status})` : ""}`;
  if (platform === "email") return `${a.email || "smtp#" + a.id}`;
  return `#${a.id}`;
}

/**
 * Phase ZF — Single-template LIVE test entry point.
 *
 * Opens the configuration wizard pre-loaded with just ONE template, then
 * (after the user confirms a "this will send a real message" prompt) fires
 * the test-live-one endpoint:
 *   - Real execution (NOT dry-run) — actual DMs/connects/posts fire
 *   - maxContacts capped to 1 by default for safety
 *   - priorityOverride: true — bypasses daily + share-weighted caps
 *   - Browser opens visibly (default for the main automation path)
 *
 * After firing, shows a small status panel polling composition-runs for
 * play-by-play status until the run completes.
 */
/**
 * Phase ZG — Skip the wizard entirely when the template has saved values.
 * Pulls the last-used config from localStorage and fires the test directly.
 */
async function _quickRetestFromTemplate(tpl) {
  const saved = _getSavedWizardConfig(tpl.id);
  if (!saved) {
    toast("No saved values for this template — opening the wizard", "info");
    return _openTestLiveOneFromTemplate(tpl);
  }
  await _fireTestLiveOne(tpl, {
    params: saved.params || {},
    channelAccounts: saved.channelAccounts || [],
  });
}

async function _openTestLiveOneFromTemplate(tpl) {
  // Re-use the same wizard but with only one template in the list.
  const wizardResult = await _openTemplateConfigWizard([tpl]);
  if (!wizardResult) return;
  if (wizardResult.navigateTo) {
    navigate(wizardResult.navigateTo);
    return;
  }

  const cfg = wizardResult.configs?.[tpl.id];
  if (!cfg || cfg.skip) {
    toast("Template was skipped — nothing to run", "warn");
    return;
  }
  await _fireTestLiveOne(tpl, cfg);
}

/**
 * Phase ZG — Shared firing logic for both the full-wizard path and the
 * quick-retest path. Confirms with the user, opens the status panel, fires
 * the backend, and surfaces the result.
 */
async function _fireTestLiveOne(tpl, cfg) {
  // Real-execution confirmation. Lists which channels will fire so the
  // user knows exactly what's about to happen.
  const channelSummary =
    (tpl.body?.channels || [])
      .map((ch, i) => {
        const pick = cfg.channelAccounts?.[i];
        const accountTxt = pick?.account_id ? ` (account #${pick.account_id})` : "";
        return `${_platformLabel(ch.platform)}${accountTxt}`;
      })
      .filter(Boolean)
      .join(", ") || "(no channels)";
  const confirmText = `LIVE TEST — REAL execution\n\nTemplate: ${tpl.name}\nChannels: ${channelSummary}\nContacts: 1 (capped for safety)\n\nThis will send a REAL DM / connect / post to ONE real contact from your audience. The browser will open visibly.\n\nProceed?`;
  if (!confirm(confirmText)) return;

  // Build status panel
  const panel = _openLiveTestStatusPanel(tpl);

  // Fire it
  try {
    panel.log(`▶ Sending test-live-one request to backend…`);
    const r = await api("composer-test-live-one", {
      body: {
        template_id: tpl.id,
        params: cfg.params || {},
        channel_accounts: cfg.channelAccounts || [],
        max_contacts: 1,
      },
    });
    if (r?.error) {
      panel.log(`Backend error: ${r.error}`);
      panel.setStatus("fail");
      return;
    }
    panel.log(`Backend accepted. Run kicked off.`);
    panel.log(
      `  audience_size=${r?.audienceSize ?? "?"} · plays_executed=${r?.playsExecuted ?? 0} · plays_succeeded=${r?.playsSucceeded ?? 0} · plays_failed=${r?.playsFailed ?? 0}`,
    );
    if (Array.isArray(r?.preview) && r.preview.length > 0) {
      panel.log(`  Preview (${r.preview.length} lines):`);
      for (const line of r.preview.slice(0, 30)) panel.log(`    ${line}`);
    }
    if (r?.ok === false) {
      panel.log(`${r.error || "run returned ok:false"}`);
      panel.setStatus("fail");
    } else if (r?.status === "skipped") {
      panel.log(`↷ Skipped: ${r.reason || "(no reason given)"}`);
      panel.setStatus("skip");
    } else if ((r?.playsFailed ?? 0) > 0 && (r?.playsSucceeded ?? 0) === 0) {
      panel.log(`All plays failed`);
      panel.setStatus("fail");
    } else {
      panel.log(`Run finished. Check the browser window for what actually happened.`);
      panel.setStatus("pass");
    }
  } catch (err) {
    panel.log(`Request failed: ${err?.message || err}`);
    panel.setStatus("fail");
  }
}

/**
 * Phase ZF — Live test status panel. A small fixed-position log + status
 * indicator that streams what's happening backend-side while the visible
 * browser does its thing in another window. User can close it any time.
 */
function _openLiveTestStatusPanel(tpl) {
  const panel = el("div", {
    style:
      "position: fixed; bottom: 16px; right: 16px; width: 480px; max-width: 95vw; max-height: 60vh; background: var(--bg-0); border: 1px solid var(--border-strong); border-radius: 8px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); z-index: 10002; display: flex; flex-direction: column;",
  });
  const statusEl = el("span", { style: "font-size: 14px; margin-right: 6px;" }, icon("launch"));
  const titleEl = el(
    "div",
    {
      style:
        "font-weight: 600; font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;",
    },
    `Test live: ${tpl.name}`,
  );
  const closeBtn = el(
    "button",
    { class: "btn btn-tiny btn-secondary", type: "button", onclick: () => panel.remove() },
    "✕",
  );
  const header = el(
    "div",
    {
      class: "row",
      style:
        "padding: 8px 12px; border-bottom: 1px solid var(--border); align-items: center; gap: 6px;",
    },
    [statusEl, titleEl, closeBtn],
  );
  panel.appendChild(header);
  const logBox = el("pre", {
    style:
      "margin: 0; padding: 10px 12px; overflow-y: auto; flex: 1; font-family: ui-monospace, Menlo, monospace; font-size: 10px; color: var(--text-2); line-height: 1.55; white-space: pre-wrap;",
  });
  panel.appendChild(logBox);
  document.body.appendChild(panel);

  return {
    log(line) {
      const ts = new Date().toISOString().split("T")[1].replace("Z", "");
      logBox.textContent += `[${ts}] ${line}\n`;
      logBox.scrollTop = logBox.scrollHeight;
    },
    setStatus(s) {
      const cfg = {
        pass: { icon: "✅", color: "#5ada5a" },
        fail: { icon: "❌", color: "#fa8a8a" },
        skip: { icon: "↷", color: "#daaa5a" },
        running: { icon: "🏃", color: "#5a8aff" },
      }[s] || { icon: "?", color: "#888" };
      statusEl.textContent = cfg.icon;
      statusEl.style.color = cfg.color;
    },
  };
}

async function _openTestAllModal() {
  const overlay = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    onclick: (ev) => {
      if (ev.target === ev.currentTarget) {
        if (
          _testAllPollTimer &&
          !confirm("Test still running. Close anyway? (the run continues server-side)")
        )
          return;
        _stopTestAllPoll();
        overlay.remove();
      }
    },
  });
  const modal = el("div", {
    style:
      "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 880px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column;",
  });
  const header = el(
    "div",
    {
      class: "row",
      style:
        "padding: 14px 18px; border-bottom: 1px solid var(--border); justify-content: space-between; align-items: center;",
    },
    [
      el("div", {}, [
        el("div", { style: "font-weight: 600; font-size: 16px;" }, "▶ Test all my workflows"),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
          "Each composition runs in dry-run mode (no real DMs / connects / posts). Watch each play execute live.",
        ),
      ]),
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          onclick: () => {
            _stopTestAllPoll();
            overlay.remove();
          },
        },
        "✕",
      ),
    ],
  );
  modal.appendChild(header);

  // Pre-load composition list + template catalog so user sees what's about
  // to run before clicking. Templates that the user has already applied
  // (have a saved composition with source_template_id) are filtered out
  // server-side too — we filter the preview list here for accurate counts.
  const [listResp, tplResp] = await Promise.all([
    api("compositions-list").catch((e) => ({ error: e?.message })),
    api("composition-templates").catch(() => ({ templates: [] })),
  ]);
  if (listResp?.error) {
    modal.appendChild(
      el(
        "div",
        { style: "padding: 24px; color: var(--red);" },
        `Failed to load compositions: ${listResp.error}`,
      ),
    );
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    return;
  }
  const compositions = listResp?.compositions || [];
  // Only consider seeded (pre-made) templates — user-saved ones aren't "pre-made".
  const allTemplates = (tplResp?.templates || []).filter((t) => t.tier === "seeded");
  const appliedTplIds = new Set(compositions.map((c) => c.source_template_id).filter(Boolean));
  const unappliedTemplates = allTemplates.filter((t) => !appliedTplIds.has(t.id));

  if (compositions.length === 0 && unappliedTemplates.length === 0) {
    modal.appendChild(
      el(
        "div",
        {
          style: "padding: 24px; text-align: center; color: var(--text-2);",
        },
        [
          el("div", { style: "font-size: 32px; margin-bottom: 12px;" }, icon("settings")),
          el("div", { style: "font-weight: 500; margin-bottom: 6px;" }, "No workflows to test"),
          el(
            "div",
            { class: "muted", style: "font-size: 11px;" },
            "Apply a template from the Build page first, then come back here.",
          ),
        ],
      ),
    );
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
    return;
  }

  // Pre-run controls + summary
  const controlsRow = el("div", {
    class: "row",
    style:
      "padding: 10px 18px; gap: 12px; align-items: center; background: var(--bg-0); border-bottom: 1px solid var(--border); flex-wrap: wrap;",
  });
  const summaryEl = el(
    "div",
    { class: "muted", style: "flex: 1; min-width: 200px; font-size: 11px;" },
    `${compositions.length} workflow(s) ready · all will be dry-run sequentially`,
  );
  // Phase ZC: visible-mode checkbox. Default ON — opens each canvas in turn.
  // Off = ZB headless mode (faster, status-card only).
  const visibleCheckbox = el("input", {
    type: "checkbox",
    id: "test-all-visible",
    checked: true,
    style: "margin-right: 4px;",
  });
  const visibleLabel = el(
    "label",
    {
      for: "test-all-visible",
      style:
        "font-size: 11px; color: var(--text-2); user-select: none; cursor: pointer; display: flex; align-items: center;",
      title:
        "Visible mode: opens each composition's canvas one by one and runs Test on 3 with the live overlay. Headless: just status cards in this modal.",
    },
    [visibleCheckbox, "Visible mode (open each canvas)"],
  );
  // "Include templates" — adds all unactivated pre-made templates to the run.
  // Forces headless mode (templates don't have a canvas to open).
  const tplCheckbox = el("input", {
    type: "checkbox",
    id: "test-all-tpls",
    checked: false,
    style: "margin-right: 4px;",
    disabled: unappliedTemplates.length === 0,
  });
  const tplLabel = el(
    "label",
    {
      for: "test-all-tpls",
      style: `font-size: 11px; color: ${unappliedTemplates.length === 0 ? "#555" : "#ccc"}; user-select: none; cursor: ${unappliedTemplates.length === 0 ? "default" : "pointer"}; display: flex; align-items: center;`,
      title:
        unappliedTemplates.length === 0
          ? "All templates already applied — nothing extra to test"
          : "Include unactivated pre-made templates. A wizard will ask you to fill in tags, accounts, and message bodies so the dry-run hits real contacts. Forces headless mode (templates have no canvas).",
    },
    [tplCheckbox, `Also test unactivated templates (${unappliedTemplates.length})`],
  );
  const runBtn = el(
    "button",
    {
      class: "btn btn-primary",
      type: "button",
    },
    `▶ Run all ${compositions.length}`,
  );
  const cancelBtn = el(
    "button",
    {
      class: "btn btn-secondary btn-tiny",
      type: "button",
      style: "display: none;",
    },
    "Cancel run",
  );
  controlsRow.appendChild(summaryEl);
  controlsRow.appendChild(visibleLabel);
  controlsRow.appendChild(tplLabel);
  controlsRow.appendChild(cancelBtn);
  controlsRow.appendChild(runBtn);
  modal.appendChild(controlsRow);

  const body = el("div", {
    style:
      "padding: 12px 18px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 8px;",
  });
  modal.appendChild(body);

  // Pre-render one card per composition (pending state)
  const cards = new Map(); // compositionId/template_id → DOM node
  for (const c of compositions) {
    const card = _buildTestAllCard(c);
    cards.set(c.id, card);
    body.appendChild(card);
  }
  // Pre-render template cards (hidden until tplCheckbox toggled on)
  const tplCards = new Map(); // template_id → DOM node
  for (const t of unappliedTemplates) {
    const card = _buildTestAllCard({
      id: `tpl:${t.id}`,
      name: `${t.name || t.id}`,
      status: "template",
    });
    card.style.display = "none";
    tplCards.set(t.id, card);
    body.appendChild(card);
  }

  // Wire checkbox interactions:
  //   - tplCheckbox on → show template cards, force headless mode, update count
  //   - tplCheckbox off → hide template cards, restore count
  //   - visibleCheckbox can't be on while tplCheckbox is on (templates have no canvas)
  function refreshControls() {
    const tplsOn = tplCheckbox.checked;
    if (tplsOn) {
      visibleCheckbox.checked = false;
      visibleCheckbox.disabled = true;
      visibleLabel.title =
        'Disabled — templates have no canvas. Uncheck "Also test unactivated templates" to re-enable.';
    } else {
      visibleCheckbox.disabled = false;
      visibleLabel.title =
        "Visible mode: opens each composition's canvas one by one and runs Test on 3 with the live overlay. Headless: just status cards in this modal.";
    }
    for (const card of tplCards.values()) {
      card.style.display = tplsOn ? "" : "none";
    }
    const total = compositions.length + (tplsOn ? unappliedTemplates.length : 0);
    runBtn.textContent = `▶ Run all ${total}`;
    summaryEl.textContent = tplsOn
      ? `${compositions.length} workflow(s) + ${unappliedTemplates.length} template(s) — all will be dry-run sequentially`
      : `${compositions.length} workflow(s) ready · all will be dry-run sequentially`;
  }
  tplCheckbox.onchange = refreshControls;

  overlay.appendChild(modal);
  document.body.appendChild(overlay);

  // Run button handler
  let currentRunId = null;
  runBtn.onclick = async () => {
    // Phase ZC: visible mode → close modal, kick off canvas-by-canvas session.
    // (Disabled when tplCheckbox is checked.)
    if (visibleCheckbox.checked && !tplCheckbox.checked) {
      runBtn.disabled = true;
      runBtn.textContent = "Starting…";
      overlay.remove();
      startTestAllCanvasSession(compositions);
      return;
    }

    // Phase ZE2: when "include templates" is on, open the configuration
    // wizard first so the user fills tags / accounts / DM bodies. The wizard
    // returns a per-template config object the backend uses for real
    // substitution (not the safe-default placeholders).
    let templateConfigs = null;
    if (tplCheckbox.checked && unappliedTemplates.length > 0) {
      const wizardResult = await _openTemplateConfigWizard(unappliedTemplates);
      if (!wizardResult) return; // user cancelled — keep modal open
      // ZE3: wizard's guidance buttons can request a navigation away
      // (e.g., "Connect LinkedIn →"). Close the test-all modal first so
      // the destination page isn't covered by our overlay.
      if (wizardResult.navigateTo) {
        _stopTestAllPoll();
        overlay.remove();
        navigate(wizardResult.navigateTo);
        return;
      }
      templateConfigs = wizardResult.configs;
      // Hide cards for templates the user marked as skip
      for (const [tplId, cfg] of Object.entries(templateConfigs)) {
        const card = tplCards.get(tplId);
        if (card && cfg.skip) card.style.display = "none";
        else if (card) card.style.display = "";
      }
    }

    runBtn.disabled = true;
    runBtn.textContent = "Starting…";
    // Headless mode (Phase ZB / ZE): backend-only, status cards in this modal
    try {
      const r = await api("composer-test-all-start", {
        body: {
          include_drafts: true,
          include_templates: tplCheckbox.checked,
          template_configs: templateConfigs,
        },
      });
      currentRunId = r.run_id;
      runBtn.style.display = "none";
      cancelBtn.style.display = "";
      summaryEl.textContent = `Run ID: ${currentRunId} · polling every second`;
      _startTestAllPoll(currentRunId, cards, tplCards, summaryEl, runBtn, cancelBtn);
    } catch (err) {
      runBtn.disabled = false;
      runBtn.textContent = `▶ Run all ${compositions.length}`;
      summaryEl.textContent = `Failed to start: ${err?.message || err}`;
    }
  };
  cancelBtn.onclick = async () => {
    if (!currentRunId) return;
    try {
      await api("composer-test-all-cancel", { runId: currentRunId });
    } catch {}
    cancelBtn.disabled = true;
    cancelBtn.textContent = "Cancelling…";
  };
}

function _buildTestAllCard(comp) {
  const card = el("div", {
    style:
      "border: 1px solid var(--border); border-left: 3px solid #444; border-radius: 6px; background: var(--bg-0); transition: border-left-color 200ms;",
  });
  card._compId = comp.id;
  // Header row — status + name + summary
  const header = el("div", {
    class: "row",
    style: "padding: 10px 12px; cursor: pointer; align-items: center; gap: 10px;",
  });
  const statusEl = el(
    "span",
    {
      class: "tac-status",
      style: "font-size: 16px; width: 22px; text-align: center;",
    },
    "⏳",
  );
  const titleEl = el("div", { style: "flex: 1; min-width: 0;" }, [
    el(
      "div",
      {
        class: "tac-name",
        style:
          "font-size: 13px; font-weight: 500; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;",
        title: comp.name,
      },
      comp.name,
    ),
    el(
      "div",
      {
        class: "tac-summary muted",
        style: "font-size: 10px; margin-top: 2px;",
      },
      `Status: ${comp.status} · pending dry-run`,
    ),
  ]);
  const durationEl = el(
    "div",
    { class: "tac-duration muted", style: "font-size: 10px; min-width: 50px; text-align: right;" },
    "",
  );
  const expandHint = el(
    "span",
    { class: "muted", style: "font-size: 10px; margin-left: 4px;" },
    "▸",
  );
  header.appendChild(statusEl);
  header.appendChild(titleEl);
  header.appendChild(durationEl);
  header.appendChild(expandHint);
  card.appendChild(header);

  // Collapsible log panel
  const logPanel = el(
    "div",
    {
      class: "tac-log",
      style:
        "display: none; padding: 8px 12px 10px; background: var(--bg-0); border-top: 1px solid var(--border); font-family: ui-monospace, Menlo, monospace; font-size: 10px; color: var(--text-2); line-height: 1.6; max-height: 240px; overflow-y: auto; white-space: pre-wrap;",
    },
    "(no log yet — run hasn't started)",
  );
  card.appendChild(logPanel);

  // Click header to expand/collapse
  let expanded = false;
  header.onclick = () => {
    expanded = !expanded;
    logPanel.style.display = expanded ? "block" : "none";
    expandHint.textContent = expanded ? "▾" : "▸";
  };
  return card;
}

function _updateTestAllCard(card, compState) {
  const statusEl = card.querySelector(".tac-status");
  const summaryEl = card.querySelector(".tac-summary");
  const durationEl = card.querySelector(".tac-duration");
  const logEl = card.querySelector(".tac-log");
  const cfg = {
    pending: { icon: "⏳", color: "#666", border: "#444" },
    running: { icon: "🏃", color: "#5a8aff", border: "#5a8aff" },
    pass: { icon: "✅", color: "#5ada5a", border: "#5ada5a" },
    fail: { icon: "❌", color: "#fa8a8a", border: "#fa8a8a" },
    skipped: { icon: "↷", color: "#daaa5a", border: "#daaa5a" },
  }[compState.test_status] || { icon: "?", color: "#666", border: "#444" };
  statusEl.textContent = cfg.icon;
  statusEl.style.color = cfg.color;
  card.style.borderLeftColor = cfg.border;
  if (compState.summary) summaryEl.textContent = compState.summary;
  else if (compState.test_status === "running") summaryEl.textContent = "Running plays…";
  if (compState.durationMs != null) {
    durationEl.textContent =
      compState.durationMs < 1000
        ? `${compState.durationMs}ms`
        : `${(compState.durationMs / 1000).toFixed(1)}s`;
  }
  // Update log content (replace fully each tick — bounded to 500 lines server-side)
  if (Array.isArray(compState.log) && compState.log.length > 0) {
    logEl.textContent = compState.log.join("\n");
    logEl.scrollTop = logEl.scrollHeight;
  }
}

function _startTestAllPoll(runId, cards, tplCards, summaryEl, runBtn, cancelBtn) {
  _stopTestAllPoll();
  _testAllPollTimer = setInterval(async () => {
    let state;
    try {
      state = await api("composer-test-all-status", { runId });
    } catch {
      return;
    }
    if (!state) return;
    for (const comp of state.compositions || []) {
      // Saved comp → cards keyed by numeric id. Template → tplCards keyed by template_id.
      const card =
        comp.kind === "template" ? tplCards && tplCards.get(comp.template_id) : cards.get(comp.id);
      if (card) _updateTestAllCard(card, comp);
    }
    const totalCount = (state.compositions || []).length;
    const agg = state.aggregate || {};
    if (state.completed) {
      _stopTestAllPoll();
      const allPassed = (agg.fail || 0) === 0;
      summaryEl.innerHTML = "";
      summaryEl.appendChild(
        el(
          "span",
          {
            style: `font-weight:600;color:${allPassed ? "#5ada5a" : "#fa8a8a"};`,
          },
          allPassed ? "All workflows passed" : `${agg.fail || 0} workflow(s) failed`,
        ),
      );
      summaryEl.appendChild(
        el(
          "span",
          { class: "muted", style: "margin-left:10px;font-size:11px;" },
          `${agg.pass || 0} pass · ${agg.skipped || 0} skipped · ${agg.fail || 0} fail`,
        ),
      );
      cancelBtn.style.display = "none";
      runBtn.style.display = "";
      runBtn.disabled = false;
      runBtn.textContent = `▶ Run again`;
    } else {
      const idx = state.current_index >= 0 ? state.current_index + 1 : 0;
      summaryEl.textContent = `Running ${idx} of ${totalCount} · ${agg.pass || 0} pass · ${agg.fail || 0} fail · ${agg.skipped || 0} skipped`;
    }
  }, 1000);
}

/**
 * Phase ZA — Composer health check modal.
 *
 * Click "Run health check" on Active workflows page → this opens.
 * Calls POST /api/composer/health-check to kick off background work,
 * then polls GET /api/composer/health-check/:runId at 1Hz to update each
 * gate row live. Failures expand inline to show details.
 */
async function _openHealthCheckModal() {
  // Start-once discipline (matches _startTestAllPoll → _stopTestAllPoll): clear
  // any prior poll before opening. No-op on the normal single-open path.
  _stopHealthCheckPoll();
  const overlay = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    onclick: (ev) => {
      if (ev.target === ev.currentTarget && _hcPollTimer) {
        if (!confirm("Health check still running. Close anyway?")) return;
        _stopHealthCheckPoll();
        overlay.remove();
      } else if (ev.target === ev.currentTarget) {
        overlay.remove();
      }
    },
  });
  const modal = el("div", {
    style:
      "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 720px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;",
  });

  const header = el(
    "div",
    {
      class: "row",
      style:
        "padding: 14px 18px; border-bottom: 1px solid var(--border); justify-content: space-between; align-items: center;",
    },
    [
      el("div", {}, [
        el("div", { style: "font-weight: 600; font-size: 16px;" }, "Composer health check"),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
          "Runs every quality gate sequentially — schema, dry-runs, lint, audit, smoke, e2e. Live status per gate.",
        ),
      ]),
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          onclick: () => {
            _stopHealthCheckPoll();
            overlay.remove();
          },
        },
        "✕",
      ),
    ],
  );
  modal.appendChild(header);

  const body = el("div", { style: "padding: 14px 18px; overflow-y: auto; flex: 1;" });
  modal.appendChild(body);

  const summary = el(
    "div",
    {
      style:
        "padding: 10px 18px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-2);",
    },
    "Starting…",
  );
  modal.appendChild(summary);

  overlay.appendChild(modal);
  document.body.appendChild(overlay);

  // Build a row per gate (we know the 6 ahead of time so initial render
  // can show pending rows immediately). Real label/desc come from the
  // first poll response and overwrite these placeholders.
  const initialGates = [
    {
      id: "schema",
      label: "Schema validation",
      description: "All composition templates have valid JSON shape",
    },
    {
      id: "dryrun",
      label: "Template dry-runs",
      description: "Each template's runComposition() succeeds",
    },
    {
      id: "lint",
      label: "Ship-risk lint",
      description: "No anti-spam patterns / missing recency gates",
    },
    {
      id: "audit",
      label: "Discovery audit",
      description: "Catalog primitives are demoed by ≥1 template",
    },
    { id: "smoke", label: "Per-primitive smoke", description: "Every play/resolver/event healthy" },
    { id: "e2e", label: "E2E test suite", description: "Full integration assertions" },
  ];
  const gateNodes = new Map();
  for (const g of initialGates) {
    const node = _buildGateRow(g);
    gateNodes.set(g.id, node);
    body.appendChild(node);
  }

  // Kick off
  let runId;
  try {
    const r = await api("composer-health-check-start", {});
    runId = r.run_id;
    summary.textContent = `Run ID: ${runId} · polling…`;
  } catch (err) {
    summary.textContent = `Failed to start: ${err?.message || err}`;
    return;
  }

  // Poll
  _hcPollTimer = setInterval(async () => {
    let state;
    try {
      state = await api("composer-health-check-status", { runId });
    } catch (err) {
      return;
    }
    if (!state) return;
    // Update each gate row
    for (const g of state.gates) {
      const node = gateNodes.get(g.id);
      if (node) _updateGateRow(node, g);
    }
    // Summary
    const passed = state.gates.filter((x) => x.status === "pass").length;
    const failed = state.gates.filter((x) => x.status === "fail").length;
    const running = state.gates.filter((x) => x.status === "running").length;
    const pending = state.gates.filter((x) => x.status === "pending").length;
    if (state.completed) {
      summary.innerHTML = "";
      const allPassed = failed === 0;
      const summaryRow = el("div", { class: "row", style: "gap:12px;align-items:center;" }, [
        el(
          "span",
          { style: `font-weight:600;color:${allPassed ? "#5ada5a" : "#fa8a8a"};` },
          allPassed ? "All gates passed" : `${failed} gate(s) failed`,
        ),
        el(
          "span",
          { class: "muted", style: "font-size:11px;" },
          `Total: ${msFormat(new Date(state.finished_at) - new Date(state.started_at))}`,
        ),
      ]);
      summary.appendChild(summaryRow);
      _stopHealthCheckPoll();
    } else {
      summary.textContent =
        `Running gate ${passed + failed + 1} of ${state.gates.length}` +
        ` · ${passed} pass · ${failed} fail · ${pending} pending`;
    }
  }, 1000);
}

let _hcPollTimer = null;
function _stopHealthCheckPoll() {
  if (_hcPollTimer) {
    clearInterval(_hcPollTimer);
    _hcPollTimer = null;
  }
}

function _buildGateRow(gate) {
  const row = el("div", {
    class: "hc-gate-row",
    "data-gate-id": gate.id,
    style:
      "padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; margin-bottom: 8px; background: var(--bg-0);",
  });
  row._gateId = gate.id;
  const top = el(
    "div",
    { class: "row", style: "justify-content: space-between; align-items: center;" },
    [
      el("div", { style: "flex: 1; min-width: 0;" }, [
        el("div", { class: "row", style: "gap: 8px; align-items: center;" }, [
          el(
            "span",
            { class: "hc-status", style: "font-size: 14px; width: 18px; text-align: center;" },
            "⏳",
          ),
          el("strong", { style: "font-size: 13px; color: var(--text-2);" }, gate.label),
          el(
            "span",
            { class: "hc-duration muted", style: "font-size: 10px; margin-left: 6px;" },
            "",
          ),
        ]),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 2px; padding-left: 26px;" },
          gate.description,
        ),
      ]),
      el(
        "div",
        {
          class: "hc-summary",
          style: "font-size: 11px; color: var(--text-2); min-width: 180px; text-align: right;",
        },
        "Pending",
      ),
    ],
  );
  row.appendChild(top);
  // Details container — populated only when failure has details
  const details = el("div", {
    class: "hc-details",
    style:
      "display: none; margin-top: 8px; padding: 8px 10px; background: var(--bg-0); border: 1px solid var(--border); border-radius: var(--radius-sm); font-family: ui-monospace, Menlo, monospace; font-size: 10px; color: var(--red); white-space: pre-wrap; max-height: 240px; overflow-y: auto;",
  });
  row.appendChild(details);
  return row;
}

function _updateGateRow(row, gate) {
  const statusIcons = {
    pending: { icon: "⏳", color: "#888" },
    running: { icon: "🏃", color: "#5a8aff" },
    pass: { icon: "✅", color: "#5ada5a" },
    fail: { icon: "❌", color: "#fa8a8a" },
  };
  const cfg = statusIcons[gate.status] || statusIcons.pending;
  const statusEl = row.querySelector(".hc-status");
  const summaryEl = row.querySelector(".hc-summary");
  const durationEl = row.querySelector(".hc-duration");
  const detailsEl = row.querySelector(".hc-details");
  if (statusEl) {
    statusEl.textContent = cfg.icon;
    statusEl.style.color = cfg.color;
  }
  if (summaryEl) {
    summaryEl.textContent = gate.summary || (gate.status === "running" ? "Running…" : "Pending");
    summaryEl.style.color = cfg.color;
  }
  if (durationEl && gate.durationMs != null) {
    durationEl.textContent = msFormat(gate.durationMs);
  }
  // Border color reflects status
  row.style.borderColor = cfg.color === "#888" ? "#222" : cfg.color + "40";
  // Show details for failures
  if (gate.status === "fail" && gate.details) {
    let detailText = "";
    if (Array.isArray(gate.details)) detailText = gate.details.join("\n");
    else if (typeof gate.details === "string") detailText = gate.details;
    else if (typeof gate.details === "object") {
      // Smoke gate's nested shape — failed_plays / failed_resolvers / dead_events / slow_resolvers
      const parts = [];
      for (const [key, list] of Object.entries(gate.details)) {
        if (!Array.isArray(list) || list.length === 0) continue;
        parts.push(`${key}:`);
        for (const item of list.slice(0, 10)) {
          if (typeof item === "string") parts.push(`  ${item}`);
          else if (item.id && item.error) parts.push(`  ${item.id}: ${item.error}`);
          else if (item.id) parts.push(`  ${item.id}${item.ms ? ` (${item.ms}ms)` : ""}`);
        }
        parts.push("");
      }
      detailText = parts.join("\n").trim();
    }
    if (detailText) {
      detailsEl.textContent = detailText;
      detailsEl.style.display = "block";
    }
  } else if (gate.status !== "fail") {
    detailsEl.style.display = "none";
  }
}

function msFormat(ms) {
  if (ms == null) return "";
  if (ms < 1000) return `${Math.round(ms)}ms`;
  return `${(ms / 1000).toFixed(1)}s`;
}

async function _openTemplatesPickerModal(onPick = null, initialTrack = "all") {
  // Phase 33: pull tracks (Phase 31a's 8-track taxonomy) alongside jtbds.
  // Tracks become the PRIMARY navigation axis since they're persona/intent-aligned;
  // jtbds become a secondary filter pill row.
  // Phase E: optional `initialTrack` pre-selects a track when opening.
  const { templates, jtbds, tracks } = await _ensureCompositionTemplates();
  if (templates.length === 0) {
    toast("No templates available — bot may need a restart to load them.", "err");
    return;
  }

  const overlay = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center;",
    onclick: (ev) => {
      if (ev.target === ev.currentTarget) overlay.remove();
    },
  });
  const modal = el("div", {
    style:
      "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 940px; max-width: 95vw; max-height: 80vh; display: flex; flex-direction: column;",
  });

  const header = el(
    "div",
    {
      class: "row",
      style:
        "padding: 14px 18px; border-bottom: 1px solid var(--border); justify-content: space-between; align-items: center;",
    },
    [
      el("div", {}, [
        el("div", { style: "font-weight: 600; font-size: 16px;" }, "Workflow templates"),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 2px;" },
          "Pick a template — the wizard opens with everything pre-filled. Customise before activating.",
        ),
      ]),
      el(
        "button",
        { class: "btn btn-tiny btn-secondary", type: "button", onclick: () => overlay.remove() },
        "✕",
      ),
    ],
  );

  const body = el("div", {
    style: "display: grid; grid-template-columns: 220px 1fr; gap: 0; flex: 1; overflow: hidden;",
  });
  const trackCol = el("div", { style: "border-right: 1px solid var(--border); overflow-y: auto;" });
  const detailCol = el("div", { style: "padding: 12px; overflow-y: auto;" });

  // Phase 33: tracks as PRIMARY navigation (Phase 31a's persona-aligned taxonomy).
  // Special "All" + "My templates" rows at top.
  // Phase E: pre-select the track passed in (validated against the available list).
  const validTrackIds = new Set(["all", "user", ...tracks.map((t) => t.id)]);
  let selectedTrack = validTrackIds.has(initialTrack) ? initialTrack : "all";
  const userTplCount = templates.filter((t) => t.tier === "user").length;

  const renderTracks = () => {
    trackCol.innerHTML = "";
    const allCount = templates.length;
    const isOnAll = selectedTrack === "all";
    trackCol.appendChild(
      el(
        "div",
        {
          style: `padding: 12px 14px; cursor: pointer; border-bottom: 1px solid var(--border); ${isOnAll ? "background: #1a1a2a; border-left: 2px solid #5a8aff;" : ""}`,
          onclick: () => {
            selectedTrack = "all";
            renderTracks();
            renderDetail();
          },
        },
        [
          el("div", { style: "font-size: 13px;" }, "All templates"),
          el(
            "div",
            { class: "muted", style: "font-size: 10px; margin-top: 2px;" },
            `${allCount} templates`,
          ),
        ],
      ),
    );
    if (userTplCount > 0) {
      const isOnUser = selectedTrack === "_user";
      trackCol.appendChild(
        el(
          "div",
          {
            style: `padding: 12px 14px; cursor: pointer; border-bottom: 1px solid var(--border); ${isOnUser ? "background: #2a1a2a; border-left: 2px solid #faaa5a;" : ""}`,
            onclick: () => {
              selectedTrack = "_user";
              renderTracks();
              renderDetail();
            },
          },
          [
            el("div", { style: "font-size: 13px;" }, "My templates"),
            el(
              "div",
              { class: "muted", style: "font-size: 10px; margin-top: 2px;" },
              `${userTplCount} saved`,
            ),
          ],
        ),
      );
    }
    // Render the 8 starter tracks (or fall back to JTBD if API didn't supply tracks)
    const navBuckets =
      Array.isArray(tracks) && tracks.length > 0
        ? tracks
        : (jtbds || []).map((j) => ({ id: j.id, label: j.label, icon: j.icon, description: "" }));
    for (const j of navBuckets) {
      const count =
        Array.isArray(tracks) && tracks.length > 0
          ? templates.filter((t) => (t.track || "operations") === j.id).length
          : templates.filter((t) => t.jtbd === j.id).length;
      if (count === 0) continue;
      const isOn = selectedTrack === j.id;
      trackCol.appendChild(
        el(
          "div",
          {
            style: `padding: 12px 14px; cursor: pointer; border-bottom: 1px solid var(--border); ${isOn ? "background: #1a1a2a; border-left: 2px solid #5a8aff;" : ""}`,
            onclick: () => {
              selectedTrack = j.id;
              renderTracks();
              renderDetail();
            },
            title: j.description || "",
          },
          [
            el("div", { style: "font-size: 13px;" }, [
              icon(icon.for(j.icon) || "layers"),
              " " + j.label,
            ]),
            el(
              "div",
              { class: "muted", style: "font-size: 10px; margin-top: 2px;" },
              `${count} template${count === 1 ? "" : "s"}`,
            ),
          ],
        ),
      );
    }
  };

  const renderDetail = () => {
    detailCol.innerHTML = "";
    const usingTracks = Array.isArray(tracks) && tracks.length > 0;
    let filtered;
    if (selectedTrack === "all") filtered = templates;
    else if (selectedTrack === "_user") filtered = templates.filter((t) => t.tier === "user");
    else if (usingTracks)
      filtered = templates.filter((t) => (t.track || "operations") === selectedTrack);
    else filtered = templates.filter((t) => t.jtbd === selectedTrack);
    if (filtered.length === 0) {
      detailCol.appendChild(
        el("div", { class: "muted", style: "font-size: 12px;" }, "No templates in this category."),
      );
      return;
    }
    // Phase 33: track header + description (when a track is selected)
    if (usingTracks && selectedTrack !== "all" && selectedTrack !== "_user") {
      const trackMeta = tracks.find((t) => t.id === selectedTrack);
      if (trackMeta?.description) {
        detailCol.appendChild(
          el(
            "div",
            {
              class: "muted",
              style:
                "font-size: 11px; line-height: 1.5; margin-bottom: 12px; padding: 8px 10px; background: var(--bg-0); border-radius: 4px;",
            },
            trackMeta.description,
          ),
        );
      }
    }
    const grid = el("div", { style: "display: grid; grid-template-columns: 1fr 1fr; gap: 10px;" });
    for (const t of filtered) {
      const platformPills = (t.platforms || []).map((p) =>
        el(
          "span",
          {
            class: "pill",
            style: "background: var(--bg-3); font-size: 9px; padding: 2px 6px; margin-right: 4px;",
          },
          p,
        ),
      );
      const tierBadge =
        t.tier === "user"
          ? el(
              "span",
              {
                class: "pill",
                style:
                  "background: #2a1a2a; color: #faaa5a; font-size: 9px; padding: 2px 6px; margin-left: 4px;",
              },
              "yours",
            )
          : null;
      const difficultyBadge = t.difficulty
        ? el(
            "span",
            {
              class: "pill",
              style: `background: ${t.difficulty === "beginner" ? "#1a2a1a" : t.difficulty === "advanced" ? "#2a1a1a" : "#1a1a2a"}; color: ${t.difficulty === "beginner" ? "#5ada5a" : t.difficulty === "advanced" ? "#fa8a8a" : "#8aaafa"}; font-size: 9px; padding: 2px 6px; margin-left: 4px;`,
            },
            t.difficulty,
          )
        : null;
      const starterPairBadge = t.starter_pair?.starter_id
        ? el(
            "span",
            {
              class: "pill",
              style:
                "background: #1a2a2a; color: #5adada; font-size: 9px; padding: 2px 6px; margin-left: 4px;",
              title: t.starter_pair.what_it_adds || "",
            },
            "↑ advanced",
          )
        : null;
      const card = el(
        "div",
        {
          style:
            "padding: 12px; background: var(--bg-0); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; position: relative;",
          onclick: async () => {
            overlay.remove();
            await _useTemplate(t.id, onPick);
          },
        },
        [
          el(
            "div",
            { style: "font-weight: 500; font-size: 13px; margin-bottom: 4px;" },
            [
              icon(icon.for(t.icon) || "layers"),
              " " + t.name,
              tierBadge,
              difficultyBadge,
              starterPairBadge,
            ].filter(Boolean),
          ),
          el(
            "div",
            { class: "muted", style: "font-size: 11px; line-height: 1.4; margin-bottom: 8px;" },
            t.description,
          ),
          el("div", { style: "margin-bottom: 6px;" }, platformPills),
          el(
            "div",
            {
              class: "row",
              style: "justify-content: space-between; align-items: center; gap: 4px;",
            },
            [
              el(
                "div",
                { class: "muted", style: "font-size: 10px; flex: 1; min-width: 0;" },
                `Audience: ${t.expected_audience || "—"} · Plays: ${(t.body?.plays || []).length}${(t.param_placeholders?.length || 0) > 0 ? ` · ${t.param_placeholders.length} param${t.param_placeholders.length === 1 ? "" : "s"}` : ""}`,
              ),
              // Phase 35.2: preview the template's shape as a graph before applying
              el(
                "button",
                {
                  class: "btn btn-tiny btn-secondary",
                  type: "button",
                  style: "font-size: 10px; padding: 2px 6px;",
                  onclick: (ev) => {
                    ev.stopPropagation();
                    _openTemplateGraphPreview(t);
                  },
                },
                "Preview",
              ),
              // Phase ZG: if this template has saved values from a previous
              // test, show a quick-fire button that skips the wizard entirely.
              _getSavedWizardConfig(t.id)
                ? el(
                    "button",
                    {
                      class: "btn btn-tiny btn-secondary",
                      type: "button",
                      style:
                        "font-size: 10px; padding: 2px 6px; background: #1a2a1a; color: var(--green);",
                      title:
                        "Re-fire with your last-used values (skips the wizard). Last tested: " +
                        (_getSavedWizardConfig(t.id)?.lastUsedAt || "—"),
                      onclick: (ev) => {
                        ev.stopPropagation();
                        overlay.remove();
                        _quickRetestFromTemplate(t);
                      },
                    },
                    "Quick re-test",
                  )
                : null,
              // Phase ZF: live test on one contact (real execution + visible browser)
              el(
                "button",
                {
                  class: "btn btn-tiny btn-primary",
                  type: "button",
                  style: "font-size: 10px; padding: 2px 6px;",
                  title:
                    "Configure this template and fire it LIVE on one contact (real DM/connect/etc — caps bypassed, browser visible).",
                  onclick: (ev) => {
                    ev.stopPropagation();
                    overlay.remove();
                    _openTestLiveOneFromTemplate(t);
                  },
                },
                "Test live",
              ),
            ].filter(Boolean),
          ),
        ],
      );
      // User templates can be deleted in-place
      if (t.tier === "user" && t._userTemplateId) {
        const delBtn = el(
          "button",
          {
            class: "btn btn-tiny btn-danger",
            type: "button",
            style: "position: absolute; top: 6px; right: 6px; opacity: 0.6;",
            title: "Delete this saved template",
            onclick: async (ev) => {
              ev.stopPropagation();
              if (!confirm(`Delete personal template "${t.name}"?`)) return;
              const r = await api("composition-delete-user-template", {
                id: t._userTemplateId,
              }).catch((e) => ({ error: e?.message }));
              if (r?.error) {
                toast(`Delete failed: ${r.error}`, "err");
                return;
              }
              // Bust cache and re-render
              try {
                _composerTemplates = null;
              } catch {}
              overlay.remove();
              _openTemplatesPickerModal(onPick);
            },
          },
          "✕",
        );
        card.appendChild(delBtn);
      }
      grid.appendChild(card);
    }
    detailCol.appendChild(grid);
  };

  body.appendChild(trackCol);
  body.appendChild(detailCol);
  modal.appendChild(header);
  modal.appendChild(body);
  overlay.appendChild(modal);
  document.body.appendChild(overlay);

  renderTracks();
  renderDetail();
}

/**
 * Phase 35.2 — Template graph preview modal.
 *
 * Opens a small modal showing the template's body as a flow diagram so
 * the user can see the shape BEFORE clicking through param entry. The
 * diagram uses the same _renderCompositionDiagram (Phase 34a static SVG)
 * that's used in the post-apply summary, so what the user sees here is
 * exactly what they'll get on the canvas.
 */
function _openTemplateGraphPreview(template) {
  const overlay = el("div", {
    style:
      "position: fixed; inset: 0; background: rgba(0,0,0,0.85); z-index: 10002; display: flex; align-items: center; justify-content: center;",
    onclick: (ev) => {
      if (ev.target === ev.currentTarget) overlay.remove();
    },
  });
  const modal = el("div", {
    style:
      "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 800px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;",
  });
  const header = el(
    "div",
    {
      class: "row",
      style:
        "padding: 14px 18px; border-bottom: 1px solid var(--border); justify-content: space-between; align-items: center;",
    },
    [
      el("div", {}, [
        el("div", { style: "font-weight: 600; font-size: 14px;" }, [
          icon(icon.for(template.icon) || "layers"),
          " " + template.name,
        ]),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 4px;" },
          template.description || "",
        ),
      ]),
      el(
        "button",
        {
          class: "btn btn-tiny btn-secondary",
          type: "button",
          onclick: () => overlay.remove(),
        },
        "✕",
      ),
    ],
  );
  const body = el("div", { style: "padding: 16px; overflow: auto; max-height: 60vh;" });
  try {
    const svg = _renderCompositionDiagram(template.body || {}, { readonly: true });
    body.appendChild(svg);
  } catch (err) {
    body.appendChild(
      el(
        "div",
        { class: "muted", style: "font-size: 12px;" },
        `Could not render preview: ${err?.message || err}`,
      ),
    );
  }
  const footer = el(
    "div",
    {
      class: "row",
      style:
        "padding: 12px 18px; border-top: 1px solid var(--border); justify-content: space-between; align-items: center;",
    },
    [
      el(
        "div",
        { class: "muted", style: "font-size: 11px;" },
        `${(template.body?.audience?.resolvers || []).length} resolver${(template.body?.audience?.resolvers || []).length === 1 ? "" : "s"} · ${(template.body?.channels || []).length} channel${(template.body?.channels || []).length === 1 ? "" : "s"} · ${(template.body?.plays || []).length} play${(template.body?.plays || []).length === 1 ? "" : "s"}`,
      ),
      el(
        "button",
        {
          class: "btn btn-tiny btn-primary",
          type: "button",
          onclick: async () => {
            overlay.remove();
            // Find the picker overlay and close it too, then apply the template
            const pickerOverlay = document.querySelector('div[style*="z-index: 9999"]');
            if (pickerOverlay) pickerOverlay.remove();
            await _useTemplate(template.id, null);
          },
        },
        "Use this template →",
      ),
    ],
  );
  modal.appendChild(header);
  modal.appendChild(body);
  modal.appendChild(footer);
  overlay.appendChild(modal);
  document.body.appendChild(overlay);
}

async function _useTemplate(templateId, onPick) {
  // Phase 33 — fetch the template definition first so we can surface
  // param_placeholders + starter_pair + runbook BEFORE applying. If the
  // template has any required placeholder, prompt the user; otherwise
  // apply directly.
  const { templates } = await _ensureCompositionTemplates();
  const tpl = templates.find((t) => t.id === templateId);
  const placeholders = Array.isArray(tpl?.param_placeholders) ? tpl.param_placeholders : [];
  const requiredPlaceholders = placeholders.filter((p) => p?.required);

  if (requiredPlaceholders.length === 0) {
    // No params needed — apply immediately
    return _applyTemplateWithParams(templateId, {}, tpl, onPick);
  }
  // Open the param input modal
  return _openTemplateParamModal(tpl, onPick);
}

/**
 * Phase 33 — Param input modal. Renders one input per param_placeholders
 * entry with type-aware widgets (number, array via comma list, boolean
 * checkbox, string textbox). Surfaces starter_pair hint at the top if the
 * template declares one. Submit calls _applyTemplateWithParams which then
 * shows runbook + audience_preview before navigating.
 */
async function _openTemplateParamModal(tpl, onPick) {
  return new Promise((resolve) => {
    const overlay = el("div", {
      style:
        "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 10000; display: flex; align-items: center; justify-content: center;",
      onclick: (ev) => {
        if (ev.target === ev.currentTarget) {
          overlay.remove();
          resolve(null);
        }
      },
    });
    const modal = el("div", {
      style:
        "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 560px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;",
    });
    const header = el(
      "div",
      { style: "padding: 14px 18px; border-bottom: 1px solid var(--border);" },
      [
        el("div", { style: "font-weight: 600; font-size: 15px;" }, [
          icon(icon.for(tpl.icon) || "layers"),
          " " + tpl.name,
        ]),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 4px; line-height: 1.4;" },
          tpl.description || "",
        ),
      ],
    );

    const formBody = el("div", { style: "padding: 16px 18px; overflow-y: auto;" });

    // Starter-pair hint (Phase 31a) — click to switch to beginner version
    if (tpl.starter_pair?.starter_id) {
      const switchBtn = el(
        "a",
        {
          style: "color: #8aaafa; cursor: pointer; text-decoration: underline; margin-left: 4px;",
          onclick: async (ev) => {
            ev.preventDefault();
            overlay.remove();
            // Re-open _useTemplate against the starter id
            await _useTemplate(tpl.starter_pair.starter_id, onPick);
            resolve(null);
          },
        },
        "→ switch to beginner",
      );
      formBody.appendChild(
        el(
          "div",
          {
            style:
              "padding: 10px 12px; background: #1a1a2a; border-left: 2px solid #5a8aff; border-radius: 4px; margin-bottom: 14px; font-size: 11px; line-height: 1.5;",
          },
          [
            el("strong", {}, "Beginner version available: "),
            el("span", {}, `${tpl.starter_pair.starter_id} `),
            tpl.starter_pair.what_it_adds
              ? el(
                  "span",
                  { class: "muted" },
                  `(this advanced version ${tpl.starter_pair.what_it_adds})`,
                )
              : null,
            el("br"),
            switchBtn,
          ].filter(Boolean),
        ),
      );
    }

    // Deprecation warning (Phase 31c)
    if (tpl.deprecated) {
      formBody.appendChild(
        el(
          "div",
          {
            style:
              "padding: 10px 12px; background: #2a1a1a; border-left: 2px solid #faaa5a; border-radius: 4px; margin-bottom: 14px; font-size: 11px; line-height: 1.5;",
          },
          [
            el("strong", { style: "color: #faaa5a;" }, "Deprecated template. "),
            el("span", {}, tpl.deprecation_reason || "Consider using a newer alternative."),
            tpl.deprecation_redirect
              ? [
                  " Recommended replacement: ",
                  el(
                    "a",
                    {
                      style: "color: #8aaafa; cursor: pointer; text-decoration: underline;",
                      onclick: async (ev) => {
                        ev.preventDefault();
                        overlay.remove();
                        await _useTemplate(tpl.deprecation_redirect, onPick);
                        resolve(null);
                      },
                    },
                    `→ ${tpl.deprecation_redirect}`,
                  ),
                ]
              : null,
          ].filter(Boolean),
        ),
      );
    }

    // Param inputs
    const inputs = {};
    const placeholders = Array.isArray(tpl.param_placeholders) ? tpl.param_placeholders : [];
    if (placeholders.length === 0) {
      formBody.appendChild(
        el(
          "div",
          { class: "muted", style: "font-size: 12px;" },
          "No parameters needed — click Apply to clone.",
        ),
      );
    }
    for (const p of placeholders) {
      const label = el(
        "div",
        { style: "font-size: 11px; color: var(--text-2); margin-bottom: 4px;" },
        [
          p.label || p.key,
          p.required ? el("span", { style: "color: var(--red); margin-left: 4px;" }, "*") : null,
          p.type
            ? el(
                "span",
                { class: "muted", style: "margin-left: 6px; font-size: 10px;" },
                `(${p.type})`,
              )
            : null,
        ].filter(Boolean),
      );
      let input;
      if (p.type === "boolean") {
        input = el("input", { type: "checkbox", style: "margin-right: 6px;" });
      } else if (p.type === "number") {
        input = el("input", {
          type: "number",
          style:
            "width: 100%; padding: 8px; background: var(--bg-0); border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-1); font-size: 12px;",
        });
      } else {
        input = el("input", {
          type: "text",
          style:
            "width: 100%; padding: 8px; background: var(--bg-0); border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text-1); font-size: 12px;",
          placeholder: p.type === "array" ? "comma-separated values" : "",
        });
      }
      inputs[p.key] = input;
      formBody.appendChild(el("div", { style: "margin-bottom: 14px;" }, [label, input]));
    }

    // Apply button
    const applyBtn = el("button", { class: "btn btn-primary", type: "button" }, "Apply template");
    const cancelBtn = el("button", { class: "btn btn-secondary", type: "button" }, "Cancel");
    const errorBox = el("div", {
      style: "color: var(--red); font-size: 11px; margin-top: 8px; min-height: 14px;",
    });

    cancelBtn.onclick = () => {
      overlay.remove();
      resolve(null);
    };
    applyBtn.onclick = async () => {
      const params = {};
      for (const p of placeholders) {
        const inp = inputs[p.key];
        if (p.type === "boolean") params[p.key] = !!inp.checked;
        else params[p.key] = (inp.value || "").trim();
      }
      // Validate required
      const missing = placeholders.filter((p) => p.required && !params[p.key]).map((p) => p.key);
      if (missing.length > 0) {
        errorBox.textContent = `Missing required: ${missing.join(", ")}`;
        return;
      }
      errorBox.textContent = "Applying…";
      applyBtn.disabled = true;
      const result = await _applyTemplateWithParams(tpl.id, params, tpl, onPick, overlay);
      resolve(result);
    };

    const footer = el(
      "div",
      {
        class: "row",
        style:
          "padding: 12px 18px; border-top: 1px solid var(--border); gap: 8px; justify-content: flex-end; align-items: center;",
      },
      [errorBox, el("div", { style: "flex: 1;" }), cancelBtn, applyBtn],
    );

    modal.appendChild(header);
    modal.appendChild(formBody);
    modal.appendChild(footer);
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
  });
}

/**
 * Phase 33 — Apply a template with user-supplied params, then surface
 * the runbook checklist + audience_preview before navigating to the
 * workflow editor. The user can review before activating.
 */
async function _applyTemplateWithParams(templateId, params, tpl, onPick, modalToClose = null) {
  const r = await api("composition-from-template", {
    templateId,
    body: { params },
  }).catch((e) => ({ error: e?.message }));
  if (r?.error) {
    toast(`Could not apply template: ${r.error}`, "err", 5000);
    return null;
  }
  const c = r?.composition;
  if (!c) {
    toast("Template returned no composition", "err");
    return null;
  }
  if (modalToClose) modalToClose.remove();

  // Phase 33 — show post-apply summary modal with runbook + audience preview
  await _showPostApplySummary(r, tpl);

  if (onPick) {
    onPick(c);
    return c;
  }
  dashboardState.composerDraft = { ...c };
  navigate("workflows/edit");
  return c;
}

/**
 * Phase 33 — Post-apply summary modal. Shows the user what they just
 * cloned: runbook checklist (Phase 31d), audience_preview (Phase 31d
 * — count + 5 sample contacts), starter_pair recap (Phase 31a). User
 * confirms before the wizard opens.
 */
async function _showPostApplySummary(applyResp, tpl) {
  return new Promise((resolve) => {
    const c = applyResp.composition;
    const runbook = Array.isArray(applyResp.runbook)
      ? applyResp.runbook
      : Array.isArray(tpl?.runbook)
        ? tpl.runbook
        : [];
    const preview = applyResp.audience_preview;

    const overlay = el("div", {
      style:
        "position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 10001; display: flex; align-items: center; justify-content: center;",
      onclick: (ev) => {
        if (ev.target === ev.currentTarget) {
          overlay.remove();
          resolve();
        }
      },
    });
    const modal = el("div", {
      style:
        "background: var(--bg-1); border: 1px solid var(--border-strong); border-radius: 8px; width: 760px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;",
    });
    const header = el(
      "div",
      { style: "padding: 14px 18px; border-bottom: 1px solid var(--border);" },
      [
        el("div", { style: "font-weight: 600; font-size: 15px;" }, `Cloned: ${c.name}`),
        el(
          "div",
          { class: "muted", style: "font-size: 11px; margin-top: 4px;" },
          "Review below before opening the wizard. Composition is in DRAFT state.",
        ),
      ],
    );
    const body = el("div", { style: "padding: 16px 18px; overflow-y: auto; max-height: 60vh;" });

    // Phase 34: render the composition shape as a diagram at the top of the
    // post-apply summary so users see the flow visually before opening the wizard.
    try {
      const diagSection = el("div", { style: "margin-bottom: 18px;" }, [
        el(
          "div",
          { style: "font-size: 12px; font-weight: 500; margin-bottom: 6px;" },
          "Workflow shape",
        ),
      ]);
      const svg = _renderCompositionDiagram(c, {
        audiencePreview: preview,
        readonly: true,
      });
      diagSection.appendChild(svg);
      body.appendChild(diagSection);
    } catch {
      /* fall back silently if diagram render fails */
    }

    // Audience preview
    if (preview) {
      const prevSection = el("div", { style: "margin-bottom: 18px;" }, [
        el(
          "div",
          { style: "font-size: 12px; font-weight: 500; margin-bottom: 6px;" },
          "Audience preview",
        ),
      ]);
      if (preview.error) {
        prevSection.appendChild(
          el(
            "div",
            { class: "muted", style: "font-size: 11px;" },
            `Could not preview: ${preview.error}`,
          ),
        );
      } else if (preview.count == null) {
        prevSection.appendChild(
          el("div", { class: "muted", style: "font-size: 11px;" }, "Preview unavailable."),
        );
      } else {
        prevSection.appendChild(
          el(
            "div",
            { style: "font-size: 13px; margin-bottom: 6px;" },
            `Will target ${preview.count} contact${preview.count === 1 ? "" : "s"}.`,
          ),
        );
        if (preview.count === 0) {
          prevSection.appendChild(
            el(
              "div",
              {
                style:
                  "padding: 8px 10px; background: #2a1a1a; border-left: 2px solid #faaa5a; border-radius: 4px; font-size: 11px;",
              },
              "Audience is empty. Verify your params, scrapers may need to populate data first, or the audience.exclude window is too tight.",
            ),
          );
        }
        if (Array.isArray(preview.sample) && preview.sample.length > 0) {
          const sampleList = el("div", {
            style:
              "font-size: 11px; color: var(--text-2); line-height: 1.6; margin-top: 6px; padding: 8px 10px; background: var(--bg-0); border-radius: 4px;",
          });
          for (const s of preview.sample) {
            const handle =
              s.telegram_username ||
              s.twitter_handle ||
              (s.linkedin_url || "").replace(/.*\/in\//, "") ||
              s.email ||
              "";
            sampleList.appendChild(
              el(
                "div",
                {},
                `• ${s.display_name || `#${s.id}`}${handle ? ` (${handle})` : ""}${s.company ? ` — ${s.company}` : ""}`,
              ),
            );
          }
          prevSection.appendChild(sampleList);
        }
      }
      body.appendChild(prevSection);
    }

    // Runbook checklist
    if (runbook.length > 0) {
      const runbookSection = el("div", {}, [
        el(
          "div",
          { style: "font-size: 12px; font-weight: 500; margin-bottom: 6px;" },
          "Pre-flight checklist",
        ),
        el(
          "div",
          { class: "muted", style: "font-size: 10px; margin-bottom: 8px;" },
          "Confirm these before activating. Required items are highlighted.",
        ),
      ]);
      for (const item of runbook) {
        runbookSection.appendChild(
          el(
            "div",
            {
              style: `padding: 8px 10px; margin-bottom: 4px; background: var(--bg-0); border-left: 2px solid ${item.required ? "#faaa5a" : "#444"}; border-radius: 4px; font-size: 11px; line-height: 1.5;`,
            },
            [
              item.required
                ? el("span", { style: "color: #faaa5a; margin-right: 6px;" }, icon("star"))
                : el("span", { class: "muted", style: "margin-right: 6px;" }, "•"),
              item.check,
            ],
          ),
        );
      }
      body.appendChild(runbookSection);
    }

    const footer = el(
      "div",
      {
        class: "row",
        style:
          "padding: 12px 18px; border-top: 1px solid var(--border); gap: 8px; justify-content: flex-end;",
      },
      [
        el(
          "button",
          {
            class: "btn btn-primary",
            type: "button",
            onclick: () => {
              overlay.remove();
              resolve();
            },
          },
          "Open in wizard →",
        ),
      ],
    );

    modal.appendChild(header);
    modal.appendChild(body);
    modal.appendChild(footer);
    overlay.appendChild(modal);
    document.body.appendChild(overlay);
  });
}
