import { isNullOrUndefined } from '@coveo/bueno';
import { arrayEqualStrictlyDifferentOrder } from '../../../utils/compare-utils.js';
import { isEmptyString } from '../../../utils/utils.js';
import { allEntriesAreValid, buildDateRanges, buildNumericRanges, castUnknownObject, isFacetObject, isObject, rangeDelimiterExclusive, rangeDelimiterInclusive, } from '../../search-parameters/search-parameter-serializer.js';
import { toArray, } from '../../search-parameters/search-parameter-utils.js';
import { buildCriterionExpression, commerceFacetsRegex, deserializeSortCriteria, keyHasObjectValue, } from './parameters-serializer.js';
export function buildParameterSerializer() {
    return { deserialize, serialize };
}
/**
 * Converts URLSearchParams or a record of commerce search parameters into a {@link CommerceSearchParameters} object.
 *
 * @param urlCommerceSearchParams - The URLSearchParams or record of commerce search parameters.
 * @returns The converted CommerceSearchParameters object.
 */
function deserialize(urlCommerceSearchParams) {
    const commerceSearchParameters = {};
    const add = (key, value) => extendSearchParams(commerceSearchParameters, key, value);
    if (urlCommerceSearchParams instanceof URLSearchParams) {
        urlCommerceSearchParams.forEach((value, key) => add(key, value));
    }
    else {
        Object.entries(urlCommerceSearchParams).forEach(([key, value]) => add(key, value));
    }
    return commerceSearchParameters;
}
/**
 * Serializes the commerce search parameters to the provided URL.
 *
 * @param commerceSearchParameters - The commerce search parameters to be serialized.
 * @param initialUrl - The initial URL to which the commerce search parameters will be added.
 * @returns The serialized URL.
 */
function serialize(commerceSearchParameters, initialUrl) {
    initialUrl = new URL(initialUrl);
    const { searchParams } = initialUrl;
    const previousState = wipeSearchParamsFromUrl(searchParams);
    Object.entries(commerceSearchParameters).forEach(([key, value]) => isValidKey(key) &&
        applyToUrlSearchParam(searchParams, previousState, [key, value]));
    return initialUrl.href;
}
function wipeSearchParamsFromUrl(urlSearchParams) {
    const previousSearchParams = {};
    const keysToDelete = [];
    urlSearchParams.forEach((value, key) => {
        if (value !== undefined && isValidSearchParam(key)) {
            previousSearchParams[key] = [...(previousSearchParams[key] || []), value];
            keysToDelete.push(key);
        }
    });
    for (const key of keysToDelete) {
        urlSearchParams.delete(key);
    }
    return previousSearchParams;
}
function applyToUrlSearchParam(urlSearchParams, previousState, pair) {
    if (isNullOrUndefined(pair[1])) {
        return;
    }
    if (isSortPair(pair)) {
        applySortToSearchParams(urlSearchParams, pair);
        return;
    }
    if (isFacetPair(pair)) {
        applyFacetValuesToSearchParams(urlSearchParams, previousState, pair);
        return;
    }
    if (isRangeFacetPair(pair)) {
        applyRangeFacetValuesToSearchParams(urlSearchParams, pair);
        return;
    }
    urlSearchParams.set(pair[0], `${pair[1]}`);
}
function applySortToSearchParams(urlSearchParams, [key, value]) {
    const sortCriteria = encodeURIComponent(buildCriterionExpression(value));
    urlSearchParams.set(key, sortCriteria);
}
function applyFacetValuesToSearchParams(urlSearchParams, previousState, [key, value]) {
    Object.entries(value).forEach(([facetId, facetValues]) => {
        const id = `${key}-${facetId}`;
        const previousFacetValues = previousState[id] ?? [];
        // Verify whether the API has provided an identical set of values but with a different sorting order. In such a scenario, refrain from updating the URL. Instead, revert to the previous state and disregard the altered state with the modified order. This ensures that a new entry is not added to the history unless there is a change beyond just the sorting order.
        if (arrayEqualStrictlyDifferentOrder(previousFacetValues, value[facetId])) {
            previousFacetValues.forEach((v) => urlSearchParams.append(id, v));
            return;
        }
        urlSearchParams.delete(id);
        facetValues.forEach((v) => urlSearchParams.append(id, v));
    });
}
function applyRangeFacetValuesToSearchParams(urlSearchParams, [key, value]) {
    Object.entries(value).forEach(([facetId, facetValues]) => {
        const id = `${key}-${facetId}`;
        urlSearchParams.delete(id);
        facetValues.forEach(({ start, end, endInclusive }) => urlSearchParams.append(id, `${start}${endInclusive ? rangeDelimiterInclusive : rangeDelimiterExclusive}${end}`));
    });
}
export function isValidKey(key) {
    return isValidBasicKey(key) || keyHasObjectValue(key);
}
export function isValidBasicKey(key) {
    const supportedBasicParameters = {
        q: true,
        sortCriteria: true,
        page: true,
        perPage: true,
    };
    return key in supportedBasicParameters;
}
/**
 * Extends the search parameters with the given key-value pair. If the key-value pair is not valid or if the value is undefined, the search parameters are not modified.
 * @param commerceSearchParams - The search parameters object to extend.
 * @param key - The key of the search parameter.
 * @param value - The value of the search parameter.
 */
