var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
/**
 * StreamVariable implementation.
 *
 * StreamVariable receives events via Server-Sent Events (SSE) and accumulates
 * them on the client side. The accumulated state is exposed via Recoil atoms.
 *
 * Architecture:
 * 1. Params selector - resolves dependencies, returns serialized atom key
 * 2. Value selector - reads params, gets atom via getOrCreateStreamVariableAtom, reads value
 * 3. Atom with effects - SSE lifecycle managed by atom effects
 *
 * Event types (keyed mode - requires key_accessor):
 * - replace: Atomically replace all items (recommended for initial state)
 * - add: Add/update items by key
 * - remove: Remove items by key
 * - clear: Clear all items
 *
 * Event types (custom state mode):
 * - json_snapshot: Replace entire state with arbitrary JSON
 * - json_patch: Apply JSON Patch operations (RFC 6902)
 *
 * Control events:
 * - reconnect: Signal to reconnect
 * - error: Signal stream error
 */
import { fetchEventSource } from '@microsoft/fetch-event-source';
import { applyPatch } from 'fast-json-patch';
import getPath from 'lodash/get';
import { nanoid } from 'nanoid';
import { atomFamily, selectorFamily } from 'recoil';
import { HTTP_METHOD } from '@darajs/ui-utils';
import {} from '../../api';
import { RequestExtrasSerializable, request } from '../../api/http';
import { handleAuthErrors } from '../../auth/auth';
import { isVariable } from '../../types';
import { getUniqueIdentifier } from '../utils/hashing';
import { normalizeRequest } from '../utils/normalization';
// eslint-disable-next-line import/no-cycle
import { resolveNested, resolveValue, resolveVariable } from './internal';
import {} from './store';
import { registerStreamConnection } from './stream-usage-tracker';
import { buildTriggerList, registerChildTriggers } from './triggers';
/**
 * Local registry for stream selector families.
 * Only stores families (factory functions), NOT instances.
 * This allows Recoil to properly release instances when components unmount.
 */
const streamSelectorFamilyRegistry = new Map();
/**
 * Extract a key from an item using the key_accessor path.
 *
 * @param item Item to extract key from
 * @param keyAccessor Dot-separated path to the key property (e.g., 'id' or 'data.id')
 * @returns The key value, or undefined if not found
 */
export function extractKey(item, keyAccessor) {
    if (item === null || item === undefined) {
        return undefined;
    }
    const key = getPath(item, keyAccessor);
    if (typeof key === 'string' || typeof key === 'number') {
        return key;
    }
    return undefined;
}
/**
 * Apply a stream event to the current state.
 *
 * @param currentState Current accumulated state
 * @param event Stream event to apply
 * @param keyAccessor Key accessor path for keyed mode (null for custom mode)
 * @returns New state after applying the event
 */
