/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const {
  Component,
  createFactory,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const asyncStorage = require("resource://devtools/shared/async-storage.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const {
  connect,
} = require("resource://devtools/client/shared/vendor/react-redux.js");
const {
  L10N,
} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
const {
  getClickedRequest,
} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
const {
  getUrlQuery,
  parseQueryString,
} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
const InputMap = createFactory(
  require("resource://devtools/client/netmonitor/src/components/new-request/InputMap.js")
);
const { button, div, footer, label, textarea, select, option } = dom;

const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.newRequestHeaders");
const CUSTOM_NEW_REQUEST_URL_LABEL = L10N.getStr(
  "netmonitor.custom.newRequestUrlLabel"
);
const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postBody");
const CUSTOM_POSTDATA_PLACEHOLDER = L10N.getStr(
  "netmonitor.custom.postBody.placeholder"
);
const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.urlParameters");
const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
const CUSTOM_CLEAR = L10N.getStr("netmonitor.custom.clear");

const FIREFOX_DEFAULT_HEADERS = [
  "Accept-Charset",
  "Accept-Encoding",
  "Access-Control-Request-Headers",
  "Access-Control-Request-Method",
  "Connection",
  "Content-Length",
  "Cookie",
  "Cookie2",
  "Date",
  "DNT",
  "Expect",
  "Feature-Policy",
  "Host",
  "Keep-Alive",
  "Origin",
  "Proxy-",
  "Sec-",
  "Referer",
  "TE",
  "Trailer",
  "Transfer-Encoding",
  "Upgrade",
  "Via",
];
// This does not include the CONNECT method as it is restricted and special.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1769572#c2 for details
const HTTP_METHODS = [
  "GET",
  "HEAD",
  "POST",
  "DELETE",
  "PUT",
  "OPTIONS",
  "TRACE",
  "PATCH",
];

/*
 * HTTP Custom request panel component
 * A network request panel which enables creating and sending new requests
 * or selecting, editing and re-sending current requests.
 */
class HTTPCustomRequestPanel extends Component {
  static get propTypes() {
    return {
      connector: PropTypes.object.isRequired,
      request: PropTypes.object,
      sendCustomRequest: PropTypes.func.isRequired,
    };
  }

  constructor(props) {
    super(props);

    this.state = {
      method: HTTP_METHODS[0],
      url: "",
      urlQueryParams: [],
      headers: [],
      postBody: "",
      // Flag to know the data from either the request or the async storage has
      // been loaded in componentDidMount
      _isStateDataReady: false,
    };

    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleChangeURL = this.handleChangeURL.bind(this);
    this.updateInputMapItem = this.updateInputMapItem.bind(this);
    this.addInputMapItem = this.addInputMapItem.bind(this);
    this.deleteInputMapItem = this.deleteInputMapItem.bind(this);
    this.checkInputMapItem = this.checkInputMapItem.bind(this);
    this.handleClear = this.handleClear.bind(this);
    this.createQueryParamsListFromURL =
      this.createQueryParamsListFromURL.bind(this);
    this.onUpdateQueryParams = this.onUpdateQueryParams.bind(this);
  }

  async componentDidMount() {
    let { connector, request } = this.props;
    if (!connector.currentTarget?.targetForm?.isPrivate) {
      const persistedCustomRequest = await asyncStorage.getItem(
        "devtools.netmonitor.customRequest"
      );
      request = request || persistedCustomRequest;
    }

    if (!request) {
      this.setState({ _isStateDataReady: true });
      return;
    }

    // We need this part because in the asyncStorage we are saving the request in one format
    // and from the edit and resend it comes in a different form with different properties,
    // so we need this to nomalize the request.
    if (request.requestHeaders) {
      request.headers = request.requestHeaders.headers;
    }

    if (request.requestPostData?.postData?.text) {
      request.postBody = request.requestPostData.postData.text;
    }

    const headers = request.headers
      .map(({ name, value }) => {
        return {
          name,
          value,
          checked: true,
          disabled: FIREFOX_DEFAULT_HEADERS.some(i => name.startsWith(i)),
        };
      })
      .sort((a, b) => {
        if (a.disabled && !b.disabled) {
          return -1;
        }
        if (!a.disabled && b.disabled) {
          return 1;
        }
        return 0;
      });

    if (request.requestPostDataAvailable && !request.postBody) {
      const requestData = await connector.requestData(
        request.id,
        "requestPostData"
      );
      request.postBody = requestData.postData.text;
    }

    this.setState({
      method: request.method,
      url: request.url,
      urlQueryParams: this.createQueryParamsListFromURL(request.url),
      headers,
      postBody: request.postBody,
      _isStateDataReady: true,
    });
  }

  componentDidUpdate(prevProps, prevState) {
    // This is when the query params change in the url params input map
    if (
      prevState.urlQueryParams !== this.state.urlQueryParams &&
      prevState.url === this.state.url
    ) {
      this.onUpdateQueryParams();
    }
  }

  componentWillUnmount() {
    if (!this.props.connector.currentTarget?.targetForm?.isPrivate) {
      asyncStorage.setItem("devtools.netmonitor.customRequest", this.state);
    }
  }

  handleChangeURL(event) {
    const { value } = event.target;

    this.setState({
      url: value,
      urlQueryParams: this.createQueryParamsListFromURL(value),
    });
  }

  handleInputChange(event) {
    const { name, value } = event.target;
    const newState = {
      [name]: value,
    };

    // If the message body changes lets make sure we
    // keep the content-length up to date.
    if (name == "postBody") {
      newState.headers = this.state.headers.map(header => {
        if (header.name == "Content-Length") {
          header.value = value.length;
        }
        return header;
      });
    }

    this.setState(newState);
  }

  updateInputMapItem(stateName, event) {
    const { name, value } = event.target;
    const [prop, index] = name.split("-");
    const updatedList = [...this.state[stateName]];
    updatedList[Number(index)][prop] = value;

    this.setState({
      [stateName]: updatedList,
    });
  }

  addInputMapItem(stateName, name, value) {
    this.setState({
      [stateName]: [
        ...this.state[stateName],
        { name, value, checked: true, disabled: false },
      ],
    });
  }

  deleteInputMapItem(stateName, index) {
    this.setState({
      [stateName]: this.state[stateName].filter((_, i) => i !== index),
    });
  }

  checkInputMapItem(stateName, index, checked) {
    this.setState({
      [stateName]: this.state[stateName].map((item, i) => {
        if (index === i) {
          return {
            ...item,
            checked,
          };
        }
        return item;
      }),
    });
  }

  onUpdateQueryParams() {
    const { urlQueryParams, url } = this.state;

    const searchParams = new URLSearchParams();
    for (const { name, value, checked } of urlQueryParams) {
      // We only want to add checked parameters with a non-empty name
      if (checked && name) {
        searchParams.append(name, value);
      }
    }

    // We can't use `getUrl` here as `url` is the value of the input and might not be a
    // valid URL. We still want to try to set the params in the URL input in such case,
    // so append them after the first "?" char we find.
    let finalURL = url.split("?")[0];
    if (searchParams.size) {
      finalURL += `?${searchParams}`;
    }

    this.setState({
      url: finalURL,
    });
  }

  createQueryParamsListFromURL(url = "") {
    const parsedQuery = parseQueryString(getUrlQuery(url) || url.split("?")[1]);
    const queryArray = parsedQuery || [];
    return queryArray.map(({ name, value }) => {
      return {
        checked: true,
        name,
        value,
      };
    });
  }

  handleClear() {
    this.setState({
      method: HTTP_METHODS[0],
      url: "",
      urlQueryParams: [],
      headers: [],
      postBody: "",
    });
  }

  render() {
    return div(
      { className: "http-custom-request-panel" },
      div(
        { className: "http-custom-request-panel-content" },
        div(
          {
            className: "tabpanel-summary-container http-custom-method-and-url",
            id: "http-custom-method-and-url",
          },
          select(
            {
              className: "http-custom-method-value",
              id: "http-custom-method-value",
              name: "method",
              onChange: this.handleInputChange,
              onBlur: this.handleInputChange,
              value: this.state.method,
            },

            HTTP_METHODS.map(item =>
              option(
                {
                  value: item,
                  key: item,
                },
                item
              )
            )
          ),
          div(
            {
              className: "auto-growing-textarea",
              "data-replicated-value": this.state.url,
              title: this.state.url,
            },
            textarea({
              className: "http-custom-url-value",
              id: "http-custom-url-value",
              name: "url",
              placeholder: CUSTOM_NEW_REQUEST_URL_LABEL,
              onChange: event => {
                this.handleChangeURL(event);
              },
              onBlur: this.handleTextareaChange,
              value: this.state.url,
              rows: 1,
            })
          )
        ),
        div(
          {
            className: "tabpanel-summary-container http-custom-section",
            id: "http-custom-query",
          },
          label(
            {
              className: "http-custom-request-label",
              htmlFor: "http-custom-query-value",
            },
            CUSTOM_QUERY
          ),
          // This is the input map for the Url Parameters Component
          InputMap({
            list: this.state.urlQueryParams,
            onUpdate: event => {
              this.updateInputMapItem(
                "urlQueryParams",
                event,
                this.onUpdateQueryParams
              );
            },
            onAdd: (name, value) =>
              this.addInputMapItem(
                "urlQueryParams",
                name,
                value,
                this.onUpdateQueryParams
              ),
            onDelete: index =>
              this.deleteInputMapItem(
                "urlQueryParams",
                index,
                this.onUpdateQueryParams
              ),
            onChecked: (index, checked) => {
              this.checkInputMapItem(
                "urlQueryParams",
                index,
                checked,
                this.onUpdateQueryParams
              );
            },
          })
        ),
        div(
          {
            id: "http-custom-headers",
            className: "tabpanel-summary-container http-custom-section",
          },
          label(
            {
              className: "http-custom-request-label",
              htmlFor: "custom-headers-value",
            },
            CUSTOM_HEADERS
          ),
          // This is the input map for the Headers Component
          InputMap({
            ref: this.headersListRef,
            list: this.state.headers,
            onUpdate: event => {
              this.updateInputMapItem("headers", event);
            },
            onAdd: (name, value) =>
              this.addInputMapItem("headers", name, value),
            onDelete: index => this.deleteInputMapItem("headers", index),
            onChecked: (index, checked) => {
              this.checkInputMapItem("headers", index, checked);
            },
          })
        ),
        div(
          {
            id: "http-custom-postdata",
            className: "tabpanel-summary-container http-custom-section",
          },
          label(
            {
              className: "http-custom-request-label",
              htmlFor: "http-custom-postdata-value",
            },
            CUSTOM_POSTDATA
          ),
          textarea({
            className: "tabpanel-summary-input",
            id: "http-custom-postdata-value",
            name: "postBody",
            placeholder: CUSTOM_POSTDATA_PLACEHOLDER,
            onChange: this.handleInputChange,
            rows: 6,
            value: this.state.postBody,
            wrap: "off",
          })
        )
      ),
      footer(
        { className: "http-custom-request-button-container" },
        button(
          {
            className: "devtools-button",
            id: "http-custom-request-clear-button",
            onClick: this.handleClear,
          },
          CUSTOM_CLEAR
        ),
        button(
          {
            className: "devtools-button",
            id: "http-custom-request-send-button",
            disabled:
              !this.state._isStateDataReady ||
              !this.state.url ||
              !this.state.method,
            onClick: () => {
              const newRequest = {
                method: this.state.method,
                url: this.state.url,
                cause: this.props.request?.cause,
                urlQueryParams: this.state.urlQueryParams.map(
                  ({ ...params }) => params
                ),
                requestHeaders: {
                  headers: this.state.headers
                    .filter(({ checked }) => checked)
                    .map(({ ...headersValues }) => headersValues),
                },
              };

              if (this.state.postBody) {
                newRequest.requestPostData = {
                  postData: {
                    text: this.state.postBody,
                  },
                };
              }
              this.props.sendCustomRequest(newRequest);
            },
          },
          CUSTOM_SEND
        )
      )
    );
  }
}

module.exports = connect(
  state => ({ request: getClickedRequest(state) }),
  dispatch => ({
    sendCustomRequest: request =>
      dispatch(Actions.sendHTTPCustomRequest(request)),
  })
)(HTTPCustomRequestPanel);
