// dashboard-operations-center-phase-2-ia-r.js — extracted verbatim from dashboard.js (pure move).

// ── OPERATIONS CENTER (Phase 2 IA — replaces wizard + picker) ──────
//
// Single catalog of all operations the system can perform, organized by
// filterable type lenses (multi-account / passive / single-platform / multi-step
// / cross-platform). Account selection is a first-class first step in the
// launch flow. All other launchers (wizard, + New menu, CRM bulk, ⌘K, picker)
// redirect here.
//
// State shape (closure-local per render):
//   { filters: Set<tag>, search: string }       ← catalog filter state
//   { opId, step, accountIds[], config{}, name } ← detail flow state

const OC_FILTER_LABELS = {
  "multi-account": { icon: "👥", label: "Multi-account" },
  passive: { icon: "♾", label: "Passive" },
  "single-platform": { icon: "🎯", label: "Single-platform" },
  "multi-step": { icon: "🔄", label: "Multi-step" },
  "cross-platform": { icon: "🌐", label: "Cross-platform" },
};

const OC_PLATFORM_LABEL = {
  twitter: "X / Twitter",
  linkedin: "LinkedIn",
  telegram: "Telegram",
  email: "Email",
};

async function renderOperationsCenter() {
  // Catalog page state — survives within this render only.
  const state = {
    filters: new Set(), // type-tag strings (multi-account, passive, etc.)
    platform: "all", // single platform filter
    search: "",
    recentOpIds: [], // top 6 most-recent unique ops (from telemetry)
    pinnedOpIds: new Set(), // user's starred ops (persisted)
    lastRunByOpId: new Map(), // opId → timestamp of last user-launched run
    viewMode: "grid", // "grid" | "list" — persisted preference
  };

  // Single storage read hydrates all client-side preferences in one call.
  try {
    const stored = await chrome.storage.local.get([
      "_telemetryLog",
      "_pinnedOps",
      "_opsCenterView",
    ]);
    const log = stored._telemetryLog || [];

    // Recently Used + Last-Run: walk telemetry newest → oldest in a single pass.
    // - card_click events feed Recently Used (top 6 unique opIds)
    // - launch events feed lastRunByOpId (latest ts per opId)
    const seenRecent = new Set();
    for (let i = log.length - 1; i >= 0; i--) {
      const e = log[i];
      if (e?.category !== "operations") continue;
      if (
        e.action === "card_click" &&
        e.opId &&
        !seenRecent.has(e.opId) &&
        state.recentOpIds.length < 6
      ) {
        if (findOperation(e.opId)) {
          // skip ops that no longer exist
          seenRecent.add(e.opId);
          state.recentOpIds.push(e.opId);
        }
      }
      if (e.action === "launch" && e.opId && !state.lastRunByOpId.has(e.opId)) {
        state.lastRunByOpId.set(e.opId, e.ts); // first occurrence walking backwards = latest
      }
    }

    // Pinned ops + view mode preference
    if (Array.isArray(stored._pinnedOps)) {
      for (const id of stored._pinnedOps) state.pinnedOpIds.add(id);
    }
    if (stored._opsCenterView === "list" || stored._opsCenterView === "grid") {
      state.viewMode = stored._opsCenterView;
    }
  } catch {
    /* hydration is best-effort — defaults are fine */
  }

  // Hydrate live state used to badge cards. Both calls go through apiCached so
  // they hit the 30s/60s caches when warm. Failure leaves the maps empty —
  // cards just don't show "Running" / "Needs setup" badges in that case.
  state.activeCountByOpId = new Map(); // opId → count of active workflows
  state.accountCountByPlatform = {}; // platform → number of connected accounts
  try {
    const [workflows, accounts] = await Promise.all([
      apiCached("workflows-list").catch(() => ({})),
      apiCached("accounts-all").catch(() => ({})),
    ]);
    // Tally active workflows by workflow_type (templateKey). Each templateKey
    // matches exactly one OPERATIONS entry (the catalog uses workflow_type as
    // the canonical opId for templated ops).
    for (const w of workflows?.workflows || []) {
      if (w.status !== "active") continue;
      const op = findOperation(w.workflow_type);
      if (!op) continue;
      state.activeCountByOpId.set(op.id, (state.activeCountByOpId.get(op.id) || 0) + 1);
    }
    // Per-platform connected-account counts. The "needs setup" check below uses
    // these to dim cards whose required platforms have zero accounts.
    state.accountCountByPlatform.twitter = (accounts?.twitter || []).length;
    state.accountCountByPlatform.linkedin = (accounts?.linkedin || []).length;
    state.accountCountByPlatform.telegram = (accounts?.telegram || []).length;
    state.accountCountByPlatform.email = (accounts?.smtp || []).length; // backend key is "smtp"
  } catch {
    /* leave both empty — cards still render without badges */
  }

  // Returns null when an op is fully ready, or { reason, platform } when at least
  // one of its required platforms has zero connected accounts. accountMode==="none"
  // ops (signal pursue, etc.) are always ready.
  function checkSetup(op) {
    if (op.accountMode === "none" || op.platforms.length === 0) return null;
    for (const p of op.platforms) {
      if ((state.accountCountByPlatform[p] || 0) === 0) {
        return { reason: "missing_account", platform: p };
      }
    }
    return null;
  }

  // Pin/unpin a single op. Persists to chrome.storage.local immediately so the
  // next render picks it up. Re-renders the catalog so the Pinned section
  // updates in place without a full reload.
  async function togglePin(opId) {
    if (state.pinnedOpIds.has(opId)) state.pinnedOpIds.delete(opId);
    else state.pinnedOpIds.add(opId);
    try {
      await chrome.storage.local.set({ _pinnedOps: [...state.pinnedOpIds] });
    } catch {}
    logTelemetry("operations", "pin_toggle", { opId, pinned: state.pinnedOpIds.has(opId) });
    render();
  }

  // Persist view mode (grid/list) and re-render.
  async function setViewMode(mode) {
    if (mode !== "grid" && mode !== "list") return;
    if (state.viewMode === mode) return;
    state.viewMode = mode;
    try {
      await chrome.storage.local.set({ _opsCenterView: mode });
    } catch {}
    render();
  }

  // Format a timestamp as "X ago" — "12m", "3h", "2d", "5w", "never" for null.
  // Compact because the card already shows a lot of metadata.
  function fmtRelativeTime(ts) {
    if (!ts) return null;
    const diff = Date.now() - ts;
    if (diff < 60_000) return "just now";
    if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m ago`;
    if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h ago`;
    if (diff < 604_800_000) return `${Math.round(diff / 86_400_000)}d ago`;
    return `${Math.round(diff / 604_800_000)}w ago`;
  }

  // Compute counts per type-tag and per-platform — used to badge filter chips.
  // Computed once at render-time off the FULL OPERATIONS list (not filtered)
  // so chip counts are stable as the user toggles filters.
  function computeTagCount(tag) {
    return OPERATIONS.filter((o) => o.tags.includes(tag)).length;
  }
  function computePlatformCount(platform) {
    if (platform === "all") return OPERATIONS.length;
    return OPERATIONS.filter((o) => o.platforms.includes(platform)).length;
  }

  function visibleOps() {
    let ops = OPERATIONS;
    if (state.filters.size > 0) {
      ops = ops.filter((o) => o.tags.some((t) => state.filters.has(t)));
    }
    if (state.platform !== "all") {
      ops = ops.filter((o) => o.platforms.includes(state.platform));
    }
    if (state.search) {
      const q = state.search.toLowerCase();
      ops = ops.filter(
        (o) =>
          o.label.toLowerCase().includes(q) ||
          (o.description || "").toLowerCase().includes(q) ||
          o.tags.some((t) => t.includes(q)),
      );
    }
    return ops;
  }

  function groupByPrimaryTag(ops) {
    // Sort ops into buckets by their FIRST tag (primary type). Untagged ops
    // (e.g., import_csv) go into "Other". Buckets render in OPERATION_TAGS order
    // so the user sees a stable layout.
    const buckets = new Map();
    for (const tag of OPERATION_TAGS) buckets.set(tag, []);
    buckets.set("other", []);
    for (const o of ops) {
      const primary = o.tags[0];
      if (primary && buckets.has(primary)) buckets.get(primary).push(o);
      else buckets.get("other").push(o);
    }
    return buckets;
  }

  function render() {
    logTelemetry("operations", "catalog_render", {
      filterCount: state.filters.size,
      platform: state.platform,
      searchLen: state.search.length,
    });

    // Type-tag filter chips with counts. Chip is dimmed when its bucket has 0
    // matches under the CURRENT other filters — purely informative; you can
    // still click to add it.
    const filterChips = el("div", { class: "op-filter-row" }, [
      el("span", { class: "op-filter-row-label" }, "Type"),
      ...OPERATION_TAGS.map((tag) => {
        const meta = OC_FILTER_LABELS[tag];
        const active = state.filters.has(tag);
        const count = computeTagCount(tag);
        return el(
          "button",
          {
            type: "button",
            class: `op-filter-chip${active ? " active" : ""}`,
            onclick: () => {
              if (active) state.filters.delete(tag);
              else state.filters.add(tag);
              render();
            },
          },
          `${meta.icon} ${meta.label} · ${count}`,
        );
      }),
      state.filters.size > 0
        ? el(
            "button",
            {
              type: "button",
              class: "op-filter-chip op-filter-clear",
              onclick: () => {
                state.filters.clear();
                render();
              },
            },
            "✕ Clear",
          )
        : null,
    ]);

    // Platform filter row — single-select, "all" is the default. Lets a user
    // ask "what can I do with my LinkedIn accounts?" without scanning tags.
    const platformChips = el("div", { class: "op-filter-row" }, [
      el("span", { class: "op-filter-row-label" }, "Platform"),
      ...["all", "twitter", "linkedin", "telegram", "email"].map((p) => {
        const active = state.platform === p;
        const label = p === "all" ? "All" : OC_PLATFORM_LABEL[p] || p;
        const count = computePlatformCount(p);
        return el(
          "button",
          {
            type: "button",
            class: `op-filter-chip${active ? " active" : ""}`,
            onclick: () => {
              state.platform = p;
              render();
            },
          },
          `${label} · ${count}`,
        );
      }),
    ]);

    const searchBox = el("input", {
      type: "text",
      class: "op-search-input",
      id: "op-search-input",
      placeholder: "Search operations…",
      value: state.search,
      oninput: (e) => {
        state.search = e.target.value;
        // Debounce the re-render slightly so typing feels smooth.
        clearTimeout(render._searchTimer);
        render._searchTimer = setTimeout(render, 120);
      },
    });

    const ops = visibleOps();
    const buckets = groupByPrimaryTag(ops);
    // Grid class adapts to view-mode preference. List view uses a 1-column
    // dense layout; grid is the default 240px-min auto-fill.
    const gridClass = state.viewMode === "list" ? "op-grid op-grid--list" : "op-grid";

    // "Pinned" + "Recently Used" — only shown on the unfiltered catalog so they
    // don't compete with active filter results. Pinned takes priority position.
    const isUnfiltered = state.filters.size === 0 && state.platform === "all" && !state.search;
    const pinnedOps = isUnfiltered
      ? [...state.pinnedOpIds].map((id) => findOperation(id)).filter(Boolean)
      : [];
    const pinnedSet = new Set(pinnedOps.map((o) => o.id));
    // Recently Used: exclude anything already shown as pinned to avoid duplication.
    const recentOps = isUnfiltered
      ? state.recentOpIds
          .map((id) => findOperation(id))
          .filter(Boolean)
          .filter((o) => !pinnedSet.has(o.id))
      : [];

    const sections = [];
    if (pinnedOps.length > 0) {
      sections.push(
        el("div", { class: "op-bucket op-bucket-pinned" }, [
          el("div", { class: "op-bucket-header" }, `★ Pinned · ${pinnedOps.length}`),
          el("div", { class: gridClass }, pinnedOps.map(opCard)),
        ]),
      );
    }
    if (recentOps.length > 0) {
      sections.push(
        el("div", { class: "op-bucket op-bucket-recent" }, [
          el("div", { class: "op-bucket-header" }, `🕘 Recently Used`),
          el("div", { class: gridClass }, recentOps.map(opCard)),
        ]),
      );
    }
    for (const [bucketTag, bucketOps] of buckets) {
      if (bucketOps.length === 0) continue;
      const meta = OC_FILTER_LABELS[bucketTag] || { icon: "•", label: "Other" };
      sections.push(
        el("div", { class: "op-bucket" }, [
          el(
            "div",
            { class: "op-bucket-header" },
            `${meta.icon} ${meta.label} · ${bucketOps.length}`,
          ),
          el("div", { class: gridClass }, bucketOps.map(opCard)),
        ]),
      );
    }

    // View-mode toggle in the page header. Two icon-buttons that swap classes
    // on the grids (no full reload). Persisted to chrome.storage.local so the
    // user's preference survives across navigations.
    const viewToggle = el("div", { class: "op-view-toggle" }, [
      el(
        "button",
        {
          type: "button",
          class: `op-view-btn${state.viewMode === "grid" ? " active" : ""}`,
          title: "Grid view",
          "aria-label": "Grid view",
          onclick: () => setViewMode("grid"),
        },
        "▦",
      ),
      el(
        "button",
        {
          type: "button",
          class: `op-view-btn${state.viewMode === "list" ? " active" : ""}`,
          title: "List view",
          "aria-label": "List view",
          onclick: () => setViewMode("list"),
        },
        "☰",
      ),
    ]);

    setContentNode(
      el("div", { class: "operations-center" }, [
        el("div", { class: "page-header" }, [
          el("div", {}, [
            el("div", { class: "page-title" }, "Operations Center"),
            el(
              "div",
              { class: "page-subtitle" },
              `${OPERATIONS.length} operations · pick what to do, then choose accounts.`,
            ),
          ]),
          viewToggle,
        ]),
        searchBox,
        filterChips,
        platformChips,
        ops.length === 0
          ? el("div", { class: "op-empty" }, [
              el("div", { class: "op-empty-icon" }, "🔍"),
              el("div", null, "No operations match your filters."),
              el(
                "button",
                {
                  type: "button",
                  class: "btn btn-tiny btn-secondary",
                  onclick: () => {
                    state.filters.clear();
                    state.platform = "all";
                    state.search = "";
                    render();
                  },
                },
                "Clear filters",
              ),
            ])
          : el("div", { class: "op-sections" }, sections),
      ]),
    );

    // Auto-focus search ONLY on initial mount. Subsequent re-renders (filter
    // toggle, search debounce) preserve focus naturally because the input is
    // re-mounted with its value intact — the value comes from state.search,
    // and the cursor lives at the end where most typing happens. Flagging
    // _focused stops us from stealing focus if the user has clicked elsewhere
    // (e.g., onto a chip).
    if (!render._focused) {
      render._focused = true;
      setTimeout(() => {
        const input = document.getElementById("op-search-input");
        if (input) {
          input.focus();
          input.setSelectionRange(input.value.length, input.value.length);
        }
      }, 0);
    }
  }

  function opCard(op) {
    const tagEls = op.tags.map((t) => el("span", { class: `op-tag op-tag-${t}` }, t));
    const platformEls =
      op.platforms.length > 0
        ? el(
            "div",
            { class: "op-platforms" },
            op.platforms.map((p) =>
              el("span", { class: "op-platform-pill" }, OC_PLATFORM_LABEL[p] || p),
            ),
          )
        : null;

    const isPinned = state.pinnedOpIds.has(op.id);
    // Star button is overlaid on the card. stopPropagation prevents the card's
    // own onclick from firing when the user clicks the star.
    const pinBtn = el(
      "button",
      {
        type: "button",
        class: `op-card-pin${isPinned ? " pinned" : ""}`,
        title: isPinned ? "Unpin" : "Pin to top",
        "aria-label": isPinned ? `Unpin ${op.label}` : `Pin ${op.label}`,
        onclick: (e) => {
          e.stopPropagation();
          togglePin(op.id);
        },
      },
      isPinned ? "★" : "☆",
    );

    // Preview badge — operations whose schema exists but engine isn't wired.
    // Lets the user see the form + save config, but they shouldn't expect
    // automatic execution. Hidden when activeCount > 0 (running supersedes).
    const isStub = isOperationStub(op.id);
    const previewBadge =
      isStub && activeCount === 0
        ? el(
            "div",
            {
              class: "op-card-preview",
              title:
                "Configurable now, but the periodic engine for this type isn't wired yet — saved configs won't auto-execute until it ships.",
              style:
                "position:absolute;top:8px;left:8px;background:#3b3635;border:1px solid #6b5b4a;color:#e8c896;font-size:9px;font-weight:600;letter-spacing:0.05em;padding:2px 6px;border-radius:3px;text-transform:uppercase;z-index:1;",
            },
            "Preview",
          )
        : null;

    // ── Live state (right-side badges + footer) ──
    // "● N running" badge — only for ops that have active workflows in the
    // backend right now. Lets the catalog double as a status board so the user
    // sees what's already in flight before launching another.
    const activeCount = state.activeCountByOpId.get(op.id) || 0;
    const runningBadge =
      activeCount > 0
        ? el(
            "div",
            {
              class: "op-card-running",
              title: `${activeCount} workflow${activeCount === 1 ? "" : "s"} active`,
            },
            `● ${activeCount} running`,
          )
        : null;

    // Last-run footer — only shown when telemetry has a recorded launch.
    const lastRunTs = state.lastRunByOpId.get(op.id);
    const lastRunEl = lastRunTs
      ? el(
          "div",
          { class: "op-card-lastrun", title: new Date(lastRunTs).toLocaleString() },
          `🕘 Ran ${fmtRelativeTime(lastRunTs)}`,
        )
      : null;

    // ── Needs-setup state ──
    // When required platforms have no connected accounts, the card becomes
    // disabled-looking and the click target switches to the Accounts page so
    // the user can fix the prerequisite. They can still pin the card.
    const setup = checkSetup(op);
    const needsSetup = setup !== null;
    const platformLabel = needsSetup ? OC_PLATFORM_LABEL[setup.platform] || setup.platform : "";
    const setupBanner = needsSetup
      ? el("div", { class: "op-card-needs-setup" }, [
          el("span", null, `Connect ${platformLabel} to use`),
          el(
            "button",
            {
              type: "button",
              class: "btn-link",
              onclick: (e) => {
                e.stopPropagation();
                navigate("accounts");
              },
            },
            "Connect →",
          ),
        ])
      : null;

    return el(
      "div",
      {
        class: `op-card${needsSetup ? " op-card--needs-setup" : ""}${activeCount > 0 ? " op-card--active" : ""}`,
        tabIndex: "0",
        role: "button",
        "aria-disabled": needsSetup ? "true" : null,
        onclick: () => {
          if (needsSetup) {
            logTelemetry("operations", "card_blocked_no_account", {
              opId: op.id,
              platform: setup.platform,
            });
            navigate("accounts");
            return;
          }
          logTelemetry("operations", "card_click", { opId: op.id });
          navigate(`operations/op/${op.id}`);
        },
        onkeydown: (e) => {
          if (e.key === "Enter" || e.key === " ") {
            e.preventDefault();
            if (needsSetup) navigate("accounts");
            else navigate(`operations/op/${op.id}`);
          }
        },
      },
      [
        pinBtn,
        previewBadge,
        runningBadge,
        el("div", { class: "op-card-header" }, [
          el("span", { class: "op-card-icon" }, op.icon),
          el("div", { class: "op-card-titles" }, [
            el("div", { class: "op-card-label" }, op.label),
            el("div", { class: "op-card-desc" }, op.description),
          ]),
        ]),
        el("div", { class: "op-card-tags" }, tagEls),
        platformEls,
        lastRunEl,
        setupBanner,
      ],
    );
  }

  render();
}
