import { useContext, useEffect } from 'react';
import { useField, useFormikContext } from 'formik';
import { CustomFieldInputProps } from './CustomFieldInputProps';
import { CustomFieldMetaProps } from './CustomFieldMetaProps';
import { FieldNameContext } from './FieldNameContext';
import { ServerErrorContext } from '../Form/ServerErrorContext';
import { NormalizationFunction } from '../Normalization/NormalizationFunction';
import { ValidationFunction } from '../Validation/ValidationFunction';

export interface useStandardFieldProps<TValue> {
  /** Id of the field. */
  id?: string;
  /** Name of the field. */
  name: string;
  /** Whether the field should be disabled. */
  disabled?: boolean;
  /** Function to validate the field. */
  validate?: ValidationFunction<TValue> | ValidationFunction<TValue>[];
  /** Function to modify the field value without making the form dirty. (e.g. phone number) */
  normalize?: NormalizationFunction<TValue>;
}

/** Provides a consistent way to deal with all form fields (non array). */
export default function useStandardField<TValue>({
  id: providedId,
  name: providedName,
  disabled,
  validate,
  normalize,
}: useStandardFieldProps<TValue>): [
  CustomFieldInputProps<TValue>,
  CustomFieldMetaProps<TValue>
] {
  // because the formik errors are evaluated all at the same time we need to keep server errors separate
  const { getError: getServerError, setError: setServerError } =
    useContext(ServerErrorContext);

  // ensure that form section values are obeyed, e.g. homeAddress.zipCode
  const fieldNameContextValue = useContext(FieldNameContext);
  const name = fieldNameContextValue
    ? `${fieldNameContextValue}.${providedName}`
    : providedName;

  // ensure that nested contexts don't have duplicate id issues when an id is specified
  const id = providedId
    ? fieldNameContextValue
      ? `${fieldNameContextValue}.${providedId}`
      : providedId
    : name;

  // ensure that our custom validation rules are handled
  // e.g. we allow arrays of validators
  const [formikInput, formikMeta] = useField<TValue>({
    name,
    id: id ? id : name,
    disabled: disabled,
    validate: callAllValidators,
  });
  const { setFieldTouched, setFieldValue, isSubmitting } = useFormikContext();

  const touched =
    formikMeta.touched !== false && formikMeta.touched !== undefined;
  useEffect(() => {
    if (!touched && isSubmitting) {
      // because we do not always register all fields up front.
      // e.g. formik expects even a 'create' form to have all fields given, at least, blank values
      // It looks like this was going to be a thing: https://github.com/jaredpalmer/formik/issues/691
      // Formik appears to not have an active maintainer: https://github.com/jaredpalmer/formik/discussions/3526
      // We previously had a different fix in place using handleBlur, but it was causing an infinite update cycle.
      // This was noted as existing, but there was a note about it not working for FieldArray (this does appear to work in my testing with FieldArray)
      setFieldTouched(name);
    }
  }, [isSubmitting, name, setFieldTouched, touched]);

  // these are the props we expect consumers of this hook to pass directly to the input (or other control)
  const resultInput: CustomFieldInputProps<TValue> = {
    name: formikInput.name,
    // pass any direct from server props through normalize without making the form dirty (e.g. phone number)
    value: normalize ? normalize(formikInput.value) : formikInput.value,
    onChange: handleChange,
    onBlur: handleBlur,
    // extensions to formik
    id: id,
  };

  const resultMeta: CustomFieldMetaProps<TValue> = {
    ...formikMeta,
    error: getServerError(name) || (touched ? formikMeta.error : undefined),
    // extensions to formik
    warning: undefined, // TODO - did this never work?
    touched: touched,
  };

  return [resultInput, resultMeta];

  function handleBlur() {
    formikInput.onBlur({ target: { name: name } });
  }

  function handleChange(value: TValue) {
    if (disabled) {
      return;
    }
    const normalized = normalize ? normalize(value) : value;
    setFieldValue(name, normalized);
    setServerError(name, undefined);
  }

  function callAllValidators(value: TValue) {
    if (disabled || !validate) {
      return;
    }

    if (!Array.isArray(validate)) {
      return validate(value);
    }

    for (let i = 0; i < validate.length; i++) {
      const result = validate[i](value);
      if (result) {
        return result;
      }
    }
  }
}