export function applyStreamEvent(currentState, event, keyAccessor) {
    var _a, _b;
    switch (event.type) {
        // === Keyed mode events ===
        case 'add': {
            if (keyAccessor === null) {
                // eslint-disable-next-line no-console
                console.warn('StreamVariable: add() event received but no key_accessor is set. Use json_snapshot/json_patch instead.');
                return currentState;
            }
            const items = Array.isArray(event.data) ? event.data : [event.data];
            let newData = ((_a = currentState.data) !== null && _a !== void 0 ? _a : {});
            for (const item of items) {
                const key = extractKey(item, keyAccessor);
                if (key === undefined) {
                    // eslint-disable-next-line no-console
                    console.warn(`StreamVariable: Could not extract key using accessor '${keyAccessor}' from item:`, item);
                    continue;
                }
                newData = Object.assign(Object.assign({}, newData), { [key]: item });
            }
            return Object.assign(Object.assign({}, currentState), { data: newData, status: 'connected', error: undefined });
        }
        case 'remove': {
            if (keyAccessor === null) {
                // eslint-disable-next-line no-console
                console.warn('StreamVariable: remove() event received but no key_accessor is set. Use json_snapshot/json_patch instead.');
                return currentState;
            }
            const keys = Array.isArray(event.data) ? event.data : [event.data];
            const currentData = ((_b = currentState.data) !== null && _b !== void 0 ? _b : {});
            const newData = Object.assign({}, currentData);
            for (const key of keys) {
                if (typeof key === 'string' || typeof key === 'number') {
                    delete newData[key];
                }
            }
            return Object.assign(Object.assign({}, currentState), { data: newData, status: 'connected', error: undefined });
        }
        case 'clear': {
            return Object.assign(Object.assign({}, currentState), { data: {}, status: 'connected', error: undefined });
        }
        case 'replace': {
            if (keyAccessor === null) {
                // eslint-disable-next-line no-console
                console.warn('StreamVariable: replace() event received but no key_accessor is set. Use json_snapshot instead.');
                return currentState;
            }
            const items = Array.isArray(event.data) ? event.data : [];
            const newData = {};
            for (const item of items) {
                const key = extractKey(item, keyAccessor);
                if (key === undefined) {
                    // eslint-disable-next-line no-console
                    console.warn(`StreamVariable: Could not extract key using accessor '${keyAccessor}' from item:`, item);
                    continue;
                }
                newData[key] = item;
            }
            return Object.assign(Object.assign({}, currentState), { data: newData, status: 'connected', error: undefined });
        }
        // === Custom state mode events ===
        case 'json_snapshot':
            return Object.assign(Object.assign({}, currentState), { data: event.data, status: 'connected', error: undefined });
        case 'json_patch': {
            if (currentState.data === undefined || currentState.data === null) {
                // eslint-disable-next-line no-console
                console.warn('StreamVariable: json_patch event received but no state exists. Send a json_snapshot first.');
                return currentState;
            }
            try {
                const operations = event.data;
                const { newDocument } = applyPatch(currentState.data, operations, true, false);
                return Object.assign(Object.assign({}, currentState), { data: newDocument, status: 'connected', error: undefined });
            }
            catch (e) {
                // eslint-disable-next-line no-console
                console.error('StreamVariable: Failed to apply json_patch:', e);
                return Object.assign(Object.assign({}, currentState), { status: 'error', error: `Patch failed: ${String(e)}` });
            }
        }
        // === Control events ===
        case 'reconnect':
            return Object.assign(Object.assign({}, currentState), { status: 'reconnecting' });
        case 'error':
            return Object.assign(Object.assign({}, currentState), { status: 'error', error: typeof event.data === 'string' ? event.data : 'Unknown error' });
        default:
            // eslint-disable-next-line no-console
            console.warn(`StreamVariable: Unknown event type: ${event.type}`);
            return currentState;
    }
}
/**
 * Get the exposed value from stream state.
 * For keyed mode, converts the internal Record to an array (values in insertion order).
 *
 * @param state Stream state
 * @param keyAccessor Key accessor (null for custom mode)
 * @returns The value to expose to components
 */
export function getStreamValue(state, keyAccessor) {
    if (keyAccessor !== null && state.data !== null && typeof state.data === 'object' && !Array.isArray(state.data)) {
        // Keyed mode: expose as array
        return Object.values(state.data);
    }
    return state.data;
}
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1000;
const MAX_DELAY_MS = 30000;
/**
 * Calculate exponential backoff delay.
 */
export function getBackoffDelay(retryCount) {
    return Math.min(BASE_DELAY_MS * Math.pow(2, retryCount), MAX_DELAY_MS);
}
export function serializeAtomParams(params) {
    return JSON.stringify(params);
}
export function deserializeAtomParams(key) {
    return JSON.parse(key);
}
/** Stream state after first data arrives */
const INITIAL_CONNECTED_STATE = {
    data: undefined,
    status: 'connected',
};
/**
 * Start an SSE connection for a stream variable.
 *
 * @param params Stream atom params (contains uid, resolved values, etc.)
 * @param callbacks Callbacks for stream events
 * @returns Object with cleanup function and AbortController
 */
function startStreamConnection(params, callbacks) {
    const controller = new AbortController();
    let retryCount = 0;
    let isFirstMessage = true;
    let currentState = INITIAL_CONNECTED_STATE;
    // Normalize values for the request
    const normalizedValues = normalizeRequest(params.resolvedValues, params.variables);
    fetchEventSource(`/api/core/stream/${params.uid}`, Object.assign(Object.assign({ method: HTTP_METHOD.POST, body: JSON.stringify({ values: normalizedValues }), signal: controller.signal, 
        // eslint-disable-next-line @typescript-eslint/require-await
        onopen: (response) => __awaiter(this, void 0, void 0, function* () {
            if (response.ok) {
                retryCount = 0; // Reset retry count on successful connection
                return;
            }
            const handledAuthError = yield handleAuthErrors(response, { authenticationFailureRedirect: 'login' });
            if (handledAuthError) {
                controller.abort();
            }
            const error = new Error(`Stream request failed: ${response.status} ${response.statusText}`);
            throw error;
        }), onmessage: (msg) => {
            let event;
            try {
                event = JSON.parse(msg.data);
            }
            catch (parseError) {
                // eslint-disable-next-line no-console
                console.error('Failed to parse SSE event:', parseError, msg.data);
                return;
            }
            // Handle reconnect event - throw to trigger retry logic
            if (event.type === 'reconnect') {
                // eslint-disable-next-line no-console
                console.info('StreamVariable: Server requested reconnect, reconnecting...');
                throw new Error('Server requested reconnect');
            }
            // Apply event to current state (accumulate)
            currentState = applyStreamEvent(currentState, event, params.keyAccessor);
            if (isFirstMessage) {
                isFirstMessage = false;
                callbacks.onFirstData(currentState);
            }
            else {
                callbacks.onUpdate(currentState);
            }
        }, onerror: (err) => {
            if (controller.signal.aborted) {
                // Connection was intentionally aborted, don't retry
                return;
            }
            retryCount++;
            if (retryCount > MAX_RETRIES) {
                callbacks.onError(err instanceof Error ? err.message : String(err));
                // Return undefined to stop retrying
                return;
            }
            // Return delay to retry
            const delay = getBackoffDelay(retryCount - 1);
            // eslint-disable-next-line no-console
            console.warn(`Stream connection failed, retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES})...`);
            return delay;
        }, onclose: () => {
            // Stream closed normally - could be server shutting down
            // The library will attempt to reconnect
        }, 
        // Use our request wrapper for auth headers
        // @ts-expect-error - fetch signature differs slightly but works
        fetch: request }, params.extras), { 
        // @ts-expect-error - Headers type doesn't match exactly
        headers: params.extras.headers }));
    // Return cleanup function and controller
    return {
        cleanup: () => {
            controller.abort();
        },
        controller,
    };
}
/**
 * Atom effect that manages SSE connection lifecycle.
 * Uses recoil-sync pattern: setSelf(promise) to enable native Recoil Suspense.
 *
 * SSE connection starts immediately when the atom effect runs.
 * Cleanup is managed by the stream usage tracker based on component subscriptions,
 * NOT by Recoil's atom release (which is unreliable).
 */
