/*
 * 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 { LinkOutlined } from '@ant-design/icons';
import type { Monaco } from '@monaco-editor/react';
import Editor from '@monaco-editor/react';
import type { DefinedError } from 'ajv';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import {
  Alert,
  Button,
  Divider,
  Drawer,
  Form,
  Input,
  notification,
  PageHeader,
  Popconfirm,
  Select,
  Space,
  Switch,
} from 'antd';
import { js_beautify } from 'js-beautify';
import { compact, omit } from 'lodash';
import type { languages } from 'monaco-editor';
import React, { useEffect, useState } from 'react';
import { useIntl } from 'umi';

import { json2yaml, yaml2json } from '../../helpers';
import * as modelCode from './modelCode';
/* eslint import/namespace: ['error', { allowComputed: true }] */
import * as allModels from './Models';
import { fetchSchema } from './service';
import { PLUGIN_UI_LIST, PluginForm } from './UI';

type Props = {
  name: string;
  type?: 'global' | 'scoped';
  schemaType: PluginComponent.Schema;
  initialData: Record<string, any>;
  pluginList: PluginComponent.Meta[];
  readonly?: boolean;
  visible: boolean;
  maskClosable?: boolean;
  isEnabled?: boolean;
  onClose?: () => void;
  onChange?: (data: PluginComponent.PluginDetailValues) => void;
};

const ajv = new Ajv();
addFormats(ajv);

const FORM_ITEM_LAYOUT = {
  labelCol: {
    span: 3,
  },
  wrapperCol: {
    span: 16,
  },
};

// NOTE: This function has side effect because it mutates the original schema data
const injectDisableProperty = (schema: Record<string, any>) => {
  // NOTE: The frontend will inject the disable property into schema just like the manager-api does
  if (!schema.properties) {
    // eslint-disable-next-line
    schema.properties = {};
  }
  // eslint-disable-next-line
  (schema.properties as any).disable = {
    type: 'boolean',
  };
  return schema;
};

