import BadDataException from "../../Types/Exception/BadDataException";
/**
 * Shunting-yard based evaluator for metric formulas such as
 * "$A + $B * 2" or "(a - b) / 100". Variables are matched by metric
 * alias (case-insensitive, with or without a leading "$"). Evaluates
 * the formula point-by-point against a time-aligned series from the
 * referenced queries/formulas.
 */
var TokenType;
(function (TokenType) {
    TokenType["Number"] = "number";
    TokenType["Variable"] = "variable";
    TokenType["Operator"] = "operator";
    TokenType["LeftParen"] = "lparen";
    TokenType["RightParen"] = "rparen";
})(TokenType || (TokenType = {}));
const OPERATOR_PRECEDENCE = {
    "u-": 4,
    "u+": 4,
    "^": 3,
    "*": 2,
    "/": 2,
    "%": 2,
    "+": 1,
    "-": 1,
};
const RIGHT_ASSOCIATIVE = new Set(["^", "u-", "u+"]);
export default class MetricFormulaEvaluator {
    /**
     * Evaluate a formula against the provided query/formula results and
     * return a synthetic AggregatedResult whose timestamps are the union
     * of all referenced series.
     */
    static evaluateFormula(input) {
        const trimmedFormula = (input.formula || "").trim();
        if (!trimmedFormula) {
            return { data: [] };
        }
        const rpn = MetricFormulaEvaluator.toRpn(trimmedFormula);
        const variableResults = MetricFormulaEvaluator.buildVariableResultMap({
            queryConfigs: input.queryConfigs,
            formulaConfigs: input.formulaConfigs,
            results: input.results,
        });
        /*
         * Validate every variable referenced in the formula actually resolves
         * to a result series. Failing loudly is better than silently returning
         * NaN when a user typos an alias.
         */
        const referencedVariables = MetricFormulaEvaluator.collectVariableNames(rpn);
        for (const variableName of referencedVariables) {
            if (!variableResults[variableName]) {
                throw new BadDataException(`Formula references unknown variable "$${variableName}". Define a metric query with that alias first.`);
            }
        }
        const timestampIndex = MetricFormulaEvaluator.buildTimestampIndex(referencedVariables, variableResults);
        const sortedTimestamps = Array.from(timestampIndex.keys()).sort();
        const resultData = [];
        for (const timestampString of sortedTimestamps) {
            const values = timestampIndex.get(timestampString) || {};
            /*
             * Skip points where any referenced variable is missing a value. A
             * partial join would produce misleading numbers (e.g. treating a
             * gap as zero silently when using subtraction).
             */
            const hasAllValues = referencedVariables.every((variable) => {
                return typeof values[variable] === "number";
            });
            if (!hasAllValues) {
                continue;
            }
            let evaluated;
            try {
                evaluated = MetricFormulaEvaluator.evaluateRpn(rpn, values);
            }
            catch (_a) {
                continue;
            }
            if (!Number.isFinite(evaluated)) {
                continue;
            }
            resultData.push({
                timestamp: new Date(timestampString),
                value: evaluated,
            });
        }
        return { data: resultData };
    }
    /**
     * Return the set of variables referenced by a formula, preserving the
     * order of first appearance. Variable names are lower-cased to match
     * the evaluator's case-insensitive lookup. Invalid formulas return an
     * empty list rather than throwing, so callers rendering UI don't have
     * to add defensive try/catch.
     */
    static getReferencedVariables(formula) {
        const trimmedFormula = (formula || "").trim();
        if (!trimmedFormula) {
            return [];
        }
        try {
            const rpn = MetricFormulaEvaluator.toRpn(trimmedFormula);
            return MetricFormulaEvaluator.collectVariableNames(rpn);
        }
        catch (_a) {
            return [];
        }
    }
    /**
     * Validate a formula's syntax without evaluating it. Returns `null`
     * when valid, otherwise a human-readable error message.
     */
    static validateFormula(input) {
        const trimmedFormula = (input.formula || "").trim();
        if (!trimmedFormula) {
            return "Formula is required.";
        }
        let rpn;
        try {
            rpn = MetricFormulaEvaluator.toRpn(trimmedFormula);
        }
        catch (err) {
            return err.message || "Invalid formula.";
        }
        const referenced = MetricFormulaEvaluator.collectVariableNames(rpn);
        const available = new Set(input.availableVariables.map((v) => {
            return v.toLowerCase();
        }));
        for (const variable of referenced) {
            if (!available.has(variable.toLowerCase())) {
                return `Formula references unknown variable "$${variable}".`;
            }
        }
        return null;
    }
    static buildVariableResultMap(input) {
        var _a, _b, _c, _d;
        const variableMap = {};
        const totalSeries = input.queryConfigs.length + input.formulaConfigs.length;
        for (let index = 0; index < totalSeries; index++) {
            const result = input.results[index];
            if (!result) {
                continue;
            }
            let alias;
            if (index < input.queryConfigs.length) {
                alias =
                    ((_b = (_a = input.queryConfigs[index]) === null || _a === void 0 ? void 0 : _a.metricAliasData) === null || _b === void 0 ? void 0 : _b.metricVariable) ||
                        undefined;
            }
            else {
                const formulaIndex = index - input.queryConfigs.length;
                alias =
                    ((_d = (_c = input.formulaConfigs[formulaIndex]) === null || _c === void 0 ? void 0 : _c.metricAliasData) === null || _d === void 0 ? void 0 : _d.metricVariable) ||
                        undefined;
            }
            if (!alias) {
                continue;
            }
            variableMap[alias.toLowerCase()] = result;
        }
        return variableMap;
    }
    static buildTimestampIndex(variables, variableResults) {
        const index = new Map();
        for (const variable of variables) {
            const series = variableResults[variable];
            if (!series) {
                continue;
            }
            for (const sample of series.data) {
                const timestampKey = MetricFormulaEvaluator.normalizeTimestamp(sample.timestamp);
                if (!index.has(timestampKey)) {
                    index.set(timestampKey, {});
                }
                const bucket = index.get(timestampKey) || {};
                bucket[variable] = sample.value;
            }
        }
        return index;
    }
    static normalizeTimestamp(timestamp) {
        if (timestamp instanceof Date) {
            return timestamp.toISOString();
        }
        /*
         * ClickHouse sometimes returns timestamps at varying precisions
         * ("2024-01-01T00:00:00" vs "2024-01-01T00:00:00.000Z"). Normalize
         * by parsing through Date so differently formatted strings for the
         * same instant align correctly.
         */
        const asDate = new Date(timestamp);
        if (!isNaN(asDate.getTime())) {
            return asDate.toISOString();
        }
        return String(timestamp);
    }
    static collectVariableNames(rpn) {
        const seen = new Set();
        const result = [];
        for (const token of rpn) {
            if (token.type === TokenType.Variable) {
                const normalized = token.value.toLowerCase();
                if (!seen.has(normalized)) {
                    seen.add(normalized);
                    result.push(normalized);
                }
            }
        }
        return result;
    }
    static tokenize(expression) {
        const tokens = [];
        let position = 0;
        while (position < expression.length) {
            const char = expression[position];
            if (char === " " || char === "\t" || char === "\n") {
                position++;
                continue;
            }
            // Number literal (with optional decimal and exponent)
            if (char >= "0" && char <= "9") {
                let numberBuffer = "";
                while (position < expression.length &&
                    MetricFormulaEvaluator.isNumberChar(expression[position], numberBuffer)) {
                    numberBuffer += expression[position];
                    position++;
                }
                tokens.push({ type: TokenType.Number, value: numberBuffer });
                continue;
            }
            // Decimal starting without leading 0 (e.g. ".5")
            if (char === "." &&
                position + 1 < expression.length &&
                expression[position + 1] >= "0" &&
                expression[position + 1] <= "9") {
                let numberBuffer = "";
                while (position < expression.length &&
                    MetricFormulaEvaluator.isNumberChar(expression[position], numberBuffer)) {
                    numberBuffer += expression[position];
                    position++;
                }
                tokens.push({ type: TokenType.Number, value: numberBuffer });
                continue;
            }
            // Variable — may be prefixed with "$" or bare ("a", "b1", etc.)
            if (char === "$" || MetricFormulaEvaluator.isIdentifierStart(char)) {
                if (char === "$") {
                    position++;
                }
                let identifier = "";
                while (position < expression.length &&
                    MetricFormulaEvaluator.isIdentifierPart(expression[position])) {
                    identifier += expression[position];
                    position++;
                }
                if (!identifier) {
                    throw new BadDataException(`Unexpected character "$" without a variable name.`);
                }
                tokens.push({ type: TokenType.Variable, value: identifier });
                continue;
            }
            if (char === "(") {
                tokens.push({ type: TokenType.LeftParen, value: char });
                position++;
                continue;
            }
            if (char === ")") {
                tokens.push({ type: TokenType.RightParen, value: char });
                position++;
                continue;
            }
            if (char === "+" ||
                char === "-" ||
                char === "*" ||
                char === "/" ||
                char === "%" ||
                char === "^") {
                tokens.push({ type: TokenType.Operator, value: char });
                position++;
                continue;
            }
            throw new BadDataException(`Unexpected character "${char}" at position ${position}.`);
        }
        return tokens;
    }
    static isNumberChar(char, currentBuffer) {
        if (char >= "0" && char <= "9") {
            return true;
        }
        if (char === "." && !currentBuffer.includes(".")) {
            return true;
        }
        if ((char === "e" || char === "E") &&
            !currentBuffer.toLowerCase().includes("e") &&
            currentBuffer.length > 0) {
            return true;
        }
        if ((char === "+" || char === "-") &&
            currentBuffer.length > 0 &&
            (currentBuffer[currentBuffer.length - 1] === "e" ||
                currentBuffer[currentBuffer.length - 1] === "E")) {
            return true;
        }
        return false;
    }
    static isIdentifierStart(char) {
        return ((char >= "a" && char <= "z") ||
            (char >= "A" && char <= "Z") ||
            char === "_");
    }
    static isIdentifierPart(char) {
        return (MetricFormulaEvaluator.isIdentifierStart(char) ||
            (char >= "0" && char <= "9"));
    }
    static toRpn(expression) {
        const tokens = MetricFormulaEvaluator.tokenize(expression);
        const output = [];
        const operatorStack = [];
        let previousToken = null;
        for (const token of tokens) {
            if (token.type === TokenType.Number) {
                output.push(token);
            }
            else if (token.type === TokenType.Variable) {
                output.push(token);
            }
            else if (token.type === TokenType.Operator) {
                let operatorValue = token.value;
                const isUnary = (operatorValue === "+" || operatorValue === "-") &&
                    (previousToken === null ||
                        previousToken.type === TokenType.Operator ||
                        previousToken.type === TokenType.LeftParen);
                if (isUnary) {
                    operatorValue = operatorValue === "-" ? "u-" : "u+";
                }
                while (operatorStack.length > 0) {
                    const top = operatorStack[operatorStack.length - 1];
                    if (top.type !== TokenType.Operator) {
                        break;
                    }
                    const topPrecedence = OPERATOR_PRECEDENCE[top.value];
                    const currentPrecedence = OPERATOR_PRECEDENCE[operatorValue];
                    const isRightAssociative = RIGHT_ASSOCIATIVE.has(operatorValue);
                    if (topPrecedence > currentPrecedence ||
                        (topPrecedence === currentPrecedence && !isRightAssociative)) {
                        output.push(operatorStack.pop());
                    }
                    else {
                        break;
                    }
                }
                operatorStack.push({ type: TokenType.Operator, value: operatorValue });
            }
            else if (token.type === TokenType.LeftParen) {
                operatorStack.push(token);
            }
            else if (token.type === TokenType.RightParen) {
                let foundLeftParen = false;
                while (operatorStack.length > 0) {
                    const top = operatorStack.pop();
                    if (top.type === TokenType.LeftParen) {
                        foundLeftParen = true;
                        break;
                    }
                    output.push(top);
                }
                if (!foundLeftParen) {
                    throw new BadDataException("Mismatched parentheses in formula.");
                }
            }
            previousToken = token;
        }
        while (operatorStack.length > 0) {
            const top = operatorStack.pop();
            if (top.type === TokenType.LeftParen ||
                top.type === TokenType.RightParen) {
                throw new BadDataException("Mismatched parentheses in formula.");
            }
            output.push(top);
        }
        return output;
    }
    static evaluateRpn(rpn, variableValues) {
        const stack = [];
        for (const token of rpn) {
            if (token.type === TokenType.Number) {
                stack.push(parseFloat(token.value));
                continue;
            }
            if (token.type === TokenType.Variable) {
                const variableName = token.value.toLowerCase();
                const variableValue = variableValues[variableName];
                if (typeof variableValue !== "number") {
                    throw new BadDataException(`Missing value for variable "${variableName}".`);
                }
                stack.push(variableValue);
                continue;
            }
            if (token.type === TokenType.Operator) {
                const operatorValue = token.value;
                if (operatorValue === "u-" || operatorValue === "u+") {
                    const operand = stack.pop();
                    if (typeof operand !== "number") {
                        throw new BadDataException("Invalid formula: missing operand.");
                    }
                    stack.push(operatorValue === "u-" ? -operand : operand);
                    continue;
                }
                const right = stack.pop();
                const left = stack.pop();
                if (typeof right !== "number" || typeof left !== "number") {
                    throw new BadDataException("Invalid formula: missing operands.");
                }
                switch (operatorValue) {
                    case "+":
                        stack.push(left + right);
                        break;
                    case "-":
                        stack.push(left - right);
                        break;
                    case "*":
                        stack.push(left * right);
                        break;
                    case "/":
                        if (right === 0) {
                            stack.push(NaN);
                        }
                        else {
                            stack.push(left / right);
                        }
                        break;
                    case "%":
                        if (right === 0) {
                            stack.push(NaN);
                        }
                        else {
                            stack.push(left % right);
                        }
                        break;
                    case "^":
                        stack.push(Math.pow(left, right));
                        break;
                }
            }
        }
        if (stack.length !== 1) {
            throw new BadDataException("Invalid formula expression.");
        }
        return stack[0];
    }
}
//# sourceMappingURL=MetricFormulaEvaluator.js.map