import dayjs from 'dayjs';
import isNil from 'lodash/isNil';
import moment from 'moment';
import { v4 as uuidv4 } from 'uuid';
import { DashboardTileTypes } from '../types/dashboard';
import { convertFieldRefToFieldId, DimensionType, isCustomSqlDimension, isDimension, isFilterableDimension, isTableCalculation, MetricType, TableCalculationType, } from '../types/field';
import { FilterOperator, FilterType, isAndFilterGroup, isFilterGroup, isFilterRule, isJoinModelRequiredFilter, UnitOfTime, } from '../types/filter';
import { TimeFrames } from '../types/timeFrames';
import assertUnreachable from './assertUnreachable';
import { getDimensionMapFromTables, getMetricsMapFromTables } from './fields';
import { formatDate, shouldShiftItemTimezone } from './formatting';
import { getItemId, getItemType, isDateItem } from './item';
export const getFilterRulesFromGroup = (filterGroup) => {
    if (filterGroup) {
        const items = isAndFilterGroup(filterGroup)
            ? filterGroup.and
            : filterGroup.or;
        return (items ?? []).reduce((sum, item) => isFilterGroup(item)
            ? [...sum, ...getFilterRulesFromGroup(item)]
            : [...sum, item], []);
    }
    return [];
};
export const getTotalFilterRules = (filters) => [
    ...getFilterRulesFromGroup(filters.dimensions),
    ...getFilterRulesFromGroup(filters.metrics),
    ...getFilterRulesFromGroup(filters.tableCalculations),
];
export const countTotalFilterRules = (filters) => getTotalFilterRules(filters).length;
export const hasNestedGroups = (filters) => {
    const hasGroups = (filterGroup) => {
        const items = isAndFilterGroup(filterGroup)
            ? filterGroup.and
            : filterGroup.or;
        return items.some(isFilterGroup);
    };
    return ((!!filters.dimensions && hasGroups(filters.dimensions)) ||
        (!!filters.metrics && hasGroups(filters.metrics)) ||
        (!!filters.tableCalculations && hasGroups(filters.tableCalculations)));
};
export const getItemsFromFilterGroup = (filterGroup) => {
    if (filterGroup) {
        return isAndFilterGroup(filterGroup) ? filterGroup.and : filterGroup.or;
    }
    return [];
};
export const getFilterGroupItemsPropertyName = (filterGroup) => {
    if (filterGroup) {
        return isAndFilterGroup(filterGroup) ? 'and' : 'or';
    }
    return 'and';
};
export const getFilterTypeFromItemType = (type) => {
    switch (type) {
        case DimensionType.STRING:
        case MetricType.STRING:
        case TableCalculationType.STRING:
            return FilterType.STRING;
        case DimensionType.NUMBER:
        case MetricType.NUMBER:
        case MetricType.PERCENTILE:
        case MetricType.MEDIAN:
        case MetricType.AVERAGE:
        case MetricType.COUNT:
        case MetricType.COUNT_DISTINCT:
        case MetricType.SUM:
        case MetricType.SUM_DISTINCT:
        case MetricType.AVERAGE_DISTINCT:
        case MetricType.MIN:
        case MetricType.MAX:
        case MetricType.PERCENT_OF_PREVIOUS:
        case MetricType.PERCENT_OF_TOTAL:
        case MetricType.RUNNING_TOTAL:
        case TableCalculationType.NUMBER:
            return FilterType.NUMBER;
        case DimensionType.TIMESTAMP:
        case MetricType.TIMESTAMP:
        case DimensionType.DATE:
        case MetricType.DATE:
        case TableCalculationType.DATE:
        case TableCalculationType.TIMESTAMP:
            return FilterType.DATE;
        case DimensionType.BOOLEAN:
        case MetricType.BOOLEAN:
        case TableCalculationType.BOOLEAN:
            return FilterType.BOOLEAN;
        default: {
            return assertUnreachable(type, `No filter type found for field type: ${type}`);
        }
    }
};
export const getFilterTypeFromItem = (item) => {
    const type = getItemType(item);
    return getFilterTypeFromItemType(type);
};
export const timeframeToUnitOfTime = (timeframe) => {
    switch (timeframe) {
        case TimeFrames.MILLISECOND:
            return UnitOfTime.milliseconds;
        case TimeFrames.SECOND:
            return UnitOfTime.seconds;
        case TimeFrames.MINUTE:
            return UnitOfTime.minutes;
        case TimeFrames.HOUR:
            return UnitOfTime.hours;
        case TimeFrames.DAY:
            return UnitOfTime.days;
        case TimeFrames.WEEK:
            return UnitOfTime.weeks;
        case TimeFrames.MONTH:
            return UnitOfTime.months;
        case TimeFrames.QUARTER:
            return UnitOfTime.quarters;
        case TimeFrames.YEAR:
            return UnitOfTime.years;
        default:
            return undefined;
    }
};
export const supportsSingleValue = (filterType, filterOperator) => [FilterType.STRING, FilterType.NUMBER].includes(filterType) &&
    [
        FilterOperator.EQUALS,
        FilterOperator.NOT_EQUALS,
        FilterOperator.STARTS_WITH,
        FilterOperator.ENDS_WITH,
        FilterOperator.INCLUDE,
        FilterOperator.NOT_INCLUDE,
    ].includes(filterOperator);
