import findLast from 'lodash/findLast';
import { v4 as uuidv4 } from 'uuid';
import { ConditionalFormattingColorApplyTo, isConditionalFormattingConfigWithColorRange, isConditionalFormattingConfigWithSingleColor, isConditionalFormattingWithCompareTarget, isConditionalFormattingWithValues, } from '../types/conditionalFormatting';
import { LightdashError, NotImplementedError } from '../types/errors';
import { CustomFormatType, Format, isField, isFilterableItem, isTableCalculation, } from '../types/field';
import { FilterOperator } from '../types/filter';
import assertUnreachable from './assertUnreachable';
import { getItemId, isNumericItem, isStringDimension } from './item';
export const createConditionalFormattingRuleWithValues = () => ({
    id: uuidv4(),
    operator: FilterOperator.EQUALS,
    values: [],
});
export const createConditionalFormattingRuleWithCompareTarget = () => ({
    id: uuidv4(),
    operator: FilterOperator.EQUALS,
    compareTarget: null,
});
export const createConditionalFormattingRuleWithCompareTargetValues = () => ({
    id: uuidv4(),
    operator: FilterOperator.EQUALS,
    compareTarget: null,
    values: [],
});
export const createConditionalFormattingConfigWithSingleColor = (defaultColor, target = null) => ({
    target,
    color: defaultColor,
    rules: [createConditionalFormattingRuleWithValues()],
});
export const createConditionalFormattingConfigWithColorRange = (defaultColor, target = null) => ({
    target,
    color: {
        start: '#ffffff',
        end: defaultColor,
    },
    rule: {
        min: 0,
        max: 100,
    },
});
export const hasPercentageFormat = (field) => {
    if (!field)
        return false;
    return ((isField(field) && field?.format === Format.PERCENT) ||
        (isTableCalculation(field) &&
            field.format?.type === CustomFormatType.PERCENT));
};
export const convertFormattedValue = (value, field) => {
    if (!field)
        return value;
    if (hasPercentageFormat(field)) {
        return typeof value === 'number' ? value * 100 : value;
    }
    return value;
};
export const getMinMaxFromMinMaxMap = (minMaxMap) => ({
    min: Math.min(...Object.values(minMaxMap).map((m) => m.min)),
    max: Math.max(...Object.values(minMaxMap).map((m) => m.max)),
});
export class ConditionalFormattingError extends LightdashError {
    constructor(message) {
        super({
            message,
            name: 'ConditionalFormattingError',
            statusCode: 400,
            data: {},
        });
    }
}
export const hasMatchingConditionalRules = (field, value, minMaxMap, config, rowFields) => {
    if (!config)
        return false;
    const parsedValue = isNumericItem(field) ? Number(value) : value;
    const convertedValue = convertFormattedValue(parsedValue, field);
    const currentFieldId = getItemId(field);
    const targetFieldId = config.target?.fieldId;
    if (targetFieldId !== undefined && targetFieldId !== currentFieldId) {
        return false;
    }
    if (isConditionalFormattingConfigWithSingleColor(config)) {
        return config.rules.every((rule) => {
            let convertedCompareValue;
            let compareField;
            if (isConditionalFormattingWithCompareTarget(rule) &&
                rule.compareTarget?.fieldId &&
                rowFields) {
                const rowField = rowFields[rule.compareTarget.fieldId];
                if (!rowField)
                    return false;
                compareField = rowField.field;
                const parsedCompareValue = isNumericItem(compareField)
                    ? Number(rowField.value)
                    : rowField.value;
                convertedCompareValue = convertFormattedValue(parsedCompareValue, compareField);
            }
            // Compares field value to values when there is no compare target
            const shouldCompareFieldToValue = isConditionalFormattingWithValues(rule) &&
                !isConditionalFormattingWithCompareTarget(rule);
            // Compares field value to compare target when there is a compare target and there are no values
            const shouldCompareFieldToTarget = isConditionalFormattingWithCompareTarget(rule) &&
                !isConditionalFormattingWithValues(rule);
            // Compares compare target value to values when there is a compare target and there are values
            const shouldCompareTargetToValue = isConditionalFormattingWithCompareTarget(rule) &&
                isConditionalFormattingWithValues(rule);
            switch (rule.operator) {
                case FilterOperator.NULL:
                    return value === null;
                case FilterOperator.NOT_NULL:
                    return value !== null;
                case FilterOperator.EQUALS:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => convertedValue === v);
                    }
                    if (shouldCompareFieldToTarget) {
                        return convertedValue === convertedCompareValue;
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => convertedCompareValue === v);
                    }
                    throw new NotImplementedError();
                case FilterOperator.NOT_EQUALS:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => convertedValue !== v);
                    }
                    if (shouldCompareFieldToTarget) {
                        return convertedValue !== convertedCompareValue;
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => convertedCompareValue !== v);
                    }
                    throw new NotImplementedError();
                case FilterOperator.LESS_THAN:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => isNumericItem(field) &&
                            typeof v === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue < v);
                    }
                    if (shouldCompareFieldToTarget) {
                        return (isNumericItem(field) &&
                            isNumericItem(compareField) &&
                            typeof convertedCompareValue === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue < convertedCompareValue);
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => isNumericItem(compareField) &&
                            typeof v === 'number' &&
                            typeof convertedCompareValue === 'number' &&
                            convertedCompareValue < v);
                    }
                    throw new NotImplementedError();
                case FilterOperator.GREATER_THAN:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => isNumericItem(field) &&
                            typeof v === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue > v);
                    }
                    if (shouldCompareFieldToTarget) {
                        return (isNumericItem(field) &&
                            isNumericItem(compareField) &&
                            typeof convertedCompareValue === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue > convertedCompareValue);
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => isNumericItem(compareField) &&
                            typeof v === 'number' &&
                            typeof convertedCompareValue === 'number' &&
                            convertedCompareValue > v);
                    }
                    throw new NotImplementedError();
                case FilterOperator.STARTS_WITH:
                case FilterOperator.ENDS_WITH:
                case FilterOperator.INCLUDE:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => isStringDimension(field) &&
                            typeof v === 'string' &&
                            typeof convertedValue === 'string' &&
                            convertedValue.includes(v));
                    }
                    if (shouldCompareFieldToTarget) {
                        return (isStringDimension(field) &&
                            isStringDimension(compareField) &&
                            typeof convertedValue === 'string' &&
                            typeof convertedCompareValue === 'string' &&
                            convertedValue.includes(convertedCompareValue));
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => isStringDimension(compareField) &&
                            typeof v === 'string' &&
                            typeof convertedCompareValue === 'string' &&
                            convertedCompareValue.includes(v));
                    }
                    throw new NotImplementedError();
                case FilterOperator.IN_BETWEEN:
                    if (isStringDimension(field)) {
                        throw new ConditionalFormattingError(`String dimensions are not supported for conditional formatting with ${rule.operator}`);
                    }
                    if (shouldCompareFieldToValue) {
                        if (typeof convertedValue !== 'number') {
                            throw new ConditionalFormattingError(`Conditional formatting with ${rule.operator} requires a numeric value`);
                        }
                        const ruleValues = rule.values;
                        if (ruleValues.length !== 2 ||
                            typeof ruleValues[0] !== 'number' ||
                            typeof ruleValues[1] !== 'number') {
                            return false;
                        }
                        return (convertedValue >= ruleValues[0] &&
                            convertedValue <= ruleValues[1]);
                    }
                    if (shouldCompareFieldToTarget) {
                        throw new ConditionalFormattingError(`Conditional formatting with ${rule.operator} does not support compare targets`);
                    }
                    if (shouldCompareTargetToValue) {
                        throw new ConditionalFormattingError(`Conditional formatting with ${rule.operator} does not support target values`);
                    }
                    // should never happen
                    return false;
                case FilterOperator.NOT_IN_BETWEEN:
                    if (isStringDimension(field)) {
                        throw new ConditionalFormattingError(`String dimensions are not supported for conditional formatting with ${rule.operator}`);
                    }
                    if (shouldCompareFieldToValue) {
                        if (typeof convertedValue !== 'number') {
                            throw new ConditionalFormattingError(`Conditional formatting with ${rule.operator} requires a numeric value`);
                        }
                        const ruleValues = rule.values;
                        if (ruleValues.length !== 2 ||
                            typeof ruleValues[0] !== 'number' ||
                            typeof ruleValues[1] !== 'number') {
                            return false;
                        }
                        return (convertedValue < ruleValues[0] ||
                            convertedValue > ruleValues[1]);
                    }
                    if (shouldCompareTargetToValue) {
                        throw new ConditionalFormattingError(`Conditional formatting with ${rule.operator} does not support target values`);
                    }
                    if (shouldCompareFieldToTarget) {
                        throw new ConditionalFormattingError(`Conditional formatting with ${rule.operator} does not support compare targets`);
                    }
                    // should never happen
                    return false;
                case FilterOperator.LESS_THAN_OR_EQUAL:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => isNumericItem(field) &&
                            typeof v === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue <= v);
                    }
                    if (shouldCompareFieldToTarget) {
                        return (isNumericItem(field) &&
                            isNumericItem(compareField) &&
                            typeof convertedCompareValue === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue <= convertedCompareValue);
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => isNumericItem(compareField) &&
                            typeof v === 'number' &&
                            typeof convertedCompareValue === 'number' &&
                            convertedCompareValue <= v);
                    }
                    throw new NotImplementedError();
                case FilterOperator.GREATER_THAN_OR_EQUAL:
                    if (shouldCompareFieldToValue) {
                        return rule.values.some((v) => isNumericItem(field) &&
                            typeof v === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue >= v);
                    }
                    if (shouldCompareFieldToTarget) {
                        return (isNumericItem(field) &&
                            isNumericItem(compareField) &&
                            typeof convertedCompareValue === 'number' &&
                            typeof convertedValue === 'number' &&
                            convertedValue >= convertedCompareValue);
                    }
                    if (shouldCompareTargetToValue) {
                        return rule.values.some((v) => isNumericItem(compareField) &&
                            typeof v === 'number' &&
                            typeof convertedCompareValue === 'number' &&
                            convertedCompareValue >= v);
                    }
                    throw new NotImplementedError();
                case FilterOperator.NOT_INCLUDE:
                case FilterOperator.IN_THE_PAST:
                case FilterOperator.NOT_IN_THE_PAST:
                case FilterOperator.IN_THE_NEXT:
                case FilterOperator.IN_THE_CURRENT:
                case FilterOperator.NOT_IN_THE_CURRENT:
                case FilterOperator.IN_PERIOD_TO_DATE:
                    throw new NotImplementedError(`Conditional formatting with ${rule.operator} is not implemented`);
                default:
                    return assertUnreachable(rule.operator, 'Unknown operator for conditional formatting');
            }
        });
    }
    if (isConditionalFormattingConfigWithColorRange(config)) {
        if (typeof convertedValue !== 'number')
            return false;
        let min;
        let max;
        if (config.rule.min === 'auto') {
            min =
                targetFieldId && targetFieldId in minMaxMap
                    ? minMaxMap[targetFieldId].min
                    : getMinMaxFromMinMaxMap(minMaxMap).min;
        }
        else {
            min = config.rule.min;
        }
        if (config.rule.max === 'auto') {
            max =
                targetFieldId && targetFieldId in minMaxMap
                    ? minMaxMap[targetFieldId].max
                    : getMinMaxFromMinMaxMap(minMaxMap).max;
        }
        else {
            max = config.rule.max;
        }
        return convertedValue >= min && convertedValue <= max;
    }
    return assertUnreachable(config, 'Unknown conditional formatting config');
};
export const getConditionalFormattingConfig = ({ field, value, minMaxMap = {}, conditionalFormattings, rowFields, applyTo, }) => {
    // For backwards compatibility with old table calculations without type
    const isCalculationTypeUndefined = field && isTableCalculation(field) && field.type === undefined;
    if (!conditionalFormattings ||
        !field ||
        (!isNumericItem(field) &&
            !isStringDimension(field) &&
            !isCalculationTypeUndefined))
        return undefined;
    return findLast(conditionalFormattings, (config) => {
        if (applyTo !== undefined &&
            (config.applyTo ?? ConditionalFormattingColorApplyTo.CELL) !==
                applyTo) {
            return false;
        }
        return hasMatchingConditionalRules(field, value, minMaxMap, config, rowFields);
    });
};
export const getConditionalFormattingDescription = (field, conditionalFormattingConfig, rowFields, getConditionalRuleLabel) => {
    if (!field || !isFilterableItem(field) || !conditionalFormattingConfig) {
        return undefined;
    }
    if (isConditionalFormattingConfigWithColorRange(conditionalFormattingConfig)) {
        return [
            `is greater than or equal to ${conditionalFormattingConfig.rule.min === 'auto'
                ? 'min value in table'
                : conditionalFormattingConfig.rule.min}`,
            `is less than or equal to ${conditionalFormattingConfig.rule.max === 'auto'
                ? 'max value in table'
                : conditionalFormattingConfig.rule.max}`,
        ].join(' and ');
    }
    if (isConditionalFormattingConfigWithSingleColor(conditionalFormattingConfig)) {
        return conditionalFormattingConfig.rules
            .map((r) => {
            const fieldLabel = getConditionalRuleLabel(r, field);
            if (isConditionalFormattingWithCompareTarget(r)) {
                const compareRowField = r.compareTarget?.fieldId
                    ? rowFields[r.compareTarget?.fieldId]
                    : undefined;
                if (!compareRowField ||
                    !isFilterableItem(compareRowField.field)) {
                    return fieldLabel;
                }
                // If there are no values, then the field is being compared to the compare target
                if (!isConditionalFormattingWithValues(r)) {
                    return {
                        ...fieldLabel,
                        value: String(compareRowField.value),
                    };
                }
                // If there are values, then the target is being compared to the values
                return {
                    ...getConditionalRuleLabel(r, compareRowField.field),
                    isComparingTargetToValues: true,
                };
            }
            return fieldLabel;
        })
            .map((l) => l.isComparingTargetToValues
            ? `${l.field} ${l.operator} ${l.value}`
            : `${l.operator} ${l.value}`)
            .join(' and ');
    }
    return assertUnreachable(conditionalFormattingConfig, 'Unknown conditional formatting config');
};
export const getConditionalFormattingColorWithColorRange = ({ field, value, config, minMaxMap = {}, getColorFromRange, }) => {
    if (!field)
        return undefined;
    const numericValue = typeof value === 'string' ? parseFloat(value) : value;
    const convertedValue = convertFormattedValue(numericValue, field);
    const currentFieldId = getItemId(field);
    const targetFieldId = config.target?.fieldId;
    if (targetFieldId !== undefined && targetFieldId !== currentFieldId) {
        return undefined;
    }
    if (typeof convertedValue !== 'number')
        return undefined;
    let min;
    let max;
    if (config.rule.min === 'auto') {
        min =
            targetFieldId && targetFieldId in minMaxMap
                ? minMaxMap[targetFieldId].min
                : getMinMaxFromMinMaxMap(minMaxMap).min;
    }
    else {
        min = config.rule.min;
    }
    if (config.rule.max === 'auto') {
        max =
            targetFieldId && targetFieldId in minMaxMap
                ? minMaxMap[targetFieldId].max
                : getMinMaxFromMinMaxMap(minMaxMap).max;
    }
    else {
        max = config.rule.max;
    }
    const color = getColorFromRange(convertedValue, config.color, { min, max });
    if (!color)
        return undefined;
    return {
        color,
        applyTo: config.applyTo ?? ConditionalFormattingColorApplyTo.CELL,
    };
};
export const getConditionalFormattingColorWithSingleColor = ({ config, }) => ({
    color: config.color,
    applyTo: config.applyTo ?? ConditionalFormattingColorApplyTo.CELL,
});
export const getConditionalFormattingColor = ({ field, value, config, minMaxMap, getColorFromRange, }) => {
    if (!config)
        return undefined;
    if (isConditionalFormattingConfigWithSingleColor(config)) {
        return getConditionalFormattingColorWithSingleColor({ config });
    }
    return getConditionalFormattingColorWithColorRange({
        field,
        value,
        config,
        minMaxMap,
        getColorFromRange,
    });
};
/**
 * Row-level conditional formatting (PROD-8058): returns the matching `applyTo:
 * ROW` config whose trigger field matches, or undefined. Evaluated once per
 * row, independent of the cell being drawn.
 */
