// (C) 2022-2026 GoodData Corporation
import { isEqual } from "lodash-es";
import { invariant } from "ts-invariant";
import { isArithmeticMeasure, isLocalIdRef, isMeasureValueFilter, isRankingFilter, isRelativeDateFilter, objRefToString, } from "@gooddata/sdk-model";
import { ARITHMETIC_OPERATORS, COMPARISON_OPERATORS, RELATIVE_OPERATORS } from "@gooddata/sdk-ui-ext";
import { AlertMetricComparatorType, } from "../../types.js";
import { createCronFromGranularity } from "./cron.js";
import { getAttributeRelatedFilterInfo, getMeasureFormat, getMeasureTitle, } from "./getters.js";
//alerts transformations
/**
 * This function transforms alert by metric. It changes title, condition and execution based on selected metric.
 * @param metrics - all available metrics
 * @param alert - alert to transform
 * @param measure - selected metric
 * @param measureFormatMap - all available measures from catalog
 * @param weekStart - week start for relative date filter
 * @param timezone - timezone for relative date filter
 */
export function transformAlertByMetric(metrics, alert, measure, measureFormatMap, weekStart, timezone) {
    const periodMeasure = measure.comparators.find((c) => c.comparator === AlertMetricComparatorType.PreviousPeriod ||
        c.comparator === AlertMetricComparatorType.SamePeriodPreviousYear);
    if (alert.alert?.condition.type === "anomalyDetection" && periodMeasure?.dataset) {
        const cond = transformToAnomalyDetectionCondition(alert.alert.condition, periodMeasure);
        const condition = {
            ...cond,
            measure: {
                ...cond.measure,
                id: measure.measure.measure.localIdentifier,
                format: getMeasureFormat(measure.measure, measureFormatMap),
                title: getMeasureTitle(measure.measure),
            },
        };
        const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, condition, measure, periodMeasure);
        return {
            ...alert,
            alert: {
                ...alert.alert,
                condition,
                execution,
            },
            schedule: {
                cron: createCronFromGranularity(alert.alert.condition.granularity, weekStart),
                timezone,
            },
            metadata: {
                ...metadata,
                originalSchedule: alert.schedule ? { ...alert.schedule } : undefined,
            },
        };
    }
    if (alert.alert?.condition.type === "relative" && periodMeasure) {
        const cond = transformToRelativeCondition(alert.alert.condition);
        const condition = {
            ...cond,
            measure: {
                ...cond.measure,
                ...transformRelativeCondition(measure, periodMeasure, measureFormatMap),
            },
        };
        const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, condition, measure, periodMeasure);
        return {
            ...alert,
            alert: {
                ...alert.alert,
                condition,
                execution,
            },
            metadata,
        };
    }
    const cond = transformToComparisonCondition(alert.alert.condition);
    const condition = {
        ...cond,
        left: {
            id: measure.measure.measure.localIdentifier,
            format: getMeasureFormat(measure.measure, measureFormatMap),
            title: getMeasureTitle(measure.measure),
        },
    };
    const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, condition, measure, undefined);
    return {
        ...alert,
        alert: {
            ...alert.alert,
            condition,
            execution,
        },
        metadata,
    };
}
/**
 * This function get selected attributed and optionally value amd added this attribute to execution.
 * If value is provided, it creates positive attribute filter with this value, otherwise it creates negative attribute filter
 * that is empty for now.
 * @param attributes - all available attributes
 * @param alert - alert to transform
 * @param attr - optionally selected attribute
 * @param value - optionally selected value
 */
