import { useField } from 'formik';
import { useCallback } from 'react';
import type { Except } from 'type-fest';

import { FormFieldProps } from '../../form/FormField/FormField';
import { GeneralFormControlProps } from '../../form/types';
import { useId } from '../../hooks/useId';
import { CommonFormikProps } from '../common/types';

/** Result of {@link useFormikFieldsProps} */
export interface UseFormikFieldsPropsResult<
  Value,
  OnChangeValue,
  ControlProps extends GeneralFormControlProps<any, any>,
> {
  /** Auto-generated field identificator */
  id: string;
  /** Props that should be used in {@link FormField} */
  formFieldProps: Omit<FormFieldProps, 'children' | 'id' | 'direction' | 'ariaDescribedBy'>;
  /** Props that should be used in form control that expect {@link GeneralFormControlProps}. */
  controlProps: Except<
    ControlProps & GeneralFormControlProps<Value, OnChangeValue>,
    'className' | 'id' | 'testId'
  >;
}

/**
 * Return general props for {@link FormField} and some form control that expect {@link GeneralFormControlProps}
 * based on current formik field state.
 */
export function useFormikFieldsProps<
  ControlProps extends GeneralFormControlProps<any, any>,
  Value = ControlProps extends GeneralFormControlProps<infer T, any> ? T : never,
  OnChange = (ControlProps extends GeneralFormControlProps<any, infer T> ? T : unknown) extends unknown
    ? Value
    : ControlProps extends GeneralFormControlProps<any, infer T>
    ? T
    : never,
>(props: CommonFormikProps<OnChange>): UseFormikFieldsPropsResult<Value, OnChange, ControlProps> {
  const {
    disabled,
    description,
    requirement,
    hintText,
    onHintClick,
    name,
    label,
    noVisualLabel,
    className,
    onChange,
    testId,
    ariaDescribedBy,
    ...otherProps
  } = props;

  const [field, meta, helpers] = useField(name);

  const id = useId();
  const errorMessageId = useId();
  const descriptionMessageId = useId();

  const handleOnChange = useCallback(
    (value: OnChange) => {
      if (onChange) {
        onChange(value);
      }
      helpers.setValue(value);
    },
    [helpers, onChange],
  );

  const handleOnBlur = useCallback(() => {
    helpers.setTouched(true);
  }, [helpers]);

  return {
    id,
    formFieldProps: {
      errorMessageId: meta.error ? errorMessageId : undefined,
      descriptionMessageId: !!description ? descriptionMessageId : undefined,
      label: noVisualLabel ? undefined : label,
      description,
      requirement,
      hintText,
      onHintClick,
      className,
      error: meta.error ? meta.error : undefined,
      testId,
    },
    controlProps: {
      ...otherProps,
      value: field.value,
      ariaLabel: noVisualLabel ? label : undefined,
      ariaErrorMessage: meta.error ? errorMessageId : undefined,
      ariaDescribedBy: ariaDescribedBy ?? (!!description ? descriptionMessageId : undefined),
      ariaInvalid: !!meta.error,
      ariaRequired: requirement ?? undefined,
      required: requirement ?? undefined,
      disabled: !!disabled,
      onChange: handleOnChange,
      onBlur: handleOnBlur,
    } as ControlProps,
  };
}
