"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getUnmetFilterRequirements = exports.isRequirementRuleSatisfied = exports.isValuelessDashboardFilterRule = exports.getFilterRequirementRules = exports.resetRequiredFilterRules = exports.reduceRequiredDimensionFiltersToFilterRules = exports.isFilterRuleInQuery = exports.createFilterRuleFromModelRequiredFilterRule = exports.applyDashboardFiltersForTile = exports.getAvailableFilterFieldIds = exports.addDashboardFiltersToMetricQuery = exports.trackWhichTimeBasedMetricFiltersToOverride = exports.overrideFilterGroupWithFilterRules = exports.overrideChartFilter = exports.addFiltersToMetricQuery = exports.stripOverridesForLockedFiltersOnTab = exports.isFilterLockedOnTab = exports.getDashboardFiltersForTile = exports.getDashboardFiltersForTileAndTables = exports.getDashboardFilterRulesForTileAndTables = exports.getDashboardFilterRulesForTables = exports.getDashboardFilterRulesForTileAndReferences = exports.getTabUuidsForFilterRules = exports.getDashboardFilterRulesForTile = exports.deleteFilterRuleFromGroup = exports.getFiltersFromGroup = exports.isDimensionValueInvalidDate = exports.addFilterRule = exports.createDashboardFilterRuleFromSqlColumn = exports.createDashboardFilterRuleFromField = exports.applyDefaultTileTargets = exports.isTileFilterable = exports.matchFieldByType = exports.matchFieldByTypeAndName = exports.matchFieldExact = exports.createFilterRuleFromField = exports.getFilterRuleFromFieldWithDefaultValue = exports.getFilterRuleWithDefaultValue = exports.isEmptyDashboardFilterRule = exports.isWithValueFilter = exports.supportsSingleValue = exports.timeframeToUnitOfTime = exports.getFilterTypeFromItem = exports.getFilterTypeFromItemType = exports.getFilterGroupItemsPropertyName = exports.getItemsFromFilterGroup = exports.hasNestedGroups = exports.countTotalFilterRules = exports.getTotalFilterRules = exports.getFilterRulesFromGroup = void 0;
const tslib_1 = require("tslib");
const dayjs_1 = tslib_1.__importDefault(require("dayjs"));
const isNil_1 = tslib_1.__importDefault(require("lodash/isNil"));
const moment_1 = tslib_1.__importDefault(require("moment"));
const uuid_1 = require("uuid");
const dashboard_1 = require("../types/dashboard");
const field_1 = require("../types/field");
const filter_1 = require("../types/filter");
const timeFrames_1 = require("../types/timeFrames");
const assertUnreachable_1 = tslib_1.__importDefault(require("./assertUnreachable"));
const fields_1 = require("./fields");
const formatting_1 = require("./formatting");
const item_1 = require("./item");
const getFilterRulesFromGroup = (filterGroup) => {
    if (filterGroup) {
        const items = (0, filter_1.isAndFilterGroup)(filterGroup)
            ? filterGroup.and
            : filterGroup.or;
        return (items ?? []).reduce((sum, item) => (0, filter_1.isFilterGroup)(item)
            ? [...sum, ...(0, exports.getFilterRulesFromGroup)(item)]
            : [...sum, item], []);
    }
    return [];
};
exports.getFilterRulesFromGroup = getFilterRulesFromGroup;
const getTotalFilterRules = (filters) => [
    ...(0, exports.getFilterRulesFromGroup)(filters.dimensions),
    ...(0, exports.getFilterRulesFromGroup)(filters.metrics),
    ...(0, exports.getFilterRulesFromGroup)(filters.tableCalculations),
];
exports.getTotalFilterRules = getTotalFilterRules;
const countTotalFilterRules = (filters) => (0, exports.getTotalFilterRules)(filters).length;
exports.countTotalFilterRules = countTotalFilterRules;
const hasNestedGroups = (filters) => {
    const hasGroups = (filterGroup) => {
        const items = (0, filter_1.isAndFilterGroup)(filterGroup)
            ? filterGroup.and
            : filterGroup.or;
        return items.some(filter_1.isFilterGroup);
    };
    return ((!!filters.dimensions && hasGroups(filters.dimensions)) ||
        (!!filters.metrics && hasGroups(filters.metrics)) ||
        (!!filters.tableCalculations && hasGroups(filters.tableCalculations)));
};
exports.hasNestedGroups = hasNestedGroups;
const getItemsFromFilterGroup = (filterGroup) => {
    if (filterGroup) {
        return (0, filter_1.isAndFilterGroup)(filterGroup) ? filterGroup.and : filterGroup.or;
    }
    return [];
};
exports.getItemsFromFilterGroup = getItemsFromFilterGroup;
const getFilterGroupItemsPropertyName = (filterGroup) => {
    if (filterGroup) {
        return (0, filter_1.isAndFilterGroup)(filterGroup) ? 'and' : 'or';
    }
    return 'and';
};
exports.getFilterGroupItemsPropertyName = getFilterGroupItemsPropertyName;
const getFilterTypeFromItemType = (type) => {
    switch (type) {
        case field_1.DimensionType.STRING:
        case field_1.MetricType.STRING:
        case field_1.TableCalculationType.STRING:
            return filter_1.FilterType.STRING;
        case field_1.DimensionType.NUMBER:
        case field_1.MetricType.NUMBER:
        case field_1.MetricType.PERCENTILE:
        case field_1.MetricType.MEDIAN:
        case field_1.MetricType.AVERAGE:
        case field_1.MetricType.COUNT:
        case field_1.MetricType.COUNT_DISTINCT:
        case field_1.MetricType.SUM:
        case field_1.MetricType.SUM_DISTINCT:
        case field_1.MetricType.AVERAGE_DISTINCT:
        case field_1.MetricType.MIN:
        case field_1.MetricType.MAX:
        case field_1.MetricType.PERCENT_OF_PREVIOUS:
        case field_1.MetricType.PERCENT_OF_TOTAL:
        case field_1.MetricType.RUNNING_TOTAL:
        case field_1.TableCalculationType.NUMBER:
            return filter_1.FilterType.NUMBER;
        case field_1.DimensionType.TIMESTAMP:
        case field_1.MetricType.TIMESTAMP:
        case field_1.DimensionType.DATE:
        case field_1.MetricType.DATE:
        case field_1.TableCalculationType.DATE:
        case field_1.TableCalculationType.TIMESTAMP:
            return filter_1.FilterType.DATE;
        case field_1.DimensionType.BOOLEAN:
        case field_1.MetricType.BOOLEAN:
        case field_1.TableCalculationType.BOOLEAN:
            return filter_1.FilterType.BOOLEAN;
        default: {
            return (0, assertUnreachable_1.default)(type, `No filter type found for field type: ${type}`);
        }
    }
};
exports.getFilterTypeFromItemType = getFilterTypeFromItemType;
const getFilterTypeFromItem = (item) => {
    const type = (0, item_1.getItemType)(item);
    return (0, exports.getFilterTypeFromItemType)(type);
};
exports.getFilterTypeFromItem = getFilterTypeFromItem;
const timeframeToUnitOfTime = (timeframe) => {
    switch (timeframe) {
        case timeFrames_1.TimeFrames.MILLISECOND:
            return filter_1.UnitOfTime.milliseconds;
        case timeFrames_1.TimeFrames.SECOND:
            return filter_1.UnitOfTime.seconds;
        case timeFrames_1.TimeFrames.MINUTE:
            return filter_1.UnitOfTime.minutes;
        case timeFrames_1.TimeFrames.HOUR:
            return filter_1.UnitOfTime.hours;
        case timeFrames_1.TimeFrames.DAY:
            return filter_1.UnitOfTime.days;
        case timeFrames_1.TimeFrames.WEEK:
            return filter_1.UnitOfTime.weeks;
        case timeFrames_1.TimeFrames.MONTH:
            return filter_1.UnitOfTime.months;
        case timeFrames_1.TimeFrames.QUARTER:
            return filter_1.UnitOfTime.quarters;
        case timeFrames_1.TimeFrames.YEAR:
            return filter_1.UnitOfTime.years;
        default:
            return undefined;
    }
};
exports.timeframeToUnitOfTime = timeframeToUnitOfTime;
const supportsSingleValue = (filterType, filterOperator) => [filter_1.FilterType.STRING, filter_1.FilterType.NUMBER].includes(filterType) &&
    [
        filter_1.FilterOperator.EQUALS,
        filter_1.FilterOperator.NOT_EQUALS,
        filter_1.FilterOperator.STARTS_WITH,
        filter_1.FilterOperator.ENDS_WITH,
        filter_1.FilterOperator.INCLUDE,
        filter_1.FilterOperator.NOT_INCLUDE,
    ].includes(filterOperator);
