// (C) 2020-2026 GoodData Corporation
import stringify from "json-stable-stringify";
import { groupBy } from "lodash-es";
import { all, call, select } from "redux-saga/effects";
import { areObjRefsEqual, dashboardAttributeFilterItemDisplayForm, dashboardFilterObjRef, filterAttributeElements, filterObjRef, idRef, insightFilters as insightDefinitionFilters, insightId, isArbitraryAttributeFilter, isAttributeDescriptor, isAttributeElementsByValue, isAttributeFilter, isComparisonCondition, isDashboardArbitraryAttributeFilter, isDashboardAttributeFilter, isDashboardTextAttributeFilter, isIdentifierRef, isMeasureValueFilter, isNegativeAttributeFilter, isRangeCondition, isTextAttributeFilter, isUriRef, measureValueFilterConditions, measureValueFilterMeasure, } from "@gooddata/sdk-model";
import { getAttributeIdentifiersPlaceholdersFromUrl, getDashboardAttributeFilterPlaceholdersFromUrl, getDashboardMeasureValueFilterPlaceholdersFromUrl, getInsightAttributeFilterPlaceholdersFromUrl, getInsightMeasureValueFilterPlaceholdersFromUrl, } from "@gooddata/sdk-model/internal";
import { isDrillIntersectionAttributeItem, } from "@gooddata/sdk-ui";
import { invalidArgumentsProvided } from "../../events/general.js";
import { selectAllCatalogDisplayFormsMap, selectAllCatalogMeasuresMap, selectCatalogDateAttributes, } from "../../store/catalog/catalogSelectors.js";
import { selectEnableMeasureValueFilterKD } from "../../store/config/configSelectors.js";
import { selectInsightByRef } from "../../store/insights/insightsSelectors.js";
import { selectDashboardId } from "../../store/meta/metaSelectors.js";
import { selectAttributeFilterConfigsOverrides } from "../../store/tabs/attributeFilterConfigs/attributeFilterConfigsSelectors.js";
import { selectFilterContextAttributeFilterItems, selectFilterContextMeasureValueFilters, } from "../../store/tabs/filterContext/filterContextSelectors.js";
import { selectAnalyticalWidgetByRef } from "../../store/tabs/layout/layoutSelectors.js";
import { DRILL_TO_URL_PLACEHOLDER } from "../../types/drillTypes.js";
import { dashboardMeasureValueFilterMatchesIdentifier, insightMeasureValueFilterMatchesIdentifier, } from "../../utils/measureValueFilterUtils.js";
import { getElementTitle, getElementsSecondaryTitles } from "./getElementTitle.js";
export function* loadElementTitle(dfRef, dfIdentifier, attrElementUri, ctx) {
    const elementTitle = yield call(getElementTitle, ctx.workspace, dfRef, attrElementUri, ctx);
    return {
        identifier: dfIdentifier,
        elementTitle,
    };
}
export function* loadSecondaryElementTitles(dfRef, attrElementValues, ctx) {
    const elementsTitles = yield call(getElementsSecondaryTitles, ctx.workspace, dfRef, attrElementValues, ctx);
    return elementsTitles;
}
function isInRefList(list, ref) {
    return list.some((itemRef) => areObjRefsEqual(itemRef, ref));
}
/**
 * Tolerant ObjRef match used when resolving drill-to-URL placeholders against a drill intersection.
 *
 * The intersection comes from the execution result while the display-form metadata comes from the
 * metadata API; the two can describe the same object with refs that differ in shape (idRef vs uriRef)
 * or in `type`. The strict {@link areObjRefsEqual} then returns false and silently breaks
 * `attribute_title(...)` resolution. This compares by the underlying identifier/uri, ignoring those
 * representational differences.
 */
function refsMatchTolerant(a, b) {
    if (!a || !b) {
        return false;
    }
    if (areObjRefsEqual(a, b)) {
        return true;
    }
    const keyOf = (ref) => isIdentifierRef(ref) ? ref.identifier : isUriRef(ref) ? ref.uri : undefined;
    const keyA = keyOf(a);
    return keyA !== undefined && keyA === keyOf(b);
}
/**
 * @internal
 */
