const BRIDGE_BASE_URL = "http://127.0.0.1:8787";
const PENDING_SOCKET_HANDOFF_KEY = "pendingSocketHandoff";
const WORKER_HEARTBEAT_ALARM = "addc-helper-worker-heartbeat";
const RESTORED_HANDOFF_TAB_ID = -1;
const volatileByTab = new Map();
let heartbeatInFlight = false;

console.info("service worker started");
void initializeWorkerLifecycle();

chrome.runtime.onInstalled.addListener(() => {
  void sendHelperHeartbeat("installed");
  void publishSafeState({
    bridgeOnline: false,
    currentMatchDetected: false,
    lastHandoffStatus: "Helper installed. Open play.autodarts.io to begin.",
    diagnostics: createDiagnostics(),
    eventCount: 0,
    updatedAt: new Date().toISOString()
  });
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message?.type === "GET_POPUP_STATE") {
    getPopupState().then(sendResponse);
    return true;
  }

  if (message?.source !== "ADDC_AUTODARTS_CONTENT") return false;

  handleContentMessage(message, sender.tab?.id, sender.frameId ?? 0).then(
    (result) => sendResponse({ ok: true, ...result }),
    (error) => sendResponse({ ok: false, error: error.message || String(error) })
  );
  return true;
});

chrome.tabs.onRemoved.addListener((tabId) => {
  volatileByTab.delete(tabId);
});

if (chrome.runtime.onSuspend) {
  chrome.runtime.onSuspend.addListener(() => {
    console.info("service worker suspended");
  });
}

if (chrome.alarms?.onAlarm) {
  chrome.alarms.onAlarm.addListener((alarm) => {
    if (alarm.name === WORKER_HEARTBEAT_ALARM) {
      void heartbeatActiveMatch();
    }
  });
}

setInterval(() => {
  void heartbeatActiveMatch();
}, 15_000);

async function initializeWorkerLifecycle() {
  await rememberWorkerStarted();
  await restorePendingSocketHandoff();
  await heartbeatActiveMatch();
  if (chrome.alarms?.create) {
    chrome.alarms.create(WORKER_HEARTBEAT_ALARM, { periodInMinutes: 0.5 });
  }
}