export function extendSearchParams(commerceSearchParams, key, value) {
    if (isNullOrUndefined(value)) {
        return;
    }
    if (key === 'sortCriteria') {
        commerceSearchParams[key] = deserializeSortCriteria(decodeURIComponent(value));
        return;
    }
    if (isValidBasicKey(key) &&
        typeof value === 'string' &&
        !isEmptyString(value)) {
        const [k, v] = cast([key, value], false);
        commerceSearchParams[k] = v;
        return;
    }
    const result = commerceFacetsRegex.exec(key);
    if (result) {
        const paramKey = result[1];
        const facetId = result[2];
        if (!keyHasObjectValue(paramKey)) {
            return;
        }
        const add = addFacetValuesToSearchParams(facetId, paramKey);
        let range;
        switch (paramKey) {
            case 'nf':
            case 'mnf':
                range = buildNumericRanges(toArray(value), 'selected');
                break;
            case 'nfExcluded':
            case 'mnfExcluded':
                range = buildNumericRanges(toArray(value), 'excluded');
                break;
            case 'df':
                range = buildDateRanges(toArray(value), 'selected');
                break;
            case 'dfExcluded':
                range = buildDateRanges(toArray(value), 'excluded');
                break;
            default:
                range = toArray(value);
                break;
        }
        add(commerceSearchParams, range);
    }
}
export function cast(pair, decode = true) {
    const [key, value] = pair;
    if (key === 'page') {
        return [key, parseInt(value)];
    }
    if (key === 'perPage') {
        return [key, parseInt(value)];
    }
    if (keyHasObjectValue(key)) {
        return [key, castUnknownObject(value)];
    }
    return [key, decode ? decodeURIComponent(value) : value];
}
export function isValidSearchParam(key) {
    return commerceFacetsRegex.exec(key) !== null || isValidBasicKey(key);
}
export function isSortPair(pair) {
    const [key, value] = pair;
    const isValidKey = key === 'sortCriteria';
    const isValidValue = isObject(value) && 'by' in value;
    return isValidKey && isValidValue;
}
export function isFacetPair(pair) {
    const [key, value] = pair;
    const isValidKey = keyHasObjectValue(key);
    const isValidValue = isFacetObject(value);
    return isValidKey && isValidValue;
}
const validRangeFacetKeys = [
    'nf',
    'nfExcluded',
    'mnf',
    'mnfExcluded',
    'df',
    'dfExcluded',
];
export function isRangeFacetPair(pair) {
    const [key, value] = pair;
    const isValidKey = validRangeFacetKeys.includes(key);
    const isValidValue = isRangeFacetObject(value);
    return isValidKey && isValidValue;
}
export function isRangeFacetObject(obj) {
    if (!isObject(obj)) {
        return false;
    }
    const isRangeValue = (v) => isObject(v) && 'start' in v && 'end' in v;
    return allEntriesAreValid(obj, isRangeValue);
}
export function addFacetValuesToSearchParams(facetId, paramKey) {
    return (searchParams, valueArray) => {
        if (paramKey in searchParams) {
            const record = (searchParams[paramKey] ?? {});
            record[facetId] = [...(record[facetId] ?? []), ...valueArray];
            searchParams[paramKey] = record;
        }
        else {
            searchParams[paramKey] = { [facetId]: valueArray };
        }
    };
}