export const isWithValueFilter = (filterOperator) => filterOperator !== FilterOperator.NULL &&
    filterOperator !== FilterOperator.NOT_NULL &&
    filterOperator !== FilterOperator.IN_PERIOD_TO_DATE;
/**
 * 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 `[]`.
 */
export const isEmptyDashboardFilterRule = (filter) => filter.disabled !== true &&
    isWithValueFilter(filter.operator) &&
    filter.operator !== FilterOperator.IN_THE_CURRENT &&
    filter.operator !== FilterOperator.NOT_IN_THE_CURRENT &&
    filter.includeNull !== true &&
    (!Array.isArray(filter.values) || filter.values.length === 0);
export const getFilterRuleWithDefaultValue = (filterType, field, filterRule, values, timezone) => {
    const filterRuleDefaults = {};
    if (![
        FilterOperator.NULL,
        FilterOperator.NOT_NULL,
        FilterOperator.IN_PERIOD_TO_DATE,
    ].includes(filterRule.operator) &&
        values !== null) {
        switch (filterType) {
            case FilterType.DATE: {
                const value = values ? values[0] : undefined;
                const isTimestamp = !field ||
                    (isCustomSqlDimension(field)
                        ? field.dimensionType
                        : field.type) === DimensionType.TIMESTAMP;
                if (filterRule.operator === FilterOperator.IN_THE_PAST ||
                    filterRule.operator === FilterOperator.NOT_IN_THE_PAST ||
                    filterRule.operator === FilterOperator.IN_THE_NEXT ||
                    filterRule.operator === FilterOperator.IN_THE_CURRENT ||
                    filterRule.operator === FilterOperator.NOT_IN_THE_CURRENT) {
                    const numberValue = value === undefined || typeof value !== 'number'
                        ? 1
                        : value;
                    const defaultUnitOfTime = isDimension(field) && field.timeInterval
                        ? timeframeToUnitOfTime(field.timeInterval)
                        : 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
                        ? dayjs(value).format()
                        : dayjs().format();
                    filterRuleDefaults.values = [timestampValue];
                }
                else {
                    const valueIsDate = value !== undefined && typeof value !== 'number';
                    const defaultTimeIntervalValues = {
                        [TimeFrames.DAY]: moment(),
                        [TimeFrames.WEEK]: moment(valueIsDate ? value : undefined).startOf('week'),
                        [TimeFrames.QUARTER]: moment(valueIsDate ? value : undefined).startOf('quarter'),
                        [TimeFrames.MONTH]: moment(valueIsDate ? value : undefined).startOf('month'),
                        [TimeFrames.YEAR]: moment(valueIsDate ? value : undefined).startOf('year'),
                    };
                    const fieldTimeInterval = isDimension(field) && field.timeInterval
                        ? field.timeInterval
                        : undefined;
                    const defaultDate = fieldTimeInterval &&
                        defaultTimeIntervalValues[fieldTimeInterval]
                        ? defaultTimeIntervalValues[fieldTimeInterval]
                        : moment();
                    // 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 = shouldShiftItemTimezone(field)
                        ? timezone || 'UTC'
                        : 'UTC';
                    const dateValue = valueIsDate
                        ? formatDate(moment
                            .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.QUARTER
                            ? undefined
                            : fieldTimeInterval, // Use the field's time interval if it has one
                        false)
                        : formatDate(defaultDate, undefined, false);
                    filterRuleDefaults.values = [dateValue];
                }
                break;
            }
            case 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 === FilterOperator.IN_PERIOD_TO_DATE) {
        filterRuleDefaults.settings = {
            unitOfTime: UnitOfTime.years,
            completed: false,
        };
    }
    return {
        ...filterRule,
        values: values !== undefined && values !== null ? values : [],
        settings: undefined,
        ...filterRuleDefaults,
    };
};
export const getFilterRuleFromFieldWithDefaultValue = (field, filterRule, values, timezone) => getFilterRuleWithDefaultValue(getFilterTypeFromItem(field), field, filterRule, values, timezone);
export const createFilterRuleFromField = (field, value, timezone) => getFilterRuleFromFieldWithDefaultValue(field, {
    id: uuidv4(),
    target: {
        fieldId: getItemId(field),
    },
    operator: value === null ? FilterOperator.NULL : FilterOperator.EQUALS,
}, value ? [value] : [], timezone);
export const matchFieldExact = (a) => (b) => a.type === b.type && a.name === b.name && a.table === b.table;
export const matchFieldByTypeAndName = (a) => (b) => a.type === b.type && a.name === b.name;
export const matchFieldByType = (a) => (b) => a.type === b.type;
export const isTileFilterable = (tile) => ![
    DashboardTileTypes.MARKDOWN,
    DashboardTileTypes.LOOM,
    DashboardTileTypes.DATA_APP,
].includes(tile.type);
const getDefaultTileTargets = (field, availableTileFilters) => Object.entries(availableTileFilters).reduce((acc, [tileUuid, availableFilters]) => {
    if (!availableFilters)
        return acc;
    const filterableField = availableFilters.find(matchFieldExact(field));
    if (!filterableField)
        return acc;
    return {
        ...acc,
        [tileUuid]: {
            fieldId: getItemId(filterableField),
            tableName: filterableField.table,
        },
    };
}, {});
export const applyDefaultTileTargets = (filterRule, field, availableTileFilters) => {
    if (!filterRule.tileTargets) {
        return {
            ...filterRule,
            tileTargets: getDefaultTileTargets(field, availableTileFilters),
        };
    }
    return filterRule;
};
export const createDashboardFilterRuleFromField = ({ field, availableTileFilters, isTemporary, value, }) => getFilterRuleFromFieldWithDefaultValue(field, {
    id: uuidv4(),
    operator: value === null ? FilterOperator.NULL : FilterOperator.EQUALS,
    target: {
        fieldId: getItemId(field),
        tableName: field.table,
        fieldName: field.name,
    },
    tileTargets: getDefaultTileTargets(field, availableTileFilters),
    disabled: !isTemporary,
    label: undefined,
}, !isNil(value) ? [value] : null);
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,
        },
    };
}, {});
export const createDashboardFilterRuleFromSqlColumn = ({ column, availableTileColumns, isTemporary, value, }) => getFilterRuleWithDefaultValue(getFilterTypeFromItemType(column.type), undefined, {
    id: uuidv4(),
    operator: value === null ? FilterOperator.NULL : FilterOperator.EQUALS,
    target: {
        fieldId: column.reference,
        tableName: 'sql_chart',
        isSqlColumn: true,
        fallbackType: column.type,
    },
    tileTargets: getDefaultTileSqlTargets(column, availableTileColumns),
    disabled: !isTemporary,
    label: undefined,
}, !isNil(value) ? [value] : null);
export const addFilterRule = ({ filters, field, value, timezone, }) => {
    const groupKey = ((f) => {
        if (isDimension(f) || isCustomSqlDimension(f)) {
            return 'dimensions';
        }
        if (isTableCalculation(f)) {
            return 'tableCalculations';
        }
        return 'metrics';
    })(field);
    const group = filters[groupKey];
    return {
        ...filters,
        [groupKey]: {
            id: uuidv4(),
            ...group,
            [getFilterGroupItemsPropertyName(group)]: [
                ...getItemsFromFilterGroup(group),
                createFilterRuleFromField(field, value, timezone),
            ],
        },
    };
};
/**
 * 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 = getItemsFromFilterGroup(filterGroup);
    return {
        id: filterGroup.id,
        [getFilterGroupItemsPropertyName(filterGroup)]: items.reduce((acc, item) => {
            if (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 (getFilterGroupItemsPropertyName(flatGroup) ===
                    getFilterGroupItemsPropertyName(filterGroup)) {
                    return [...acc, ...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
 */
