/**
 * DerivedVariable resolution process:
 * 1. 'result-selector' (getOrRegisterDerivedVariableResult) - handle resolving dependencies, return a previous/cached result or signify that we need to refetch.
 * This one explicitly does not use 'nested' in its key to share results across different nested values.
 * 2. 'selector' (getOrRegisterDerivedVariableValue) - handle fetching if needed, waiting for task result if task response
 * This one also does NOT use 'nested', also shares results.
 * 3. 'nested-selector' (getOrRegisterDerivedVariable) - handle resolving to a nested value if 'nested' is set
 * This one DOES use 'nested' in its key to resolve the shared values to different nested values.
 */
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());
    });
};
/* eslint-disable react-hooks/exhaustive-deps */
import isEqual from 'lodash/isEqual';
import set from 'lodash/set';
import { nanoid } from 'nanoid';
import { useCallback, useMemo } from 'react';
import { isRecoilValue, selectorFamily, useRecoilValue, useSetRecoilState, } from 'recoil';
import { BehaviorSubject, Observable, from } from 'rxjs';
import { debounceTime, filter, share, switchMap, take } from 'rxjs/operators';
import { HTTP_METHOD, validateResponse } from '@darajs/ui-utils';
import { fetchTaskResult, request } from '../../api';
import { RequestExtrasSerializable } from '../../api/http';
import { TaskError } from '../../api/websocket';
import { handleAuthErrors } from '../../auth/auth';
import { getUniqueIdentifier } from '../../shared/utils/hashing';
import { normalizeRequest } from '../../shared/utils/normalization';
import useInterval from '../../shared/utils/use-interval';
import { isCondition, isResolvedDerivedVariable, isResolvedServerVariable, isResolvedSwitchVariable, isVariable, } from '../../types';
import { deferred, isDeferred } from '../utils/deferred';
// eslint-disable-next-line import/no-cycle
import { cleanArgs, getOrRegisterTrigger, resolveNested, resolveVariable, resolveVariableStatic } from './internal';
import { depsRegistry, getRegistryKey, selectorFamilyMembersRegistry, selectorFamilyRegistry, } from './store';
import { buildTriggerList, registerChildTriggers, resolveTriggerStatic } from './triggers';
export function isTaskResponse(dvResponse) {
    return 'task_id' in dvResponse;
}
/**
 * Fetch the value of a derived variable from the backend
 *
 * @param input Function inputs
 * - `cache`, the cache option for the derived variable.
 * - `extras`, request extras to be merged into the options
 * - `uid`, the uid of the derived variable
 * - `values`, values to pass in the request
 * - `wsClient`, websocket client
 */
export function fetchDerivedVariable(_a) {
    return __awaiter(this, arguments, void 0, function* ({ cache, extras, force_key, variableUid, values, wsClient, }) {
        const cacheControl = cache === null ?
            {
                'cache-control': 'none',
            }
            : undefined;
        const ws_channel = yield wsClient.getChannel();
        const res = yield request(`/api/core/derived-variable/${variableUid}`, {
            body: JSON.stringify({ values, ws_channel, force_key: force_key !== null && force_key !== void 0 ? force_key : null }),
            headers: Object.assign({}, cacheControl),
            method: HTTP_METHOD.POST,
        }, extras);
        yield handleAuthErrors(res, { authenticationFailureRedirect: 'login' });
        yield validateResponse(res, `Failed to fetch the derived variable with uid: ${variableUid}`);
        return res.json();
    });
}
/**
 * Add a debounced version of the fetch so as to not overload the backend on startup. This needs to be cached per uid
 * so that we only debounce calls to the same DerivedVariable. Debouncing is done with rxjs as everything here is
 * promise based and async
 */