exports.supportsSingleValue = supportsSingleValue;
const isWithValueFilter = (filterOperator) => filterOperator !== filter_1.FilterOperator.NULL &&
    filterOperator !== filter_1.FilterOperator.NOT_NULL &&
    filterOperator !== filter_1.FilterOperator.IN_PERIOD_TO_DATE;
exports.isWithValueFilter = isWithValueFilter;
/**
 * An "empty" dashboard filter is active (not disabled), uses a value-requiring
 * operator, and has no value set — an unset default control the viewer fills in
 * at runtime. It must not override chart-level filters until a value is picked.
 *
 * IN_THE_CURRENT / NOT_IN_THE_CURRENT are excluded: they compile from
 * settings.unitOfTime, not values, so a value-less one is still an active
 * filter. `values` uses Array.isArray to tolerate hand-authored YAML where
 * `values:` / `values: ~` parses to JS `null` instead of `[]`.
 */
const isEmptyDashboardFilterRule = (filter) => filter.disabled !== true &&
    (0, exports.isWithValueFilter)(filter.operator) &&
    filter.operator !== filter_1.FilterOperator.IN_THE_CURRENT &&
    filter.operator !== filter_1.FilterOperator.NOT_IN_THE_CURRENT &&
    filter.includeNull !== true &&
    (!Array.isArray(filter.values) || filter.values.length === 0);
