/**
 * Client Command Center (#clients/:id) — everything about ONE customer.
 *
 * Replaces a picture that was spread across five disconnected surfaces: the
 * topbar workspace switcher, the Spanish #agency table, #clients,
 * #client-report/:id and the orphaned #company/:id console. One page, tabs.
 *
 * The Automations tab uses the row shape that answers "is everything running
 * for this client" with zero clicks: name · inline pause/resume · last-run
 * status · a mini bar of recent run outcomes. Borrowed from Airflow's DAG row,
 * which the competitor research identified as the highest-value single row in
 * any ops dashboard.
 *
 * Data: GET /api/clients/:id/console (db/client-console.js buildClientConsole).
 * Sections are failure-isolated server-side; `degraded[]` names any that failed
 * and this page SAYS SO rather than rendering an empty section as if it were
 * genuinely empty — a silent zero is the failure mode this product exists to
 * avoid.
 */

const CC_TABS = [
  { key: "automations", label: "Automations" },
  { key: "replies", label: "Replies" },
  { key: "activity", label: "Activity" },
  { key: "results", label: "Results" },
  { key: "accounts", label: "Accounts & limits" },
  { key: "settings", label: "Settings" },
];

/** Shared status grammar: green worked · red failed · amber degraded · grey idle. */
function _ccStateTone(state) {
  if (state === "live") return "var(--status-ok)";
  if (state === "paused") return "var(--status-warn)";
  if (state === "done") return "#6b7280";
  return "var(--text-dim, #9aa4b2)"; // draft
}

function _ccStatePillFor(state) {
  const label = state === "live" ? "LIVE" : String(state || "draft").toUpperCase();
  return el(
    "span",
    {
      class: "badge",
      style: `color:${_ccStateTone(state)};border-color:${_ccStateTone(state)};`,
    },
    label,
  );
}

/**
 * Mini run-outcome bar: one tick per recent run. Only rendered when the kind
 * actually reports runs — compositions do, the others do not yet, and drawing
 * an empty strip for them would imply "ran and did nothing" rather than
 * "this engine keeps no run log".
 */
function _ccRunBars(runStats) {
  if (!runStats || runStats.recentRuns == null) {
    return el("span", { class: "muted", title: "this engine keeps no per-run log" }, "—");
  }
  const ok = Number(runStats.ok || 0);
  const failed = Number(runStats.failed || 0);
  const skipped = Number(runStats.skipped || 0);
  const total = Math.max(0, Number(runStats.recentRuns || 0));
  const wrap = el("span", { style: "display:inline-flex;gap:2px;align-items:center;" });
  // Failures red, skips GREY, successes green. A skip is a run that deliberately
  // did NOT happen; painting it green reports a stopped automation as working,
  // in this page's own success colour.
  const tick = (color, title) =>
    el("span", {
      title,
      style: `display:inline-block;width:4px;height:12px;border-radius:1px;background:${color};`,
    });
  let drawn = 0;
  for (let i = 0; i < failed && drawn < 10; i++, drawn++)
    wrap.appendChild(tick("var(--status-bad)", "failed"));
  for (let i = 0; i < skipped && drawn < 10; i++, drawn++)
    wrap.appendChild(tick("#6b7280", "skipped — did not run"));
  for (let i = 0; i < ok && drawn < 10; i++, drawn++)
    wrap.appendChild(tick("var(--status-ok)", "ok"));
  if (!total) wrap.appendChild(el("span", { class: "muted" }, "never run"));
  else
    wrap.appendChild(
      el(
        "span",
        { class: "muted", style: "margin-left:6px;" },
        skipped ? `${ok}/${Math.max(0, total - skipped)} · ${skipped} skipped` : `${ok}/${total}`,
      ),
    );
  return wrap;
}

function _ccAgo(ts) {
  if (!ts) return "never";
  const t = new Date(
    String(ts).replace(" ", "T") + (String(ts).endsWith("Z") ? "" : "Z"),
  ).getTime();
  if (isNaN(t)) return String(ts);
  const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
  if (s < 60) return `${s}s ago`;
  if (s < 3600) return `${Math.floor(s / 60)}m ago`;
  if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
  return `${Math.floor(s / 86400)}d ago`;
}

/** Banner naming every section the server could not build. */
function _ccDegradedBanner(degraded) {
  if (!degraded || !degraded.length) return null;
  return el(
    "div",
    {
      class: "card",
      style:
        "border-color:var(--status-warn);margin-bottom:12px;display:flex;gap:10px;align-items:center;",
    },
    [
      el("span", {}, icon("alert")),
      el(
        "div",
        {},
        `Could not load: ${degraded.join(", ")}. Those sections are showing empty because the query failed, not because there is nothing there.`,
      ),
    ],
  );
}

/**
 * Persist the current row order. Called by BOTH the drag handler and the ↑/↓
 * buttons so there is one write path — the buttons are the accessible
 * equivalent, not a second-class fallback.
 *
 * Optimistic: the DOM already shows the new order, so on failure we re-render
 * from the server rather than leaving the user looking at an order that was
 * never saved.
 */
async function _ccSaveOrder(clientId, orderedIds) {
  const res = await api("client-automation-order", {
    id: clientId,
    body: { order: orderedIds },
  }).catch((e) => ({ error: e?.message || "failed" }));
  if (res?.error) {
    toast(`Order not saved: ${res.error}`, "err");
    renderClientCommandCenter({ id: clientId });
    return false;
  }
  toast("Order saved", "ok", 1500);
  return true;
}

/** Read the current visual order straight off the tbody. */
function _ccCurrentOrder(tbody) {
  return [...tbody.querySelectorAll("tr[data-aid]")].map((tr) => tr.dataset.aid);
}