const debouncedFetchSubjects = {};
const debouncedFetchCache = {};
function debouncedFetchDerivedVariable(_a) {
    return __awaiter(this, arguments, void 0, function* ({ variableUid, selectorKey, values, wsClient, extras, cache, force_key, }) {
        // If this is the first time this is called then set up a subject and return stream for this selector
        if (!debouncedFetchSubjects[selectorKey]) {
            debouncedFetchSubjects[selectorKey] = new BehaviorSubject(null);
            debouncedFetchCache[selectorKey] = debouncedFetchSubjects[selectorKey].pipe(filter((args) => !!args), debounceTime(10), switchMap((args) => from(fetchDerivedVariable(args))), share());
        }
        // Push the next set of args to the subject
        debouncedFetchSubjects[selectorKey].next({
            cache,
            extras,
            force_key,
            selectorKey,
            values,
            variableUid,
            wsClient,
        });
        // Return the debounced response from the backend
        return new Promise((resolve, reject) => {
            debouncedFetchCache[selectorKey].pipe(take(1)).subscribe(resolve, reject);
        });
    });
}
/**
 * Resolve a value to a format understood by the backend, resolving atoms to values.
 *
 * @param value value to resolve
 * @param getter recoil getter function
 */
export function resolveValue(value, getter) {
    if (isResolvedDerivedVariable(value)) {
        const resolvedValues = value.values.map((v) => resolveValue(v, getter));
        return Object.assign(Object.assign({}, value), { values: resolvedValues });
    }
    if (isResolvedSwitchVariable(value)) {
        return Object.assign(Object.assign({}, value), { value: resolveValue(value.value, getter), value_map: resolveValue(value.value_map, getter), default: resolveValue(value.default, getter) });
    }
    if (isResolvedServerVariable(value)) {
        return Object.assign(Object.assign({}, value), { sequence_number: isRecoilValue(value.sequence_number) ? getter(value.sequence_number) : value.sequence_number });
    }
    if (isCondition(value)) {
        return Object.assign(Object.assign({}, value), { 
            // at this point condition would've already resolved a variable to a recoil value
            variable: resolveValue(value.variable, getter) });
    }
    if (isRecoilValue(value)) {
        return getter(value);
    }
    return value;
}
/**
 * Recursively build a deps array from a list of values
 * Values not in deps array are replaced with empty array
 *
 * @param values array of primitives and ResolvedDerivedVariable objects
 * @param deps array of indexes of dependencies to get values from
 */
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export function getDeps(values, deps) {
    return values.map((val, idx) => {
        if (deps && !deps.includes(idx)) {
            return [];
        }
        if (isResolvedDerivedVariable(val)) {
            return getDeps(val.values, val.deps).flat();
        }
        if (isResolvedServerVariable(val)) {
            return val.sequence_number;
        }
        if (isResolvedSwitchVariable(val)) {
            // For switch variables, return the constituent parts as dependencies
            return getDeps([val.value, val.value_map, val.default], [0, 1, 2]).flat();
        }
        return val;
    });
}
/**
 * A helper hook that turns a DerivedVariable into its trigger index value.
 * Useful to subscribe a component to forced trigger updates.
 *
 * @param variable variable to use
 */
export function useTriggerIndex(variable) {
    return useRecoilValue(getOrRegisterTrigger(variable));
}
/**
 * Resolve a derived value from a list of dependant variables and their resolved values.
 *
 * Handles a dependency array, where in case 'relevant' (i.e. present in deps) values did not change,
 * an earlier result from the `depsRegistry` is returned.
 *
 * This is the core of the derived variable resolution logic, extracted so that it can be used in both
 * DerivedVariable and py_component logic.
 *
 * @param key unique key to look up `depsRegistry` entry from
 * @param variables dependant variables
 * @param deps list of relevant dependant variables, akin to useEffect dependency array
 * @param resolvedVariables resolved values of dependant variables - turned into primitives and Resolved forms
 * @param get getter function to resolve atoms to values
 * @param triggerList list of trigger info objects
 * @param triggers list of triggers to register as dependencies; first one is expected to be the self-trigger
 */
