import {
  Control,
  FieldError,
  FieldPath,
  FieldValues,
  PathValue,
  useController,
  UseControllerProps,
} from 'react-hook-form'
import {
  Autocomplete,
  AutocompleteChangeDetails,
  AutocompleteChangeReason,
  AutocompleteProps,
  AutocompleteValue,
  AutocompleteValueOrFreeSoloValueMapping,
  Checkbox,
  ChipTypeMap,
  CircularProgress,
  TextField,
  TextFieldProps,
  useForkRef,
} from '@mui/material'
import {setRef} from '@mui/material/utils'
import {useFormError} from './FormErrorProvider'
import {
  ElementType,
  forwardRef,
  type ReactElement,
  ReactNode,
  Ref,
  RefAttributes,
  SyntheticEvent,
} from 'react'
import {useTransform} from './useTransform'
import {propertyExists} from './utils'

export type AutocompleteElementProps<
  TValue,
  Multiple extends boolean | undefined,
  DisableClearable extends boolean | undefined,
  FreeSolo extends boolean | undefined,
  ChipComponent extends ElementType = ChipTypeMap['defaultComponent'],
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName
  control?: Control<TFieldValues>
  options: TValue[]
  loading?: boolean
  multiple?: Multiple
  loadingIndicator?: ReactNode
  rules?: UseControllerProps<TFieldValues, TName>['rules']
  parseError?: (error: FieldError) => ReactNode
  required?: boolean
  label?: TextFieldProps['label']
  showCheckbox?: boolean
  matchId?: boolean
  autocompleteProps?: Omit<
    AutocompleteProps<
      TValue,
      Multiple,
      DisableClearable,
      FreeSolo,
      ChipComponent
    >,
    'name' | 'options' | 'loading' | 'renderInput'
  >
  textFieldProps?: Omit<TextFieldProps, 'name' | 'required' | 'label'>
  transform?: {
    input?: (
      value: PathValue<TFieldValues, TName>
    ) => AutocompleteValue<TValue, Multiple, DisableClearable, FreeSolo>
    output?: (
      event: SyntheticEvent,
      value: AutocompleteValue<TValue, Multiple, DisableClearable, FreeSolo>,
      reason: AutocompleteChangeReason,
      details?: AutocompleteChangeDetails<TValue>
    ) => PathValue<TFieldValues, TName>
  }
}

type AutocompleteElementComponent = <
  TValue,
  Multiple extends boolean | undefined = false,
  DisableClearable extends boolean | undefined = false,
  FreeSolo extends boolean | undefined = false,
  ChipComponent extends ElementType = ChipTypeMap['defaultComponent'],
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
  props: AutocompleteElementProps<
    TValue,
    Multiple,
    DisableClearable,
    FreeSolo,
    ChipComponent,
    TFieldValues,
    TName
  > &
    RefAttributes<HTMLDivElement>
) => ReactElement

