const DEFAULT_PORT = 18792;

const BADGE = {
  on: { text: "ON", color: "#FF5A36" },
  off: { text: "", color: "#000000" },
  connecting: { text: "…", color: "#F59E0B" },
  error: { text: "!", color: "#B91C1C" },
};

/** @type {WebSocket|null} */
let relayWs = null;
/** @type {Promise<void>|null} */
let relayConnectPromise = null;

let debuggerListenersInstalled = false;

let nextSession = 1;

/** @type {Map<number, {state:'connecting'|'connected', sessionId?:string, targetId?:string, attachOrder?:number}>} */
const tabs = new Map();
/** @type {Map<string, number>} */
const tabBySession = new Map();
/** @type {Map<string, number>} */
const childSessionToTab = new Map();

/** @type {Map<number, {resolve:(v:any)=>void, reject:(e:Error)=>void}>} */
const pending = new Map();

function nowStack() {
  try {
    return new Error().stack || "";
  } catch {
    return "";
  }
}

async function getRelayPort() {
  const stored = await chrome.storage.local.get(["relayPort"]);
  const raw = stored.relayPort;
  const n = Number.parseInt(String(raw || ""), 10);
  if (!Number.isFinite(n) || n <= 0 || n > 65535) return DEFAULT_PORT;
  return n;
}

async function getGatewayToken() {
  const stored = await chrome.storage.local.get(["gatewayToken"]);
  const token = String(stored.gatewayToken || "").trim();
  return token || "";
}

function setBadge(tabId, kind) {
  const cfg = BADGE[kind];
  void chrome.action.setBadgeText({ tabId, text: cfg.text });
  void chrome.action.setBadgeBackgroundColor({ tabId, color: cfg.color });
  void chrome.action.setBadgeTextColor({ tabId, color: "#FFFFFF" }).catch(() => {});
}

async function ensureRelayConnection() {
  if (relayWs && relayWs.readyState === WebSocket.OPEN) return;
  if (relayConnectPromise) return await relayConnectPromise;

  relayConnectPromise = (async () => {
    const port = await getRelayPort();
    const gatewayToken = await getGatewayToken();
    const httpBase = `http://127.0.0.1:${port}`;
    const wsUrl = gatewayToken
      ? `ws://127.0.0.1:${port}/extension?token=${encodeURIComponent(gatewayToken)}`
      : `ws://127.0.0.1:${port}/extension`;

    // Fast preflight: is the relay server up?
    try {
      await fetch(`${httpBase}/`, { method: "HEAD", signal: AbortSignal.timeout(2000) });
    } catch (err) {
      throw new Error(`Relay server not reachable at ${httpBase} (${String(err)})`);
    }

    if (!gatewayToken) {
      throw new Error(
        "Missing gatewayToken in extension settings (chrome.storage.local.gatewayToken)",
      );
    }

    const ws = new WebSocket(wsUrl);
    relayWs = ws;

    await new Promise((resolve, reject) => {
      const t = setTimeout(() => reject(new Error("WebSocket connect timeout")), 5000);
      ws.onopen = () => {
        clearTimeout(t);
        resolve();
      };
      ws.onerror = () => {
        clearTimeout(t);
        reject(new Error("WebSocket connect failed"));
      };
      ws.onclose = (ev) => {
        clearTimeout(t);
        reject(new Error(`WebSocket closed (${ev.code} ${ev.reason || "no reason"})`));
      };
    });

    ws.onmessage = (event) => void onRelayMessage(String(event.data || ""));
    ws.onclose = () => onRelayClosed("closed");
    ws.onerror = () => onRelayClosed("error");

    if (!debuggerListenersInstalled) {
      debuggerListenersInstalled = true;
      chrome.debugger.onEvent.addListener(onDebuggerEvent);
      chrome.debugger.onDetach.addListener(onDebuggerDetach);
    }
  })();

  try {
    await relayConnectPromise;
  } finally {
    relayConnectPromise = null;
  }
}

function onRelayClosed(reason) {
  relayWs = null;
  for (const [id, p] of pending.entries()) {
    pending.delete(id);
    p.reject(new Error(`Relay disconnected (${reason})`));
  }

  for (const tabId of tabs.keys()) {
    void chrome.debugger.detach({ tabId }).catch(() => {});
    setBadge(tabId, "connecting");
    void chrome.action.setTitle({
      tabId,
      title: "Clawdbot Browser Relay: disconnected (click to re-attach)",
    });
  }
  tabs.clear();
  tabBySession.clear();
  childSessionToTab.clear();
}