export const isDimensionValueInvalidDate = (item, value) => isDateItem(item) && value.raw === 'Invalid Date'; // Message from moment.js when it can't parse a date
/**
 * 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
 */
export const getFiltersFromGroup = (filterGroup, fields) => {
    const flatFilterGroup = flattenSameFilterGroupType(filterGroup);
    const items = getItemsFromFilterGroup(flatFilterGroup);
    return items.reduce((accumulator, item) => {
        if (isFilterRule(item)) {
            // when filter group item is a filter rule, we find the field it's targeting
            const fieldInRule = fields.find((field) => 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 (isDimension(fieldInRule) ||
                    isCustomSqlDimension(fieldInRule)) {
                    accumulator.dimensions = {
                        id: uuidv4(),
                        ...accumulator.dimensions,
                        [getFilterGroupItemsPropertyName(flatFilterGroup)]: [
                            ...getItemsFromFilterGroup(accumulator.dimensions),
                            item,
                        ],
                    };
                }
                else if (isTableCalculation(fieldInRule)) {
                    accumulator.tableCalculations = {
                        id: uuidv4(),
                        ...accumulator.tableCalculations,
                        [getFilterGroupItemsPropertyName(flatFilterGroup)]: [
                            ...getItemsFromFilterGroup(accumulator.tableCalculations),
                            item,
                        ],
                    };
                }
                else {
                    accumulator.metrics = {
                        id: uuidv4(),
                        ...accumulator.metrics,
                        [getFilterGroupItemsPropertyName(flatFilterGroup)]: [
                            ...getItemsFromFilterGroup(accumulator.metrics),
                            item,
                        ],
                    };
                }
            }
        }
        if (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 = getFiltersFromGroup(item, fields);
            if (filters.dimensions) {
                accumulator.dimensions = {
                    id: uuidv4(),
                    ...accumulator.dimensions,
                    [getFilterGroupItemsPropertyName(flatFilterGroup)]: [
                        ...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: uuidv4(),
                    ...accumulator.metrics,
                    [getFilterGroupItemsPropertyName(flatFilterGroup)]: [
                        ...getItemsFromFilterGroup(accumulator.metrics),
                        { ...filters.metrics, id: item.id },
                    ],
                };
            }
            if (filters.tableCalculations) {
                accumulator.tableCalculations = {
                    id: uuidv4(),
                    ...accumulator.tableCalculations,
                    [getFilterGroupItemsPropertyName(flatFilterGroup)]: [
                        ...getItemsFromFilterGroup(accumulator.tableCalculations),
                        { ...filters.tableCalculations, id: item.id },
                    ],
                };
            }
        }
        return accumulator;
    }, {});
};
export const deleteFilterRuleFromGroup = (filterGroup, id) => {
    const items = 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,
            [getFilterGroupItemsPropertyName(filterGroup)]: items.filter((rule) => rule.id !== id),
        };
    }
    const groupGroups = items.filter(isFilterGroup);
    const groupItems = items.filter(isFilterRule);
    // If the filter group contains nested groups, we recursively call this function on each nested group
    const newGroups = groupGroups.map((group) => deleteFilterRuleFromGroup(group, id));
    return {
        id: filterGroup.id,
        [getFilterGroupItemsPropertyName(filterGroup)]: [
            ...groupItems,
            ...newGroups,
        ],
    };
};
export const getDashboardFilterRulesForTile = (tileUuid, rules, needsExplicitTileOverride = false) => rules
    // Skip disabled and unset (empty) filters — neither applies to a tile.
    .filter((rule) => !rule.disabled && !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);
