<template>
  <form @submit.prevent="onSubmit">
    <div class="flex flex-col space-y-5">
      <slot />
    </div>
  </form>
</template>

<script setup lang="ts">
import { useDebounceFn } from '@vueuse/shared';
import get from 'lodash.get';
import { provide, ref, watch } from 'vue';
import deepUnref from '../../utils/deep-unref';
import { FormData, FormErrors, FormSubmit } from './form.types';
import { FormValidator } from './validator.types';

const props = withDefaults(
  defineProps<{
    modelValue?: FormData;
    errors?: FormErrors;
    validators?: FormValidator[];
  }>(),
  {
    modelValue: () => ({}),
    errors: () => ({}),
    validators: () => [],
  }
);

const isValid = ref(true);
const invalidFields = ref<string[]>([]);
const formData = ref<FormData>(props.modelValue);
const formErrors = ref<FormErrors>(props.errors);
const validateField = async (field: string) => {
  let valid = true;
  if (formErrors.value[field]) {
    delete formErrors.value[field];
    if (invalidFields.value.includes(field)) {
      invalidFields.value = invalidFields.value.filter((x) => x !== field);
    }
  }

  for (const validator of props.validators) {
    if (validator.field !== field) continue;
    for (const fn of validator.validators) {
      const result = await fn(
        field,
        get(formData.value, field),
        formData.value
      );
      if (!result.isValid) {
        formErrors.value = {
          ...formErrors.value,
          [field]: [...(formErrors.value[field] || []), result.message],
        };
        invalidFields.value.push(field);
        valid = false;
      }
    }
  }

  if (!valid && isValid.value) {
    isValid.value = false;
    return;
  }

  isValid.value = !invalidFields.value.length;
};

const validateAll = async () => {
  const fields = props.validators
    .map((validator) => validator.field)
    .filter((value, index, array) => array.indexOf(value) === index);
  if (!fields.length) return;
  return Promise.allSettled(fields.map((field) => validateField(field)));
};

const fieldChange = useDebounceFn(validateField, 300);

provide('formData', formData);
provide('formErrors', formErrors);
provide('fieldChange', fieldChange);

const emit = defineEmits<{
  (e: 'submit', data: FormSubmit): void;
  (e: 'update:modelValue', data: FormData): void;
  (e: 'update:errors', errors: FormErrors): void;
  (e: 'update:validity', validity: boolean): void;
}>();

watch(formData, (val) => emit('update:modelValue', val), { deep: true });
watch(formErrors, (val) => emit('update:errors', val));
watch(isValid, (val) => emit('update:validity', val));

const onSubmit = async () => {
  await validateAll();
  emit('submit', {
    isValid: isValid.value,
    formData: deepUnref(formData.value),
    fieldErrors: deepUnref(formErrors.value),
  });
};

defineExpose({
  isValid,
});
</script>
