/*
 * 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 React, {
  ForwardRefRenderFunction,
  forwardRef,
  useImperativeHandle,
  useEffect,
} from 'react';
import { Form, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';

import isEmpty from 'lodash/isEmpty';
import classnames from 'classnames';

import { scrollElementIntoView } from '@/utils';
import type * as Type from '@/common/interface';

import type {
  JSONSchema,
  FormProps,
  FormRef,
  BaseUIOptions,
  FormKit,
  InputGroupOptions,
} from './types';
import {
  Legend,
  Select,
  Check,
  Switch,
  Timezone,
  Upload,
  Textarea,
  Input,
  Button as SfButton,
  InputGroup,
  TagSelector,
} from './components';

export * from './types';

/**
 * TODO:
 *  - [!] Standardised `Admin/Plugins/Config/index.tsx` method for generating dynamic form configurations.
 *  - Normalize and document `formData[key].hidden && 'd-none'`
 *  - Normalize and document `hiddenSubmit`
 *  - Improving field hints for `formData`
 *  - Optimise form data updates
 *    * Automatic field type conversion
 *    * Dynamic field generation
 */

/**
 * json schema form
 * @param schema json schema
 * @param uiSchema ui schema
 * @param formData form data
 * @param onChange change event
 * @param onSubmit submit event
 */
