/**
 * Web shim — run the dashboard (Catalog / Composer / CRM / Campaigns) in a PLAIN
 * BROWSER, with NO Chrome extension.
 *
 * The dashboard talks to the backend exclusively through
 * `chrome.runtime.sendMessage({ action, ...payload }, cb)`, which the extension's
 * background.js translates into an authenticated `fetch`. This shim provides the
 * SAME contract for a web page: it defines `window.chrome` so `sendMessage`
 * resolves an action → a REST call against the backend (Bearer token + apiBase
 * from localStorage), and stubs `chrome.storage`/`tabs`/`cookies` so init runs.
 *
 * Served same-origin from the backend at `/app` (so `/api/*` needs no CORS).
 *
 * Scope: a CURATED route table covering the no-browser flows (Catalog, the
 * Composer's CRM segment → Telegram/email campaign, CRM browse, Campaigns,
 * accounts). Extension-only actions (live LinkedIn DOM batches, browser-session
 * capture, hidden worker tabs) have no web equivalent and resolve to an empty
 * object so the dashboard degrades gracefully instead of crashing.
 *
 * Load this BEFORE dashboard.js. Exposes window.NEXUS_WEB_SHIM (+ CommonJS
 * export) so the route resolution is unit-testable.
 */
(function () {
  "use strict";

  // action → (msg) => { method, path, body? }. Mirrors the relevant subset of
  // background.js's route map; keep new no-browser endpoints in sync here.
  const q = (m) => (m && m.queryString) || "";
  const ROUTES = {
    // accounts (Composer account picker)
    "accounts-all": () => ({ method: "GET", path: "/api/accounts/all" }),
    "li-budget": () => ({ method: "GET", path: "/api/linkedin/limits-check" }),
    // CRM-as-a-source (Composer)
    "crm-segment": (m) => ({ method: "GET", path: `/api/crm/segment${q(m)}` }),
    "crm-segment-options": (m) => ({ method: "GET", path: `/api/crm/segment/options${q(m)}` }),
    "crm-segment-ids": (m) => ({ method: "GET", path: `/api/crm/segment/ids${q(m)}` }),
    "crm-bulk-campaign": (m) => ({
      method: "POST",
      path: "/api/crm/contacts/bulk-campaign",
      body: m.body,
    }),
    // CRM browse
    "crm-contacts-list": (m) => ({ method: "GET", path: `/api/crm/contacts${q(m)}` }),
    "crm-companies-list": (m) => ({ method: "GET", path: `/api/crm/companies${q(m)}` }),
    "crm-search": (m) => ({ method: "GET", path: `/api/crm/search${q(m)}` }),
    "crm-contacts-tags": () => ({ method: "GET", path: "/api/crm/contacts/tags" }),
    "crm-contacts-sectors": () => ({ method: "GET", path: "/api/crm/contacts/sectors" }),
    "crm-contact-detail": (m) => ({
      method: "GET",
      path: `/api/crm/contacts/${encodeURIComponent(m.contactId)}`,
    }),
    "crm-contact-timeline": (m) => ({
      method: "GET",
      path: `/api/crm/contacts/${encodeURIComponent(m.contactId)}/timeline`,
    }),
    "crm-suggested-follow-ups": () => ({ method: "GET", path: `/api/crm/suggested-follow-ups` }),
    // Basic mode — Objective Search funnel
    "search-start": (m) => ({ method: "POST", path: `/api/search`, body: m.body }),
    "search-poll": (m) => ({ method: "GET", path: `/api/search/${encodeURIComponent(m.slug)}` }),
    "search-select": (m) => ({
      method: "POST",
      path: `/api/search/${encodeURIComponent(m.slug)}/select`,
      body: m.body,
    }),
    "search-activation-plan": (m) => ({
      method: "GET",
      path: `/api/search/${encodeURIComponent(m.slug)}/activation-plan`,
    }),
    "search-activate": (m) => ({
      method: "POST",
      path: `/api/search/${encodeURIComponent(m.slug)}/activate`,
      body: m.body,
    }),
    "search-launch": (m) => ({
      method: "POST",
      path: `/api/search/${encodeURIComponent(m.slug)}/launch`,
      body: m.body,
    }),
    "crm-contact-update": (m) => ({
      method: "POST",
      path: `/api/crm/contacts/${encodeURIComponent(m.contactId)}/update`,
      body: m.body,
    }),
    "crm-stats": () => ({ method: "GET", path: "/api/crm/pipeline" }),
    // Campaigns (the Composer's sends land here)
    "campaigns-dm": (m) => ({ method: "GET", path: `/api/campaigns/dm${q(m)}` }),
    "campaigns-email": (m) => ({ method: "GET", path: `/api/campaigns/email${q(m)}` }),
    "campaigns-outreach": (m) => ({ method: "GET", path: `/api/campaigns/outreach${q(m)}` }),
    "campaigns-stats": () => ({ method: "GET", path: "/api/campaigns/stats" }),
    "campaigns-all": (m) => ({ method: "GET", path: `/api/campaigns/all${q(m)}` }),
    "campaigns-action": (m) => ({ method: "POST", path: `/api/campaigns/action`, body: m.body }),
    "campaigns-detail": (m) => ({ method: "GET", path: `/api/campaigns/detail${q(m)}` }),
    // Templates + Playbooks (browse-to-create). Without these the web build showed
    // "No templates available" and an empty Playbooks hub (routes were extension-only).
    "composition-templates": () => ({ method: "GET", path: "/api/compositions/templates" }),
    "playbooks-list": () => ({ method: "GET", path: "/api/playbooks" }),
    "playbook-create": (m) => ({ method: "POST", path: "/api/playbooks", body: m.body }),
    "playbook-templates": () => ({ method: "GET", path: "/api/playbooks/templates" }),
    "playbook-install": (m) => ({ method: "POST", path: "/api/playbooks/install", body: m.body }),
    "playbook-get": (m) => ({ method: "GET", path: `/api/playbooks/${encodeURIComponent(m.id)}` }),
    // Core read/manage surfaces a web user lands on — Today, the bell, Monitor,
    // Find, Pipeline, Clients, Proposals (were extension-only → empty in /web).
    today: () => ({ method: "GET", path: "/api/today" }),
    "mission-control": () => ({ method: "GET", path: "/api/mission-control" }),
    "stats-overview": (m) => ({ method: "GET", path: `/api/stats/overview${q(m)}` }),
    "live-map": () => ({ method: "GET", path: "/api/admin/live-map" }),
    "quests-summary": () => ({ method: "GET", path: "/api/quests/summary" }),
    "automations-proof": () => ({ method: "GET", path: "/api/admin/automations/proof" }),
    "activity-feed": (m) => ({ method: "GET", path: `/api/activity/feed${q(m)}` }),
    "notifications-list": (m) => ({ method: "GET", path: `/api/notifications${q(m)}` }),
    "notification-read": (m) => ({
      method: "POST",
      path: `/api/notifications/${encodeURIComponent(m.id)}/read`,
      body: {},
    }),
    "notifications-read-all": (m) => ({
      method: "POST",
      path: `/api/notifications/read-all${q(m)}`,
      body: {},
    }),
    "signals-list": (m) => ({ method: "GET", path: `/api/signals${q(m)}` }),
    "signals-search": (m) => ({ method: "POST", path: "/api/signals/search", body: m.body }),
    "crm-pipeline": () => ({ method: "GET", path: "/api/crm/pipeline" }),
    "crm-deals": (m) => ({ method: "GET", path: `/api/crm/deals${q(m)}` }),
    "crm-inbox": (m) => ({ method: "GET", path: `/api/crm/inbox${q(m)}` }),
    "clients-list": () => ({ method: "GET", path: "/api/clients" }),
    "proposals-list": (m) => ({ method: "GET", path: `/api/proposals${q(m)}` }),
    "compositions-list": (m) => ({ method: "GET", path: `/api/compositions${q(m)}` }),
    "workflows-list": () => ({ method: "GET", path: "/api/campaigns/workflows" }),
    // shell
    "settings-get": () => ({ method: "GET", path: "/api/settings" }),
    "engine-health": () => ({ method: "GET", path: "/api/engine/health" }),
  };

  function resolveRoute(action, msg) {
    const m = msg || {};
    const fn = ROUTES[action];
    if (typeof fn === "function") return fn(m); // curated overrides + web-specific shapes win
    // Fall back to the FULL shared route map (dashboard-routes.js, a copy of
    // background.js's DASHBOARD_ROUTES) so the /web build reaches every
    // server-backed action, not just the curated subset above. Its entries are
    // { method, path:(m)=>str, body?:(m)=>any } — adapt to our {method,path,body}.
    const shared = (typeof globalThis !== "undefined" && globalThis.NEXUS_DASHBOARD_ROUTES) || null;
    const r = shared && shared[action];
    if (r && typeof r.path === "function") {
      return { method: r.method || "GET", path: r.path(m), body: r.body ? r.body(m) : undefined };
    }
    return null;
  }

  function getCfg() {
    let base = "http://localhost:9091";
    let token = "";
    let workspaceId = "";
    try {
      base = (localStorage.getItem("apiBase") || base).replace(/\/$/, "");
      token = localStorage.getItem("token") || "";
      // Namespaced (not a SHARED_KEY), so it lives under cs_workspaceId — the
      // same slot chrome.storage.local.set({workspaceId}) writes through lsKey().
      workspaceId = localStorage.getItem("cs_workspaceId") || "";
      if (workspaceId.startsWith('"')) workspaceId = JSON.parse(workspaceId);
    } catch {
      /* no localStorage (tests) */
    }
    return { base, token, workspaceId };
  }

  async function callApi(msg) {
    const route = resolveRoute(msg && msg.action, msg);
    if (!route) return {}; // unmapped → graceful empty (don't crash init)
    const { base, token, workspaceId } = getCfg();
    const opts = { method: route.method, headers: { Authorization: `Bearer ${token}` } };
    // Multi-tenant: the hosted /web app must scope to the active workspace the
    // same way the extension does (background.js handleConnect). Without this
    // header nexus-bot/src/utils/auth.js falls back to the personal owner, so
    // workspace scoping was inert for every hosted user on every route.
    if (workspaceId) opts.headers["X-Workspace-Id"] = workspaceId;
    if (route.body !== undefined) {
      opts.headers["Content-Type"] = "application/json";
      opts.body = JSON.stringify(route.body);
    }
    try {
      const res = await fetch(base + route.path, opts);
      const j = await res.json().catch(() => ({}));
      if (res.status === 401) return { unauthorized: true, ...j };
      return j;
    } catch (e) {
      return { error: String((e && e.message) || e) };
    }
  }

  // ── chrome.* stub ────────────────────────────────────────────────────────
  const noop = () => {};
  // `token` & `apiBase` are shared with the web token-gate (app.html) and
  // getCfg() above, which read/write the UNPREFIXED localStorage keys. Only the
  // rest is namespaced under cs_ to avoid clobbering unrelated page storage. If
  // these were prefixed, the dashboard's chrome.storage.local.get(["token"])
  // would never find the token the gate stored under "token" → "Not paired".
  const SHARED_KEYS = new Set(["token", "apiBase"]);
  const lsKey = (k) => (SHARED_KEYS.has(k) ? k : "cs_" + k);
  // MV3 chrome.storage returns a Promise when called with NO callback, and the
  // dashboard uses that form (`await chrome.storage.local.get(...)`). These
  // shims MUST mirror it: invoke the callback if given, else resolve a Promise.
  // (Returning undefined made `await` yield undefined → dashboard init threw →
  // the web app hung on "Loading… / Offline".)
  const lsGet = (keys, cb) => {
    const out = {};
    try {
      const ks = Array.isArray(keys)
        ? keys
        : typeof keys === "string"
          ? [keys]
          : Object.keys(keys || {});
      for (const k of ks) {
        const v = localStorage.getItem(lsKey(k));
        if (v != null) {
          try {
            out[k] = JSON.parse(v);
          } catch {
            out[k] = v;
          }
        } else if (keys && !Array.isArray(keys) && typeof keys === "object") out[k] = keys[k]; // defaults
      }
    } catch {
      /* ignore */
    }
    if (typeof cb === "function") {
      cb(out);
      return;
    }
    return Promise.resolve(out);
  };
  const lsSet = (obj, cb) => {
    try {
      for (const k in obj) {
        // Keep shared keys as raw strings so they stay byte-identical to what
        // the token-gate wrote and getCfg reads; JSON-encode everything else.
        localStorage.setItem(
          lsKey(k),
          SHARED_KEYS.has(k) ? String(obj[k]) : JSON.stringify(obj[k]),
        );
      }
    } catch {
      /* ignore */
    }
    if (typeof cb === "function") {
      cb();
      return;
    }
    return Promise.resolve();
  };
  const lsRemove = (keys, cb) => {
    try {
      (Array.isArray(keys) ? keys : [keys]).forEach((k) => localStorage.removeItem(lsKey(k)));
    } catch {
      /* ignore */
    }
    if (typeof cb === "function") {
      cb();
      return;
    }
    return Promise.resolve();
  };

  const chromeStub = {
    runtime: {
      id: "nexus-web",
      sendMessage: (msg, cb) => {
        const p = Promise.resolve(callApi(msg || {}));
        if (typeof cb === "function") p.then(cb, () => cb({}));
        return true;
      },
      onMessage: { addListener: noop, removeListener: noop },
      getURL: (p) => p,
      lastError: null,
      connect: () => ({
        onMessage: { addListener: noop },
        postMessage: noop,
        onDisconnect: { addListener: noop },
      }),
    },
    storage: {
      local: { get: lsGet, set: lsSet, remove: lsRemove },
      sync: {
        get: (k, cb) => {
          if (typeof cb === "function") {
            cb({});
            return;
          }
          return Promise.resolve({});
        },
        set: (o, cb) => {
          if (typeof cb === "function") {
            cb();
            return;
          }
          return Promise.resolve();
        },
      },
      onChanged: { addListener: noop },
    },
    tabs: {
      query: (qy, cb) => cb && cb([]),
      create: noop,
      update: noop,
      onUpdated: { addListener: noop },
      onRemoved: { addListener: noop },
    },
    cookies: {
      get: (d, cb) => cb && cb(null),
      getAll: (d, cb) => cb && cb([]),
      onChanged: { addListener: noop },
    },
  };

  if (typeof window !== "undefined") {
    // Only install if there's no real extension chrome.runtime already present.
    if (
      !window.chrome ||
      !window.chrome.runtime ||
      !window.chrome.runtime.id ||
      window.chrome.runtime.id === "nexus-web"
    ) {
      window.chrome = chromeStub;
    }
    window.NEXUS_WEB_SHIM = { ROUTES, resolveRoute, callApi };
  }
  if (typeof module !== "undefined" && module.exports) module.exports = { ROUTES, resolveRoute };
})();