async function handleContentMessage(message, tabId, frameId = 0) {
  if (!tabId) return { ignored: true };

  const state = volatileByTab.get(tabId) || {};
  if (message.type === "INJECT_MAIN_WORLD_HOOK") {
    await injectMainWorldHook(tabId, frameId);
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: "WebSocket capture armed",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        pageHookInjectedAt: new Date().toISOString(),
        constructorHookedAt: new Date().toISOString()
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    return { status: "main_world_hook_requested" };
  }

  if (message.type === "HOOK_READY" || message.type === "PAGE_HOOK_INJECTED") {
    console.info("Page hook injected");
    void sendHelperHeartbeat("active");
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: "Page hook injected",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        pageHookInjectedAt: new Date().toISOString()
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    return { status: "page_hook_injected" };
  }

  if (message.type === "WEBSOCKET_CONSTRUCTOR_HOOKED") {
    console.info("websocket constructor hooked");
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: "WebSocket capture armed",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        constructorHookedAt: new Date().toISOString()
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    return { status: "websocket_constructor_hooked" };
  }

  if (message.type === "WEBSOCKET_RECONNECT_DETECTION") {
    const lastError = safeError(message.reason || "Socket may have opened before hook.");
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: "WebSocket capture armed. Waiting for reconnect.",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        reconnectDetectionAt: new Date().toISOString(),
        lastError
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    return { status: "websocket_reconnect_detection" };
  }

  if (message.type === "WEBSOCKET_URL_CAPTURED") {
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: "WebSocket URL captured",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        websocketUrlCapturedAt: new Date().toISOString()
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    return { status: "websocket_url_captured" };
  }

  if (message.type === "PAGE_MIRROR_ATTACHED") {
    const messageMatchId = stringField(message.matchId);
    const messageMatchUrl = stringField(message.matchUrl);
    if (messageMatchId) state.matchId = messageMatchId;
    if (messageMatchUrl) state.matchUrl = messageMatchUrl;
    console.info("Autodarts page socket mirror attached", {
      matchId: state.matchId || state.safe?.matchId || null,
      mirrorAttachedCount: Number.isFinite(message.mirrorAttachedCount) ? message.mirrorAttachedCount : 0
    });
    state.safe = {
      ...(state.safe || {}),
      currentMatchDetected: Boolean(state.matchId || state.safe?.matchId),
      matchId: state.matchId || state.safe?.matchId || null,
      matchUrl: state.matchUrl || state.safe?.matchUrl || null,
      lastHandoffStatus: "Autodarts page socket mirror attached",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        pageMirrorAttachedAt: new Date().toISOString(),
        pageMirrorAttachedCount: Number.isFinite(message.mirrorAttachedCount) ? message.mirrorAttachedCount : 0
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    await sendHelperHeartbeat("page_mirror_attached", state.matchId || state.safe?.matchId || null, state.safe.diagnostics);
    return { status: "page_mirror_attached" };
  }

  if (message.type === "PAGE_MIRROR_ATTACH_FAILED") {
    const lastError = safeError(message.error || "Page mirror attach failed");
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: "Page mirror attach failed",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        pageMirrorAttachFailedAt: new Date().toISOString(),
        lastError
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    await sendHelperHeartbeat("page_mirror_attach_failed", state.matchId || state.safe?.matchId || null, state.safe.diagnostics);
    return { status: "page_mirror_attach_failed" };
  }

  if (message.type === "WEBSOCKET_OPEN" || message.type === "WEBSOCKET_CLOSE" || message.type === "WEBSOCKET_ERROR") {
    const messageMatchId = stringField(message.matchId);
    const messageMatchUrl = stringField(message.matchUrl);
    if (messageMatchId) state.matchId = messageMatchId;
    if (messageMatchUrl) state.matchUrl = messageMatchUrl;
    const updates = {
      pageSocketOpenAt: message.type === "WEBSOCKET_OPEN" ? new Date().toISOString() : state.safe?.diagnostics?.pageSocketOpenAt || "",
      pageSocketCloseAt: message.type === "WEBSOCKET_CLOSE" ? new Date().toISOString() : state.safe?.diagnostics?.pageSocketCloseAt || "",
      pageSocketErrorAt: message.type === "WEBSOCKET_ERROR" ? new Date().toISOString() : state.safe?.diagnostics?.pageSocketErrorAt || "",
      pageSocketCloseCode: message.type === "WEBSOCKET_CLOSE" && message.closeCode !== null ? String(message.closeCode) : state.safe?.diagnostics?.pageSocketCloseCode || ""
    };
    state.safe = {
      ...(state.safe || {}),
      currentMatchDetected: Boolean(state.matchId || state.safe?.matchId),
      matchId: state.matchId || state.safe?.matchId || null,
      matchUrl: state.matchUrl || state.safe?.matchUrl || null,
      lastHandoffStatus: message.type === "WEBSOCKET_OPEN" ? "Autodarts page socket open" : state.safe?.lastHandoffStatus || "Autodarts live connection detected",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, updates),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    await sendHelperHeartbeat(
      message.type === "WEBSOCKET_OPEN" ? "page_socket_open" : message.type === "WEBSOCKET_CLOSE" ? "page_socket_closed" : "page_socket_error",
      state.matchId || state.safe?.matchId || null,
      state.safe.diagnostics
    );
    return { status: message.type.toLowerCase() };
  }

  if (message.type === "WEBSOCKET_MESSAGE") {
    const messageMatchId = stringField(message.matchId);
    const messageMatchUrl = stringField(message.matchUrl);
    if (messageMatchId) state.matchId = messageMatchId;
    if (messageMatchUrl) state.matchUrl = messageMatchUrl;
    if (!state.matchId || !state.matchUrl) return { ignored: true, reason: "missing_match" };

    const byteLength = Number.isFinite(message.byteLength) ? message.byteLength : 0;
    const payloadHashPrefix = stringField(message.payloadHashPrefix);
    state.safe = {
      ...(state.safe || {}),
      currentMatchDetected: true,
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      lastHandoffStatus: "Forwarding Autodarts page events",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        pageMirroredMessagesCount: (state.safe?.diagnostics?.pageMirroredMessagesCount || 0) + 1,
        pageMirroredLastFrameType: stringField(message.frameType) || "unknown",
        pageMirroredLastByteLength: byteLength,
        pageMirroredLastHashPrefix: payloadHashPrefix,
        pageMirroredPageCount: Number.isFinite(message.mirrorMessageCount) ? message.mirrorMessageCount : 0,
        pageMirroredLastAt: new Date().toISOString()
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState(state.safe);
    await sendHelperHeartbeat("page_frame_seen", state.matchId, state.safe.diagnostics);
    await forwardPageSocketMessage(state, message);
    return { status: "websocket_message_forwarded" };
  }

  if (message.type === "PAGE_HOOK_ERROR") {
    const lastError = safeError(message.error || "Page hook injection failed");
    console.warn("Last error", lastError);
    state.safe = {
      ...(state.safe || {}),
      lastHandoffStatus: lastError,
      diagnostics: updateDiagnostics(state.safe?.diagnostics, { lastError }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await publishSafeState({
      ...state.safe
    });
    return { status: "page_hook_error" };
  }

  if (message.type === "MATCH_DETECTED") {
    state.matchId = stringField(message.matchId);
    state.matchUrl = stringField(message.matchUrl);
    state.safe = {
      ...(state.safe || {}),
      currentMatchDetected: Boolean(state.matchId),
      matchId: state.matchId || null,
      matchUrl: state.matchUrl || null,
      lastHandoffStatus: "Match detected. Waiting for Autodarts socket.",
      diagnostics: updateDiagnostics(state.safe?.diagnostics),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    void sendHelperHeartbeat("match_detected", state.matchId);
    await publishSafeState(state.safe);
    await handoffMatchOnly(state);
    await trySubscribe(tabId);
    return { status: "match_detected" };
  }

  if (message.type === "WEBSOCKET_DETECTED" || message.type === "socket_detected") {
    console.info("Autodarts live connection detected");
    const messageMatchId = stringField(message.matchId);
    const messageMatchUrl = stringField(message.matchUrl);
    if (messageMatchId) state.matchId = messageMatchId;
    if (messageMatchUrl) state.matchUrl = messageMatchUrl;
    state.websocketUrl = stringField(message.websocketUrl);
    const metadata = websocketUrlMetadata(state.websocketUrl);
    console.info("background worker received websocketUrl", {
      matchId: state.matchId || state.safe?.matchId || null,
      matchUrl: state.matchUrl || state.safe?.matchUrl || null,
      websocketUrlPresent: metadata.websocketUrlPresent,
      websocketUrlLength: metadata.websocketUrlLength,
      websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
    });
    state.safe = {
      ...(state.safe || {}),
      currentMatchDetected: Boolean(state.matchId || state.safe?.matchId),
      matchId: state.matchId || state.safe?.matchId || null,
      matchUrl: state.matchUrl || state.safe?.matchUrl || null,
      lastHandoffStatus: "Autodarts live connection detected",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        websocketInstanceDetectedAt: new Date().toISOString(),
        liveConnectionDetectedAt: new Date().toISOString(),
        websocketUrlCapturedAt: new Date().toISOString(),
        websocketUrlPresent: metadata.websocketUrlPresent,
        websocketUrlLength: metadata.websocketUrlLength,
        websocketUrlHashPrefix: metadata.websocketUrlHashPrefix,
        pageMirrorAttachedAt: message.pageMirrorAttached === true ? new Date().toISOString() : state.safe?.diagnostics?.pageMirrorAttachedAt || "",
        pageMirrorAttachedCount: Number.isFinite(message.mirrorAttachedCount)
          ? message.mirrorAttachedCount
          : state.safe?.diagnostics?.pageMirrorAttachedCount || 0
      }),
      updatedAt: new Date().toISOString()
    };
    volatileByTab.set(tabId, state);
    await persistPendingSocketHandoff(state, tabId);
    await sendHelperHeartbeat("socket_detected", state.matchId);
    await publishSafeState(state.safe);
    await trySubscribe(tabId);
    return { status: "websocket_detected" };
  }

  return { ignored: true };
}

async function forwardPageSocketMessage(state, message) {
  const response = await postToBridge("/autodarts-frame", {
    matchId: state.matchId,
    matchUrl: state.matchUrl,
    frameType: stringField(message.frameType) || "unknown",
    payloadText: typeof message.payloadText === "string" ? message.payloadText : "",
    payloadBase64: typeof message.payloadBase64 === "string" ? message.payloadBase64 : "",
    byteLength: Number.isFinite(message.byteLength) ? message.byteLength : 0,
    payloadHashPrefix: stringField(message.payloadHashPrefix),
    decodeError: stringField(message.decodeError),
    source: "page_socket_mirror"
  });
  const latestState = [...volatileByTab.values()].find(candidate => candidate.matchId === state.matchId) || state;
  latestState.safe = diagnosticsSafeState(latestState.safe, {
    pageMirroredForwardedCount: (latestState.safe?.diagnostics?.pageMirroredForwardedCount || 0) + 1,
    pageMirroredBackendAcceptedCount: response.ok ? (latestState.safe?.diagnostics?.pageMirroredBackendAcceptedCount || 0) + 1 : latestState.safe?.diagnostics?.pageMirroredBackendAcceptedCount || 0,
    pageMirroredLastResponseAt: new Date().toISOString(),
    lastError: ""
  });
  await publishSafeState(latestState.safe);
}

async function injectMainWorldHook(tabId, frameId = 0) {
  await chrome.scripting.executeScript({
    target: { tabId, frameIds: [frameId] },
    files: ["src/page-websocket-hook.js"],
    world: "MAIN",
    injectImmediately: true
  });
}

async function trySubscribe(tabId) {
  const state = volatileByTab.get(tabId);
  if (!state?.matchId || !state?.matchUrl) {
    return;
  }
  if (!state.websocketUrl) {
    console.info("Match detected but live socket not captured yet.");
    const safe = {
      ...(state.safe || {}),
      currentMatchDetected: true,
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      bridgeConnected: false,
      lastHandoffStatus: "Waiting for Autodarts live connection",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, {
        lastError: "Match detected but live socket not captured yet."
      }),
      updatedAt: new Date().toISOString()
    };
    state.safe = safe;
    volatileByTab.set(tabId, state);
    void sendHelperHeartbeat("waiting_for_live_socket", state.matchId);
    await publishSafeState(safe);
    return;
  }

  try {
    await markBridgeFetchStarted(tabId, "/autodarts-session-handoff");
    const metadata = websocketUrlMetadata(state.websocketUrl);
    console.info("Sending live socket handoff to Companion", {
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      hasWebSocketUrl: Boolean(state.websocketUrl),
      websocketUrlLength: metadata.websocketUrlLength,
      websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
    });
    console.info("POST payload includes websocketUrl=true", {
      matchId: state.matchId,
      websocketUrlPresent: metadata.websocketUrlPresent,
      websocketUrlLength: metadata.websocketUrlLength,
      websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
    });
    const response = await postToBridge("/autodarts-session-handoff", {
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      websocketUrl: state.websocketUrl,
      sessionStatus: "socket_detected"
    });
    console.info("bridge handoff response", {
      matchId: state.matchId,
      websocketAvailable: response.websocketAvailable === true,
      state: response.state || "",
      statusCode: response.statusCode || null
    });
    if (response.websocketAvailable !== true) {
      throw bridgeFetchError("/autodarts-session-handoff", response.statusCode || null, "Bridge did not accept websocketUrl in handoff response.");
    }
    await markBridgeFetchSuccess(tabId, "/autodarts-session-handoff", response.statusCode || 202);
    const matchStatus = await fetchMatchStatus(state.matchId).catch(() => null);
    const latestDiagnostics = volatileByTab.get(tabId)?.safe?.diagnostics || state.safe?.diagnostics;
    const safe = {
      currentMatchDetected: true,
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      bridgeOnline: true,
      bridgeConnected: true,
      lastHandoffStatus: "Sent live session to Companion",
      diagnostics: updateDiagnostics(latestDiagnostics, {
        sentLiveSessionAt: new Date().toISOString(),
        websocketHandoffSentAt: new Date().toISOString(),
        lastError: ""
      }),
      eventCount: matchStatus?.receivedCount || 0,
      updatedAt: new Date().toISOString()
    };
    state.safe = safe;
    delete state.websocketUrl;
    volatileByTab.set(tabId, state);
    console.info("Sent live session to Companion");
    await chrome.storage.session.remove(PENDING_SOCKET_HANDOFF_KEY);
    await sendHelperHeartbeat("live_session_sent", state.matchId, safe.diagnostics);
    await publishSafeState(safe);
  } catch (error) {
    const lastError = bridgeFetchErrorText(error);
    await markBridgeFetchFailed(tabId, "/autodarts-session-handoff", error);
    const latestDiagnostics = volatileByTab.get(tabId)?.safe?.diagnostics || state.safe?.diagnostics;
    console.warn("Last error", lastError);
    const safe = {
      ...(state.safe || {}),
      bridgeOnline: false,
      bridgeConnected: false,
      lastHandoffStatus: `Bridge subscribe failed: ${lastError}`,
      diagnostics: updateDiagnostics(latestDiagnostics, { lastError }),
      updatedAt: new Date().toISOString()
    };
    state.safe = safe;
    volatileByTab.set(tabId, state);
    void sendHelperHeartbeat("bridge_subscribe_failed", state.matchId);
    await publishSafeState(safe);
  }
}

async function persistPendingSocketHandoff(state, tabId) {
  if (!state?.matchId || !state?.matchUrl || !state?.websocketUrl) return;

  const storedAt = new Date().toISOString();
  const metadata = websocketUrlMetadata(state.websocketUrl);
  await chrome.storage.session.set({
    [PENDING_SOCKET_HANDOFF_KEY]: {
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      websocketUrl: state.websocketUrl,
      hasWebSocketUrl: true,
      websocketUrlLength: metadata.websocketUrlLength,
      websocketUrlHashPrefix: metadata.websocketUrlHashPrefix,
      tabId,
      storedAt
    }
  });
  console.info("persisted pending socket handoff", {
    matchId: state.matchId,
    matchUrl: state.matchUrl,
    hasWebSocketUrl: true,
    websocketUrlLength: metadata.websocketUrlLength,
    websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
  });
  state.safe = diagnosticsSafeState(state.safe, {
    pendingSocketStoredAt: storedAt,
    websocketUrlPresent: metadata.websocketUrlPresent,
    websocketUrlLength: metadata.websocketUrlLength,
    websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
  });
  volatileByTab.set(tabId, state);
}

async function restorePendingSocketHandoff() {
  const stored = await chrome.storage.session.get(PENDING_SOCKET_HANDOFF_KEY);
  const pending = stored[PENDING_SOCKET_HANDOFF_KEY];
  if (!pending?.matchId || !pending?.matchUrl || !pending?.websocketUrl) return;

  const restoredAt = new Date().toISOString();
  console.info("restored pending socket handoff", {
    matchId: pending.matchId,
    matchUrl: pending.matchUrl,
    hasWebSocketUrl: true,
    websocketUrlLength: pending.websocketUrlLength || websocketUrlMetadata(pending.websocketUrl).websocketUrlLength,
    websocketUrlHashPrefix: pending.websocketUrlHashPrefix || websocketUrlMetadata(pending.websocketUrl).websocketUrlHashPrefix
  });
  const { safeState } = await chrome.storage.local.get("safeState");
  const tabId = Number.isInteger(pending.tabId) ? pending.tabId : RESTORED_HANDOFF_TAB_ID;
  const state = {
    matchId: pending.matchId,
    matchUrl: pending.matchUrl,
    websocketUrl: pending.websocketUrl,
    safe: {
      ...(safeState || {}),
      currentMatchDetected: true,
      matchId: pending.matchId,
      matchUrl: pending.matchUrl,
      lastHandoffStatus: "Retrying websocket handoff",
      diagnostics: updateDiagnostics(safeState?.diagnostics, {
        pendingSocketRestoredAt: restoredAt,
        websocketUrlPresent: true,
        websocketUrlLength: pending.websocketUrlLength || websocketUrlMetadata(pending.websocketUrl).websocketUrlLength,
        websocketUrlHashPrefix: pending.websocketUrlHashPrefix || websocketUrlMetadata(pending.websocketUrl).websocketUrlHashPrefix
      }),
      updatedAt: restoredAt
    }
  };
  volatileByTab.set(tabId, state);
  await publishSafeState(state.safe);
  await retryPendingSocketHandoff(tabId);
}

async function retryPendingSocketHandoff(tabId = RESTORED_HANDOFF_TAB_ID) {
  const state = volatileByTab.get(tabId);
  if (!state?.matchId || !state?.matchUrl || !state?.websocketUrl) return;
  const retryAt = new Date().toISOString();
  const metadata = websocketUrlMetadata(state.websocketUrl);
  console.info("retrying websocket handoff", {
    matchId: state.matchId,
    matchUrl: state.matchUrl,
    hasWebSocketUrl: true,
    websocketUrlLength: metadata.websocketUrlLength,
    websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
  });
  state.safe = diagnosticsSafeState(state.safe, {
    pendingSocketRetryAt: retryAt,
    websocketUrlPresent: metadata.websocketUrlPresent,
    websocketUrlLength: metadata.websocketUrlLength,
    websocketUrlHashPrefix: metadata.websocketUrlHashPrefix
  });
  volatileByTab.set(tabId, state);
  await publishSafeState(state.safe);
  await trySubscribe(tabId);
}

async function heartbeatActiveMatch() {
  if (heartbeatInFlight) return;
  heartbeatInFlight = true;
  try {
    const [{ safeState = {} }, storedPending] = await Promise.all([
      chrome.storage.local.get("safeState"),
      chrome.storage.session.get(PENDING_SOCKET_HANDOFF_KEY)
    ]);
    const pending = storedPending[PENDING_SOCKET_HANDOFF_KEY];

    if (pending?.matchId && pending?.matchUrl && pending?.websocketUrl) {
      const tabId = Number.isInteger(pending.tabId) ? pending.tabId : RESTORED_HANDOFF_TAB_ID;
      if (!volatileByTab.has(tabId)) {
        volatileByTab.set(tabId, {
          matchId: pending.matchId,
          matchUrl: pending.matchUrl,
          websocketUrl: pending.websocketUrl,
          safe: {
            ...(safeState || {}),
            currentMatchDetected: true,
            matchId: pending.matchId,
            matchUrl: pending.matchUrl,
            diagnostics: updateDiagnostics(safeState?.diagnostics),
            updatedAt: new Date().toISOString()
          }
        });
      }
      await retryPendingSocketHandoff(tabId);
      return;
    }

    const matchId = safeState.matchId || null;
    if (!matchId) return;
    const handoffSent = Boolean(safeState.diagnostics?.websocketHandoffSentAt || safeState.diagnostics?.sentLiveSessionAt);
    await sendHelperHeartbeat(handoffSent ? "live_session_sent" : "waiting_for_live_socket", matchId, safeState.diagnostics);
    await publishSafeState(diagnosticsSafeState(safeState, {
      workerHeartbeatAt: new Date().toISOString()
    }));
  } finally {
    heartbeatInFlight = false;
  }
}

async function rememberWorkerStarted() {
  const { safeState = {} } = await chrome.storage.local.get("safeState");
  await publishSafeState(diagnosticsSafeState(safeState, {
    workerStartedAt: new Date().toISOString()
  }));
}

async function handoffMatchOnly(state) {
  if (!state?.matchId || !state?.matchUrl) return;
  try {
    state.safe = diagnosticsSafeState(state.safe, {
      bridgeFetchStartedAt: new Date().toISOString(),
      bridgeFetchPath: "/autodarts-session-handoff",
      bridgeFetchStatusCode: "",
      bridgeFetchExceptionText: ""
    });
    console.info("bridge fetch started", { path: "/autodarts-session-handoff", target: BRIDGE_BASE_URL });
    const response = await postToBridge("/autodarts-session-handoff", {
      matchId: state.matchId,
      matchUrl: state.matchUrl,
      sessionStatus: "match_detected"
    });
    await markBridgeFetchSuccessForState(state, "/autodarts-session-handoff", response.statusCode || 202);
    const matchStatus = await fetchMatchStatus(state.matchId).catch(() => null);
    await publishSafeState({
      ...(state.safe || {}),
      bridgeOnline: true,
      bridgeConnected: false,
      lastHandoffStatus: state.websocketUrl
        ? matchStatus?.state || "match_detected_waiting_for_socket"
        : "Waiting for Autodarts live connection",
      diagnostics: updateDiagnostics(state.safe?.diagnostics, state.websocketUrl ? {} : {
        lastError: "Match detected but live socket not captured yet."
      }),
      eventCount: matchStatus?.receivedCount || 0,
      updatedAt: new Date().toISOString()
    });
  } catch (error) {
    const lastError = bridgeFetchErrorText(error);
    state.safe = diagnosticsSafeState(state.safe, {
      bridgeFetchFailedAt: new Date().toISOString(),
      bridgeFetchPath: "/autodarts-session-handoff",
      bridgeFetchStatusCode: error?.statusCode ? String(error.statusCode) : "",
      bridgeFetchExceptionText: lastError,
      lastError
    });
    console.warn("bridge fetch failed", {
      path: "/autodarts-session-handoff",
      statusCode: error?.statusCode || null,
      exceptionText: lastError
    });
    await publishSafeState({
      ...(state.safe || {}),
      bridgeOnline: false,
      bridgeConnected: false,
      lastHandoffStatus: `Bridge handoff failed: ${lastError}`,
      diagnostics: updateDiagnostics(state.safe?.diagnostics, { lastError }),
      eventCount: 0,
      updatedAt: new Date().toISOString()
    });
  }
}

async function getPopupState() {
  const stored = await chrome.storage.local.get("safeState");
  const safeState = stored.safeState || {};
  if (safeState.matchId) {
    try {
      const healthStartedAt = new Date().toISOString();
      await publishSafeState(diagnosticsSafeState(safeState, {
        bridgeFetchStartedAt: healthStartedAt,
        bridgeFetchPath: "/health",
        bridgeFetchStatusCode: "",
        bridgeFetchExceptionText: ""
      }));
      console.info("bridge fetch started", { path: "/health", target: BRIDGE_BASE_URL });
      const health = await fetchHealth();
      const status = await fetchMatchStatus(safeState.matchId);
      const socketHandoffSent = Boolean(safeState.diagnostics?.websocketHandoffSentAt || safeState.diagnostics?.sentLiveSessionAt);
      const healthDiagnostics = updateDiagnostics(safeState.diagnostics, {
        bridgeFetchStartedAt: healthStartedAt,
        bridgeFetchSuccessAt: new Date().toISOString(),
        bridgeFetchPath: "/health",
        bridgeFetchStatusCode: String(health.statusCode || 200),
        bridgeFetchExceptionText: "",
        lastError: ""
      });
      const updated = {
        ...safeState,
        bridgeOnline: true,
        bridgeConnected: socketHandoffSent && status.matchConnected === true,
        lastHandoffStatus: socketHandoffSent
          ? status.state || safeState.lastHandoffStatus || "Bridge online"
          : "Waiting for Autodarts live connection",
        diagnostics: healthDiagnostics,
        eventCount: status.receivedCount || 0,
        updatedAt: new Date().toISOString()
      };
      await publishSafeState(updated);
      return updated;
    } catch (error) {
      const lastError = bridgeFetchErrorText(error);
      const failed = diagnosticsSafeState(safeState, {
        bridgeFetchFailedAt: new Date().toISOString(),
        bridgeFetchPath: "/health",
        bridgeFetchStatusCode: error?.statusCode ? String(error.statusCode) : "",
        bridgeFetchExceptionText: lastError,
        lastError
      });
      await publishSafeState(failed);
      return {
        ...safeState,
        bridgeOnline: false,
        bridgeConnected: false,
        diagnostics: failed.diagnostics,
        updatedAt: new Date().toISOString()
      };
    }
  }

  const bridgeOnline = await fetchHealth().then(() => true).catch(() => false);
  return {
    bridgeOnline,
    bridgeConnected: false,
    currentMatchDetected: false,
    lastHandoffStatus: bridgeOnline ? "Bridge online. Waiting for Autodarts match." : "Bridge offline.",
    diagnostics: updateDiagnostics(safeState.diagnostics),
    eventCount: 0,
    updatedAt: new Date().toISOString()
  };
}

async function postToBridge(path, body) {
  const headers = { "content-type": "application/json" };

  try {
    const response = await fetch(`${BRIDGE_BASE_URL}${path}`, {
      method: "POST",
      headers,
      body: JSON.stringify(body)
    });
    const json = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw bridgeFetchError(path, response.status, json.error || `HTTP ${response.status}`);
    }
    return { ...json, statusCode: response.status };
  } catch (error) {
    if (error?.bridgeFetch) throw error;
    throw bridgeFetchError(path, null, error?.message || String(error), error);
  }
}

async function fetchHealth() {
  try {
    const response = await fetch(`${BRIDGE_BASE_URL}/health`);
    if (!response.ok) throw bridgeFetchError("/health", response.status, `HTTP ${response.status}`);
    const json = await response.json();
    return { ...json, statusCode: response.status };
  } catch (error) {
    if (error?.bridgeFetch) throw error;
    throw bridgeFetchError("/health", null, error?.message || String(error), error);
  }
}

async function fetchMatchStatus(matchId) {
  const response = await fetch(`${BRIDGE_BASE_URL}/match-status?matchId=${encodeURIComponent(matchId)}`);
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

async function sendHelperHeartbeat(status, matchId = null, diagnostics = null) {
  try {
    const safeStatus = status === "match_detected" ? "waiting_for_live_socket" : status;
    await fetch(`${BRIDGE_BASE_URL}/helper-heartbeat`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        status: safeStatus,
        matchId,
        version: chrome.runtime.getManifest().version,
        diagnostics: diagnostics ? sanitizeDiagnosticsForBridge(diagnostics) : undefined
      })
    });
  } catch {
    // The companion may not be running yet. Popup state still shows bridge offline.
  }
}

