import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { createErrorHandler } from '@openmrs/esm-framework';
import type { FormField, FormFieldInputProps, RenderType } from '../../types';
import { FormFieldRenderer } from '../renderer/field/form-field-renderer.component';
import { clearSubmission, isViewMode } from '../../utils/common-utils';
import { cloneRepeatField } from './helpers';
import { evaluateAsyncExpression, evaluateExpression } from '../../utils/expression-runner';
import { isEmpty } from '../../validators/form-validator';
import { useFormFactory } from '../../provider/form-factory-provider';
import { useFormProviderContext } from '../../provider/form-provider';
import RepeatControls from './repeat-controls.component';
import styles from './repeat.scss';

const renderingByTypeMap: Record<string, RenderType> = {
  obsGroup: 'group',
  testOrder: 'select',
  diagnosis: 'ui-select-extended',
};

const Repeat: React.FC<FormFieldInputProps> = ({ field }) => {
  const [rows, setRows] = useState([]);
  const context = useFormProviderContext();
  const { handleConfirmQuestionDeletion } = useFormFactory();
  const {
    patient,
    sessionMode,
    formFieldAdapters,
    formFields,
    methods: { getValues, setValue, unregister },
    addFormField,
    removeFormField,
    deletedFields,
    setDeletedFields,
    visit,
  } = context;

  useEffect(() => {
    const repeatedFields = formFields.filter(
      (_field) =>
        _field.questionOptions.concept === field.questionOptions.concept &&
        _field.id.startsWith(field.id) &&
        !_field.meta?.repeat?.wasDeleted,
    );
    setRows(repeatedFields);
  }, [formFields, field]);

  const handleAdd = useCallback(
    (counter: number) => {
      const clonedFieldsBuffer: FormField[] = [];
      function evaluateExpressions(field: FormField) {
        if (field.hide?.hideWhenExpression) {
          field.isHidden = evaluateExpression(
            field.hide.hideWhenExpression,
            { value: field, type: 'field' },
            [...formFields, ...clonedFieldsBuffer],
            getValues(),
            {
              mode: sessionMode,
              patient: patient,
              visit,
            },
          );
        }
        if (field.questionOptions.calculate?.calculateExpression) {
          evaluateAsyncExpression(
            field.questionOptions.calculate?.calculateExpression,
            { value: field, type: 'field' },
            [...formFields, ...clonedFieldsBuffer],
            getValues(),
            {
              mode: sessionMode,
              patient: patient,
              visit,
            },
          ).then((result) => {
            if (!isEmpty(result)) {
              setValue(field.id, result);
              formFieldAdapters[field.type]?.transformFieldValue(field, result, context);
            }
          });
        }
      }

      const clonedField = cloneRepeatField(field, null, counter);
      clonedFieldsBuffer.push(clonedField);

      // Handle nested questions
      if (clonedField.type === 'obsGroup') {
        clonedField.questions?.forEach((childField) => {
          clonedFieldsBuffer.push(childField);
        });
      }

      clonedFieldsBuffer.forEach((field) => {
        evaluateExpressions(field);
        addFormField(field);
      });
      setRows([...rows, clonedField]);
    },
    [formFields, field, rows, context],
  );

  const removeNthRow = (field: FormField) => {
    if (field.meta.initialValue?.omrsObject) {
      formFieldAdapters[field.type]?.transformFieldValue(field, null, context);
      field.meta.repeat = { ...(field.meta.repeat || {}), wasDeleted: true };
      if (field.type === 'obsGroup') {
        field.questions.forEach((child) => {
          child.meta.repeat = { ...(field.meta.repeat || {}), wasDeleted: true };
          formFieldAdapters[child.type]?.transformFieldValue(child, null, context);
        });
      }
    } else {
      clearSubmission(field);
      // Unregister from react-hook-form so a re-added row with the same id
      // starts empty instead of rehydrating the deleted row's value.
      unregister(field.id);
      field.questions?.forEach((child) => unregister(child.id));
    }
    setRows(rows.filter((q) => q.id !== field.id));
    setDeletedFields([...deletedFields, field]);
    removeFormField(field.id);
  };

  const onClickDeleteQuestion = (field: Readonly<FormField>) => {
    if (handleConfirmQuestionDeletion && typeof handleConfirmQuestionDeletion === 'function') {
      const result = handleConfirmQuestionDeletion(field);
      if (result && typeof result.then === 'function' && typeof result.catch === 'function') {
        result.then(() => removeNthRow(field)).catch(() => createErrorHandler());
      } else if (typeof result === 'boolean') {
        result && removeNthRow(field);
      } else {
        removeNthRow(field);
      }
    } else {
      removeNthRow(field);
    }
  };

  const nodes = useMemo(() => {
    return rows.map((row, index) => {
      const component = (
        <FormFieldRenderer
          fieldId={row.id}
          valueAdapter={formFieldAdapters[row.type]}
          repeatOptions={{ targetRendering: getQuestionWithSupportedRendering(row).questionOptions.rendering }}
        />
      );
      return (
        <div key={row.id + '_wrapper'}>
          {index !== 0 && (
            <div>
              <hr className={styles.divider} />
            </div>
          )}
          <div className={styles.nodeContainer}>{component}</div>
          {!isViewMode(sessionMode) && (
            <RepeatControls
              question={row}
              rows={rows}
              questionIndex={index}
              handleDelete={() => {
                onClickDeleteQuestion(row);
              }}
              handleAdd={() => {
                // Use max-existing-suffix + 1 so new rows don't collide with a surviving
                // row after a middle row is deleted (the buggy `length - 1 + 1` would).
                const nextSuffix =
                  rows.reduce((max, r) => {
                    const suffix = Number(r.id.slice(field.id.length + 1));
                    return Number.isFinite(suffix) ? Math.max(max, suffix) : max;
                  }, 0) + 1;
                handleAdd(nextSuffix);
              }}
            />
          )}
        </div>
      );
    });
  }, [rows]);

  if (field.isHidden || !nodes || !hasVisibleField(field)) {
    return null;
  }

  return (
    <React.Fragment>
      <div>{nodes}</div>
    </React.Fragment>
  );
};

function hasVisibleField(field: FormField) {
  if (field.questions?.length) {
    return field.questions?.some((child) => !child.isHidden);
  }
  return !field.isHidden;
}

function getQuestionWithSupportedRendering(field: FormField) {
  return {
    ...field,
    questionOptions: {
      ...field.questionOptions,
      rendering: renderingByTypeMap[field.type] || null,
    },
  };
}

export default Repeat;