export function findDrillIntersectionAttributeHeaderItem(drillIntersectionElements, displayForm) {
    const intersectionForAttribute = drillIntersectionElements.find(({ header }) => {
        if (!isAttributeDescriptor(header)) {
            return false;
        }
        const { identifier, ref: labelRef, formOf } = header.attributeHeader;
        // A placeholder names a display form by identifier. Match that identifier string directly —
        // it is immune to ObjRef shape/type differences between the execution result and the
        // metadata API. Fall back to tolerant ref matches on the display form and its parent
        // attribute so a differently-labelled column on the same attribute still resolves.
        return (displayForm.id === identifier ||
            refsMatchTolerant(displayForm.ref, labelRef) ||
            refsMatchTolerant(displayForm.attribute, formOf.ref));
    });
    if (intersectionForAttribute && isDrillIntersectionAttributeItem(intersectionForAttribute.header)) {
        const { attributeHeader } = intersectionForAttribute.header;
        // Tripwire: if the strict comparison the resolver relied on before (attribute ref equality)
        // would NOT have matched this element, the tolerant fallback / identifier match rescued it.
        // That means the execution result and the metadata API disagree on the attribute's ObjRef
        // representation — the drill still works, but the underlying (likely backend) mismatch should
        // be known and fixed at the source.
        if (!areObjRefsEqual(displayForm.attribute, attributeHeader.formOf.ref)) {
            console.warn(`Drill to custom URL: attribute "${displayForm.id}" matched the drill intersection only via a tolerant reference comparison; the execution result and metadata report different references for the same attribute.`, {
                displayFormAttributeRef: displayForm.attribute,
                intersectionAttributeRef: attributeHeader.formOf.ref,
            });
        }
        return intersectionForAttribute.header.attributeHeaderItem;
    }
    // No matching attribute element in the intersection — the {attribute_title(...)} placeholder will
    // stay unresolved and the drill will fail with "Failed to load URL". Log what we looked for vs. what
    // the intersection actually carried, so the cause (cross-API ref differences, or a drilled column
    // that does not carry the referenced attribute) is diagnosable from the console alone.
    const intersectionAttributes = drillIntersectionElements
        .map(({ header }) => (isAttributeDescriptor(header) ? header.attributeHeader : undefined))
        .filter((attributeHeader) => !!attributeHeader)
        .map((attributeHeader) => ({
        identifier: attributeHeader.identifier,
        ref: attributeHeader.ref,
        attribute: attributeHeader.formOf.ref,
    }));
    console.warn(`Drill to custom URL: attribute "${displayForm.id}" referenced by an attribute_title placeholder was not found in the drill intersection.`, {
        wanted: { id: displayForm.id, ref: displayForm.ref, attribute: displayForm.attribute },
        intersectionAttributes,
    });
    return undefined;
}
export function* splitDFToLoadingAndMapping(attributesDisplayForms, ctx) {
    if (ctx.backend.capabilities.supportsElementUris) {
        return {
            displayFormForValueLoad: attributesDisplayForms,
            displayFormsWithKnownValues: [],
        };
    }
    // in tiger there are no uris for attribute values, only primary values
    // we can't call collectLabelElements for date attributes because there are no date "elements", but we can use this values directly.
    const dateAttributes = yield select(selectCatalogDateAttributes);
    const dateAttributeRefs = dateAttributes.map((da) => da.attribute.ref);
    const { true: displayFormsWithKnownValues = [], false: displayFormForValueLoad = [] } = groupBy(attributesDisplayForms, (df) => isInRefList(dateAttributeRefs, df.attribute));
    return {
        displayFormsWithKnownValues,
        displayFormForValueLoad,
    };
}
export function* loadAttributeElementsForDrillIntersection(drillIntersectionElements, attributesDisplayForms, ctx) {
    const splitDisplayForms = yield call(splitDFToLoadingAndMapping, attributesDisplayForms, ctx);
    const { displayFormsWithKnownValues, displayFormForValueLoad } = splitDisplayForms;
    const mappedElements = displayFormsWithKnownValues.reduce((acc, displayForm) => {
        const attributeHeaderItem = findDrillIntersectionAttributeHeaderItem(drillIntersectionElements, displayForm);
        if (!attributeHeaderItem) {
            return acc;
        }
        acc.push({
            identifier: displayForm.id,
            elementTitle: attributeHeaderItem.uri,
        });
        return acc;
    }, []);
    const loadedElement = yield all(displayFormForValueLoad.reduce((acc, displayForm) => {
        const { id: dfIdentifier, ref: dfRef } = displayForm;
        const attributeHeaderItem = findDrillIntersectionAttributeHeaderItem(drillIntersectionElements, displayForm);
        if (!attributeHeaderItem) {
            return acc;
        }
        acc.push(call(loadElementTitle, dfRef, dfIdentifier, attributeHeaderItem.uri, ctx));
        return acc;
    }, []));
    return [...mappedElements, ...loadedElement];
}
const encodeParameterIfSet = (parameter) => parameter === null || parameter === undefined ? parameter : encodeURIComponent(parameter);
export function getAttributeDisplayForms(projectId, objRefs, ctx) {
    return ctx.backend.workspace(projectId).attributes().getAttributeDisplayForms(objRefs);
}
export function* getAttributeIdentifiersReplacements(url, drillIntersectionElements, ctx) {
    const attributeIdentifiersPlaceholders = getAttributeIdentifiersPlaceholdersFromUrl(url);
    if (attributeIdentifiersPlaceholders.length === 0) {
        return [];
    }
    const displayForms = yield call(getAttributeDisplayForms, ctx.workspace, attributeIdentifiersPlaceholders.map((placeholder) => idRef(placeholder.identifier)), ctx);
    const elements = yield call(loadAttributeElementsForDrillIntersection, drillIntersectionElements, displayForms, ctx);
    return attributeIdentifiersPlaceholders.map(({ placeholder: toBeReplaced, identifier, toBeEncoded }) => {
        const elementTitle = elements.find((element) => element.identifier === identifier)?.elementTitle;
        const replacement = toBeEncoded ? encodeParameterIfSet(elementTitle) : elementTitle;
        return {
            toBeReplaced,
            replacement: replacement,
        };
    });
}
function* resolveDashboardAttributeFilterReplacement({ placeholder: toBeReplaced, ref }, attributeFilters, catalogDisplayForms, attributeFilterConfigs, ctx) {
    // Check for text filters (arbitrary/match) first — they use different value structures.
    const textFilter = attributeFilters.find((filter) => {
        if (!isDashboardTextAttributeFilter(filter)) {
            return false;
        }
        const df = catalogDisplayForms.get(dashboardAttributeFilterItemDisplayForm(filter));
        return df && areObjRefsEqual(idRef(df.id), ref);
    });
    if (textFilter && isDashboardTextAttributeFilter(textFilter)) {
        const parsedFilter = stringifyTextFilterSelection(textFilter);
        const replacement = encodeParameterIfSet(parsedFilter);
        return {
            toBeReplaced,
            replacement: replacement,
        };
    }
    // Standard (element-based) attribute filters
    const standardFilters = attributeFilters.filter(isDashboardAttributeFilter);
    let usedFilter = standardFilters.find((filter) => {
        const df = catalogDisplayForms.get(filter.attributeFilter.displayForm);
        return df && areObjRefsEqual(idRef(df.id), ref);
    });
    let attributeElementsValues = [];
    if (usedFilter) {
        const elements = usedFilter?.attributeFilter.attributeElements;
        attributeElementsValues = isAttributeElementsByValue(elements)
            ? elements.values
            : (elements?.uris ?? []);
    }
    else {
        const usedConfig = attributeFilterConfigs.find((config) => {
            return config.displayAsLabel && areObjRefsEqual(config.displayAsLabel, ref);
        });
        usedFilter = standardFilters.find((filter) => {
            return filter.attributeFilter.localIdentifier === usedConfig?.localIdentifier;
        });
        const elements = usedFilter?.attributeFilter.attributeElements;
        const primaryElementsValues = elements
            ? isAttributeElementsByValue(elements)
                ? elements.values
                : elements.uris
            : [];
        attributeElementsValues = (yield call(loadSecondaryElementTitles, ref, primaryElementsValues, ctx));
    }
    const isNegative = usedFilter?.attributeFilter.negativeSelection;
    const parsedFilter = usedFilter
        ? stringifyAttributeFilterSelection(attributeElementsValues, isNegative)
        : undefined;
    const replacement = encodeParameterIfSet(parsedFilter);
    return {
        toBeReplaced,
        replacement: replacement,
    };
}
export function* getDashboardAttributeFilterReplacements(url, ctx) {
    const attributeFilterPlaceholders = getDashboardAttributeFilterPlaceholdersFromUrl(url);
    if (attributeFilterPlaceholders.length === 0) {
        return [];
    }
    const attributeFilters = yield select(selectFilterContextAttributeFilterItems);
    const catalogDisplayForms = yield select(selectAllCatalogDisplayFormsMap);
    const attributeFilterConfigs = yield select(selectAttributeFilterConfigsOverrides);
    return yield all(attributeFilterPlaceholders.map((placeholder) => call(resolveDashboardAttributeFilterReplacement, placeholder, attributeFilters, catalogDisplayForms, attributeFilterConfigs, ctx)));
}
function resolveDashboardMeasureValueFilterReplacement({ placeholder: toBeReplaced, identifier }, measureValueFilters, catalogMeasures) {
    const usedFilter = measureValueFilters.find((filter) => {
        const measureRef = dashboardFilterObjRef(filter);
        return dashboardMeasureValueFilterMatchesIdentifier(measureRef, identifier, catalogMeasures);
    });
    const parsedFilter = stringifyMeasureValueFilterCondition(usedFilter?.dashboardMeasureValueFilter.conditions);
    const replacement = usedFilter ? encodeParameterIfSet(parsedFilter) : undefined;
    return {
        toBeReplaced,
        replacement: replacement,
    };
}
export function* getDashboardMeasureValueFilterReplacements(url) {
    const measureValueFilterPlaceholders = getDashboardMeasureValueFilterPlaceholdersFromUrl(url);
    if (measureValueFilterPlaceholders.length === 0) {
        return [];
    }
    const measureValueFilters = yield select(selectFilterContextMeasureValueFilters);
    const catalogMeasures = yield select(selectAllCatalogMeasuresMap);
    return measureValueFilterPlaceholders.map((placeholder) => resolveDashboardMeasureValueFilterReplacement(placeholder, measureValueFilters, catalogMeasures));
}
function resolveInsightMeasureValueFilterReplacement({ placeholder: toBeReplaced, identifier }, measureValueFilters) {
    const usedFilter = measureValueFilters.find((filter) => {
        const measureRef = measureValueFilterMeasure(filter);
        return insightMeasureValueFilterMatchesIdentifier(measureRef, identifier);
    });
    const parsedFilter = stringifyMeasureValueFilterCondition(usedFilter ? measureValueFilterConditions(usedFilter) : undefined);
    const replacement = usedFilter ? encodeParameterIfSet(parsedFilter) : undefined;
    return {
        toBeReplaced,
        replacement: replacement,
    };
}
export function* getInsightMeasureValueFilterReplacements(url, widgetRef) {
    const measureValueFilterPlaceholders = getInsightMeasureValueFilterPlaceholdersFromUrl(url);
    if (measureValueFilterPlaceholders.length === 0) {
        return [];
    }
    const widget = yield select(selectAnalyticalWidgetByRef(widgetRef));
    const insight = yield select(selectInsightByRef(widget.insight));
    const measureValueFilters = insight ? insightDefinitionFilters(insight).filter(isMeasureValueFilter) : [];
    return measureValueFilterPlaceholders.map((placeholder) => resolveInsightMeasureValueFilterReplacement(placeholder, measureValueFilters));
}
export function* getInsightAttributeFilterReplacements(url, widgetRef) {
    const attributeFilterPlaceholders = getInsightAttributeFilterPlaceholdersFromUrl(url);
    if (attributeFilterPlaceholders.length === 0) {
        return [];
    }
    // The {attribute_filter_selection(...)} placeholder is scoped to the visualization, so it reads the
    // insight's own filters. Resolving it against the widget's effective filters would let a dashboard
    // filter on the same attribute take precedence and override the insight's selection.
    const widget = yield select(selectAnalyticalWidgetByRef(widgetRef));
    const insight = yield select(selectInsightByRef(widget.insight));
    const insightAttributeFilters = insight
        ? insightDefinitionFilters(insight).filter(isAttributeFilter)
        : [];
    const catalogDisplayForms = yield select(selectAllCatalogDisplayFormsMap);
    return attributeFilterPlaceholders.map(({ placeholder: toBeReplaced, ref }) => {
        const usedFilter = insightAttributeFilters.find((filter) => {
            const filterRef = filterObjRef(filter);
            const df = filterRef && catalogDisplayForms.get(filterRef);
            return df && areObjRefsEqual(idRef(df.id), ref);
        });
        let parsedFilter;
        if (usedFilter && isTextAttributeFilter(usedFilter)) {
            parsedFilter = stringifyTextFilterSelection(usedFilter);
        }
        else if (usedFilter) {
            const elements = filterAttributeElements(usedFilter);
            const attributeElementsValues = isAttributeElementsByValue(elements)
                ? elements.values
                : (elements?.uris ?? []);
            const isNegative = isNegativeAttributeFilter(usedFilter);
            parsedFilter = stringifyAttributeFilterSelection(attributeElementsValues, isNegative);
        }
        const replacement = encodeParameterIfSet(parsedFilter);
        return {
            toBeReplaced,
            replacement: replacement,
        };
    });
}
const createIdentifierReplacement = (toBeReplaced, replacement = "") => ({ toBeReplaced, replacement, replaceGlobally: true });
export function* getInsightIdentifiersReplacements(customUrl, widgetRef, ctx) {
    const { workspace, clientId, dataProductId } = ctx;
    const dashboardId = yield select(selectDashboardId);
    const widget = yield select(selectAnalyticalWidgetByRef(widgetRef));
    const insight = yield select(selectInsightByRef(widget.insight));
    const replacements = [
        createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_PROJECT_ID, workspace),
        createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_WORKSPACE_ID, workspace),
        createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_DASHBOARD_ID, dashboardId),
        createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_CLIENT_ID, clientId),
        createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_DATA_PRODUCT_ID, dataProductId),
        createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_INSIGHT_ID, insightId(insight)),
    ];
    if (customUrl.includes(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_WIDGET_ID)) {
        return [
            ...replacements,
            createIdentifierReplacement(DRILL_TO_URL_PLACEHOLDER.DRILL_TO_URL_PLACEHOLDER_WIDGET_ID, widget.identifier),
        ];
    }
    return replacements;
}
const applyReplacements = (url, replacements) => replacements.reduce((customUrlWithReplacedPlaceholders, { toBeReplaced, replacement, replaceGlobally }) => customUrlWithReplacedPlaceholders.replace(replaceGlobally ? new RegExp(toBeReplaced, "g") : toBeReplaced, replacement), url);
export function* resolveDrillToCustomUrl(drillConfig, widgetRef, event, ctx, cmd) {
    const customUrl = drillConfig.target.url;
    const attributeIdentifiersReplacements = yield call(getAttributeIdentifiersReplacements, customUrl, event.drillContext.intersection, ctx);
    const dashboardAttributeFilterReplacements = yield call(getDashboardAttributeFilterReplacements, customUrl, ctx);
    const insightAttributeFilterReplacements = yield call(getInsightAttributeFilterReplacements, customUrl, widgetRef);
    const dashboardMeasureValueFilterReplacements = yield call(getDashboardMeasureValueFilterReplacements, customUrl);
    const enableMeasureValueFilterKD = yield select(selectEnableMeasureValueFilterKD);
    const insightMeasureValueFilterReplacements = enableMeasureValueFilterKD
        ? yield call(getInsightMeasureValueFilterReplacements, customUrl, widgetRef)
        : getInsightMeasureValueFilterPlaceholdersFromUrl(customUrl).map(({ placeholder }) => ({
            toBeReplaced: placeholder,
            replacement: undefined,
        }));
    const missingReplacements = [
        ...attributeIdentifiersReplacements,
        ...dashboardAttributeFilterReplacements,
        ...insightAttributeFilterReplacements,
        ...dashboardMeasureValueFilterReplacements,
        ...insightMeasureValueFilterReplacements,
    ].filter(({ replacement }) => replacement === undefined);
    if (missingReplacements.length > 0) {
        // Surface every unresolved placeholder (not just the first) and the URL, so a "Failed to load
        // URL" is diagnosable from the console without a debugger.
        console.warn(`Drill to custom URL: could not resolve parameter(s) ${missingReplacements
            .map(({ toBeReplaced }) => toBeReplaced)
            .join(", ")} in URL "${customUrl}". The drill cannot open.`);
        throw invalidArgumentsProvided(ctx, cmd, `Drill to custom URL unable to resolve missing parameter ${missingReplacements[0].toBeReplaced}`);
    }
    const insightIdentifiersReplacements = yield call(getInsightIdentifiersReplacements, customUrl, widgetRef, ctx);
    const replacements = [
        ...attributeIdentifiersReplacements,
        ...dashboardAttributeFilterReplacements,
        ...insightAttributeFilterReplacements,
        ...dashboardMeasureValueFilterReplacements,
        ...insightMeasureValueFilterReplacements,
        ...insightIdentifiersReplacements,
    ];
    return applyReplacements(customUrl, replacements);
}
function stringifyAttributeFilterSelection(selection, isNegative) {
    return `${isNegative ? "NOT_IN" : "IN"}${stringify(selection)}`;
}
/**
 * @internal
 */