const AutocompleteElement = forwardRef(function AutocompleteElement<
  TValue,
  Multiple extends boolean | undefined = false,
  DisableClearable extends boolean | undefined = false,
  FreeSolo extends boolean | undefined = false,
  ChipComponent extends ElementType = ChipTypeMap['defaultComponent'],
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(
  props: AutocompleteElementProps<
    TValue,
    Multiple,
    DisableClearable,
    FreeSolo,
    ChipComponent,
    TFieldValues,
    TName
  >,
  ref: Ref<HTMLDivElement>
) {
  const {
    textFieldProps,
    autocompleteProps,
    name,
    control,
    options,
    loading,
    showCheckbox,
    rules,
    loadingIndicator,
    required,
    multiple,
    label,
    parseError,
    transform,
    matchId,
  } = props

  const errorMsgFn = useFormError()
  const customErrorFn = parseError || errorMsgFn

  const validationRules = {
    ...rules,
    ...(required && {
      required: rules?.required || 'This field is required',
    }),
  }

  const {
    field,
    fieldState: {error},
  } = useController({
    name,
    control,
    disabled: autocompleteProps?.disabled,
    rules: validationRules,
  })

  const getOptionLabel = (
    option: AutocompleteValueOrFreeSoloValueMapping<TValue, FreeSolo>
  ): string => {
    if (typeof autocompleteProps?.getOptionLabel === 'function') {
      return autocompleteProps.getOptionLabel(option)
    }
    if (typeof option === 'string') {
      return option
    }
    if (propertyExists(option, 'label')) {
      return `${option?.label}`
    }
    return `${option}`
  }

  const isOptionEqualToValue = (
    option: TValue,
    value: AutocompleteValueOrFreeSoloValueMapping<TValue, FreeSolo>
  ): boolean => {
    if (typeof autocompleteProps?.isOptionEqualToValue == 'function') {
      return autocompleteProps.isOptionEqualToValue(option, value)
    }
    const optionKey = propertyExists(option, 'id') ? option.id : option
    if (typeof value === 'string' || typeof value === 'number') {
      return optionKey === value
    }
    const valueKey = propertyExists(value, 'id') ? value.id : value
    return optionKey === valueKey
  }

  const matchOptionByValue = (currentValue: TValue) => {
    return options.find((option) => {
      if (matchId && propertyExists(option, 'id')) {
        return option.id === currentValue
      }
      return isOptionEqualToValue(option, currentValue)
    })
  }

  const {value, onChange} = useTransform<
    TFieldValues,
    TName,
    AutocompleteValue<TValue, Multiple, DisableClearable, FreeSolo>
  >({
    value: field.value,
    onChange: field.onChange,
    transform: {
      input:
        typeof transform?.input === 'function'
          ? transform.input
          : (newValue) => {
              return (
                multiple
                  ? (Array.isArray(newValue) ? newValue : []).map(
                      (v) =>
                        matchOptionByValue(v) ??
                        (autocompleteProps?.freeSolo ? v : undefined)
                    )
                  : matchOptionByValue(newValue) ??
                    (autocompleteProps?.freeSolo ? newValue : null)
              ) as AutocompleteValue<
                TValue,
                Multiple,
                DisableClearable,
                FreeSolo
              >
            },
      output:
        typeof transform?.output === 'function'
          ? transform.output
          : (
              _event: SyntheticEvent,
              newValue: AutocompleteValue<
                TValue,
                Multiple,
                DisableClearable,
                FreeSolo
              >
            ) => {
              if (multiple) {
                const newValues = Array.isArray(newValue) ? newValue : []
                return (
                  matchId
                    ? newValues.map((currentValue) =>
                        propertyExists(currentValue, 'id')
                          ? currentValue.id
                          : currentValue
                      )
                    : newValues
                ) as PathValue<TFieldValues, TName>
              }
              return (
                matchId && propertyExists(newValue, 'id')
                  ? newValue.id
                  : newValue
              ) as PathValue<TFieldValues, TName>
            },
    },
  })

  const handleInputRef = useForkRef(field.ref, textFieldProps?.inputRef)

  const loadingElement = loadingIndicator || (
    <CircularProgress color="inherit" size={20} />
  )

  return (
    <Autocomplete
      {...autocompleteProps}
      value={value}
      loading={loading}
      multiple={multiple}
      options={options}
      disableCloseOnSelect={
        typeof autocompleteProps?.disableCloseOnSelect === 'boolean'
          ? autocompleteProps.disableCloseOnSelect
          : !!multiple
      }
      isOptionEqualToValue={isOptionEqualToValue}
      getOptionLabel={getOptionLabel}
      onChange={(event, newValue, reason, details) => {
        onChange(event, newValue, reason, details)
        if (autocompleteProps?.onChange) {
          autocompleteProps.onChange(event, newValue, reason, details)
        }
      }}
      ref={ref}
      renderOption={
        autocompleteProps?.renderOption ??
        (showCheckbox
          ? (props, option, {selected}) => {
              return (
                <li {...props} key={props.key}>
                  <Checkbox sx={{marginRight: 1}} checked={selected} />
                  {getOptionLabel(option)}
                </li>
              )
            }
          : undefined)
      }
      onBlur={(event) => {
        field.onBlur()
        if (typeof autocompleteProps?.onBlur === 'function') {
          autocompleteProps.onBlur(event)
        }
      }}
      renderInput={(params) => {
        const paramsHtml = params.slotProps.htmlInput
        const tfHtml = textFieldProps?.slotProps?.htmlInput
        const paramsHtmlRef =
          paramsHtml && typeof paramsHtml === 'object' && 'ref' in paramsHtml
            ? (paramsHtml as {ref?: React.Ref<HTMLInputElement | null>}).ref
            : undefined
        const tfHtmlRef =
          tfHtml && typeof tfHtml === 'object' && 'ref' in tfHtml
            ? (tfHtml as {ref?: React.Ref<HTMLInputElement | null>}).ref
            : undefined
        const htmlInputRef = (instance: HTMLInputElement | null) => {
          setRef(handleInputRef, instance)
          setRef(paramsHtmlRef, instance)
          setRef(tfHtmlRef, instance)
        }
        return (
          <TextField
            {...textFieldProps}
            name={name}
            required={rules?.required ? true : required}
            label={label}
            id={params.id}
            disabled={params.disabled}
            fullWidth={params.fullWidth}
            size={params.size}
            error={!!error}
            helperText={
              error
                ? typeof customErrorFn === 'function'
                  ? customErrorFn(error)
                  : error.message
                : textFieldProps?.helperText
            }
            slotProps={{
              ...textFieldProps?.slotProps,
              inputLabel: {
                ...params.slotProps.inputLabel,
                ...textFieldProps?.slotProps?.inputLabel,
              },
              input: {
                ...params.slotProps.input,
                ...textFieldProps?.slotProps?.input,
                endAdornment: (
                  <>
                    {loading ? loadingElement : null}
                    {params.slotProps.input.endAdornment}
                  </>
                ),
              },
              htmlInput: {
                ...params.slotProps.htmlInput,
                ...(typeof tfHtml === 'object' && tfHtml !== null
                  ? tfHtml
                  : {}),
                ref: htmlInputRef,
              },
            }}
          />
        )
      }}
    />
  )
})
AutocompleteElement.displayName = 'AutocompleteElement'
export default AutocompleteElement as AutocompleteElementComponent