export const getTabUuidsForFilterRules = (dashboardTiles, filters, filterableFieldsByTileUuid) => {
    if (!dashboardTiles)
        return {};
    return dashboardTiles.reduce((acc, tile) => {
        if (!tile.tabUuid || !isTileFilterable(tile)) {
            return acc;
        }
        const filterIdsForTile = 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) => 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;
    }, {});
};
export const getDashboardFilterRulesForTileAndReferences = (tileUuid, references, rules) => getDashboardFilterRulesForTile(tileUuid, rules, true).filter((f) => f.target.isSqlColumn && references.includes(f.target.fieldId));
/**
 * 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.
 */
export 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) &&
    !isEmptyDashboardFilterRule(f));
export const getDashboardFilterRulesForTileAndTables = (tileUuid, availableFieldIds, rules) => getDashboardFilterRulesForTables(availableFieldIds, getDashboardFilterRulesForTile(tileUuid, rules));
export const getDashboardFiltersForTileAndTables = (tileUuid, availableFieldIds, dashboardFilters) => ({
    dimensions: getDashboardFilterRulesForTileAndTables(tileUuid, availableFieldIds, dashboardFilters.dimensions),
    metrics: getDashboardFilterRulesForTileAndTables(tileUuid, availableFieldIds, dashboardFilters.metrics),
    tableCalculations: getDashboardFilterRulesForTileAndTables(tileUuid, availableFieldIds, dashboardFilters.tableCalculations),
});
export const getDashboardFiltersForTile = (tileUuid, dashboardFilters, dashboardTemporaryFilters) => ({
    dimensions: getDashboardFilterRulesForTile(tileUuid, [
        ...dashboardFilters.dimensions,
        ...(dashboardTemporaryFilters?.dimensions ?? []),
    ]),
    metrics: getDashboardFilterRulesForTile(tileUuid, [
        ...dashboardFilters.metrics,
        ...(dashboardTemporaryFilters?.metrics ?? []),
    ]),
    tableCalculations: getDashboardFilterRulesForTile(tileUuid, [
        ...dashboardFilters.tableCalculations,
        ...(dashboardTemporaryFilters?.tableCalculations ?? []),
    ]),
});
// 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".
export 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);
};
const buildLockedTargetKeysForTab = (rules, tabUuid, hasTabs) => {
    const keys = new Set();
    rules.forEach((rule) => {
        if (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.
 */
export 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,
    };
};
const combineFilterGroups = (a, b) => ({
    id: uuidv4(),
    and: [a, b].filter((f) => !!f),
});
export 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),
    },
});
/**
 * 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 = 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;
};
export const overrideChartFilter = (filterGroup, filterRules, timeBasedOverrideMap) => 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)),
    };
const getDeduplicatedFilterRules = (filterRules, filterGroup) => {
    const groupFilterRules = 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
 */
