import { useCallback, useMemo, useReducer } from 'react'

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

import { Field, FieldValidation, validateRequiredField } from '../validators'
import bindActionCreators from './bindActionCreators'

export type Validator<V> = (field: Field<V>) => FieldValidation
type Adapter<A, B> = (value: A, field: Field<B>) => B
type Accessor<V> = string | ((state: any) => V)

export interface BaseDescription {
  /**
   * If set to false, ignores the current state of this field
   * for state validation. Useful for fields which might be in
   * the form state but aren't modified (e.g. id).
   */
  // ignoreValidation?: boolean
  /**
   * Whether the field needs to be filled (i.e. different from null,
   * undefined and empty string). If set to true, the default Required
   * validator is attributed to this field.
   */
  required: boolean
  /**
   * Transforms the incoming value for the field into a new value to be
   * stored.
   */
  adapter?: Adapter<any, any>
  /**
   * A property name or a function to extract a value from the initial state.
   */
  accessor?: Accessor<any>
  /**
   * Validators are tested in order.
   */
  validators?: Validator<any>[]
  /**
   * Value to be used when no initial state is provided, no accessor is provided,
   * or if the accessed value is undefined, null or an empty string.
   */
  defaultValue?: any
}

type DescriptionValidator<V> = Omit<BaseDescription, 'validators'> & {
  validators: Validator<V>[]
}
type DescriptionAdapter<A, B> = Omit<BaseDescription, 'adapter'> & {
  adapter: Adapter<A, B>
}
type DescriptionAccessor<V> = Omit<BaseDescription, 'accessor'> & {
  accessor: Accessor<V>
}
type DescriptionDefaultValue<V> = Omit<BaseDescription, 'defaultValue'> & {
  defaultValue: V
}

type FieldValue<X> = X extends DescriptionAdapter<infer A, infer B>
  ? B
  : X extends DescriptionValidator<infer V1>
  ? V1
  : X extends DescriptionDefaultValue<infer V2>
  ? V2
  : X extends DescriptionAccessor<infer V3>
  ? V3
  : unknown

type UpdateValue<X> = X extends DescriptionAdapter<infer A, infer B>
  ? A
  : X extends DescriptionValidator<infer V1>
  ? V1
  : X extends DescriptionDefaultValue<infer V2>
  ? V2
  : X extends DescriptionAccessor<infer V3>
  ? V3
  : unknown

type GetTypedDescription<X> = X extends DescriptionAdapter<infer A, infer B>
  ? {
      defaultValue?: B
      validators?: Validator<B>[]
      accessor?: Accessor<B>
    }
  : X extends DescriptionValidator<infer V1>
  ? {
      defaultValue?: V1
      adapter?: Adapter<any, V1>
      accessor?: Accessor<V1>
    }
  : X extends DescriptionAccessor<infer V2>
  ? {
      defaultValue?: V2
      validators?: Validator<V2>[]
      adapter?: Adapter<any, V2>
    }
  : X extends DescriptionDefaultValue<infer V3>
  ? {
      validators?: Validator<V3>[]
      adapter?: Adapter<any, V3>
      accessor?: Accessor<V3>
    }
  : Record<string, any>

type Description<X> = X & GetTypedDescription<X>
type Descriptor<T> = Record<string, Description<T>>

type FormState<D> = {
  [K in keyof D]: Field<FieldValue<D[K]>>
}
type FormValidator<D> = (
  state: FormState<D>
) => Partial<Record<keyof D, FieldValidation>>

export function createFormReducer<
  T extends BaseDescription,
  U extends Descriptor<T>