function _ccMoveRow(tbody, tr, delta, clientId) {
  const rowsEls = [...tbody.querySelectorAll("tr[data-aid]")];
  const i = rowsEls.indexOf(tr);
  const j = i + delta;
  if (i < 0 || j < 0 || j >= rowsEls.length) return;
  if (delta < 0) tbody.insertBefore(tr, rowsEls[j]);
  else tbody.insertBefore(rowsEls[j], tr);
  _ccSaveOrder(clientId, _ccCurrentOrder(tbody));
}

/**
 * HTML5 drag-to-reorder. The drag IS the write — no separate save step, which
 * is the only shape of drag-and-drop the research found earns its cost.
 */
function _ccWireDrag(tbody, clientId) {
  let dragged = null;
  tbody.addEventListener("dragstart", (e) => {
    const tr = e.target.closest("tr[data-aid]");
    if (!tr) return;
    dragged = tr;
    tr.style.opacity = "0.4";
    e.dataTransfer.effectAllowed = "move";
    // Firefox requires data to be set for the drag to start at all.
    e.dataTransfer.setData("text/plain", tr.dataset.aid);
  });
  tbody.addEventListener("dragover", (e) => {
    if (!dragged) return;
    e.preventDefault();
    const over = e.target.closest("tr[data-aid]");
    if (!over || over === dragged) return;
    const rect = over.getBoundingClientRect();
    const below = e.clientY - rect.top > rect.height / 2;
    tbody.insertBefore(dragged, below ? over.nextSibling : over);
  });
  tbody.addEventListener("dragend", () => {
    if (!dragged) return;
    dragged.style.opacity = "";
    dragged = null;
    _ccSaveOrder(clientId, _ccCurrentOrder(tbody));
  });
}

/**
 * Add an automation to this client from the preset catalog.
 *
 * Defaults to activate:true. A clone lands status='draft' otherwise, and
 * refreshCustomWorkflowSchedule only registers ACTIVE rows — so "added" would
 * mean "silently never runs", which is the trap this flow exists to close. The
 * opt-out is visible rather than the default, and the cadence is shown before
 * confirming so nobody starts something live without seeing how often it fires.
 */
async function _ccAddAutomationModal(clientId) {
  // "automations-list" (GET /api/workflows) is the PRESET CATALOG. Do not use
  // "workflows-list" — despite the name it maps to /api/campaigns/workflows,
  // which returns campaign_workflows rows (the Telegram engine) and carries no
  // `kind`, so the picker silently came up empty.
  const res = await api("automations-list").catch((e) => ({ error: e?.message || "unreachable" }));
  if (res?.error) {
    toast(`${res.error}`, "err");
    return;
  }
  // GET /api/campaigns/workflows returns {workflows:[...customs, ...presets]} —
  // customs FIRST, and there is no `presets` key. Taking the whole list put an
  // already-cloned custom (id "custom_68") at the top as the default selection,
  // and cloning that 404s with "Preset 'custom_68' not found". Only rows tagged
  // kind:"preset" are clonable.
  const presets = (res.workflows || []).filter((p) => p && p.kind === "preset" && p.id);
  if (!presets.length) {
    toast("No presets available to add", "info");
    return;
  }

  const select = el(
    "select",
    { class: "input", style: "width:100%;" },
    presets.map((p) =>
      el(
        "option",
        { value: p.id },
        `${p.name || p.id}${p.schedule_min ? ` — every ${p.schedule_min}min` : ""}`,
      ),
    ),
  );
  const startPaused = el("input", { type: "checkbox" });
  const cadence = el("div", { class: "muted", style: "margin-top:8px;" }, "");
  const showCadence = () => {
    const p = presets.find((x) => String(x.id) === select.value);
    cadence.textContent = p?.schedule_min
      ? `Runs automatically every ${p.schedule_min} minutes once active.`
      : "Cadence is defined by the preset's own trigger.";
  };
  select.addEventListener("change", showCadence);
  showCadence();

  openModal({
    title: "Add an automation to this client",
    body: el("div", {}, [
      el("div", { class: "muted", style: "margin-bottom:6px;" }, "Preset"),
      select,
      cadence,
      el("label", { style: "display:flex;gap:8px;align-items:center;margin-top:12px;" }, [
        startPaused,
        el("span", {}, "Start paused (add it as a draft — it will not run until activated)"),
      ]),
    ]),
    confirmLabel: "Add",
    onConfirm: async () => {
      const r = await api("automation-clone", {
        body: { presetId: select.value, activate: !startPaused.checked, clientId },
      }).catch((e) => ({ error: e?.message || "failed" }));
      if (r?.error) {
        toast(`${r.error}`, "err");
        return false;
      }
      // Belt and braces on attribution: the X-Client-Id header auto-tags new
      // work, but this page can be opened without an active client set, so tag
      // it explicitly too rather than relying on ambient state.
      if (r.custom_id) {
        await api("work-tag-client", {
          kind: "custom_workflow",
          workId: r.custom_id,
          body: { client_id: clientId },
        }).catch(() => ({}));
      }
      toast(startPaused.checked ? "Added as a draft (not running)" : "Added and activated", "ok");
      renderClientCommandCenter({ id: clientId });
      return true;
    },
  });
}

/**
 * "Did this actually do anything?" — the verdict a green status pill hides.
 *
 * A composition can run on schedule, return ok, and execute ZERO plays every
 * time: the receipt says the cron fired, not that work happened. Real example
 * on this account: 21 consecutive ok runs, every one "0/0 plays". Showing that
 * as green is how an automation idles for a month unnoticed.
 */
