import { action, computed, makeObservable, reaction, } from 'mobx';
import { CollectionOptimisticActionsState } from './CollectionOptimisticActionsState';
import { CollectionOptimisticProcessor } from './CollectionOptimisticProcessor';
import { OptimisticPatch, } from './CollectionOptimisticPatch';
import { CollectionOptimisticExecutor } from './CollectionOptimisticExecutor';
import { EventEmitter } from 'events';
import { isHttpError } from '../util';
import { QueryResultSimulationDefault } from './QueryResultSimulationDefault';
import { CollectionOptimisticSequenceExecutor } from './CollectionOptimisticSequenceExecutor';
import merge from 'lodash/merge';
export class CollectionOptimisticActions {
    constructor(params) {
        this.events = new EventEmitter();
        this.processors = [];
        this.reorderModeCanceledMessage = 'REORDER_MODE_CANCELED';
        this._resumeNavigation = null;
        this._orderByMode = 'orders';
        this._commitMode = false;
        this._clearPatch = action((patch) => {
            const { state, collection, collections, noInvalidate, processors, _commitMode, } = this;
            if (noInvalidate) {
                return;
            }
            state.patches.remove(patch);
            if (_commitMode && !state.patches.length) {
                processors.forEach(({ processorHelpers }) => processorHelpers.commit());
            }
            collection.invalidate();
            collections.forEach((p) => p.refreshAllPages());
        });
        this._clearPatchOnNextResultOrUnmount = (patch) => {
            const { events } = this;
            const clearPatch = () => {
                events.off('unmount', clearPatch);
                this._clearPatch(patch);
            };
            const onRefresh = ({ isStale }) => {
                events.off('refresh', onRefresh);
                // clear only when results are not going to come from cache
                if (isStale) {
                    events.once('beforeResultUpdated', clearPatch);
                }
            };
            events.emit('updated');
            if (this._commitMode) {
                this._clearPatch(patch);
            }
            else {
                // Clear the patch on the next collection result update to eliminate inconsistencies between server state and local patches
                events.on('refresh', onRefresh);
                // since not clearing the patch immediately, clear it on unmount if was not cleared yet
                events.on('unmount', clearPatch);
            }
        };
        this.collection = params.collection;
        this.query = params.query ?? this.collection.query;
        this.collections = params.collections ?? [];
        this.sequences =
            params.sequences ??
                new Map();
        this.state = new CollectionOptimisticActionsState({
            source: this.collection,
            patchesCache: params.container.patchesCache,
            cacheNamespace: params.cacheNamespace,
        });
        this.resultSimulationDefault = new QueryResultSimulationDefault({
            collection: this.collection,
        });
        this.predicate = params.predicate ?? this.resultSimulationDefault.predicate;
        this.orderBy = params.orderBy ?? this.resultSimulationDefault.orderBy;
        this.createdAt = params.createdAt;
        this.noInvalidate = params.noInvalidate;
        this.executor = new CollectionOptimisticExecutor({
            batchWait: params.batchWait ?? 0,
            errorMonitor: this.collection.errorMonitor,
            showToast: this.collection.showToast,
            translate: this.collection.translate,
            lodash: this.collection.lodash,
            container: params.container,
        });
        this.processors.push(new CollectionOptimisticProcessor({
            ...params,
            optimistic: this,
        }));
        if (params.collections != null) {
            params.collections.forEach((collection) => {
                this.processors.push(new CollectionOptimisticProcessor({
                    ...params,
                    collection,
                    optimistic: this,
                }));
            });
        }
        this.collection._optimisticActions = this;
        makeObservable(this, {
            pushPatchAndReprocess: action,
            replacePatchAndReprocess: action,
            rollback: action.bound,
            clear: action,
            move: action,
            pendingMovePatches: computed,
        });
    }
    get _queryNames() {
        return [
            this.collection.queryName,
            ...this.collections.map((c) => c.queryName),
        ];
    }
    get hasRunningSequence() {
        return Array.from(this.sequences.values()).some((s) => s.seq.state.running);
    }
    get pendingMovePatches() {
        return this.state.patches.filter((p) => p.move);
    }
    rollback(patch) {
        const { events, state } = this;
        const patches = Array.isArray(patch) ? patch : [patch];
        for (const p of patches) {
            state.patches.remove(p);
        }
        events.emit('reprocess');
    }
    pushPatchAndReprocess(patch) {
        const { state, events } = this;
        const patches = Array.isArray(patch) ? patch : [patch];
        state.patches.push(...patches);
        events.emit('reprocess');
    }
    replacePatchAndReprocess(newPatch, oldPatch) {
        const { state, events } = this;
        const oldPatchIndex = state.patches.findIndex((patch) => patch === oldPatch);
        state.patches[oldPatchIndex] = newPatch;
        events.emit('reprocess');
    }
    createOne(item, params) {
        return this.createMany([item], params);
    }
    createMany(items, params) {
        const { collection, executor } = this;
        const { itemKey, result } = collection;
        const { submit, updateServerAfter, onUndo, onTryAgain, onError, afterItemKey, } = params;
        const optimisticPatchParams = {
            queryCache: collection.queryCache,
            queryNames: this._queryNames,
            patches: this.state.patches,
            actionType: 'createMany',
            patchedItems: new Map(items.map((item) => [itemKey(item), item])),
            originalItems: new Map(),
            originalTotal: result.total,
            patch: (item, { patchedItems }) => patchedItems.get(itemKey(item)),
            selector: (item, { patchedItems }) => patchedItems.has(itemKey(item)),
            afterItemKey,
        };
        const optimisticPatch = new OptimisticPatch(optimisticPatchParams);
        this.pushPatchAndReprocess(optimisticPatch);
        optimisticPatch.events.on('scheduleRemoval', this._clearPatchOnNextResultOrUnmount);
        return executor.execute([optimisticPatch], {
            ...params,
            submit: action(async (actions) => {
                const submitResult = await submit(Array.from(actions
                    .filter(({ actionType }) => actionType === 'createMany')
                    .reduce((map, { patchedItemsEntries }) => {
                    for (const [key, item] of patchedItemsEntries) {
                        map.set(key, item);
                    }
                    return map;
                }, new Map())
                    .values()));
                if (!submitResult) {
                    return;
                }
                const updatedOptimisticPatch = new OptimisticPatch({
                    ...optimisticPatchParams,
                    patchedItems: new Map(submitResult.map((item) => [itemKey(item), item])),
                });
                this.replacePatchAndReprocess(updatedOptimisticPatch, optimisticPatch);
                this._clearPatchOnNextResultOrUnmount(updatedOptimisticPatch);
                return submitResult;
            }),
            onRollback: () => {
                this.rollback(optimisticPatch);
            },
            eventuallyUpdated: updateServerAfter,
            onEventuallyUpdated: optimisticPatch.onEventuallyUpdated,
            onTryAgain: async () => {
                this.pushPatchAndReprocess(optimisticPatch);
                await onTryAgain?.();
            },
            onError,
            onUndo,
        });
    }
    /**
     * Updates multiple items in the collection.
     * @param items - The items to update.
     * @param params - Additional parameters for the update operation.
     * @external
     */
    updateMany(items, params) {
        const { executor, collection } = this;
        const patchedItems = params.patch
            ? items.map((item) => merge({}, item, params.patch ?? {}))
            : items;
        const { itemKey, result } = collection;
        const { submit, updateServerAfter, keepPosition, onUndo, onTryAgain, onError, } = params;
        const optimisticPatch = new OptimisticPatch({
            queryCache: collection.queryCache,
            queryNames: this._queryNames,
            patches: this.state.patches,
            actionType: 'updateMany',
            keepPosition,
            patchedItems: new Map(patchedItems.map((item) => [itemKey(item), item])),
            originalItems: new Map(patchedItems.map((item) => {
                const key = itemKey(item);
                const originalItem = result.get(itemKey(item))?.item ?? item;
                return [key, originalItem];
            })),
            originalTotal: result.total,
            patch: (item, { patchedItems }) => patchedItems.get(itemKey(item)),
            selector: (item, { patchedItems }) => patchedItems.has(itemKey(item)),
        });
        this.pushPatchAndReprocess(optimisticPatch);
        optimisticPatch.events.on('scheduleRemoval', this._clearPatchOnNextResultOrUnmount);
        return executor.execute([optimisticPatch], {
            ...params,
            submit: action((actions) => submit(Array.from(actions
                .filter(({ actionType }) => actionType === 'updateMany')
                .reduce((map, { patchedItemsEntries }) => {
                for (const [key, item] of patchedItemsEntries) {
                    map.set(key, item);
                }
                return map;
            }, new Map())
                .values()))),
            onRollback: () => {
                this.rollback(optimisticPatch);
            },
            eventuallyUpdated: updateServerAfter,
            onEventuallyUpdated: optimisticPatch.onEventuallyUpdated,
            onTryAgain: async () => {
                this.pushPatchAndReprocess(optimisticPatch);
                await onTryAgain?.();
            },
            onError,
            onUndo,
        });
    }
    enqueueMovePatch(optimisticPatch, params) {
        const { submit, onTryAgain } = params;
        const { collection, state, events } = this;
        const { patches } = state;
        const event = optimisticPatch.move;
        if (event == null) {
            return;
        }
        const { filtersKeyHash } = event;
        const _initSequence = () => {
            const { sequences } = this;
            const seq = sequences.get(filtersKeyHash) ??
                new CollectionOptimisticSequenceExecutor({
                    showToast: collection.showToast,
                    translate: collection.translate,
                    errorMonitor: collection.errorMonitor,
                    onRollback: (err) => {
                        const pendingPatches = patches.filter((p) => p.move?.filtersKeyHash === filtersKeyHash);
                        this.rollback(pendingPatches);
                        if (err instanceof Error &&
                            err.message === this.reorderModeCanceledMessage) {
                            executor.seq.clear();
                            return;
                        }
                        events.emit('dragAndDropSequenceError', {
                            patches: pendingPatches,
                        });
                        executor.seq.clear();
                        const requestId = isHttpError(err) ? err.requestId : undefined;
                        return {
                            message: collection.translate('cairo.dragAndDrop.error.toast'),
                            biName: 'cairo-drag-and-drop-submit-error',
                            details: requestId
                                ? {
                                    requestId,
                                }
                                : undefined,
                            action: {
                                onClick: async () => {
                                    this.pushPatchAndReprocess(pendingPatches);
                                    for (const p of pendingPatches) {
                                        this.enqueueMovePatch(p, params);
                                    }
                                    events.emit('dragAndDropSequenceRetry', {
                                        patches: pendingPatches,
                                    });
                                    await onTryAgain?.();
                                },
                            },
                        };
                    },
                });
            if (!sequences.has(filtersKeyHash)) {
                sequences.set(filtersKeyHash, seq);
            }
            return seq;
        };
        const executor = _initSequence();
        executor.sequence({
            submit: () => submit?.(event),
            onEventuallyUpdated: optimisticPatch.onEventuallyUpdated,
        });
    }
    async move(event, params) {
        const { collection, state } = this;
        const { itemKey, result } = collection;
        const { patches } = state;
        const optimisticPatch = new OptimisticPatch({
            queryCache: collection.queryCache,
            queryNames: this._queryNames,
            patches,
            actionType: 'move',
            patchedItems: new Map(),
            originalItems: new Map(),
            originalTotal: result.total,
            selector: (item, { patchedItems }) => patchedItems.has(itemKey(item)),
            move: event,
            collectionSnapshot: params.collectionSnapshot,
        });
        this.pushPatchAndReprocess(optimisticPatch);
        optimisticPatch.events.on('scheduleRemoval', this._clearPatchOnNextResultOrUnmount);
        this.enqueueMovePatch(optimisticPatch, params);
    }
    /**
     * Updates a single item in the collection.
     * @param item - The item to update.
     * @param params - Additional parameters for the operation.
     * @external
     */
    updateOne(item, params) {
        return this.updateMany([item], params);
    }
    updateAll(patchOrFn, params) {
        const { executor, collection, predicate, createdAt } = this;
        const { submit, updateServerAfter, keepPosition, onUndo, onTryAgain, onError, } = params;
        const { itemKey, result: { originQuery, items, total }, } = collection;
        const itemPredicate = predicate(originQuery);
        const updatedAt = new Date();
        const { patch, actionType } = typeof patchOrFn === 'function'
            ? { actionType: 'updateAll', patch: patchOrFn }
            : {
                actionType: 'updateAllConst',
                patch: (item) => ({ ...item, ...patchOrFn }),
            };
        const optimisticPatch = new OptimisticPatch({
            queryCache: collection.queryCache,
            patches: this.state.patches,
            actionType,
            keepPosition,
            queryNames: this._queryNames,
            patchedItems: new Map(items.map((item) => [itemKey(item), { ...item, ...patch(item) }])),
            originalItems: new Map(items.map((item) => [itemKey(item), item])),
            originalTotal: total,
            selector: (item) => itemPredicate(item) &&
                (createdAt == null || createdAt(item) <= updatedAt),
            patch,
        });
        this.pushPatchAndReprocess(optimisticPatch);
        optimisticPatch.events.on('scheduleRemoval', this._clearPatchOnNextResultOrUnmount);
        executor.execute([optimisticPatch], {
            ...params,
            submit,
            onRollback: () => {
                this.rollback(optimisticPatch);
            },
            eventuallyUpdated: updateServerAfter,
            onEventuallyUpdated: optimisticPatch.onEventuallyUpdated,
            onTryAgain: async () => {
                this.pushPatchAndReprocess(optimisticPatch);
                await onTryAgain?.();
            },
            onError,
            onUndo,
        });
    }
    /**
     * Deletes multiple items from the collection.
     * @param items - An array of items to delete.
     * @param params - Additional parameters for the delete operation.
     * @external
     */
    deleteMany(items, params) {
        const { executor, collection } = this;
        const { itemKey, result: { total }, } = collection;
        const { submit, updateServerAfter, onUndo, onTryAgain, onError } = params;
        const patchedItems = new Map(items.map((item) => [itemKey(item), item]));
        const optimisticPatch = new OptimisticPatch({
            queryCache: collection.queryCache,
            patches: this.state.patches,
            actionType: 'deleteMany',
            patchedItems,
            queryNames: this._queryNames,
            originalItems: patchedItems,
            originalTotal: total,
            selector: (item, { patchedItems }) => patchedItems.has(itemKey(item)),
        });
        this.pushPatchAndReprocess(optimisticPatch);
        optimisticPatch.events.on('scheduleRemoval', this._clearPatchOnNextResultOrUnmount);
        executor.execute([optimisticPatch], {
            ...params,
            submit: action((actions) => submit(Array.from(actions
                .filter(({ actionType }) => actionType === 'deleteMany')
                .reduce((map, { patchedItemsEntries }) => {
                for (const [key, item] of patchedItemsEntries) {
                    map.set(key, item);
                }
                return map;
            }, new Map())
                .values()))),
            onRollback: () => {
                this.rollback(optimisticPatch);
            },
            eventuallyUpdated: updateServerAfter,
            onEventuallyUpdated: optimisticPatch.onEventuallyUpdated,
            onTryAgain: async () => {
                this.pushPatchAndReprocess(optimisticPatch);
                await onTryAgain?.();
            },
            onError,
            onUndo,
        });
    }
    /**
     * Deletes a single item from the collection.
     * @param item - The item to delete.
     * @param params - Additional parameters for the operation.
     * @external
     */
    deleteOne(item, params) {
        return this.deleteMany([item], params);
    }
    /**
     * Deletes all items from the collection.
     * @param params - Additional parameters for the operation.
     * @external
     */
    deleteAll(params) {
        const { executor, collection, predicate, createdAt } = this;
        const { submit, updateServerAfter, onUndo, onTryAgain, onError } = params;
        const { itemKey, result: { originQuery, items, total }, } = collection;
        const itemPredicate = predicate(originQuery);
        const updatedAt = new Date();
        const patchedItems = new Map(items.map((item) => [itemKey(item), item]));
        const optimisticPatch = new OptimisticPatch({
            patches: this.state.patches,
            queryCache: collection.queryCache,
            actionType: 'deleteAll',
            patchedItems,
            queryNames: this._queryNames,
            originalItems: patchedItems,
            originalTotal: total,
            selector: (item) => itemPredicate(item) &&
                (createdAt == null || createdAt(item) <= updatedAt),
        });
        optimisticPatch.events.on('scheduleRemoval', this._clearPatchOnNextResultOrUnmount);
        this.pushPatchAndReprocess(optimisticPatch);
        executor.execute([optimisticPatch], {
            ...params,
            submit,
            onRollback: () => {
                this.rollback(optimisticPatch);
            },
            eventuallyUpdated: updateServerAfter,
            onEventuallyUpdated: optimisticPatch.onEventuallyUpdated,
            onTryAgain: async () => {
                this.pushPatchAndReprocess(optimisticPatch);
                await onTryAgain?.();
            },
            onError,
            onUndo,
        });
    }
    initCollection(collection) {
        this.collections.push(collection);
        new CollectionOptimisticProcessor({
            collection,
            optimistic: this,
            orderBy: this.orderBy,
            predicate: this.predicate,
        });
        return () => {
            const i = this.collections.indexOf(collection);
            if (i !== -1) {
                this.collections.splice(i, 1);
            }
        };
    }
    init() {
        const { collection, events } = this;
        // The only case when this.state._patchesBeforeUnmount won't be null -
        // when the state was cached with patterns router, so we want to restore the patches that were existed before the unmount
        if (this.state._patchesBeforeUnmount) {
            this.state.patches.clear();
            this.state.patches.push(...this.state._patchesBeforeUnmount);
        }
        const emitRefresh = (params) => {
            events.emit('refresh', params);
        };
        const emitBeforeResultUpdated = () => events.emit('beforeResultUpdated');
        collection.emitter.on('refresh', emitRefresh);
        collection.emitter.on('beforeResultUpdated', emitBeforeResultUpdated);
        const disposers = [
            reaction(() => {
                const checkUnprocessedFilteredOut = (() => {
                    const { result: { _pages, _pagesAtom }, } = collection;
                    _pagesAtom.reportObserved();
                    const unprocessed = _pages.unprocessed[_pages.unprocessed.length - 1];
                    const processed = _pages.processed[_pages.processed.length - 1];
                    if (unprocessed == null || processed == null) {
                        return false;
                    }
                    // check if some items in unprocessed were filtered out
                    return unprocessed.length > processed.length;
                })();
                const checkIsEmptyAndHasUnprocessed = (() => {
                    const { result: { isEmpty, _pages }, } = collection;
                    const unprocessed = _pages.unprocessed.flatMap((page) => page);
                    return isEmpty && unprocessed.length > 0;
                })();
                return collection.emitter.listeners('triggerReachBottomListener')
                    .length
                    ? checkUnprocessedFilteredOut
                    : checkIsEmptyAndHasUnprocessed;
            }, (_filteredOut) => {
                if (!_filteredOut) {
                    return;
                }
                if (collection.emitter.listeners('triggerReachBottomListener').length) {
                    if (process.env.NODE_ENV === 'test' &&
                        window.__CAIRO_TEST_TRIGGER_REACH_BOTTOM_LISTENER__) {
                        collection.emitter.emit('triggerReachBottomListener');
                    }
                }
                else {
                    collection.fetchNextPageIfNeeded();
                }
            }),
        ];
        return () => {
            collection.emitter.off('refresh', emitRefresh);
            collection.emitter.off('beforeResultUpdated', emitBeforeResultUpdated);
            disposers.forEach((disposer) => disposer());
            this.state._patchesBeforeUnmount = this.state.patches.toJSON();
            events.emit('unmount');
        };
    }
    clear() {
        const { patches } = this.state;
        const remaining = patches.clear();
        for (const patch of remaining) {
            if (!patch.isStale) {
                patches.push(patch);
            }
        }
    }
}
//# sourceMappingURL=CollectionOptimisticActions.js.map