import { matchesEventPath } from './matcher.js';
import { logger } from '../utils/logger.js';
import { summarizeError } from '../utils/error-display.js';
/** Parse a duration string like "30s" or "5m" into milliseconds */
function parseDuration(s) {
    const match = s.match(/^(\d+(\.\d+)?)(s|m|ms)$/);
    if (!match)
        return 0;
    const value = parseFloat(match[1]);
    const unit = match[3];
    if (unit === 'ms')
        return value;
    if (unit === 's')
        return value * 1000;
    if (unit === 'm')
        return value * 60_000;
    return 0;
}
function tokenize(input) {
    const tokens = [];
    let i = 0;
    while (i < input.length) {
        // Skip whitespace
        if (/\s/.test(input[i])) {
            i++;
            continue;
        }
        // String literals (single or double quoted)
        if (input[i] === '\'' || input[i] === '"') {
            const quote = input[i++];
            let s = '';
            while (i < input.length && input[i] !== quote) {
                if (input[i] === '\\' && i + 1 < input.length) {
                    i++;
                }
                s += input[i++];
            }
            i++; // closing quote
            tokens.push({ kind: 'string', value: s });
            continue;
        }
        // Numbers
        if (/[0-9]/.test(input[i]) || (input[i] === '-' && /[0-9]/.test(input[i + 1] ?? ''))) {
            let n = '';
            if (input[i] === '-')
                n += input[i++];
            while (i < input.length && /[0-9.]/.test(input[i]))
                n += input[i++];
            tokens.push({ kind: 'number', value: parseFloat(n) });
            continue;
        }
        // Operators: ===, !==, >=, <=, >, <, &&, ||
        if (input.startsWith('===', i)) {
            tokens.push({ kind: 'op', value: '===' });
            i += 3;
            continue;
        }
        if (input.startsWith('!==', i)) {
            tokens.push({ kind: 'op', value: '!==' });
            i += 3;
            continue;
        }
        if (input.startsWith('>=', i)) {
            tokens.push({ kind: 'op', value: '>=' });
            i += 2;
            continue;
        }
        if (input.startsWith('<=', i)) {
            tokens.push({ kind: 'op', value: '<=' });
            i += 2;
            continue;
        }
        if (input.startsWith('&&', i)) {
            tokens.push({ kind: 'op', value: '&&' });
            i += 2;
            continue;
        }
        if (input.startsWith('||', i)) {
            tokens.push({ kind: 'op', value: '||' });
            i += 2;
            continue;
        }
        if (input[i] === '>') {
            tokens.push({ kind: 'op', value: '>' });
            i++;
            continue;
        }
        if (input[i] === '<') {
            tokens.push({ kind: 'op', value: '<' });
            i++;
            continue;
        }
        if (input[i] === '!') {
            tokens.push({ kind: 'bang' });
            i++;
            continue;
        }
        if (input[i] === '(') {
            tokens.push({ kind: 'lparen' });
            i++;
            continue;
        }
        if (input[i] === ')') {
            tokens.push({ kind: 'rparen' });
            i++;
            continue;
        }
        if (input[i] === '.') {
            tokens.push({ kind: 'dot' });
            i++;
            continue;
        }
        // Identifiers and keywords
        if (/[a-zA-Z_$]/.test(input[i])) {
            let id = '';
            while (i < input.length && /[a-zA-Z0-9_$]/.test(input[i]))
                id += input[i++];
            if (id === 'true')
                tokens.push({ kind: 'bool', value: true });
            else if (id === 'false')
                tokens.push({ kind: 'bool', value: false });
            else if (id === 'null')
                tokens.push({ kind: 'null' });
            else if (id === 'undefined')
                tokens.push({ kind: 'undefined' });
            else
                tokens.push({ kind: 'ident', value: id });
            continue;
        }
        // Unknown character — skip (will surface as parse error)
        i++;
    }
    return tokens;
}
/**
 * Evaluate a safe condition expression against a context object.
 *
 * Allowed syntax:
 *   - Property access on context: `message`, `count`, `status`
 *   - Dot-chained property access: `event.type`
 *   - Comparison operators: `===`, `!==`, `>`, `<`, `>=`, `<=`
 *   - Logical operators: `&&`, `||`
 *   - Unary negation: `!expr`
 *   - String methods: `.startsWith()`, `.endsWith()`, `.includes()`
 *   - Literals: strings, numbers, booleans, null, undefined
 *   - Grouping: `(expr)`
 *
 * Does NOT use `eval`, `new Function`, or `vm.runInNewContext`.
 * Any expression that references identifiers not in the context, or
 * uses unsupported constructs, evaluates to false.
 */
