import { MessagePortRpcClient, WebWorkerRpcClient } from '@lvce-editor/rpc';

const getFetchErrorMessage = error => {
  if (error instanceof Error) {
    return error.message;
  }
  return 'request failed';
};

const getFetchThrownErrorResult = error => {
  return {
    headers: {},
    response: getFetchErrorMessage(error),
    statusCode: 0,
    type: 'error'
  };
};

const getHeadersObject = headers => {
  const result = {};
  for (const [key, value] of headers) {
    result[key] = value;
  }
  return result;
};

const getRequestInit = options => {
  return {
    ...(options.postBody === undefined ? {} : {
      body: JSON.stringify(options.postBody)
    }),
    ...(options.headers ? {
      headers: options.headers
    } : {}),
    method: options.method
  };
};

const parseResponseJson = responseText => {
  if (!responseText) {
    return null;
  }
  return JSON.parse(responseText);
};

const makeApiRequest = async options => {
  let response;
  try {
    response = await fetch(options.url, getRequestInit(options));
  } catch (error) {
    return getFetchThrownErrorResult(error);
  }
  const headers = getHeadersObject(response.headers);
  const responseText = await response.text();
  if (!response.ok) {
    return {
      headers,
      response: responseText,
      statusCode: response.status,
      type: 'error'
    };
  }
  let parsed;
  try {
    parsed = parseResponseJson(responseText);
  } catch {
    return {
      headers,
      response: responseText,
      statusCode: response.status,
      type: 'error'
    };
  }
  return {
    body: parsed,
    headers,
    statusCode: response.status,
    type: 'success'
  };
};

const getSseDataLines = event => {
  const lines = event.split(/\r?\n/);
  const dataLines = [];
  for (const line of lines) {
    if (!line.startsWith('data:')) {
      continue;
    }
    dataLines.push(line.slice(5).trimStart());
  }
  return dataLines;
};

const parseSseData = text => {
  if (text === '[DONE]') {
    return text;
  }
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
};

const splitSseEvents = chunk => {
  return chunk.split(/\r?\n\r?\n/);
};

const parseSseFromReader = async responseBody => {
  if (!responseBody) {
    return [];
  }
  const reader = responseBody.getReader();
  const decoder = new TextDecoder();
  let remainder = '';
  const events = [];
  let done = false;
  while (!done) {
    const {
      done: streamDone,
      value
    } = await reader.read();
    if (streamDone) {
      done = true;
    } else if (value) {
      remainder += decoder.decode(value, {
        stream: true
      });
    }
    const chunks = splitSseEvents(remainder);
    remainder = chunks.pop() ?? '';
    for (const chunk of chunks) {
      const dataLines = getSseDataLines(chunk);
      for (const line of dataLines) {
        events.push(parseSseData(line));
      }
    }
  }
  if (remainder) {
    const dataLines = getSseDataLines(remainder);
    for (const line of dataLines) {
      events.push(parseSseData(line));
    }
  }
  return events;
};

const makeStreamingApiRequest = async options => {
  let response;
  try {
    response = await fetch(options.url, getRequestInit(options));
  } catch (error) {
    return getFetchThrownErrorResult(error);
  }
  const headers = getHeadersObject(response.headers);
  if (!response.ok) {
    const responseText = await response.text();
    return {
      headers,
      response: responseText,
      statusCode: response.status,
      type: 'error'
    };
  }
  const body = await parseSseFromReader(response.body);
  return {
    body,
    headers,
    statusCode: response.status,
    type: 'success'
  };
};

const networkCommandMap = {
  'ChatNetwork.makeApiRequest': makeApiRequest,
  'ChatNetwork.makeStreamingApiRequest': makeStreamingApiRequest
};

const handleMessagePort = async port => {
  await MessagePortRpcClient.create({
    commandMap: networkCommandMap,
    messagePort: port
  });
};

const commandMap = {
  ...networkCommandMap,
  'HandleMessagePort.handleMessagePort': handleMessagePort
};

const listen = async () => {
  await WebWorkerRpcClient.create({
    commandMap: commandMap
  });
};

const main = async () => {
  await listen();
};

main();
