/* eslint-disable prefer-rest-params */

/**
 * 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.
 *
 * Vendored from: https://github.com/callstackincubator/rozenite/blob/402e3579878f72cbae7f8123f9ca80459dc1fe7f/packages/network-activity-plugin/src/react-native/http/xhr-interceptor.ts
 * Original source: https://github.com/facebook/react-native/blob/2c683c5787dd03ac15d2aad45dcc53650529ee7f/packages/react-native/src/private/devsupport/devmenu/elementinspector/XHRInterceptor.js
 */

const originalXHROpen = XMLHttpRequest.prototype.open
const originalXHRSend = XMLHttpRequest.prototype.send
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 | undefined,
  responseSize: number | undefined,
  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.
 */
export 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
  },
}