exports.isEmptyDashboardFilterRule = isEmptyDashboardFilterRule;
const getFilterRuleWithDefaultValue = (filterType, field, filterRule, values, timezone) => {
    const filterRuleDefaults = {};
    if (![
        filter_1.FilterOperator.NULL,
        filter_1.FilterOperator.NOT_NULL,
        filter_1.FilterOperator.IN_PERIOD_TO_DATE,
    ].includes(filterRule.operator) &&
        values !== null) {
        switch (filterType) {
            case filter_1.FilterType.DATE: {
                const value = values ? values[0] : undefined;
                const isTimestamp = !field ||
                    ((0, field_1.isCustomSqlDimension)(field)
                        ? field.dimensionType
                        : field.type) === field_1.DimensionType.TIMESTAMP;
                if (filterRule.operator === filter_1.FilterOperator.IN_THE_PAST ||
                    filterRule.operator === filter_1.FilterOperator.NOT_IN_THE_PAST ||
                    filterRule.operator === filter_1.FilterOperator.IN_THE_NEXT ||
                    filterRule.operator === filter_1.FilterOperator.IN_THE_CURRENT ||
                    filterRule.operator === filter_1.FilterOperator.NOT_IN_THE_CURRENT) {
                    const numberValue = value === undefined || typeof value !== 'number'
                        ? 1
                        : value;
                    const defaultUnitOfTime = (0, field_1.isDimension)(field) && field.timeInterval
                        ? (0, exports.timeframeToUnitOfTime)(field.timeInterval)
                        : filter_1.UnitOfTime.days;
                    filterRuleDefaults.values = [numberValue];
                    filterRuleDefaults.settings = {
                        unitOfTime: defaultUnitOfTime,
                        completed: false,
                    };
                }
                else if (isTimestamp) {
                    const valueIsDate = value !== undefined && typeof value !== 'number';
                    // NOTE: Using .format() makes this a standard ISO string
                    const timestampValue = valueIsDate
                        ? (0, dayjs_1.default)(value).format()
                        : (0, dayjs_1.default)().format();
                    filterRuleDefaults.values = [timestampValue];
                }
                else {
                    const valueIsDate = value !== undefined && typeof value !== 'number';
                    const defaultTimeIntervalValues = {
                        [timeFrames_1.TimeFrames.DAY]: (0, moment_1.default)(),
                        [timeFrames_1.TimeFrames.WEEK]: (0, moment_1.default)(valueIsDate ? value : undefined).startOf('week'),
                        [timeFrames_1.TimeFrames.QUARTER]: (0, moment_1.default)(valueIsDate ? value : undefined).startOf('quarter'),
                        [timeFrames_1.TimeFrames.MONTH]: (0, moment_1.default)(valueIsDate ? value : undefined).startOf('month'),
                        [timeFrames_1.TimeFrames.YEAR]: (0, moment_1.default)(valueIsDate ? value : undefined).startOf('year'),
                    };
                    const fieldTimeInterval = (0, field_1.isDimension)(field) && field.timeInterval
                        ? field.timeInterval
                        : undefined;
                    const defaultDate = fieldTimeInterval &&
                        defaultTimeIntervalValues[fieldTimeInterval]
                        ? defaultTimeIntervalValues[fieldTimeInterval]
                        : (0, moment_1.default)();
                    // Shift TIMESTAMP-base time-interval dims into the project
                    // TZ before extracting the date. Plain DATE / DATE-base
                    // intervals are calendar values and must not be shifted.
                    const effectiveZone = (0, formatting_1.shouldShiftItemTimezone)(field)
                        ? timezone || 'UTC'
                        : 'UTC';
                    const dateValue = valueIsDate
                        ? (0, formatting_1.formatDate)(moment_1.default
                            .utc(value)
                            .tz(effectiveZone)
                            .format('YYYY-MM-DD'), 
                        // For QUARTER, we don't want to use the field's time interval(YYYY-[Q]Q) because the date is already in the correct format when generating the SQL
                        fieldTimeInterval === timeFrames_1.TimeFrames.QUARTER
                            ? undefined
                            : fieldTimeInterval, // Use the field's time interval if it has one
                        false)
                        : (0, formatting_1.formatDate)(defaultDate, undefined, false);
                    filterRuleDefaults.values = [dateValue];
                }
                break;
            }
            case filter_1.FilterType.BOOLEAN: {
                // No default value: an unset boolean filter is a no-op until the
                // user picks true/false (see renderBooleanFilterSql)
                filterRuleDefaults.values = values !== undefined ? values : [];
                break;
            }
            default:
                break;
        }
    }
    if (filterRule.operator === filter_1.FilterOperator.IN_PERIOD_TO_DATE) {
        filterRuleDefaults.settings = {
            unitOfTime: filter_1.UnitOfTime.years,
            completed: false,
        };
    }
    return {
        ...filterRule,
        values: values !== undefined && values !== null ? values : [],
        settings: undefined,
        ...filterRuleDefaults,
    };
};
exports.getFilterRuleWithDefaultValue = getFilterRuleWithDefaultValue;
const getFilterRuleFromFieldWithDefaultValue = (field, filterRule, values, timezone) => (0, exports.getFilterRuleWithDefaultValue)((0, exports.getFilterTypeFromItem)(field), field, filterRule, values, timezone);
exports.getFilterRuleFromFieldWithDefaultValue = getFilterRuleFromFieldWithDefaultValue;
const createFilterRuleFromField = (field, value, timezone) => (0, exports.getFilterRuleFromFieldWithDefaultValue)(field, {
    id: (0, uuid_1.v4)(),
    target: {
        fieldId: (0, item_1.getItemId)(field),
    },
    operator: value === null ? filter_1.FilterOperator.NULL : filter_1.FilterOperator.EQUALS,
}, value ? [value] : [], timezone);
exports.createFilterRuleFromField = createFilterRuleFromField;
const matchFieldExact = (a) => (b) => a.type === b.type && a.name === b.name && a.table === b.table;
exports.matchFieldExact = matchFieldExact;
const matchFieldByTypeAndName = (a) => (b) => a.type === b.type && a.name === b.name;
exports.matchFieldByTypeAndName = matchFieldByTypeAndName;
const matchFieldByType = (a) => (b) => a.type === b.type;
exports.matchFieldByType = matchFieldByType;
const isTileFilterable = (tile) => ![
    dashboard_1.DashboardTileTypes.MARKDOWN,
    dashboard_1.DashboardTileTypes.LOOM,
    dashboard_1.DashboardTileTypes.DATA_APP,
].includes(tile.type);
exports.isTileFilterable = isTileFilterable;
const getDefaultTileTargets = (field, availableTileFilters) => Object.entries(availableTileFilters).reduce((acc, [tileUuid, availableFilters]) => {
    if (!availableFilters)
        return acc;
    const filterableField = availableFilters.find((0, exports.matchFieldExact)(field));
    if (!filterableField)
        return acc;
    return {
        ...acc,
        [tileUuid]: {
            fieldId: (0, item_1.getItemId)(filterableField),
            tableName: filterableField.table,
        },
    };
}, {});
const applyDefaultTileTargets = (filterRule, field, availableTileFilters) => {
    if (!filterRule.tileTargets) {
        return {
            ...filterRule,
            tileTargets: getDefaultTileTargets(field, availableTileFilters),
        };
    }
    return filterRule;
};
exports.applyDefaultTileTargets = applyDefaultTileTargets;
const createDashboardFilterRuleFromField = ({ field, availableTileFilters, isTemporary, value, }) => (0, exports.getFilterRuleFromFieldWithDefaultValue)(field, {
    id: (0, uuid_1.v4)(),
    operator: value === null ? filter_1.FilterOperator.NULL : filter_1.FilterOperator.EQUALS,
    target: {
        fieldId: (0, item_1.getItemId)(field),
        tableName: field.table,
        fieldName: field.name,
    },
    tileTargets: getDefaultTileTargets(field, availableTileFilters),
    disabled: !isTemporary,
    label: undefined,
}, !(0, isNil_1.default)(value) ? [value] : null);
exports.createDashboardFilterRuleFromField = createDashboardFilterRuleFromField;
const getDefaultTileSqlTargets = (column, availableTileColumns) => Object.entries(availableTileColumns).reduce((acc, [tileUuid, availableColumns]) => {
    if (!availableColumns)
        return acc;
    const filterableField = availableColumns.find((target) => target.reference === column.reference);
    if (!filterableField)
        return acc;
    return {
        ...acc,
        [tileUuid]: {
            fieldId: filterableField.reference,
            tableName: `sql_chart`,
            isSqlColumn: true,
            fallbackType: filterableField.type,
        },
    };
}, {});
const createDashboardFilterRuleFromSqlColumn = ({ column, availableTileColumns, isTemporary, value, }) => (0, exports.getFilterRuleWithDefaultValue)((0, exports.getFilterTypeFromItemType)(column.type), undefined, {
    id: (0, uuid_1.v4)(),
    operator: value === null ? filter_1.FilterOperator.NULL : filter_1.FilterOperator.EQUALS,
    target: {
        fieldId: column.reference,
        tableName: 'sql_chart',
        isSqlColumn: true,
        fallbackType: column.type,
    },
    tileTargets: getDefaultTileSqlTargets(column, availableTileColumns),
    disabled: !isTemporary,
    label: undefined,
}, !(0, isNil_1.default)(value) ? [value] : null);
exports.createDashboardFilterRuleFromSqlColumn = createDashboardFilterRuleFromSqlColumn;
const addFilterRule = ({ filters, field, value, timezone, }) => {
    const groupKey = ((f) => {
        if ((0, field_1.isDimension)(f) || (0, field_1.isCustomSqlDimension)(f)) {
            return 'dimensions';
        }
        if ((0, field_1.isTableCalculation)(f)) {
            return 'tableCalculations';
        }
        return 'metrics';
    })(field);
    const group = filters[groupKey];
    return {
        ...filters,
        [groupKey]: {
            id: (0, uuid_1.v4)(),
            ...group,
            [(0, exports.getFilterGroupItemsPropertyName)(group)]: [
                ...(0, exports.getItemsFromFilterGroup)(group),
                (0, exports.createFilterRuleFromField)(field, value, timezone),
            ],
        },
    };
};
exports.addFilterRule = addFilterRule;
/**
 * Takes a filter group and flattens it by merging nested groups into the parent group if they are the same filter group type
 * @param filterGroup - The filter group to flatten
 * @returns Flattened filter group
 */
