/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { CopyOutlined } from '@ant-design/icons';
import type { Monaco } from '@monaco-editor/react';
import Editor from '@monaco-editor/react';
import { Button, Card, Drawer, Form, Input, notification, Radio, Select, Spin, Tabs } from 'antd';
import Base64 from 'base-64';
import type * as monacoEditor from 'monaco-editor';
import queryString from 'query-string';
import React, { useEffect, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useIntl } from 'umi';

import PanelSection from '@/components/PanelSection';

import {
  DEBUG_BODY_MODE_SUPPORTED,
  DEBUG_BODY_TYPE_SUPPORTED,
  DEBUG_RESPONSE_BODY_MODE_SUPPORTED,
  DebugBodyFormDataValueType,
  DEFAULT_DEBUG_AUTH_FORM_DATA,
  DEFAULT_DEBUG_PARAM_FORM_DATA,
  HTTP_METHOD_OPTION_LIST,
  PROTOCOL_SUPPORTED,
} from '../../constants';
import { debugRoute } from '../../service';
import { AuthenticationView, DebugFormDataView, DebugParamsView } from '.';
import styles from './index.less';

const { Option } = Select;
const { Search } = Input;
const { TabPane } = Tabs;

const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => {
  const { formatMessage } = useIntl();
  const [httpMethod, setHttpMethod] = useState(HTTP_METHOD_OPTION_LIST[0]);
  const [requestProtocol, setRequestProtocol] = useState(PROTOCOL_SUPPORTED[0]);
  const [showBodyTab, setShowBodyTab] = useState(false);
  const [queryForm] = Form.useForm();
  const [urlencodedForm] = Form.useForm();
  const [formDataForm] = Form.useForm();
  const [authForm] = Form.useForm();
  const [headerForm] = Form.useForm();
  const [response, setResponse] = useState<RouteModule.debugResponse | null>();
  const [loading, setLoading] = useState(false);
  const [body, setBody] = useState('');
  const [height, setHeight] = useState(50);
  const [bodyType, setBodyType] = useState('none');
  const methodWithoutBody = ['GET', 'HEAD'];
  const [responseBodyMode, setResponseBodyMode] = useState(
    DEBUG_RESPONSE_BODY_MODE_SUPPORTED[0].mode,
  );
  const [bodyMode, setBodyCodeMode] = useState(DEBUG_BODY_MODE_SUPPORTED[0].mode);

  enum DebugBodyType {
    None = 0,
    FormUrlencoded,
    FormData,
    RawInput,
  }

  const resetForms = () => {
    queryForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA);
    urlencodedForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA);
    formDataForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA);
    headerForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA);
    authForm.setFieldsValue(DEFAULT_DEBUG_AUTH_FORM_DATA);
    setResponse(null);
    setBodyType(DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.None]);
  };

  useEffect(() => {
    resetForms();
  }, []);

  const transformBodyParamsFormData = () => {
    if (methodWithoutBody.includes(httpMethod)) {
      return {
        bodyFormData: undefined,
      };
    }

    switch (bodyType) {
      case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormUrlencoded]: {
        let transformFormUrlencoded: string[] = [];
        const FormUrlencodedData: RouteModule.debugRequestParamsFormData[] = urlencodedForm.getFieldsValue()
          .params;
        transformFormUrlencoded = (FormUrlencodedData || [])
          .filter((data) => data && data.check)
          .map((data) => {
            return `${data.key}=${data.value}`;
          });

        return {
          bodyFormData: transformFormUrlencoded.join('&'),
          header: {
            'Content-type': ['application/x-www-form-urlencoded;charset=UTF-8'],
          },
        };
      }
      case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput]: {
        let contentType = [''];
        switch (bodyMode) {
          case DEBUG_BODY_MODE_SUPPORTED[0].mode:
            contentType = ['application/json;charset=UTF-8'];
            break;
          case DEBUG_BODY_MODE_SUPPORTED[1].mode:
            contentType = ['text/plain;charset=UTF-8'];
            break;
          case DEBUG_BODY_MODE_SUPPORTED[2].mode:
            contentType = ['application/xml;charset=UTF-8'];
            break;
          default:
            break;
        }

        return {
          bodyFormData: body,
          header: {
            'Content-type': contentType,
          },
        };
      }
      case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormData]: {
        const transformFormData = new FormData();
        const formDataData: RouteModule.debugRequestParamsFormData[] = formDataForm.getFieldsValue()
          .params;

        (formDataData || [])
          .filter((data) => data && data.check)
          .forEach((data) => {
            if (data.type === DebugBodyFormDataValueType.File) {
              transformFormData.append(data.key, data.value.originFileObj);
            } else {
              transformFormData.append(data.key, data.value);
            }
          });
        return {
          bodyFormData: transformFormData,
        };
      }
      case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.None]:
      default:
        return {
          bodyFormData: undefined,
        };
    }
  };

  const transformHeaderAndQueryParamsFormData = (
    formData: RouteModule.debugRequestParamsFormData[],
  ) => {
    let transformData = {};
    (formData || [])
      .filter((data) => data && data.check)
      .forEach((data) => {
        transformData = {
          ...transformData,
          [data.key]: [...(transformData[data.key] || []), data.value],
        };
      });

    return transformData;
  };

  const transformAuthFormData = (
    formData: RouteModule.authData,
    userHeaderData: any,
    formHeaderData: any,
  ) => {
    const { authType } = formData;

    switch (authType) {
      case 'basic-auth':
        return {
          ...formHeaderData,
          ...userHeaderData,
          Authorization: [`Basic ${Base64.encode(`${formData.username}:${formData.password}`)}`],
        };
      case 'jwt-auth':
        return {
          ...formHeaderData,
          ...userHeaderData,
          Authorization: [formData.Authorization],
        };
      case 'key-auth':
        return {
          ...formHeaderData,
          ...userHeaderData,
          apikey: [formData.apikey],
        };
      default:
        break;
    }

    return {
      ...formHeaderData,
      ...userHeaderData,
    };
  };

  const handleDebug = (url: string) => {
    const queryFormData = transformHeaderAndQueryParamsFormData(queryForm.getFieldsValue().params);
    const bodyFormRelateData = transformBodyParamsFormData();
    const { bodyFormData, header: bodyFormHeader } = bodyFormRelateData;
    const pureHeaderFormData = transformHeaderAndQueryParamsFormData(
      headerForm.getFieldsValue().params,
    );
    const headerFormData = transformAuthFormData(
      authForm.getFieldsValue(),
      pureHeaderFormData,
      bodyFormHeader,
    );
    const urlQueryString =
      url.indexOf('?') === -1
        ? `?${queryString.stringify(queryFormData)}`
        : `&${queryString.stringify(queryFormData)}`;

    setLoading(true);
    // TODO: grpc and websocket
    debugRoute(
      {
        online_debug_header_params: JSON.stringify(headerFormData),
        online_debug_url: `${requestProtocol}://${url}${urlQueryString}`,
        online_debug_request_protocol: requestProtocol,
        online_debug_method: httpMethod,
      },
      bodyFormData,
    )
      .then((req) => {
        setLoading(false);
        const resp: RouteModule.debugResponse = req.data;
        if (typeof resp.data !== 'string') {
          resp.data = JSON.stringify(resp.data, null, 2);
        }
        setResponse(resp);
        const contentType = resp.header['Content-Type'];
        if (contentType == null || contentType.length !== 1) {
          setResponseBodyMode('TEXT');
        } else if (contentType[0].toLowerCase().indexOf('json') !== -1) {
          setResponseBodyMode('JSON');
        } else if (contentType[0].toLowerCase().indexOf('xml') !== -1) {
          setResponseBodyMode('XML');
        } else if (contentType[0].toLowerCase().indexOf('html') !== -1) {
          setResponseBodyMode('HTML');
        } else {
          setResponseBodyMode('TEXT');
        }
      })
      .catch(() => {
        setLoading(false);
      });
  };

  const handleEditorMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: Monaco) => {
    editor.onDidChangeModelDecorations(() => {
      if (!editor.getDomNode()) {
        return;
      }
      const padding = 40;
      const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight);
      const lineCount = editor.getModel()?.getLineCount() || 1;
      setHeight(editor.getTopForLineNumber(lineCount + 1) + lineHeight + padding);
    });
  };

  return (
    <Drawer
      title={formatMessage({ id: 'page.route.onlineDebug' })}
      mask={false}
      maskClosable={false}
      visible={props.visible}
      width={650}
      onClose={() => {
        props.onClose();
      }}
      className={styles.routeDebugDraw}
      data-cy="debug-draw"
    >
      <Card bordered={false}>
        <Input.Group compact>
          <Select
            defaultValue={httpMethod}
            style={{ width: '20%' }}
            onChange={(value) => {
              setHttpMethod(value);
              setShowBodyTab(!(methodWithoutBody.indexOf(value) > -1));
            }}
            size="large"
            data-cy="debug-method"
          >
            {HTTP_METHOD_OPTION_LIST.map((method) => {
              return (
                <Option key={method} value={method}>
                  {method}
                </Option>
              );
            })}
          </Select>
          <Select
            defaultValue={requestProtocol}
            style={{ width: '18%' }}
            onChange={(value) => {
              setRequestProtocol(value);
            }}
            size="large"
            data-cy="debug-protocol"
          >
            {PROTOCOL_SUPPORTED.map((protocol) => {
              return (
                <Option key={protocol} value={protocol}>
                  {`${protocol}://`}
                </Option>
              );
            })}
          </Select>
          <Search
            id="debugUri"
            placeholder={formatMessage({ id: 'page.route.input.placeholder.requestUrl' })}
            allowClear
            enterButton={formatMessage({ id: 'page.route.button.send' })}
            size="large"
            style={{ width: '62%' }}
            onSearch={handleDebug}
            onPressEnter={(e) => {
              handleDebug(e.currentTarget.value);
            }}
            onChange={(e) => {
              if (e.currentTarget.value === '') {
                resetForms();
              }
            }}
          />
        </Input.Group>
        <PanelSection
          title={formatMessage({ id: 'page.route.PanelSection.title.defineRequestParams' })}
        >
          <Tabs>
            <TabPane
              data-cy="query"
              tab={formatMessage({ id: 'page.route.TabPane.queryParams' })}
              key="query"
            >
              <DebugParamsView form={queryForm} name="queryForm" />
            </TabPane>
            <TabPane
              data-cy="auth"
              tab={formatMessage({ id: 'page.route.TabPane.authentication' })}
              key="auth"
            >
              <AuthenticationView form={authForm} />
            </TabPane>
            <TabPane
              data-cy="header"
              tab={formatMessage({ id: 'page.route.TabPane.headerParams' })}
              key="header"
            >
              <DebugParamsView form={headerForm} name="headerForm" inputType="header" />
            </TabPane>
            {showBodyTab && (
              <TabPane
                data-cy="body"
                tab={formatMessage({ id: 'page.route.TabPane.bodyParams' })}
                key="body"
              >
                <Radio.Group
                  onChange={(e) => {
                    setBodyType(e.target.value);
                  }}
                  value={bodyType}
                >
                  {DEBUG_BODY_TYPE_SUPPORTED.map((type) => (
                    <Radio value={type} key={type}>
                      {type}
                    </Radio>
                  ))}
                </Radio.Group>
                {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput] && (
                  <Select
                    size="small"
                    onChange={(value) => {
                      setBodyCodeMode(value);
                    }}
                    style={{ width: 100 }}
                    defaultValue={bodyMode}
                  >
                    {DEBUG_BODY_MODE_SUPPORTED.map((modeObj) => (
                      <Option key={modeObj.name} value={modeObj.mode}>
                        {modeObj.name}
                      </Option>
                    ))}
                  </Select>
                )}
                <div style={{ marginTop: 16 }}>
                  {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormUrlencoded] && (
                    <DebugParamsView form={urlencodedForm} name="urlencodedForm" />
                  )}

                  {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormData] && (
                    <DebugFormDataView form={formDataForm} />
                  )}

                  {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput] && (
                    <Form>
                      <Form.Item>
                        <Editor
                          value={body}
                          language={bodyMode.toLowerCase()}
                          onChange={(text) => {
                            if (text) {
                              setBody(text);
                            } else {
                              setBody('');
                            }
                          }}
                          height={250}
                          beforeMount={(monaco) => {
                            monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
                              validate: false,
                            });
                          }}
                          options={{
                            scrollbar: {
                              vertical: 'hidden',
                              horizontal: 'hidden',
                            },
                            wordWrap: 'on',
                            minimap: { enabled: false },
                          }}
                        />
                      </Form.Item>
                    </Form>
                  )}
                </div>
              </TabPane>
            )}
          </Tabs>
        </PanelSection>
        <PanelSection title={formatMessage({ id: 'page.route.PanelSection.title.responseResult' })}>
          <Spin tip="Loading..." spinning={loading}>
            <Tabs
              tabBarExtraContent={
                response
                  ? response.message
                  : formatMessage({ id: 'page.route.debug.showResultAfterSendRequest' })
              }
            >
              <TabPane tab={formatMessage({ id: 'page.route.TabPane.response' })} key="response">
                <Select
                  disabled={response == null}
                  value={responseBodyMode}
                  onSelect={(mode) => setResponseBodyMode(mode as string)}
                >
                  {DEBUG_RESPONSE_BODY_MODE_SUPPORTED.map((mode) => {
                    return (
                      <Option value={mode.mode} key={mode.mode}>
                        {mode.name}
                      </Option>
                    );
                  })}
                </Select>
                <CopyToClipboard
                  text={response ? response.data : ''}
                  onCopy={(_: string, result: boolean) => {
                    if (!result) {
                      notification.error({
                        message: formatMessage({ id: 'component.global.copyFail' }),
                      });
                      return;
                    }
                    notification.success({
                      message: formatMessage({ id: 'component.global.copySuccess' }),
                    });
                  }}
                >
                  <Button type="text" disabled={!response}>
                    <CopyOutlined />
                  </Button>
                </CopyToClipboard>
                <div id="monaco-response" style={{ marginTop: 16 }}>
                  <Editor
                    value={response ? response.data : ''}
                    height={height}
                    language={responseBodyMode.toLowerCase()}
                    onMount={handleEditorMount}
                    beforeMount={(monaco) => {
                      monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
                        validate: false,
                      });
                    }}
                    options={{
                      automaticLayout: true,
                      scrollbar: {
                        vertical: 'hidden',
                        horizontal: 'hidden',
                      },
                      wordWrap: 'on',
                      minimap: { enabled: false },
                      readOnly: true,
                    }}
                  />
                </div>
              </TabPane>
              <TabPane tab={formatMessage({ id: 'page.route.TabPane.header' })} key="header">
                {response &&
                  Object.keys(response.header).map((header) => {
                    return response.header[header].map((value) => {
                      return (
                        <div>
                          <b>{header}</b>: {value}
                        </div>
                      );
                    });
                  })}
              </TabPane>
            </Tabs>
          </Spin>
        </PanelSection>
      </Card>
    </Drawer>
  );
};

export default DebugDrawView;