export function stringifyTextFilterSelection(filter) {
    if (isDashboardArbitraryAttributeFilter(filter) || isArbitraryAttributeFilter(filter)) {
        const { values, negativeSelection } = filter.arbitraryAttributeFilter;
        return stringifyAttributeFilterSelection(values, negativeSelection ?? false);
    }
    const { literal, operator, negativeSelection } = filter.matchAttributeFilter;
    return stringifyMatchFilterSelection(literal, operator, negativeSelection ?? false);
}
const MATCH_OPERATOR_MAP = {
    contains: "CONTAINS",
    startsWith: "STARTS_WITH",
    endsWith: "ENDS_WITH",
};
function stringifyMatchFilterSelection(literal, operator, isNegative) {
    const operatorStr = MATCH_OPERATOR_MAP[operator];
    const prefix = isNegative ? `NOT_${operatorStr}` : operatorStr;
    return `${prefix}${stringify([literal])}`;
}
/**
 * @internal
 */
export function stringifyMeasureValueFilterCondition(conditions) {
    if (!conditions || conditions.length === 0) {
        return "ALL";
    }
    return conditions
        .map((condition) => {
        if (isComparisonCondition(condition)) {
            return `${condition.comparison.operator}(${condition.comparison.value})`;
        }
        if (isRangeCondition(condition)) {
            return `${condition.range.operator}(${condition.range.from},${condition.range.to})`;
        }
        return "ALL";
    })
        .join("|");
}