function _ccWorkVerdict(runs) {
  if (!runs || !runs.length) return null;
  const withPlays = runs.filter((r) => {
    const m = String(r.label || "").match(/(\d+)\s*\/\s*(\d+)\s*plays/);
    return m && Number(m[1]) > 0;
  });
  const anyPlays = withPlays.length > 0;
  const looksIdle = !anyPlays && runs.some((r) => /0\s*\/\s*0\s*plays/.test(String(r.label || "")));
  if (!looksIdle) return null;
  return el(
    "div",
    {
      class: "card",
      style: "border-color:var(--status-warn);margin:10px 0;padding:10px;",
    },
    [
      el("div", { style: "color:var(--status-warn);font-weight:600;" }, "Ran, but did no work"),
      el(
        "div",
        { class: "muted", style: "margin-top:4px;" },
        `Every one of the last ${runs.length} runs executed 0 plays. The schedule is firing and the run is recorded as "ok", but nothing was actually sent. Usually means the audience resolved empty, or no connected account matched the channel.`,
      ),
    ],
  );
}

/**
 * Where a group campaign is actually posting (group_campaign kind only).
 *
 * A campaign with 4 targets of which 2 are `failed` reads as healthy if you
 * only count posts — and "failed" here is permanent: the group refused the
 * message (CHAT_WRITE_FORBIDDEN) and rotation will never serve it again. The
 * split has to be visible next to the send count or the number lies.
 */
function _ccTargetsCard(targets) {
  if (!Array.isArray(targets) || !targets.length) return null;
  const tone = (s) =>
    s === "joined"
      ? "var(--status-ok)"
      : s === "failed"
        ? "var(--status-bad)"
        : "var(--text-dim,#9aa4b2)";
  const dead = targets.filter((t) => t.status === "failed").length;
  const tbl = el("table", { class: "table" });
  tbl.appendChild(
    el("thead", {}, [
      el("tr", {}, [el("th", {}, "Group"), el("th", {}, "Status"), el("th", {}, "Posts")]),
    ]),
  );
  const tb = el("tbody", {});
  for (const t of targets) {
    tb.appendChild(
      el("tr", {}, [
        el("td", {}, [
          el("div", {}, t.group_name || t.group_identifier || "—"),
          t.group_identifier
            ? el("div", { class: "muted", style: "font-size:11px;" }, t.group_identifier)
            : null,
        ]),
        el(
          "td",
          { style: `color:${tone(t.status)};` },
          t.status === "failed" ? "rejected us" : t.status || "—",
        ),
        el("td", { class: "muted" }, String(t.post_count ?? "—")),
      ]),
    );
  }
  tbl.appendChild(tb);
  return el("div", { class: "card", style: "margin:12px 0;" }, [
    el("div", { class: "card-title" }, `Groups (${targets.length})`),
    dead
      ? el(
          "div",
          { style: "color:var(--status-bad);font-size:12px;margin:4px 0 8px;" },
          `${dead} of ${targets.length} rejected the message and are permanently out of rotation.`,
        )
      : null,
    tbl,
  ]);
}