export function resolveDerivedValue({ key, variables, deps, resolvedVariables, resolutionStrategy, triggerList, triggers, }) {
    /**
     * Array of values:
     * - primitive values are resolved to themselves
     * - simple variables are resolved to their values
     * - derived variables are resolved to ResolvedDerivedVariable objects with nested values/deps resolved to values
     * - server variables are resolved to their sequence number
     */
    const values = resolvedVariables.map((v) => {
        if (resolutionStrategy.name === 'static') {
            return resolveVariableStatic(v, resolutionStrategy.snapshot, resolutionStrategy.params);
        }
        return resolveValue(v, resolutionStrategy.get);
    });
    /**
     * Array of values with ResolvedDerivedVariable objects replaced
     * (recursively) by nested arrays of their dependency values
     *
     * Dependencies which aren't included in `deps` are replaced with empty arrays
     */
    const depsValues = getDeps(values);
    /**
     * A map of variable UID-nested -> its primitive value (or array of dependencies).
     * Only includes variables present in deps
     *
     * `nested` is used to generate a key so that the same variable with different nested property will be
     * present in the map separately.
     */
    const variableValueMap = variables.reduce((acc, v, idx) => {
        // skip non-variables
        if (!isVariable(v)) {
            return acc;
        }
        return acc.set(getUniqueIdentifier(v), depsValues[idx]);
    }, new Map());
    /**
     * Deps handling
     */
    const previousEntry = depsRegistry.get(key);
    // Get relevant values based on deps
    const relevantValues = deps
        .filter(isVariable)
        .map((dep) => variableValueMap.get(getUniqueIdentifier(dep)))
        .concat(triggers.map((trigger) => trigger.inc)); // Append triggerValues to make triggers force a recalc even when deps didn't change
    // If there's no entry it's the first run so skip; otherwise:
    if (previousEntry) {
        const areArgsTheSame = isEqual(previousEntry.args, relevantValues);
        // If the relevant arg values didn't change, return previous result
        if (areArgsTheSame) {
            const previousValue = previousEntry.result;
            if (isDeferred(previousValue)) {
                return {
                    type: 'cached',
                    response: previousValue,
                    depsKey: key,
                    values,
                    relevantValues,
                    currentResult: {
                        relevantValues,
                        selfTriggerForceKey: null,
                        type: 'current',
                        values,
                        depsKey: key,
                    },
                };
            }
            return {
                entry: previousEntry,
                type: 'previous',
                values,
                relevantValues,
                depsKey: key,
            };
        }
        // Otherwise continue and fetch new value, but first check what caused the change
        // Assumption: Triggers are always stored after the values, with the triggers of the current variable being first
        const previousTriggerCounters = previousEntry.args.slice(previousEntry.args.length - triggers.length);
        // Find which trigger changed and handle force_key appropriately
        let selfTriggerForceKey = null;
        for (const [idx, triggerValue] of triggers.entries()) {
            /**
             *  If any of the nested trigger value has changed (this execution was caused by a trigger)
             *  handle the force_key appropriately
             */
            if (triggerValue.inc !== previousTriggerCounters[idx]) {
                if (idx === 0) {
                    // This is a self-trigger (polling, manual trigger) - use global force
                    selfTriggerForceKey = triggerValue.force_key;
                }
                else if (triggerValue.force_key) {
                    // This is a nested variable trigger - embed force_key in the specific variable
                    // shift index back by 1 because we prepended a self trigger
                    const valueIndex = idx - 1;
                    const triggerInfo = triggerList[valueIndex];
                    // If the trigger path is empty, it means we need to force the DV itself
                    // This can happen when e.g. a dependant DataVariable is triggered
                    if (triggerInfo.path.length === 0) {
                        selfTriggerForceKey = triggerValue.force_key;
                    }
                    else {
                        set(values, [...triggerInfo.path, 'force_key'], triggerValue.force_key);
                    }
                }
                break;
            }
        }
        return {
            relevantValues,
            selfTriggerForceKey,
            type: 'current',
            values,
            depsKey: key,
        };
    }
    return {
        relevantValues,
        selfTriggerForceKey: null,
        type: 'current',
        values,
        depsKey: key,
    };
}
/**
 * Get or register a recoil selector for a given derived variable result object.
 * Resolves the primitive values and returns either the previous cached result or an object
 * signifying that we need to refetch the value.
 *
 * First of the 3 stages of resolving a derived variable.
 */
