import type { IncomingMessage } from "node:http";
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import type { Duplex } from "node:stream";
import WebSocket, { WebSocketServer } from "ws";
import { isLoopbackAddress, isLoopbackHost } from "../gateway/net.js";
import { rawDataToString } from "../infra/ws.js";
import {
  probeAuthenticatedOpenClawRelay,
  resolveRelayAcceptedTokensForPort,
  resolveRelayAuthTokenForPort,
} from "./extension-relay-auth.js";

type CdpCommand = {
  id: number;
  method: string;
  params?: unknown;
  sessionId?: string;
};

type CdpResponse = {
  id: number;
  result?: unknown;
  error?: { message: string };
  sessionId?: string;
};

type CdpEvent = {
  method: string;
  params?: unknown;
  sessionId?: string;
};

type ExtensionForwardCommandMessage = {
  id: number;
  method: "forwardCDPCommand";
  params: { method: string; params?: unknown; sessionId?: string };
};

type ExtensionResponseMessage = {
  id: number;
  result?: unknown;
  error?: string;
};

type ExtensionForwardEventMessage = {
  method: "forwardCDPEvent";
  params: { method: string; params?: unknown; sessionId?: string };
};

type ExtensionPingMessage = { method: "ping" };
type ExtensionPongMessage = { method: "pong" };

type ExtensionMessage =
  | ExtensionResponseMessage
  | ExtensionForwardEventMessage
  | ExtensionPongMessage;

type TargetInfo = {
  targetId: string;
  type?: string;
  title?: string;
  url?: string;
  attached?: boolean;
};

type AttachedToTargetEvent = {
  sessionId: string;
  targetInfo: TargetInfo;
  waitingForDebugger?: boolean;
};

type DetachedFromTargetEvent = {
  sessionId: string;
  targetId?: string;
};

type ConnectedTarget = {
  sessionId: string;
  targetId: string;
  targetInfo: TargetInfo;
};

const RELAY_AUTH_HEADER = "x-openclaw-relay-token";
const DEFAULT_EXTENSION_RECONNECT_GRACE_MS = 20_000;
const DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS = 3_000;

function headerValue(value: string | string[] | undefined): string | undefined {
  if (!value) {
    return undefined;
  }
  if (Array.isArray(value)) {
    return value[0];
  }
  return value;
}

function getHeader(req: IncomingMessage, name: string): string | undefined {
  return headerValue(req.headers[name.toLowerCase()]);
}

function getRelayAuthTokenFromRequest(req: IncomingMessage, url?: URL): string | undefined {
  const headerToken = getHeader(req, RELAY_AUTH_HEADER)?.trim();
  if (headerToken) {
    return headerToken;
  }
  const queryToken = url?.searchParams.get("token")?.trim();
  if (queryToken) {
    return queryToken;
  }
  return undefined;
}

export type ChromeExtensionRelayServer = {
  host: string;
  bindHost: string;
  port: number;
  baseUrl: string;
  cdpWsUrl: string;
  extensionConnected: () => boolean;
  stop: () => Promise<void>;
};

type RelayRuntime = {
  server: ChromeExtensionRelayServer;
  relayAuthToken: string;
};

function parseUrlPort(parsed: URL): number | null {
  const port =
    parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
  if (!Number.isFinite(port) || port <= 0 || port > 65535) {
    return null;
  }
  return port;
}

function parseBaseUrl(raw: string): {
  host: string;
  port: number;
  baseUrl: string;
} {
  const parsed = new URL(raw.trim().replace(/\/$/, ""));
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    throw new Error(`extension relay cdpUrl must be http(s), got ${parsed.protocol}`);
  }
  const host = parsed.hostname;
  const port = parseUrlPort(parsed);
  if (!port) {
    throw new Error(`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`);
  }
  return { host, port, baseUrl: parsed.toString().replace(/\/$/, "") };
}

