// Polyfill Symbol.dispose for Safari and older browsers
if (typeof Symbol.dispose === "undefined") {
    Symbol.dispose = Symbol("Symbol.dispose");
}
if (typeof Symbol.asyncDispose === "undefined") {
    Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
}
// ============================================================================
// Default States
// ============================================================================
const DEFAULT_ACTION_STATE = {
    state: "idle",
    actionId: null,
    payload: null,
    error: null,
    result: null,
};
/**
 * Check if a subscription ID matches an action's full ID.
 *
 * When subscriptionId contains '#', it's a full ID and requires exact match.
 * When subscriptionId has no '#', it's just an action name and matches by suffix.
 * This allows useAction("addToCart") to match "hash#addToCart" or "src/file.ts#addToCart".
 */
function matchesActionId(subscriptionId, entryActionId) {
    if (subscriptionId.includes("#")) {
        // Full ID: exact match
        return subscriptionId === entryActionId;
    }
    // Action name only: suffix match (matches "anything#actionName")
    return entryActionId.endsWith(`#${subscriptionId}`);
}
/**
 * Create an event controller for managing navigation and action state
 *
 * The controller uses a reactive model where:
 * - Source of truth: currentNavigation, inflightActions, location
 * - Derived state: navState, isStreaming computed from source
 *
 * Navigation uses switchMap semantics (new nav cancels previous).
 * Actions use mergeMap semantics (all run concurrently, consolidate at end).
 */