export function getOrRegisterDerivedVariableResult(variable, wsClient, taskContext, currentExtras) {
    const key = getRegistryKey(variable, 'result-selector');
    if (!selectorFamilyRegistry.has(key)) {
        getOrRegisterTrigger(variable);
        // register a family for this particular variable
        selectorFamilyRegistry.set(key, selectorFamily({
            cachePolicy_UNSTABLE: {
                eviction: 'most-recent',
            },
            get: (extrasSerializable) => (_a) => __awaiter(this, [_a], void 0, function* ({ get }) {
                /**
                 * Recursively resolve variables to list of values
                 *
                 * For derived variables, put ResolvedDerivedVariable object with values resolved to recoil atoms/selectors,
                 * and deps resolved to array of indexes of variables (or null if not set)
                 */
                const resolvedVariables = yield Promise.all(variable.variables.map((v) => __awaiter(this, void 0, void 0, function* () {
                    // Handle non-variables - plain values could be injected via LoopVariable
                    if (!isVariable(v)) {
                        return Promise.resolve(v);
                    }
                    return resolveVariable(v, wsClient, taskContext, extrasSerializable.extras);
                })));
                const selfTrigger = get(getOrRegisterTrigger(variable));
                // for deps use a different key for each selector instance rather than one per family
                const selectorKey = key + extrasSerializable.toJSON();
                // Build trigger map once for efficient lookups
                const triggerList = buildTriggerList(variable.variables);
                // Register nested triggers as dependencies so triggering one of the nested derived variables will trigger a recalculation here
                const triggers = registerChildTriggers(triggerList, get);
                triggers.unshift(selfTrigger);
                const derivedResult = resolveDerivedValue({
                    key: selectorKey,
                    variables: variable.variables,
                    deps: variable.deps,
                    resolvedVariables,
                    resolutionStrategy: { name: 'get', get },
                    triggerList,
                    triggers,
                });
                return derivedResult;
            }),
            key: nanoid(),
        }));
    }
    const family = selectorFamilyRegistry.get(key);
    // Get a selector instance for this particular extras value
    // This is required as otherwise the selector is not aware of different possible extras values
    // at the call site of e.g. useVariable and would otherwise be a stale closure using the initial extras when
    // first registered
    const serializableExtras = new RequestExtrasSerializable(currentExtras);
    const selectorInstance = family(serializableExtras);
    // register selector instance in the selector family registry
    if (!selectorFamilyMembersRegistry.has(family)) {
        selectorFamilyMembersRegistry.set(family, new Map());
    }
    selectorFamilyMembersRegistry.get(family).set(serializableExtras.toJSON(), selectorInstance);
    return selectorInstance;
}
const NOT_SET = Symbol('NOT_SET');
/**
 * Get a derived variable from the selector registry, registering it if not already registered
 *  Handles the 'derived result' returned from the first stage selector, fetching the value
 *  if value is 'current', awaiting cached deferred values and waiting for task results if needed.
 *
 * Second of the 3 stages of resolving a derived variable.
 */