const PluginDetail: React.FC<Props> = ({
  name,
  type = 'scoped',
  schemaType = 'route',
  visible,
  pluginList = [],
  readonly = false,
  maskClosable = true,
  isEnabled = false,
  initialData = {},
  onClose = () => {},
  onChange = () => {},
}) => {
  const { formatMessage } = useIntl();
  enum monacoModeList {
    JSON = 'JSON',
    YAML = 'YAML',
    UIForm = 'Form',
  }
  const [form] = Form.useForm();
  const [UIForm] = Form.useForm();
  const data = initialData[name] || {};
  const pluginType = pluginList.find((item) => item.name === name)?.originType;
  const schemaName = name === 'basic-auth' ? 'consumer_schema' : 'schema';
  const pluginSchema = pluginList.find((item) => item.name === name)?.[schemaName];
  const [content, setContent] = useState<string>(JSON.stringify(data, null, 2));
  const [monacoMode, setMonacoMode] = useState<PluginComponent.MonacoLanguage>(monacoModeList.JSON);
  const modeOptions: { label: string; value: string }[] = [
    { label: monacoModeList.JSON, value: monacoModeList.JSON },
    { label: monacoModeList.YAML, value: monacoModeList.YAML },
  ];
  const targetPluginName = pluginList.find((item) => item.name === name)?.name;
  const filteredName = name.replace('-', '');
  const targetModel = allModels[`${filteredName}Model`];
  const targetModelCode = modelCode?.[`${filteredName}`];

  if (PLUGIN_UI_LIST.includes(name)) {
    modeOptions.push({
      label: formatMessage({ id: 'component.plugin.form' }),
      value: monacoModeList.UIForm,
    });
  }

  const getUIFormData = () => {
    const formData = UIForm.getFieldsValue();

    if (name === 'cors') {
      const newMethods = formData.allow_methods.join(',');
      const isFilterAllowRegex = compact(formData.allow_origins_by_regex).length === 0;
      const isFilterAllowMetadata = compact(formData.allow_origins_by_metadata).length === 0;
      // Note: default allow_origins_by_regex and allow_origins_by_metadata setted for UI is [''], but this is not allowed, omit it.
      if (isFilterAllowRegex || isFilterAllowMetadata) {
        const filterAllowRegex = (isFilterAllowRegex && 'allow_origins_by_regex') || '';
        const filterAllowMetadata = (isFilterAllowMetadata && 'allow_origins_by_metadata') || '';
        return omit({ ...formData, allow_methods: newMethods }, [
          filterAllowRegex,
          filterAllowMetadata,
        ]);
      }

      return { ...formData, allow_methods: newMethods };
    }

    if (name === 'referer-restriction') {
      if ('whitelist' in formData) {
        formData.whitelist = formData.whitelist.filter((item: string) => !!item);
        if (formData.whitelist <= 0) {
          delete formData.whitelist;
        }
      }
      if ('blacklist' in formData) {
        formData.blacklist = formData.blacklist.filter((item: string) => !!item);
        if (formData.blacklist <= 0) {
          delete formData.blacklist;
        }
      }
    }

    return formData;
  };

  const setUIFormData = (formData: any) => {
    if (name === 'cors' && formData) {
      const methods = (formData.allow_methods || '').length
        ? formData.allow_methods.split(',')
        : ['*'];
      UIForm.setFieldsValue({ ...formData, allow_methods: methods });
      return;
    }
    UIForm.setFieldsValue(formData);
  };

  useEffect(() => {
    form.setFieldsValue({
      disable: isEnabled ? true : initialData[name] && !initialData[name]?._meta?.disable,
      scope: 'global',
    });
    if (PLUGIN_UI_LIST.includes(name)) {
      setMonacoMode(monacoModeList.UIForm);
      setUIFormData(initialData[name]);
    }
  }, []);

  const formatYaml = (yaml: string): string => {
    const json = yaml2json(yaml, true);
    if (json.error) {
      return yaml;
    }
    return json2yaml(json.data).data;
  };

  const editorWillMount = (monaco: Monaco) => {
    fetchSchema(name, schemaType).then((schema) => {
      const schemaConfig: languages.json.DiagnosticsOptions = {
        validate: true,
        schemas: [
          {
            // useless placeholder
            uri: `https://apisix.apache.org/`,
            fileMatch: ['*'],
            schema,
          },
        ],
        trailingCommas: 'error',
        enableSchemaRequest: false,
      };
      const yamlFormatProvider: languages.DocumentFormattingEditProvider = {
        provideDocumentFormattingEdits(model) {
          return [
            {
              text: formatYaml(model.getValue()),
              range: model.getFullModelRange(),
            },
          ];
        },
      };
      monaco.languages.registerDocumentFormattingEditProvider('yaml', yamlFormatProvider);
      monaco.editor.getModels().forEach((model) => model.updateOptions({ tabSize: 2 }));
      monaco.languages.json.jsonDefaults.setDiagnosticsOptions(schemaConfig);
    });
  };

  const validateData = (pluginName: string, value: PluginComponent.Data) => {
    return fetchSchema(pluginName, schemaType).then((schema) => {
      return new Promise((resolve) => {
        if (schema.oneOf) {
          (schema.oneOf || []).forEach((item: any) => {
            injectDisableProperty(item);
          });
        } else {
          injectDisableProperty(schema);
        }
        const validate = ajv.compile(schema);
        if (validate(value)) {
          resolve(value);
          return;
        }

        // eslint-disable-next-line
        for (const err of validate.errors as DefinedError[]) {
          let description = '';
          switch (err.keyword) {
            case 'enum':
              description = `${err.dataPath} ${err.message}: ${err.params.allowedValues.join(
                ', ',
              )}`;
              break;
            case 'minItems':
            case 'type':
              description = `${err.dataPath} ${err.message}`;
              break;
            case 'oneOf':
            case 'required':
              description = err.message || '';
              break;
            default:
              description = `${err.schemaPath} ${err.message}`;
          }
          notification.error({
            message: 'Invalid plugin data',
            description,
          });
        }
      });
    });
  };

  const handleModeChange = (value: PluginComponent.MonacoLanguage) => {
    switch (value) {
      case monacoModeList.JSON: {
        if (monacoMode === monacoModeList.YAML) {
          const { data: yamlData, error } = yaml2json(content, true);
          if (error) {
            notification.error({ message: formatMessage({ id: 'component.global.invalidYaml' }) });
            return;
          }
          setContent(js_beautify(yamlData, { indent_size: 2 }));
        } else {
          setContent(js_beautify(JSON.stringify(getUIFormData()), { indent_size: 2 }));
        }
        break;
      }
      case monacoModeList.YAML: {
        const jsonData =
          monacoMode === monacoModeList.JSON ? content : JSON.stringify(getUIFormData());
        const { data: yamlData, error } = json2yaml(jsonData);
        if (error) {
          notification.error({ message: formatMessage({ id: 'component.global.invalidJson' }) });
          return;
        }
        setContent(yamlData);
        break;
      }

      case monacoModeList.UIForm: {
        if (monacoMode === monacoModeList.JSON) {
          setUIFormData(JSON.parse(content));
        } else {
          const { data: yamlData, error } = yaml2json(content, true);
          if (error) {
            notification.error({ message: formatMessage({ id: 'component.global.invalidYaml' }) });
            return;
          }
          setUIFormData(JSON.parse(yamlData));
        }
        break;
      }
      default:
        break;
    }
    setMonacoMode(value);
  };

  const isNoConfigurationRequired =
    pluginType === 'auth' &&
    schemaType !== 'consumer' &&
    monacoMode !== monacoModeList.UIForm &&
    targetPluginName !== 'key-auth';

  return (
    <Drawer
      title={formatMessage({ id: 'component.plugin.editor' })}
      visible={visible}
      placement="right"
      closable={false}
      maskClosable={maskClosable}
      destroyOnClose
      onClose={onClose}
      width={700}
      footer={
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
          {' '}
          <Button onClick={onClose} key={1}>
            {formatMessage({ id: 'component.global.cancel' })}
          </Button>
          <Space>
            <Popconfirm
              title={formatMessage({ id: 'page.plugin.drawer.popconfirm.title.delete' })}
              okText={formatMessage({ id: 'component.global.confirm' })}
              cancelText={formatMessage({ id: 'component.global.cancel' })}
              disabled={readonly}
              onConfirm={() => {
                onChange({
                  formData: form.getFieldsValue(),
                  monacoData: {},
                  shouldDelete: true,
                });
              }}
            >
              {initialData[name] ? (
                <Button key={3} type="primary" danger disabled={readonly}>
                  {formatMessage({ id: 'component.global.delete' })}
                </Button>
              ) : null}
            </Popconfirm>
            <Button
              key={2}
              disabled={readonly}
              type="primary"
              onClick={() => {
                try {
                  let editorData;
                  if (monacoMode === monacoModeList.JSON) {
                    editorData = JSON.parse(content);
                  } else if (monacoMode === monacoModeList.YAML) {
                    editorData = yaml2json(content, false).data;
                  } else {
                    editorData = getUIFormData();
                  }

                  validateData(name, editorData).then((value) => {
                    onChange({ formData: form.getFieldsValue(), monacoData: value });
                  });
                } catch (error) {
                  notification.error({
                    message: formatMessage({ id: 'component.global.invalidJson' }),
                  });
                }
              }}
            >
              {formatMessage({ id: 'component.global.submit' })}
            </Button>
          </Space>
        </div>
      }
    >
      <style>
        {`
        .site-page-header {
          border: 1px solid rgb(235, 237, 240);
          margin-top:10px;
        }
        .ant-input[disabled] {
          color: #000;
        }
      `}
      </style>

      <Form {...FORM_ITEM_LAYOUT} style={{ marginTop: '10px' }} form={form}>
        <Form.Item label={formatMessage({ id: 'component.global.name' })}>
          <Input value={name} bordered={false} disabled />
        </Form.Item>
        <Form.Item
          label={formatMessage({ id: 'component.global.enable' })}
          valuePropName="checked"
          name="disable"
        >
          <Switch
            defaultChecked={
              isEnabled ? true : initialData[name] && !initialData[name]?._meta?.disable
            }
            disabled={readonly || isEnabled}
          />
        </Form.Item>
        {type === 'global' && (
          <Form.Item label={formatMessage({ id: 'component.global.scope' })} name="scope">
            <Select disabled>
              <Select.Option value="global">{formatMessage({ id: 'other.global' })}</Select.Option>
            </Select>
          </Form.Item>
        )}
      </Form>
      <Divider orientation="left">{formatMessage({ id: 'component.global.data.editor' })}</Divider>
      <PageHeader
        title=""
        subTitle={
          isNoConfigurationRequired ? (
            <Alert
              message={formatMessage({ id: 'component.plugin.noConfigurationRequired' })}
              type="warning"
            />
          ) : null
        }
        ghost={false}
        extra={[
          <Select
            defaultValue={monacoModeList.JSON}
            value={monacoMode}
            options={modeOptions}
            onChange={handleModeChange}
            data-cy="monaco-mode"
            key={1}
          />,
          <Button
            type="default"
            icon={<LinkOutlined />}
            onClick={() => {
              if (name.startsWith('serverless')) {
                window.open('https://apisix.apache.org/docs/apisix/plugins/serverless');
              } else {
                window.open(`https://apisix.apache.org/docs/apisix/plugins/${name}`);
              }
            }}
            key={3}
          >
            {formatMessage({ id: 'component.global.document' })}
          </Button>,
        ]}
      />
      {Boolean(monacoMode === monacoModeList.UIForm) && (
        <PluginForm
          name={name}
          schema={pluginSchema}
          form={UIForm}
          renderForm={!(pluginType === 'auth' && schemaType !== 'consumer')}
        />
      )}
      <div style={{ display: monacoMode === monacoModeList.UIForm ? 'none' : 'unset' }}>
        <Editor
          value={content}
          onChange={(text) => {
            if (text) {
              setContent(text);
            } else {
              setContent('');
            }
          }}
          language={monacoMode.toLocaleLowerCase()}
          beforeMount={editorWillMount}
          onMount={(editor) => {
            // NOTE: for debug & test
            // @ts-ignore
            window.monacoEditor = editor;
            if (targetModel && content === '{}') editor.setValue(targetModelCode);
          }}
          options={{
            scrollbar: {
              vertical: 'hidden',
              horizontal: 'hidden',
            },
            wordWrap: 'on',
            minimap: { enabled: false },
            readOnly: readonly,
          }}
        />
      </div>
    </Drawer>
  );
};

export default PluginDetail;
