import { defineStore } from "pinia";
import { computed, del, ref, set } from "vue";

import {
    type CollectionEntry,
    type DCESummary,
    type HDCADetailed,
    type HDCASummary,
    type HistoryContentItemBase,
    isHDCA,
} from "@/api";
import { fetchCollectionDetails, fetchElementsFromCollection } from "@/api/datasetCollections";
import { ensureDefined } from "@/utils/assertions";
import { ActionSkippedError, LastQueue } from "@/utils/lastQueue";
import { isRetryableApiError, MAX_RETRIES } from "@/utils/simple-error";

/**
 * Represents an element in a collection that has not been fetched yet.
 */
export interface ContentPlaceholder {
    /**
     * The index of the element in the containing collection.
     *
     * This is used to determine the offset to fetch the element from when
     * scrolling through the collection.
     */
    element_index: number;
    /**
     * Whether the element is currently being fetched.
     *
     * This is used to prevent fetching the same element multiple times.
     */
    fetching?: boolean;
}

export type InvalidDCEEntry = (ContentPlaceholder | DCESummary) & {
    valid: false;
};

export type DCEEntry = ContentPlaceholder | DCESummary | InvalidDCEEntry;

const FETCH_LIMIT = 50;

export const useCollectionElementsStore = defineStore("collectionElementsStore", () => {
    const storedCollections = ref<{ [key: string]: HDCASummary }>({});
    const storedCollectionsDetailed = ref<{ [key: string]: HDCADetailed }>({});
    const loadingCollectionElements = ref<{ [key: string]: boolean }>({});
    const loadingCollectionElementsErrors = ref<{ [key: string]: Error }>({});
    const storedCollectionElements = ref<{ [key: string]: DCEEntry[] }>({});
    const retryCounts: { [key: string]: number } = {};

    /**
     * Returns a key that can be used to store or retrieve the elements of a collection in the store.
     *
     * It consistently returns a DatasetCollection ID for (top level) HDCAs or sub-collections.
     */
    function getCollectionKey(collection: CollectionEntry): string {
        if (isHDCA(collection)) {
            return collection.collection_id;
        }
        return collection.id;
    }

    const getCollectionElements = computed(() => {
        return (collection: CollectionEntry) => {
            return storedCollectionElements.value[getCollectionKey(collection)];
        };
    });

    const isLoadingCollectionElements = computed(() => {
        return (collection: CollectionEntry) => {
            return loadingCollectionElements.value[getCollectionKey(collection)] ?? false;
        };
    });

    const getLoadingCollectionElementsError = computed(() => {
        return (collection: CollectionEntry) => {
            return loadingCollectionElementsErrors.value[getCollectionKey(collection)] ?? false;
        };
    });

    type FetchParams = {
        storedElements: DCEEntry[];
        collection: CollectionEntry;
        offset: number;
        limit: number;
    };

    async function fetchMissing({ storedElements, collection, offset, limit = FETCH_LIMIT }: FetchParams) {
        const collectionKey = getCollectionKey(collection);

        try {
            if (collection.element_count !== null) {
                // We should fetch only missing (placeholder) elements from the range
                const firstMissingIndexInRange = storedElements
                    .slice(offset, offset + limit)
                    .findIndex((element) => (isPlaceholder(element) && !element.fetching) || isInvalid(element));

                if (firstMissingIndexInRange === -1) {
                    // All elements in the range are already stored or being fetched
                    return;
                }

                // Adjust the offset to the first missing element
                offset += firstMissingIndexInRange;
            } else {
                // Edge case where element_count is incorrect
                // TODO: remove me once element_count is reported reliably
                offset = 0;
            }

            set(loadingCollectionElements.value, collectionKey, true);
            // Mark all elements in the range as fetching
            storedElements
                .slice(offset, offset + limit)
                .forEach((element) => isPlaceholder(element) && (element.fetching = true));

            const fetchedElements = await fetchElementsFromCollection({
                entry: collection,
                offset: offset,
                limit: limit,
            });

            return { fetchedElements, elementOffset: offset };
        } catch (error) {
            set(loadingCollectionElementsErrors.value, collectionKey, error);
        } finally {
            del(loadingCollectionElements.value, collectionKey);
        }
    }

    const lastQueue = new LastQueue<typeof fetchMissing>(1000, true);

    async function fetchMissingElements(collection: CollectionEntry, offset: number, limit = FETCH_LIMIT) {
        const key = getCollectionKey(collection);
        let storedElements = storedCollectionElements.value[key];

        if (!storedElements) {
            storedElements = initWithPlaceholderElements(collection);
            set(storedCollectionElements.value, key, storedElements);
        }

        try {
            const data = await lastQueue.enqueue(fetchMissing, { storedElements, collection, offset, limit }, key);

            if (data) {
                const from = data.elementOffset;
                const to = from + data.fetchedElements.length;

                for (let index = from; index < to; index++) {
                    const element = ensureDefined(data.fetchedElements[index - from]);
                    set(storedElements, index, element);
                }

                set(storedCollectionElements.value, key, storedElements);
            }
        } catch (e) {
            if (!(e instanceof ActionSkippedError)) {
                throw e;
            }
        }
    }

    async function invalidateCollectionElements(collection: CollectionEntry) {
        const storedElements = storedCollectionElements.value[getCollectionKey(collection)] ?? [];
        storedElements.forEach((element) => {
            (element as InvalidDCEEntry).valid = false;
        });
    }

    function saveCollection(collection: HDCASummary | HDCADetailed) {
        set<HDCASummary>(storedCollections.value, collection.id, collection);
        if ("elements" in collection) {
            set<HDCADetailed>(storedCollectionsDetailed.value, collection.id, collection);
        }
    }

    function saveCollections(historyContentsPayload: HistoryContentItemBase[]) {
        const collectionsInHistory = historyContentsPayload.filter(
            (entry) => entry.history_content_type === "dataset_collection",
        ) as HDCASummary[];
        for (const collection of collectionsInHistory) {
            saveCollection(collection);
        }
    }

    /** Returns collection from storedCollections, will load collection if not in store */
    const getCollectionById = computed(() => {
        return (collectionId: string) => {
            if (!storedCollections.value[collectionId]) {
                const existingError = loadingCollectionElementsErrors.value[collectionId];
                const canRetry =
                    existingError &&
                    isRetryableApiError(existingError) &&
                    (retryCounts[collectionId] ?? 0) <= MAX_RETRIES;
                if (!existingError || canRetry) {
                    fetchCollection({ id: collectionId });
                }
            }
            return storedCollections.value[collectionId] ?? null;
        };
    });

    const getDetailedCollectionById = computed(() => {
        return (collectionId: string) => {
            if (!storedCollectionsDetailed.value[collectionId]) {
                const existingError = loadingCollectionElementsErrors.value[collectionId];
                const canRetry =
                    existingError &&
                    isRetryableApiError(existingError) &&
                    (retryCounts[collectionId] ?? 0) <= MAX_RETRIES;
                if (!existingError || canRetry) {
                    fetchCollection({ id: collectionId });
                }
            }
            return storedCollectionsDetailed.value[collectionId] ?? null;
        };
    });

    async function fetchCollection(params: { id: string }) {
        set(loadingCollectionElements.value, params.id, true);
        try {
            const result = await fetchCollectionDetails({ hdca_id: params.id });
            if (result.error) {
                retryCounts[params.id] = (retryCounts[params.id] ?? 0) + 1;
                set(loadingCollectionElementsErrors.value, params.id, result.error);
            } else {
                saveCollection(result.data);
                del(loadingCollectionElementsErrors.value, params.id);
                delete retryCounts[params.id];
            }
        } finally {
            del(loadingCollectionElements.value, params.id);
        }
    }

    function isPlaceholder(element: DCEEntry): element is ContentPlaceholder {
        return "id" in element === false;
    }

    function isInvalid(element: DCEEntry): element is InvalidDCEEntry {
        return (element as InvalidDCEEntry)["valid"] === false;
    }

    function initWithPlaceholderElements(collection: CollectionEntry): ContentPlaceholder[] {
        const totalElements = collection.element_count ?? 0;
        const placeholderElements = new Array<ContentPlaceholder>(totalElements);
        for (let i = 0; i < totalElements; i++) {
            placeholderElements[i] = { element_index: i };
        }
        return placeholderElements;
    }

    return {
        storedCollections,
        storedCollectionElements,
        getCollectionElements,
        isLoadingCollectionElements,
        getLoadingCollectionElementsError,
        loadingCollectionElementsErrors,
        getCollectionById,
        getDetailedCollectionById,
        fetchCollection,
        invalidateCollectionElements,
        saveCollections,
        getCollectionKey,
        fetchMissingElements,
    };
});