function text(res: Duplex, status: number, bodyText: string) {
  const body = Buffer.from(bodyText);
  res.write(
    `HTTP/1.1 ${status} ${status === 200 ? "OK" : "ERR"}\r\n` +
      "Content-Type: text/plain; charset=utf-8\r\n" +
      `Content-Length: ${body.length}\r\n` +
      "Connection: close\r\n" +
      "\r\n",
  );
  res.write(body);
  res.end();
}

function rejectUpgrade(socket: Duplex, status: number, bodyText: string) {
  text(socket, status, bodyText);
  try {
    socket.destroy();
  } catch {
    // ignore
  }
}

function envMsOrDefault(name: string, fallback: number): number {
  const raw = process.env[name];
  if (!raw || raw.trim() === "") {
    return fallback;
  }
  const parsed = Number.parseInt(raw, 10);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    return fallback;
  }
  return parsed;
}

const relayRuntimeByPort = new Map<number, RelayRuntime>();
const relayInitByPort = new Map<number, Promise<ChromeExtensionRelayServer>>();

function isAddrInUseError(err: unknown): boolean {
  return (
    typeof err === "object" &&
    err !== null &&
    "code" in err &&
    (err as { code?: unknown }).code === "EADDRINUSE"
  );
}

function relayAuthTokenForUrl(url: string): string | null {
  try {
    const parsed = new URL(url);
    if (!isLoopbackHost(parsed.hostname)) {
      return null;
    }
    const port = parseUrlPort(parsed);
    if (!port) {
      return null;
    }
    return relayRuntimeByPort.get(port)?.relayAuthToken ?? null;
  } catch {
    return null;
  }
}

export function getChromeExtensionRelayAuthHeaders(url: string): Record<string, string> {
  const token = relayAuthTokenForUrl(url);
  if (!token) {
    return {};
  }
  return { [RELAY_AUTH_HEADER]: token };
}

