/**
 * ============================================================
 *  plugins/workflow-runner/src/expressions.ts —— workflow 表达式引擎（mini）
 * ------------------------------------------------------------
 *  WHY 不直接 eval / Function？
 *    Workflow 来源不可信（用户编辑器生成 / 别人贡献的 yaml），允许任意
 *    JS 执行 = 任意远程代码执行。所以我们手写一个**只支持必要语法**的
 *    递归下降解析器：常量、变量引用、点路径、字符串/数字字面量、数组、
 *    比较、布尔运算、白名单内置函数。
 *
 *  支持的语法：
 *    expr    := or
 *    or      := and ( '||' and )*
 *    and     := not ( '&&' not )*
 *    not     := '!' not | comparison
 *    comparison := additive ( ('==' | '!=' | '>=' | '<=' | '>' | '<') additive )?
 *    additive := primary ( '+' primary )*        // number 相加 / 否则 stringify 后拼接
 *    primary := number | string | array | call | path | interp | '(' expr ')'
 *    array   := '[' expr (',' expr)* ']'
 *    call    := IDENT '(' expr (',' expr)* ')'   // 白名单
 *    path    := IDENT ('.' IDENT)*               // 变量 + 点路径
 *    interp  := '${' EXPR '}'                     // 取 EXPR 的"值"（任意类型，不字符串化）
 *    string  := "…" | '…'                         // 内嵌 ${EXPR} 走 template-literal 插值
 *
 *  白名单函数：fileExists / length / lower / upper
 *
 *  ★ P2 根治：表达式上下文（when/condition/over/assert/vote.input）直接 evalExpr，
 *    不再"先 interpolate 再 eval"（旧双求值的 footgun 源头）。四种写法都 OK：
 *      裸 ${x} == 'y'   → ${x} 取值后比较（修掉旧 footgun：旧实现把 ${x} 替成裸字面量被当变量名）
 *      "a/${x}/b"       → 字符串字面量内插值拼路径（向后兼容旧 fileExists("…${x}…") 写法）
 *      "a/" + x + "/b"  → + 拼接（新增）
 *      x == 'y'         → 裸变量名（向后兼容）
 *    安全性也更好：变量的字符串内容不再被当作表达式二次执行（旧实现若插值结果落在引号外会被 eval）。
 *
 *  插值函数 interpolate(${EXPR}) 仍用于**模板上下文**（prompt / args / file_path）——
 *  把 EXPR 求值后 toString 替换；对象/数组用 JSON.stringify。interpolateDeep 递归处理对象/数组/字符串。
 * ============================================================
 */