const flattenSameFilterGroupType = (filterGroup) => {
    const items = (0, exports.getItemsFromFilterGroup)(filterGroup);
    return {
        id: filterGroup.id,
        [(0, exports.getFilterGroupItemsPropertyName)(filterGroup)]: items.reduce((acc, item) => {
            if ((0, filter_1.isFilterGroup)(item)) {
                const flatGroup = flattenSameFilterGroupType(item);
                // If the parent group is the same type as the current group, we merge the current group items into the parent group
                if ((0, exports.getFilterGroupItemsPropertyName)(flatGroup) ===
                    (0, exports.getFilterGroupItemsPropertyName)(filterGroup)) {
                    return [...acc, ...(0, exports.getItemsFromFilterGroup)(flatGroup)];
                }
                // If the parent group is not the same type as the current group, we just add the current group as an item
                return [...acc, flatGroup];
            }
            return [...acc, item];
        }, []),
    };
};
/**
 * Checks if a dimension value is an invalid date before it is added to the filter
 * @param item - The field to compare against the value
 * @param value - The value to check
 * @returns True if the value is an invalid date, false otherwise
 */
const isDimensionValueInvalidDate = (item, value) => (0, item_1.isDateItem)(item) && value.raw === 'Invalid Date'; // Message from moment.js when it can't parse a date
exports.isDimensionValueInvalidDate = isDimensionValueInvalidDate;
/**
 * Takes a filter group and build a filters object from it based on the field type
 * @param filterGroup - The filter group to extract filters from
 * @param fields - Fields to compare against the filter group items to determine types
 * @returns Filters object with dimensions, metrics, and table calculations
 */
const getFiltersFromGroup = (filterGroup, fields) => {
    const flatFilterGroup = flattenSameFilterGroupType(filterGroup);
    const items = (0, exports.getItemsFromFilterGroup)(flatFilterGroup);
    return items.reduce((accumulator, item) => {
        if ((0, filter_1.isFilterRule)(item)) {
            // when filter group item is a filter rule, we find the field it's targeting
            const fieldInRule = fields.find((field) => (0, item_1.getItemId)(field) === item.target.fieldId);
            // determine the type of the field and add the rule it to the correct filters object property
            // always keep the parent filter group type (AND/OR) when adding the filter rules
            if (fieldInRule) {
                if ((0, field_1.isDimension)(fieldInRule) ||
                    (0, field_1.isCustomSqlDimension)(fieldInRule)) {
                    accumulator.dimensions = {
                        id: (0, uuid_1.v4)(),
                        ...accumulator.dimensions,
                        [(0, exports.getFilterGroupItemsPropertyName)(flatFilterGroup)]: [
                            ...(0, exports.getItemsFromFilterGroup)(accumulator.dimensions),
                            item,
                        ],
                    };
                }
                else if ((0, field_1.isTableCalculation)(fieldInRule)) {
                    accumulator.tableCalculations = {
                        id: (0, uuid_1.v4)(),
                        ...accumulator.tableCalculations,
                        [(0, exports.getFilterGroupItemsPropertyName)(flatFilterGroup)]: [
                            ...(0, exports.getItemsFromFilterGroup)(accumulator.tableCalculations),
                            item,
                        ],
                    };
                }
                else {
                    accumulator.metrics = {
                        id: (0, uuid_1.v4)(),
                        ...accumulator.metrics,
                        [(0, exports.getFilterGroupItemsPropertyName)(flatFilterGroup)]: [
                            ...(0, exports.getItemsFromFilterGroup)(accumulator.metrics),
                            item,
                        ],
                    };
                }
            }
        }
        if ((0, filter_1.isFilterGroup)(item)) {
            // when filter group item is a filter group, we need to recursively call this function to extract filters objects from the nested group
            // then we add each field type filter group - from nested filters group - into the correct parent filters object property keeping the parent filter group type (AND/OR)
            const filters = (0, exports.getFiltersFromGroup)(item, fields);
            if (filters.dimensions) {
                accumulator.dimensions = {
                    id: (0, uuid_1.v4)(),
                    ...accumulator.dimensions,
                    [(0, exports.getFilterGroupItemsPropertyName)(flatFilterGroup)]: [
                        ...(0, exports.getItemsFromFilterGroup)(accumulator.dimensions),
                        // Preserve the original group ID so React can maintain
                        // stable keys and avoid unmounting/remounting nested
                        // FilterGroupForm components on every filter edit
                        { ...filters.dimensions, id: item.id },
                    ],
                };
            }
            if (filters.metrics) {
                accumulator.metrics = {
                    id: (0, uuid_1.v4)(),
                    ...accumulator.metrics,
                    [(0, exports.getFilterGroupItemsPropertyName)(flatFilterGroup)]: [
                        ...(0, exports.getItemsFromFilterGroup)(accumulator.metrics),
                        { ...filters.metrics, id: item.id },
                    ],
                };
            }
            if (filters.tableCalculations) {
                accumulator.tableCalculations = {
                    id: (0, uuid_1.v4)(),
                    ...accumulator.tableCalculations,
                    [(0, exports.getFilterGroupItemsPropertyName)(flatFilterGroup)]: [
                        ...(0, exports.getItemsFromFilterGroup)(accumulator.tableCalculations),
                        { ...filters.tableCalculations, id: item.id },
                    ],
                };
            }
        }
        return accumulator;
    }, {});
};
exports.getFiltersFromGroup = getFiltersFromGroup;
const deleteFilterRuleFromGroup = (filterGroup, id) => {
    const items = (0, exports.getItemsFromFilterGroup)(filterGroup);
    // If the filter group contains the rule we want to delete, we remove it
    if (items.some((rule) => rule.id === id)) {
        return {
            id: filterGroup.id,
            [(0, exports.getFilterGroupItemsPropertyName)(filterGroup)]: items.filter((rule) => rule.id !== id),
        };
    }
    const groupGroups = items.filter(filter_1.isFilterGroup);
    const groupItems = items.filter(filter_1.isFilterRule);
    // If the filter group contains nested groups, we recursively call this function on each nested group
    const newGroups = groupGroups.map((group) => (0, exports.deleteFilterRuleFromGroup)(group, id));
    return {
        id: filterGroup.id,
        [(0, exports.getFilterGroupItemsPropertyName)(filterGroup)]: [
            ...groupItems,
            ...newGroups,
        ],
    };
};
exports.deleteFilterRuleFromGroup = deleteFilterRuleFromGroup;
const getDashboardFilterRulesForTile = (tileUuid, rules, needsExplicitTileOverride = false) => rules
    // Skip disabled and unset (empty) filters — neither applies to a tile.
    .filter((rule) => !rule.disabled && !(0, exports.isEmptyDashboardFilterRule)(rule))
    .map((filter) => {
    const tileConfig = filter.tileTargets?.[tileUuid];
    // If the config is false, we remove this filter
    if (tileConfig === false) {
        return null;
    }
    // If the tile isn't in the tileTarget overrides,
    // we return the filter and don't treat this tile
    // differently.
    if (tileConfig === undefined) {
        // If needs explicit override, we remove this filter
        if (needsExplicitTileOverride) {
            return null;
        }
        return filter;
    }
    return {
        ...filter,
        target: tileConfig,
    };
})
    .filter((f) => f !== null);
