"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.renderFilterRuleSqlFromField = exports.renderFilterRuleSql = exports.renderTableCalculationFilterRuleSql = exports.renderBooleanFilterSql = exports.renderDateFilterSql = exports.renderNumberFilterSql = exports.renderStringFilterSql = void 0;
const tslib_1 = require("tslib");
const moment_timezone_1 = tslib_1.__importDefault(require("moment-timezone"));
const dbt_1 = require("../types/dbt");
const errors_1 = require("../types/errors");
const field_1 = require("../types/field");
const filter_1 = require("../types/filter");
const assertUnreachable_1 = tslib_1.__importDefault(require("../utils/assertUnreachable"));
const booleanConverter_1 = require("../utils/booleanConverter");
const formatting_1 = require("../utils/formatting");
const item_1 = require("../utils/item");
const time_1 = require("../utils/time");
// NOTE: This function requires a complete date as input.
// It produces a timezoneless string which is implied to be in UTC.
// We could probably have it be a string WITH a timezone in the future.
// Calling .utc() here makes it safe to drop the tz.
const formatTimestampAsUTCWithNoTimezone = (date) => (0, moment_timezone_1.default)(date).utc().format('YYYY-MM-DD HH:mm:ss');
const raiseInvalidFilterError = (type, filter) => {
    let targetString = '';
    if ((0, filter_1.isMetricFilterTarget)(filter.target)) {
        targetString = ` on "${filter.target.fieldRef}" field`;
    }
    else if ((0, filter_1.isFilterTarget)(filter.target)) {
        targetString = ` on "${filter.target.fieldId}" field`;
    }
    throw new errors_1.CompileError(`No function has been implemented to render SQL for the filter operator "${filter.operator}"${targetString} of type "${type}"`);
};
const renderStringFilterSql = (dimensionSql, filter, stringQuoteChar) => {
    const nonEmptyFilterValues = filter.values?.filter((v) => v !== '');
    switch (filter.operator) {
        case filter_1.FilterOperator.EQUALS:
            return filter.values && filter.values.length > 0
                ? `(${dimensionSql}) IN (${filter.values
                    .map((v) => `${stringQuoteChar}${v}${stringQuoteChar}`)
                    .join(',')})`
                : 'true';
        case filter_1.FilterOperator.NOT_EQUALS:
            return filter.values && filter.values.length > 0
                ? `((${dimensionSql}) NOT IN (${filter.values
                    .map((v) => `${stringQuoteChar}${v}${stringQuoteChar}`)
                    .join(',')}) OR (${dimensionSql}) IS NULL)`
                : 'true';
        case filter_1.FilterOperator.INCLUDE:
            if (nonEmptyFilterValues && nonEmptyFilterValues.length > 0) {
                const includesQuery = nonEmptyFilterValues.map((v) => `LOWER(${dimensionSql}) LIKE LOWER('%${v}%')`);
                if (includesQuery.length > 1)
                    return `(${includesQuery.join('\n  OR\n  ')})`;
                return includesQuery.join('\n  OR\n  ');
            }
            return 'true';
        case filter_1.FilterOperator.NOT_INCLUDE:
            if (nonEmptyFilterValues && nonEmptyFilterValues.length > 0) {
                const notIncludeQuery = nonEmptyFilterValues.map((v) => `LOWER(${dimensionSql}) NOT LIKE LOWER('%${v}%')`);
                return notIncludeQuery.join('\n  AND\n  ');
            }
            return 'true';
        case filter_1.FilterOperator.NULL:
            return `(${dimensionSql}) IS NULL`;
        case filter_1.FilterOperator.NOT_NULL:
            return `(${dimensionSql}) IS NOT NULL`;
        case filter_1.FilterOperator.STARTS_WITH:
            const startWithQuery = nonEmptyFilterValues?.map((v) => `(${dimensionSql}) LIKE ${stringQuoteChar}${v}%${stringQuoteChar}`);
            return startWithQuery?.join('\n  OR\n  ') || 'true';
        case filter_1.FilterOperator.ENDS_WITH:
            const endsWithQuery = nonEmptyFilterValues?.map((v) => `(${dimensionSql}) LIKE ${stringQuoteChar}%${v}${stringQuoteChar}`);
            return endsWithQuery?.join('\n  OR\n  ') || 'true';
        default:
            return raiseInvalidFilterError('string', filter);
    }
};
exports.renderStringFilterSql = renderStringFilterSql;
// Validate that all values are valid numbers
const validateAndSanitizeNumber = (value) => {
    const num = Number(value);
    if (Number.isNaN(num) || !Number.isFinite(num)) {
        throw new errors_1.CompileError(`Invalid number value in filter: "${value}". Expected a valid number.`);
    }
    return num;
};
const isValidNumberFilterValue = (values) => !!values && values.length > 0;
const renderNumberFilterSql = (dimensionSql, filter) => {
    switch (filter.operator) {
        case filter_1.FilterOperator.EQUALS:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) IN (${filter.values
                    .map(validateAndSanitizeNumber)
                    .join(',')})`
                : 'true';
        case filter_1.FilterOperator.NOT_EQUALS:
            return !filter.values || filter.values.length === 0
                ? 'true'
                : `((${dimensionSql}) NOT IN (${filter.values
                    .map(validateAndSanitizeNumber)
                    .join(',')}) OR (${dimensionSql}) IS NULL)`;
        case filter_1.FilterOperator.NULL:
            return `(${dimensionSql}) IS NULL`;
        case filter_1.FilterOperator.NOT_NULL:
            return `(${dimensionSql}) IS NOT NULL`;
        case filter_1.FilterOperator.GREATER_THAN:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) > (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.GREATER_THAN_OR_EQUAL:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) >= (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.LESS_THAN:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) < (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.LESS_THAN_OR_EQUAL:
            return isValidNumberFilterValue(filter.values)
                ? `(${dimensionSql}) <= (${validateAndSanitizeNumber(filter.values[0])})`
                : 'true';
        case filter_1.FilterOperator.IN_BETWEEN:
            return !isValidNumberFilterValue(filter.values) ||
                filter.values.length < 2
                ? 'true'
                : `(${dimensionSql}) >= (${validateAndSanitizeNumber(filter.values[0])}) AND (${dimensionSql}) <= (${validateAndSanitizeNumber(filter.values[1])})`;
        case filter_1.FilterOperator.NOT_IN_BETWEEN:
            return !isValidNumberFilterValue(filter.values) ||
                filter.values.length < 2
                ? 'true'
                : `(${dimensionSql}) < (${validateAndSanitizeNumber(filter.values[0])}) OR (${dimensionSql}) > (${validateAndSanitizeNumber(filter.values[1])})`;
        default:
            return raiseInvalidFilterError('number', filter);
    }
};
exports.renderNumberFilterSql = renderNumberFilterSql;
const renderDateFilterSql = (dimensionSql, filter, adapterType, timezone, dateFormatter = formatting_1.formatDate, startOfWeek = undefined) => {
    const castValue = (value) => {
        switch (adapterType) {
            case dbt_1.SupportedDbtAdapter.TRINO: {
                return `CAST('${value}' AS timestamp)`;
            }
            default:
                return `('${value}')`;
        }
    };
    switch (filter.operator) {
        case filter_1.FilterOperator.EQUALS:
            return `(${dimensionSql}) = ${castValue(dateFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.NOT_EQUALS:
            return `((${dimensionSql}) != ${castValue(dateFormatter(filter.values?.[0]))} OR (${dimensionSql}) IS NULL)`;
        case filter_1.FilterOperator.NULL:
            return `(${dimensionSql}) IS NULL`;
        case filter_1.FilterOperator.NOT_NULL:
            return `(${dimensionSql}) IS NOT NULL`;
        case filter_1.FilterOperator.GREATER_THAN:
            return `(${dimensionSql}) > ${castValue(dateFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.GREATER_THAN_OR_EQUAL:
            return `(${dimensionSql}) >= ${castValue(dateFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.LESS_THAN:
            return `(${dimensionSql}) < ${castValue(dateFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.LESS_THAN_OR_EQUAL:
            return `(${dimensionSql}) <= ${castValue(dateFormatter(filter.values?.[0]))}`;
        case filter_1.FilterOperator.NOT_IN_THE_PAST:
        case filter_1.FilterOperator.IN_THE_PAST: {
            const unitOfTime = filter.settings?.unitOfTime || filter_1.UnitOfTime.days;
            const completed = !!filter.settings?.completed;
            const not = filter.operator === filter_1.FilterOperator.NOT_IN_THE_PAST
                ? 'NOT '
                : '';
            if (completed) {
                const completedDate = (0, moment_timezone_1.default)((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                    .startOf(unitOfTime)
                    .format(filter_1.unitOfTimeFormat[unitOfTime])).toDate();
                const untilDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                    .startOf(unitOfTime)
                    .toDate());
                return `${not}((${dimensionSql}) >= ${castValue(dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek, completedDate)
                    .subtract(filter.values?.[0], unitOfTime)
                    .toDate()))} AND (${dimensionSql}) < ${castValue(untilDate)})`;
            }
            const untilDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek).toDate());
            return `${not}((${dimensionSql}) >= ${castValue(dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                .subtract(filter.values?.[0], unitOfTime)
                .toDate()))} AND (${dimensionSql}) <= ${castValue(untilDate)})`;
        }
        case filter_1.FilterOperator.IN_THE_NEXT: {
            const unitOfTime = filter.settings?.unitOfTime || filter_1.UnitOfTime.days;
            const completed = !!filter.settings?.completed;
            if (completed) {
                const fromDate = (0, moment_timezone_1.default)((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                    .add(1, unitOfTime)
                    .startOf(unitOfTime)).toDate();
                const toDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek, fromDate)
                    .add(filter.values?.[0], unitOfTime)
                    .toDate());
                return `((${dimensionSql}) >= ${castValue(dateFormatter(fromDate))} AND (${dimensionSql}) < ${castValue(toDate)})`;
            }
            const fromDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek).toDate());
            const toDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                .add(filter.values?.[0], unitOfTime)
                .toDate());
            return `((${dimensionSql}) >= ${castValue(fromDate)} AND (${dimensionSql}) <= ${castValue(toDate)})`;
        }
        case filter_1.FilterOperator.IN_THE_CURRENT: {
            const unitOfTime = filter.settings?.unitOfTime || filter_1.UnitOfTime.days;
            const fromDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                .tz(timezone)
                .startOf(unitOfTime)
                .utc()
                .toDate());
            const untilDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                .tz(timezone)
                .endOf(unitOfTime)
                .utc()
                .toDate());
            const castedFromDate = castValue(fromDate);
            const castedUntilDate = castValue(untilDate);
            return `((${dimensionSql}) >= ${castedFromDate} AND (${dimensionSql}) <= ${castedUntilDate})`;
        }
        case filter_1.FilterOperator.NOT_IN_THE_CURRENT: {
            const unitOfTime = filter.settings?.unitOfTime || filter_1.UnitOfTime.days;
            const fromDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                .tz(timezone)
                .startOf(unitOfTime)
                .utc()
                .toDate());
            const untilDate = dateFormatter((0, time_1.getMomentDateWithCustomStartOfWeek)(startOfWeek)
                .tz(timezone)
                .endOf(unitOfTime)
                .utc()
                .toDate());
            const castedFromDate = castValue(fromDate);
            const castedUntilDate = castValue(untilDate);
            return `(NOT ((${dimensionSql}) >= ${castedFromDate} AND (${dimensionSql}) <= ${castedUntilDate}))`;
        }
        case filter_1.FilterOperator.IN_BETWEEN: {
            const startDate = dateFormatter(filter.values?.[0]);
            const endDate = dateFormatter(filter.values?.[1]);
            return `((${dimensionSql}) >= ${castValue(startDate)} AND (${dimensionSql}) <= ${castValue(endDate)})`;
        }
        default:
            return raiseInvalidFilterError('date', filter);
    }
};
exports.renderDateFilterSql = renderDateFilterSql;
const renderBooleanFilterSql = (dimensionSql, filter) => {
    switch (filter.operator) {
        case 'equals':
            return `(${dimensionSql}) = ${(0, booleanConverter_1.convertToBooleanValue)(filter.values?.[0])}`;
        case 'notEquals':
            return `((${dimensionSql}) != ${(0, booleanConverter_1.convertToBooleanValue)(filter.values?.[0])} OR (${dimensionSql}) IS NULL)`;
        case 'isNull':
            return `(${dimensionSql}) IS NULL`;
        case 'notNull':
            return `(${dimensionSql}) IS NOT NULL`;
        default:
            return raiseInvalidFilterError('boolean', filter);
    }
};
exports.renderBooleanFilterSql = renderBooleanFilterSql;
const escapeStringValuesOnFilterRule = (filterRule, escapeString) => ({
    ...filterRule,
    values: filterRule.values?.map((v) => typeof v === 'string'
        ? escapeString(v) // escape the string quote char
        : v),
});
const renderTableCalculationFilterRuleSql = (filterRule, field, fieldQuoteChar, stringQuoteChar, escapeString, adapterType, startOfWeek, timezone = 'UTC') => {
    if (!field)
        return '1=1';
    const fieldSql = `${fieldQuoteChar}${(0, item_1.getItemId)(field)}${fieldQuoteChar}`;
    const escapedFilterRule = escapeStringValuesOnFilterRule(filterRule, escapeString);
    // First we default to field.type
    // otherwise, we check the custom format for backwards compatibility
    switch (field.type) {
        case field_1.TableCalculationType.STRING:
            return (0, exports.renderStringFilterSql)(fieldSql, escapedFilterRule, stringQuoteChar);
        case field_1.TableCalculationType.DATE:
        case field_1.TableCalculationType.TIMESTAMP:
            return (0, exports.renderDateFilterSql)(fieldSql, escapedFilterRule, adapterType, timezone, undefined, startOfWeek);
        case field_1.TableCalculationType.NUMBER:
            return (0, exports.renderNumberFilterSql)(fieldSql, escapedFilterRule);
        case field_1.TableCalculationType.BOOLEAN:
            return (0, exports.renderBooleanFilterSql)(fieldSql, escapedFilterRule);
        default:
        // Do nothing here. This will try with format.type for backwards compatibility
    }
    switch (field.format?.type) {
        case field_1.CustomFormatType.PERCENT:
        case field_1.CustomFormatType.CURRENCY:
        case field_1.CustomFormatType.NUMBER: {
            return (0, exports.renderNumberFilterSql)(fieldSql, escapedFilterRule);
        }
        default:
            return (0, exports.renderStringFilterSql)(fieldSql, escapedFilterRule, stringQuoteChar);
    }
};
exports.renderTableCalculationFilterRuleSql = renderTableCalculationFilterRuleSql;
const renderFilterRuleSql = (filterRule, fieldType, fieldSql, stringQuoteChar, escapeString, startOfWeek, adapterType, timezone = 'UTC') => {
    if (filterRule.disabled) {
        return `1=1`; // When filter is disabled, we want to return all rows
    }
    const escapedFilterRule = escapeStringValuesOnFilterRule(filterRule, escapeString);
    switch (fieldType) {
        case field_1.DimensionType.STRING:
        case field_1.MetricType.STRING: {
            return (0, exports.renderStringFilterSql)(fieldSql, escapedFilterRule, stringQuoteChar);
        }
        case field_1.DimensionType.NUMBER:
        case field_1.MetricType.NUMBER:
        case field_1.MetricType.PERCENTILE:
        case field_1.MetricType.MEDIAN:
        case field_1.MetricType.AVERAGE:
        case field_1.MetricType.COUNT:
        case field_1.MetricType.COUNT_DISTINCT:
        case field_1.MetricType.SUM:
        case field_1.MetricType.MIN:
        case field_1.MetricType.MAX:
        case field_1.MetricType.PERCENT_OF_PREVIOUS:
        case field_1.MetricType.PERCENT_OF_TOTAL:
        case field_1.MetricType.RUNNING_TOTAL: {
            return (0, exports.renderNumberFilterSql)(fieldSql, escapedFilterRule);
        }
        case field_1.DimensionType.DATE:
        case field_1.MetricType.DATE: {
            return (0, exports.renderDateFilterSql)(fieldSql, escapedFilterRule, adapterType, timezone, undefined, startOfWeek);
        }
        case field_1.DimensionType.TIMESTAMP:
        case field_1.MetricType.TIMESTAMP: {
            return (0, exports.renderDateFilterSql)(fieldSql, escapedFilterRule, adapterType, timezone, formatTimestampAsUTCWithNoTimezone, startOfWeek);
        }
        case field_1.DimensionType.BOOLEAN:
        case field_1.MetricType.BOOLEAN: {
            return (0, exports.renderBooleanFilterSql)(fieldSql, escapedFilterRule);
        }
        default: {
            return (0, assertUnreachable_1.default)(fieldType, `No function implemented to render sql for filter group type ${fieldType}`);
        }
    }
};
exports.renderFilterRuleSql = renderFilterRuleSql;
// To be used for filters with a field that is a dimension or metric
const renderFilterRuleSqlFromField = (filterRule, field, fieldQuoteChar, stringQuoteChar, escapeString, startOfWeek, adapterType, timezone = 'UTC') => {
    const fieldType = (0, field_1.isCompiledCustomSqlDimension)(field)
        ? field.dimensionType
        : field.type;
    const fieldSql = (0, field_1.isMetric)(field)
        ? `${fieldQuoteChar}${(0, item_1.getItemId)(field)}${fieldQuoteChar}`
        : field.compiledSql;
    return (0, exports.renderFilterRuleSql)(filterRule, fieldType, fieldSql, stringQuoteChar, escapeString, startOfWeek, adapterType, timezone);
};
exports.renderFilterRuleSqlFromField = renderFilterRuleSqlFromField;
//# sourceMappingURL=filtersCompiler.js.map