export const fetchJson = async url => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`${url} returned ${response.status} ${response.statusText}`);
  }
  return await response.json();
};

export class CdpSession {
  constructor(webSocketUrl, timeoutMs) {
    this.webSocketUrl = webSocketUrl;
    this.timeoutMs = timeoutMs;
    this.nextId = 1;
    this.pending = new Map();
    this.events = [];
  }

  async connect() {
    this.socket = new WebSocket(this.webSocketUrl);
    this.socket.addEventListener("message", event => this.onMessage(event));
    await new Promise((resolve, reject) => {
      const timer = setTimeout(
        () => reject(new Error(`Timed out connecting to ${this.webSocketUrl}`)),
        this.timeoutMs,
      );
      this.socket.addEventListener("open", () => {
        clearTimeout(timer);
        resolve();
      }, { once: true });
      this.socket.addEventListener("error", error => {
        clearTimeout(timer);
        reject(new Error(`WebSocket connection failed: ${error.message || error.type}`));
      }, { once: true });
    });
  }

  onMessage(event) {
    const message = JSON.parse(event.data);
    if (message.id && this.pending.has(message.id)) {
      const { resolve, reject } = this.pending.get(message.id);
      this.pending.delete(message.id);
      if (message.error) {
        reject(new Error(message.error.message || JSON.stringify(message.error)));
      } else {
        resolve(message.result ?? {});
      }
      return;
    }
    if (message.method) {
      this.events.push(message);
    }
  }

  send(method, params = {}) {
    const id = this.nextId;
    this.nextId += 1;
    const payload = JSON.stringify({ id, method, params });
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pending.delete(id);
        reject(new Error(`Timed out waiting for CDP method ${method}`));
      }, this.timeoutMs);
      this.pending.set(id, {
        resolve: result => {
          clearTimeout(timer);
          resolve(result);
        },
        reject: error => {
          clearTimeout(timer);
          reject(error);
        },
      });
      this.socket.send(payload);
    });
  }

  close() {
    this.socket?.close();
  }
}

export const createPageTarget = async cdpUrl => {
  const response = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/new?about:blank`, {
    method: "PUT",
  });
  if (!response.ok) {
    throw new Error(`CDP target creation failed: ${response.status} ${response.statusText}`);
  }
  return await response.json();
};

export const closePageTarget = async ({ cdpUrl, id, timeoutMs, sleep }) => {
  try {
    const base = cdpUrl.replace(/\/+$/, "");
    const response = await fetch(`${base}/json/close/${encodeURIComponent(id)}`);
    if (!response.ok) return false;
    const deadline = Date.now() + Math.min(timeoutMs, 2000);
    while (Date.now() < deadline) {
      await sleep(100);
      const targets = await fetchJson(`${base}/json/list`);
      if (!targets.some(target => target?.id === id)) {
        return true;
      }
    }
    return false;
  } catch (_) {
    return false;
  }
};

export const evaluate = async (session, expression) => {
  const result = await session.send("Runtime.evaluate", {
    expression,
    returnByValue: true,
    awaitPromise: true,
  });
  if (result.exceptionDetails) {
    throw new Error(
      result.exceptionDetails.text || result.exceptionDetails.exception?.description || "evaluation failed",
    );
  }
  return result.result?.value;
};