export function transformAlertByAttribute(attributes, alert, attr, value) {
    const { filterDefinition } = getAttributeRelatedFilterInfo(attributes, alert, alert.alert?.execution.attributes[0]);
    const originalFilters = alert.alert?.execution.filters.filter((f) => !isEqual(f, filterDefinition)) ?? [];
    const customFilters = [];
    if (attr) {
        if (value === undefined) {
            customFilters.push({
                negativeAttributeFilter: {
                    displayForm: {
                        localIdentifier: attr.attribute.attribute.localIdentifier,
                    },
                    notIn: {
                        values: [],
                    },
                    localIdentifier: undefined,
                },
            });
        }
        else {
            customFilters.push({
                positiveAttributeFilter: {
                    displayForm: {
                        localIdentifier: attr.attribute.attribute.localIdentifier,
                    },
                    in: {
                        values: [value.name],
                    },
                    localIdentifier: undefined,
                },
            });
        }
    }
    return {
        ...alert,
        alert: {
            ...alert.alert,
            execution: {
                ...alert.alert.execution,
                attributes: attr ? [ensureAttributeShowAllValues(attr.attribute)] : [],
                filters: [...originalFilters, ...customFilters],
            },
        },
    };
}
// Fix equality check for attributes (converted from backend, this prop is always there)
function ensureAttributeShowAllValues(attribute) {
    return {
        ...attribute,
        attribute: {
            ...attribute.attribute,
            showAllValues: attribute.attribute.showAllValues ?? false,
        },
    };
}
/**
 * This function transforms alert by comparison operator. It changes operator in condition.
 * @param metrics - all available metrics
 * @param alert - alert to transform
 * @param measure - selected metric
 * @param comparisonOperator - selected comparison operator
 */
export function transformAlertByComparisonOperator(metrics, alert, measure, comparisonOperator) {
    const cond = transformToComparisonCondition(alert.alert.condition);
    const condition = {
        ...cond,
        operator: comparisonOperator,
    };
    const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, condition, measure, undefined);
    return {
        ...alert,
        alert: {
            ...alert.alert,
            condition,
            execution,
        },
        ...(alert.metadata?.originalSchedule
            ? {
                schedule: alert.metadata.originalSchedule,
            }
            : {}),
        metadata: {
            ...metadata,
            originalSchedule: undefined,
        },
    };
}
/**
 * This function transforms alert by relative operator. It changes operator in condition and measure in condition.
 * @param metrics - all available metrics
 * @param alert - alert to transform
 * @param measure - selected metric
 * @param relativeOperator - selected relative operator
 * @param arithmeticOperator - selected arithmetic operator
 * @param measureFormatMap - all available measures from catalog
 * @param comparatorType - selected comparator type
 * @param granularity - selected granularity
 */
export function transformAlertByRelativeOperator(metrics, alert, measure, relativeOperator, arithmeticOperator, measureFormatMap, comparatorType, granularity) {
    const periodMeasure = measure.comparators.filter((c) => comparatorType === undefined
        ? true
        : c.comparator === comparatorType && (granularity ? c.granularity === granularity : true));
    const cond = transformToRelativeCondition(alert.alert.condition);
    const condition = {
        ...cond,
        measure: {
            ...cond.measure,
            operator: arithmeticOperator,
            ...transformRelativeCondition(measure, periodMeasure[0], measureFormatMap),
        },
        operator: relativeOperator,
    };
    const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, condition, measure, periodMeasure[0]);
    return {
        ...alert,
        alert: {
            ...alert.alert,
            condition,
            execution,
        },
        ...(alert.metadata?.originalSchedule
            ? {
                schedule: alert.metadata.originalSchedule,
            }
            : {}),
        metadata: {
            ...metadata,
            originalSchedule: undefined,
        },
    };
}
export function transformAlertByAnomalyDetection(metrics, alert, measure, weekStart, timezone, enableAlertOncePerInterval) {
    const periodMeasure = measure.comparators;
    const first = periodMeasure[0];
    const cond = transformToAnomalyDetectionCondition(alert.alert.condition, first);
    const condition = {
        ...cond,
    };
    const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, condition, measure, first);
    return {
        ...alert,
        alert: {
            ...alert.alert,
            condition,
            execution,
            ...(enableAlertOncePerInterval
                ? {
                    trigger: {
                        state: "ACTIVE",
                        ...alert.alert?.trigger,
                        mode: "ONCE_PER_INTERVAL",
                        interval: cond.granularity === "HOUR" ? "DAY" : cond.granularity,
                    },
                }
                : {}),
        },
        schedule: {
            cron: createCronFromGranularity(cond.granularity, weekStart),
            timezone,
        },
        metadata: {
            ...metadata,
            originalSchedule: alert.schedule ? { ...alert.schedule } : undefined,
        },
    };
}
/**
 * This function transforms alert by threshold value. It changes threshold in condition.
 * @param alert - alert to transform
 * @param value - selected threshold value
 */