import { existsSync } from 'node:fs';
import { join, isAbsolute } from 'node:path';
import { getWorkingDir } from '../../../src/plugin-sdk.js';
const PUNCT2 = new Set(['==', '!=', '>=', '<=', '&&', '||']);
const PUNCT1 = new Set(['(', ')', '[', ']', ',', '.', '!', '>', '<', '+']);
function tokenize(src) {
    const out = [];
    let i = 0;
    while (i < src.length) {
        const c = src[i];
        if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
            i++;
            continue;
        }
        // 独立 ${...} 取值 token。注意：字符串字面量内部的 ${} 不会到这里——
        // 会被下面的字符串分支整体读进 str token，由 parsePrimary 在 eval 时插值。
        if (c === '$' && src[i + 1] === '{') {
            let depth = 1;
            let j = i + 2;
            while (j < src.length && depth > 0) {
                if (src[j] === '{')
                    depth++;
                else if (src[j] === '}')
                    depth--;
                if (depth > 0)
                    j++;
            }
            if (depth !== 0)
                throw new Error(`未闭合的 \${ 在表达式 "${src}"`);
            out.push({ kind: 'interp', value: src.slice(i + 2, j) });
            i = j + 1;
            continue;
        }
        // 字符串
        if (c === '"' || c === "'") {
            const quote = c;
            let s = '';
            i++;
            while (i < src.length && src[i] !== quote) {
                if (src[i] === '\\' && i + 1 < src.length) {
                    const nx = src[i + 1];
                    s += nx === 'n' ? '\n' : nx === 't' ? '\t' : nx;
                    i += 2;
                }
                else {
                    s += src[i++];
                }
            }
            if (src[i] !== quote)
                throw new Error(`未闭合的字符串：${src}`);
            i++;
            out.push({ kind: 'str', value: s });
            continue;
        }
        // 数字
        if ((c >= '0' && c <= '9') || (c === '-' && src[i + 1] >= '0' && src[i + 1] <= '9')) {
            let j = i;
            if (src[j] === '-')
                j++;
            while (j < src.length && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.'))
                j++;
            out.push({ kind: 'num', value: parseFloat(src.slice(i, j)) });
            i = j;
            continue;
        }
        // 双字符标点
        const two = src.slice(i, i + 2);
        if (PUNCT2.has(two)) {
            out.push({ kind: 'punct', value: two });
            i += 2;
            continue;
        }
        // 单字符标点
        if (PUNCT1.has(c)) {
            out.push({ kind: 'punct', value: c });
            i++;
            continue;
        }
        // 标识符
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_') {
            let j = i;
            while (j < src.length &&
                ((src[j] >= 'a' && src[j] <= 'z') ||
                    (src[j] >= 'A' && src[j] <= 'Z') ||
                    (src[j] >= '0' && src[j] <= '9') ||
                    src[j] === '_')) {
                j++;
            }
            out.push({ kind: 'ident', value: src.slice(i, j) });
            i = j;
            continue;
        }
        throw new Error(`非法字符 '${c}' 在表达式 "${src}"`);
    }
    return out;
}
function peek(c) {
    return c.pos < c.toks.length ? c.toks[c.pos] : null;
}
function eat(c) {
    return c.toks[c.pos++];
}
function isPunct(c, v) {
    const t = peek(c);
    return !!t && t.kind === 'punct' && t.value === v;
}
const BUILTINS = {
    fileExists(args) {
        const p = String(args[0] ?? '');
        if (!p)
            return false;
        const abs = isAbsolute(p) ? p : join(getWorkingDir(), p);
        return existsSync(abs);
    },
    length(args) {
        const v = args[0];
        if (typeof v === 'string' || Array.isArray(v))
            return v.length;
        if (v && typeof v === 'object')
            return Object.keys(v).length;
        return 0;
    },
    lower(args) {
        return String(args[0] ?? '').toLowerCase();
    },
    upper(args) {
        return String(args[0] ?? '').toUpperCase();
    },
};
function parseOr(c, vars) {
    let left = parseAnd(c, vars);
    while (isPunct(c, '||')) {
        eat(c);
        const right = parseAnd(c, vars);
        left = truthy(left) || truthy(right);
    }
    return left;
}
function parseAnd(c, vars) {
    let left = parseNot(c, vars);
    while (isPunct(c, '&&')) {
        eat(c);
        const right = parseNot(c, vars);
        left = truthy(left) && truthy(right);
    }
    return left;
}
function parseNot(c, vars) {
    if (isPunct(c, '!')) {
        eat(c);
        return !truthy(parseNot(c, vars));
    }
    return parseComparison(c, vars);
}
function parseComparison(c, vars) {
    const left = parseAdditive(c, vars);
    const t = peek(c);
    if (t && t.kind === 'punct' && ['==', '!=', '>=', '<=', '>', '<'].includes(t.value)) {
        eat(c);
        const right = parseAdditive(c, vars);
        switch (t.value) {
            case '==':
                // eslint-disable-next-line eqeqeq
                return left == right;
            case '!=':
                // eslint-disable-next-line eqeqeq
                return left != right;
            case '>':
                return left > right;
            case '<':
                return left < right;
            case '>=':
                return left >= right;
            case '<=':
                return left <= right;
        }
    }
    return left;
}
// 加法/拼接层：number + number → 相加；否则两边 stringify 后字符串拼接。
// 表达式语言原本无拼接符，导致拼路径只能"先插值再 eval"（footgun 源头）；
// 有了 + 之后可写 fileExists("videos/" + inputs.book + "/x") 这种纯表达式。
function parseAdditive(c, vars) {
    let left = parsePrimary(c, vars);
    while (isPunct(c, '+')) {
        eat(c);
        const right = parsePrimary(c, vars);
        left = addValues(left, right);
    }
    return left;
}
function addValues(l, r) {
    if (typeof l === 'number' && typeof r === 'number')
        return l + r;
    return stringify(l) + stringify(r);
}
function parsePrimary(c, vars) {
    const t = peek(c);
    if (!t)
        throw new Error('表达式意外结束');
    if (t.kind === 'punct' && t.value === '(') {
        eat(c);
        const v = parseOr(c, vars);
        if (!isPunct(c, ')'))
            throw new Error("缺少 ')'");
        eat(c);
        return v;
    }
    if (t.kind === 'punct' && t.value === '[') {
        eat(c);
        const arr = [];
        if (!isPunct(c, ']')) {
            arr.push(parseOr(c, vars));
            while (isPunct(c, ',')) {
                eat(c);
                arr.push(parseOr(c, vars));
            }
        }
        if (!isPunct(c, ']'))
            throw new Error("缺少 ']'");
        eat(c);
        return arr;
    }
    if (t.kind === 'num') {
        eat(c);
        return t.value;
    }
    if (t.kind === 'str') {
        eat(c);
        // 字符串字面量支持内嵌 ${...} 插值（template-literal 语义）：
        // fileExists("videos/${inputs.book}/x") 不必预插值也能工作。无 ${} 时原样返回。
        return interpolate(t.value, vars);
    }
    if (t.kind === 'interp') {
        eat(c);
        // 独立 ${EXPR}：求值为 EXPR 的"值"（任意类型，不字符串化）。
        // 修掉"裸 ${x} == 'y' 被当成变量名 x"的 footgun。
        return evalExpr(t.value, vars);
    }
    if (t.kind === 'ident') {
        eat(c);
        // 函数调用？
        if (isPunct(c, '(')) {
            eat(c);
            const argv = [];
            if (!isPunct(c, ')')) {
                argv.push(parseOr(c, vars));
                while (isPunct(c, ',')) {
                    eat(c);
                    argv.push(parseOr(c, vars));
                }
            }
            if (!isPunct(c, ')'))
                throw new Error("缺少 ')'");
            eat(c);
            const fn = BUILTINS[t.value];
            if (!fn)
                throw new Error(`未知函数 "${t.value}"。白名单：${Object.keys(BUILTINS).join(', ')}`);
            return fn(argv);
        }
        // 字面常量
        if (t.value === 'true')
            return true;
        if (t.value === 'false')
            return false;
        if (t.value === 'null')
            return null;
        // 变量 + 点路径
        let cur = vars.get(t.value);
        while (isPunct(c, '.')) {
            eat(c);
            const next = eat(c);
            if (next.kind !== 'ident')
                throw new Error('点号后必须是标识符');
            // S1：禁止经点路径访问原型链（__proto__ / constructor / prototype），防原型污染 / 函数对象逃逸。
            if (next.value === '__proto__' ||
                next.value === 'constructor' ||
                next.value === 'prototype') {
                cur = undefined;
            }
            else {
                cur =
                    cur != null && typeof cur === 'object'
                        ? cur[next.value]
                        : undefined;
            }
        }
        return cur;
    }
    throw new Error(`无法解析 token: ${JSON.stringify(t)}`);
}
function truthy(v) {
    if (v === null || v === undefined)
        return false;
    if (typeof v === 'boolean')
        return v;
    if (typeof v === 'number')
        return v !== 0 && !Number.isNaN(v);
    if (typeof v === 'string')
        return v.length > 0;
    if (Array.isArray(v))
        return v.length > 0;
    if (typeof v === 'object')
        return Object.keys(v).length > 0;
    return true;
}
// ---------------- 对外 API ----------------
/** 计算表达式（裸的，不带 ${} 包裹）。失败抛错。 */
export function evalExpr(expr, vars) {
    const toks = tokenize(expr);
    if (toks.length === 0)
        return undefined;
    const cursor = { toks, pos: 0 };
    const v = parseOr(cursor, vars);
    if (cursor.pos !== toks.length) {
        throw new Error(`表达式多余 token: "${expr}"`);
    }
    return v;
}
/**
 * 字符串插值：把 "${EXPR}" 替换成 EXPR 求值结果。
 * 多个 ${} 串联也支持。括号配对识别 —— "${ ${nested} }" 会被当成一个外层
 * EXPR（内部 ${nested} 文本原样，但表达式里不允许 ${} 语法，所以会报错）。
 *
 * 转义：`\$` → 字面 $，跳过插值。
 */
