/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

import Networking from '../../Network/RCTNetworking';
import DevLoadingView from '../../Utilities/DevLoadingView';
import HMRClient from '../../Utilities/HMRClient';
import getDevServer from './getDevServer';

declare var global: {globalEvalWithSourceUrl?: (string, string) => mixed, ...};

let pendingRequests = 0;

const cachedPromisesByUrl = new Map<string, Promise<void>>();

export class LoadBundleFromServerError extends Error {
  url: string;
  isTimeout: boolean;
  constructor(
    message: string,
    url: string,
    isTimeout: boolean,
    options?: {cause: mixed, ...},
  ): void {
    super(message, options);
    this.url = url;
    this.isTimeout = isTimeout;
    this.name = 'LoadBundleFromServerError';
  }
}

export class LoadBundleFromServerRequestError extends LoadBundleFromServerError {
  constructor(
    message: string,
    url: string,
    isTimeout: boolean,
    options?: {cause: mixed, ...},
  ): void {
    super(message, url, isTimeout, options);
    this.name = 'LoadBundleFromServerRequestError';
  }
}

function asyncRequest(
  url: string,
): Promise<{body: string, headers: {[string]: string}}> {
  let id = null;
  let responseText = null;
  let headers = null;
  let dataListener;
  let completeListener;
  let responseListener;
  let incrementalDataListener;
  return new Promise<{body: string, headers: {[string]: string}}>(
    (resolve, reject) => {
      dataListener = Networking.addListener(
        'didReceiveNetworkData',
        ([requestId, response]) => {
          if (requestId === id) {
            responseText = response;
          }
        },
      );
      incrementalDataListener = Networking.addListener(
        'didReceiveNetworkIncrementalData',
        ([requestId, data]) => {
          if (requestId === id) {
            if (responseText != null) {
              responseText += data;
            } else {
              responseText = data;
            }
          }
        },
      );
      responseListener = Networking.addListener(
        'didReceiveNetworkResponse',
        ([requestId, status, responseHeaders]) => {
          if (requestId === id) {
            headers = responseHeaders;
          }
        },
      );
      completeListener = Networking.addListener(
        'didCompleteNetworkResponse',
        ([requestId, errorMessage, isTimeout]) => {
          if (requestId === id) {
            if (errorMessage) {
              reject(
                new LoadBundleFromServerRequestError(
                  'Could not load bundle',
                  url,
                  isTimeout,
                  {
                    cause: errorMessage,
                  },
                ),
              );
            } else {
              //$FlowFixMe[incompatible-call]
              resolve({body: responseText, headers});
            }
          }
        },
      );
      Networking.sendRequest(
        'GET',
        'asyncRequest',
        url,
        {},
        '',
        'text',
        true,
        0,
        requestId => {
          id = requestId;
        },
        true,
      );
    },
  ).finally(() => {
    dataListener?.remove();
    completeListener?.remove();
    responseListener?.remove();
    incrementalDataListener?.remove();
  });
}

function buildUrlForBundle(bundlePathAndQuery: string) {
  const {url: serverUrl} = getDevServer();
  return (
    serverUrl.replace(/\/+$/, '') + '/' + bundlePathAndQuery.replace(/^\/+/, '')
  );
}

export default function loadBundleFromServer(
  bundlePathAndQuery: string,
): Promise<void> {
  const requestUrl = buildUrlForBundle(bundlePathAndQuery);
  let loadPromise = cachedPromisesByUrl.get(requestUrl);

  if (loadPromise) {
    return loadPromise;
  }
  DevLoadingView.showMessage('Downloading...', 'load');
  ++pendingRequests;

  loadPromise = asyncRequest(requestUrl)
    .then<void>(({body, headers}) => {
      if (
        headers['Content-Type'] != null &&
        headers['Content-Type'].indexOf('application/json') >= 0
      ) {
        // Errors are returned as JSON.
        throw new LoadBundleFromServerError(
          'Could not load bundle',
          bundlePathAndQuery,
          false, // isTimeout
          {
            cause:
              JSON.parse(body).message ||
              `Unknown error fetching '${bundlePathAndQuery}'`,
          },
        );
      }

      HMRClient.registerBundle(requestUrl);

      // Some engines do not support `sourceURL` as a comment. We expose a
      // `globalEvalWithSourceUrl` function to handle updates in that case.
      if (global.globalEvalWithSourceUrl) {
        global.globalEvalWithSourceUrl(body, requestUrl);
      } else {
        // eslint-disable-next-line no-eval
        eval(body);
      }
    })
    .catch<void>(e => {
      cachedPromisesByUrl.delete(requestUrl);
      throw e;
    })
    .finally(() => {
      if (!--pendingRequests) {
        DevLoadingView.hide();
      }
    });

  cachedPromisesByUrl.set(requestUrl, loadPromise);
  return loadPromise;
}