export function transformAlertByValue(alert, value) {
    if (alert.alert?.condition.type === "anomalyDetection") {
        // Do nothing, anomaly detection does not have threshold
        return { ...alert };
    }
    if (alert.alert?.condition.type === "relative") {
        return {
            ...alert,
            alert: {
                ...alert.alert,
                condition: {
                    ...alert.alert.condition,
                    threshold: value,
                },
            },
        };
    }
    return {
        ...alert,
        alert: {
            ...alert.alert,
            condition: {
                ...alert.alert.condition,
                right: value,
            },
        },
    };
}
/**
 * This function transforms alert by title. It changes title of alert.
 * @param alert - alert to transform
 * @param title - title of alert
 */
export function transformAlertByTitle(alert, title) {
    return {
        ...alert,
        title: title ?? "",
        description: "",
    };
}
/**
 * This function transforms alert by notification channel. It changes notification channel in alert.
 * @param alert - alert to transform
 * @param notificationChannel - selected notification channel
 * @param recipients - optionally selected recipients
 */
export function transformAlertByDestination(alert, notificationChannel, recipients) {
    return {
        ...alert,
        ...(recipients ? { recipients } : {}),
        notificationChannel,
    };
}
/**
 * This function transforms alert execution by metric. It changes measures and auxMeasures in execution based on selected metric.
 * @param metrics - all available metrics
 * @param alert - alert
 * @param condition - alert condition
 * @param measure - selected metric
 * @param periodMeasure - alert comparison
 */