exports.getDashboardFilterRulesForTile = getDashboardFilterRulesForTile;
const getTabUuidsForFilterRules = (dashboardTiles, filters, filterableFieldsByTileUuid) => {
    if (!dashboardTiles)
        return {};
    return dashboardTiles.reduce((acc, tile) => {
        if (!tile.tabUuid || !(0, exports.isTileFilterable)(tile)) {
            return acc;
        }
        const filterIdsForTile = (0, exports.getDashboardFilterRulesForTile)(tile.uuid, filters.dimensions)
            .filter((filterRule) => {
            const tileConfig = filterRule.tileTargets?.[tile.uuid];
            // TODO: Move this fallback logic to the getDashboardFilterRulesForTile function
            if (tileConfig === undefined && filterableFieldsByTileUuid) {
                return filterableFieldsByTileUuid[tile.uuid]?.some((f) => (0, item_1.getItemId)(f) === filterRule.target.fieldId);
            }
            // Apply filter to tile
            return !!tileConfig;
        })
            .map((tileFilter) => tileFilter.id);
        // Set filter id as key and tile tab uuids as values
        filterIdsForTile.forEach((filterId) => {
            if (!acc[filterId]) {
                acc[filterId] = [];
            }
            if (tile.tabUuid && !acc[filterId].includes(tile.tabUuid)) {
                acc[filterId].push(tile.tabUuid);
            }
        });
        return acc;
    }, {});
};
exports.getTabUuidsForFilterRules = getTabUuidsForFilterRules;
const getDashboardFilterRulesForTileAndReferences = (tileUuid, references, rules) => (0, exports.getDashboardFilterRulesForTile)(tileUuid, rules, true).filter((f) => f.target.isSqlColumn && references.includes(f.target.fieldId));
exports.getDashboardFilterRulesForTileAndReferences = getDashboardFilterRulesForTileAndReferences;
/**
 * Filter dashboard filter rules to those whose target field id is actually
 * present in the explore.
 *
 * Previously this matched by `target.tableName` against the explore's joined
 * table aliases. That diverged from the UI's "is this filter applicable to
 * this tile?" check, which matches by field id (see TileFilterConfiguration's
 * `selectedField` computation). When a filter rule's `target.tableName` is
 * stale (e.g. the dbt model was renamed since the filter was created) but
 * `target.fieldId` still resolves against the explore, the UI rendered the
 * filter as applied while the server silently dropped it — resulting in
 * unfiltered data on dashboards (notably a PII leak risk for org-scoping
 * filters). Matching by field id makes both checks use the same source of
 * truth.
 */
const getDashboardFilterRulesForTables = (availableFieldIds, rules) => rules.filter((f) => 
// Unset (empty) filters don't apply, so they must not appear in the
// applied-filters set that callers diff against chart-level filters.
availableFieldIds.includes(f.target.fieldId) &&
    !(0, exports.isEmptyDashboardFilterRule)(f));
exports.getDashboardFilterRulesForTables = getDashboardFilterRulesForTables;
const getDashboardFilterRulesForTileAndTables = (tileUuid, availableFieldIds, rules) => (0, exports.getDashboardFilterRulesForTables)(availableFieldIds, (0, exports.getDashboardFilterRulesForTile)(tileUuid, rules));
exports.getDashboardFilterRulesForTileAndTables = getDashboardFilterRulesForTileAndTables;
const getDashboardFiltersForTileAndTables = (tileUuid, availableFieldIds, dashboardFilters) => ({
    dimensions: (0, exports.getDashboardFilterRulesForTileAndTables)(tileUuid, availableFieldIds, dashboardFilters.dimensions),
    metrics: (0, exports.getDashboardFilterRulesForTileAndTables)(tileUuid, availableFieldIds, dashboardFilters.metrics),
    tableCalculations: (0, exports.getDashboardFilterRulesForTileAndTables)(tileUuid, availableFieldIds, dashboardFilters.tableCalculations),
});
exports.getDashboardFiltersForTileAndTables = getDashboardFiltersForTileAndTables;
const getDashboardFiltersForTile = (tileUuid, dashboardFilters, dashboardTemporaryFilters) => ({
    dimensions: (0, exports.getDashboardFilterRulesForTile)(tileUuid, [
        ...dashboardFilters.dimensions,
        ...(dashboardTemporaryFilters?.dimensions ?? []),
    ]),
    metrics: (0, exports.getDashboardFilterRulesForTile)(tileUuid, [
        ...dashboardFilters.metrics,
        ...(dashboardTemporaryFilters?.metrics ?? []),
    ]),
    tableCalculations: (0, exports.getDashboardFilterRulesForTile)(tileUuid, [
        ...dashboardFilters.tableCalculations,
        ...(dashboardTemporaryFilters?.tableCalculations ?? []),
    ]),
});
exports.getDashboardFiltersForTile = getDashboardFiltersForTile;
// When a dashboard has no tabs, the lock toggle stores the dashboard uuid as
// a sentinel in lockedTabUuids — so any non-empty list means "locked".
const isFilterLockedOnTab = (rule, tabUuid, hasTabs) => {
    if (!rule.lockedTabUuids || rule.lockedTabUuids.length === 0)
        return false;
    if (!hasTabs)
        return true;
    if (!tabUuid)
        return false;
    return rule.lockedTabUuids.includes(tabUuid);
};
exports.isFilterLockedOnTab = isFilterLockedOnTab;
const buildLockedTargetKeysForTab = (rules, tabUuid, hasTabs) => {
    const keys = new Set();
    rules.forEach((rule) => {
        if ((0, exports.isFilterLockedOnTab)(rule, tabUuid, hasTabs)) {
            keys.add(`${rule.target.tableName}::${rule.target.fieldId}`);
        }
    });
    return keys;
};
const dropRulesTargetingLockedFields = (overrideRules, lockedKeys) => {
    if (lockedKeys.size === 0) {
        return { kept: overrideRules, droppedCount: 0 };
    }
    let droppedCount = 0;
    const kept = overrideRules.filter((rule) => {
        const key = `${rule.target.tableName}::${rule.target.fieldId}`;
        if (lockedKeys.has(key)) {
            droppedCount += 1;
            return false;
        }
        return true;
    });
    return { kept, droppedCount };
};
/**
 * Drop override rules that target a field whose saved filter is locked on the
 * given tab. For tab-less dashboards (`hasTabs=false`) any non-empty
 * `lockedTabUuids` is treated as a dashboard-wide lock. For tabbed dashboards
 * `tabUuid` is required to decide; when undefined nothing is stripped.
 */
