/**
 * wfrun/interpolate.mjs — String-substitution helpers for C17 workflows.
 *
 * Grammar (literal path lookup only — no expressions, filters, or defaulting):
 *   ${inputs.<name>}
 *   ${<nodeId>.outputs.<key>}
 *
 * Escaping: `\${…}` is left as-is (the backslash is stripped). A bare `$`
 * not followed by `{` is left alone.
 *
 * Modes:
 *   'run'  — every reference must resolve; missing paths throw InterpolationError.
 *   'plan' — unknown `<nodeId>.outputs.*` refs are left as literal `${…}`
 *            tokens so the plan can be emitted before agent nodes have run.
 *            `inputs.*` must still resolve.
 */
export class InterpolationError extends Error {
    constructor(message, path) {
        super(message);
        this.name = 'InterpolationError';
        this.path = path;
    }
}
const REF_RE = /(\\?)\$\{([^}]+)\}/g;
/**
 * Enumerate all `${…}` reference paths in a template string.
 * Escaped `\${…}` tokens are skipped.
 * @param {string} template
 * @returns {string[]}
 */
export function collectReferences(template) {
    if (typeof template !== 'string')
        return [];
    const refs = [];
    for (const m of template.matchAll(REF_RE)) {
        if (m[1] === '\\')
            continue;
        refs.push(m[2].trim());
    }
    return refs;
}
/**
 * Resolve a single dotted path against a context object.
 * @returns {{ok: true, value: string} | {ok: false}}
 */
function lookup(path, context) {
    const parts = path.split('.').map((p) => p.trim());
    let cur = context;
    for (const part of parts) {
        if (cur == null || typeof cur !== 'object')
            return { ok: false };
        if (!Object.hasOwn(cur, part))
            return { ok: false };
        cur = cur[part];
    }
    if (cur == null)
        return { ok: false };
    return { ok: true, value: String(cur) };
}
/**
 * Interpolate a single string template.
 * @param {string} template
 * @param {object} context { inputs: {...}, <nodeId>: { outputs: {...} }, ... }
 * @param {{mode?: 'run'|'plan'}} [opts]
 * @returns {string}
 */
export function interpolate(template, context, opts = {}) {
    const mode = opts.mode ?? 'run';
    if (typeof template !== 'string')
        return template;
    return template.replace(REF_RE, (match, esc, expr) => {
        if (esc === '\\')
            return match.slice(1); // strip the backslash, emit ${…}
        const path = expr.trim();
        const res = lookup(path, context);
        if (res.ok)
            return res.value;
        if (mode === 'plan') {
            // Only allow deferring node-output refs; inputs.* must resolve now.
            if (path.startsWith('inputs.')) {
                throw new InterpolationError(`unresolved input reference: \${${path}}`, path);
            }
            return match;
        }
        throw new InterpolationError(`unresolved reference: \${${path}}`, path);
    });
}
/**
 * Deep-interpolate every string leaf of a node body. Returns a new node —
 * input is not mutated. Recurses into plain objects and arrays only.
 * @param {object} node
 * @param {object} context
 * @param {{mode?: 'run'|'plan'}} [opts]
 */
export function interpolateNode(node, context, opts = {}) {
    return walk(node, context, opts);
}
function walk(value, context, opts) {
    if (typeof value === 'string')
        return interpolate(value, context, opts);
    if (Array.isArray(value))
        return value.map((v) => walk(v, context, opts));
    if (value != null && typeof value === 'object') {
        const out = {};
        for (const [k, v] of Object.entries(value)) {
            out[k] = walk(v, context, opts);
        }
        return out;
    }
    return value;
}
//# sourceMappingURL=interpolate.mjs.map