export function createEventController(config) {
    // ========================================================================
    // Source of Truth
    // ========================================================================
    // Current navigation in progress (null = idle)
    let currentNavigation = null;
    // All in-flight actions (keyed by unique instance ID)
    const inflightActions = new Map();
    // Committed location (updated when navigation completes)
    let location = config?.initialLocation ??
        (typeof window !== "undefined"
            ? new URL(window.location.href)
            : new URL("/", "http://localhost"));
    // Track if any concurrent actions occurred (for consolidation)
    let hadAnyConcurrentActions = false;
    // Track segments revalidated by concurrent actions
    const concurrentRevalidatedSegments = new Set();
    // Active streaming count (independent of navigation/action lifecycle)
    let activeStreamCount = 0;
    // Handle data from RSC payload
    let handleData = {};
    let handleSegmentOrder = [];
    // ========================================================================
    // Listeners
    // ========================================================================
    const stateListeners = new Set();
    const actionListeners = new Map();
    const handleListeners = new Set();
    // Debounce state notifications to batch rapid updates
    let notifyTimeout = null;
    function notify() {
        if (notifyTimeout !== null) {
            clearTimeout(notifyTimeout);
        }
        notifyTimeout = setTimeout(() => {
            notifyTimeout = null;
            stateListeners.forEach((listener) => listener());
        }, 0);
    }
    // Debounce per-action notifications
    const actionNotifyTimeouts = new Map();
    function notifyAction(actionId) {
        const existing = actionNotifyTimeouts.get(actionId);
        if (existing !== undefined) {
            clearTimeout(existing);
        }
        actionNotifyTimeouts.set(actionId, setTimeout(() => {
            actionNotifyTimeouts.delete(actionId);
            // Notify all listeners whose subscription ID matches this action
            // This includes exact matches and suffix matches (e.g., "addToCart" matches "hash#addToCart")
            for (const [subscriptionId, listeners] of actionListeners) {
                if (matchesActionId(subscriptionId, actionId)) {
                    const state = getActionState(subscriptionId);
                    listeners.forEach((listener) => listener(state));
                }
            }
        }, 0));
    }
    // Debounce handle notifications
    let handleNotifyTimeout = null;
    function notifyHandles() {
        if (handleNotifyTimeout !== null) {
            clearTimeout(handleNotifyTimeout);
        }
        handleNotifyTimeout = setTimeout(() => {
            handleNotifyTimeout = null;
            handleListeners.forEach((listener) => listener());
        }, 0);
    }
    // ========================================================================
    // Derived State
    // ========================================================================
    function getState() {
        // Build inflight actions list (for compatibility with existing API)
        const inflightActionsList = [...inflightActions.values()]
            .filter((a) => a.phase !== "settling")
            .map((a) => ({
            id: a.id,
            actionId: a.actionId,
            payload: a.payload,
            startedAt: a.startedAt,
        }));
        // State: loading if navigation OR actions are in progress
        // Background revalidations (skipLoadingState) don't affect visible state
        const hasActiveActions = inflightActionsList.length > 0;
        const isVisibleNavigation = currentNavigation !== null &&
            !currentNavigation.options?.skipLoadingState;
        const state = isVisibleNavigation || hasActiveActions ? "loading" : "idle";
        // Streaming: true if any active streams (navigation or action) or loading
        const isStreaming = activeStreamCount > 0 || state === "loading";
        return {
            state,
            isStreaming,
            location,
            // pendingUrl only during fetching phase - once streaming starts (URL changed), not pending
            // Background revalidations don't expose a pending URL
            pendingUrl: currentNavigation?.phase === "fetching" &&
                !currentNavigation.options?.skipLoadingState
                ? currentNavigation.url
                : null,
            inflightActions: inflightActionsList,
        };
    }
    function getActionState(actionId) {
        // Find the most recent action with this ID that's not settling
        // Uses suffix matching when actionId is just a name (no #)
        const activeEntry = [...inflightActions.values()]
            .filter((a) => matchesActionId(actionId, a.actionId) && a.phase !== "settling")
            .sort((a, b) => b.startedAt - a.startedAt)[0];
        // Also check for settling entries to get result/error
        const settlingEntry = [...inflightActions.values()]
            .filter((a) => matchesActionId(actionId, a.actionId) && a.phase === "settling")
            .sort((a, b) => b.startedAt - a.startedAt)[0];
        const entry = activeEntry || settlingEntry;
        if (!entry) {
            return { ...DEFAULT_ACTION_STATE };
        }
        // Derive state from phase
        let state;
        switch (entry.phase) {
            case "fetching":
                state = "loading";
                break;
            case "streaming":
                state = "streaming";
                break;
            case "settling":
                state = "idle";
                break;
        }
        return {
            state,
            actionId: entry.actionId,
            payload: entry.payload,
            error: entry.error ?? null,
            result: entry.result ?? null,
        };
    }
    // ========================================================================
    // Navigation Operations
    // ========================================================================
    function startNavigation(url, options) {
        // Cancel existing navigation (switchMap semantics)
        if (currentNavigation) {
            currentNavigation.abort.abort();
            currentNavigation = null;
        }
        const abort = new AbortController();
        const entry = {
            url,
            abort,
            phase: "fetching",
            startedAt: Date.now(),
            options,
        };
        currentNavigation = entry;
        notify();
        let completed = false;
        return {
            abort,
            signal: abort.signal,
            get completed() {
                return completed;
            },
            startStreaming() {
                let ended = false;
                activeStreamCount++;
                notify();
                return {
                    end() {
                        if (ended)
                            return;
                        ended = true;
                        activeStreamCount = Math.max(0, activeStreamCount - 1);
                        notify();
                    },
                };
            },
            complete(newLocation) {
                if (currentNavigation === entry) {
                    completed = true;
                    location = newLocation;
                    currentNavigation = null;
                    notify();
                }
            },
            // Disposable: cleanup if not completed (e.g., error thrown)
            [Symbol.dispose]() {
                // If aborted by another navigation, don't touch state
                if (abort.signal.aborted)
                    return;
                // If not completed, reset to idle
                if (!completed && currentNavigation === entry) {
                    currentNavigation = null;
                    notify();
                }
            },
        };
    }
    function abortNavigation() {
        if (currentNavigation) {
            currentNavigation.abort.abort();
            currentNavigation = null;
            notify();
        }
    }
    function setLocation(newLocation) {
        location = newLocation;
        notify();
    }
    // ========================================================================
    // Action Operations
    // ========================================================================
    function startAction(actionId, args) {
        const id = `${actionId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
        const abort = new AbortController();
        // Track if this action started while others were pending (concurrent)
        const hadConcurrent = inflightActions.size > 0;
        if (hadConcurrent) {
            hadAnyConcurrentActions = true;
        }
        const entry = {
            id,
            actionId,
            abort,
            phase: "fetching",
            payload: args,
            revalidatedSegments: [],
            startedAt: Date.now(),
        };
        inflightActions.set(id, entry);
        notify();
        notifyAction(actionId);
        let settled = false;
        let streamingEnded = false;
        let actionCompleted = false;
        let pendingResult = null;
        function doSettle() {
            if (settled)
                return;
            settled = true;
            // Cleanup after brief delay (allow useAction to read result)
            setTimeout(() => {
                inflightActions.delete(id);
                // Check for consolidation
                if (inflightActions.size === 0) {
                    // All actions done - reset tracking
                    hadAnyConcurrentActions = false;
                    concurrentRevalidatedSegments.clear();
                }
                notify();
                notifyAction(actionId);
            }, 100);
        }
        // Called when both action is done AND streaming has ended
        function tryFinalize() {
            if (!actionCompleted || !streamingEnded)
                return;
            if (settled)
                return;
            // Apply the pending result
            if (pendingResult?.type === "error") {
                entry.error = pendingResult.value;
            }
            else if (pendingResult?.type === "success") {
                entry.result = pendingResult.value;
            }
            entry.phase = "settling";
            notify();
            notifyAction(actionId);
            doSettle();
        }
        return {
            id,
            abort,
            signal: abort.signal,
            hadConcurrentActions: hadConcurrent,
            get settled() {
                return settled;
            },
            startStreaming() {
                let ended = false;
                activeStreamCount++;
                entry.phase = "streaming";
                notify();
                notifyAction(actionId);
                return {
                    end() {
                        if (ended)
                            return;
                        ended = true;
                        streamingEnded = true;
                        activeStreamCount = Math.max(0, activeStreamCount - 1);
                        notify();
                        // Try to finalize if action was already completed
                        tryFinalize();
                    },
                };
            },
            recordRevalidatedSegments(segmentIds) {
                entry.revalidatedSegments.push(...segmentIds);
                segmentIds.forEach((id) => concurrentRevalidatedSegments.add(id));
            },
            complete(result) {
                if (!inflightActions.has(id) || settled)
                    return;
                actionCompleted = true;
                entry.completed = true;
                pendingResult = { type: "success", value: result };
                // If streaming never started or already ended, finalize immediately
                // Otherwise wait for streaming to end
                if (entry.phase === "fetching" || streamingEnded) {
                    streamingEnded = true; // Mark as ended if never started
                    tryFinalize();
                }
                // If streaming is in progress, tryFinalize() will be called when streaming ends
            },
            fail(error) {
                if (!inflightActions.has(id) || settled)
                    return;
                actionCompleted = true;
                entry.completed = true;
                pendingResult = { type: "error", value: error };
                // If streaming never started or already ended, finalize immediately
                // Otherwise wait for streaming to end
                if (entry.phase === "fetching" || streamingEnded) {
                    streamingEnded = true; // Mark as ended if never started
                    tryFinalize();
                }
                // If streaming is in progress, tryFinalize() will be called when streaming ends
            },
            getConsolidationSegments() {
                // Only consolidate if all actions have at least received their response
                // We don't need to wait for streaming to complete since we're refetching anyway
                // Count actions that are still fetching (waiting for server response)
                const stillFetchingCount = [...inflightActions.values()].filter((a) => a.phase === "fetching").length;
                if (stillFetchingCount > 0) {
                    return null; // Some actions still waiting for server response
                }
                if (!hadAnyConcurrentActions) {
                    return null; // No concurrent actions occurred
                }
                if (concurrentRevalidatedSegments.size === 0) {
                    return null; // No segments to consolidate
                }
                return Array.from(concurrentRevalidatedSegments);
            },
            clearConsolidation() {
                concurrentRevalidatedSegments.clear();
                hadAnyConcurrentActions = false;
            },
            // Disposable: cleanup if not settled (e.g., error thrown without calling fail)
            [Symbol.dispose]() {
                // If aborted, another navigation/error took over - don't touch state
                if (abort.signal.aborted) {
                    inflightActions.delete(id);
                    notify();
                    notifyAction(actionId);
                    return;
                }
                // If action was already completed, let the streaming token handle finalization
                // The action is legitimately waiting for streaming to end
                if (actionCompleted) {
                    return;
                }
                // If not settled and not completed, this is an error case - force finalize
                if (!settled && inflightActions.has(id)) {
                    actionCompleted = true;
                    streamingEnded = true;
                    tryFinalize();
                }
            },
        };
    }
    function abortAllActions() {
        for (const entry of inflightActions.values()) {
            entry.abort.abort();
        }
        inflightActions.clear();
        hadAnyConcurrentActions = false;
        concurrentRevalidatedSegments.clear();
        notify();
        // Notify all action listeners
        for (const actionId of actionListeners.keys()) {
            notifyAction(actionId);
        }
    }
    // ========================================================================
    // Handle Operations
    // ========================================================================
    /**
     * Filter segment IDs to only include routes and layouts.
     * Excludes parallels (contain .@) and loaders (contain D followed by digit).
     */
    function filterSegmentOrder(matched) {
        return matched.filter((id) => {
            if (id.includes(".@"))
                return false;
            if (/D\d+\./.test(id))
                return false;
            return true;
        });
    }
    function setHandleData(data, matched, isPartial) {
        const newSegmentOrder = filterSegmentOrder(matched ?? []);
        if (isPartial && newSegmentOrder.length > 0) {
            // Partial update: merge new data with existing
            for (const handleName of Object.keys(data)) {
                if (!handleData[handleName]) {
                    handleData[handleName] = {};
                }
                for (const segmentId of Object.keys(data[handleName])) {
                    handleData[handleName][segmentId] = data[handleName][segmentId];
                }
            }
            // Clean up data from segments no longer in the matched list
            for (const handleName of Object.keys(handleData)) {
                for (const segmentId of Object.keys(handleData[handleName])) {
                    if (!newSegmentOrder.includes(segmentId)) {
                        delete handleData[handleName][segmentId];
                    }
                }
            }
        }
        else {
            // Full update: replace all data
            handleData = data;
        }
        handleSegmentOrder = newSegmentOrder;
        notifyHandles();
    }
    function getHandleState() {
        return {
            data: handleData,
            segmentOrder: handleSegmentOrder,
        };
    }
    // ========================================================================
    // Subscriptions
    // ========================================================================
    function subscribe(listener) {
        stateListeners.add(listener);
        return () => stateListeners.delete(listener);
    }
    function subscribeToAction(actionId, listener) {
        let listeners = actionListeners.get(actionId);
        if (!listeners) {
            listeners = new Set();
            actionListeners.set(actionId, listeners);
        }
        listeners.add(listener);
        return () => {
            listeners.delete(listener);
            if (listeners.size === 0) {
                actionListeners.delete(actionId);
            }
        };
    }
    function subscribeToHandles(listener) {
        handleListeners.add(listener);
        return () => handleListeners.delete(listener);
    }
    // ========================================================================
    // Return Controller
    // ========================================================================
    return {
        // Navigation
        startNavigation,
        abortNavigation,
        // Actions
        startAction,
        abortAllActions,
        // State
        getState,
        getActionState,
        setLocation,
        // Handles
        setHandleData,
        getHandleState,
        // Subscriptions
        subscribe,
        subscribeToAction,
        subscribeToHandles,
        // Direct access
        getCurrentNavigation: () => currentNavigation,
        getInflightActions: () => inflightActions,
    };
}
// ============================================================================
// Singleton
// ============================================================================
let controllerInstance = null;
/**
 * Initialize the global event controller
 */
export function initEventController(config) {
    if (!controllerInstance) {
        controllerInstance = createEventController(config);
    }
    return controllerInstance;
}
/**
 * Get the global event controller
 */
export function getEventController() {
    if (!controllerInstance) {
        throw new Error("Event controller not initialized. Call initEventController first.");
    }
    return controllerInstance;
}
/**
 * Reset the controller instance (for testing)
 */
export function resetEventController() {
    controllerInstance = null;
}
//# sourceMappingURL=event-controller.js.map