export function transformAlertExecutionByMetric(metrics, alert, condition, measure, periodMeasure) {
    const execution = alert.alert?.execution;
    // Remove all filters that are create for need of alert
    const localFilters = alert.metadata?.filters ?? [];
    const originalFilters = execution?.filters.filter((filter) => {
        if (isRelativeDateFilter(filter)) {
            return !localFilters.includes(filter.relativeDateFilter.localIdentifier ?? "");
        }
        return true;
    }) ?? [];
    if (condition.type === "relative" && periodMeasure) {
        const addedFilters = [];
        // Add filter for period measure only if can be defined
        // For example headline not used this at all
        if (periodMeasure.dataset && periodMeasure.granularity) {
            const localIdentifier = `relativeDateFilter_${objRefToString(periodMeasure.dataset.ref)}_${periodMeasure.granularity}`;
            const filter = {
                relativeDateFilter: {
                    from: 0,
                    to: 0,
                    dataSet: periodMeasure.dataset.ref,
                    granularity: periodMeasure.granularity,
                    localIdentifier,
                },
            };
            originalFilters.unshift(filter);
            addedFilters.push(localIdentifier);
        }
        const measures = [measure.measure, periodMeasure.measure];
        const auxMeasures = [
            ...collectAllRelatedMeasures(metrics, measure.measure),
            ...collectAllRelatedMeasures(metrics, periodMeasure.measure),
            ...collectAllRelatedMeasuresFromFilters(metrics, originalFilters),
        ];
        return {
            execution: {
                attributes: [],
                ...execution,
                filters: [...originalFilters],
                measures,
                auxMeasures: filterMeasures(auxMeasures, measures),
            },
            metadata: {
                ...alert.metadata,
                filters: addedFilters.length ? addedFilters : undefined,
            },
        };
    }
    //Anomaly detection and comparison are same
    const measures = [measure.measure];
    const auxMeasures = [
        ...collectAllRelatedMeasures(metrics, measure.measure),
        ...collectAllRelatedMeasuresFromFilters(metrics, originalFilters),
    ];
    return {
        execution: {
            attributes: [],
            ...execution,
            filters: [...originalFilters],
            measures,
            auxMeasures: filterMeasures(auxMeasures, measures),
        },
        metadata: {
            ...alert.metadata,
            filters: undefined,
        },
    };
}
//alerts transformations utils
function transformToComparisonCondition(condition) {
    if (condition.type === "relative") {
        return {
            type: "comparison",
            operator: COMPARISON_OPERATORS.COMPARISON_OPERATOR_GREATER_THAN,
            left: condition.measure.left,
            right: condition.threshold,
        };
    }
    if (condition.type === "anomalyDetection") {
        return {
            type: "comparison",
            operator: COMPARISON_OPERATORS.COMPARISON_OPERATOR_GREATER_THAN,
            left: condition.measure,
            right: undefined,
        };
    }
    return {
        type: "comparison",
        operator: condition.operator,
        right: condition.right,
        left: condition.left,
    };
}
function transformToRelativeCondition(condition) {
    if (condition.type === "comparison") {
        return {
            type: "relative",
            operator: RELATIVE_OPERATORS.RELATIVE_OPERATOR_INCREASE_BY,
            measure: {
                operator: ARITHMETIC_OPERATORS.ARITHMETIC_OPERATOR_CHANGE,
                left: condition.left,
                right: undefined,
            },
            threshold: condition.right,
        };
    }
    if (condition.type === "anomalyDetection") {
        return {
            type: "relative",
            operator: RELATIVE_OPERATORS.RELATIVE_OPERATOR_INCREASE_BY,
            measure: {
                operator: ARITHMETIC_OPERATORS.ARITHMETIC_OPERATOR_CHANGE,
                left: condition.measure,
                right: undefined,
            },
            threshold: undefined,
        };
    }
    return {
        type: "relative",
        operator: condition.operator,
        measure: condition.measure,
        threshold: condition.threshold,
    };
}
function transformToAnomalyDetectionCondition(condition, comparator) {
    invariant(comparator?.dataset, "Dataset is not set for composition in anomaly detection alert.");
    if (condition.type === "comparison") {
        return {
            type: "anomalyDetection",
            measure: condition.left,
            sensitivity: "MEDIUM",
            granularity: getGranularityFromComparator(comparator),
            dataset: comparator.dataset.ref,
        };
    }
    if (condition.type === "relative") {
        return {
            type: "anomalyDetection",
            measure: condition.measure.left,
            sensitivity: "MEDIUM",
            granularity: getGranularityFromComparator(comparator),
            dataset: comparator.dataset.ref,
        };
    }
    return {
        type: "anomalyDetection",
        measure: condition.measure,
        sensitivity: condition.sensitivity,
        granularity: condition.granularity,
        dataset: comparator.dataset.ref,
    };
}
function transformRelativeCondition(measure, periodMeasure, measureFormatMap) {
    const isNormalSetup = measure.isPrimary && !periodMeasure?.isPrimary;
    const isReverseSetup = !measure.isPrimary && periodMeasure?.isPrimary;
    //NOTE: This is case primary for headline where normal measure is in secondary bucket
    // and period measure is in primary bucket
    if (isReverseSetup) {
        return {
            left: {
                id: periodMeasure.measure.measure.localIdentifier,
                format: getMeasureFormat(periodMeasure.measure, measureFormatMap),
                title: getMeasureTitle(periodMeasure.measure),
            },
            right: {
                id: measure.measure.measure.localIdentifier,
                format: getMeasureFormat(measure.measure, measureFormatMap),
                title: getMeasureTitle(measure.measure),
            },
        };
    }
    //NOTE: This is case for normal setup where normal measure is in primary bucket
    // and period measure is in secondary bucket
    if (isNormalSetup) {
        return {
            left: {
                id: measure.measure.measure.localIdentifier,
                format: getMeasureFormat(measure.measure, measureFormatMap),
                title: getMeasureTitle(measure.measure),
            },
            right: {
                id: periodMeasure?.measure.measure.localIdentifier,
                format: getMeasureFormat(periodMeasure?.measure, measureFormatMap),
                title: periodMeasure?.measure ? getMeasureTitle(periodMeasure?.measure) : undefined,
            },
        };
    }
    //NOTE: This is case for normal setup where normal measure and period measure are
    // in same bucket, primary or secondary, but same bucket
    return {
        left: {
            id: measure.measure.measure.localIdentifier,
            format: getMeasureFormat(measure.measure, measureFormatMap),
            title: getMeasureTitle(measure.measure),
        },
        right: {
            id: periodMeasure?.measure.measure.localIdentifier,
            format: getMeasureFormat(periodMeasure?.measure, measureFormatMap),
            title: periodMeasure?.measure ? getMeasureTitle(periodMeasure?.measure) : undefined,
        },
    };
}
export function transformAlertBySensitivity(alert, sensitivity) {
    if (alert.alert?.condition.type === "anomalyDetection") {
        return {
            ...alert,
            alert: {
                ...alert.alert,
                condition: {
                    ...alert.alert.condition,
                    sensitivity,
                },
            },
        };
    }
    return alert;
}
export function transformAlertByGranularity(metrics, alert, measure, granularity, weekStart) {
    if (alert.alert?.condition.type === "anomalyDetection") {
        const periodMeasure = measure.comparators;
        const { execution, metadata } = transformAlertExecutionByMetric(metrics, alert, {
            ...alert.alert.condition,
            granularity,
        }, measure, periodMeasure[0]);
        return {
            ...alert,
            alert: {
                ...alert.alert,
                condition: {
                    ...alert.alert.condition,
                    granularity,
                },
                execution,
            },
            schedule: {
                ...alert.schedule,
                cron: createCronFromGranularity(granularity, weekStart),
            },
            metadata,
        };
    }
    return alert;
}
//utils
function collectAllRelatedMeasures(metrics, measure) {
    if (isArithmeticMeasure(measure)) {
        const included = measure.measure.definition.arithmeticMeasure.measureIdentifiers;
        const related = metrics.filter((m) => {
            return included.includes(m.measure.measure.localIdentifier);
        });
        return related.map((m) => m.measure);
    }
    return [];
}
function collectAllRelatedMeasuresFromFilters(metrics, filters) {
    return filters.reduce((acc, filter) => {
        if (isRankingFilter(filter)) {
            const measure = filter.rankingFilter.measure;
            collectMeasure(metrics, measure, acc);
        }
        if (isMeasureValueFilter(filter)) {
            const measure = filter.measureValueFilter.measure;
            collectMeasure(metrics, measure, acc);
        }
        return acc;
    }, []);
}
/**
 * Alerts execute with `execution.attributes: []` and `execution.measures` containing only the selected alert measure(s).
 *
 * Widget filters (MVF / ranking) reference measures by localId (`localIdRef("...")`). In time-comparison scenarios,
 * those filters may target *comparator* measures (e.g. PoP comparator localId often looks like `${masterLocalId}_pop`).
 *
 * If we don't resolve these localIds to actual measure objects and include them in `execution.auxMeasures`,
 * the resulting alert execution can end up referencing a measure localId that is not present in the execution.
 */