export const overrideFilterGroupWithFilterRules = (filterGroup, filterRules, timeBasedOverrideMap) => {
    if (!filterGroup) {
        return {
            id: uuidv4(),
            and: filterRules,
        };
    }
    const overriddenGroup = 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 = isAndFilterGroup(overriddenGroup)
        ? overriddenGroup.and
        : [overriddenGroup];
    return {
        id: uuidv4(),
        and: [...overridenGroupItems, ...deduplicatedRules],
    };
};
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
 */
export 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 getItemsFromFilterGroup(filterGroup).reduce((acc, item) => {
            if (isFilterGroup(item)) {
                return [...acc, ...traverseFilterGroup(item)];
            }
            if (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 };
};
/**
 * 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
 */
export 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) => !isEmptyDashboardFilterRule(filter))
        .map((filter) => {
        const result = trackWhichTimeBasedMetricFiltersToOverride(metricQuery.filters?.dimensions, filter, explore);
        if (result.overrideData) {
            timeBasedOverrideMap[filter.id] = result.overrideData;
        }
        return result.filter;
    })
        .map(convertDashboardFilterRuleToFilterRule);
    return {
        ...metricQuery,
        filters: {
            dimensions: overrideFilterGroupWithFilterRules(metricQuery.filters?.dimensions, processedDimensionFilters, timeBasedOverrideMap),
            metrics: overrideFilterGroupWithFilterRules(metricQuery.filters?.metrics, dashboardFilters.metrics
                .filter((filter) => !isEmptyDashboardFilterRule(filter))
                .map(convertDashboardFilterRuleToFilterRule), undefined),
            tableCalculations: overrideFilterGroupWithFilterRules(metricQuery.filters?.tableCalculations, dashboardFilters.tableCalculations
                .filter((filter) => !isEmptyDashboardFilterRule(filter))
                .map(convertDashboardFilterRuleToFilterRule), undefined),
        },
    };
};
// Mirrors the UI's getAvailableFiltersForSavedQueries. Matching on field-id (not tableName) is the source of truth — see getDashboardFilterRulesForTables below for why.
export const getAvailableFilterFieldIds = (explore) => [
    ...Object.entries(getDimensionMapFromTables(explore.tables))
        .filter(([, field]) => isFilterableDimension(field) && !field.hidden)
        .map(([fieldId]) => fieldId),
    ...Object.entries(getMetricsMapFromTables(explore.tables))
        .filter(([, field]) => !field.hidden)
        .map(([fieldId]) => fieldId),
];
export const applyDashboardFiltersForTile = ({ tileUuid, metricQuery, dashboardFilters, explore, }) => {
    const appliedDashboardFilters = getDashboardFiltersForTileAndTables(tileUuid, getAvailableFilterFieldIds(explore), dashboardFilters);
    return {
        metricQuery: addDashboardFiltersToMetricQuery(metricQuery, appliedDashboardFilters, explore),
        appliedDashboardFilters,
    };
};
export const createFilterRuleFromModelRequiredFilterRule = (filter, tableName) => ({
    id: filter.id,
    target: {
        fieldId: 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,
});
export 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) => 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 &&
            getFilterTypeFromItem(candidateDimension) ===
                getFilterTypeFromItem(dimension));
    };
    return getFilterRulesFromGroup(dimensionsFilterGroup).some((queryRule) => {
        if (queryRule.target.fieldId === filterRule.target.fieldId) {
            return true;
        }
        const queryDimension = getDimensionFromExplore(queryRule.target.fieldId);
        return queryDimension
            ? isCompatibleDerivedTimeDimension(queryDimension)
            : false;
    });
};
export 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 = createFilterRuleFromModelRequiredFilterRule(filter, table.name);
        if (isJoinModelRequiredFilter(filter)) {
            const joinedTable = explore.tables[filter.target.tableName];
            if (joinedTable) {
                dimension = Object.values(joinedTable.dimensions).find((d) => getItemId(d) === filterRule.target.fieldId);
            }
        }
        else {
            dimension = Object.values(table.dimensions).find((tc) => getItemId(tc) === filterRule.target.fieldId);
        }
        if (dimension &&
            !isFilterRuleInQuery(dimension, filterRule, filters, explore)) {
            return [...acc, filterRule];
        }
        return acc;
    }, []);
};
export const resetRequiredFilterRules = (filterGroup, requiredFiltersRef) => {
    // Check if the input is a valid filter group
    if (!isFilterGroup(filterGroup))
        return filterGroup;
    const filterGroupItems = 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 (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 (isFilterGroup(filterGroupItem)) {
            return resetRequiredFilterRules(filterGroupItem, requiredFiltersRef);
        }
        return filterGroupItem;
    });
    return {
        ...filterGroup,
        [getFilterGroupItemsPropertyName(filterGroup)]: updatedItems,
    };
};
/**
 * 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.
 */
export 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;
};
/**
 * 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.
 */
export const isValuelessDashboardFilterRule = (rule) => rule.disabled === true || isEmptyDashboardFilterRule(rule);
/** A rule is satisfied when any member actually filters the query */
export const isRequirementRuleSatisfied = (rule) => rule.members.some((member) => !isValuelessDashboardFilterRule(member));
/**
 * Unmet requirements over dimensions + metrics, derived from
 * `getFilterRequirementRules` so the dashboard lock and the requirement UIs
 * share a single rule derivation.
 */
export const getUnmetFilterRequirements = (dashboardFilters) => getFilterRequirementRules(dashboardFilters)
    .filter((rule) => !isRequirementRuleSatisfied(rule))
    .map((rule) => rule.type === 'single'
    ? { type: 'single', filter: rule.members[0] }
    : { type: 'group', groupId: rule.id, filters: rule.members });
//# sourceMappingURL=filters.js.map