function sanitizeDiagnosticsForBridge(diagnostics) {
  const safe = { ...(diagnostics || {}) };
  delete safe.websocketUrl;
  return safe;
}

async function publishSafeState(safeState) {
  const { websocketUrl, ...safeWithoutSocket } = safeState || {};
  await chrome.storage.local.set({ safeState: safeWithoutSocket });
}

async function markBridgeFetchStarted(tabId, path) {
  const state = volatileByTab.get(tabId) || {};
  const safe = diagnosticsSafeState(state.safe, {
    bridgeFetchStartedAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: "",
    bridgeFetchExceptionText: ""
  });
  state.safe = safe;
  volatileByTab.set(tabId, state);
  await publishSafeState(safe);
  console.info("bridge fetch started", { path, target: BRIDGE_BASE_URL });
}

async function markBridgeFetchSuccess(tabId, path, statusCode) {
  const state = volatileByTab.get(tabId) || {};
  const safe = diagnosticsSafeState(state.safe, {
    bridgeFetchSuccessAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: String(statusCode || ""),
    bridgeFetchExceptionText: "",
    lastError: ""
  });
  state.safe = safe;
  volatileByTab.set(tabId, state);
  await publishSafeState(safe);
  console.info("bridge fetch success", { path, statusCode });
}

