// (C) 2024-2026 GoodData Corporation
import { isEqual, omit } from "lodash-es";
import { areObjRefsEqual, dashboardAttributeFilterItemDisplayForm, dashboardAttributeFilterItemFilterElementsBy, dashboardAttributeFilterItemFilterElementsByDate, dashboardAttributeFilterItemLocalIdentifier, dashboardAttributeFilterItemTitle, dashboardAttributeFilterItemValidateElementsBy, dashboardMeasureValueFilterLocalIdentifier, isDashboardArbitraryAttributeFilter, isDashboardAttributeFilter, isDashboardAttributeFilterItem, isDashboardDateFilter, isDashboardMatchAttributeFilter, isDashboardMeasureValueFilter, isDashboardTextAttributeFilter, isFilterContext, isSingleSelectionFilter, } from "@gooddata/sdk-model";
import { isFilterTypeCompatibleWithSelectionType } from "./attributeFilterSelectionTypeCompatibility.js";
const findMatchingAttributeFilterByLocalIdentifier = (filter, viewFilters) => viewFilters.find((item) => isDashboardAttributeFilter(item) &&
    item.attributeFilter.localIdentifier === filter.attributeFilter.localIdentifier);
const findMatchingTextFilterByLocalIdentifier = (filter, viewFilters) => {
    const localId = dashboardAttributeFilterItemLocalIdentifier(filter);
    if (!localId)
        return undefined;
    return viewFilters.find((item) => isDashboardTextAttributeFilter(item) &&
        dashboardAttributeFilterItemLocalIdentifier(item) === localId);
};
/** Finds any attribute filter item (list or text) by localIdentifier. */
const findMatchingAttributeFilterItemByLocalIdentifier = (localId, viewFilters) => viewFilters.find((item) => isDashboardAttributeFilterItem(item) &&
    dashboardAttributeFilterItemLocalIdentifier(item) === localId);
// date filters do not have localIdentifier set, compare them by dataSet instead
const findMatchingDateFilterByDataSet = (filter, viewFilters) => viewFilters.find((item) => isDashboardDateFilter(item) &&
    ((item.dateFilter.dataSet === undefined && filter.dateFilter.dataSet === undefined) ||
        areObjRefsEqual(item.dateFilter.dataSet, filter.dateFilter.dataSet)));
// MVF: match only when both localIdentifier and the referenced metric agree.
// The metric ref is set in edit mode and not changeable in view mode, so a localId
// collision with a different metric must not import stale conditions.
const findMatchingMeasureValueFilter = (filter, viewFilters) => viewFilters.find((item) => isDashboardMeasureValueFilter(item) &&
    dashboardMeasureValueFilterLocalIdentifier(item) ===
        dashboardMeasureValueFilterLocalIdentifier(filter) &&
    areObjRefsEqual(item.dashboardMeasureValueFilter.measure, filter.dashboardMeasureValueFilter.measure));
const OMITTED_ATTRIBUTE_FILTER_PATHS = [
    "attributeFilter.attributeElements",
    "attributeFilter.negativeSelection",
];
const hasSameAttributeFilterConfiguration = (filterA, filterB) => {
    return isEqual(omit(filterA, OMITTED_ATTRIBUTE_FILTER_PATHS), omit(filterB, OMITTED_ATTRIBUTE_FILTER_PATHS));
};
/**
 * Checks that the non-view-editable configuration of two text filters matches.
 * Only displayForm and title are compared — everything changeable in view mode
 * (type, values, operator, literal, caseSensitive, negativeSelection) is ignored.
 */
const hasSameTextFilterConfiguration = (filterA, filterB) => {
    return (areObjRefsEqual(dashboardAttributeFilterItemDisplayForm(filterA), dashboardAttributeFilterItemDisplayForm(filterB)) && dashboardAttributeFilterItemTitle(filterA) === dashboardAttributeFilterItemTitle(filterB));
};
/**
 * Resolves the effective display form for a filter in the context of cross-type comparison.
 * List filters use displayAsLabel from config (falls back to primary displayForm).
 * Text filters use their displayForm directly.
 */
const getEffectiveDisplayForm = (filter, filterConfig) => {
    if (isDashboardAttributeFilter(filter)) {
        return filterConfig?.displayAsLabel ?? filter.attributeFilter.displayForm;
    }
    return dashboardAttributeFilterItemDisplayForm(filter);
};
/**
 * Checks that the non-view-editable configuration of two attribute filters of different types matches.
 *
 * - DisplayForm: list filters use displayAsLabel from config; text filters use their own displayForm.
 * - Parent/validation props (filterElementsBy, filterElementsByDate, validateElementsBy):
 *   compared only when the view filter is arbitrary (which supports them).
 *   Match filters don't support parent dependencies, so these are skipped for match overrides.
 * - Title is always compared.
 */