function streamConnectionEffect(atomKey) {
    return ({ setSelf }) => {
        // Deserialize params from atom key
        const params = deserializeAtomParams(atomKey);
        // Create a Promise that resolves when first data arrives.
        // Following recoil-sync pattern: setSelf(promise) enables native Suspense.
        let resolveInitialData = null;
        let rejectInitialData = null;
        const initialDataPromise = new Promise((resolve, reject) => {
            resolveInitialData = resolve;
            rejectInitialData = reject;
        });
        // Set atom to the Promise - Recoil will handle Suspense natively
        setSelf(initialDataPromise);
        // Create start function that the tracker will call (and can re-call on restart)
        const startConnection = () => {
            return startStreamConnection(params, {
                onFirstData: (state) => {
                    // Resolve the initial Promise - this wakes up suspended components
                    if (resolveInitialData) {
                        resolveInitialData(state);
                        resolveInitialData = null;
                        rejectInitialData = null;
                    }
                    // Also explicitly set the atom value to ensure Recoil sees the update
                    setSelf(state);
                },
                onUpdate: (state) => {
                    // Subsequent updates use setSelf directly
                    setSelf(state);
                },
                onError: (error) => {
                    if (rejectInitialData) {
                        // If we haven't received first data yet, reject the Promise
                        rejectInitialData(new Error(error));
                        resolveInitialData = null;
                        rejectInitialData = null;
                    }
                    else {
                        // Otherwise update the state with error
                        setSelf({
                            data: undefined,
                            status: 'error',
                            error,
                        });
                    }
                },
            });
        };
        // Register with stream usage tracker - it starts SSE immediately and manages restarts
        // The tracker is the primary cleanup mechanism, not Recoil's atom release
        // Keyed by uid+extras so different auth contexts are independent
        const unregister = registerStreamConnection(params.uid, params.extras, atomKey, startConnection);
        // Return unregister as backup cleanup (in case Recoil does release the atom)
        return unregister;
    };
}
/**
 * Stream atom family - holds the accumulated state for each unique params combination.
 * Key is JSON-serialized StreamAtomParams containing everything needed for SSE.
 * No default value - atom starts in "pending" state. The effect sets it to a Promise
 * that resolves when first data arrives (recoil-sync pattern for native Suspense).
 *
 * SSE lifecycle is managed by the stream usage tracker, not Recoil's retention.
 * The tracker uses component-level subscription counting to pause/resume SSE connections.
 */
const streamAtomFamily = atomFamily({
    key: 'StreamVariable/state',
    effects: (atomKey) => [streamConnectionEffect(atomKey)],
});
/**
 * Get or create a stream variable atom for the given params.
 * The atom has effects that manage the SSE connection lifecycle.
 * Uses Recoil's atomFamily internal caching - no external registry needed.
 *
 * @param atomKey Serialized StreamAtomParams
 * @returns Recoil atom for stream state
 */
export function getOrCreateStreamVariableAtom(atomKey) {
    // atomFamily internally caches atoms by key - no need for external registry
    return streamAtomFamily(atomKey);
}
/**
 * Registry key for stream variable selectors.
 */
