import { interpolate, hasInterpolation, InterpolationError } from "./interpolate.js";
import { validateDeclarations, validateOverrides } from "./validate.js";
export class VariableResolutionError extends Error {
    issues;
    constructor(issues, msg = "variable resolution failed") {
        super(`${msg}: ${issues.map((i) => i.message).join("; ")}`);
        this.issues = issues;
        this.name = "VariableResolutionError";
    }
}
/**
 * Build the values map: declared defaults overlaid by overrides.
 */
function mergeValues(declarations, overrides = {}) {
    const merged = {};
    if (declarations) {
        for (const d of declarations) {
            merged[d.id] = d.default;
        }
    }
    for (const [k, v] of Object.entries(overrides)) {
        merged[k] = v;
    }
    return merged;
}
/**
 * Recursively walk an object/array tree, returning a deep clone in which
 * every string leaf has been passed through `interpolate`. Non-string
 * primitives, null, undefined, Date, etc. pass through untouched. Arrays
 * and plain objects are recursed.
 *
 * Interpolation NEVER throws here — strict mode is enforced at the
 * resolve() boundary by collecting interpolation issues into the issue
 * list before deciding whether to throw VariableResolutionError. This
 * guarantees the caller sees ALL problems at once (declaration +
 * override + missing-token) rather than getting whichever happens to
 * fail first.
 */
function interpolateTree(node, vars, issues) {
    if (typeof node === "string") {
        if (!hasInterpolation(node))
            return node;
        try {
            return interpolate(node, vars, { strict: true });
        }
        catch (err) {
            if (err instanceof InterpolationError) {
                issues.push({
                    severity: "error",
                    ruleId: "override-unknown-key",
                    variableId: err.key,
                    message: `string field references unknown variable "${err.key}"`,
                });
                // Leave the token literal in the cloned tree (lenient fallback).
                return interpolate(node, vars, { strict: false });
            }
            throw err;
        }
    }
    if (Array.isArray(node)) {
        return node.map((item) => interpolateTree(item, vars, issues));
    }
    if (node !== null && typeof node === "object") {
        const out = {};
        for (const [k, v] of Object.entries(node)) {
            out[k] = interpolateTree(v, vars, issues);
        }
        return out;
    }
    return node;
}
export function resolve(composition, opts = {}) {
    const declarations = composition.variables;
    const overrides = opts.overrides ?? {};
    const strict = opts.strict === true;
    // Collect ALL issues across declaration / override / interpolation
    // before deciding whether to throw. This way the caller sees the
    // complete picture in one shot rather than chasing whichever check
    // fails first.
    const issues = [
        ...validateDeclarations(declarations),
        ...validateOverrides(declarations ?? [], overrides, { strict }),
    ];
    const resolvedValues = mergeValues(declarations, overrides);
    const interpolated = interpolateTree(composition, resolvedValues, issues);
    if (strict && issues.some((i) => i.severity === "error")) {
        throw new VariableResolutionError(issues);
    }
    return { composition: interpolated, resolvedValues, issues };
}
//# sourceMappingURL=resolve.js.map