import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { fetchItems, fetchItemStubs } from "../services/contentService";
import { getItemDescriptor } from "../utils";
import { convertFullItemToStub } from "../utils/itemConverters";
export function shouldMarkFieldSaved(modifiedField, value, saveSequence) {
    if (!modifiedField.isDirty)
        return false;
    const sequenceMatches = modifiedField.saveSequence === saveSequence;
    const valueMatches = modifiedField.value === value;
    const noNewerEditPending = modifiedField.saveSequence <= saveSequence;
    return sequenceMatches || (valueMatches && noNewerEditPending);
}
export function markFieldSavedInStore(fieldModificationStore, field, value, saveSequence) {
    const modifiedField = fieldModificationStore?.getField(field.fieldId, field.item.id, field.item.language, field.item.version);
    if (!fieldModificationStore || !modifiedField || !modifiedField.isDirty) {
        return;
    }
    if (shouldMarkFieldSaved(modifiedField, value, saveSequence)) {
        fieldModificationStore.updateField({ ...modifiedField, isDirty: false });
    }
}
// Standard Sitecore template IDs for template-related items
const TEMPLATE_TEMPLATE_ID = "AB86861A-6030-46C5-B394-E8F99E8B87DB"; // Template template
const TEMPLATE_FIELD_TEMPLATE_ID = "455A3E98-A627-4B40-8035-E683A0331AC7"; // Template field template
const TEMPLATE_SECTION_TEMPLATE_ID = "E269FBB5-3750-427A-9149-7AA950B49301"; // Template section template
/**
 * Checks if an item change represents a template-related modification that should trigger cache clearing
 */