function getStreamRegistryKey(variable, suffix) {
    return `StreamVariable:${getUniqueIdentifier(variable, { useNested: false })}:${suffix}`;
}
/**
 * Get or register the params selector for a StreamVariable.
 * Resolves dependencies and returns serialized atom params.
 *
 * @param variable StreamVariable definition
 * @param client WebSocket client (for resolving dependencies)
 * @param taskContext Global task context
 * @param extras Request extras
 * @returns Recoil selector that resolves to serialized atom params (string)
 */
function getOrRegisterStreamVariableParams(variable, client, taskContext, extras) {
    const key = getStreamRegistryKey(variable, 'params-selector');
    if (!streamSelectorFamilyRegistry.has(key)) {
        streamSelectorFamilyRegistry.set(key, selectorFamily({
            key: nanoid(),
            get: (extrasSerializable) => (_a) => __awaiter(this, [_a], void 0, function* ({ get }) {
                // Resolve all dependency variables to Recoil atoms/ResolvedDerivedVariable/etc
                const resolvedVariables = yield Promise.all(variable.variables.map((v) => __awaiter(this, void 0, void 0, function* () {
                    if (!isVariable(v)) {
                        return v;
                    }
                    return resolveVariable(v, client, taskContext, extrasSerializable.extras);
                })));
                // Build trigger list and register child triggers as dependencies
                // This ensures we re-run when any nested DerivedVariable is triggered
                const triggerList = buildTriggerList(variable.variables);
                registerChildTriggers(triggerList, get);
                // Get primitive values from resolved variables
                const resolvedValues = resolvedVariables.map((v) => resolveValue(v, get));
                // Create and serialize atom params
                const params = {
                    uid: variable.uid,
                    resolvedValues,
                    variables: variable.variables,
                    keyAccessor: variable.key_accessor,
                    extras: extrasSerializable.extras,
                };
                return serializeAtomParams(params);
            }),
        }));
    }
    // Get instance from family - selectorFamily caches internally, no need for external instance registry
    const family = streamSelectorFamilyRegistry.get(key);
    return family(new RequestExtrasSerializable(extras));
}
/**
 * Get or register the value selector for a StreamVariable.
 * Reads params from params selector, gets atom, returns raw stream value.
 * Does NOT handle nested resolution - that's done by the outer selector.
 *
 * Layer 2 of 3.
 */
function getOrRegisterStreamVariableValue(variable, client, taskContext, extras) {
    // NO nested in key - share across different nested paths
    const key = getStreamRegistryKey(variable, 'value-selector');
    if (!streamSelectorFamilyRegistry.has(key)) {
        streamSelectorFamilyRegistry.set(key, selectorFamily({
            key: nanoid(),
            get: (extrasSerializable) => ({ get }) => {
                // Get params from params selector
                const paramsSelector = getOrRegisterStreamVariableParams(variable, client, taskContext, extrasSerializable.extras);
                const atomKey = get(paramsSelector);
                // Get atom from atomFamily - it caches internally
                const atom = streamAtomFamily(atomKey);
                const streamState = get(atom);
                // Get the exposed value (array for keyed mode)
                return getStreamValue(streamState, variable.key_accessor);
            },
        }));
    }
    // Get instance from family - selectorFamily caches internally, no need for external instance registry
    const family = streamSelectorFamilyRegistry.get(key);
    return family(new RequestExtrasSerializable(extras));
}
/**
 * Get or register selector for a StreamVariable.
 * Handles nested resolution if 'nested' is set.
 *
 * Layer 3 of 3.
 *
 * @param variable StreamVariable definition
 * @param client WebSocket client (for resolving dependencies)
 * @param taskContext Global task context
 * @param extras Request extras
 * @returns Recoil selector that resolves to the stream value
 */
export function getOrRegisterStreamVariable(variable, client, taskContext, extras) {
    // Key USES nested - different selector per nested path
    const key = `StreamVariable:${getUniqueIdentifier(variable, { useNested: true })}:nested-selector`;
    if (!streamSelectorFamilyRegistry.has(key)) {
        streamSelectorFamilyRegistry.set(key, selectorFamily({
            key: nanoid(),
            get: (extrasSerializable) => ({ get }) => {
                // Get value from layer 2
                const valueSelector = getOrRegisterStreamVariableValue(variable, client, taskContext, extrasSerializable.extras);
                const value = get(valueSelector);
                // Unwrap nested if needed
                return 'nested' in variable ?
                    resolveNested(value, variable.nested)
                    : value;
            },
        }));
    }
    // Get instance from family - selectorFamily caches internally, no need for external instance registry
    const family = streamSelectorFamilyRegistry.get(key);
    return family(new RequestExtrasSerializable(extras));
}
/**
 * Internal exports for testing
 */
export const _internal = {
    streamAtomFamily,
    getBackoffDelay,
    serializeAtomParams,
    deserializeAtomParams,
    startStreamConnection,
};
//# sourceMappingURL=stream-variable.js.map