const stripOverridesForLockedFiltersOnTab = (saved, overrides, tabUuid, hasTabs) => {
    const dimensions = dropRulesTargetingLockedFields(overrides.dimensions, buildLockedTargetKeysForTab(saved.dimensions, tabUuid, hasTabs));
    const metrics = dropRulesTargetingLockedFields(overrides.metrics, buildLockedTargetKeysForTab(saved.metrics, tabUuid, hasTabs));
    const tableCalculations = dropRulesTargetingLockedFields(overrides.tableCalculations, buildLockedTargetKeysForTab(saved.tableCalculations, tabUuid, hasTabs));
    return {
        filters: {
            dimensions: dimensions.kept,
            metrics: metrics.kept,
            tableCalculations: tableCalculations.kept,
        },
        droppedCount: dimensions.droppedCount +
            metrics.droppedCount +
            tableCalculations.droppedCount,
    };
};
exports.stripOverridesForLockedFiltersOnTab = stripOverridesForLockedFiltersOnTab;
const combineFilterGroups = (a, b) => ({
    id: (0, uuid_1.v4)(),
    and: [a, b].filter((f) => !!f),
});
const addFiltersToMetricQuery = (metricQuery, filters) => ({
    ...metricQuery,
    filters: {
        dimensions: combineFilterGroups(metricQuery.filters?.dimensions, filters.dimensions),
        metrics: combineFilterGroups(metricQuery.filters?.metrics, filters.metrics),
        tableCalculations: combineFilterGroups(metricQuery.filters?.tableCalculations, filters.tableCalculations),
    },
});
exports.addFiltersToMetricQuery = addFiltersToMetricQuery;
/**
 * This function is used to override the chart filter with the dashboard filter
 * if the dashboard filter is a time or date dimension and the chart filter is a different granularity of the same dimension
 * or if the dashboard filter is the same dimension as the chart filter
 * Example:
 * Chart has a filter order_date_month; User applies Dashboard filter order_date_year
 * The chart filter will be overridden with the dashboard filter
 * set in the timeBasedOverrideMap so we know which metric filter to override
 * Another example:
 * Chart has a filter is_completed: true; User applies Dashboard filter is_completed: false
 * The chart filter will be overridden with the dashboard filter (is_completed: false)
 * @param item - The item to override
 * @param filterRulesList - The list of filter rules to check against
 * @param timeBasedOverrideMap - The map of overridden filters
 * @returns The overridden item
 */
const findAndOverrideChartFilter = (item, filterRulesList, timeBasedOverrideMap) => {
    const identicalDashboardFilter = (0, filter_1.isFilterRule)(item)
        ? filterRulesList.find((dashboardFilter) => {
            const overrideData = timeBasedOverrideMap?.[dashboardFilter.id];
            return (overrideData?.fieldsToChange.includes(item.target.fieldId) ||
                dashboardFilter.target.fieldId === item.target.fieldId);
        })
        : undefined;
    return identicalDashboardFilter
        ? {
            ...item,
            target: {
                fieldId: identicalDashboardFilter.target.fieldId,
            },
            id: identicalDashboardFilter.id,
            values: identicalDashboardFilter.values,
            ...(identicalDashboardFilter.settings && {
                settings: identicalDashboardFilter.settings,
            }),
            operator: identicalDashboardFilter.operator,
        }
        : item;
};
const overrideChartFilter = (filterGroup, filterRules, timeBasedOverrideMap) => (0, filter_1.isAndFilterGroup)(filterGroup)
    ? {
        id: filterGroup.id,
        and: filterGroup.and.map((item) => findAndOverrideChartFilter(item, filterRules, timeBasedOverrideMap)),
    }
    : {
        id: filterGroup.id,
        or: filterGroup.or.map((item) => findAndOverrideChartFilter(item, filterRules, timeBasedOverrideMap)),
    };
exports.overrideChartFilter = overrideChartFilter;
const getDeduplicatedFilterRules = (filterRules, filterGroup) => {
    const groupFilterRules = (0, exports.getFilterRulesFromGroup)(filterGroup);
    return filterRules.filter((rule) => !groupFilterRules.some((groupRule) => groupRule.id === rule.id));
};
/**
 * Merges dashboard filters with existing filters using override tracking
 * @param filterGroup - Existing filter group to override
 * @param filterRules - Dashboard filter rules to apply
 * @param timeBasedOverrideMap - Dictionary tracking time-based field relationships
 * @returns New combined filter group with overrides applied
 */
const overrideFilterGroupWithFilterRules = (filterGroup, filterRules, timeBasedOverrideMap) => {
    if (!filterGroup) {
        return {
            id: (0, uuid_1.v4)(),
            and: filterRules,
        };
    }
    const overriddenGroup = (0, exports.overrideChartFilter)(filterGroup, filterRules, timeBasedOverrideMap);
    // deduplicate the dashboard filter rules from the ones used when overriding the chart filterGroup
    const deduplicatedRules = getDeduplicatedFilterRules(filterRules, overriddenGroup);
    // if it's AND group we don't need to sub-group the rules - all can be in the same group
    // if it's OR group we need to sub-group the rules
    const overridenGroupItems = (0, filter_1.isAndFilterGroup)(overriddenGroup)
        ? overriddenGroup.and
        : [overriddenGroup];
    return {
        id: (0, uuid_1.v4)(),
        and: [...overridenGroupItems, ...deduplicatedRules],
    };
};
exports.overrideFilterGroupWithFilterRules = overrideFilterGroupWithFilterRules;
const convertDashboardFilterRuleToFilterRule = (dashboardFilterRule) => ({
    id: dashboardFilterRule.id,
    target: {
        // ...dashboardFilterRule.target,
        fieldId: dashboardFilterRule.target.fieldId,
    },
    operator: dashboardFilterRule.operator,
    values: dashboardFilterRule.values,
    ...(dashboardFilterRule.settings && {
        settings: dashboardFilterRule.settings,
    }),
    ...(dashboardFilterRule.disabled && {
        disabled: dashboardFilterRule.disabled,
    }),
});
const getFieldIdWithoutTable = (fieldId, tableName) => fieldId.replace(`${tableName}_`, '');
const getBaseTimeDimensionName = (timeDimension) => {
    if (timeDimension.isIntervalBase)
        return timeDimension.name;
    return timeDimension.timeIntervalBaseDimensionName;
};
/**
 * Tracks time-based metric filters that need overriding via external map
 * @param metricQueryDimensionFilters - Existing dimension filters in the query
 * @param dashboardFilterRule - Dashboard filter being applied
 * @param explore - Explore context for field relationships
 * @returns Filter rule with override data for external tracking
 */