const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
  {
    schema,
    uiSchema = {},
    refreshConfig,
    formData,
    onChange,
    onSubmit,
    hiddenSubmit = false,
  },
  ref,
) => {
  const { t } = useTranslation('translation', {
    keyPrefix: 'form',
  });
  const { required = [], properties = {} } = schema || {};
  // check required field
  const excludes = required.filter((key) => !properties[key]);
  if (excludes.length > 0) {
    console.error(t('not_found_props', { key: excludes.join(', ') }));
  }
  formData ||= {};
  const keys = Object.keys(properties);
  /**
   * Prevent components such as `select` from having default values,
   * which are not generated on `formData`
   */
  const setDefaultValueAsDomBehaviour = () => {
    keys.forEach((k) => {
      const fieldVal = formData![k]?.value;
      const metaProp = properties[k];
      const uiCtrl = uiSchema[k]?.['ui:widget'];
      if (!metaProp || !uiCtrl || fieldVal !== undefined) {
        return;
      }
      if (uiCtrl === 'select' && metaProp.enum?.[0] !== undefined) {
        formData![k] = {
          errorMsg: '',
          isInvalid: false,
          value: metaProp.enum?.[0],
        };
      }
    });
  };
  useEffect(() => {
    setDefaultValueAsDomBehaviour();
  }, [formData]);

  const formKitWithContext: FormKit = {
    refreshConfig() {
      if (typeof refreshConfig === 'function') {
        refreshConfig();
      }
    },
  };

  /**
   * Form validation
   * - Currently only dynamic forms are in use, the business form validation has been handed over to the server
   */
  const requiredValidator = () => {
    const errors: string[] = [];
    required.forEach((key) => {
      if (!formData![key] || !formData![key].value) {
        errors.push(key);
      }
    });
    return errors;
  };

  const syncValidator = () => {
    const errors: Array<{ key: string; msg: string }> = [];
    const promises: Array<{
      key: string;
      promise;
    }> = [];
    keys.forEach((key) => {
      const { validator } = uiSchema[key]?.['ui:options'] || {};
      if (validator instanceof Function) {
        const value = formData![key]?.value;
        promises.push({
          key,
          promise: validator(value, formData),
        });
      }
    });
    return Promise.allSettled(promises.map((item) => item.promise)).then(
      (results) => {
        results.forEach((result, index) => {
          const { key } = promises[index];
          if (result.status === 'rejected') {
            errors.push({
              key,
              msg: result.reason.message,
            });
          }

          if (result.status === 'fulfilled') {
            const msg = result.value;
            if (typeof msg === 'string') {
              errors.push({
                key,
                msg,
              });
            }
          }
        });
        return errors;
      },
    );
  };

  const validator = async (): Promise<boolean> => {
    const errors = requiredValidator();
    if (errors.length > 0) {
      formData = errors.reduce((acc, cur) => {
        acc[cur] = {
          ...formData![cur],
          isInvalid: true,
          errorMsg:
            uiSchema[cur]?.['ui:options']?.empty ||
            `${properties[cur]?.title} ${t('empty')}`,
        };
        return acc;
      }, formData || {});
      if (onChange instanceof Function) {
        onChange({ ...formData });
      }
      scrollElementIntoView(document.getElementById(errors[0]));
      return false;
    }
    const syncErrors = await syncValidator();
    if (syncErrors.length > 0) {
      formData = syncErrors.reduce((acc, cur) => {
        acc[cur.key] = {
          ...formData![cur.key],
          isInvalid: true,
          errorMsg: cur.msg || `${properties[cur.key].title} ${t('invalid')}`,
        };
        return acc;
      }, formData || {});
      if (onChange instanceof Function) {
        onChange({ ...formData });
      }
      scrollElementIntoView(document.getElementById(syncErrors[0].key));
      return false;
    }
    return true;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const isValid = await validator();
    if (!isValid) {
      return;
    }

    Object.keys(formData!).forEach((key) => {
      formData![key].isInvalid = false;
      formData![key].errorMsg = '';
    });
    if (onChange instanceof Function) {
      onChange(formData!);
    }
    if (onSubmit instanceof Function) {
      onSubmit(e);
    }
  };

  useImperativeHandle(ref, () => ({
    validator,
  }));
  if (!formData || !schema || isEmpty(schema.properties)) {
    return null;
  }

  return (
    <Form noValidate onSubmit={handleSubmit}>
      {keys.map((key) => {
        const {
          title,
          description,
          enum: enumValues = [],
          enumNames = [],
          max_length = 0,
          max,
          min = 0,
        } = properties[key];
        const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
          uiSchema?.[key] || {};
        formData ||= {};
        const fieldState = formData[key];
        if (uiOpt?.class_name) {
          uiOpt.className = uiOpt.class_name;
        }

        const uiSimplify = widget === 'legend' || uiOpt?.simplify;
        let groupClassName: BaseUIOptions['field_class_name'] = uiOpt?.simplify
          ? 'mb-2'
          : 'mb-3';
        if (widget === 'legend') {
          groupClassName = 'mb-0';
        }
        if (uiOpt?.field_class_name) {
          groupClassName = uiOpt.field_class_name;
        }

        const readOnly = uiOpt?.readOnly || false;

        return (
          <Form.Group
            key={`${title}-${key}`}
            controlId={key}
            className={classnames(
              groupClassName,
              formData[key]?.hidden ? 'd-none' : null,
            )}>
            {/* Uniform processing `label` */}
            {title && !uiSimplify ? <Form.Label>{title}</Form.Label> : null}
            {/* Handling of individual specific controls */}
            {widget === 'legend' ? (
              <Legend title={title} className={String(uiOpt?.className)} />
            ) : null}
            {widget === 'select' ? (
              <Select
                desc={description}
                fieldName={key}
                onChange={onChange}
                enumValues={enumValues}
                enumNames={enumNames}
                formData={formData}
                readOnly={readOnly}
              />
            ) : null}
            {widget === 'radio' || widget === 'checkbox' ? (
              <Check
                type={widget}
                fieldName={key}
                onChange={onChange}
                enumValues={enumValues}
                enumNames={enumNames}
                formData={formData}
                readOnly={readOnly}
              />
            ) : null}
            {widget === 'switch' ? (
              <Switch
                label={uiOpt && 'label' in uiOpt ? uiOpt.label : ''}
                fieldName={key}
                onChange={onChange}
                formData={formData}
                readOnly={readOnly}
              />
            ) : null}
            {widget === 'timezone' ? (
              <Timezone
                fieldName={key}
                onChange={onChange}
                formData={formData}
                readOnly={readOnly}
              />
            ) : null}
            {widget === 'upload' ? (
              <Upload
                type={
                  uiOpt && 'imageType' in uiOpt ? uiOpt.imageType : undefined
                }
                acceptType={
                  uiOpt && 'acceptType' in uiOpt ? uiOpt.acceptType : ''
                }
                fieldName={key}
                onChange={onChange}
                formData={formData}
                readOnly={readOnly}
                imgClassNames={
                  uiOpt && 'className' in uiOpt ? uiOpt.className : ''
                }
              />
            ) : null}
            {widget === 'textarea' ? (
              <Textarea
                placeholder={
                  uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
                }
                rows={uiOpt && 'rows' in uiOpt ? uiOpt.rows : 3}
                className={uiOpt && 'className' in uiOpt ? uiOpt.className : ''}
                fieldName={key}
                onChange={onChange}
                formData={formData}
                readOnly={readOnly}
              />
            ) : null}
            {widget === 'input' ? (
              <Input
                type={uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'}
                inputMode={
                  uiOpt && 'inputMode' in uiOpt ? uiOpt.inputMode : 'text'
                }
                placeholder={
                  uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
                }
                min={min}
                max={max}
                fieldName={key}
                onChange={onChange}
                formData={formData}
                readOnly={readOnly}
              />
            ) : null}
            {widget === 'button' ? (
              <SfButton
                fieldName={key}
                text={uiOpt && 'text' in uiOpt ? uiOpt.text : ''}
                action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined}
                formKit={formKitWithContext}
                readOnly={readOnly}
                variant={
                  uiOpt && 'variant' in uiOpt ? uiOpt.variant : undefined
                }
                size={uiOpt && 'size' in uiOpt ? uiOpt.size : undefined}
                title={uiOpt && 'title' in uiOpt ? uiOpt?.title : ''}
              />
            ) : null}
            {widget === 'input_group' ? (
              <InputGroup
                formKitWithContext={formKitWithContext}
                uiOpt={uiOpt as InputGroupOptions}
                prefixText={
                  (uiOpt && 'prefixText' in uiOpt && uiOpt.prefixText) || ''
                }
                suffixText={
                  (uiOpt && 'suffixText' in uiOpt && uiOpt.suffixText) || ''
                }>
                <Input
                  type={
                    uiOpt && 'inputType' in uiOpt ? uiOpt.inputType : 'text'
                  }
                  inputMode={
                    uiOpt && 'inputMode' in uiOpt ? uiOpt.inputMode : 'text'
                  }
                  placeholder={
                    uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
                  }
                  min={min}
                  max={max}
                  fieldName={key}
                  onChange={onChange}
                  formData={formData}
                  readOnly={readOnly}
                />
              </InputGroup>
            ) : null}
            {widget === 'tag_selector' ? (
              <TagSelector
                maxTagLength={max_length}
                fieldName={key}
                onChange={onChange}
                formData={formData}
                description={description}
              />
            ) : null}
            {/* Unified handling of `Feedback` and `Text` */}
            {description && widget !== 'tag_selector' ? (
              <Form.Text dangerouslySetInnerHTML={{ __html: description }} />
            ) : null}
            <Form.Control.Feedback type="invalid">
              {fieldState?.errorMsg}
            </Form.Control.Feedback>
          </Form.Group>
        );
      })}
      {!hiddenSubmit && (
        <Button variant="primary" type="submit">
          {t('btn_submit')}
        </Button>
      )}
    </Form>
  );
};

export const initFormData = (schema: JSONSchema): Type.FormDataType => {
  const formData: Type.FormDataType = {};
  const props: JSONSchema['properties'] = schema?.properties || {};
  Object.keys(props).forEach((key) => {
    const prop = props[key];
    let defaultVal: any = '';
    if (Array.isArray(prop.default) && prop.enum && prop.enum.length > 0) {
      // for checkbox default values
      defaultVal = prop.enum;
    } else {
      defaultVal = prop?.default;
    }
    formData[key] = {
      value: defaultVal,
      isInvalid: false,
      errorMsg: '',
    };
  });
  return formData;
};

export const mergeFormData = (
  target: Type.FormDataType | null,
  origin: Type.FormDataType | null,
) => {
  if (!target) {
    return origin;
  }
  if (!origin) {
    return target;
  }
  Object.keys(target).forEach((k) => {
    const oi = origin[k];
    if (oi && oi.value !== undefined) {
      target[k] = {
        value: oi.value,
        isInvalid: false,
        errorMsg: '',
      };
    }
  });
  return target;
};

export default forwardRef(SchemaForm);
