import { reactive, computed, toRaw, isReactive, watch } from "vue";
import isEqual from "fast-deep-equal";

export const useFormUpdateTracker = (initialFormValue) => {
  const form = reactive(deepClone(initialFormValue));
  let originalForm = deepClone(initialFormValue);
  const updatedFields = reactive({});

  const getUpdatedFields = computed(() => {
    return updatedFields;
  });

  const hasUpdatedFields = computed(() => {
    return Object.keys(updatedFields).length > 0;
  });

  watch(
    form,
    (newValue) => {
      const changes = diff(toRaw(newValue), originalForm);
      Object.keys(updatedFields).forEach((key) => delete updatedFields[key]);
      Object.assign(updatedFields, changes);
    },
    { deep: true },
  );

  const resetUpdateForm = () => {
    const newFormState = deepClone(originalForm);
    Object.keys(form).forEach((key) => delete form[key]);
    Object.assign(form, newFormState);
  };

  const setUpdateFormAsOriginal = () => {
    originalForm = deepClone(toRaw(form));
  };

  const getFormAsFormData = () => {
    const formData = new FormData();
    buildFormData(formData, form);
    return formData;
  };

  const getUpdatedFieldsAsFormData = (spoofMethod = null) => {
    const formData = new FormData();
    buildFormData(formData, updatedFields);
    if (spoofMethod) {
      formData.append("_method", spoofMethod.toUpperCase());
    }

    return formData;
  };

  return {
    form,
    getUpdatedFields,
    hasUpdatedFields,
    resetUpdateForm,
    setUpdateFormAsOriginal,
    getFormAsFormData,
    getUpdatedFieldsAsFormData,
  };
};

/**
 * A recursive function to find the differences between two objects.
 * It returns a new object containing only the keys that have been added,
 * changed, or removed (set to null).
 * @param {object} currentObj The current state of the object.
 * @param {object} baseObj The original object to compare against.
 * @returns {object} An object with only the changed, new, or deleted keys.
 */
function diff(currentObj, baseObj) {
  const changes = {};
  const currentKeys = Object.keys(currentObj);
  const baseKeys = Object.keys(baseObj);

  for (const key of currentKeys) {
    const currentValue = currentObj[key];
    const baseValue = baseObj[key];

    if (!isEqual(currentValue, baseValue)) {
      if (
        currentValue &&
        typeof currentValue === "object" &&
        !Array.isArray(currentValue) &&
        baseValue &&
        typeof baseValue === "object" &&
        !Array.isArray(baseValue)
      ) {
        const nestedChanges = diff(currentValue, baseValue);
        if (Object.keys(nestedChanges).length > 0) {
          changes[key] = nestedChanges;
        }
      } else {
        changes[key] = deepClone(currentValue);
      }
    }
  }

  for (const key of baseKeys) {
    if (!Object.prototype.hasOwnProperty.call(currentObj, key)) {
      changes[key] = null;
    }
  }

  return changes;
}

/**
 * Recursively builds a FormData object from a nested plain object using bracket notation
 * for keys, which is compatible with PHP/Laravel backends.
 * @param {FormData} formData The FormData instance.
 * @param {object} data The plain object to serialize.
 * @param {string} parentKey The base key for nested properties.
 */
function buildFormData(formData, data, parentKey = "") {
  if (data === null || data === undefined) {
    return;
  }

  if (Array.isArray(data)) {
    data.forEach((value, index) => {
      buildFormData(formData, value, `${parentKey}[${index}]`);
    });
    return;
  }

  if (
    typeof data === "object" &&
    !(data instanceof File) &&
    !(data instanceof Date)
  ) {
    Object.keys(data).forEach((key) => {
      const propName = parentKey ? `${parentKey}[${key}]` : key;
      buildFormData(formData, data[key], propName);
    });
    return;
  }

  let valueToAppend = data;
  if (data instanceof Date) {
    valueToAppend = data.toISOString().slice(0, 19).replace("T", " ");
  }

  formData.append(parentKey, valueToAppend);
}

/** * Deep clones an object using structuredClone if available,
 * otherwise falls back to JSON methods.
 * If the object is reactive, it converts it to a raw object first.
 * @param {object} obj The object to clone.
 * @returns {object} A deep clone of the input object.
 */
function deepClone(obj) {
  if (obj && isReactive(obj)) {
    obj = toRaw(obj);
  }
  if (typeof structuredClone === "function") {
    try {
      return structuredClone(obj);
    } catch (e) {
      console.warn(
        "structuredClone not supported, using JSON methods instead: " + e,
      );
    }
  }
  return JSON.parse(JSON.stringify(obj));
}