const trackWhichTimeBasedMetricFiltersToOverride = (metricQueryDimensionFilters, dashboardFilterRule, explore) => {
    if (!explore)
        return { filter: dashboardFilterRule };
    const baseDimension = explore.tables[dashboardFilterRule.target.tableName]?.dimensions[getFieldIdWithoutTable(dashboardFilterRule.target.fieldId, dashboardFilterRule.target.tableName)];
    const baseTimeDimensionName = baseDimension
        ? getBaseTimeDimensionName(baseDimension)
        : undefined;
    if (!baseTimeDimensionName) {
        return { filter: dashboardFilterRule };
    }
    const traverseFilterGroup = (filterGroup) => {
        if (!filterGroup)
            return [];
        return (0, exports.getItemsFromFilterGroup)(filterGroup).reduce((acc, item) => {
            if ((0, filter_1.isFilterGroup)(item)) {
                return [...acc, ...traverseFilterGroup(item)];
            }
            if ((0, filter_1.isFilterRule)(item)) {
                const itemFieldId = getFieldIdWithoutTable(item.target.fieldId, dashboardFilterRule.target.tableName);
                const itemDimension = explore.tables[dashboardFilterRule.target.tableName]
                    ?.dimensions[itemFieldId];
                const isTimeOrDateDimension = itemDimension !== undefined &&
                    getBaseTimeDimensionName(itemDimension) ===
                        baseTimeDimensionName;
                if (isTimeOrDateDimension) {
                    return [...acc, item.target.fieldId];
                }
            }
            return acc;
        }, []);
    };
    const fieldsToChange = traverseFilterGroup(metricQueryDimensionFilters);
    return fieldsToChange.length > 0
        ? {
            filter: dashboardFilterRule,
            overrideData: {
                baseTimeDimensionName,
                fieldsToChange,
            },
        }
        : { filter: dashboardFilterRule };
};
exports.trackWhichTimeBasedMetricFiltersToOverride = trackWhichTimeBasedMetricFiltersToOverride;
/**
 * Adds dashboard filters to a metric query while tracking time-based overrides
 * @param metricQuery - The original metric query
 * @param dashboardFilters - Dashboard filters to apply
 * @param explore - Explore context for field validation
 * @returns Enhanced metric query with merged filters and override map
 */