async function markBridgeFetchFailed(tabId, path, error) {
  const state = volatileByTab.get(tabId) || {};
  const safe = diagnosticsSafeState(state.safe, {
    bridgeFetchFailedAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: error?.statusCode ? String(error.statusCode) : "",
    bridgeFetchExceptionText: bridgeFetchErrorText(error),
    lastError: bridgeFetchErrorText(error)
  });
  state.safe = safe;
  volatileByTab.set(tabId, state);
  await publishSafeState(safe);
  console.warn("bridge fetch failed", {
    path,
    statusCode: error?.statusCode || null,
    exceptionText: bridgeFetchErrorText(error)
  });
}

async function markBridgeFetchStartedForSafeState(safeState, path) {
  await publishSafeState(diagnosticsSafeState(safeState, {
    bridgeFetchStartedAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: "",
    bridgeFetchExceptionText: ""
  }));
  console.info("bridge fetch started", { path, target: BRIDGE_BASE_URL });
}

async function markBridgeFetchSuccessForSafeState(safeState, path, statusCode) {
  await publishSafeState(diagnosticsSafeState(safeState, {
    bridgeFetchSuccessAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: String(statusCode || ""),
    bridgeFetchExceptionText: "",
    lastError: ""
  }));
  console.info("bridge fetch success", { path, statusCode });
}

