interface ElectronHttpRequest {
  url: string;
  method?: string;
  headers?: Record<string, string>;
  bodyText?: string;
  bodyBase64?: string;
}

interface ElectronHttpResponse {
  ok: boolean;
  status: number;
  statusText: string;
  headers: Record<string, string>;
  bodyBase64: string;
}

interface RuntimeResponse {
  ok: boolean;
  status: number;
  statusText: string;
  headers: {
    get(name: string): string | null;
  };
  text(): Promise<string>;
  json<T>(): Promise<T>;
  arrayBuffer(): Promise<ArrayBuffer>;
  blob(): Promise<Blob>;
}

const textDecoder = new TextDecoder();

const bytesToBase64 = (bytes: Uint8Array) => {
  let binary = '';

  for (let index = 0; index < bytes.length; index += 0x8000) {
    const chunk = bytes.subarray(index, index + 0x8000);
    binary += String.fromCharCode(...chunk);
  }

  return btoa(binary);
};

const base64ToBytes = (value: string) => {
  if (!value) {
    return new Uint8Array(0);
  }

  const binary = atob(value);
  const bytes = new Uint8Array(binary.length);

  for (let index = 0; index < binary.length; index += 1) {
    bytes[index] = binary.charCodeAt(index);
  }

  return bytes;
};

const normalizeHeaders = (headers: Record<string, string>) => {
  const normalized = new Map<string, string>();

  for (const [key, value] of Object.entries(headers)) {
    normalized.set(key.toLowerCase(), value);
  }

  return normalized;
};

const createRuntimeResponse = (response: ElectronHttpResponse): RuntimeResponse => {
  const headers = normalizeHeaders(response.headers);
  const bytes = base64ToBytes(response.bodyBase64);
  const contentType = headers.get('content-type') || '';

  return {
    ok: response.ok,
    status: response.status,
    statusText: response.statusText,
    headers: {
      get(name: string) {
        return headers.get(name.toLowerCase()) ?? null;
      },
    },
    async text() {
      return textDecoder.decode(bytes);
    },
    async json<T>() {
      return JSON.parse(textDecoder.decode(bytes)) as T;
    },
    async arrayBuffer() {
      return bytes.buffer.slice(
        bytes.byteOffset,
        bytes.byteOffset + bytes.byteLength,
      ) as ArrayBuffer;
    },
    async blob() {
      return new Blob([bytes], { type: contentType });
    },
  };
};

const headersToObject = (headers?: HeadersInit) => {
  if (!headers) {
    return {};
  }

  return Object.fromEntries(new Headers(headers).entries());
};

const serializeBody = async (body?: BodyInit | null) => {
  if (body == null) {
    return {};
  }

  if (typeof body === 'string') {
    return { bodyText: body };
  }

  if (body instanceof ArrayBuffer) {
    return { bodyBase64: bytesToBase64(new Uint8Array(body)) };
  }

  if (ArrayBuffer.isView(body)) {
    return {
      bodyBase64: bytesToBase64(
        new Uint8Array(body.buffer, body.byteOffset, body.byteLength),
      ),
    };
  }

  if (body instanceof Blob) {
    return {
      bodyBase64: bytesToBase64(new Uint8Array(await body.arrayBuffer())),
    };
  }

  throw new Error('Unsupported request body for Electron runtime.');
};

export const runtimeFetch = async (
  url: string,
  init: RequestInit = {},
): Promise<Response | RuntimeResponse> => {
  if (!window.electronDesktop?.httpRequest) {
    return fetch(url, init);
  }

  const payload: ElectronHttpRequest = {
    url,
    method: init.method,
    headers: headersToObject(init.headers),
    ...(await serializeBody(init.body)),
  };

  const response = await window.electronDesktop.httpRequest(payload);
  return createRuntimeResponse(response);
};
