/**
 * 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.
 *
 * @format
 * @flow strict-local
 */

'use strict';

const XMLHttpRequest = require('./XMLHttpRequest');
// $FlowFixMe[method-unbinding]
const originalXHROpen = XMLHttpRequest.prototype.open;
// $FlowFixMe[method-unbinding]
const originalXHRSend = XMLHttpRequest.prototype.send;
// $FlowFixMe[method-unbinding]
const originalXHRSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

type XHRInterceptorOpenCallback = (
  method: string,
  url: string,
  request: XMLHttpRequest,
) => void;

type XHRInterceptorSendCallback = (
  data: string,
  request: XMLHttpRequest,
) => void;

type XHRInterceptorRequestHeaderCallback = (
  header: string,
  value: string,
  request: XMLHttpRequest,
) => void;

type XHRInterceptorHeaderReceivedCallback = (
  responseContentType: string | void,
  responseSize: number | void,
  allHeaders: string,
  request: XMLHttpRequest,
) => void;

type XHRInterceptorResponseCallback = (
  status: number,
  timeout: number,
  response: string,
  responseURL: string,
  responseType: string,
  request: XMLHttpRequest,
) => void;

let openCallback: XHRInterceptorOpenCallback | null;
let sendCallback: XHRInterceptorSendCallback | null;
let requestHeaderCallback: XHRInterceptorRequestHeaderCallback | null;
let headerReceivedCallback: XHRInterceptorHeaderReceivedCallback | null;
let responseCallback: XHRInterceptorResponseCallback | null;

let isInterceptorEnabled = false;

/**
 * A network interceptor which monkey-patches XMLHttpRequest methods
 * to gather all network requests/responses, in order to show their
 * information in the React Native inspector development tool.
 * This supports interception with XMLHttpRequest API, including Fetch API
 * and any other third party libraries that depend on XMLHttpRequest.
 */
const XHRInterceptor = {
  /**
   * Invoked before XMLHttpRequest.open(...) is called.
   */
  setOpenCallback(callback: XHRInterceptorOpenCallback) {
    openCallback = callback;
  },

  /**
   * Invoked before XMLHttpRequest.send(...) is called.
   */
  setSendCallback(callback: XHRInterceptorSendCallback) {
    sendCallback = callback;
  },

  /**
   * Invoked after xhr's readyState becomes xhr.HEADERS_RECEIVED.
   */
  setHeaderReceivedCallback(callback: XHRInterceptorHeaderReceivedCallback) {
    headerReceivedCallback = callback;
  },

  /**
   * Invoked after xhr's readyState becomes xhr.DONE.
   */
  setResponseCallback(callback: XHRInterceptorResponseCallback) {
    responseCallback = callback;
  },

  /**
   * Invoked before XMLHttpRequest.setRequestHeader(...) is called.
   */
  setRequestHeaderCallback(callback: XHRInterceptorRequestHeaderCallback) {
    requestHeaderCallback = callback;
  },

  isInterceptorEnabled(): boolean {
    return isInterceptorEnabled;
  },

  enableInterception() {
    if (isInterceptorEnabled) {
      return;
    }
    // Override `open` method for all XHR requests to intercept the request
    // method and url, then pass them through the `openCallback`.
    // $FlowFixMe[cannot-write]
    // $FlowFixMe[missing-this-annot]
    XMLHttpRequest.prototype.open = function (method: string, url: string) {
      if (openCallback) {
        openCallback(method, url, this);
      }
      originalXHROpen.apply(this, arguments);
    };

    // Override `setRequestHeader` method for all XHR requests to intercept
    // the request headers, then pass them through the `requestHeaderCallback`.
    // $FlowFixMe[cannot-write]
    // $FlowFixMe[missing-this-annot]
    XMLHttpRequest.prototype.setRequestHeader = function (
      header: string,
      value: string,
    ) {
      if (requestHeaderCallback) {
        requestHeaderCallback(header, value, this);
      }
      originalXHRSetRequestHeader.apply(this, arguments);
    };

    // Override `send` method of all XHR requests to intercept the data sent,
    // register listeners to intercept the response, and invoke the callbacks.
    // $FlowFixMe[cannot-write]
    // $FlowFixMe[missing-this-annot]
    XMLHttpRequest.prototype.send = function (data: string) {
      if (sendCallback) {
        sendCallback(data, this);
      }
      if (this.addEventListener) {
        this.addEventListener(
          'readystatechange',
          () => {
            if (!isInterceptorEnabled) {
              return;
            }
            if (this.readyState === this.HEADERS_RECEIVED) {
              const contentTypeString = this.getResponseHeader('Content-Type');
              const contentLengthString =
                this.getResponseHeader('Content-Length');
              let responseContentType, responseSize;
              if (contentTypeString) {
                responseContentType = contentTypeString.split(';')[0];
              }
              if (contentLengthString) {
                responseSize = parseInt(contentLengthString, 10);
              }
              if (headerReceivedCallback) {
                headerReceivedCallback(
                  responseContentType,
                  responseSize,
                  this.getAllResponseHeaders(),
                  this,
                );
              }
            }
            if (this.readyState === this.DONE) {
              if (responseCallback) {
                responseCallback(
                  this.status,
                  this.timeout,
                  this.response,
                  this.responseURL,
                  this.responseType,
                  this,
                );
              }
            }
          },
          false,
        );
      }
      originalXHRSend.apply(this, arguments);
    };
    isInterceptorEnabled = true;
  },

  // Unpatch XMLHttpRequest methods and remove the callbacks.
  disableInterception() {
    if (!isInterceptorEnabled) {
      return;
    }
    isInterceptorEnabled = false;
    // $FlowFixMe[cannot-write]
    XMLHttpRequest.prototype.send = originalXHRSend;
    // $FlowFixMe[cannot-write]
    XMLHttpRequest.prototype.open = originalXHROpen;
    // $FlowFixMe[cannot-write]
    XMLHttpRequest.prototype.setRequestHeader = originalXHRSetRequestHeader;
    responseCallback = null;
    openCallback = null;
    sendCallback = null;
    headerReceivedCallback = null;
    requestHeaderCallback = null;
  },
};

module.exports = XHRInterceptor;