const addDashboardFiltersToMetricQuery = (metricQuery, dashboardFilters, explore) => {
    const timeBasedOverrideMap = {};
    // Drop unset (empty) filters so they neither add a clause nor replace a
    // chart-level filter on the same field.
    const processedDimensionFilters = dashboardFilters.dimensions
        .filter((filter) => !(0, exports.isEmptyDashboardFilterRule)(filter))
        .map((filter) => {
        const result = (0, exports.trackWhichTimeBasedMetricFiltersToOverride)(metricQuery.filters?.dimensions, filter, explore);
        if (result.overrideData) {
            timeBasedOverrideMap[filter.id] = result.overrideData;
        }
        return result.filter;
    })
        .map(convertDashboardFilterRuleToFilterRule);
    return {
        ...metricQuery,
        filters: {
            dimensions: (0, exports.overrideFilterGroupWithFilterRules)(metricQuery.filters?.dimensions, processedDimensionFilters, timeBasedOverrideMap),
            metrics: (0, exports.overrideFilterGroupWithFilterRules)(metricQuery.filters?.metrics, dashboardFilters.metrics
                .filter((filter) => !(0, exports.isEmptyDashboardFilterRule)(filter))
                .map(convertDashboardFilterRuleToFilterRule), undefined),
            tableCalculations: (0, exports.overrideFilterGroupWithFilterRules)(metricQuery.filters?.tableCalculations, dashboardFilters.tableCalculations
                .filter((filter) => !(0, exports.isEmptyDashboardFilterRule)(filter))
                .map(convertDashboardFilterRuleToFilterRule), undefined),
        },
    };
};
exports.addDashboardFiltersToMetricQuery = addDashboardFiltersToMetricQuery;
// Mirrors the UI's getAvailableFiltersForSavedQueries. Matching on field-id (not tableName) is the source of truth — see getDashboardFilterRulesForTables below for why.
const getAvailableFilterFieldIds = (explore) => [
    ...Object.entries((0, fields_1.getDimensionMapFromTables)(explore.tables))
        .filter(([, field]) => (0, field_1.isFilterableDimension)(field) && !field.hidden)
        .map(([fieldId]) => fieldId),
    ...Object.entries((0, fields_1.getMetricsMapFromTables)(explore.tables))
        .filter(([, field]) => !field.hidden)
        .map(([fieldId]) => fieldId),
];
exports.getAvailableFilterFieldIds = getAvailableFilterFieldIds;
const applyDashboardFiltersForTile = ({ tileUuid, metricQuery, dashboardFilters, explore, }) => {
    const appliedDashboardFilters = (0, exports.getDashboardFiltersForTileAndTables)(tileUuid, (0, exports.getAvailableFilterFieldIds)(explore), dashboardFilters);
    return {
        metricQuery: (0, exports.addDashboardFiltersToMetricQuery)(metricQuery, appliedDashboardFilters, explore),
        appliedDashboardFilters,
    };
};
exports.applyDashboardFiltersForTile = applyDashboardFiltersForTile;
const createFilterRuleFromModelRequiredFilterRule = (filter, tableName) => ({
    id: filter.id,
    target: {
        fieldId: (0, field_1.convertFieldRefToFieldId)(filter.target.fieldRef, tableName),
    },
    operator: filter.operator,
    values: filter.values,
    ...(filter.settings?.unitOfTime && {
        settings: {
            unitOfTime: filter.settings.unitOfTime,
        },
    }),
    required: filter.required === undefined ? true : filter.required,
});
exports.createFilterRuleFromModelRequiredFilterRule = createFilterRuleFromModelRequiredFilterRule;
const isFilterRuleInQuery = (dimension, filterRule, dimensionsFilterGroup, explore) => {
    if (!dimensionsFilterGroup)
        return undefined;
    const getDimensionFromExplore = (fieldId) => Object.values(explore.tables)
        .flatMap((table) => Object.values(table.dimensions))
        .find((candidateDimension) => (0, item_1.getItemId)(candidateDimension) === fieldId);
    const isCompatibleDerivedTimeDimension = (candidateDimension) => {
        const shouldAllowSiblingTimeDimensions = dimension.isIntervalBase || dimension.timeInterval !== undefined;
        if (!shouldAllowSiblingTimeDimensions) {
            return false;
        }
        const requiredBaseDimensionName = getBaseTimeDimensionName(dimension);
        const candidateBaseDimensionName = getBaseTimeDimensionName(candidateDimension);
        return (requiredBaseDimensionName !== undefined &&
            candidateBaseDimensionName === requiredBaseDimensionName &&
            candidateDimension.table === dimension.table &&
            (0, exports.getFilterTypeFromItem)(candidateDimension) ===
                (0, exports.getFilterTypeFromItem)(dimension));
    };
    return (0, exports.getFilterRulesFromGroup)(dimensionsFilterGroup).some((queryRule) => {
        if (queryRule.target.fieldId === filterRule.target.fieldId) {
            return true;
        }
        const queryDimension = getDimensionFromExplore(queryRule.target.fieldId);
        return queryDimension
            ? isCompatibleDerivedTimeDimension(queryDimension)
            : false;
    });
};
exports.isFilterRuleInQuery = isFilterRuleInQuery;
const reduceRequiredDimensionFiltersToFilterRules = (requiredFilters, filters, explore) => {
    const table = explore.tables[explore.baseTable];
    return requiredFilters.reduce((acc, filter) => {
        let dimension;
        // This function already takes care of falling back to the base table if the fieldRef doesn't have 2 parts (falls back to base table name)
        const filterRule = (0, exports.createFilterRuleFromModelRequiredFilterRule)(filter, table.name);
        if ((0, filter_1.isJoinModelRequiredFilter)(filter)) {
            const joinedTable = explore.tables[filter.target.tableName];
            if (joinedTable) {
                dimension = Object.values(joinedTable.dimensions).find((d) => (0, item_1.getItemId)(d) === filterRule.target.fieldId);
            }
        }
        else {
            dimension = Object.values(table.dimensions).find((tc) => (0, item_1.getItemId)(tc) === filterRule.target.fieldId);
        }
        if (dimension &&
            !(0, exports.isFilterRuleInQuery)(dimension, filterRule, filters, explore)) {
            return [...acc, filterRule];
        }
        return acc;
    }, []);
};
exports.reduceRequiredDimensionFiltersToFilterRules = reduceRequiredDimensionFiltersToFilterRules;
const resetRequiredFilterRules = (filterGroup, requiredFiltersRef) => {
    // Check if the input is a valid filter group
    if (!(0, filter_1.isFilterGroup)(filterGroup))
        return filterGroup;
    const filterGroupItems = (0, filter_1.isAndFilterGroup)(filterGroup)
        ? filterGroup.and
        : filterGroup.or;
    // Iterate over each item in the filter group
    const updatedItems = filterGroupItems.map((filterGroupItem) => {
        // If the item is a filter rule, check if its id is not in the required filters reference
        if ((0, filter_1.isFilterRule)(filterGroupItem) &&
            !requiredFiltersRef.includes(filterGroupItem.target.fieldId)) {
            // Mark the filter rule as not required
            const newFilterRule = {
                ...filterGroupItem,
                required: false,
            };
            return newFilterRule;
        }
        // If the item is a nested filter group, recursively call the function
        if ((0, filter_1.isFilterGroup)(filterGroupItem)) {
            return (0, exports.resetRequiredFilterRules)(filterGroupItem, requiredFiltersRef);
        }
        return filterGroupItem;
    });
    return {
        ...filterGroup,
        [(0, exports.getFilterGroupItemsPropertyName)(filterGroup)]: updatedItems,
    };
};
exports.resetRequiredFilterRules = resetRequiredFilterRules;
/**
 * Unified view of filter requirements: every requirement is a rule, a set of
 * filters where at least one must be set. `required: true` is a one-member
 * rule; filters sharing a `requiredGroupId` form one rule. Rules are returned
 * in first-appearance order (dimensions before metrics). `required` wins when
 * hand-authored JSON sets both flags on the same filter.
 */
const getFilterRequirementRules = (dashboardFilters) => {
    const filterRules = [
        ...dashboardFilters.dimensions,
        ...dashboardFilters.metrics,
    ];
    const rules = [];
    const groupRulesById = new Map();
    filterRules.forEach((filterRule) => {
        if (filterRule.required) {
            rules.push({
                type: 'single',
                id: filterRule.id,
                members: [filterRule],
            });
            return;
        }
        if (!filterRule.requiredGroupId)
            return;
        const existingRule = groupRulesById.get(filterRule.requiredGroupId);
        if (existingRule) {
            existingRule.members.push(filterRule);
            return;
        }
        const rule = {
            type: 'group',
            id: filterRule.requiredGroupId,
            members: [filterRule],
        };
        groupRulesById.set(filterRule.requiredGroupId, rule);
        rules.push(rule);
    });
    return rules;
};
exports.getFilterRequirementRules = getFilterRequirementRules;
/**
 * A rule only satisfies a requirement when it actually filters the query:
 * disabled rules and enabled rules with a value-requiring operator and no
 * values (e.g. a stale URL override) are both valueless. Value-less operators
 * like `isNull` / `inTheCurrent` still filter.
 */
const isValuelessDashboardFilterRule = (rule) => rule.disabled === true || (0, exports.isEmptyDashboardFilterRule)(rule);
exports.isValuelessDashboardFilterRule = isValuelessDashboardFilterRule;
/** A rule is satisfied when any member actually filters the query */
const isRequirementRuleSatisfied = (rule) => rule.members.some((member) => !(0, exports.isValuelessDashboardFilterRule)(member));
exports.isRequirementRuleSatisfied = isRequirementRuleSatisfied;
/**
 * Unmet requirements over dimensions + metrics, derived from
 * `getFilterRequirementRules` so the dashboard lock and the requirement UIs
 * share a single rule derivation.
 */
const getUnmetFilterRequirements = (dashboardFilters) => (0, exports.getFilterRequirementRules)(dashboardFilters)
    .filter((rule) => !(0, exports.isRequirementRuleSatisfied)(rule))
    .map((rule) => rule.type === 'single'
    ? { type: 'single', filter: rule.members[0] }
    : { type: 'group', groupId: rule.id, filters: rule.members });
exports.getUnmetFilterRequirements = getUnmetFilterRequirements;
//# sourceMappingURL=filters.js.map