async function markBridgeFetchFailedForSafeState(safeState, path, error) {
  await publishSafeState(diagnosticsSafeState(safeState, {
    bridgeFetchFailedAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: error?.statusCode ? String(error.statusCode) : "",
    bridgeFetchExceptionText: bridgeFetchErrorText(error),
    lastError: bridgeFetchErrorText(error)
  }));
  console.warn("bridge fetch failed", {
    path,
    statusCode: error?.statusCode || null,
    exceptionText: bridgeFetchErrorText(error)
  });
}

async function markBridgeFetchSuccessForState(state, path, statusCode) {
  state.safe = diagnosticsSafeState(state.safe, {
    bridgeFetchSuccessAt: new Date().toISOString(),
    bridgeFetchPath: path,
    bridgeFetchStatusCode: String(statusCode || ""),
    bridgeFetchExceptionText: "",
    lastError: ""
  });
  await publishSafeState(state.safe);
  console.info("bridge fetch success", { path, statusCode });
}

function diagnosticsSafeState(safeState, updates) {
  return {
    ...(safeState || {}),
    diagnostics: updateDiagnostics(safeState?.diagnostics, updates),
    updatedAt: new Date().toISOString()
  };
}

function stringField(value) {
  return typeof value === "string" ? value.trim() : "";
}

