"use client";
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef, } from "react";
import { normalizeItemId } from "./useNodeIndicators";
import { useEditContext } from "../client/editContext";
import { fetchIndicatorProviders, fetchIndicatorsStream, } from "../services/indicatorService";
const GutterContext = createContext(null);
const BATCH_DELAY_MS = 100;
const INVALIDATION_BATCH_DELAY_MS = 50;
const PRESENCE_REFRESH_DELAY_MS = 500;
const INDICATOR_TRIGGER_DEPENDENCIES = {
    "active-sessions": ["presence"],
    "comment-updated": ["feedback"],
    "comment-deleted": ["feedback"],
    "suggested-edit-updated": ["feedback"],
    "suggested-edit-deleted": ["feedback"],
    "item-changed": [],
    "language-version-changed": [],
    "manual-refresh": [],
};
function getFetchedSet(map, providerId) {
    return map[providerId] ?? new Set();
}
export function GutterProvider({ children, initialNodeIds = [], defaultVisibleGutters = [], language = "en", version = 1, }) {
    const editContext = useEditContext();
    // Track whether we've loaded preferences from the backend
    const hasLoadedPrefsRef = useRef(false);
    const [visibleGutters, setVisibleGuttersState] = useState(() => {
        // Try to load from user preferences (may not be available on initial mount)
        const savedPrefs = editContext?.userInfo?.preferences?.gutterVisibility;
        if (Array.isArray(savedPrefs)) {
            hasLoadedPrefsRef.current = true;
            return savedPrefs; // Respect user's choice, even if empty
        }
        return defaultVisibleGutters;
    });
    // Show badges preference (default to true)
    const [showBadges, setShowBadges] = useState(() => {
        const savedPref = editContext?.userInfo?.preferences?.gutterShowBadges;
        return savedPref !== false; // Default to true if not set
    });
    // Display names preference (default to false to preserve current behavior)
    const [showDisplayNames, setShowDisplayNames] = useState(() => {
        return editContext?.userInfo?.preferences?.showDisplayNames === true;
    });
    // Load preferences when they become available after mount (e.g., after page reload)
    const userPreferences = editContext?.userInfo?.preferences;
    useEffect(() => {
        // Only load once, and only if we haven't already loaded them
        if (hasLoadedPrefsRef.current)
            return;
        const savedPrefs = userPreferences?.gutterVisibility;
        if (Array.isArray(savedPrefs)) {
            hasLoadedPrefsRef.current = true;
            setVisibleGuttersState(savedPrefs); // Respect user's choice, even if empty
        }
        // Load badges preference
        if (userPreferences?.gutterShowBadges !== undefined) {
            setShowBadges(userPreferences.gutterShowBadges !== false);
        }
        if (userPreferences?.showDisplayNames !== undefined) {
            setShowDisplayNames(userPreferences.showDisplayNames === true);
        }
    }, [userPreferences]);
    // Available providers - fetched once on mount
    const [availableProviders, setAvailableProviders] = useState([]);
    const [providersLoading, setProvidersLoading] = useState(true);
    // Track ALL visible node IDs - accumulated from GutterColumns renders
    const [allNodeIds, setAllNodeIds] = useState(initialNodeIds);
    const pendingNodeIdsRef = useRef(new Set(initialNodeIds));
    const batchTimeoutRef = useRef(null);
    // Cached indicators - persisted across fetches.
    const [cachedIndicators, setCachedIndicators] = useState({});
    // Track fetched state per provider so we can refresh only specific indicators.
    const [fetchedByProvider, setFetchedByProvider] = useState({});
    const fetchedByProviderRef = useRef({});
    // Track in-flight requests per provider+item to avoid duplicate fetch loops.
    const inFlightByProviderRef = useRef({});
    // Track active requests to drive loading state.
    const [activeRequests, setActiveRequests] = useState(0);
    const requestControllersRef = useRef(new Set());
    // Track items pending invalidation (batched)
    const pendingInvalidationsRef = useRef(new Set());
    const invalidationTimeoutRef = useRef(null);
    useEffect(() => {
        fetchedByProviderRef.current = fetchedByProvider;
    }, [fetchedByProvider]);
    // Fetch available providers on mount
    useEffect(() => {
        let cancelled = false;
        fetchIndicatorProviders()
            .then((providers) => {
            if (!cancelled) {
                setAvailableProviders(providers);
                setProvidersLoading(false);
                // Validate saved preferences against available provider IDs
                const validIds = new Set(providers.map((p) => p.id));
                setVisibleGuttersState((prev) => {
                    const validGutters = prev.filter((id) => validIds.has(id));
                    return validGutters;
                });
            }
        })
            .catch((err) => {
            if (!cancelled) {
                console.error("Failed to fetch indicator providers:", err);
                setProvidersLoading(false);
            }
        });
        return () => {
            cancelled = true;
        };
    }, []);
    // Register visible nodes with batching
    const registerVisibleNode = useCallback((nodeId) => {
        if (pendingNodeIdsRef.current.has(nodeId))
            return;
        pendingNodeIdsRef.current.add(nodeId);
        // Batch updates to avoid too many re-renders
        if (batchTimeoutRef.current) {
            clearTimeout(batchTimeoutRef.current);
        }
        batchTimeoutRef.current = setTimeout(() => {
            setAllNodeIds(Array.from(pendingNodeIdsRef.current));
            batchTimeoutRef.current = null;
        }, BATCH_DELAY_MS);
    }, []);
    const resolveProvidersForTrigger = useCallback((trigger, fallbackProviders = visibleGutters) => {
        const configured = INDICATOR_TRIGGER_DEPENDENCIES[trigger] ?? [];
        if (configured.length === 0) {
            return fallbackProviders.filter((id, index, arr) => arr.indexOf(id) === index);
        }
        const visibleSet = new Set(visibleGutters);
        return configured.filter((providerId) => visibleSet.has(providerId));
    }, [visibleGutters]);
    const markProviderFetchState = useCallback((providerId, itemIds, options) => {
        const normalizedIds = itemIds.map(normalizeItemId);
        const shouldMarkFetched = options?.fetched ?? false;
        const shouldMarkInFlight = options?.inFlight ?? false;
        if (shouldMarkFetched) {
            setFetchedByProvider((prev) => {
                const next = { ...prev };
                const set = new Set(getFetchedSet(next, providerId));
                for (const id of normalizedIds) {
                    set.add(id);
                }
                next[providerId] = set;
                return next;
            });
        }
        const nextInFlight = { ...inFlightByProviderRef.current };
        const inFlightSet = new Set(getFetchedSet(nextInFlight, providerId));
        if (shouldMarkInFlight) {
            for (const id of normalizedIds) {
                inFlightSet.add(id);
            }
        }
        else {
            for (const id of normalizedIds) {
                inFlightSet.delete(id);
            }
        }
        nextInFlight[providerId] = inFlightSet;
        inFlightByProviderRef.current = nextInFlight;
    }, []);
    const fetchProviderForItems = useCallback(async (providerId, itemIds) => {
        if (itemIds.length === 0)
            return;
        const dedupedItemIds = itemIds.filter((id, index, arr) => arr.indexOf(id) === index);
        const inFlightSet = getFetchedSet(inFlightByProviderRef.current, providerId);
        const idsToFetch = dedupedItemIds.filter((id) => !inFlightSet.has(normalizeItemId(id)));
        if (idsToFetch.length === 0)
            return;
        markProviderFetchState(providerId, idsToFetch, { inFlight: true });
        const controller = new AbortController();
        requestControllersRef.current.add(controller);
        setActiveRequests((prev) => prev + 1);
        try {
            await fetchIndicatorsStream(idsToFetch, [providerId], language, (chunk) => {
                if (controller.signal.aborted)
                    return;
                if (chunk.type === "indicator") {
                    const normalizedId = normalizeItemId(chunk.itemId);
                    setCachedIndicators((prev) => ({
                        ...prev,
                        [normalizedId]: {
                            ...prev[normalizedId],
                            [chunk.providerId]: chunk.data,
                        },
                    }));
                }
            }, controller.signal, editContext?.sessionId);
            if (!controller.signal.aborted) {
                markProviderFetchState(providerId, idsToFetch, { fetched: true });
            }
        }
        catch (error) {
            if (!controller.signal.aborted) {
                console.error(`Failed to fetch ${providerId} indicators`, error);
            }
        }
        finally {
            markProviderFetchState(providerId, idsToFetch, { inFlight: false });
            requestControllersRef.current.delete(controller);
            setActiveRequests((prev) => Math.max(0, prev - 1));
        }
    }, [editContext?.sessionId, language, markProviderFetchState]);
    const fetchProvidersForItems = useCallback(async (providerIds, itemIds) => {
        if (providerIds.length === 0 || itemIds.length === 0)
            return;
        const uniqueProviders = providerIds.filter((id, index, arr) => arr.indexOf(id) === index);
        await Promise.all(uniqueProviders.map((providerId) => fetchProviderForItems(providerId, itemIds)));
    }, [fetchProviderForItems]);
    const removeProviderDataFromCache = useCallback((providerIds, itemIds) => {
        const normalizedIds = new Set(itemIds.map(normalizeItemId));
        setCachedIndicators((prev) => {
            const next = { ...prev };
            for (const normalizedId of normalizedIds) {
                const itemData = next[normalizedId];
                if (!itemData)
                    continue;
                const nextItemData = { ...itemData };
                for (const providerId of providerIds) {
                    delete nextItemData[providerId];
                }
                if (Object.keys(nextItemData).length === 0) {
                    delete next[normalizedId];
                }
                else {
                    next[normalizedId] = nextItemData;
                }
            }
            return next;
        });
    }, []);
    const clearFetchedStateForProviders = useCallback((providerIds, itemIds) => {
        const normalizedIds = itemIds.map(normalizeItemId);
        setFetchedByProvider((prev) => {
            const next = { ...prev };
            for (const providerId of providerIds) {
                const set = new Set(getFetchedSet(next, providerId));
                for (const id of normalizedIds) {
                    set.delete(id);
                }
                next[providerId] = set;
            }
            return next;
        });
    }, []);
    const invalidateProvidersForItems = useCallback((providerIds, itemIds) => {
        if (providerIds.length === 0 || itemIds.length === 0)
            return;
        const uniqueProviders = providerIds.filter((id, index, arr) => arr.indexOf(id) === index);
        const uniqueItems = itemIds.filter((id, index, arr) => arr.indexOf(id) === index);
        clearFetchedStateForProviders(uniqueProviders, uniqueItems);
        removeProviderDataFromCache(uniqueProviders, uniqueItems);
        void fetchProvidersForItems(uniqueProviders, uniqueItems);
    }, [clearFetchedStateForProviders, removeProviderDataFromCache, fetchProvidersForItems]);
    const refreshProviders = useCallback((providerIds, itemIds) => {
        const targetProviders = providerIds.filter((id, index, arr) => arr.indexOf(id) === index);
        const targetItems = itemIds && itemIds.length > 0 ? itemIds : allNodeIds;
        if (targetProviders.length === 0 || targetItems.length === 0)
            return;
        invalidateProvidersForItems(targetProviders, targetItems);
    }, [allNodeIds, invalidateProvidersForItems]);
    // Ensure all visible nodes have all visible providers fetched (provider-aware incremental fetch).
    useEffect(() => {
        if (visibleGutters.length === 0 || allNodeIds.length === 0)
            return;
        for (const providerId of visibleGutters) {
            const fetchedSet = getFetchedSet(fetchedByProviderRef.current, providerId);
            const inFlightSet = getFetchedSet(inFlightByProviderRef.current, providerId);
            const missingIds = allNodeIds.filter((itemId) => {
                const normalizedId = normalizeItemId(itemId);
                return !fetchedSet.has(normalizedId) && !inFlightSet.has(normalizedId);
            });
            if (missingIds.length > 0) {
                void fetchProviderForItems(providerId, missingIds);
            }
        }
    }, [allNodeIds, fetchProviderForItems, fetchedByProvider, visibleGutters]);
    // Track previous language and version to detect changes
    const prevLanguageRef = useRef(language);
    const prevVersionRef = useRef(version);
    // When language or version changes, clear cache and refetch all visible providers.
    useEffect(() => {
        const languageChanged = prevLanguageRef.current !== language;
        const versionChanged = prevVersionRef.current !== version;
        if (languageChanged || versionChanged) {
            requestControllersRef.current.forEach((controller) => controller.abort());
            requestControllersRef.current.clear();
            inFlightByProviderRef.current = {};
            setActiveRequests(0);
            setCachedIndicators({});
            setFetchedByProvider({});
            const providersToRefresh = resolveProvidersForTrigger("language-version-changed", visibleGutters);
            if (allNodeIds.length > 0 && providersToRefresh.length > 0) {
                void fetchProvidersForItems(providersToRefresh, allNodeIds);
            }
        }
        prevLanguageRef.current = language;
        prevVersionRef.current = version;
    }, [
        allNodeIds,
        fetchProvidersForItems,
        language,
        resolveProvidersForTrigger,
        version,
        visibleGutters,
    ]);
    // Cleanup timeout on unmount
    useEffect(() => {
        return () => {
            if (batchTimeoutRef.current) {
                clearTimeout(batchTimeoutRef.current);
            }
            if (invalidationTimeoutRef.current) {
                clearTimeout(invalidationTimeoutRef.current);
            }
            requestControllersRef.current.forEach((controller) => controller.abort());
            requestControllersRef.current.clear();
        };
    }, []);
    // Save visible gutters to user preferences when they change
    const setUserPreferences = editContext?.setUserPreferences;
    const prevVisibleGuttersRef = useRef(visibleGutters);
    useEffect(() => {
        const hasChanged = prevVisibleGuttersRef.current.length !== visibleGutters.length ||
            prevVisibleGuttersRef.current.some((g, i) => g !== visibleGutters[i]);
        // Save when there's a change - allow saving empty array when user deselects all indicators
        if (setUserPreferences && hasChanged) {
            prevVisibleGuttersRef.current = visibleGutters;
            setUserPreferences({ gutterVisibility: visibleGutters });
        }
    }, [visibleGutters, setUserPreferences]);
    // Save badges preference when it changes
    const prevShowBadgesRef = useRef(showBadges);
    useEffect(() => {
        if (setUserPreferences && prevShowBadgesRef.current !== showBadges) {
            prevShowBadgesRef.current = showBadges;
            setUserPreferences({ gutterShowBadges: showBadges });
        }
    }, [showBadges, setUserPreferences]);
    // Save display names preference when it changes
    const prevShowDisplayNamesRef = useRef(showDisplayNames);
    useEffect(() => {
        if (setUserPreferences &&
            prevShowDisplayNamesRef.current !== showDisplayNames) {
            prevShowDisplayNamesRef.current = showDisplayNames;
            setUserPreferences({ showDisplayNames });
        }
    }, [showDisplayNames, setUserPreferences]);
    const isLoading = providersLoading || activeRequests > 0;
    const providers = availableProviders;
    const indicators = cachedIndicators;
    const toggleGutter = useCallback((providerId) => {
        setVisibleGuttersState((prev) => {
            if (prev.includes(providerId)) {
                return prev.filter((id) => id !== providerId);
            }
            else {
                return [...prev, providerId];
            }
        });
    }, []);
    const setVisibleGutters = useCallback((ids) => {
        setVisibleGuttersState(ids);
    }, []);
    const toggleBadges = useCallback(() => {
        setShowBadges((prev) => !prev);
    }, []);
    const toggleDisplayNames = useCallback(() => {
        setShowDisplayNames((prev) => !prev);
    }, []);
    const getIndicator = useCallback((itemId, providerId) => {
        const normalizedId = normalizeItemId(itemId);
        return indicators[normalizedId]?.[providerId];
    }, [indicators]);
    // Full refresh now delegates to provider-scoped invalidation for all visible providers.
    const refresh = useCallback(() => {
        const providersToRefresh = resolveProvidersForTrigger("manual-refresh", visibleGutters);
        refreshProviders(providersToRefresh);
    }, [refreshProviders, resolveProvidersForTrigger, visibleGutters]);
    // Invalidate a specific item (remove from cache and refetch)
    const invalidateItem = useCallback((itemId) => {
        const normalizedId = normalizeItemId(itemId);
        // Add to pending invalidations for batching
        pendingInvalidationsRef.current.add(normalizedId);
        // Batch invalidations
        if (invalidationTimeoutRef.current) {
            clearTimeout(invalidationTimeoutRef.current);
        }
        invalidationTimeoutRef.current = setTimeout(() => {
            const idsToInvalidate = Array.from(pendingInvalidationsRef.current);
            pendingInvalidationsRef.current.clear();
            // Find original (non-normalized) IDs that are still visible
            const originalIds = allNodeIds.filter((id) => idsToInvalidate.includes(normalizeItemId(id)));
            const targetIds = originalIds.length > 0 ? originalIds : idsToInvalidate;
            const providersToRefresh = resolveProvidersForTrigger("item-changed", visibleGutters);
            invalidateProvidersForItems(providersToRefresh, targetIds);
            invalidationTimeoutRef.current = null;
        }, INVALIDATION_BATCH_DELAY_MS);
    }, [
        allNodeIds,
        invalidateProvidersForItems,
        resolveProvidersForTrigger,
        visibleGutters,
    ]);
    // Subscribe to item changes to invalidate cache when items are saved
    useEffect(() => {
        const itemsRepository = editContext?.itemsRepository;
        if (!itemsRepository?.subscribeItemsChanged)
            return;
        const unsubscribe = itemsRepository.subscribeItemsChanged((changes) => {
            // Invalidate cache for any items that were updated
            for (const change of changes) {
                const hasMeaningfulItemChange = !!change.changes?.properties ||
                    !!change.changes?.children ||
                    (change.changes?.fields?.length ?? 0) > 0;
                if ((change.action === "update" || change.action === "add") &&
                    hasMeaningfulItemChange) {
                    // Refresh only items/providers that have already been fetched.
                    const normalizedId = normalizeItemId(change.item.id);
                    const isFetchedByAnyVisibleProvider = visibleGutters.some((providerId) => getFetchedSet(fetchedByProviderRef.current, providerId).has(normalizedId));
                    if (isFetchedByAnyVisibleProvider) {
                        invalidateItem(change.item.id);
                    }
                }
            }
        });
        return unsubscribe;
    }, [editContext?.itemsRepository, invalidateItem, visibleGutters]);
    // Subscribe to WebSocket messages for indicator updates (feedback and presence)
    const presenceRefreshTimeoutRef = useRef(null);
    const hasInitialFetchRef = useRef(false);
    // Track the last active-sessions payload fingerprint to avoid unnecessary refreshes
    const lastActiveSessionsFingerprintRef = useRef("");
    // Track when initial fetch completes
    useEffect(() => {
        if (Object.values(fetchedByProvider).some((fetchedSet) => fetchedSet && fetchedSet.size > 0)) {
            hasInitialFetchRef.current = true;
        }
    }, [fetchedByProvider]);
    useEffect(() => {
        const addSocketMessageListener = editContext?.addSocketMessageListener;
        if (!addSocketMessageListener)
            return;
        const feedbackProviders = resolveProvidersForTrigger("comment-updated");
        const presenceProviders = resolveProvidersForTrigger("active-sessions");
        const hasFeedbackProvider = feedbackProviders.length > 0;
        const hasPresenceProvider = presenceProviders.length > 0;
        // Only subscribe if at least one relevant provider is enabled
        if (!hasFeedbackProvider && !hasPresenceProvider)
            return;
        const unsubscribe = addSocketMessageListener((message) => {
            // Feedback provider: comment/suggestion updates
            if (hasFeedbackProvider) {
                if (message.type === "comment-updated" &&
                    message.payload?.comment?.mainItemId) {
                    const providersToRefresh = resolveProvidersForTrigger("comment-updated");
                    invalidateProvidersForItems(providersToRefresh, [
                        message.payload.comment.mainItemId,
                    ]);
                }
                if (message.type === "comment-deleted") {
                    const providersToRefresh = resolveProvidersForTrigger("comment-deleted");
                    refreshProviders(providersToRefresh);
                }
                if (message.type === "suggested-edit-updated" &&
                    message.payload?.suggestedEdit?.mainItemId) {
                    const providersToRefresh = resolveProvidersForTrigger("suggested-edit-updated");
                    invalidateProvidersForItems(providersToRefresh, [
                        message.payload.suggestedEdit.mainItemId,
                    ]);
                }
                if (message.type === "suggested-edit-deleted") {
                    const providersToRefresh = resolveProvidersForTrigger("suggested-edit-deleted");
                    refreshProviders(providersToRefresh);
                }
            }
            // Presence provider: session changes (debounced)
            // Only refresh if we have already fetched indicators (skip initial load race condition)
            // AND the sessions data has actually changed (avoid unnecessary refetches)
            if (hasPresenceProvider &&
                message.type === "active-sessions" &&
                hasInitialFetchRef.current) {
                // Build a fingerprint of the sessions that matter for presence indicators.
                // Only refresh when the fingerprint changes (i.e. someone joined, left, or changed item).
                let fingerprint = "";
                try {
                    const sessions = message.payload?.sessions ?? message.payload;
                    if (Array.isArray(sessions)) {
                        fingerprint = sessions
                            .filter((s) => s?.sessionId !== editContext?.sessionId)
                            .map((s) => `${s.sessionId ?? ""}:${s.item?.id ?? ""}:${s.item?.language ?? ""}`)
                            .sort()
                            .join("|");
                    }
                    else {
                        fingerprint = JSON.stringify(message.payload);
                    }
                }
                catch {
                    fingerprint = String(Date.now()); // fallback: always refresh on parse error
                }
                if (fingerprint === lastActiveSessionsFingerprintRef.current) {
                    return; // Sessions haven't changed, skip refresh
                }
                lastActiveSessionsFingerprintRef.current = fingerprint;
                if (presenceRefreshTimeoutRef.current) {
                    clearTimeout(presenceRefreshTimeoutRef.current);
                }
                presenceRefreshTimeoutRef.current = setTimeout(() => {
                    refreshProviders(presenceProviders);
                    presenceRefreshTimeoutRef.current = null;
                }, PRESENCE_REFRESH_DELAY_MS);
            }
        });
        return () => {
            unsubscribe();
            if (presenceRefreshTimeoutRef.current) {
                clearTimeout(presenceRefreshTimeoutRef.current);
            }
        };
    }, [
        editContext?.addSocketMessageListener,
        editContext?.sessionId,
        visibleGutters,
        invalidateProvidersForItems,
        refreshProviders,
        resolveProvidersForTrigger,
    ]);
    const value = useMemo(() => ({
        visibleGutters,
        toggleGutter,
        setVisibleGutters,
        providers,
        indicators,
        isLoading,
        getIndicator,
        refresh,
        invalidateItem,
        registerVisibleNode,
        availableProviders,
        providersLoading,
        showBadges,
        toggleBadges,
        showDisplayNames,
        toggleDisplayNames,
    }), [
        visibleGutters,
        toggleGutter,
        setVisibleGutters,
        providers,
        indicators,
        isLoading,
        getIndicator,
        refresh,
        invalidateItem,
        registerVisibleNode,
        availableProviders,
        providersLoading,
        showBadges,
        toggleBadges,
        showDisplayNames,
        toggleDisplayNames,
    ]);
    return (_jsx(GutterContext.Provider, { value: value, children: children }));
}
export function useGutterContext() {
    const context = useContext(GutterContext);
    if (!context) {
        throw new Error("useGutterContext must be used within a GutterProvider");
    }
    return context;
}
/**
 * Safe version that returns null if not in a provider.
 * Useful for optional gutter support.
 */
export function useGutterContextSafe() {
    return useContext(GutterContext);
}
//# sourceMappingURL=GutterContext.js.map