import { _ as _define_property } from "@swc/helpers/_/_define_property";
import { _ as _object_spread } from "@swc/helpers/_/_object_spread";
import { _ as _object_spread_props } from "@swc/helpers/_/_object_spread_props";
import { _ as _object_without_properties } from "@swc/helpers/_/_object_without_properties";
import { _ as _to_consumable_array } from "@swc/helpers/_/_to_consumable_array";
import algoliasearchHelper from "algoliasearch-helper";
import createWidgetsManager from "./createWidgetsManager";
import { HIGHLIGHT_TAGS } from "./highlight";
import { hasMultipleIndices } from "./indexUtils";
import { version as ReactVersion } from "react";
import version from "./version";
function addAlgoliaAgents(searchClient) {
    if (typeof searchClient.addAlgoliaAgent === "function") {
        searchClient.addAlgoliaAgent("react (".concat(ReactVersion, ")"));
        searchClient.addAlgoliaAgent("react-instantsearch (".concat(version, ")"));
    }
}
var isMultiIndexContext = function isMultiIndexContext(widget) {
    return hasMultipleIndices({
        ais: widget.props.contextValue,
        multiIndexContext: widget.props.indexContextValue
    });
};
var isTargetedIndexEqualIndex = function isTargetedIndexEqualIndex(widget, indexId) {
    return widget.props.indexContextValue.targetedIndex === indexId;
};
// Relying on the `indexId` is a bit brittle to detect the `Index` widget.
// Since it's a class we could rely on `instanceof` or similar. We never
// had an issue though. Works for now.
var isIndexWidget = function isIndexWidget(widget) {
    return Boolean(widget.props.indexId);
};
var isIndexWidgetEqualIndex = function isIndexWidgetEqualIndex(widget, indexId) {
    return widget.props.indexId === indexId;
};
var sortIndexWidgetsFirst = function sortIndexWidgetsFirst(firstWidget, secondWidget) {
    var isFirstWidgetIndex = isIndexWidget(firstWidget);
    var isSecondWidgetIndex = isIndexWidget(secondWidget);
    if (isFirstWidgetIndex && !isSecondWidgetIndex) {
        return -1;
    }
    if (!isFirstWidgetIndex && isSecondWidgetIndex) {
        return 1;
    }
    return 0;
};
// This function is copied from the algoliasearch v4 API Client. If modified,
// consider updating it also in `serializeQueryParameters` from `@algolia/transporter`.
function serializeQueryParameters(parameters) {
    var isObjectOrArray = function isObjectOrArray(value) {
        return Object.prototype.toString.call(value) === "[object Object]" || Object.prototype.toString.call(value) === "[object Array]";
    };
    var encode = function encode(format) {
        for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){
            args[_key - 1] = arguments[_key];
        }
        var i = 0;
        return format.replace(/%s/g, function() {
            return encodeURIComponent(args[i++]);
        });
    };
    return Object.keys(parameters).map(function(key) {
        return encode("%s=%s", key, isObjectOrArray(parameters[key]) ? JSON.stringify(parameters[key]) : parameters[key]);
    }).join("&");
}
/**
 * Creates a new instance of the InstantSearchManager which controls the widgets and
 * trigger the search when the widgets are updated.
 * @param {string} indexName - the main index name
 * @param {object} initialState - initial widget state
 * @param {object} SearchParameters - optional additional parameters to send to the algolia API
 * @param {number} stalledSearchDelay - time (in ms) after the search is stalled
 * @return {InstantSearchManager} a new instance of InstantSearchManager
 */ export default function createInstantSearchManager(param) {
    var indexName = param.indexName, _param_initialState = param.initialState, initialState = _param_initialState === void 0 ? {} : _param_initialState, searchClient = param.searchClient, resultsState = param.resultsState, stalledSearchDelay = param.stalledSearchDelay;
    function createStore(initialState) {
        var state = initialState;
        var listeners = [];
        return {
            getState: function getState() {
                return state;
            },
            setState: function setState(nextState) {
                state = nextState;
                listeners.forEach(function(listener) {
                    return listener();
                });
            },
            subscribe: function subscribe(listener) {
                listeners.push(listener);
                return function unsubscribe() {
                    listeners.splice(listeners.indexOf(listener), 1);
                };
            }
        };
    }
    var helper = algoliasearchHelper(searchClient, indexName, _object_spread({}, HIGHLIGHT_TAGS));
    addAlgoliaAgents(searchClient);
    helper.on("search", handleNewSearch).on("result", handleSearchSuccess({
        indexId: indexName
    })).on("error", handleSearchError);
    var skip = false;
    var stalledSearchTimer = null;
    var initialSearchParameters = helper.state;
    var widgetsManager = createWidgetsManager(onWidgetsUpdate);
    hydrateSearchClient(searchClient, resultsState);
    var store = createStore({
        widgets: initialState,
        metadata: hydrateMetadata(resultsState),
        results: hydrateResultsState(resultsState),
        error: null,
        searching: false,
        isSearchStalled: true,
        searchingForFacetValues: false
    });
    function skipSearch() {
        skip = true;
    }
    function updateClient(client) {
        addAlgoliaAgents(client);
        helper.setClient(client);
        search();
    }
    function clearCache() {
        helper.clearCache();
        search();
    }
    function getMetadata(state) {
        return widgetsManager.getWidgets().filter(function(widget) {
            return Boolean(widget.getMetadata);
        }).map(function(widget) {
            return widget.getMetadata(state);
        });
    }
    function getSearchParameters() {
        var sharedParameters = widgetsManager.getWidgets().filter(function(widget) {
            return Boolean(widget.getSearchParameters);
        }).filter(function(widget) {
            return !isMultiIndexContext(widget) && !isIndexWidget(widget);
        }).reduce(function(res, widget) {
            return widget.getSearchParameters(res);
        }, initialSearchParameters);
        var mainParameters = widgetsManager.getWidgets().filter(function(widget) {
            return Boolean(widget.getSearchParameters);
        }).filter(function(widget) {
            var targetedIndexEqualMainIndex = isMultiIndexContext(widget) && isTargetedIndexEqualIndex(widget, indexName);
            var subIndexEqualMainIndex = isIndexWidget(widget) && isIndexWidgetEqualIndex(widget, indexName);
            return targetedIndexEqualMainIndex || subIndexEqualMainIndex;
        })// We have to sort the `Index` widgets first so the `index` parameter
        // is correctly set in the `reduce` function for the following widgets
        .sort(sortIndexWidgetsFirst).reduce(function(res, widget) {
            return widget.getSearchParameters(res);
        }, sharedParameters);
        var derivedIndices = widgetsManager.getWidgets().filter(function(widget) {
            return Boolean(widget.getSearchParameters);
        }).filter(function(widget) {
            var targetedIndexNotEqualMainIndex = isMultiIndexContext(widget) && !isTargetedIndexEqualIndex(widget, indexName);
            var subIndexNotEqualMainIndex = isIndexWidget(widget) && !isIndexWidgetEqualIndex(widget, indexName);
            return targetedIndexNotEqualMainIndex || subIndexNotEqualMainIndex;
        })// We have to sort the `Index` widgets first so the `index` parameter
        // is correctly set in the `reduce` function for the following widgets
        .sort(sortIndexWidgetsFirst).reduce(function(indices, widget) {
            var indexId = isMultiIndexContext(widget) ? widget.props.indexContextValue.targetedIndex : widget.props.indexId;
            var widgets = indices[indexId] || [];
            return _object_spread_props(_object_spread({}, indices), _define_property({}, indexId, widgets.concat(widget)));
        }, {});
        var derivedParameters = Object.keys(derivedIndices).map(function(indexId) {
            return {
                parameters: derivedIndices[indexId].reduce(function(res, widget) {
                    return widget.getSearchParameters(res);
                }, sharedParameters),
                indexId: indexId
            };
        });
        return {
            mainParameters: mainParameters,
            derivedParameters: derivedParameters
        };
    }
    function search() {
        if (!skip) {
            var _getSearchParameters = getSearchParameters(helper.state), mainParameters = _getSearchParameters.mainParameters, derivedParameters = _getSearchParameters.derivedParameters;
            // We have to call `slice` because the method `detach` on the derived
            // helpers mutates the value `derivedHelpers`. The `forEach` loop does
            // not iterate on each value and we're not able to correctly clear the
            // previous derived helpers (memory leak + useless requests).
            helper.derivedHelpers.slice().forEach(function(derivedHelper) {
                // Since we detach the derived helpers on **every** new search they
                // won't receive intermediate results in case of a stalled search.
                // Only the last result is dispatched by the derived helper because
                // they are not detached yet:
                //
                // - a -> main helper receives results
                // - ap -> main helper receives results
                // - app -> main helper + derived helpers receive results
                //
                // The quick fix is to avoid to detach them on search but only once they
                // received the results. But it means that in case of a stalled search
                // all the derived helpers not detached yet register a new search inside
                // the helper. The number grows fast in case of a bad network and it's
                // not deterministic.
                derivedHelper.detach();
            });
            derivedParameters.forEach(function(param) {
                var indexId = param.indexId, parameters = param.parameters;
                var derivedHelper = helper.derive(function() {
                    return parameters;
                });
                derivedHelper.on("result", handleSearchSuccess({
                    indexId: indexId
                })).on("error", handleSearchError);
            });
            helper.setState(mainParameters);
            helper.search();
        }
    }
    function handleSearchSuccess(param) {
        var indexId = param.indexId;
        return function(event) {
            var state = store.getState();
            var isDerivedHelpersEmpty = !helper.derivedHelpers.length;
            var results = state.results ? state.results : {};
            // Switching from mono index to multi index and vice versa must reset the
            // results to an empty object, otherwise we keep reference of stalled and
            // unused results.
            results = !isDerivedHelpersEmpty && results.getFacetByName ? {} : results;
            if (!isDerivedHelpersEmpty) {
                results = _object_spread_props(_object_spread({}, results), _define_property({}, indexId, event.results));
            } else {
                results = event.results;
            }
            var currentState = store.getState();
            var nextIsSearchStalled = currentState.isSearchStalled;
            if (!helper.hasPendingRequests()) {
                clearTimeout(stalledSearchTimer);
                stalledSearchTimer = null;
                nextIsSearchStalled = false;
            }
            var resultsFacetValues = currentState.resultsFacetValues, partialState = _object_without_properties(currentState, [
                "resultsFacetValues"
            ]);
            store.setState(_object_spread_props(_object_spread({}, partialState), {
                results: results,
                isSearchStalled: nextIsSearchStalled,
                searching: false,
                error: null
            }));
        };
    }
    function handleSearchError(param) {
        var error = param.error;
        var currentState = store.getState();
        var nextIsSearchStalled = currentState.isSearchStalled;
        if (!helper.hasPendingRequests()) {
            clearTimeout(stalledSearchTimer);
            nextIsSearchStalled = false;
        }
        var resultsFacetValues = currentState.resultsFacetValues, partialState = _object_without_properties(currentState, [
            "resultsFacetValues"
        ]);
        store.setState(_object_spread_props(_object_spread({}, partialState), {
            isSearchStalled: nextIsSearchStalled,
            error: error,
            searching: false
        }));
    }
    function handleNewSearch() {
        if (!stalledSearchTimer) {
            stalledSearchTimer = setTimeout(function() {
                var _store_getState = store.getState(), resultsFacetValues = _store_getState.resultsFacetValues, partialState = _object_without_properties(_store_getState, [
                    "resultsFacetValues"
                ]);
                store.setState(_object_spread_props(_object_spread({}, partialState), {
                    isSearchStalled: true
                }));
            }, stalledSearchDelay);
        }
    }
    function hydrateSearchClient(client, results) {
        if (!results) {
            return;
        }
        // Disable cache hydration on:
        // - Algoliasearch API Client < v4 with cache disabled
        // - Third party clients (detected by the `addAlgoliaAgent` function missing)
        if ((!client.transporter || client._cacheHydrated) && (!client._useCache || typeof client.addAlgoliaAgent !== "function")) {
            return;
        }
        // Algoliasearch API Client >= v4
        // To hydrate the client we need to populate the cache with the data from
        // the server (done in `hydrateSearchClientWithMultiIndexRequest` or
        // `hydrateSearchClientWithSingleIndexRequest`). But since there is no way
        // for us to compute the key the same way as `algoliasearch-client` we need
        // to populate it on a custom key and override the `search` method to
        // search on it first.
        if (client.transporter && !client._cacheHydrated) {
            client._cacheHydrated = true;
            var baseMethod = client.search;
            client.search = function(requests) {
                for(var _len = arguments.length, methodArgs = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){
                    methodArgs[_key - 1] = arguments[_key];
                }
                var requestsWithSerializedParams = requests.map(function(request) {
                    return _object_spread_props(_object_spread({}, request), {
                        params: serializeQueryParameters(request.params)
                    });
                });
                return client.transporter.responsesCache.get({
                    method: "search",
                    args: [
                        requestsWithSerializedParams
                    ].concat(_to_consumable_array(methodArgs))
                }, function() {
                    return baseMethod.apply(void 0, [
                        requests
                    ].concat(_to_consumable_array(methodArgs)));
                });
            };
        }
        if (Array.isArray(results.results)) {
            hydrateSearchClientWithMultiIndexRequest(client, results.results);
            return;
        }
        hydrateSearchClientWithSingleIndexRequest(client, results);
    }
    function hydrateSearchClientWithMultiIndexRequest(client, results) {
        // Algoliasearch API Client >= v4
        // Populate the cache with the data from the server
        if (client.transporter) {
            client.transporter.responsesCache.set({
                method: "search",
                args: [
                    results.reduce(function(acc, result) {
                        return acc.concat(result.rawResults.map(function(request) {
                            return {
                                indexName: request.index,
                                params: request.params
                            };
                        }));
                    }, [])
                ]
            }, {
                results: results.reduce(function(acc, result) {
                    return acc.concat(result.rawResults);
                }, [])
            });
            return;
        }
        // Algoliasearch API Client < v4
        // Prior to client v4 we didn't have a proper API to hydrate the client
        // cache from the outside. The following code populates the cache with
        // a single-index result. You can find more information about the
        // computation of the key inside the client (see link below).
        // https://github.com/algolia/algoliasearch-client-javascript/blob/c27e89ff92b2a854ae6f40dc524bffe0f0cbc169/src/AlgoliaSearchCore.js#L232-L240
        var key = "/1/indexes/*/queries_body_".concat(JSON.stringify({
            requests: results.reduce(function(acc, result) {
                return acc.concat(result.rawResults.map(function(request) {
                    return {
                        indexName: request.index,
                        params: request.params
                    };
                }));
            }, [])
        }));
        client.cache = _object_spread_props(_object_spread({}, client.cache), _define_property({}, key, JSON.stringify({
            results: results.reduce(function(acc, result) {
                return acc.concat(result.rawResults);
            }, [])
        })));
    }
    function hydrateSearchClientWithSingleIndexRequest(client, results) {
        // Algoliasearch API Client >= v4
        // Populate the cache with the data from the server
        if (client.transporter) {
            client.transporter.responsesCache.set({
                method: "search",
                args: [
                    results.rawResults.map(function(request) {
                        return {
                            indexName: request.index,
                            params: request.params
                        };
                    })
                ]
            }, {
                results: results.rawResults
            });
            return;
        }
        // Algoliasearch API Client < v4
        // Prior to client v4 we didn't have a proper API to hydrate the client
        // cache from the outside. The following code populates the cache with
        // a single-index result. You can find more information about the
        // computation of the key inside the client (see link below).
        // https://github.com/algolia/algoliasearch-client-javascript/blob/c27e89ff92b2a854ae6f40dc524bffe0f0cbc169/src/AlgoliaSearchCore.js#L232-L240
        var key = "/1/indexes/*/queries_body_".concat(JSON.stringify({
            requests: results.rawResults.map(function(request) {
                return {
                    indexName: request.index,
                    params: request.params
                };
            })
        }));
        client.cache = _object_spread_props(_object_spread({}, client.cache), _define_property({}, key, JSON.stringify({
            results: results.rawResults
        })));
    }
    function hydrateResultsState(results) {
        if (!results) {
            return null;
        }
        if (Array.isArray(results.results)) {
            return results.results.reduce(function(acc, result) {
                return _object_spread_props(_object_spread({}, acc), _define_property({}, result._internalIndexId, new algoliasearchHelper.SearchResults(new algoliasearchHelper.SearchParameters(result.state), result.rawResults)));
            }, {});
        }
        return new algoliasearchHelper.SearchResults(new algoliasearchHelper.SearchParameters(results.state), results.rawResults);
    }
    // Called whenever a widget has been rendered with new props.
    function onWidgetsUpdate() {
        var metadata = getMetadata(store.getState().widgets);
        store.setState(_object_spread_props(_object_spread({}, store.getState()), {
            metadata: metadata,
            searching: true
        }));
        // Since the `getSearchParameters` method of widgets also depends on props,
        // the result search parameters might have changed.
        search();
    }
    function transitionState(nextSearchState) {
        var searchState = store.getState().widgets;
        return widgetsManager.getWidgets().filter(function(widget) {
            return Boolean(widget.transitionState);
        }).reduce(function(res, widget) {
            return widget.transitionState(searchState, res);
        }, nextSearchState);
    }
    function onExternalStateUpdate(nextSearchState) {
        var metadata = getMetadata(nextSearchState);
        store.setState(_object_spread_props(_object_spread({}, store.getState()), {
            widgets: nextSearchState,
            metadata: metadata,
            searching: true
        }));
        search();
    }
    function onSearchForFacetValues(param) {
        var facetName = param.facetName, query = param.query, _param_maxFacetHits = param.maxFacetHits, maxFacetHits = _param_maxFacetHits === void 0 ? 10 : _param_maxFacetHits;
        // The values 1, 100 are the min / max values that the engine accepts.
        // see: https://www.algolia.com/doc/api-reference/api-parameters/maxFacetHits
        var maxFacetHitsWithinRange = Math.max(1, Math.min(maxFacetHits, 100));
        store.setState(_object_spread_props(_object_spread({}, store.getState()), {
            searchingForFacetValues: true
        }));
        helper.searchForFacetValues(facetName, query, maxFacetHitsWithinRange).then(function(content) {
            var _obj;
            store.setState(_object_spread_props(_object_spread({}, store.getState()), {
                error: null,
                searchingForFacetValues: false,
                resultsFacetValues: _object_spread_props(_object_spread({}, store.getState().resultsFacetValues), (_obj = {}, _define_property(_obj, facetName, content.facetHits), _define_property(_obj, "query", query), _obj))
            }));
        }, function(error) {
            store.setState(_object_spread_props(_object_spread({}, store.getState()), {
                searchingForFacetValues: false,
                error: error
            }));
        }).catch(function(error) {
            // Since setState is synchronous, any error that occurs in the render of a
            // component will be swallowed by this promise.
            // This is a trick to make the error show up correctly in the console.
            // See http://stackoverflow.com/a/30741722/969302
            setTimeout(function() {
                throw error;
            });
        });
    }
    function updateIndex(newIndex) {
        initialSearchParameters = initialSearchParameters.setIndex(newIndex);
    // No need to trigger a new search here as the widgets will also update and trigger it if needed.
    }
    function getWidgetsIds() {
        return store.getState().metadata.reduce(function(res, meta) {
            return typeof meta.id !== "undefined" ? res.concat(meta.id) : res;
        }, []);
    }
    return {
        store: store,
        widgetsManager: widgetsManager,
        getWidgetsIds: getWidgetsIds,
        getSearchParameters: getSearchParameters,
        onSearchForFacetValues: onSearchForFacetValues,
        onExternalStateUpdate: onExternalStateUpdate,
        transitionState: transitionState,
        updateClient: updateClient,
        updateIndex: updateIndex,
        clearCache: clearCache,
        skipSearch: skipSearch
    };
}
function hydrateMetadata(resultsState) {
    if (!resultsState) {
        return [];
    }
    // add a value noop, which gets replaced once the widgets are mounted
    return resultsState.metadata.map(function(datum) {
        return _object_spread_props(_object_spread({
            value: function value() {
                return {};
            }
        }, datum), {
            items: datum.items && datum.items.map(function(item) {
                return _object_spread_props(_object_spread({
                    value: function value() {
                        return {};
                    }
                }, item), {
                    items: item.items && item.items.map(function(nestedItem) {
                        return _object_spread({
                            value: function value() {
                                return {};
                            }
                        }, nestedItem);
                    })
                });
            })
        });
    });
}