export function interpolate(template, vars) {
    let out = '';
    let i = 0;
    while (i < template.length) {
        const c = template[i];
        if (c === '\\' && template[i + 1] === '$') {
            out += '$';
            i += 2;
            continue;
        }
        if (c === '$' && template[i + 1] === '{') {
            // 找匹配的 '}'
            let depth = 1;
            let j = i + 2;
            while (j < template.length && depth > 0) {
                if (template[j] === '{')
                    depth++;
                else if (template[j] === '}')
                    depth--;
                if (depth > 0)
                    j++;
            }
            if (depth !== 0) {
                // 不匹配；原样输出
                out += template.slice(i);
                return out;
            }
            const expr = template.slice(i + 2, j);
            const v = evalExpr(expr, vars);
            out += stringify(v);
            i = j + 1;
            continue;
        }
        out += c;
        i++;
    }
    return out;
}
function stringify(v) {
    if (v === null || v === undefined)
        return '';
    if (typeof v === 'string')
        return v;
    if (typeof v === 'number' || typeof v === 'boolean')
        return String(v);
    try {
        return JSON.stringify(v);
    }
    catch {
        return String(v);
    }
}
/** 递归对对象/数组/字符串做插值；其它原样返回。 */
export function interpolateDeep(value, vars) {
    if (typeof value === 'string') {
        return interpolate(value, vars);
    }
    if (Array.isArray(value)) {
        return value.map((v) => interpolateDeep(v, vars));
    }
    if (value && typeof value === 'object') {
        const out = {};
        for (const [k, v] of Object.entries(value)) {
            // 注意：key 也插值（loop 里 `hook_${idx}` 这种），但只对字符串 key 有意义
            const newKey = interpolate(k, vars);
            out[newKey] = interpolateDeep(v, vars);
        }
        return out;
    }
    return value;
}