export function getOrRegisterDerivedVariableValue(variable, wsClient, taskContext, currentExtras) {
    const key = getRegistryKey(variable, 'derived-selector');
    if (!selectorFamilyRegistry.has(key)) {
        getOrRegisterTrigger(variable);
        // register a family for this particular variable
        selectorFamilyRegistry.set(key, selectorFamily({
            cachePolicy_UNSTABLE: {
                eviction: 'most-recent',
            },
            get: (extrasSerializable) => (_a) => __awaiter(this, [_a], void 0, function* ({ get }) {
                const selectorKey = key + extrasSerializable.toJSON();
                const throwError = (error) => {
                    // On DV task error put selectorId and extras into the error so the boundary can reset the selector cache
                    error.selectorId = key;
                    error.selectorExtras = extrasSerializable.toJSON();
                    throw error;
                };
                const handleError = (func) => {
                    try {
                        return func();
                    }
                    catch (e) {
                        throwError(e);
                    }
                };
                // get the result selector instance for this extras value
                const dvResultSelector = getOrRegisterDerivedVariableResult(variable, wsClient, taskContext, extrasSerializable.extras);
                let derivedResult = get(dvResultSelector);
                if (derivedResult.type === 'previous') {
                    return derivedResult.entry.result;
                }
                const { extras } = extrasSerializable;
                let variableResponse = null;
                // whether to check for an initial task
                let shouldFetchTask = false;
                // Skip fetching if we have a cached result, await it instead
                if (derivedResult.type === 'cached') {
                    try {
                        const response = yield derivedResult.response.getValue();
                        shouldFetchTask = true;
                        if (!response.ok) {
                            throwError(new Error(response.value));
                        }
                        variableResponse = response.value;
                        derivedResult = derivedResult.currentResult;
                    }
                    catch (e) {
                        throwError(e);
                    }
                }
                else {
                    variableResponse = yield handleError(() => debouncedFetchDerivedVariable({
                        cache: variable.cache,
                        extras,
                        force_key: derivedResult.selfTriggerForceKey,
                        selectorKey,
                        values: normalizeRequest(cleanArgs(derivedResult.values), variable.variables),
                        variableUid: variable.uid,
                        wsClient,
                    }));
                }
                let variableValue = NOT_SET;
                // If there is a task running related to the current variable then something has changed, so cancel them
                taskContext.cleanupRunningTasks(variable.uid);
                // If the variable is computed as a task then wait for it to finish and fetch the result
                if (isTaskResponse(variableResponse)) {
                    const taskId = variableResponse.task_id;
                    // pre-fetch task result since it could already be available without us receiving the notif
                    if (shouldFetchTask) {
                        try {
                            const taskResult = yield fetchTaskResult(taskId, extras);
                            if (taskResult.status === 'ok') {
                                variableValue = taskResult.result;
                            }
                        }
                        catch (e) {
                            throwError(e);
                        }
                    }
                    // continue waiting for the task if it's not available yet
                    if (variableValue === NOT_SET) {
                        // register task being started
                        taskContext.startTask(taskId, variable.uid, getRegistryKey(variable, 'trigger'));
                        try {
                            yield wsClient.waitForTask(taskId);
                        }
                        catch (e) {
                            if (e instanceof TaskError) {
                                throwError(e);
                            }
                            // should be a TaskCancelledError
                            // It should be safe to return `null` here as the selector will re-run and throw suspense again
                            return null;
                        }
                        finally {
                            taskContext.endTask(taskId);
                        }
                        variableValue = yield handleError(() => __awaiter(this, void 0, void 0, function* () {
                            const result = yield fetchTaskResult(taskId, extras);
                            if (result.status === 'not_found') {
                                throw new Error('Task result not found');
                            }
                            return result.result;
                        }));
                    }
                }
                else {
                    variableValue = variableResponse.value;
                }
                // Store the final result and arguments used
                depsRegistry.set(derivedResult.depsKey, {
                    args: derivedResult.relevantValues,
                    result: variableValue,
                });
                return variableValue;
            }),
            key: nanoid(),
        }));
    }
    const family = selectorFamilyRegistry.get(key);
    // Get a selector instance for this particular extras value
    // This is required as otherwise the selector is not aware of different possible extras values
    // at the call site of e.g. useVariable and would otherwise be a stale closure using the initial extras when
    // first registered
    const serializableExtras = new RequestExtrasSerializable(currentExtras);
    const selectorInstance = family(serializableExtras);
    // register selector instance in the selector family registry
    if (!selectorFamilyMembersRegistry.has(family)) {
        selectorFamilyMembersRegistry.set(family, new Map());
    }
    selectorFamilyMembersRegistry.get(family).set(serializableExtras.toJSON(), selectorInstance);
    return selectorInstance;
}
/**
 * Resolve the nested value of a DerivedVariable if needed.
 * Unwraps the raw result of a DerivedVariable.
 *
 * Third of the 3 stages of resolving a derived variable. Unwraps the 'nested' value
 * if set on the variable.
 *
 * @param variable variable to register
 * @param wsClient WebSocket client from context
 * @param taskContext global task context
 * @param search search query from location
 * @param extras request extras to be merged into the options
 */
