import object from '@jsep-plugin/object';
import jsep from 'jsep';
jsep.plugins.register(object);
// Default operator precedence from https://github.com/EricSmekens/jsep/blob/master/src/jsep.js#L55
const DEFAULT_PRECEDENCE = {
    '||': 1,
    '&&': 2,
    '|': 3,
    '^': 4,
    '&': 5,
    '==': 6,
    '!=': 6,
    '===': 6,
    '!==': 6,
    '<': 7,
    '>': 7,
    '<=': 7,
    '>=': 7,
    '<<': 8,
    '>>': 8,
    '>>>': 8,
    '+': 9,
    '-': 9,
    '*': 10,
    '/': 10,
    '%': 10
};
/**
 * Evaluation code from JSEP project, under MIT License.
 * Copyright (c) 2013 Stephen Oney, http://jsep.from.so/
 */
const binops = {
    // '|'(a, b) {
    //   return a | b;
    // },
    // '^'(a, b) {
    //   return a ^ b;
    // },
    // '&'(a, b) {
    //   return a & b;
    // },
    '=='(a, b) {
        // biome-ignore lint/suspicious/noDoubleEquals: ignore
        return a == b;
    },
    '!='(a, b) {
        // biome-ignore lint/suspicious/noDoubleEquals: ignore
        return a != b;
    },
    '==='(a, b) {
        return a === b;
    },
    '!=='(a, b) {
        return a !== b;
    },
    '<'(a, b) {
        return a < b;
    },
    '>'(a, b) {
        return a > b;
    },
    '<='(a, b) {
        return a <= b;
    },
    '>='(a, b) {
        return a >= b;
    },
    // '<<'(a, b) {
    //   return a << b;
    // },
    // '>>'(a, b) {
    //   return a >> b;
    // },
    // '>>>'(a, b) {
    //   return a >>> b;
    // },
    '+'(a, b) {
        return a + b;
    },
    '-'(a, b) {
        return a - b;
    },
    '*'(a, b) {
        return a * b;
    },
    '/'(a, b) {
        return a / b;
    },
    '%'(a, b) {
        return a % b;
    }
};
const unops = {
    '-'(a) {
        return -a;
    },
    '+'(a) {
        return Number(a);
    },
    // '~'(a) {
    //   return ~a;
    // },
    '!'(a) {
        return !a;
    }
};
function evaluateArray(list, context) {
    return list.map((v) => (v ? evaluate(v, context) : undefined));
}
function evaluateObject(properties, context) {
    const obj = {};
    for (const node of properties) {
        const key = !node.computed && node.key.type === 'Identifier' ? node.key.name : evaluate(node.key, context);
        obj[key] = evaluate(node.value, context);
    }
    return obj;
}
function isObject(x) {
    return Boolean(x) && typeof x === 'object';
}
export const METHODS_NOT_SUPPORTED = 'Methods are not supported in expressions';
export const THIS_NOT_SUPPORTED = 'This is not supported in expressions';
function evaluateMember(node, context) {
    const object = evaluate(node.object, context);
    let key;
    if (node.computed) {
        key = evaluate(node.property, context);
    }
    else {
        key = node.property.name;
    }
    if (typeof key === 'string' && /^__proto__|prototype|constructor$/.test(key)) {
        throw Error(`Access to member "${key}" disallowed.`);
    }
    if (isObject(object) && Object.hasOwn(object, key)) {
        return [object, object[key]];
    }
}
function evaluate(_node, context) {
    const node = _node;
    switch (node.type) {
        case 'ArrayExpression':
            return evaluateArray(node.elements, context);
        case 'ObjectExpression':
            return evaluateObject(node.properties, context);
        case 'CallExpression': {
            if (node.callee.type === 'MemberExpression') {
                throw Error(METHODS_NOT_SUPPORTED);
            }
            const fn = evaluate(node.callee, context);
            if (typeof fn !== 'function') {
                return undefined;
            }
            return fn.apply(null, evaluateArray(node.arguments, context));
        }
        case 'ConditionalExpression':
            return evaluate(node.test, context) ? evaluate(node.consequent, context) : evaluate(node.alternate, context);
        case 'Identifier':
            return context[node.name];
        case 'Literal':
            return node.value;
        case 'BinaryExpression': {
            if (node.operator === '||') {
                return evaluate(node.left, context) || evaluate(node.right, context);
            }
            if (node.operator === '&&') {
                return evaluate(node.left, context) && evaluate(node.right, context);
            }
            const op = binops[node.operator];
            if (!op) {
                throw new Error(`Unsupported operator ${node.operator}`);
            }
            return op(evaluate(node.left, context), evaluate(node.right, context));
        }
        case 'UnaryExpression': {
            const op = unops[node.operator];
            if (!op) {
                throw new Error(`Unsupported operator ${node.operator}`);
            }
            return op(evaluate(node.argument, context));
        }
        case 'MemberExpression':
            return evaluateMember(node, context)?.[1];
        case 'ThisExpression':
            return context;
        default:
            return undefined;
    }
}
export function parseExpression(expression) {
    return jsep(expression);
}
function validateAST(options, rootNode) {
    const validate = (_node, parent, parentKey) => {
        const node = _node;
        switch (node.type) {
            case 'ArrayExpression':
                return node.elements.flatMap((v) => (v ? validate(v, node, 'elements') : []));
            case 'ObjectExpression':
                return node.properties.flatMap((v) => validate(v, node, 'properties'));
            case 'Property': {
                {
                    const keyErrors = !node.computed && node.key.type === 'Identifier' ? undefined : validate(node.key, node, 'key');
                    const valueErrors = validate(node.value, node, 'value');
                    return keyErrors ? [...keyErrors, ...valueErrors] : valueErrors;
                }
            }
            case 'CallExpression': {
                if (node.callee.type === 'MemberExpression') {
                    return [{ message: METHODS_NOT_SUPPORTED }];
                }
                return [
                    ...validate(node.callee, node, 'callee'),
                    ...node.arguments.flatMap((v) => (v ? validate(v, node, 'arguments') : []))
                ];
            }
            case 'ConditionalExpression':
                return [
                    ...validate(node.test, node, 'test'),
                    ...validate(node.consequent, node, 'consequent'),
                    ...validate(node.alternate, node, 'alternate')
                ];
            case 'Identifier': {
                const errors = [];
                if (parent?.type === 'CallExpression' && parentKey === 'callee' && options.allowedFunctions) {
                    if (!options.allowedFunctions.includes(node.name)) {
                        errors.push({ message: `Unknown function "${node.name}"` });
                    }
                }
                else {
                    if (parent?.type === 'MemberExpression' && !parent.computed && parentKey === 'property') {
                        return errors;
                    }
                    if (options.allowedVars && !options.allowedVars.includes(node.name)) {
                        errors.push({ message: `Unknown variable "${node.name}"` });
                    }
                }
                return errors;
            }
            case 'Literal':
                return [];
            case 'BinaryExpression': {
                const op = binops[node.operator];
                if (!op) {
                    throw new Error(`Unsupported operator ${node.operator}`);
                }
                return [...validate(node.left, node, 'left'), ...validate(node.right, node, 'right')];
            }
            case 'UnaryExpression': {
                const op = unops[node.operator];
                if (!op) {
                    return [{ message: `Unsupported operator ${node.operator}` }];
                }
                return validate(node.argument, node, 'argument');
            }
            case 'MemberExpression':
                return [
                    ...validate(node.object, node, 'object'),
                    ...(node.computed ? validate(node.property, node, 'property') : [])
                ];
            case 'ThisExpression':
                return [{ message: THIS_NOT_SUPPORTED }];
        }
    };
    return validate(rootNode, undefined, undefined);
}
const contextKeys = [
    'currentStateId',
    'previousStateId',
    'currentStateOutput',
    'sessionMemory',
    'claims',
    'args',
    'vars'
];
export function validateExpression(expression) {
    try {
        const ast = parseExpression(expression);
        const errors = validateAST({
            allowedVars: contextKeys,
            allowedFunctions: ['contains']
        }, ast);
        if (errors.length) {
            return {
                valid: false,
                errors: errors.map((e) => e.message)
            };
        }
        return { valid: true };
    }
    catch (e) {
        return { valid: false, errors: [e.message] };
    }
}
export function compileExpression(expression) {
    const ast = parseExpression(expression);
    return (context) => evaluate(ast, context);
}
export function expressionToString(_node, parent) {
    if (!_node) {
        return '';
    }
    const node = _node;
    if (node.type === 'BinaryExpression') {
        const wrap = parent?.type === 'BinaryExpression' && DEFAULT_PRECEDENCE[parent.operator] > DEFAULT_PRECEDENCE[node.operator];
        const str = `${expressionToString(node.left, node)} ${node.operator} ${expressionToString(node.right, node)}`;
        return wrap ? `(${str})` : str;
    }
    if (node.type === 'CallExpression') {
        const args = node.arguments.map((arg) => expressionToString(arg, node));
        return `${expressionToString(node.callee, node)}(${args.join(', ')})`;
    }
    if (node.type === 'UnaryExpression') {
        return node.operator + expressionToString(node.argument, node);
    }
    if (node.type === 'MemberExpression') {
        if (node.computed) {
            return `${expressionToString(node.object, node)}[${expressionToString(node.property, node)}]`;
        }
        return `${expressionToString(node.object, node)}.${expressionToString(node.property, node)}`;
    }
    if (node.type === 'ArrayExpression') {
        const arr = node.elements.map((element) => expressionToString(element, node)).join(',');
        return `[${arr}]`;
    }
    if (node.type === 'Property') {
        const keyStr = expressionToString(node.key, node);
        if (node.shorthand) {
            return keyStr;
        }
        return `${node.computed ? `[${keyStr}]` : keyStr}: ${expressionToString(node.value, node)}`;
    }
    if (node.type === 'ObjectExpression') {
        const props = node.properties.map((element) => expressionToString(element, node)).join(',');
        return `{ ${props} }`;
    }
    if (node.type === 'Literal') {
        return node.raw;
    }
    if (node.type === 'Identifier') {
        return node.name;
    }
    if (node.type === 'ConditionalExpression') {
        return `${expressionToString(node.test, node)} ? ${expressionToString(node.consequent, node)} : ${expressionToString(node.alternate, node)}`;
    }
    if (node.type === 'ThisExpression') {
        return 'this';
    }
    return '';
}