>(descriptor: U, stateValidators?: FormValidator<U>[]) {
  type State = FormState<U>
  return function<S>(initialState?: S) {
    const initializeState = useCallback(function(initialValues?: S) {
      const val = Object.entries(descriptor).reduce((acc, [k, description]) => {
        const key = k as keyof U
        const { accessor, defaultValue } = description
        let value: any
        if (!initialValues) {
          value = defaultValue
        } else if (accessor) {
          value =
            typeof accessor === 'function'
              ? accessor(initialValues)
              : initialValues[accessor as keyof S]
          value = value !== undefined ? value : defaultValue
        }
        acc[key] = {
          value,
          error: false,
          invalid: false,
          success: false,
          errorText: undefined
        }
        return acc
      }, {} as State)

      return val
    }, [])

    type UpdatePayload = {
      [k in keyof State]?: UpdateValue<U[k]> | undefined
    }

    const slice = useMemo(
      () =>
        createSlice({
          name: 'formReducer',
          initialState: initializeState(),
          reducers: {
            update(state, action: PayloadAction<UpdatePayload>) {
              Object.entries(action.payload).forEach(([k, v]) => {
                const key = k as keyof UpdatePayload
                const value = v as UpdatePayload[typeof key]
                const description = descriptor[key]
                const field = (state as any)[key] as State[typeof key]
                if (description.adapter) {
                  const adaptedValue = description.adapter(value, field)
                  field.value = adaptedValue as typeof field.value
                } else {
                  field.value = (value as any) as typeof field.value
                }
                if (description.required) {
                  const { error } = validateRequiredField(field)
                  field.invalid = error
                }

                if (description.validators) {
                  field.invalid = description.validators.some(
                    (validator) => validator(field).error
                  )
                }

                field.success = !!field.value && !field.invalid
                field.error = false
                field.errorText = undefined
              })

              if (stateValidators) {
                stateValidators.forEach((validator) => {
                  const validatedFields = validator((state as any) as State)
                  Object.keys(validatedFields).forEach((k) => {
                    const field = state[k]
                    const validation = validatedFields[k]
                    field.invalid = field.invalid || !!validation?.error
                    field.error = field.error || false
                    field.errorText = field.errorText || undefined
                    field.success = !!field.value && !field.invalid
                  })
                })
              }
            },
            validate(state, action: PayloadAction<keyof State>) {
              const key = action.payload
              const description = descriptor[key]
              const field = (state as any)[key] as State[typeof key]
              if (field) {
                field.invalid = false
                if (description.required) {
                  const { error, errorText } = validateRequiredField(field)
                  field.error = error
                  field.errorText = errorText
                }
                if (description.validators && !field.error) {
                  description.validators.some((validator) => {
                    const { error: vError, errorText: vErrorText } = validator(
                      field
                    )
                    field.error = vError
                    field.errorText = vErrorText
                    return vError
                  })
                }
                field.success = !field.error && !!field.value
              }

              if (stateValidators) {
                const fieldKeysToValidate = Object.keys(state).filter(
                  (k) => !state[k].error
                )
                stateValidators.forEach((validator) => {
                  const validatedFields = validator((state as any) as State)
                  fieldKeysToValidate.forEach((k) => {
                    const field = state[k]
                    const validation = validatedFields[k]
                    if (!field.error && validation?.error) {
                      field.error = validation.error
                      field.errorText = validation.errorText
                    }
                    field.success = !field.error && !!field.value
                  })
                })
              }
            },
            reset(state, action: PayloadAction<S | undefined>) {
              return initializeState(action.payload)
            }
          }
        }),
      [initializeState, stateValidators]
    )

    type Actions = Omit<typeof slice.actions, 'update'> & {
      update(
        payload: UpdatePayload
      ): {
        payload: UpdatePayload
        type: string
      }
    }

    const [currentState, dispatch] = useReducer(
      slice.reducer,
      initialState,
      initializeState
    )
    const actions = useMemo(
      () => bindActionCreators(slice.actions, dispatch) as Actions,
      [slice.actions, dispatch]
    )

    type FormValues = {
      [k in keyof U]: FieldValue<U[k]>
    }

    const getValues = useCallback(() => {
      return Object.entries(currentState).reduce((acc, [k, f]) => {
        const key = k as keyof State
        const field = f as State[typeof key]
        acc[k as keyof FormValues] = field.value
        return acc
      }, {} as FormValues)
    }, [currentState])

    const invalidState = useMemo(() => {
      return Object.entries(currentState).some(([k, f]) => {
        const key = k as keyof State
        const field = f as State[typeof key]
        const description = descriptor[key]
        if (!description.required) {
          return field.error || field.invalid
        } else {
          return (
            validateRequiredField(field).error || field.error || field.invalid
          )
        }
      })
    }, [currentState])

    const getInvalidExcept = useCallback(
      (keys: Array<keyof State>) => {
        return Object.entries(currentState).some(([k, f]) => {
          const key = k as keyof State
          if (keys.includes(key)) return false
          const field = f as State[typeof key]
          const description = descriptor[key]
          if (!description.required) {
            return field.error || field.invalid
          } else {
            return (
              validateRequiredField(field).error || field.error || field.invalid
            )
          }
        })
      },
      [currentState]
    )

    return {
      state: currentState,
      actions,
      invalidState,
      getValues,
      getInvalidExcept
    }
  }
}