export async function ensureChromeExtensionRelayServer(opts: {
  cdpUrl: string;
  bindHost?: string;
}): Promise<ChromeExtensionRelayServer> {
  const info = parseBaseUrl(opts.cdpUrl);
  if (!isLoopbackHost(info.host)) {
    throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
  }
  const bindHost = opts.bindHost ?? info.host;

  const existing = relayRuntimeByPort.get(info.port);
  if (existing) {
    if (existing.server.bindHost !== bindHost) {
      await existing.server.stop();
    } else {
      return existing.server;
    }
  }

  const inFlight = relayInitByPort.get(info.port);
  if (inFlight) {
    const server = await inFlight;
    if (server.bindHost === bindHost) {
      return server;
    }
    await server.stop();
  }

  const extensionReconnectGraceMs = envMsOrDefault(
    "OPENCLAW_EXTENSION_RELAY_RECONNECT_GRACE_MS",
    DEFAULT_EXTENSION_RECONNECT_GRACE_MS,
  );
  const extensionCommandReconnectWaitMs = envMsOrDefault(
    "OPENCLAW_EXTENSION_RELAY_COMMAND_RECONNECT_WAIT_MS",
    DEFAULT_EXTENSION_COMMAND_RECONNECT_WAIT_MS,
  );

  const initPromise = (async (): Promise<ChromeExtensionRelayServer> => {
    const relayAuthToken = await resolveRelayAuthTokenForPort(info.port);
    const relayAuthTokens = new Set(await resolveRelayAcceptedTokensForPort(info.port));

    let extensionWs: WebSocket | null = null;
    const cdpClients = new Set<WebSocket>();
    const connectedTargets = new Map<string, ConnectedTarget>();
    const extensionConnected = () => extensionWs?.readyState === WebSocket.OPEN;
    const hasConnectedTargets = () => connectedTargets.size > 0;
    let extensionDisconnectCleanupTimer: NodeJS.Timeout | null = null;
    const extensionReconnectWaiters = new Set<(connected: boolean) => void>();

    const flushExtensionReconnectWaiters = (connected: boolean) => {
      if (extensionReconnectWaiters.size === 0) {
        return;
      }
      const waiters = Array.from(extensionReconnectWaiters);
      extensionReconnectWaiters.clear();
      for (const waiter of waiters) {
        waiter(connected);
      }
    };

    const clearExtensionDisconnectCleanupTimer = () => {
      if (!extensionDisconnectCleanupTimer) {
        return;
      }
      clearTimeout(extensionDisconnectCleanupTimer);
      extensionDisconnectCleanupTimer = null;
    };

    const closeCdpClientsAfterExtensionDisconnect = () => {
      connectedTargets.clear();
      for (const client of cdpClients) {
        try {
          client.close(1011, "extension disconnected");
        } catch {
          // ignore
        }
      }
      cdpClients.clear();
      flushExtensionReconnectWaiters(false);
    };

    const scheduleExtensionDisconnectCleanup = () => {
      clearExtensionDisconnectCleanupTimer();
      extensionDisconnectCleanupTimer = setTimeout(() => {
        extensionDisconnectCleanupTimer = null;
        if (extensionConnected()) {
          return;
        }
        closeCdpClientsAfterExtensionDisconnect();
      }, extensionReconnectGraceMs);
    };

    const waitForExtensionReconnect = async (timeoutMs: number): Promise<boolean> => {
      if (extensionConnected()) {
        return true;
      }
      return await new Promise<boolean>((resolve) => {
        let settled = false;
        const waiter = (connected: boolean) => {
          if (settled) {
            return;
          }
          settled = true;
          clearTimeout(timer);
          extensionReconnectWaiters.delete(waiter);
          resolve(connected);
        };
        const timer = setTimeout(() => {
          waiter(false);
        }, timeoutMs);
        extensionReconnectWaiters.add(waiter);
      });
    };

    const pendingExtension = new Map<
      number,
      {
        resolve: (v: unknown) => void;
        reject: (e: Error) => void;
        timer: NodeJS.Timeout;
      }
    >();
    let nextExtensionId = 1;

    const sendToExtension = async (payload: ExtensionForwardCommandMessage): Promise<unknown> => {
      const ws = extensionWs;
      if (!ws || ws.readyState !== WebSocket.OPEN) {
        throw new Error("Chrome extension not connected");
      }
      ws.send(JSON.stringify(payload));
      return await new Promise<unknown>((resolve, reject) => {
        const timer = setTimeout(() => {
          pendingExtension.delete(payload.id);
          reject(new Error(`extension request timeout: ${payload.params.method}`));
        }, 30_000);
        pendingExtension.set(payload.id, { resolve, reject, timer });
      });
    };

    const broadcastToCdpClients = (evt: CdpEvent) => {
      const msg = JSON.stringify(evt);
      for (const ws of cdpClients) {
        if (ws.readyState !== WebSocket.OPEN) {
          continue;
        }
        ws.send(msg);
      }
    };

    const sendResponseToCdp = (ws: WebSocket, res: CdpResponse) => {
      if (ws.readyState !== WebSocket.OPEN) {
        return;
      }
      ws.send(JSON.stringify(res));
    };

    const dropConnectedTargetSession = (sessionId: string): ConnectedTarget | undefined => {
      const existing = connectedTargets.get(sessionId);
      if (!existing) {
        return undefined;
      }
      connectedTargets.delete(sessionId);
      return existing;
    };

    const dropConnectedTargetsByTargetId = (targetId: string): ConnectedTarget[] => {
      const removed: ConnectedTarget[] = [];
      for (const [sessionId, target] of connectedTargets) {
        if (target.targetId !== targetId) {
          continue;
        }
        connectedTargets.delete(sessionId);
        removed.push(target);
      }
      return removed;
    };

    const broadcastDetachedTarget = (target: ConnectedTarget, targetId?: string) => {
      broadcastToCdpClients({
        method: "Target.detachedFromTarget",
        params: {
          sessionId: target.sessionId,
          targetId: targetId ?? target.targetId,
        },
        sessionId: target.sessionId,
      });
    };

    const isMissingTargetError = (err: unknown) => {
      const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
      return (
        message.includes("target not found") ||
        message.includes("no target with given id") ||
        message.includes("session not found") ||
        message.includes("cannot find session")
      );
    };

    const pruneStaleTargetsFromCommandFailure = (cmd: CdpCommand, err: unknown) => {
      if (!isMissingTargetError(err)) {
        return;
      }
      if (cmd.sessionId) {
        const removed = dropConnectedTargetSession(cmd.sessionId);
        if (removed) {
          broadcastDetachedTarget(removed);
          return;
        }
      }
      const params = (cmd.params ?? {}) as { targetId?: unknown };
      const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
      if (!targetId) {
        return;
      }
      const removedTargets = dropConnectedTargetsByTargetId(targetId);
      for (const removed of removedTargets) {
        broadcastDetachedTarget(removed, targetId);
      }
    };

    const ensureTargetEventsForClient = (ws: WebSocket, mode: "autoAttach" | "discover") => {
      for (const target of connectedTargets.values()) {
        if (mode === "autoAttach") {
          ws.send(
            JSON.stringify({
              method: "Target.attachedToTarget",
              params: {
                sessionId: target.sessionId,
                targetInfo: { ...target.targetInfo, attached: true },
                waitingForDebugger: false,
              },
            } satisfies CdpEvent),
          );
        } else {
          ws.send(
            JSON.stringify({
              method: "Target.targetCreated",
              params: { targetInfo: { ...target.targetInfo, attached: true } },
            } satisfies CdpEvent),
          );
        }
      }
    };

    const routeCdpCommand = async (cmd: CdpCommand): Promise<unknown> => {
      switch (cmd.method) {
        case "Browser.getVersion":
          return {
            protocolVersion: "1.3",
            product: "Chrome/OpenClaw-Extension-Relay",
            revision: "0",
            userAgent: "OpenClaw-Extension-Relay",
            jsVersion: "V8",
          };
        case "Browser.setDownloadBehavior":
          return {};
        case "Target.setAutoAttach":
        case "Target.setDiscoverTargets":
          return {};
        case "Target.getTargets":
          return {
            targetInfos: Array.from(connectedTargets.values()).map((t) => ({
              ...t.targetInfo,
              attached: true,
            })),
          };
        case "Target.getTargetInfo": {
          const params = (cmd.params ?? {}) as { targetId?: string };
          const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
          if (targetId) {
            for (const t of connectedTargets.values()) {
              if (t.targetId === targetId) {
                return { targetInfo: t.targetInfo };
              }
            }
          }
          if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
            const t = connectedTargets.get(cmd.sessionId);
            if (t) {
              return { targetInfo: t.targetInfo };
            }
          }
          const first = Array.from(connectedTargets.values())[0];
          return { targetInfo: first?.targetInfo };
        }
        case "Target.attachToTarget": {
          const params = (cmd.params ?? {}) as { targetId?: string };
          const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
          if (!targetId) {
            throw new Error("targetId required");
          }
          for (const t of connectedTargets.values()) {
            if (t.targetId === targetId) {
              return { sessionId: t.sessionId };
            }
          }
          throw new Error("target not found");
        }
        default: {
          const id = nextExtensionId++;
          return await sendToExtension({
            id,
            method: "forwardCDPCommand",
            params: {
              method: cmd.method,
              sessionId: cmd.sessionId,
              params: cmd.params,
            },
          });
        }
      }
    };

    const server = createServer((req, res) => {
      const url = new URL(req.url ?? "/", info.baseUrl);
      const path = url.pathname;
      const origin = getHeader(req, "origin");
      const isChromeExtensionOrigin =
        typeof origin === "string" && origin.startsWith("chrome-extension://");

      if (isChromeExtensionOrigin && origin) {
        // Let extension pages call relay HTTP endpoints cross-origin.
        res.setHeader("Access-Control-Allow-Origin", origin);
        res.setHeader("Vary", "Origin");
      }

      // Handle CORS preflight requests from the browser extension.
      if (req.method === "OPTIONS") {
        if (origin && !isChromeExtensionOrigin) {
          res.writeHead(403);
          res.end("Forbidden");
          return;
        }
        const requestedHeaders = (getHeader(req, "access-control-request-headers") ?? "")
          .split(",")
          .map((header) => header.trim().toLowerCase())
          .filter((header) => header.length > 0);
        const allowedHeaders = new Set(["content-type", RELAY_AUTH_HEADER, ...requestedHeaders]);
        res.writeHead(204, {
          "Access-Control-Allow-Origin": origin ?? "*",
          "Access-Control-Allow-Methods": "GET, PUT, POST, OPTIONS",
          "Access-Control-Allow-Headers": Array.from(allowedHeaders).join(", "),
          "Access-Control-Max-Age": "86400",
          Vary: "Origin, Access-Control-Request-Headers",
        });
        res.end();
        return;
      }

      if (path.startsWith("/json")) {
        const token = getRelayAuthTokenFromRequest(req, url);
        if (!token || !relayAuthTokens.has(token)) {
          res.writeHead(401);
          res.end("Unauthorized");
          return;
        }
      }

      if (req.method === "HEAD" && path === "/") {
        res.writeHead(200);
        res.end();
        return;
      }

      if (path === "/") {
        res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
        res.end("OK");
        return;
      }

      if (path === "/extension/status") {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ connected: extensionConnected() }));
        return;
      }

      const hostHeader = req.headers.host?.trim() || `${info.host}:${info.port}`;
      const wsHost = `ws://${hostHeader}`;
      const cdpWsUrl = `${wsHost}/cdp`;

      if (
        (path === "/json/version" || path === "/json/version/") &&
        (req.method === "GET" || req.method === "PUT")
      ) {
        const payload: Record<string, unknown> = {
          Browser: "OpenClaw/extension-relay",
          "Protocol-Version": "1.3",
        };
        // Keep reporting CDP WS while attached targets are cached, so callers can
        // reconnect through brief MV3 worker disconnects.
        if (extensionConnected() || hasConnectedTargets()) {
          payload.webSocketDebuggerUrl = cdpWsUrl;
        }
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(payload));
        return;
      }

      const listPaths = new Set(["/json", "/json/", "/json/list", "/json/list/"]);
      if (listPaths.has(path) && (req.method === "GET" || req.method === "PUT")) {
        const list = Array.from(connectedTargets.values()).map((t) => ({
          id: t.targetId,
          type: t.targetInfo.type ?? "page",
          title: t.targetInfo.title ?? "",
          description: t.targetInfo.title ?? "",
          url: t.targetInfo.url ?? "",
          webSocketDebuggerUrl: cdpWsUrl,
          devtoolsFrontendUrl: `/devtools/inspector.html?ws=${cdpWsUrl.replace("ws://", "")}`,
        }));
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(list));
        return;
      }

      const handleTargetActionRoute = (
        match: RegExpMatchArray | null,
        cdpMethod: "Target.activateTarget" | "Target.closeTarget",
      ): boolean => {
        if (!match || (req.method !== "GET" && req.method !== "PUT")) {
          return false;
        }
        let targetId = "";
        try {
          targetId = decodeURIComponent(match[1] ?? "").trim();
        } catch {
          res.writeHead(400);
          res.end("invalid targetId encoding");
          return true;
        }
        if (!targetId) {
          res.writeHead(400);
          res.end("targetId required");
          return true;
        }
        void (async () => {
          try {
            await sendToExtension({
              id: nextExtensionId++,
              method: "forwardCDPCommand",
              params: { method: cdpMethod, params: { targetId } },
            });
          } catch {
            // ignore
          }
        })();
        res.writeHead(200);
        res.end("OK");
        return true;
      };

      if (
        handleTargetActionRoute(path.match(/^\/json\/activate\/(.+)$/), "Target.activateTarget")
      ) {
        return;
      }
      if (handleTargetActionRoute(path.match(/^\/json\/close\/(.+)$/), "Target.closeTarget")) {
        return;
      }

      res.writeHead(404);
      res.end("not found");
    });

    const wssExtension = new WebSocketServer({ noServer: true });
    const wssCdp = new WebSocketServer({ noServer: true });

    server.on("upgrade", (req, socket, head) => {
      const url = new URL(req.url ?? "/", info.baseUrl);
      const pathname = url.pathname;
      const remote = req.socket.remoteAddress;

      // When bindHost is explicitly non-loopback (e.g. 0.0.0.0 for WSL2),
      // allow non-loopback connections; otherwise enforce loopback-only.
      if (!isLoopbackAddress(remote) && isLoopbackHost(bindHost)) {
        rejectUpgrade(socket, 403, "Forbidden");
        return;
      }

      const origin = headerValue(req.headers.origin);
      if (origin && !origin.startsWith("chrome-extension://")) {
        rejectUpgrade(socket, 403, "Forbidden: invalid origin");
        return;
      }

      if (pathname === "/extension") {
        const token = getRelayAuthTokenFromRequest(req, url);
        if (!token || !relayAuthTokens.has(token)) {
          rejectUpgrade(socket, 401, "Unauthorized");
          return;
        }
        // MV3 worker reconnect races can leave a stale non-OPEN socket reference.
        if (extensionWs && extensionWs.readyState !== WebSocket.OPEN) {
          try {
            extensionWs.terminate();
          } catch {
            // ignore
          }
          extensionWs = null;
        }
        if (extensionConnected()) {
          rejectUpgrade(socket, 409, "Extension already connected");
          return;
        }
        wssExtension.handleUpgrade(req, socket, head, (ws) => {
          wssExtension.emit("connection", ws, req);
        });
        return;
      }

      if (pathname === "/cdp") {
        const token = getRelayAuthTokenFromRequest(req, url);
        if (!token || !relayAuthTokens.has(token)) {
          rejectUpgrade(socket, 401, "Unauthorized");
          return;
        }
        // Allow CDP clients to connect even during brief extension worker drops.
        // Individual commands already wait briefly for extension reconnect.
        wssCdp.handleUpgrade(req, socket, head, (ws) => {
          wssCdp.emit("connection", ws, req);
        });
        return;
      }

      rejectUpgrade(socket, 404, "Not Found");
    });

    wssExtension.on("connection", (ws) => {
      extensionWs = ws;
      clearExtensionDisconnectCleanupTimer();
      flushExtensionReconnectWaiters(true);

      const ping = setInterval(() => {
        if (ws.readyState !== WebSocket.OPEN) {
          return;
        }
        ws.send(JSON.stringify({ method: "ping" } satisfies ExtensionPingMessage));
      }, 5000);

      ws.on("message", (data) => {
        if (extensionWs !== ws) {
          return;
        }
        let parsed: ExtensionMessage | null = null;
        try {
          parsed = JSON.parse(rawDataToString(data)) as ExtensionMessage;
        } catch {
          return;
        }

        if (
          parsed &&
          typeof parsed === "object" &&
          "id" in parsed &&
          typeof parsed.id === "number"
        ) {
          const pending = pendingExtension.get(parsed.id);
          if (!pending) {
            return;
          }
          pendingExtension.delete(parsed.id);
          clearTimeout(pending.timer);
          if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) {
            pending.reject(new Error(parsed.error));
          } else {
            pending.resolve(parsed.result);
          }
          return;
        }

        if (parsed && typeof parsed === "object" && "method" in parsed) {
          if ((parsed as ExtensionPongMessage).method === "pong") {
            return;
          }
          if ((parsed as ExtensionForwardEventMessage).method !== "forwardCDPEvent") {
            return;
          }
          const evt = parsed as ExtensionForwardEventMessage;
          const method = evt.params?.method;
          const params = evt.params?.params;
          const sessionId = evt.params?.sessionId;
          if (!method || typeof method !== "string") {
            return;
          }

          if (method === "Target.attachedToTarget") {
            const attached = (params ?? {}) as AttachedToTargetEvent;
            const targetType = attached?.targetInfo?.type ?? "page";
            if (targetType !== "page") {
              return;
            }
            if (attached?.sessionId && attached?.targetInfo?.targetId) {
              const prev = connectedTargets.get(attached.sessionId);
              const nextTargetId = attached.targetInfo.targetId;
              const prevTargetId = prev?.targetId;
              const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
              connectedTargets.set(attached.sessionId, {
                sessionId: attached.sessionId,
                targetId: nextTargetId,
                targetInfo: attached.targetInfo,
              });
              if (changedTarget && prevTargetId) {
                broadcastToCdpClients({
                  method: "Target.detachedFromTarget",
                  params: { sessionId: attached.sessionId, targetId: prevTargetId },
                  sessionId: attached.sessionId,
                });
              }
              if (!prev || changedTarget) {
                broadcastToCdpClients({ method, params, sessionId });
              }
              return;
            }
          }

          if (method === "Target.detachedFromTarget") {
            const detached = (params ?? {}) as DetachedFromTargetEvent;
            if (detached?.sessionId) {
              dropConnectedTargetSession(detached.sessionId);
            } else if (detached?.targetId) {
              dropConnectedTargetsByTargetId(detached.targetId);
            }
            broadcastToCdpClients({ method, params, sessionId });
            return;
          }

          if (method === "Target.targetDestroyed" || method === "Target.targetCrashed") {
            const targetEvent = (params ?? {}) as { targetId?: string };
            if (targetEvent.targetId) {
              dropConnectedTargetsByTargetId(targetEvent.targetId);
            }
            broadcastToCdpClients({ method, params, sessionId });
            return;
          }

          // Keep cached tab metadata fresh for /json/list.
          // After navigation, Chrome updates URL/title via Target.targetInfoChanged.
          if (method === "Target.targetInfoChanged") {
            const changed = (params ?? {}) as { targetInfo?: { targetId?: string; type?: string } };
            const targetInfo = changed?.targetInfo;
            const targetId = targetInfo?.targetId;
            if (targetId && (targetInfo?.type ?? "page") === "page") {
              for (const [sid, target] of connectedTargets) {
                if (target.targetId !== targetId) {
                  continue;
                }
                connectedTargets.set(sid, {
                  ...target,
                  targetInfo: { ...target.targetInfo, ...(targetInfo as object) },
                });
              }
            }
          }

          broadcastToCdpClients({ method, params, sessionId });
        }
      });

      ws.on("close", () => {
        clearInterval(ping);
        if (extensionWs !== ws) {
          return;
        }
        extensionWs = null;
        for (const [, pending] of pendingExtension) {
          clearTimeout(pending.timer);
          pending.reject(new Error("extension disconnected"));
        }
        pendingExtension.clear();
        scheduleExtensionDisconnectCleanup();
      });
    });

    wssCdp.on("connection", (ws) => {
      cdpClients.add(ws);

      ws.on("message", async (data) => {
        let cmd: CdpCommand | null = null;
        try {
          cmd = JSON.parse(rawDataToString(data)) as CdpCommand;
        } catch {
          return;
        }
        if (!cmd || typeof cmd !== "object") {
          return;
        }
        if (typeof cmd.id !== "number" || typeof cmd.method !== "string") {
          return;
        }

        if (!extensionConnected()) {
          const reconnected = await waitForExtensionReconnect(extensionCommandReconnectWaitMs);
          if (!reconnected || !extensionConnected()) {
            sendResponseToCdp(ws, {
              id: cmd.id,
              sessionId: cmd.sessionId,
              error: { message: "Extension not connected" },
            });
            return;
          }
        }

        try {
          const result = await routeCdpCommand(cmd);

          if (cmd.method === "Target.setAutoAttach" && !cmd.sessionId) {
            ensureTargetEventsForClient(ws, "autoAttach");
          }
          if (cmd.method === "Target.setDiscoverTargets") {
            const discover = (cmd.params ?? {}) as { discover?: boolean };
            if (discover.discover === true) {
              ensureTargetEventsForClient(ws, "discover");
            }
          }
          if (cmd.method === "Target.attachToTarget") {
            const params = (cmd.params ?? {}) as { targetId?: string };
            const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
            if (targetId) {
              const target = Array.from(connectedTargets.values()).find(
                (t) => t.targetId === targetId,
              );
              if (target) {
                ws.send(
                  JSON.stringify({
                    method: "Target.attachedToTarget",
                    params: {
                      sessionId: target.sessionId,
                      targetInfo: { ...target.targetInfo, attached: true },
                      waitingForDebugger: false,
                    },
                  } satisfies CdpEvent),
                );
              }
            }
          }

          sendResponseToCdp(ws, { id: cmd.id, sessionId: cmd.sessionId, result });
        } catch (err) {
          pruneStaleTargetsFromCommandFailure(cmd, err);
          sendResponseToCdp(ws, {
            id: cmd.id,
            sessionId: cmd.sessionId,
            error: { message: err instanceof Error ? err.message : String(err) },
          });
        }
      });

      ws.on("close", () => {
        cdpClients.delete(ws);
      });
    });

    try {
      await new Promise<void>((resolve, reject) => {
        server.listen(info.port, bindHost, () => resolve());
        server.once("error", reject);
      });
    } catch (err) {
      if (
        isAddrInUseError(err) &&
        (await probeAuthenticatedOpenClawRelay({
          baseUrl: info.baseUrl,
          relayAuthHeader: RELAY_AUTH_HEADER,
          relayAuthToken,
        }))
      ) {
        const existingRelay: ChromeExtensionRelayServer = {
          host: info.host,
          bindHost,
          port: info.port,
          baseUrl: info.baseUrl,
          cdpWsUrl: `ws://${info.host}:${info.port}/cdp`,
          extensionConnected: () => false,
          stop: async () => {
            relayRuntimeByPort.delete(info.port);
          },
        };
        relayRuntimeByPort.set(info.port, { server: existingRelay, relayAuthToken });
        return existingRelay;
      }
      throw err;
    }

    const addr = server.address() as AddressInfo | null;
    const port = addr?.port ?? info.port;
    const actualBindHost = addr?.address || bindHost;
    const host = info.host;
    const baseUrl = `${new URL(info.baseUrl).protocol}//${host}:${port}`;

    const relay: ChromeExtensionRelayServer = {
      host,
      bindHost: actualBindHost,
      port,
      baseUrl,
      cdpWsUrl: `ws://${host}:${port}/cdp`,
      extensionConnected,
      stop: async () => {
        relayRuntimeByPort.delete(port);
        clearExtensionDisconnectCleanupTimer();
        flushExtensionReconnectWaiters(false);
        for (const [, pending] of pendingExtension) {
          clearTimeout(pending.timer);
          pending.reject(new Error("server stopping"));
        }
        pendingExtension.clear();
        try {
          extensionWs?.close(1001, "server stopping");
        } catch {
          // ignore
        }
        for (const ws of cdpClients) {
          try {
            ws.close(1001, "server stopping");
          } catch {
            // ignore
          }
        }
        await new Promise<void>((resolve) => {
          server.close(() => resolve());
        });
        wssExtension.close();
        wssCdp.close();
      },
    };

    relayRuntimeByPort.set(port, { server: relay, relayAuthToken });
    return relay;
  })();
  relayInitByPort.set(info.port, initPromise);
  try {
    return await initPromise;
  } finally {
    relayInitByPort.delete(info.port);
  }
}

export async function stopChromeExtensionRelayServer(opts: { cdpUrl: string }): Promise<boolean> {
  const info = parseBaseUrl(opts.cdpUrl);
  const existing = relayRuntimeByPort.get(info.port);
  if (!existing) {
    return false;
  }
  await existing.server.stop();
  return true;
}