/** Drawer: full run history for one automation, opened from its row. */
async function _ccOpenAutomationDrawer(a, clientId) {
  const body = el("div", {}, [el("div", { class: "muted" }, "Loading run history…")]);
  openDrawer(a.name || a.id, body);

  const res = await api("campaigns-detail", {
    queryString: `?id=${encodeURIComponent(a.id)}`,
  }).catch((e) => ({ error: e?.message || "unreachable" }));

  body.innerHTML = "";
  if (res?.error || res?.ok === false) {
    body.appendChild(
      el("div", { class: "muted" }, `Could not load detail: ${res.error || "unsupported kind"}`),
    );
    return;
  }
  const c = res.campaign || a;
  const s = res.summary || {};
  const runs = res.runs || [];

  body.appendChild(
    el("div", { style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, [
      _ccStatePillFor(c.state),
      el("span", { class: "muted" }, c.kind || ""),
      c.channel ? el("span", { class: "muted" }, "· " + c.channel) : null,
      c.bundleName ? el("span", { class: "muted" }, "· in " + c.bundleName) : null,
    ]),
  );

  body.appendChild(
    el("div", { class: "stats-grid", style: "margin:12px 0;" }, [
      statCard("Total runs", s.totalRuns ?? "—", "since created"),
      statCard("Recent ok", s.ok ?? 0, `of ${s.recentRuns ?? 0} shown`),
      statCard("Failed", s.failed ?? 0, "in the same window"),
      statCard("Last run", _ccAgo(s.lastRunAt || c.lastRunAt), s.lastRunAt || "never"),
    ]),
  );

  const verdict = _ccWorkVerdict(runs);
  if (verdict) body.appendChild(verdict);

  const targetsCard = _ccTargetsCard(res.targets);
  if (targetsCard) body.appendChild(targetsCard);

  if (!runs.length) {
    body.appendChild(
      el(
        "div",
        { class: "muted" },
        "No run history recorded. Either it has never fired, or this engine keeps no per-run log.",
      ),
    );
  } else {
    const tbl = el("table", { class: "table" });
    tbl.appendChild(
      el("thead", {}, [
        el("tr", {}, [el("th", {}, "When"), el("th", {}, "Status"), el("th", {}, "What it did")]),
      ]),
    );
    const tb = el("tbody", {});
    for (const r of runs) {
      const tone =
        r.status === "ok"
          ? "var(--status-ok)"
          : r.status === "failed"
            ? "var(--status-bad)"
            : "#6b7280";
      tb.appendChild(
        el("tr", {}, [
          el("td", { class: "muted", title: r.at || "" }, _ccAgo(r.at)),
          el("td", { style: `color:${tone};` }, r.status || "—"),
          el("td", { class: "muted" }, [
            el("div", {}, r.label || ""),
            // The exact text that went out. For a send-based kind this IS the
            // proof of work — a run row saying "posted" without the message is
            // the same unverifiable claim the rest of this page exists to kill.
            r.text
              ? el("details", { style: "margin-top:4px;" }, [
                  el("summary", { style: "cursor:pointer;" }, "view message"),
                  el(
                    "pre",
                    {
                      style:
                        "white-space:pre-wrap;margin:6px 0 0;font-size:11px;line-height:1.45;max-height:240px;overflow:auto;",
                    },
                    r.text,
                  ),
                ])
              : null,
          ]),
        ]),
      );
    }
    tbl.appendChild(tb);
    body.appendChild(el("div", { class: "card" }, [tbl]));
  }

  const isLive = c.state === "live";
  body.appendChild(
    el("div", { style: "margin-top:12px;display:flex;gap:8px;" }, [
      el(
        "button",
        {
          class: "btn btn-ghost",
          onclick: (ev) =>
            mutateAction(
              ev,
              "campaigns-action",
              { body: { id: a.id, action: isLive ? "pause" : "resume" } },
              {
                success: isLive ? "Paused" : "Resumed",
                failPrefix: isLive ? "Pause failed" : "Resume failed",
                after: () => {
                  closeDrawer();
                  renderClientCommandCenter({ id: clientId });
                },
              },
            ),
        },
        isLive ? "Pause" : "Resume",
      ),
    ]),
  );
}

/**
 * State filter above the table. This exists because of what the real data looks
 * like: OnchainBureau carries 64 tagged automations, 45 of them drafts that have
 * never run once. The four that ARE running were below the fold, so the page
 * that is supposed to answer "is everything working" answered it last.
 *
 * Drafts are hidden by default once they outnumber the working set. Rows are
 * hidden with `display:none` and NOT removed: _ccCurrentOrder() reads every
 * tr[data-aid] off the tbody and the PUT rewrites the full set, so dropping
 * them from the DOM would silently delete their saved position.
 */
function _ccStateFilterBar(rows, tbody) {
  const count = (s) => rows.filter((r) => (r.state || "draft") === s).length;
  const drafts = count("draft");
  const working = rows.length - drafts;
  // Only auto-hide when drafts genuinely bury the signal; a client with 3 of
  // each is better shown whole than filtered.
  let hideDrafts = drafts > 5 && drafts > working;
  const apply = () => {
    for (const tr of tbody.querySelectorAll("tr[data-aid]"))
      tr.style.display = hideDrafts && tr.dataset.state === "draft" ? "none" : "";
  };
  const chip = (label, tone) =>
    el(
      "span",
      {
        class: "badge",
        style: `margin-right:6px;${tone ? `color:${tone};border-color:${tone};` : ""}`,
      },
      label,
    );
  const toggle = el(
    "button",
    {
      class: "btn btn-ghost",
      onclick: (ev) => {
        hideDrafts = !hideDrafts;
        ev.currentTarget.textContent = hideDrafts ? `Show ${drafts} drafts` : "Hide drafts";
        apply();
      },
    },
    hideDrafts ? `Show ${drafts} drafts` : "Hide drafts",
  );
  const bar = el(
    "div",
    {
      style:
        "display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;font-size:12px;",
    },
    [
      chip(`${count("live")} live`, "var(--status-ok)"),
      chip(`${count("paused")} paused`, "var(--status-warn)"),
      chip(`${drafts} draft`, null),
      drafts ? toggle : null,
      drafts
        ? el("span", { class: "muted" }, "Drafts have never run and will not run until activated.")
        : null,
    ],
  );
  // Deferred one tick: tbody rows exist by then, and this keeps the filter's
  // initial state and its click handler running through the exact same path.
  setTimeout(apply, 0);
  return bar;
}

function _ccAutomationsTab(data, clientId) {
  const rows = data.automations || [];
  if (!rows.length) {
    return el("div", { class: "empty-state" }, [
      el("div", { class: "empty-state-icon" }, icon("settings")),
      el("div", { class: "empty-state-title" }, "No automations tagged to this client"),
      el(
        "div",
        { class: "empty-state-hint" },
        "Campaigns become visible here once they carry this client's tag — set the active client before launching, or tag existing work from the Clients page.",
      ),
      // Without this the empty state is a dead end: no automations and no way
      // to add one from the page whose job is managing them.
      el(
        "button",
        {
          class: "btn btn-primary",
          style: "margin-top:12px;",
          onclick: () => _ccAddAutomationModal(clientId),
        },
        "+ Add automation",
      ),
    ]);
  }
  const tbl = el("table", { class: "table" });
  tbl.appendChild(
    el("thead", {}, [
      el("tr", {}, [
        el("th", { style: "width:28px;" }, ""),
        el("th", {}, "Automation"),
        el("th", {}, "Kind"),
        el("th", {}, "State"),
        el("th", {}, "Last run"),
        el("th", {}, "Recent outcomes"),
        el("th", { style: "text-align:right;" }, ""),
      ]),
    ]),
  );
  const tbody = el("tbody", {});
  for (const a of rows) {
    const isLive = a.state === "live";
    const tr = el("tr", { draggable: "true", "data-aid": a.id, "data-state": a.state || "draft" });
    for (const cell of [
      el(
        "td",
        {
          style: "cursor:grab;color:var(--text-dim,#9aa4b2);user-select:none;",
          title: "Drag to reorder — or use the ↑ ↓ buttons",
        },
        "⠿",
      ),
      el("td", {}, [
        el(
          "div",
          {
            style: "font-weight:600;cursor:pointer;text-decoration:underline dotted;",
            title: "Open run history",
            onclick: () => _ccOpenAutomationDrawer(a, clientId),
          },
          a.name || a.id,
        ),
        a.bundleName ? el("div", { class: "muted" }, `in ${a.bundleName}`) : null,
        // A clone that lands as a draft silently never runs. Say so loudly
        // rather than letting it look like a paused-but-configured automation.
        a.state === "draft"
          ? el(
              "div",
              { style: "color:var(--status-warn);font-size:11px;" },
              "DRAFT — this will not run until it is activated",
            )
          : null,
      ]),
      el("td", { class: "muted" }, a.kind),
      el("td", {}, _ccStatePillFor(a.state)),
      el("td", { class: "muted" }, _ccAgo(a.runStats?.lastRunAt || a.lastRunAt)),
      el("td", {}, _ccRunBars(a.runStats)),
      el("td", { style: "text-align:right;white-space:nowrap;" }, [
        // Keyboard/accessible equivalent of the drag. Same write path.
        el(
          "button",
          {
            class: "btn btn-ghost",
            title: "Move up",
            "aria-label": `Move ${a.name || a.id} up`,
            onclick: (ev) => _ccMoveRow(tbody, ev.currentTarget.closest("tr"), -1, clientId),
          },
          "↑",
        ),
        el(
          "button",
          {
            class: "btn btn-ghost",
            title: "Move down",
            "aria-label": `Move ${a.name || a.id} down`,
            onclick: (ev) => _ccMoveRow(tbody, ev.currentTarget.closest("tr"), 1, clientId),
          },
          "↓",
        ),
        el(
          "button",
          {
            class: "btn btn-ghost",
            style: "margin-left:6px;",
            onclick: (ev) =>
              mutateAction(
                ev,
                "campaigns-action",
                { body: { id: a.id, action: isLive ? "pause" : "resume" } },
                {
                  success: isLive ? "Paused" : "Resumed",
                  failPrefix: isLive ? "Pause failed" : "Resume failed",
                  after: () => renderClientCommandCenter({ id: clientId }),
                },
              ),
          },
          isLive ? "Pause" : "Resume",
        ),
        // "Remove" is two different intentions and conflating them loses work:
        // archiving stops it running; untagging only stops attributing it to
        // this client and leaves it running for the owner.
        el(
          "button",
          {
            class: "btn btn-ghost",
            style: "margin-left:6px;",
            onclick: () => _ccRemoveModal(a, clientId),
          },
          "Remove",
        ),
      ]),
    ]) {
      if (cell) tr.appendChild(cell);
    }
    tbody.appendChild(tr);
  }
  tbl.appendChild(tbody);
  _ccWireDrag(tbody, clientId);
  return el("div", {}, [
    _ccStateFilterBar(rows, tbody),
    el(
      "div",
      {
        style:
          "display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:8px;",
      },
      [
        el(
          "div",
          { class: "muted" },
          // Deliberately NOT "the order they run in". Budget arbitration was
          // reverted (it conferred no real priority), so this order controls how
          // the list is presented and nothing else — each automation still fires
          // on its own cron. Saying otherwise would promise scheduling power the
          // product does not have.
          "Drag a row (or use ↑ ↓) to set the order these are listed in — saved immediately. This does not change when they run; each still fires on its own schedule.",
        ),
        el(
          "button",
          { class: "btn btn-primary", onclick: () => _ccAddAutomationModal(clientId) },
          "+ Add automation",
        ),
      ],
    ),
    el("div", { class: "card" }, [tbl]),
  ]);
}

/**
 * Remove-from-client, with the two intentions kept separate. The wording is the
 * feature here: "remove" alone would leave the operator guessing whether the
 * automation stopped or merely stopped being attributed.
 */
function _ccRemoveModal(a, clientId) {
  openModal({
    title: `Remove "${a.name || a.id}" from this client?`,
    body: el("div", {}, [
      el(
        "div",
        { class: "muted", style: "margin-bottom:10px;" },
        "These do different things. Pick the one you mean:",
      ),
      el("div", { style: "margin-bottom:8px;" }, [
        el("strong", {}, "Stop it running"),
        el(
          "div",
          { class: "muted" },
          "Archives the automation. It stops sending for everyone, not just this client.",
        ),
      ]),
      el("div", {}, [
        el("strong", {}, "Stop attributing it"),
        el(
          "div",
          { class: "muted" },
          "Keeps it running, but it leaves this client's console and report.",
        ),
      ]),
    ]),
    confirmLabel: "Stop attributing it",
    onConfirm: async () => {
      // The read-model's kind names are NOT the work-tagging API's kind names:
      // campaigns-model emits workflow/email, WORK_TABLES expects
      // custom_workflow/email_campaign. Sending the raw prefix 400s with
      // "unknown_kind", so untagging was impossible for those two kinds.
      const READ_KIND_TO_WORK_KIND = {
        workflow: "custom_workflow",
        email: "email_campaign",
        composition: "composition",
        boost: "boost",
        campaign_workflow: "campaign_workflow",
      };
      const [rawKind, refId] = String(a.id).split(":");
      const kind = READ_KIND_TO_WORK_KIND[rawKind];
      if (!kind) {
        toast(`Don't know how to untag a "${rawKind}"`, "err");
        return false;
      }
      const r = await api("work-tag-client", {
        kind,
        workId: refId,
        body: { client_id: null },
      }).catch((e) => ({ error: e?.message || "failed" }));
      if (r?.error) {
        toast(`${r.error}`, "err");
        return false;
      }
      toast("Untagged — it keeps running, just not under this client", "ok");
      renderClientCommandCenter({ id: clientId });
      return true;
    },
  });
}

/**
 * Replies to the campaign — the only tab showing work waiting on a HUMAN.
 *
 * The pitch going into hundreds of groups ends with "DM @Onchainbureau and I
 * will send the requirements". Until now a reply to that landed nowhere and
 * appeared on no screen, so the product could manufacture demand and not notice
 * it arriving. Unanswered sorts first and is coloured, because the failure mode
 * here is not a wrong number — it is a real person waiting on an answer.
 */
function _ccRepliesTab(data, clientId) {
  const inbound = data.inbound || { unanswered: 0, rows: [] };
  const rows = inbound.rows || [];
  if (!rows.length) {
    return el("div", { class: "empty-state" }, [
      el("div", { class: "empty-state-icon" }, icon("message")),
      el("div", { class: "empty-state-title" }, "No replies yet"),
      el(
        "div",
        { class: "empty-state-hint" },
        "Inbound DMs to this client's accounts land here the moment they arrive, and you get a Telegram ping. Nothing is auto-answered — you write the reply.",
      ),
    ]);
  }

  const tbl = el("table", { class: "table" });
  tbl.appendChild(
    el("thead", {}, [
      el("tr", {}, [
        el("th", {}, "From"),
        el("th", {}, "Message"),
        el("th", {}, "When"),
        el("th", { style: "text-align:right;" }, ""),
      ]),
    ]),
  );
  const tb = el("tbody", {});
  // Unanswered first: this is a queue, not a log.
  const ordered = [...rows].sort(
    (a, b) => (a.status === "new" ? -1 : 0) - (b.status === "new" ? -1 : 0),
  );
  for (const r of ordered) {
    const isNew = r.status === "new";
    const who = r.peer_username ? "@" + r.peer_username : r.peer_name || "id " + r.peer_id;
    tb.appendChild(
      el("tr", {}, [
        el("td", {}, [
          el("div", { style: isNew ? "font-weight:600;" : "" }, who),
          el(
            "div",
            { class: "muted", style: "font-size:11px;" },
            r.contact_id ? "in your CRM" : "not in CRM",
          ),
        ]),
        el("td", {}, [el("div", { style: "white-space:pre-wrap;" }, r.content || "")]),
        el("td", { class: "muted", title: r.received_at || "" }, _ccAgo(r.received_at)),
        el("td", { style: "text-align:right;white-space:nowrap;" }, [
          isNew
            ? el(
                "span",
                {
                  class: "badge",
                  style: "color:var(--status-warn);border-color:var(--status-warn);",
                },
                "UNANSWERED",
              )
            : el("span", { class: "muted" }, String(r.status || "").toUpperCase()),
          el(
            "button",
            {
              class: "btn btn-ghost",
              style: "margin-left:8px;",
              onclick: (ev) =>
                mutateAction(
                  ev,
                  "inbound-dm-status",
                  { id: r.id, body: { status: isNew ? "answered" : "new" } },
                  {
                    success: isNew ? "Marked answered" : "Reopened",
                    failPrefix: "Update failed",
                    after: () => renderClientCommandCenter({ id: clientId }),
                  },
                ),
            },
            isNew ? "Mark answered" : "Reopen",
          ),
        ]),
      ]),
    );
  }
  tbl.appendChild(tb);

  return el("div", {}, [
    inbound.unanswered
      ? el(
          "div",
          {
            class: "card",
            style: "border-color:var(--status-warn);margin-bottom:12px;padding:10px;",
          },
          [
            el(
              "div",
              { style: "color:var(--status-warn);font-weight:600;" },
              `${inbound.unanswered} unanswered ${inbound.unanswered === 1 ? "reply" : "replies"}`,
            ),
            el(
              "div",
              { class: "muted", style: "margin-top:4px;" },
              "These are real people who responded to the campaign. Nothing replies on your behalf.",
            ),
          ],
        )
      : null,
    el("div", { class: "card" }, [tbl]),
  ]);
}

function _ccActivityTab(data) {
  const act = data.activity || { scope: "owner", rows: [] };
  const rows = act.rows || [];
  const scopeNote =
    act.scope === "client"
      ? "Activity for this client's contacts."
      : "Showing your recent activity across ALL clients — this client has no company mapping yet, so its own feed cannot be isolated.";
  const head = el("div", { class: "muted", style: "margin-bottom:8px;" }, scopeNote);
  if (!rows.length) {
    return el("div", {}, [
      head,
      el("div", { class: "empty-state" }, [
        el("div", { class: "empty-state-icon" }, icon("signal")),
        el("div", { class: "empty-state-title" }, "No activity recorded yet"),
      ]),
    ]);
  }
  const tbl = el("table", { class: "table" });
  tbl.appendChild(
    el("thead", {}, [
      el("tr", {}, [
        el("th", {}, "When"),
        el("th", {}, "Channel"),
        el("th", {}, "Action"),
        el("th", {}, "Status"),
        el("th", {}, "Summary"),
      ]),
    ]),
  );
  const tbody = el("tbody", {});
  for (const r of rows) {
    tbody.appendChild(
      el("tr", {}, [
        el("td", { class: "muted" }, _ccAgo(r.occurred_at)),
        el("td", {}, r.channel || "—"),
        el("td", {}, r.action || "—"),
        el(
          "td",
          {
            style: `color:${r.status === "ok" ? "var(--status-ok)" : r.status ? "var(--status-bad)" : "inherit"};`,
          },
          r.status || "—",
        ),
        el("td", { class: "muted" }, r.summary || ""),
      ]),
    );
  }
  tbl.appendChild(tbody);
  return el("div", {}, [head, el("div", { class: "card" }, [tbl])]);
}

/** Status → the shared colour grammar. */
function _ccLimitTone(status) {
  if (status === "restricted") return "var(--status-bad)";
  if (status === "spacing" || status === "warming" || status === "in_flight")
    return "var(--status-warn)";
  if (status === "unavailable" || status === "unknown") return "var(--text-dim, #9aa4b2)";
  return "var(--status-ok)";
}

/** "3/20" style consumption for the action types that actually have caps. */
function _ccCapsSummary(row) {
  if (!row.caps) return el("span", { class: "muted" }, "—");
  const used = row.usedToday || {};
  // Only the types with real consumption today, plus the always-interesting
  // ones — a full 13-row dump per account is noise on a client page.
  const keys = Object.keys(row.caps)
    .filter((k) => (used[k] || 0) > 0 || ["dm_send", "group_message"].includes(k))
    .slice(0, 5);
  if (!keys.length) return el("span", { class: "muted" }, "nothing used today");
  const wrap = el("span", { style: "display:inline-flex;gap:10px;flex-wrap:wrap;" });
  for (const k of keys) {
    const u = used[k] || 0;
    const cap = row.caps[k];
    const pct = cap ? u / cap : 0;
    wrap.appendChild(
      el(
        "span",
        {
          title: k,
          style: `color:${pct >= 1 ? "var(--status-bad)" : pct >= 0.8 ? "var(--status-warn)" : "inherit"};`,
        },
        `${k.replace(/_/g, " ")} ${u}/${cap}`,
      ),
    );
  }
  return wrap;
}

function _ccOwnerLimitsCard(owner) {
  if (!owner) return null;
  const kids = [
    el("div", { class: "card-title" }, "Owner-level limits"),
    // Said plainly: these are NOT this client's budget. Presenting a shared
    // quota as if it belonged to one client is how an operator over-commits.
    el("div", { class: "muted", style: "margin:6px 0 10px;" }, owner.note || ""),
  ];
  const b = owner.dailyBudget;
  if (b) {
    const pct = b.limit ? b.used / b.limit : 0;
    kids.push(
      el(
        "div",
        {
          style: `margin-bottom:8px;color:${pct >= 1 ? "var(--status-bad)" : pct >= 0.8 ? "var(--status-warn)" : "inherit"};`,
        },
        `Daily budget: ${b.used}/${b.limit} used (${b.remaining} left, tier ${b.tier})${b.reason ? ` — ${b.reason}` : ""}`,
      ),
    );
  }
  const li = owner.linkedin;
  if (li && typeof li === "object") {
    // Field names verified against the live endpoint: getLinkedInBudget emits
    // todayCount/dailyLimit/weekCount/weekLimit per category — NOT count/limit.
    // Filtering on the wrong key silently rendered no LinkedIn table at all.
    const cats = Object.entries(li).filter(
      ([, v]) => v && typeof v === "object" && "dailyLimit" in v,
    );
    if (cats.length) {
      const tbl = el("table", { class: "table" });
      tbl.appendChild(
        el("thead", {}, [
          el("tr", {}, [
            el("th", {}, "LinkedIn"),
            el("th", {}, "Today"),
            el("th", {}, "This week"),
            el("th", {}, "Blocked"),
          ]),
        ]),
      );
      const tb = el("tbody", {});
      for (const [cat, v] of cats) {
        const pct = v.dailyLimit ? (v.todayCount || 0) / v.dailyLimit : 0;
        tb.appendChild(
          el("tr", {}, [
            el("td", {}, cat.replace(/_/g, " ")),
            el(
              "td",
              {
                style: `color:${pct >= 1 ? "var(--status-bad)" : pct >= 0.8 ? "var(--status-warn)" : "inherit"};`,
              },
              `${v.todayCount ?? 0}/${v.dailyLimit ?? "—"}`,
            ),
            el("td", {}, v.weekLimit ? `${v.weekCount ?? 0}/${v.weekLimit}` : "—"),
            el(
              "td",
              { style: v.blockedReason ? "color:var(--status-bad);" : "" },
              v.blockedReason ||
                (v.allowed === false ? "blocked" : v.quietHours ? "quiet hours" : "—"),
            ),
          ]),
        );
      }
      tbl.appendChild(tb);
      kids.push(tbl);
    }
  }
  return el("div", { class: "card", style: "margin-top:12px;" }, kids);
}

async function _ccRenderLimitsInto(container, clientId) {
  const data = await api("client-limits", { id: clientId }).catch((e) => ({
    error: e?.message || "unreachable",
  }));
  container.innerHTML = "";
  if (data?.error) {
    container.appendChild(el("div", { class: "muted" }, `Could not load limits: ${data.error}`));
    return;
  }
  const deg = _ccDegradedBanner(data.degraded);
  if (deg) container.appendChild(deg);

  const rows = data.accounts || [];
  if (rows.length) {
    const tbl = el("table", { class: "table" });
    tbl.appendChild(
      el("thead", {}, [
        el("tr", {}, [
          el("th", {}, "Account"),
          el("th", {}, "State"),
          el("th", {}, "Used today"),
          el("th", {}, "Why"),
        ]),
      ]),
    );
    const tb = el("tbody", {});
    for (const r of rows) {
      tb.appendChild(
        el("tr", {}, [
          el("td", {}, [
            el("div", { style: "font-weight:600;" }, r.label || `#${r.accountId}`),
            el("div", { class: "muted" }, r.platform),
          ]),
          el("td", { style: `color:${_ccLimitTone(r.status)};` }, r.status),
          el("td", {}, _ccCapsSummary(r)),
          el("td", { class: "muted" }, r.reason || "—"),
        ]),
      );
    }
    tbl.appendChild(tb);
    container.appendChild(el("div", { class: "card" }, [tbl]));
  }
  const ownerCard = _ccOwnerLimitsCard(data.owner);
  if (ownerCard) container.appendChild(ownerCard);
}

function _ccAccountsTab(data) {
  const accounts = data.accounts || [];
  if (!accounts.length) {
    // No senders assigned, but the OWNER-level budget still gates this client's
    // sends — so the limits box loads here too rather than leaving the operator
    // to believe nothing constrains them.
    const emptyLimits = el("div", { style: "margin-top:12px;" });
    _ccRenderLimitsInto(emptyLimits, data.client?.id || dashboardState.currentParams?.id);
    return el("div", {}, [
      el("div", { class: "empty-state" }, [
        el("div", { class: "empty-state-icon" }, icon("link")),
        el("div", { class: "empty-state-title" }, "No accounts assigned to this client"),
        el(
          "div",
          { class: "empty-state-hint" },
          "Assign the accounts this client's work sends from, on the Clients page. Owner-level limits below still apply.",
        ),
      ]),
      emptyLimits,
    ]);
  }
  const tbl = el("table", { class: "table" });
  tbl.appendChild(
    el("thead", {}, [
      el("tr", {}, [
        el("th", {}, "Platform"),
        el("th", {}, "Account"),
        el("th", {}, "Status"),
        el("th", {}, "Health"),
      ]),
    ]),
  );
  const tbody = el("tbody", {});
  for (const a of accounts) {
    tbody.appendChild(
      el("tr", {}, [
        el("td", {}, a.platform),
        el("td", {}, a.identifier || a.label || `#${a.accountId}`),
        el("td", {}, a.status || "—"),
        el("td", { class: "muted" }, a.health == null ? "—" : String(a.health)),
      ]),
    );
  }
  tbl.appendChild(tbody);
  // Limits load separately: four independent authorities, one of which
  // (LinkedIn) counts live off the action log, so keeping it out of the console
  // payload keeps the main page fast.
  const limitsBox = el("div", { style: "margin-top:12px;" }, [
    el("div", { class: "muted" }, "Loading rate limits…"),
  ]);
  _ccRenderLimitsInto(limitsBox, data.client?.id || dashboardState.currentParams?.id);
  return el("div", {}, [el("div", { class: "card" }, [tbl]), limitsBox]);
}

function _ccSettingsTab(client, clientId) {
  const nameInput = el("input", {
    class: "input",
    type: "text",
    value: client?.name || "",
    style: "width:100%;max-width:420px;",
  });
  return el("div", { class: "card" }, [
    el("div", { class: "card-title" }, "Client"),
    el("div", { class: "muted", style: "margin:8px 0 4px;" }, "Name"),
    nameInput,
    el("div", { style: "margin-top:14px;" }, [
      el(
        "button",
        {
          class: "btn btn-primary",
          onclick: (ev) => {
            const name = nameInput.value.trim();
            if (!name) {
              toast("Name is required", "err");
              return;
            }
            mutateAction(
              ev,
              "client-update",
              { id: clientId, body: { name } },
              {
                success: "Saved",
                failPrefix: "Save failed",
                after: () => renderClientCommandCenter({ id: clientId }),
              },
            );
          },
        },
        "Save",
      ),
      el(
        "button",
        {
          class: "btn btn-ghost",
          style: "margin-left:8px;",
          onclick: () => navigate("clients"),
        },
        "Back to all clients",
      ),
    ]),
  ]);
}

async function renderClientCommandCenter(params) {
  const clientId = params?.id || dashboardState.currentParams?.id;
  if (!clientId) {
    navigate("clients");
    return;
  }
  showLoading("Loading client…");
  const data = await api("client-console", { id: clientId }).catch((e) => ({
    error: e?.message || "unreachable",
  }));
  if (data?.error) {
    showError(data.error, () => renderClientCommandCenter({ id: clientId }));
    return;
  }

  const client = data.client || { id: clientId, name: clientId };
  const stats = data.stats || {};
  // Tab selection is sticky WITHIN one client but resets when you open a
  // different one: landing on another customer's Settings tab because that is
  // where you happened to leave the previous customer is disorienting, and it
  // hides the Automations list that is the point of the page.
  if (dashboardState._ccTabClientId !== clientId) {
    dashboardState._ccTab = "automations";
    dashboardState._ccTabClientId = clientId;
  }
  const tab = dashboardState.currentParams?.tab || dashboardState._ccTab || "automations";
  dashboardState._ccTab = tab;

  // Build into a detached node and hand it to setContentNode(), which owns
  // the real #content container. There is no #main element in the page.
  const main = el("div", {});

  main.appendChild(
    el("div", { class: "page-header" }, [
      el("div", {}, [
        el("div", { class: "page-title" }, client.name || "Client"),
        el(
          "div",
          { class: "page-subtitle" },
          "Everything running for this customer, and what it produced.",
        ),
      ]),
      el("div", {}, [
        el(
          "button",
          { class: "btn btn-ghost", onclick: () => navigate(`client-report/${clientId}`) },
          "Open report",
        ),
        el(
          "button",
          { class: "btn btn-ghost", style: "margin-left:8px;", onclick: () => navigate("clients") },
          "← All clients",
        ),
      ]),
    ]),
  );

  const deg = _ccDegradedBanner(data.degraded);
  if (deg) main.appendChild(deg);

  main.appendChild(
    el("div", { class: "stats-grid" }, [
      statCard("Active automations", stats.activeAutomations ?? 0, "running now"),
      statCard("Actions today", stats.actionsToday ?? 0, "sent in the last 24h"),
      statCard("Replies (7d)", stats.replies7d ?? 0, "last 7 days"),
      statCard("Accounts", (data.accounts || []).length, "assigned senders"),
    ]),
  );

  const tabRow = el("div", { class: "tabs", style: "margin:14px 0;" });
  for (const t of CC_TABS) {
    tabRow.appendChild(
      el(
        "button",
        {
          class: `tab${tab === t.key ? " active" : ""}`,
          onclick: () => {
            dashboardState._ccTab = t.key;
            renderClientCommandCenter({ id: clientId });
          },
        },
        t.label,
      ),
    );
  }
  main.appendChild(tabRow);

  const body = el("div", {});
  if (tab === "automations") body.appendChild(_ccAutomationsTab(data, clientId));
  else if (tab === "replies") body.appendChild(_ccRepliesTab(data, clientId));
  else if (tab === "activity") body.appendChild(_ccActivityTab(data));
  else if (tab === "accounts") body.appendChild(_ccAccountsTab(data));
  else if (tab === "settings") body.appendChild(_ccSettingsTab(client, clientId));
  else if (tab === "results") {
    // The report is a full renderer of its own; don't duplicate it here.
    body.appendChild(
      el("div", { class: "card" }, [
        el("div", { class: "card-title" }, "Client report"),
        el(
          "div",
          { class: "muted", style: "margin:8px 0 12px;" },
          "The full 'everything we did for you' report — branded, shareable, and printable.",
        ),
        el(
          "button",
          { class: "btn btn-primary", onclick: () => navigate(`client-report/${clientId}`) },
          "Open report",
        ),
      ]),
    );
  }
  main.appendChild(body);
  setContentNode(main);
}