const isTemplateChange = async (change) => {
    try {
        // Check if the item is under the /sitecore/templates path
        if (change.item?.path &&
            change.item.path.toLowerCase().startsWith("/sitecore/templates")) {
            return true;
        }
        return false;
    }
    catch (error) {
        console.error("Error checking if item change is template-related:", error);
        return false;
    }
};
export function useItemsRepository(addRecentEdit, pageItem, getFieldModificationStores) {
    const itemsMap = useRef(new Map());
    const stubsMap = useRef(new Map());
    const fallbackDependencyMap = useRef(new Map());
    const pendingRequests = useRef(new Map());
    const pendingStubsRequests = useRef(new Map());
    const saveSequenceCounter = useRef(0);
    const pageItemRef = useRef(pageItem);
    // Update pageItem ref when it changes
    useEffect(() => {
        pageItemRef.current = pageItem;
    }, [pageItem]);
    const [revision, setRevision] = useState(0);
    const itemsChangedListeners = useRef(new Set());
    const fetchItemsFromServer = useCallback(async (itemsToLoad) => {
        // Group items by their keys to deduplicate requests
        const requestGroups = new Map();
        itemsToLoad.forEach((item) => {
            const key = generateKey(item);
            if (!requestGroups.has(key)) {
                // If item is already an ItemDescriptor, use it directly to preserve database property
                // Otherwise, use getItemDescriptor to extract from FullItem
                const descriptor = "descriptor" in item ? getItemDescriptor(item) : item;
                requestGroups.set(key, descriptor);
            }
        });
        const relevantPendingPromises = Array.from(new Set(Array.from(requestGroups.keys())
            .map((key) => pendingRequests.current.get(key))
            .filter((promise) => !!promise)));
        // Create a single promise for all non-pending items
        const itemsToFetch = Array.from(requestGroups.entries())
            .filter(([key]) => !pendingRequests.current.has(key))
            .map(([_, item]) => item);
        if (itemsToFetch.length === 0) {
            await Promise.all(relevantPendingPromises);
            return;
        }
        const fetchPromise = (async () => {
            try {
                const fetchedItems = await fetchItems(itemsToFetch, pageItemRef.current);
                if (!fetchedItems) {
                    return;
                }
                fetchedItems.forEach((item, index) => {
                    if (!item)
                        return;
                    const descriptor = getItemDescriptor(item);
                    const itemToStore = {
                        ...item,
                        descriptor,
                    };
                    const primaryKey = generateKey(item);
                    itemsMap.current.set(primaryKey, itemToStore);
                    // Only store version 0 if the current item isn't already version 0
                    if (itemsToFetch[index]?.version === 0 && item.version !== 0) {
                        const versionZeroKey = generateKey(itemsToFetch[index]);
                        itemsMap.current.set(versionZeroKey, itemToStore);
                    }
                    if (item.fields) {
                        item.fields.forEach((field) => {
                            if (field.fallbackChain) {
                                const descriptor = getItemDescriptor(item);
                                field.fallbackChain.forEach((fallback) => {
                                    const prev = fallbackDependencyMap.current.get(getItemIdLanguageKey(fallback.item));
                                    if (!prev?.find((x) => x.id === descriptor.id &&
                                        x.language === descriptor.language &&
                                        x.version === descriptor.version)) {
                                        fallbackDependencyMap.current.set(getItemIdLanguageKey(fallback.item), [...(prev || []), descriptor]);
                                    }
                                });
                            }
                        });
                    }
                });
            }
            catch (error) {
                console.error("Error fetching items:", error);
                throw error;
            }
        })();
        // Store the promise for each item being fetched
        itemsToFetch.forEach((item) => {
            const key = generateKey(item);
            pendingRequests.current.set(key, fetchPromise);
        });
        try {
            await Promise.all([...relevantPendingPromises, fetchPromise]);
        }
        finally {
            // Clean up pending requests
            itemsToFetch.forEach((item) => {
                pendingRequests.current.delete(generateKey(item));
            });
        }
    }, []);
    const getItems = useCallback(async (descriptors) => {
        const keys = descriptors.map((x) => ({
            key: generateKey(x),
            descriptor: x,
        }));
        // Cache version 0 items normally - the cache is invalidated via websocket
        // when a new version is added (invalidateVersionZeroCache)
        const itemsMissing = keys.filter((entry) => !itemsMap.current.has(entry.key));
        const map = itemsMap;
        if (itemsMissing.length > 0) {
            await fetchItemsFromServer(itemsMissing.map((x) => x.descriptor));
        }
        return descriptors
            .map((x) => map.current.get(generateKey(x)))
            .filter((x) => x !== undefined);
    }, [fetchItemsFromServer]);
    const fetchStubsFromServer = useCallback(async (itemsToLoad) => {
        // Group items by their keys to deduplicate requests
        const requestGroups = new Map();
        itemsToLoad.forEach((item) => {
            const key = generateKey(item);
            if (!requestGroups.has(key)) {
                requestGroups.set(key, item);
            }
        });
        const relevantPendingPromises = Array.from(new Set(Array.from(requestGroups.keys())
            .map((key) => pendingStubsRequests.current.get(key))
            .filter((promise) => !!promise)));
        // Create a single promise for all non-pending items
        const itemsToFetch = Array.from(requestGroups.entries())
            .filter(([key]) => !pendingStubsRequests.current.has(key))
            .map(([_, item]) => item);
        if (itemsToFetch.length === 0) {
            await Promise.all(relevantPendingPromises);
            return;
        }
        const fetchPromise = (async () => {
            try {
                const fetchedStubs = await fetchItemStubs(itemsToFetch, pageItemRef.current);
                if (!fetchedStubs)
                    return;
                fetchedStubs.forEach((stub, index) => {
                    if (!stub)
                        return;
                    const primaryKey = generateKey(stub);
                    stubsMap.current.set(primaryKey, stub);
                    // Only store version 0 if the current stub isn't already version 0
                    if (itemsToFetch[index]?.version === 0 && stub.version !== 0) {
                        const versionZeroKey = generateKey(itemsToFetch[index]);
                        stubsMap.current.set(versionZeroKey, stub);
                    }
                });
            }
            catch (error) {
                console.error("Error fetching stubs:", error);
                throw error;
            }
        })();
        // Store the promise for each item being fetched
        itemsToFetch.forEach((item) => {
            const key = generateKey(item);
            pendingStubsRequests.current.set(key, fetchPromise);
        });
        try {
            await Promise.all([...relevantPendingPromises, fetchPromise]);
        }
        finally {
            // Clean up pending requests
            itemsToFetch.forEach((item) => {
                pendingStubsRequests.current.delete(generateKey(item));
            });
        }
    }, []);
    const getItemsStubs = useCallback(async (descriptors, pageItem) => {
        const keys = descriptors.map((x) => ({
            key: generateKey(x),
            descriptor: x,
        }));
        // Update pageItem ref if provided (for authorization context)
        if (pageItem) {
            pageItemRef.current = pageItem;
        }
        // First try to get stubs from full items if available
        // Cache version 0 items normally - the cache is invalidated via websocket
        // when a new version is added (invalidateVersionZeroCache)
        const result = [];
        const itemsToFetch = [];
        for (const { key, descriptor } of keys) {
            // If we have the full item, convert to stub
            const fullItem = itemsMap.current.get(key);
            if (fullItem) {
                result.push(convertFullItemToStub(fullItem));
                continue;
            }
            // If we have a cached stub, use it
            const cachedStub = stubsMap.current.get(key);
            if (cachedStub) {
                result.push(cachedStub);
                continue;
            }
            // Otherwise, fetch it
            itemsToFetch.push(descriptor);
        }
        if (itemsToFetch.length > 0) {
            await fetchStubsFromServer(itemsToFetch);
            // Add fetched stubs to the result
            for (const descriptor of itemsToFetch) {
                const key = generateKey(descriptor);
                const stub = stubsMap.current.get(key);
                if (stub) {
                    result.push(stub);
                }
            }
        }
        return result;
    }, [fetchStubsFromServer]);
    const getItem = useCallback(async (descriptor) => {
        const items = await getItems([descriptor]);
        const result = items[0];
        return result;
    }, [getItems]);
    const getField = useCallback(async (fieldDescriptor) => {
        try {
            const item = await getItem(fieldDescriptor.item);
            return item?.fields.find((x) => x.id === fieldDescriptor.fieldId);
        }
        catch (error) {
            console.error("Error getting field:", error);
            return undefined;
        }
    }, [getItem]);
    const updateFieldValue = useCallback(async (field, user, isDirty, value, rawValue, fieldModificationStore) => {
        const item = await getItem(field.item);
        if (!item)
            return 0;
        const fieldToUpdate = item.fields.find((x) => x.id === field.fieldId);
        if (!fieldToUpdate)
            return 0;
        const updatedFieldValue = {
            ...fieldToUpdate,
            rawValue: rawValue !== undefined ? rawValue : String(value),
            value,
        };
        // Update the field value in the item copy
        const updatedItem = {
            ...item,
            fields: item.fields.map((f) => f.id === field.fieldId ? updatedFieldValue : f),
        };
        itemsMap.current.set(generateKey(field.item), updatedItem);
        const val = rawValue !== undefined ? rawValue : value;
        const currentSaveSequence = ++saveSequenceCounter.current;
        const updatedField = {
            ...field,
            value: val,
            isDirty: isDirty,
            modifiedBy: user,
            timestamp: Date.now(),
            saveSequence: currentSaveSequence,
        };
        fieldModificationStore?.updateField(updatedField);
        if (user.ai || user.agentId) {
            addRecentEdit({ ...field, timestamp: Date.now(), user });
        }
        triggerItemsChangedEvent([
            {
                item: field.item,
                action: "update",
                source: "local-field-update",
                changes: {
                    fields: [field.fieldId],
                    fieldValues: [updatedFieldValue],
                },
            },
        ]);
        return currentSaveSequence;
    }, [getItem, addRecentEdit]);
    const refreshItems = useCallback(async (items, changes) => {
        // If only children changed and no properties changed, avoid fetching items.
        // Still notify listeners so UIs like ContentTree can reload children lists.
        const onlyChildrenChanged = changes?.children === true && changes?.fields === undefined;
        if (onlyChildrenChanged) {
            triggerItemsChangedEvent(items.map((x) => ({
                item: x,
                action: "update",
                changes: changes ?? {},
            })));
            return;
        }
        // Filter to only include items that are already cached
        const itemsToFetch = items.filter((item) => itemsMap.current.has(generateKey(item)));
        // Don't add version 0 to fetch list - it's redundant since fetching specific version
        // gives us the same data. Instead, invalidate version 0 cache so it gets re-fetched when needed.
        // Also clear stub cache for these items.
        items.forEach((item) => {
            stubsMap.current.delete(generateKey(item));
            // Also clear version 0 stubs and items cache
            stubsMap.current.delete(generateKey({ ...item, version: 0 }));
            if (item.version !== 0) {
                itemsMap.current.delete(generateKey({ ...item, version: 0 }));
            }
        });
        if (itemsToFetch.length !== 0) {
            const time = Date.now();
            const startSequence = saveSequenceCounter.current;
            await fetchItemsFromServer(itemsToFetch);
            const getFieldValue = (ItemDescriptor, fieldId) => {
                const item = itemsMap.current.get(generateKey(ItemDescriptor));
                if (!item)
                    return null;
                const field = item.fields?.find((f) => f.id === fieldId);
                if (!field)
                    return null;
                return field.rawValue ?? null;
            };
            const updateModifiedFields = (prevModifiedFields) => {
                return prevModifiedFields.map((modifiedField) => {
                    // Only consider fields for items we fetched
                    const wasFetched = itemsToFetch.find((fetched) => fetched.id === modifiedField.item.id &&
                        fetched.language === modifiedField.item.language &&
                        fetched.version === modifiedField.item.version);
                    if (!wasFetched)
                        return modifiedField;
                    const serverValue = getFieldValue(modifiedField.item, modifiedField.fieldId);
                    // Avoid clobbering in-flight edits: only sync to server value if not dirty
                    if (isOutdated(modifiedField) && !modifiedField.isDirty) {
                        return { ...modifiedField, value: serverValue };
                    }
                    // If the server value matches our modified value, the field is effectively saved.
                    // This can happen when the save acknowledgment arrived out-of-order or was missed.
                    // In that case, clear the dirty flag so UI exits the "saving" state.
                    if (serverValue === modifiedField.value && modifiedField.isDirty) {
                        return { ...modifiedField, isDirty: false };
                    }
                    return modifiedField;
                });
            };
            const isOutdated = (x) => {
                if (x.saveSequence > startSequence)
                    return false;
                if (x.timestamp > time)
                    return false;
                const item = itemsMap.current.get(generateKey(x.item));
                if (!item)
                    return true;
                const field = item.fields?.find((f) => f.id === x.fieldId);
                if (!field)
                    return true;
                return field.rawValue !== x.value;
            };
            getFieldModificationStores?.().forEach((fieldModificationStore) => {
                const prevFields = fieldModificationStore.getAllFields();
                const newFields = updateModifiedFields(prevFields);
                fieldModificationStore.updateFields(newFields);
            });
            setRevision((prev) => prev + 1);
        }
        triggerItemsChangedEvent(items.map((x) => ({
            item: x,
            action: "update",
            changes: changes ?? {},
        })));
    }, [setRevision, fetchItemsFromServer, getFieldModificationStores]);
    const onFieldSaved = useCallback((field, value, saveSequence, fieldModificationStore) => {
        markFieldSavedInStore(fieldModificationStore, field, value, saveSequence);
        // Refresh any items that depend on this field via fallback
        const dependencies = fallbackDependencyMap.current.get(getItemIdLanguageKey(field.item));
        if (dependencies) {
            refreshItems(dependencies);
        }
        // Notify listeners about the field change
        triggerItemsChangedEvent([
            {
                item: field.item,
                action: "update",
                changes: { fields: [field.fieldId] },
            },
        ]);
    }, [refreshItems]);
    const onItemsDeleted = useCallback((items) => {
        items.forEach((item) => {
            const key = generateKey(item.item);
            itemsMap.current.delete(key);
            stubsMap.current.delete(key);
        });
        triggerItemsChangedEvent(items.map((x) => ({
            item: x.item,
            action: "delete",
            parentId: x.parentId,
            changes: {},
        })));
        setRevision((prev) => prev + 1);
    }, [setRevision]);
    const clear = useCallback(() => {
        itemsMap.current.clear();
        stubsMap.current.clear();
        fallbackDependencyMap.current.clear();
        //setRevision((x) => x + 1);
    }, []);
    // Clear the dirty state for a field (used for error recovery)
    // This prevents fields from getting stuck in "saving" state when an error occurs
    const clearDirtyState = useCallback((field, fieldModificationStore) => {
        fieldModificationStore?.clearDirtyState(field.fieldId, field.item.id, field.item.language, field.item.version);
    }, []);
    // Invalidate version 0 cache entries for a specific item
    // Called when a new version is added via websocket to ensure "latest" resolves correctly
    const invalidateVersionZeroCache = useCallback((itemId, language) => {
        // Generate keys for version 0 with both master and web databases
        const masterKey = generateKey({
            id: itemId,
            language,
            version: 0,
            database: "master",
        });
        const webKey = generateKey({
            id: itemId,
            language,
            version: 0,
            database: "web",
        });
        const defaultKey = generateKey({ id: itemId, language, version: 0 });
        // Remove from items cache
        itemsMap.current.delete(masterKey);
        itemsMap.current.delete(webKey);
        itemsMap.current.delete(defaultKey);
        // Remove from stubs cache
        stubsMap.current.delete(masterKey);
        stubsMap.current.delete(webKey);
        stubsMap.current.delete(defaultKey);
        // Increment revision to notify subscribers
        setRevision((prev) => prev + 1);
    }, [setRevision]);
    const subscribeItemsChanged = useCallback((callback) => {
        const wrappedCallback = async (changes) => {
            if (changes.every((change) => change.source === "local-field-update")) {
                callback(changes);
                return;
            }
            // Check if any of the changes are template-related
            const templateChanges = await Promise.all(changes.map((change) => isTemplateChange(change)));
            // If any template changes are detected, clear the cache
            if (templateChanges.some((isTemplate) => isTemplate)) {
                console.log("Template changes detected, clearing cache...");
                clear();
                setRevision((prev) => prev + 1);
            }
            // Call the original callback
            callback(changes);
        };
        itemsChangedListeners.current.add(wrappedCallback);
        // Return an unsubscribe function
        return () => {
            itemsChangedListeners.current.delete(wrappedCallback);
        };
    }, [clear]);
    const triggerItemsChangedEvent = (changes) => {
        const listeners = itemsChangedListeners.current;
        if (listeners) {
            listeners.forEach((callback) => callback(changes));
        }
    };
    const repo = useMemo(() => {
        return {
            getItems,
            getItemsStubs,
            getItem,
            getField,
            updateFieldValue,
            //     addToModifiedFields,
            refreshItems,
            revision,
            onFieldSaved,
            onItemsDeleted,
            invalidateVersionZeroCache,
            subscribeItemsChanged,
            clear,
            clearDirtyState,
        };
    }, [
        getItems,
        getItemsStubs,
        getItem,
        getField,
        revision,
        updateFieldValue,
        refreshItems,
        onFieldSaved,
        onItemsDeleted,
        invalidateVersionZeroCache,
        subscribeItemsChanged,
        clear,
        clearDirtyState,
    ]);
    return repo;
}
function generateKey(descriptor) {
    const database = descriptor.database || "master";
    return `${descriptor.id}-${descriptor.language}-${descriptor.version}-${database}`.toLowerCase();
}
function getItemIdLanguageKey(descriptor) {
    return `${descriptor.id}${descriptor.language}`.toLowerCase();
}
//# sourceMappingURL=itemsRepository.js.map