export function getOrRegisterDerivedVariable(variable, wsClient, taskContext, currentExtras) {
    const key = getRegistryKey(variable, 'selector-nested');
    if (!selectorFamilyRegistry.has(key)) {
        selectorFamilyRegistry.set(key, selectorFamily({
            get: (extrasSerializable) => ({ get }) => {
                // get the right selector instance for this extras value
                const dvSelector = getOrRegisterDerivedVariableValue(variable, wsClient, taskContext, extrasSerializable.extras);
                const value = get(dvSelector);
                // unwrap the nested result if needed
                return 'nested' in variable ? resolveNested(value, variable.nested) : value;
            },
            key: nanoid(),
        }));
    }
    const family = selectorFamilyRegistry.get(key);
    // Get a selector instance for this particular extras value
    // This is required as otherwise the selector is not aware of different possible extras values
    // at the call site of e.g. useVariable and would otherwise be a stale closure using the initial extras when
    // first registered
    const serializableExtras = new RequestExtrasSerializable(currentExtras);
    const selectorInstance = family(serializableExtras);
    // register selector instance in the selector family registry
    if (!selectorFamilyMembersRegistry.has(family)) {
        selectorFamilyMembersRegistry.set(family, new Map());
    }
    selectorFamilyMembersRegistry.get(family).set(serializableExtras.toJSON(), selectorInstance);
    return selectorInstance;
}
/**
 * Preload a derived value.
 * Replicates the logic of normal resolution but resolving the values
 * from snapshot or falling back to the defaults.
 *
 * For details, see the original implementation comments.
 *
 * Returns null if there already is a cached value for the variable.
 * Otherwise, returns an object with values so be sent to the backend
 * and handles to resolve the cached promise to the result.
 */
export function preloadDerivedValue({ key, variables, deps, triggerList, triggers, snapshot, params, }) {
    const derivedResult = resolveDerivedValue({
        key,
        variables,
        deps,
        resolvedVariables: variables,
        resolutionStrategy: { name: 'static', snapshot, params },
        triggerList,
        triggers,
    });
    // otherwise we already have a valid entry, nothing to do here
    if (derivedResult.type === 'previous') {
        return null;
    }
    // no previous entry or values changed - have to recompute
    return { result: derivedResult, handle: deferred() };
}
/**
 * Preload a derived variable value.
 */
export function preloadDerivedVariable(variable, snapshot, params) {
    // assume no extras in top-level variables
    const key = getRegistryKey(variable, 'result-selector') + new RequestExtrasSerializable({}).toJSON();
    // prepare trigger list as usual but use static resolution
    const triggerList = [...buildTriggerList(variable.variables), { path: [], variable }];
    const triggers = triggerList.map((ti) => resolveTriggerStatic(getOrRegisterTrigger(ti.variable), snapshot));
    return preloadDerivedValue({
        key,
        variables: variable.variables,
        deps: variable.deps,
        triggers,
        triggerList,
        snapshot,
        params,
    });
}
/**
 * Helper hook to get the (data) derived variable selector, while correctly registering triggers and setting up polling interval.
 *
 * @param variable derived variable to use
 * @param WsClient websocket client instance
 * @param taskContext global task context
 * @param search search query
 * @param extras request extras to be merged into the options
 */
export function useDerivedVariable(variable, WsClient, taskContext, extras, pollingInterval) {
    const dvSelector = getOrRegisterDerivedVariable(variable, WsClient, taskContext, extras);
    /**
     * Workaround for forcing a re-calculation for derived variables by creating a triggerIndex atom and making it a dependency of
     * the recoil selector. This way the selector can be re-run and derived variable can be re-fetched from the
     * backend by just updating this atom.
     */
    const triggerIndex = useMemo(() => getOrRegisterTrigger(variable), []);
    // Creating a setter function for triggerIndex
    const triggerUpdates = useSetRecoilState(triggerIndex);
    const trigger = useCallback((force = true) => triggerUpdates((val) => ({
        force_key: force ? nanoid() : null,
        inc: val.inc + 1,
    })), [triggerUpdates]);
    // Using useInterval to poll, forcing recalculation everytime
    useInterval(trigger, pollingInterval);
    return dvSelector;
}
//# sourceMappingURL=derived-variable.js.map