function findMeasureByLocalIdentifier(metrics, localIdentifier) {
    const direct = metrics.find((m) => m.measure.measure.localIdentifier === localIdentifier);
    if (direct) {
        return direct.measure;
    }
    for (const metric of metrics) {
        const comparator = metric.comparators.find((c) => c.measure.measure.localIdentifier === localIdentifier);
        if (comparator) {
            return comparator.measure;
        }
    }
    return undefined;
}
function collectMeasure(metrics, measure, acc) {
    if (isLocalIdRef(measure)) {
        const related = findMeasureByLocalIdentifier(metrics, measure.localIdentifier);
        if (related) {
            acc.push(related);
        }
    }
}
function filterMeasures(auxMeasures, measures) {
    return auxMeasures.filter((m, index, arr) => {
        if (measures.includes(m)) {
            return false;
        }
        return arr.indexOf(m) === index;
    });
}
function getGranularityFromComparator(comparator) {
    if (!comparator?.granularity) {
        return "WEEK";
    }
    switch (comparator.granularity) {
        case "GDC.time.hour":
        case "GDC.time.minute":
        case "GDC.time.minute_in_hour":
        case "GDC.time.hour_in_day":
            //NOTE: We dont want to allow hour and minute granularity as default
            // Some users are not allowed to use hour granularity but we dont know
            // this setting here.
            return "DAY";
        case "GDC.time.date":
        case "GDC.time.day_in_month":
        case "GDC.time.day_in_week":
        case "GDC.time.day_in_euweek":
        case "GDC.time.day_in_quarter":
        case "GDC.time.day_in_year":
            return "DAY";
        case "GDC.time.week_us":
        case "GDC.time.week":
        case "GDC.time.week_in_year":
        case "GDC.time.week_in_quarter":
        case "GDC.time.euweek_in_year":
        case "GDC.time.euweek_in_quarter":
            return "WEEK";
        case "GDC.time.month":
        case "GDC.time.month_in_quarter":
        case "GDC.time.month_in_year":
            return "MONTH";
        case "GDC.time.quarter":
        case "GDC.time.quarter_in_year":
            return "QUARTER";
        case "GDC.time.year":
            return "YEAR";
        default:
            return "WEEK";
    }
}
