import {cloneDeep, isEqual} from 'lodash'
import { reactive, watch } from 'vue'
import router from './router';

type FormDataType = object

interface FormProps<TForm extends FormDataType> {
  isDirty: boolean
  errors: Partial<Record<keyof TForm, string>>
  hasErrors: boolean
  processing: boolean
  wasSuccessful: boolean
  recentlySuccessful: boolean
  data(): TForm
  transform(callback: (data: TForm) => object): this
  defaults(): this
  defaults(field: keyof TForm, value: any): this
  defaults(fields: Partial<TForm>): this
  reset(...fields: (keyof TForm)[]): this
  clearErrors(...fields: (keyof TForm)[]): this
  setError(field: keyof TForm, value: string): this
  setError(errors: Record<keyof TForm, string>): this
  submit(method: string, url: string, options?: any): void
  get(url: string, options?: any): void
  post(url: string, options?: any): void
  put(url: string, options?: any): void
  patch(url: string, options?: any): void
  delete(url: string, options?: any): void
  cancel(): void
  __rememberable: boolean
  __remember(): { data: TForm; errors: Partial<Record<keyof TForm, string>> }
  __restore(restored: { data: TForm; errors: Partial<Record<keyof TForm, string>> }): void
}


export type Form<TForm extends FormDataType> = TForm & FormProps<TForm>

export default function useForm<TForm extends FormDataType>(data: TForm | (() => TForm)): Form<TForm>
export default function useForm<TForm extends FormDataType>(
  rememberKey: string,
  data: TForm | (() => TForm),
): Form<TForm>
export default function useForm<TForm extends FormDataType>(
  rememberKeyOrData: string | TForm | (() => TForm),
  maybeData?: TForm | (() => TForm),
): Form<TForm> {
  const rememberKey = typeof rememberKeyOrData === 'string' ? rememberKeyOrData : null
  const data = typeof rememberKeyOrData === 'string' ? maybeData : rememberKeyOrData
  let defaults = typeof data === 'object' ? cloneDeep(data) : cloneDeep(data())
  let recentlySuccessfulTimeoutId = null
  let transform = (data) => data

  const memory: Record<string, { data: TForm; errors: Partial<Record<keyof TForm, string>> }> = {}

  const form = reactive({
    ...(cloneDeep(defaults)),
    isDirty: false,
    errors: {},
    hasErrors: false,
    processing: false,
    wasSuccessful: false,
    recentlySuccessful: false,
    validated: false,
    data() {
      return (Object.keys(defaults) as Array<keyof TForm>).reduce((carry, key) => {
        carry[key] = this[key]
        return carry
      }, {} as Partial<TForm>) as TForm
    },
    transform(callback) {
      transform = callback
      return this
    },
    defaults(fieldOrFields?: keyof TForm | Partial<TForm>, maybeValue?: any) {
      if (typeof data === 'function') {
        throw new Error('You cannot call `defaults()` when using a function to define your form data.')
      }

      if (typeof fieldOrFields === 'undefined') {
        defaults = this.data()
      } else {
        defaults = Object.assign(
          {},
          cloneDeep(defaults),
          typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields,
        )
      }

      return this
    },
    reset(...fields) {
      const resolvedData = typeof data === 'object' ? cloneDeep(defaults) : cloneDeep(data())
      const clonedData = cloneDeep(resolvedData)
      if (fields.length === 0) {
        defaults = clonedData
        Object.assign(this, resolvedData)
      } else {
        Object.keys(resolvedData)
          .filter((key) => fields.includes(key))
          .forEach((key) => {
            defaults[key] = clonedData[key]
            this[key] = resolvedData[key]
          })
      }

      return this
    },
    setError(fieldOrFields: keyof TForm | Record<keyof TForm, string>, maybeValue?: string) {
      Object.assign(this.errors, typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields)

      this.hasErrors = Object.keys(this.errors).length > 0

      return this
    },
    clearErrors(...fields) {
      this.errors = Object.keys(this.errors).reduce(
        (carry, field) => ({
          ...carry,
          ...(fields.length > 0 && !fields.includes(field) ? { [field]: this.errors[field] } : {}),
        }),
        {},
      )

      this.hasErrors = Object.keys(this.errors).length > 0

      return this
    },
    submit(method, url, options = {}) {
      const data = transform(this.data())
      this.validated = true; 
      const _options = {
        ...options,
        onCancelToken: (token) => {
          cancelToken = token

          if (options.onCancelToken) {
            return options.onCancelToken(token)
          }
        },
        onBefore: (visit) => {
          this.wasSuccessful = false
          this.recentlySuccessful = false
          clearTimeout(recentlySuccessfulTimeoutId)

          if (options.onBefore) {
            return options.onBefore(visit)
          }
        },
        onStart: (visit) => {
          this.processing = true

          if (options.onStart) {
            return options.onStart(visit)
          }
        },
        onProgress: (event) => {
          this.progress = event

          if (options.onProgress) {
            return options.onProgress(event)
          }
        },
        onSuccess: async (page) => {
          this.processing = false
          this.progress = null
          this.clearErrors()
          this.wasSuccessful = true
          this.recentlySuccessful = true
          recentlySuccessfulTimeoutId = setTimeout(() => (this.recentlySuccessful = false), 2000)

          const onSuccess = options.onSuccess ? await options.onSuccess(page) : null
          defaults = cloneDeep(this.data())
          this.isDirty = false
          return onSuccess
        },
        onError: (errors) => {
          this.processing = false
          this.progress = null
          this.clearErrors().setError(errors.errors || errors)

          if (options.onError) {
            return options.onError(errors)
          }
        },
        onCancel: () => {
          this.processing = false
          this.progress = null

          if (options.onCancel) {
            return options.onCancel()
          }
        },
        onFinish: (visit) => {
          this.processing = false
          this.progress = null
          cancelToken = null

          if (options.onFinish) {
            return options.onFinish(visit)
          }
        },
      }

      if (method === 'delete') {
        router.delete(url, { ..._options, data })
      } else {
        router[method](url, data, _options)
      }
    },
    get(url, options) {
      this.submit('get', url, options)
    },
    post(url, options) {
      this.submit('post', url, options)
    },
    put(url, options) {
      this.submit('put', url, options)
    },
    patch(url, options) {
      this.submit('patch', url, options)
    },
    delete(url, options) {
      this.submit('delete', url, options)
    },
    cancel() {
      // Implement cancellation logic if necessary
    },
    __rememberable: rememberKey !== null,
    __remember() {
      return { data: this.data(), errors: this.errors }
    },
    __restore(restored: { data: TForm; errors: Partial<Record<keyof TForm, string>> }) {
      Object.assign(this, restored.data)
      this.setError(restored.errors)
    },
  })

  watch(
    form,
    (newValue) => {
      form.isDirty = !isEqual(form.data(), defaults)
      if (rememberKey) {
        memory[rememberKey] = { data: cloneDeep(newValue.__remember().data), errors: cloneDeep(newValue.errors) }
      }
    },
    { immediate: true, deep: true },
  )

  // Restore from memory if it exists
  if (rememberKey && memory[rememberKey]) {
    form.__restore(memory[rememberKey])
  }

  return form
}