function sendToRelay(payload) {
  const ws = relayWs;
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    throw new Error("Relay not connected");
  }
  ws.send(JSON.stringify(payload));
}

async function maybeOpenHelpOnce() {
  try {
    const stored = await chrome.storage.local.get(["helpOnErrorShown"]);
    if (stored.helpOnErrorShown === true) return;
    await chrome.storage.local.set({ helpOnErrorShown: true });
    await chrome.runtime.openOptionsPage();
  } catch {
    // ignore
  }
}

async function onRelayMessage(text) {
  /** @type {any} */
  let msg;
  try {
    msg = JSON.parse(text);
  } catch {
    return;
  }

  if (msg && msg.method === "ping") {
    try {
      sendToRelay({ method: "pong" });
    } catch {
      // ignore
    }
    return;
  }

  if (msg && typeof msg.id === "number" && (msg.result !== undefined || msg.error !== undefined)) {
    const p = pending.get(msg.id);
    if (!p) return;
    pending.delete(msg.id);
    if (msg.error) p.reject(new Error(String(msg.error)));
    else p.resolve(msg.result);
    return;
  }

  if (msg && typeof msg.id === "number" && msg.method === "forwardCDPCommand") {
    try {
      const result = await handleForwardCdpCommand(msg);
      sendToRelay({ id: msg.id, result });
    } catch (err) {
      sendToRelay({ id: msg.id, error: err instanceof Error ? err.message : String(err) });
    }
  }
}

function getTabBySessionId(sessionId) {
  const direct = tabBySession.get(sessionId);
  if (direct) return { tabId: direct, kind: "main" };
  const child = childSessionToTab.get(sessionId);
  if (child) return { tabId: child, kind: "child" };
  return null;
}

function getTabByTargetId(targetId) {
  for (const [tabId, tab] of tabs.entries()) {
    if (tab.targetId === targetId) return tabId;
  }
  return null;
}

async function attachTab(tabId, opts = {}) {
  const debuggee = { tabId };
  await chrome.debugger.attach(debuggee, "1.3");
  await chrome.debugger.sendCommand(debuggee, "Page.enable").catch(() => {});

  const info = /** @type {any} */ (
    await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo")
  );
  const targetInfo = info?.targetInfo;
  const targetId = String(targetInfo?.targetId || "").trim();
  if (!targetId) {
    throw new Error("Target.getTargetInfo returned no targetId");
  }

  const sessionId = `cb-tab-${nextSession++}`;
  const attachOrder = nextSession;

  tabs.set(tabId, { state: "connected", sessionId, targetId, attachOrder });
  tabBySession.set(sessionId, tabId);
  void chrome.action.setTitle({
    tabId,
    title: "Clawdbot Browser Relay: attached (click to detach)",
  });

  if (!opts.skipAttachedEvent) {
    sendToRelay({
      method: "forwardCDPEvent",
      params: {
        method: "Target.attachedToTarget",
        params: {
          sessionId,
          targetInfo: { ...targetInfo, attached: true },
          waitingForDebugger: false,
        },
      },
    });
  }

  setBadge(tabId, "on");
  return { sessionId, targetId };
}

async function detachTab(tabId, reason) {
  const tab = tabs.get(tabId);
  if (tab?.sessionId && tab?.targetId) {
    try {
      sendToRelay({
        method: "forwardCDPEvent",
        params: {
          method: "Target.detachedFromTarget",
          params: { sessionId: tab.sessionId, targetId: tab.targetId, reason },
        },
      });
    } catch {
      // ignore
    }
  }

  if (tab?.sessionId) tabBySession.delete(tab.sessionId);
  tabs.delete(tabId);

  for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
    if (parentTabId === tabId) childSessionToTab.delete(childSessionId);
  }

  try {
    await chrome.debugger.detach({ tabId });
  } catch {
    // ignore
  }

  setBadge(tabId, "off");
  void chrome.action.setTitle({
    tabId,
    title: "Clawdbot Browser Relay (click to attach/detach)",
  });
}

