import React, { useReducer, useContext, useEffect, useState } from "react"
import { useId } from "@reach/auto-id"
import set from "lodash.set"
import get from "lodash.get"
import useProgress from "../use-progress"
import useDebounce from "../use-trailing-debounce"

const deepClone = (obj) => JSON.parse(JSON.stringify(obj))

const defaultRequest = async (action, { method, data }) => {
  const response = await fetch(action, {
    method: method,
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      "X-CSRF-Token": getCSRFToken(),
      // TODO: Remove
      "X-React-RB": true,
    },
    body: JSON.stringify(data),
  })

  const result = await response.json()

  // TODO: Remove
  if (result.redirect) {
    window.location = result.redirect
  }

  return result
}

const FormContext = React.createContext(null)

const FormConfigContext = React.createContext({
  request: defaultRequest,
})

export const FormConfig = ({ request = defaultRequest, children }) => {
  return (
    <FormConfigContext.Provider value={{ request }}>
      {children}
    </FormConfigContext.Provider>
  )
}

export function useFormContext() {
  return useContext(FormContext)
}

export function getCSRFToken() {
  try {
    return document.querySelector("meta[name=csrf-token]").content
  } catch (e) {
    return ""
  }
}

/**
 *
 * @param {Object} props
 * @param {Object} form the returned value from the useForm hook
 * @param {string} method
 * @param {string} action
 * @param {function} onSubmit
 */
export function Form({
  form,
  children,
  method,
  action,
  onSubmit = () => {},
  validation = false,
  ...props
}) {
  useEffect(() => {
    form.__configure({ method, action, validation })
  }, [method, action, validation])

  const handleSubmit = async (event) => {
    if (form.status == "loading") {
      event.preventDefault()
      return
    }

    onSubmit(event)

    if (event.defaultPrevented) {
      // consumer handled this submission
      return
    }

    event.preventDefault()
    form.submit()
  }

  return (
    <FormContext.Provider value={form}>
      <form
        method={method}
        action={action}
        onSubmit={handleSubmit}
        onReset={form.reset}
        {...props}
      >
        {children}
      </form>
    </FormContext.Provider>
  )
}

function reducer(state, action) {
  switch (action.type) {
    case "SET_DATA": {
      const { data } = action
      return { ...state, data }
    }
    case "SET_VALUE": {
      const { name, value } = action
      return { ...state, data: { ...set(state.data, name, value) } }
    }
    case "SET_TOUCHED": {
      const { name, touched } = action
      return { ...state, touched: { ...set(state.touched, name, touched) } }
    }
    case "SET_ERROR": {
      const { name, error } = action
      return {
        ...state,
        status: "idle",
        errors: { ...set(state.errors, name, error) },
      }
    }
    case "SET_ERRORS": {
      const { errors } = action
      return { ...state, errors, status: "idle" }
    }
    case "SUBMIT": {
      return { ...state, errors: {}, status: "loading" }
    }
    case "RESOLVE": {
      return { ...state, status: "success" }
    }
    case "REJECT": {
      return { ...state, status: "error" }
    }
    case "RESET": {
      return {
        initialData: state.initialData,
        data: deepClone(state.initialData),
        errors: {},
        touched: {},
        status: "idle",
      }
    }
    default:
      return state
  }
}

export function useField({ name, id, value, onChange = () => {} }) {
  const form = useFormContext()
  id = useId(id)
  // If value is not undefined, the component is controlled.
  value = value === undefined ? form?.getValue(name) : value

  const onBlur = () => {
    form?.setTouched(name)
  }

  const handleChange = (event) => {
    const target = event.target

    const value = target.type === "checkbox" ? target.checked : target.value

    form?.setValue(name, value)
    onChange(event)
  }

  const error = form?.getError(name)
  const touched = form?.getTouched(name)

  return {
    id,
    value,
    onBlur,
    error,
    touched,
    onChange: handleChange,
  }
}

/**
 *
 * @param {Object} options
 * @param {Object} options.initialData
 */
function useForm({ initialData = {} } = {}) {
  const progress = useProgress()
  const { request } = useContext(FormConfigContext)
  const [config, __configure] = useState({})
  const [state, dispatch] = useReducer(reducer, {
    status: "idle",
    initialData,
    data: deepClone(initialData),
    touched: {},
    errors: {},
  })

  const validate = async () => {
    if (config.validation) {
      // If config.validation is true, we're going to use inline validations,
      // but only when the form is submitted.
      if (config.validation !== true) {
        const errors = await request("/validations", {
          method: "post",
          data: {
            validation_data: state.data,
            validation_type: config.validation,
          },
        })

        dispatch({ type: "SET_ERRORS", errors: {} })

        errors.forEach((error) => {
          setError(error.field, error.message)
        })
      }
    }
  }

  const [debouncedValidation] = useDebounce(() => {
    validate()
  }, 700)

  const { data, touched, errors, status } = state

  /* Values */
  const setValue = (name, value) => {
    debouncedValidation()
    dispatch({ type: "SET_VALUE", name, value })
  }

  const getValue = (name) => get(data, name)

  const setData = (data) => {
    dispatch({ type: "SET_DATA", data })
  }

  /* Touched */
  const setTouched = (name, touched = true) => {
    dispatch({ type: "SET_TOUCHED", name, touched })
  }

  const getTouched = (name) => get(touched, name, false)

  /* Errors */
  const setError = (name, error) => {
    dispatch({ type: "SET_ERROR", name, error })
  }

  const reset = () => {
    dispatch({ type: "RESET" })
  }

  const submit = async () => {
    if (state.status === "loading") return

    dispatch({ type: "SUBMIT" })
    progress.go(20)

    try {
      const data = await request(config.action, {
        method: config.method,
        data: state.data,
      })

      if (data.errors) {
        progress.finish()
        if (config.validation) {
          data.errors.forEach((error) => {
            setError(error.field, error.message)
          })
        } else {
          dispatch({ type: "SET_ERRORS", errors: data.errors })
        }
        return { status: "error" }
      } else {
        progress.finish()
        dispatch({ type: "RESOLVE" })
        return { status: "success" }
      }
    } catch (error) {
      console.error(error)
      progress.finish()
      dispatch({ type: "REJECT" })
      return { status: "error" }
    }
  }

  const getError = (name) => get(errors, name)

  return {
    __configure,
    config,
    data,
    status,
    reset,
    errors,
    submit,
    setData,
    setValue,
    getValue,
    setTouched,
    getTouched,
    setError,
    getError,
  }
}

export default useForm