function createDiagnostics() {
  return {
    workerStartedAt: "",
    workerHeartbeatAt: "",
    pendingSocketStoredAt: "",
    pendingSocketRestoredAt: "",
    pendingSocketRetryAt: "",
    pageHookInjectedAt: "",
    constructorHookedAt: "",
    websocketInstanceDetectedAt: "",
    liveConnectionDetectedAt: "",
    websocketUrlCapturedAt: "",
    sentLiveSessionAt: "",
    websocketHandoffSentAt: "",
    reconnectDetectionAt: "",
    bridgeFetchStartedAt: "",
    bridgeFetchSuccessAt: "",
    bridgeFetchFailedAt: "",
    bridgeFetchPath: "",
    bridgeFetchStatusCode: "",
    bridgeFetchExceptionText: "",
    websocketUrlPresent: false,
    websocketUrlLength: 0,
    websocketUrlHashPrefix: "",
    pageSocketOpenAt: "",
    pageSocketCloseAt: "",
    pageSocketErrorAt: "",
    pageSocketCloseCode: "",
    pageMirrorAttachedAt: "",
    pageMirrorAttachFailedAt: "",
    pageMirrorAttachedCount: 0,
    pageMirroredMessagesCount: 0,
    pageMirroredPageCount: 0,
    pageMirroredForwardedCount: 0,
    pageMirroredBackendAcceptedCount: 0,
    pageMirroredLastFrameType: "",
    pageMirroredLastByteLength: 0,
    pageMirroredLastHashPrefix: "",
    pageMirroredLastAt: "",
    pageMirroredLastResponseAt: "",
    lastError: ""
  };
}