const hasSameCrossTypeFilterConfiguration = (dashboardFilter, viewFilter, filterConfig) => {
    const hasSameDisplayForm = areObjRefsEqual(getEffectiveDisplayForm(dashboardFilter, filterConfig), getEffectiveDisplayForm(viewFilter, filterConfig));
    const hasSameTitle = dashboardAttributeFilterItemTitle(dashboardFilter) === dashboardAttributeFilterItemTitle(viewFilter);
    // Parent/validation props only apply to arbitrary filters — match filters don't support them
    const hasSameParentConfig = !isDashboardArbitraryAttributeFilter(viewFilter) ||
        (isEqual(dashboardAttributeFilterItemFilterElementsBy(dashboardFilter), dashboardAttributeFilterItemFilterElementsBy(viewFilter)) &&
            isEqual(dashboardAttributeFilterItemFilterElementsByDate(dashboardFilter), dashboardAttributeFilterItemFilterElementsByDate(viewFilter)) &&
            isEqual(dashboardAttributeFilterItemValidateElementsBy(dashboardFilter), dashboardAttributeFilterItemValidateElementsBy(viewFilter)));
    return hasSameDisplayForm && hasSameTitle && hasSameParentConfig;
};
/** Creates "All" state for text filters: always a negative arbitrary filter with empty selection */
const createResetTextFilter = (filter) => {
    const displayForm = dashboardAttributeFilterItemDisplayForm(filter);
    const localIdentifier = dashboardAttributeFilterItemLocalIdentifier(filter);
    const title = dashboardAttributeFilterItemTitle(filter);
    if (isDashboardArbitraryAttributeFilter(filter)) {
        const { filterElementsBy, filterElementsByDate, validateElementsBy } = filter.arbitraryAttributeFilter;
        return {
            arbitraryAttributeFilter: {
                displayForm,
                values: [],
                negativeSelection: true,
                localIdentifier,
                title,
                filterElementsBy,
                filterElementsByDate,
                validateElementsBy,
            },
        };
    }
    // Match filter - no validateElementsBy, filterElementsBy, or filterElementsByDate
    return {
        arbitraryAttributeFilter: {
            displayForm,
            values: [],
            negativeSelection: true,
            localIdentifier,
            title,
        },
    };
};
const isCommonDateFilter = (filter) => isDashboardDateFilter(filter) && filter.dateFilter.dataSet === undefined;
const handleCommonDateFilter = (filterContextFilters, filterViewFilters, mergedFilters) => {
    // If common date is set to All, it is not included in filter context filters.
    // Common filter differs from date filters by not having date dataset (it is provided by widget).
    const isCommonDateOriginallyOnAll = !filterContextFilters.some(isCommonDateFilter);
    const nonAllCommonDateFilterFromView = filterViewFilters.find(isCommonDateFilter);
    if (isCommonDateOriginallyOnAll && nonAllCommonDateFilterFromView) {
        // inject value of common date from view to filter context to override filter context's All value
        return [nonAllCommonDateFilterFromView, ...mergedFilters];
    }
    else if (!nonAllCommonDateFilterFromView) {
        // set common date to All based on filter view
        return mergedFilters.filter((filter) => !isCommonDateFilter(filter));
    }
    return mergedFilters;
};
/**
 * Applies the view filter's type and values to the dashboard filter,
 * preserving the dashboard filter's config (title, filterElementsBy,
 * filterElementsByDate, validateElementsBy) and taking only selection
 * fields from the view. Handles type changes between arbitrary and match filters.
 */
const applyTextFilterFromView = (dashboardFilter, viewFilter) => {
    const dashboardFilterTitle = dashboardAttributeFilterItemTitle(dashboardFilter);
    if (isDashboardArbitraryAttributeFilter(viewFilter)) {
        const dashboardArbitrary = isDashboardArbitraryAttributeFilter(dashboardFilter)
            ? dashboardFilter.arbitraryAttributeFilter
            : undefined;
        return {
            arbitraryAttributeFilter: {
                ...viewFilter.arbitraryAttributeFilter,
                title: dashboardFilterTitle,
                filterElementsBy: dashboardArbitrary?.filterElementsBy,
                filterElementsByDate: dashboardArbitrary?.filterElementsByDate,
                validateElementsBy: dashboardArbitrary?.validateElementsBy,
            },
        };
    }
    if (isDashboardMatchAttributeFilter(viewFilter)) {
        return {
            matchAttributeFilter: {
                ...viewFilter.matchAttributeFilter,
                title: dashboardFilterTitle,
            },
        };
    }
    return viewFilter;
};
/**
 * Applies displayAsLabel config updates to attribute filter configs after cross-type filter replacement.
 */