async function connectOrToggleForActiveTab() {
  const [active] = await chrome.tabs.query({ active: true, currentWindow: true });
  const tabId = active?.id;
  if (!tabId) return;

  const existing = tabs.get(tabId);
  if (existing?.state === "connected") {
    await detachTab(tabId, "toggle");
    return;
  }

  tabs.set(tabId, { state: "connecting" });
  setBadge(tabId, "connecting");
  void chrome.action.setTitle({
    tabId,
    title: "Clawdbot Browser Relay: connecting to local relay…",
  });

  try {
    await ensureRelayConnection();
    await attachTab(tabId);
  } catch (err) {
    tabs.delete(tabId);
    setBadge(tabId, "error");
    void chrome.action.setTitle({
      tabId,
      title: "Clawdbot Browser Relay: relay not running (open options for setup)",
    });
    void maybeOpenHelpOnce();
    // Extra breadcrumbs in chrome://extensions service worker logs.
    const message = err instanceof Error ? err.message : String(err);
    console.warn("attach failed", message, nowStack());
  }
}

async function handleForwardCdpCommand(msg) {
  const method = String(msg?.params?.method || "").trim();
  const params = msg?.params?.params || undefined;
  const sessionId = typeof msg?.params?.sessionId === "string" ? msg.params.sessionId : undefined;

  // Map command to tab
  const bySession = sessionId ? getTabBySessionId(sessionId) : null;
  const targetId = typeof params?.targetId === "string" ? params.targetId : undefined;
  const tabId =
    bySession?.tabId ||
    (targetId ? getTabByTargetId(targetId) : null) ||
    (() => {
      // No sessionId: pick the first connected tab (stable-ish).
      for (const [id, tab] of tabs.entries()) {
        if (tab.state === "connected") return id;
      }
      return null;
    })();

  if (!tabId) throw new Error(`No attached tab for method ${method}`);

  /** @type {chrome.debugger.DebuggerSession} */
  const debuggee = { tabId };

  if (method === "Runtime.enable") {
    try {
      await chrome.debugger.sendCommand(debuggee, "Runtime.disable");
      await new Promise((r) => setTimeout(r, 50));
    } catch {
      // ignore
    }
    return await chrome.debugger.sendCommand(debuggee, "Runtime.enable", params);
  }

  if (method === "Target.createTarget") {
    const url = typeof params?.url === "string" ? params.url : "about:blank";
    const tab = await chrome.tabs.create({ url, active: false });
    if (!tab.id) throw new Error("Failed to create tab");
    await new Promise((r) => setTimeout(r, 100));
    const attached = await attachTab(tab.id);
    return { targetId: attached.targetId };
  }

  if (method === "Target.closeTarget") {
    const target = typeof params?.targetId === "string" ? params.targetId : "";
    const toClose = target ? getTabByTargetId(target) : tabId;
    if (!toClose) return { success: false };
    try {
      await chrome.tabs.remove(toClose);
    } catch {
      return { success: false };
    }
    return { success: true };
  }

  if (method === "Target.activateTarget") {
    const target = typeof params?.targetId === "string" ? params.targetId : "";
    const toActivate = target ? getTabByTargetId(target) : tabId;
    if (!toActivate) return {};
    const tab = await chrome.tabs.get(toActivate).catch(() => null);
    if (!tab) return {};
    if (tab.windowId) {
      await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {});
    }
    await chrome.tabs.update(toActivate, { active: true }).catch(() => {});
    return {};
  }

  const tabState = tabs.get(tabId);
  const mainSessionId = tabState?.sessionId;
  const debuggerSession =
    sessionId && mainSessionId && sessionId !== mainSessionId
      ? { ...debuggee, sessionId }
      : debuggee;

  return await chrome.debugger.sendCommand(debuggerSession, method, params);
}

function onDebuggerEvent(source, method, params) {
  const tabId = source.tabId;
  if (!tabId) return;
  const tab = tabs.get(tabId);
  if (!tab?.sessionId) return;

  if (method === "Target.attachedToTarget" && params?.sessionId) {
    childSessionToTab.set(String(params.sessionId), tabId);
  }

  if (method === "Target.detachedFromTarget" && params?.sessionId) {
    childSessionToTab.delete(String(params.sessionId));
  }

  try {
    sendToRelay({
      method: "forwardCDPEvent",
      params: {
        sessionId: source.sessionId || tab.sessionId,
        method,
        params,
      },
    });
  } catch {
    // ignore
  }
}

function onDebuggerDetach(source, reason) {
  const tabId = source.tabId;
  if (!tabId) return;
  if (!tabs.has(tabId)) return;
  void detachTab(tabId, reason);
}

chrome.action.onClicked.addListener(() => void connectOrToggleForActiveTab());

chrome.runtime.onInstalled.addListener(() => {
  // Useful: first-time instructions.
  void chrome.runtime.openOptionsPage();
});