export const getRowConditionalFormattingConfig = ({ conditionalFormattings, rowFields, minMaxMap = {}, }) => {
    if (!conditionalFormattings)
        return undefined;
    return conditionalFormattings.find((config) => {
        if (config.applyTo !== ConditionalFormattingColorApplyTo.ROW) {
            return false;
        }
        const targetFieldId = config.target?.fieldId;
        if (!targetFieldId)
            return false;
        const trigger = rowFields[targetFieldId];
        if (!trigger)
            return false;
        return hasMatchingConditionalRules(trigger.field, trigger.value, minMaxMap, config, rowFields);
    });
};
/**
 * Row-level conditional formatting (PROD-8058): returns the background color to
 * paint across an entire row when a single `applyTo: ROW` rule's trigger field
 * matches. Returns null when no ROW rule matches.
 */
export const getRowConditionalFormattingColor = (args) => {
    const match = getRowConditionalFormattingConfig(args);
    if (!match)
        return null;
    return isConditionalFormattingConfigWithSingleColor(match)
        ? match.color
        : null;
};
/**
 * Merges the text styling (bold/italic/underline) from every matching config
 * that applies to a cell — its cell-level, text-level and row-level matches —
 * into a single style. Returns undefined when no style is set so callers can
 * skip styling entirely.
 */
export const getConditionalFormattingTextStyle = (configs) => {
    const merged = configs.reduce((acc, config) => {
        const style = config?.textStyle;
        if (!style)
            return acc;
        return {
            bold: acc.bold || !!style.bold,
            italic: acc.italic || !!style.italic,
            underline: acc.underline || !!style.underline,
        };
    }, { bold: false, italic: false, underline: false });
    return merged.bold || merged.italic || merged.underline
        ? merged
        : undefined;
};
/**
 * Canonical key for a pivot cell's dimension context (index + header dims).
 * Used by both pivotQueryResults (to store hidden values) and the pivot
 * renderer (to look them up), so the keys cannot drift. Entries are sorted by
 * fieldId; null and undefined raw values are normalised to the same token.
 */
export const getPivotRowContextKey = (dimValues) => {
    const entries = Object.keys(dimValues)
        .sort()
        .map((fieldId) => {
        const raw = dimValues[fieldId];
        const token = raw === null || raw === undefined ? ' ' : String(raw);
        return [fieldId, token];
    });
    return JSON.stringify(entries);
};
//# sourceMappingURL=conditionalFormatting.js.map