function applyConfigUpdates(configs, updates) {
    if (updates.size === 0) {
        return configs;
    }
    return (configs ?? []).map((config) => {
        const update = updates.get(config.localIdentifier);
        return update ? { ...config, displayAsLabel: update.displayAsLabel } : config;
    });
}
export const changeFilterContextSelection = (filterContext, filterViewFilters, attributeFilterConfigs) => {
    const configsMap = new Map((attributeFilterConfigs ?? []).map((config) => [config.localIdentifier, config]));
    const configUpdates = new Map();
    const mergedFilters = filterContext.filters.map((filter) => {
        if (isDashboardAttributeFilter(filter)) {
            const filterLocalId = filter.attributeFilter.localIdentifier;
            const filterConfig = filterLocalId ? configsMap.get(filterLocalId) : undefined;
            const configSelectionType = filterConfig?.selectionType;
            // Try same-type (list) match first
            const viewFilter = findMatchingAttributeFilterByLocalIdentifier(filter, filterViewFilters);
            const listDefaultSelectionType = isSingleSelectionFilter(filter) ? "list" : undefined;
            if (viewFilter !== undefined &&
                hasSameAttributeFilterConfiguration(filter, viewFilter) &&
                isFilterTypeCompatibleWithSelectionType("list", configSelectionType, listDefaultSelectionType)) {
                return {
                    attributeFilter: {
                        ...filter.attributeFilter,
                        attributeElements: viewFilter.attributeFilter.attributeElements,
                        negativeSelection: viewFilter.attributeFilter.negativeSelection,
                    },
                };
            }
            // Try cross-type (text) match if selectionType allows text
            if (filterLocalId &&
                isFilterTypeCompatibleWithSelectionType("text", configSelectionType, listDefaultSelectionType)) {
                const viewTextFilter = findMatchingAttributeFilterItemByLocalIdentifier(filterLocalId, filterViewFilters);
                if (viewTextFilter &&
                    isDashboardTextAttributeFilter(viewTextFilter) &&
                    hasSameCrossTypeFilterConfiguration(filter, viewTextFilter, filterConfig)) {
                    // List → text: update displayAsLabel to text filter's displayForm
                    configUpdates.set(filterLocalId, {
                        displayAsLabel: dashboardAttributeFilterItemDisplayForm(viewTextFilter),
                    });
                    return viewTextFilter;
                }
            }
            // No compatible match found, reset
            return {
                attributeFilter: {
                    ...filter.attributeFilter,
                    attributeElements: {
                        uris: [],
                    },
                    negativeSelection: true,
                },
            };
        }
        else if (isDashboardDateFilter(filter)) {
            const viewFilter = findMatchingDateFilterByDataSet(filter, filterViewFilters);
            if (viewFilter) {
                return {
                    dateFilter: {
                        ...filter.dateFilter,
                        from: viewFilter.dateFilter.from,
                        to: viewFilter.dateFilter.to,
                        granularity: viewFilter.dateFilter.granularity,
                        type: viewFilter.dateFilter.type,
                    },
                };
            }
            else {
                return {
                    // reset filter that has not been found in the view
                    dateFilter: {
                        ...filter.dateFilter,
                        from: undefined,
                        to: undefined,
                        granularity: "GDC.time.date",
                        type: "relative",
                    },
                };
            }
        }
        else if (isDashboardTextAttributeFilter(filter)) {
            const filterLocalId = dashboardAttributeFilterItemLocalIdentifier(filter);
            const filterConfig = filterLocalId ? configsMap.get(filterLocalId) : undefined;
            const configSelectionType = filterConfig?.selectionType;
            // Try same-type (text) match first
            if (isFilterTypeCompatibleWithSelectionType("text", configSelectionType)) {
                const viewFilter = findMatchingTextFilterByLocalIdentifier(filter, filterViewFilters);
                if (viewFilter !== undefined && hasSameTextFilterConfiguration(filter, viewFilter)) {
                    return applyTextFilterFromView(filter, viewFilter);
                }
            }
            // Try cross-type (list) match if selectionType allows list
            if (filterLocalId && isFilterTypeCompatibleWithSelectionType("list", configSelectionType)) {
                const viewListFilter = findMatchingAttributeFilterItemByLocalIdentifier(filterLocalId, filterViewFilters);
                if (viewListFilter &&
                    isDashboardAttributeFilter(viewListFilter) &&
                    hasSameCrossTypeFilterConfiguration(filter, viewListFilter, filterConfig)) {
                    // Text → list: set displayAsLabel to the text filter's displayForm
                    configUpdates.set(filterLocalId, {
                        displayAsLabel: dashboardAttributeFilterItemDisplayForm(filter),
                    });
                    return viewListFilter;
                }
            }
            // No compatible match found, reset
            return createResetTextFilter(filter);
        }
        else if (isDashboardMeasureValueFilter(filter)) {
            const viewFilter = findMatchingMeasureValueFilter(filter, filterViewFilters);
            if (viewFilter) {
                return {
                    dashboardMeasureValueFilter: {
                        ...filter.dashboardMeasureValueFilter,
                        conditions: viewFilter.dashboardMeasureValueFilter.conditions,
                    },
                };
            }
            // reset MVF that has not been found in the view to "All" (empty conditions)
            return {
                dashboardMeasureValueFilter: {
                    ...filter.dashboardMeasureValueFilter,
                    conditions: [],
                },
            };
        }
        else {
            return filter;
        }
    });
    return {
        filterContext: {
            ...filterContext,
            filters: handleCommonDateFilter(filterContext.filters, filterViewFilters, mergedFilters),
        },
        configUpdates,
    };
};
export function applyDefaultFilterView(dashboard, filterViews) {
    // If dashboard has tabs, apply default views per tab
    if (dashboard.tabs && dashboard.tabs.length > 0) {
        // Find legacy global default view (without tabLocalIdentifier) as fallback for first tab
        const legacyDefaultView = filterViews.find((view) => view.isDefault && !view.tabLocalIdentifier);
        const updatedTabs = dashboard.tabs.map((tab, index) => {
            // Find default view specific to this tab
            const tabDefaultView = filterViews.find((view) => view.isDefault && view.tabLocalIdentifier === tab.localIdentifier);
            // Use tab-specific default, or fall back to legacy global default only for the first tab
            const isFirstTab = index === 0;
            const effectiveDefaultView = tabDefaultView ?? (isFirstTab ? legacyDefaultView : undefined);
            const parameters = applyFilterViewParameterValues(tab.parameters, effectiveDefaultView?.parameters);
            if (effectiveDefaultView && tab.filterContext && isFilterContext(tab.filterContext)) {
                const { filterContext: updatedFilterContext, configUpdates } = changeFilterContextSelection(tab.filterContext, effectiveDefaultView.filterContext.filters, tab.attributeFilterConfigs);
                return {
                    ...tab,
                    ...(parameters === undefined ? {} : { parameters }),
                    filterContext: updatedFilterContext,
                    attributeFilterConfigs: applyConfigUpdates(tab.attributeFilterConfigs, configUpdates),
                };
            }
            return parameters === undefined ? tab : { ...tab, parameters };
        });
        return {
            ...dashboard,
            tabs: updatedTabs,
        };
    }
    // Legacy behavior: apply default view to root-level filter context
    const defaultFilterView = filterViews.find((view) => view.isDefault);
    if (defaultFilterView && isFilterContext(dashboard.filterContext)) {
        const { filterContext: updatedFilterContext, configUpdates } = changeFilterContextSelection(dashboard.filterContext, defaultFilterView.filterContext.filters, dashboard.attributeFilterConfigs);
        return {
            ...dashboard,
            filterContext: updatedFilterContext,
            attributeFilterConfigs: applyConfigUpdates(dashboard.attributeFilterConfigs, configUpdates),
            parameters: applyFilterViewParameterValues(dashboard.parameters, defaultFilterView.parameters),
        };
    }
    return dashboard;
}
/**
 * Default filter views are applied before tab parameters are hydrated into runtime state.
 * Copy captured filter view values into matching dashboard parameters so hydration initializes chips
 * from the default view.
 */
function applyFilterViewParameterValues(dashboardParameters, filterViewParameters) {
    if (!dashboardParameters || !filterViewParameters || filterViewParameters.length === 0) {
        return dashboardParameters;
    }
    return dashboardParameters.map((parameter) => {
        const filterViewParameter = filterViewParameters.find((item) => areObjRefsEqual(item.ref, parameter.ref));
        if (!filterViewParameter) {
            return parameter;
        }
        if (filterViewParameter.value === undefined) {
            return omit(parameter, ["value"]);
        }
        return { ...parameter, value: filterViewParameter.value };
    });
}