function updateDiagnostics(existing, updates = {}) {
  return {
    ...createDiagnostics(),
    ...(existing || {}),
    ...updates
  };
}

function safeError(value) {
  const text = stringField(String(value || "Unknown error"));
  return text.replace(/wss?:\/\/\S+/gi, "[redacted websocket]").slice(0, 180);
}

function websocketUrlMetadata(value) {
  const text = stringField(value);
  return {
    websocketUrlPresent: Boolean(text),
    websocketUrlLength: text.length,
    websocketUrlHashPrefix: safeHashPrefix(text)
  };
}

function safeHashPrefix(value) {
  let hash = 2166136261;
  for (let index = 0; index < value.length; index += 1) {
    hash ^= value.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return (hash >>> 0).toString(16).padStart(8, "0").slice(0, 8);
}

function bridgeFetchError(path, statusCode, message, cause = null) {
  const details = [
    `Bridge fetch failed for ${path}`,
    statusCode ? `status ${statusCode}` : "no status",
    message || "Unknown error",
    cause?.name ? `type ${cause.name}` : ""
  ].filter(Boolean).join(": ");
  const error = new Error(details);
  error.bridgeFetch = true;
  error.statusCode = statusCode || null;
  error.path = path;
  error.causeName = cause?.name || "";
  return error;
}

function bridgeFetchErrorText(error) {
  if (!error) return "Unknown bridge fetch error.";
  const text = [
    error.message || String(error),
    error.causeName ? `(${error.causeName})` : ""
  ].filter(Boolean).join(" ");
  return safeError(text);
}
