import { useLocalStorage, useOnline } from "@vueuse/core";
import { useUserApi } from "~/composables/api";
import type { ShoppingListItemOut, ShoppingListOut } from "~/lib/api/types/household";
import type { RequestResponse } from "~/lib/api/types/non-generated";

const localStorageKey = "shopping-list-queue";
const queueTimeout = 5 * 60 * 1000; // 5 minutes

type ItemQueueType = "create" | "update" | "delete";

interface ShoppingListQueue {
  create: ShoppingListItemOut[];
  update: ShoppingListItemOut[];
  delete: ShoppingListItemOut[];

  lastUpdate: number;
}

interface Storage {
  [key: string]: ShoppingListQueue;
}

export function useShoppingListItemActions(shoppingListId: string) {
  const isOnline = useOnline();
  const api = useUserApi();
  const storage = useLocalStorage(localStorageKey, {} as Storage, { deep: true });
  const queue = reactive(getQueue());
  const queueEmpty = computed(() => !queue.create.length && !queue.update.length && !queue.delete.length);
  if (queueEmpty.value) {
    queue.lastUpdate = Date.now();
  }

  storage.value[shoppingListId] = { ...queue };
  watch(
    () => queue,
    (value) => {
      storage.value[shoppingListId] = { ...value };
    },
    {
      deep: true,
      immediate: true,
    },
  );

  function isValidQueueObject(obj: any): obj is ShoppingListQueue {
    if (typeof obj !== "object" || obj === null) {
      return false;
    }

    const hasRequiredProps = "create" in obj && "update" in obj && "delete" in obj && "lastUpdate" in obj;
    if (!hasRequiredProps) {
      return false;
    }

    const arraysValid = Array.isArray(obj.create) && Array.isArray(obj.update) && Array.isArray(obj.delete);

    const lastUpdateValid = typeof obj.lastUpdate === "number" && !isNaN(new Date(obj.lastUpdate).getTime());

    return arraysValid && lastUpdateValid;
  }

  function createEmptyQueue(): ShoppingListQueue {
    const newQueue = { create: [], update: [], delete: [], lastUpdate: Date.now() };
    return newQueue;
  }

  function getQueue(): ShoppingListQueue {
    try {
      const fetchedQueue = storage.value[shoppingListId];
      if (!isValidQueueObject(fetchedQueue)) {
        console.log("Invalid queue object in local storage; resetting queue.");
        return createEmptyQueue();
      }
      else {
        return fetchedQueue;
      }
    }
    catch (error) {
      console.log("Error validating queue object in local storage; resetting queue.", error);
      return createEmptyQueue();
    }
  }

  function removeFromQueue(itemQueue: ShoppingListItemOut[], item: ShoppingListItemOut): boolean {
    const index = itemQueue.findIndex(i => i.id === item.id);
    if (index === -1) {
      return false;
    }

    itemQueue.splice(index, 1);
    return true;
  }

  function mergeListItemsByLatest(
    list1: ShoppingListItemOut[],
    list2: ShoppingListItemOut[],
  ) {
    const mergedList = [...list1];
    list2.forEach((list2Item) => {
      const conflictingItem = mergedList.find(item => item.id === list2Item.id);
      if (conflictingItem
        && list2Item.updatedAt && conflictingItem.updatedAt
        && list2Item.updatedAt > conflictingItem.updatedAt) {
        mergedList.splice(mergedList.indexOf(conflictingItem), 1, list2Item);
      }
      else if (!conflictingItem) {
        mergedList.push(list2Item);
      }
    });
    return mergedList;
  }

  async function getList() {
    const response = await api.shopping.lists.getOne(shoppingListId);
    if (response.data) {
      // Merge pending local changes (both online and offline)
      const createAndUpdateQueues = mergeListItemsByLatest(queue.update, queue.create);
      const deleteQueueIds = new Set(queue.delete.map(item => item.id));

      const filteredLocalChanges = createAndUpdateQueues.filter(item => !deleteQueueIds.has(item.id));
      let mergedItems = mergeListItemsByLatest(response.data.listItems ?? [], filteredLocalChanges);
      mergedItems = mergedItems.filter(item => !deleteQueueIds.has(item.id));

      response.data.listItems = mergedItems;
    }
    return response.data;
  }

  function createItem(item: ShoppingListItemOut) {
    removeFromQueue(queue.create, item);
    removeFromQueue(queue.update, item);
    removeFromQueue(queue.delete, item);

    queue.create.push(item);
  }

  function updateItem(item: ShoppingListItemOut) {
    const removedFromCreate = removeFromQueue(queue.create, item);
    removeFromQueue(queue.update, item);
    removeFromQueue(queue.delete, item);

    if (removedFromCreate) {
      // This item hasn't been created yet, so keep it in create queue with updated data
      queue.create.push(item);
    }
    else {
      queue.update.push(item);
    }
  }

  function deleteItem(item: ShoppingListItemOut) {
    const removedFromCreate = removeFromQueue(queue.create, item);
    if (removedFromCreate) {
      // This item hasn't been created yet, so we don't need to delete it
      return;
    }

    removeFromQueue(queue.update, item);
    removeFromQueue(queue.delete, item);
    queue.delete.push(item);
  }

  function getQueueItems(itemQueueType: ItemQueueType) {
    return queue[itemQueueType];
  }

  function clearQueueItems(itemQueueType: ItemQueueType | "all", itemIds: string[] | null = null) {
    if (itemQueueType === "create" || itemQueueType === "all") {
      queue.create = itemIds ? queue.create.filter(item => !itemIds.includes(item.id)) : [];
    }
    if (itemQueueType === "update" || itemQueueType === "all") {
      queue.update = itemIds ? queue.update.filter(item => !itemIds.includes(item.id)) : [];
    }
    if (itemQueueType === "delete" || itemQueueType === "all") {
      queue.delete = itemIds ? queue.delete.filter(item => !itemIds.includes(item.id)) : [];
    }
    if (queueEmpty.value) {
      queue.lastUpdate = Date.now();
    }
  }

  function checkUpdateState(list: ShoppingListOut) {
    const cutoffDate = new Date(queue.lastUpdate + queueTimeout).toISOString();
    if (list.updatedAt && list.updatedAt > cutoffDate) {
      // If the queue is too far behind the shopping list to reliably do updates, we clear the queue
      console.log("Out of sync with server; clearing queue");
      clearQueueItems("all");
    }
  }

  /**
   * Processes the queue items and returns whether the processing was successful.
   */
  async function processQueueItems(
    action: (items: ShoppingListItemOut[]) => Promise<RequestResponse<any>>,
    itemQueueType: ItemQueueType,
  ): Promise<boolean> {
    let queueItems: ShoppingListItemOut[];
    try {
      queueItems = getQueueItems(itemQueueType);
      if (!queueItems.length) {
        return true;
      }
    }
    catch (error) {
      console.log(`Error fetching queue items of type ${itemQueueType}:`, error);
      clearQueueItems(itemQueueType);
      return false;
    }

    try {
      const itemsToProcess = [...queueItems];
      const itemIdsToProcess = itemsToProcess.map(item => item.id);

      await action(itemsToProcess)
        .then(() => {
          if (isOnline.value) {
            clearQueueItems(itemQueueType, itemIdsToProcess);
          }
        });
    }
    catch (error) {
      console.log(`Error processing queue items of type ${itemQueueType}:`, error);
      clearQueueItems(itemQueueType);
      return false;
    }

    return true;
  }

  async function process() {
    if (queueEmpty.value) {
      queue.lastUpdate = Date.now();
      return;
    }

    const data = await getList();
    if (!data) {
      return;
    }
    checkUpdateState(data);

    // We send each bulk request one at a time, since the backend may merge items
    // "failures" here refers to an actual error, rather than failing to reach the backend
    let failures = 0;
    if (!(await processQueueItems(items => api.shopping.items.deleteMany(items), "delete"))) failures++;
    if (!(await processQueueItems(items => api.shopping.items.updateMany(items), "update"))) failures++;
    if (!(await processQueueItems(items => api.shopping.items.createMany(items), "create"))) failures++;

    // If we're online, or the queue is empty, the queue is fully processed, so we're up to date
    // Otherwise, if all three queue processes failed, we've already reset the queue, so we need to reset the date
    if (isOnline.value || queueEmpty.value || failures === 3) {
      queue.lastUpdate = Date.now();
    }
  }

  return {
    getList,
    createItem,
    updateItem,
    deleteItem,
    process,
  };
}