export function safeEvaluate(condition, context) {
    const tokens = tokenize(condition);
    let pos = 0;
    function peek() { return tokens[pos]; }
    function consume() { return tokens[pos++]; }
    function parseExpr() { return parseOr(); }
    function parseOr() {
        let left = parseAnd();
        while (peek()?.kind === 'op' && peek().value === '||') {
            consume();
            const right = parseAnd();
            left = !!(left) || !!(right);
        }
        return left;
    }
    function parseAnd() {
        let left = parseComparison();
        while (peek()?.kind === 'op' && peek().value === '&&') {
            consume();
            const right = parseComparison();
            left = !!(left) && !!(right);
        }
        return left;
    }
    function parseComparison() {
        let left = parseUnary();
        const t = peek();
        if (t?.kind === 'op') {
            const op = t.value;
            if (['===', '!==', '>', '<', '>=', '<='].includes(op)) {
                consume();
                const right = parseUnary();
                switch (op) {
                    case '===': return left === right;
                    case '!==': return left !== right;
                    case '>': return left > right;
                    case '<': return left < right;
                    case '>=': return left >= right;
                    case '<=': return left <= right;
                }
            }
        }
        return left;
    }
    function parseUnary() {
        if (peek()?.kind === 'bang') {
            consume();
            return !parseUnary();
        }
        return parsePostfix();
    }
    function parsePostfix() {
        let val = parsePrimary();
        // Handle dot-chained property access and allowed string methods
        while (peek()?.kind === 'dot') {
            consume(); // consume '.'
            const t = peek();
            if (!t || t.kind !== 'ident')
                break;
            const name = t.value;
            consume(); // consume method/property name
            // Check for method call syntax: .startsWith(...), .endsWith(...), .includes(...)
            if (peek()?.kind === 'lparen') {
                consume(); // '('
                const arg = parsePrimary();
                if (peek()?.kind === 'rparen')
                    consume(); // ')'
                if (typeof val === 'string') {
                    if (name === 'startsWith') {
                        val = val.startsWith(String(arg));
                        continue;
                    }
                    if (name === 'endsWith') {
                        val = val.endsWith(String(arg));
                        continue;
                    }
                    if (name === 'includes') {
                        val = val.includes(String(arg));
                        continue;
                    }
                }
                val = false;
            }
            else {
                // Property access
                if (val !== null && val !== undefined && typeof val === 'object') {
                    val = val[name];
                }
                else {
                    val = undefined;
                }
            }
        }
        return val;
    }
    function parsePrimary() {
        const t = peek();
        if (!t)
            return undefined;
        if (t.kind === 'lparen') {
            consume();
            const val = parseExpr();
            if (peek()?.kind === 'rparen')
                consume();
            return val;
        }
        if (t.kind === 'string') {
            consume();
            return t.value;
        }
        if (t.kind === 'number') {
            consume();
            return t.value;
        }
        if (t.kind === 'bool') {
            consume();
            return t.value;
        }
        if (t.kind === 'null') {
            consume();
            return null;
        }
        if (t.kind === 'undefined') {
            consume();
            return undefined;
        }
        if (t.kind === 'ident') {
            consume();
            // Only allow access to keys present in context
            return Object.prototype.hasOwnProperty.call(context, t.value)
                ? context[t.value]
                : undefined;
        }
        return undefined;
    }
    try {
        return !!(parseExpr());
    }
    catch (err) {
        logger.error('ChainEngine: condition evaluation error', {
            condition,
            error: summarizeError(err),
        });
        return false;
    }
}
export class ChainEngine {
    dispatcher;
    chains = [];
    states = new Map();
    constructor(dispatcher) {
        this.dispatcher = dispatcher;
    }
    /** Register a chain and initialize its state */
    register(chain) {
        this.chains.push(chain);
        this.states.set(chain.name, {
            currentStep: 0,
            captures: {},
            lastAdvance: 0,
        });
    }
    /** Called after each event to advance matching chains */
    async evaluate(event) {
        let result = null;
        for (const chain of this.chains) {
            const state = this.states.get(chain.name);
            if (!state)
                continue;
            const step = chain.steps[state.currentStep];
            if (!step)
                continue;
            // Check within timeout: if exceeded since last advance, reset
            if (step.within && state.lastAdvance > 0) {
                const withinMs = parseDuration(step.within);
                if (withinMs > 0 && Date.now() - state.lastAdvance > withinMs) {
                    this._resetState(chain.name);
                    // Re-check from step 0 with same event
                    const freshState = this.states.get(chain.name);
                    const freshStep = chain.steps[freshState.currentStep];
                    if (!freshStep || !matchesEventPath(freshStep.match, event.path))
                        continue;
                }
            }
            if (!matchesEventPath(step.match, event.path))
                continue;
            // Evaluate condition if present
            if (step.condition) {
                if (!safeEvaluate(step.condition, event.payload))
                    continue;
            }
            // Handle debounce for optional steps
            if (step.debounce && step.optional) {
                const debounceMs = parseDuration(step.debounce);
                if (state.debounceTimer !== undefined) {
                    clearTimeout(state.debounceTimer);
                }
                state.pendingDebounce = event;
                state.debounceTimer = setTimeout(async () => {
                    try {
                        state.debounceTimer = undefined;
                        const pendingEvent = state.pendingDebounce;
                        if (!pendingEvent)
                            return;
                        state.pendingDebounce = undefined;
                        // Advance after debounce
                        this._advanceState(chain, state, pendingEvent, step);
                        // Check if chain is complete
                        if (state.currentStep >= chain.steps.length) {
                            try {
                                await this.dispatcher.fire({
                                    ...pendingEvent,
                                    path: chain.action.match,
                                });
                            }
                            catch (err) {
                                logger.error('ChainEngine: chain action error', {
                                    chain: chain.name,
                                    error: summarizeError(err),
                                });
                            }
                            this._resetState(chain.name);
                        }
                    }
                    catch (err) {
                        logger.error('ChainEngine: debounce callback error', {
                            chain: chain.name,
                            error: summarizeError(err),
                        });
                    }
                }, debounceMs);
                state.debounceTimer.unref?.();
                continue;
            }
            // Advance step
            this._advanceState(chain, state, event, step);
            // Fire chain action if all steps complete
            if (state.currentStep >= chain.steps.length) {
                const actionEvent = {
                    ...event,
                    path: chain.action.match,
                };
                try {
                    result = await this.dispatcher.fire(actionEvent);
                }
                catch (err) {
                    logger.error('ChainEngine: chain action error', {
                        chain: chain.name,
                        error: summarizeError(err),
                    });
                    result = { ok: false, error: summarizeError(err) };
                }
                this._resetState(chain.name);
            }
        }
        return result;
    }
    _advanceState(chain, state, event, step) {
        // Capture variables if specified
        if (step.capture) {
            for (const [varName, payloadKey] of Object.entries(step.capture)) {
                const val = event.payload[payloadKey];
                if (val !== undefined) {
                    state.captures[varName] = String(val);
                }
            }
        }
        state.lastAdvance = Date.now();
        state.currentStep++;
    }
    _resetState(chainName) {
        const state = this.states.get(chainName);
        if (state?.debounceTimer !== undefined) {
            clearTimeout(state.debounceTimer);
        }
        this.states.set(chainName, {
            currentStep: 0,
            captures: {},
            lastAdvance: 0,
        });
    }
    /** Reset a specific chain */
    reset(chainName) {
        this._resetState(chainName);
    }
    /** Reset all chains */
    resetAll() {
        for (const chain of this.chains) {
            this._resetState(chain.name);
        }
    }
    /** Get chain states for debugging */
    getStates() {
        const result = new Map();
        for (const [name, state] of this.states.entries()) {
            result.set(name, {
                currentStep: state.currentStep,
                captures: { ...state.captures },
                lastAdvance: state.lastAdvance,
            });
        }
        return result;
    }
}
