import { startTransition } from "react";
import { mergeSegmentLoaders, needsLoaderMerge, insertMissingDiffSegments, } from "./merge-segment-loaders.js";
import { assertSegmentStructure } from "./segment-structure-assert.js";
/**
 * Create a partial updater for fetching and applying RSC partial updates
 *
 * This function is shared between navigation-bridge and server-action-bridge
 * to handle partial RSC updates with HMR resilience.
 *
 * @param config - Partial update configuration
 * @returns fetchPartialUpdate function
 *
 * @example
 * ```typescript
 * const fetchPartialUpdate = createPartialUpdater({
 *   store,
 *   client,
 *   onUpdate: (update) => store.emit(update),
 *   renderSegments,
 * });
 *
 * await fetchPartialUpdate('/new-page');
 * ```
 */
export function createPartialUpdater(config) {
    const { store, client, onUpdate, renderSegments, version } = config;
    /**
     * Build a lookup map from current page's cached segments
     */
    function getCurrentSegmentMap() {
        const currentKey = store.getHistoryKey();
        const cached = store.getCachedSegments(currentKey);
        const cachedSegments = cached?.segments || [];
        const map = new Map();
        cachedSegments.forEach((s) => map.set(s.id, s));
        return map;
    }
    /**
     * Fetch partial update and trigger UI update
     * Returns a promise that resolves when the RSC stream is fully consumed
     *
     * @param tx - Transaction for committing segment state (required)
     * @param signal - AbortSignal to check if navigation is stale (not for aborting fetch)
     */
    async function fetchPartialUpdate(targetUrl, segmentIds, isRetry, signal, tx, options) {
        const { isAction = false, staleRevalidation = false, interceptSourceUrl, targetCacheSegments, targetCacheHandleData, leavingIntercept = false, } = options || {};
        const segmentState = store.getSegmentState();
        const url = targetUrl || window.location.href;
        // Capture history key at start for stale revalidation consistency check
        const historyKeyAtStart = store.getHistoryKey();
        // When leaving intercept, don't send current segment IDs - we need fresh non-intercept segments
        // Filter out intercept-related segments (parallel slots like @modal) from current segments
        let segments;
        if (leavingIntercept) {
            // When leaving intercept, only send segments that aren't intercept-specific
            // The server will return the non-intercept version of the route
            const currentSegments = segmentIds ?? segmentState.currentSegmentIds;
            // Filter out intercept-specific parallel slots (containing ".@") so the
            // server resolves the base route instead of the modal overlay.
            segments = currentSegments.filter((id) => !id.includes(".@"));
            console.log(`[Browser] Leaving intercept - filtered segments: ${segments.join(", ")}`);
        }
        else {
            segments = segmentIds ?? segmentState.currentSegmentIds;
        }
        // For intercept revalidation, use the intercept source URL as previousUrl
        // This tells the server the route should be treated as an intercept
        const previousUrl = interceptSourceUrl || tx.currentUrl || segmentState.currentUrl;
        console.log(`\n[Browser] >>> NAVIGATION`);
        console.log(`[Browser] From: ${previousUrl}`);
        console.log(`[Browser] To: ${url}`);
        console.log(`[Browser] Segments to send: ${segments.join(", ")}`);
        if (interceptSourceUrl) {
            console.log(`[Browser] Intercept context from: ${interceptSourceUrl}`);
        }
        // Build segment map for merging with server diff.
        // When targetCacheSegments is provided (navigating to a cached route), use those
        // to ensure consistency - we use the same segments we told the server we have.
        // Otherwise fall back to current page's segments (for same-route revalidation).
        let currentSegmentMap;
        if (targetCacheSegments && targetCacheSegments.length > 0) {
            currentSegmentMap = new Map();
            targetCacheSegments.forEach((s) => currentSegmentMap.set(s.id, s));
        }
        else {
            currentSegmentMap = getCurrentSegmentMap();
        }
        // Mark navigation as streaming (response received, now parsing RSC)
        // The token is ended when the stream completes
        const streamingToken = tx.startStreaming();
        // Fetch partial payload (no abort signal - RSC doesn't support it well)
        // Wrapped in try/catch to ensure streamingToken.end() is called if fetch throws,
        // preventing isStreaming from being permanently stuck as true.
        let fetchResult;
        try {
            fetchResult = await client.fetchPartial({
                targetUrl: url,
                segmentIds: segments,
                previousUrl,
                staleRevalidation,
                version,
            });
        }
        catch (err) {
            streamingToken.end();
            throw err;
        }
        const { payload, streamComplete: rawStreamComplete } = fetchResult;
        console.log("payload.metadata", payload.metadata);
        const streamComplete = rawStreamComplete.then(() => {
            streamingToken.end();
        });
        if (payload.metadata?.isPartial) {
            const { segments: newSegments, matched, diff } = payload.metadata;
            // Check if this navigation is stale (a newer one started)
            if (signal?.aborted) {
                console.log(`[Browser] Ignoring stale navigation (aborted)`);
                return streamComplete;
            }
            console.log(`[Browser] Partial update - matched: ${matched?.join(", ")}`);
            console.log(`[Browser] Diff: ${diff?.join(", ")}`);
            // Create lookup for new segments from server
            const newSegmentMap = new Map();
            (newSegments || []).forEach((s) => newSegmentMap.set(s.id, s));
            // If diff is empty, nothing changed on server side.
            // However, if we're navigating with targetCacheSegments (to a different route),
            // we still need to render those segments since the UI is showing the old route.
            if (!diff || diff.length === 0) {
                const matchedIds = matched || [];
                const existingSegments = matchedIds
                    .map((id) => currentSegmentMap.get(id))
                    .filter(Boolean);
                // When navigating with cached segments to a different route, render them.
                // targetCacheSegments being provided means we're navigating to a cached route.
                if (targetCacheSegments && targetCacheSegments.length > 0) {
                    console.log(`[Browser] No diff but navigating with cached segments - rendering target route`);
                    const newTree = await renderSegments(existingSegments, {
                        forceAwait: true,
                    });
                    tx.commit(matchedIds, existingSegments);
                    // Include cachedHandleData in metadata so NavigationProvider can restore
                    // breadcrumbs and other handle data from cache.
                    // IMPORTANT: Remove `handles` from metadata to prevent NavigationProvider from
                    // processing an empty handles stream, which would clear the cached breadcrumbs.
                    // When rendering from cache with empty diff, we want to use cachedHandleData instead.
                    const { handles: _unusedHandles, ...metadataWithoutHandles } = payload.metadata;
                    onUpdate({
                        root: newTree,
                        metadata: {
                            ...metadataWithoutHandles,
                            cachedHandleData: targetCacheHandleData,
                        },
                    });
                    console.log(`[Browser] Navigation complete (rendered from cache)\n`);
                    return streamComplete;
                }
                // When leaving intercept, force re-render even with empty diff
                // The matched segments are the non-intercept segments, which we need to render
                // to remove the modal from the UI
                if (leavingIntercept) {
                    console.log(`[Browser] Leaving intercept - forcing re-render to remove modal`);
                    const newTree = await renderSegments(existingSegments, {
                        forceAwait: true,
                    });
                    tx.commit(matchedIds, existingSegments);
                    onUpdate({
                        root: newTree,
                        metadata: payload.metadata,
                    });
                    console.log(`[Browser] Navigation complete (left intercept)\n`);
                    return streamComplete;
                }
                // Same route revalidation with no changes - skip UI update
                console.log(`[Browser] No changes - all revalidations returned false, keeping existing UI`);
                tx.commit(matchedIds, existingSegments);
                console.log(`[Browser] Navigation complete (no re-render)\n`);
                return streamComplete;
            }
            // Build full segment list by merging:
            // - New/changed segments from server response (diff)
            // - Unchanged segments from current page's cache
            const matchedIds = matched || [];
            console.log(`[Browser] matchedIds: ${matchedIds.join(", ")}`);
            console.log(`[Browser] currentSegmentMap keys: ${[...currentSegmentMap.keys()].join(", ")}`);
            console.log(`[Browser] newSegmentMap keys: ${[...newSegmentMap.keys()].join(", ")}`, newSegmentMap);
            // First pass: build segments from matched IDs
            const matchedIdSet = new Set(matchedIds);
            const allSegments = matchedIds
                .map((id) => {
                // First check server response (new/updated segments)
                const fromServer = newSegmentMap.get(id);
                if (fromServer) {
                    // For partial revalidation (stale or action), merge server's new loader data
                    // with cached loader data when server returns fewer loaders than cached
                    const fromCache = currentSegmentMap.get(id);
                    // Dev-mode assertion: warn if tree structure would change
                    if (fromCache) {
                        assertSegmentStructure(fromCache, fromServer, "partial-update");
                    }
                    if ((staleRevalidation || isAction) &&
                        needsLoaderMerge(fromServer, fromCache)) {
                        return mergeSegmentLoaders(fromServer, fromCache);
                    }
                    // Preserve cached structural properties to maintain consistent React tree.
                    // The server may return different values for loading (isSSR context) and
                    // mountPath (include scope), which change the element nesting depth
                    // (with/without RouteContentWrapper, MountContextProvider).
                    if (fromCache) {
                        let merged = fromServer;
                        // When server returns component: null for a layout segment, it means
                        // "this segment doesn't need re-rendering" - preserve the cached component
                        // to maintain the outlet chain and prevent React tree changes
                        if (fromServer.component === null &&
                            fromServer.type === "layout" &&
                            fromCache.component != null) {
                            merged = { ...merged, component: fromCache.component };
                        }
                        if (fromCache.loading !== undefined &&
                            fromServer.loading !== fromCache.loading) {
                            merged = { ...merged, loading: fromCache.loading };
                        }
                        if (fromServer.mountPath !== fromCache.mountPath) {
                            merged = { ...merged, mountPath: fromCache.mountPath };
                        }
                        return merged;
                    }
                    return fromServer;
                }
                // Fall back to current page's cached segments
                const fromCache = currentSegmentMap.get(id);
                if (!fromCache) {
                    console.warn(`[Browser] Missing segment: ${id}`);
                    return fromCache;
                }
                // For cached segments the server decided not to re-render:
                // - During actions: preserve loading exactly as-is to maintain tree structure.
                //   Changing loading between renders alters the React tree
                //   (with/without RouteContentWrapper), causing remounts that destroy
                //   useActionState and other client state.
                // - During navigation: preserve loading=false (suppressed boundary) but
                //   clear truthy loading (active skeleton) to prevent suspense on cached content.
                if (isAction) {
                    return fromCache;
                }
                if (fromCache.loading !== undefined && fromCache.loading !== false) {
                    return { ...fromCache, loading: undefined };
                }
                return fromCache;
            })
                .filter(Boolean);
            // Insert diff segments not in matchedIds (e.g., loader segments from consolidation fetch)
            insertMissingDiffSegments(allSegments, diff, matchedIdSet, newSegmentMap);
            // HMR RESILIENCE: Check if we're missing any matched segments
            // Note: allSegments may include additional diff segments, so we check matchedIds specifically
            const allSegmentIdSet = new Set(allSegments.map((s) => s.id));
            const missingIds = matchedIds.filter((id) => !allSegmentIdSet.has(id));
            if (missingIds.length > 0) {
                const missingCount = missingIds.length;
                if (isRetry) {
                    console.warn("Missing ids", { missingIds });
                    throw new Error(`[Browser] Failed to fetch segments after retry. Missing: [${missingIds.join(", ")}]`);
                }
                if (signal?.aborted) {
                    console.log(`[Browser] Ignoring stale navigation (aborted during HMR retry)`);
                    return streamComplete;
                }
                if (isAction) {
                    return streamComplete;
                }
                console.warn(`[Browser] HMR detected: Missing ${missingCount} segments. Refetching all...`);
                // Refetch with empty segments = server sends everything
                return fetchPartialUpdate(url, [], true, signal, tx, { isAction });
            }
            // INTERCEPT HANDLING: Separate intercept segments for explicit injection
            // Intercept segments have namespace starting with "intercept:" or ID containing .@
            // This makes the flow clearer and easier to debug
            const isInterceptSegment = (s) => s.namespace?.startsWith("intercept:") ||
                (s.type === "parallel" && s.id.includes(".@"));
            const interceptSegments = allSegments.filter(isInterceptSegment);
            const mainSegments = allSegments.filter((s) => !isInterceptSegment(s));
            if (signal?.aborted) {
                console.log(`[Browser] Ignoring stale navigation (aborted before render)`);
                return streamComplete;
            }
            // Rebuild tree on client (await for loader data resolution)
            // Race against abort signal to allow cancellation during loader awaiting
            // Pass intercept segments separately for explicit handling
            // For stale revalidation, use forceAwait to ensure no loading fallbacks
            const renderOptions = {
                isAction,
                forceAwait: staleRevalidation,
                interceptSegments: interceptSegments.length > 0 ? interceptSegments : undefined,
            };
            const newTree = await (signal
                ? Promise.race([
                    renderSegments(mainSegments, renderOptions),
                    new Promise((_, reject) => {
                        if (signal.aborted) {
                            reject(new DOMException("Navigation aborted", "AbortError"));
                        }
                        signal.addEventListener("abort", () => {
                            reject(new DOMException("Navigation aborted", "AbortError"));
                        });
                    }),
                ])
                : renderSegments(mainSegments, renderOptions));
            // Final abort check before committing - another navigation may have started
            if (signal?.aborted) {
                console.log(`[Browser] Ignoring stale navigation (aborted before commit)`);
                return streamComplete;
            }
            // Check if this is an intercept response (any slot is active)
            // If so, disable scroll to keep the current scroll position
            const hasActiveIntercept = payload.metadata?.slots
                ? Object.values(payload.metadata.slots).some((slot) => slot.active)
                : false;
            // Track intercept context for action revalidation (only on navigation, not actions or stale revalidation)
            if (!isAction && !staleRevalidation) {
                if (hasActiveIntercept) {
                    // Save the source URL for action revalidation to maintain intercept context
                    store.setInterceptSourceUrl(segmentState.currentUrl);
                }
                else {
                    // Clear intercept context when navigating to a non-intercept route
                    store.setInterceptSourceUrl(null);
                }
            }
            // Commit navigation - transaction handles all store mutations atomically
            // For intercept responses: disable scroll, mark as intercept, include source URL
            // Use allSegmentIds (derived from allSegments) instead of matchedIds because
            // we may have added diff segments (like loader segments) not in the matched array
            const allSegmentIds = allSegments.map((s) => s.id);
            tx.commit(allSegmentIds, allSegments, hasActiveIntercept
                ? {
                    scroll: false,
                    intercept: true,
                    interceptSourceUrl: segmentState.currentUrl,
                }
                : undefined);
            // For stale revalidation: verify history key hasn't changed before updating UI
            // If user navigated away, skip UI update to avoid corrupting current view
            if (staleRevalidation) {
                const historyKeyNow = store.getHistoryKey();
                if (historyKeyNow !== historyKeyAtStart) {
                    console.log(`[Browser] Stale revalidation: history key changed (${historyKeyAtStart} -> ${historyKeyNow}), skipping UI update`);
                    return streamComplete;
                }
            }
            console.log("[partial-update] updating document");
            // Emit update to trigger React render
            // For stale revalidation: wait for stream to complete (loaders resolved), then update
            // For actions: wrap in startTransition to avoid UI flickering
            if (isAction || staleRevalidation) {
                startTransition(() => {
                    onUpdate({
                        root: newTree,
                        metadata: payload.metadata,
                    });
                });
            }
            else {
                onUpdate({
                    root: newTree,
                    metadata: payload.metadata,
                });
            }
            console.log(`[Browser] Navigation complete\n`);
            return streamComplete;
        }
        else {
            // Full update (fallback)
            // Use client-side renderSegments instead of payload.root to ensure
            // consistent component references with action revalidation.
            // Server-rendered RSC tree has different component references than
            // client-created tree, which causes React to remount LoaderBoundary
            // when actions trigger revalidation.
            console.warn(`[Browser] Full update (fallback)`);
            const segments = payload.metadata?.segments || [];
            // Check if this navigation is stale (a newer one started)
            if (signal?.aborted) {
                console.log(`[Browser] Ignoring stale navigation (aborted)`);
                return streamComplete;
            }
            const segmentIds = segments.map((s) => s.id);
            // Render on client for consistent component references
            const newTree = await renderSegments(segments);
            // Final abort check before committing - another navigation may have started
            if (signal?.aborted) {
                console.log(`[Browser] Ignoring stale navigation (aborted before commit)`);
                return streamComplete;
            }
            // Commit navigation - transaction handles all store mutations atomically
            tx.commit(segmentIds, segments);
            // Emit update to trigger React render
            // For stale revalidation: wait for stream to complete, then update
            // For actions: wrap in startTransition to avoid UI flickering
            if (staleRevalidation) {
                await rawStreamComplete;
                startTransition(() => {
                    onUpdate({
                        root: newTree,
                        metadata: payload.metadata,
                    });
                });
            }
            else if (isAction) {
                startTransition(async () => {
                    onUpdate({
                        root: newTree,
                        metadata: payload.metadata,
                    });
                });
            }
            else {
                onUpdate({
                    root: newTree,
                    metadata: payload.metadata,
                });
            }
            return streamComplete;
        }
    }
    return fetchPartialUpdate;
}
export { createPartialUpdater as default };
//# sourceMappingURL=partial-update.js.map