"use strict";
/**
 * ConditionParser — tiny boolean expression engine for workflow step
 * conditions.
 *
 * Story 10.3 — OpenLife v1.4.
 *
 * Replaces the v1.2/v1.3 literal-only `conditionMet()` in WorkflowEngine.
 *
 * Grammar (precedence-climbing, lowest to highest):
 *   expr      := orExpr
 *   orExpr    := andExpr ( "OR" andExpr )*
 *   andExpr   := notExpr ( "AND" notExpr )*
 *   notExpr   := "NOT" notExpr | primary
 *   primary   := "(" expr ")" | comparison | identifier
 *   comparison := identifier ( "==" | "!=" ) literal
 *   identifier := [A-Za-z_][A-Za-z0-9_.]*
 *   literal    := number | "true" | "false" | "null" | quoted-string
 *
 * Backward-compat:
 *   A bare identifier `foo` evaluates to `ctx.foo === true` (same as v1.2).
 *
 * Never throws. On parse failure, evaluate() returns false and the
 * `parse()` result carries the error.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseCondition = parseCondition;
exports.evaluate = evaluate;
exports.evaluateCondition = evaluateCondition;
function tokenize(input) {
    const tokens = [];
    let i = 0;
    while (i < input.length) {
        const c = input[i];
        if (/\s/.test(c)) {
            i++;
            continue;
        }
        if (c === '(' || c === ')') {
            tokens.push({ type: 'op', value: c });
            i++;
            continue;
        }
        if (c === '=' && input[i + 1] === '=') {
            tokens.push({ type: 'op', value: '==' });
            i += 2;
            continue;
        }
        if (c === '!' && input[i + 1] === '=') {
            tokens.push({ type: 'op', value: '!=' });
            i += 2;
            continue;
        }
        if (c === '"' || c === "'") {
            const end = input.indexOf(c, i + 1);
            if (end === -1)
                return null; // unterminated
            tokens.push({ type: 'string', value: input.slice(i + 1, end) });
            i = end + 1;
            continue;
        }
        if (/[0-9-]/.test(c)) {
            const m = input.slice(i).match(/^-?\d+(?:\.\d+)?/);
            if (m) {
                tokens.push({ type: 'number', value: Number(m[0]) });
                i += m[0].length;
                continue;
            }
        }
        if (/[A-Za-z_]/.test(c)) {
            const m = input.slice(i).match(/^[A-Za-z_][A-Za-z0-9_.]*/);
            if (m) {
                const word = m[0];
                if (word === 'AND')
                    tokens.push({ type: 'op', value: 'AND' });
                else if (word === 'OR')
                    tokens.push({ type: 'op', value: 'OR' });
                else if (word === 'NOT')
                    tokens.push({ type: 'op', value: 'NOT' });
                else if (word === 'true')
                    tokens.push({ type: 'bool', value: true });
                else if (word === 'false')
                    tokens.push({ type: 'bool', value: false });
                else if (word === 'null')
                    tokens.push({ type: 'null' });
                else
                    tokens.push({ type: 'ident', value: word });
                i += word.length;
                continue;
            }
        }
        return null; // unknown char
    }
    return tokens;
}
// ─── Parser (recursive-descent, precedence-climbing) ────────
class Parser {
    tokens;
    pos = 0;
    constructor(tokens) {
        this.tokens = tokens;
    }
    parseTopLevel() {
        const node = this.parseOr();
        if (this.pos < this.tokens.length)
            return null; // leftover tokens
        return node;
    }
    peek() { return this.tokens[this.pos]; }
    consume() { return this.tokens[this.pos++]; }
    parseOr() {
        let left = this.parseAnd();
        if (!left)
            return null;
        while (this.peek()?.type === 'op' && this.peek().value === 'OR') {
            this.consume();
            const right = this.parseAnd();
            if (!right)
                return null;
            left = { kind: 'or', left, right };
        }
        return left;
    }
    parseAnd() {
        let left = this.parseNot();
        if (!left)
            return null;
        while (this.peek()?.type === 'op' && this.peek().value === 'AND') {
            this.consume();
            const right = this.parseNot();
            if (!right)
                return null;
            left = { kind: 'and', left, right };
        }
        return left;
    }
    parseNot() {
        if (this.peek()?.type === 'op' && this.peek().value === 'NOT') {
            this.consume();
            const inner = this.parseNot();
            if (!inner)
                return null;
            return { kind: 'not', inner };
        }
        return this.parsePrimary();
    }
    parsePrimary() {
        const tok = this.peek();
        if (!tok)
            return null;
        if (tok.type === 'op' && tok.value === '(') {
            this.consume();
            const inner = this.parseOr();
            const close = this.consume();
            if (!close || close.type !== 'op' || close.value !== ')')
                return null;
            return inner;
        }
        if (tok.type === 'ident') {
            this.consume();
            // Check for comparison operator
            const next = this.peek();
            if (next?.type === 'op' && (next.value === '==' || next.value === '!=')) {
                const opTok = this.consume();
                const litTok = this.consume();
                if (!litTok)
                    return null;
                let lit;
                if (litTok.type === 'string')
                    lit = litTok.value;
                else if (litTok.type === 'number')
                    lit = litTok.value;
                else if (litTok.type === 'bool')
                    lit = litTok.value;
                else if (litTok.type === 'null')
                    lit = null;
                else
                    return null;
                return { kind: opTok.value === '==' ? 'eq' : 'neq', ident: tok.value, literal: lit };
            }
            return { kind: 'truthy', ident: tok.value };
        }
        return null;
    }
}
// ─── Public API ────────────────────────────────────────────
function parseCondition(input) {
    const tokens = tokenize(input);
    if (!tokens)
        return { ok: false, error: 'tokenize_failed' };
    if (tokens.length === 0)
        return { ok: false, error: 'empty_expression' };
    const ast = new Parser(tokens).parseTopLevel();
    if (!ast)
        return { ok: false, error: 'parse_failed' };
    return { ok: true, ast };
}
/** Resolve `a.b.c` against a context object. Returns undefined on miss. */
function resolveIdent(ident, ctx) {
    const parts = ident.split('.');
    let cur = ctx;
    for (const p of parts) {
        if (cur === null || cur === undefined || typeof cur !== 'object')
            return undefined;
        cur = cur[p];
    }
    return cur;
}
function evaluate(ast, ctx) {
    switch (ast.kind) {
        case 'and': return evaluate(ast.left, ctx) && evaluate(ast.right, ctx);
        case 'or': return evaluate(ast.left, ctx) || evaluate(ast.right, ctx);
        case 'not': return !evaluate(ast.inner, ctx);
        case 'truthy': return resolveIdent(ast.ident, ctx) === true;
        case 'eq': return resolveIdent(ast.ident, ctx) === ast.literal;
        case 'neq': return resolveIdent(ast.ident, ctx) !== ast.literal;
        default: return false;
    }
}
/**
 * One-shot helper used by WorkflowEngine. Parses + evaluates in one call.
 * Backwards-compatible: empty string returns true (no condition = pass).
 * Parse failures return false (refuse to run if expression is broken).
 */
function evaluateCondition(expr, ctx) {
    if (!expr || expr.trim() === '')
        return true;
    const r = parseCondition(expr);
    if (!r.ok || !r.ast)
        return false;
    return evaluate(r.ast, ctx);
}
