/**
 * wfrun/parse.mjs — Load + validate a C17 declarative YAML workflow file.
 *
 * Pipeline:
 *   1. readFileSync(UTF-8) → yaml.parse → raw object
 *   2. WorkflowSchema.safeParse(raw) → structural validation
 *   3. extra semantic checks: duplicate node ids, dangling depends_on targets.
 *
 * Returns a tagged result:
 *   { ok: true, workflow }           — validated and normalized
 *   { ok: false, errors: [...] }     — array of { path: string, message: string }
 *
 * parse.mjs does NOT perform DAG cycle detection or interpolation validation
 * — those live in dag.mjs and run.mjs respectively.
 */
import { readFileSync } from 'node:fs';
import * as YAML from 'yaml';
import { WorkflowSchema } from './schema.mjs';
/**
 * Parse a YAML workflow file from disk.
 * @param {string} absPath absolute path to the YAML file
 * @returns {{ok: true, workflow: any} | {ok: false, errors: {path: string, message: string}[]}}
 */
export function parseWorkflowFile(absPath) {
    let raw;
    try {
        raw = readFileSync(absPath, 'utf-8');
    }
    catch (err) {
        return { ok: false, errors: [{ path: absPath, message: `cannot read file: ${err.message}` }] };
    }
    return parseWorkflowString(raw, absPath);
}
/**
 * Parse a YAML workflow from a string (exposed for tests).
 * @param {string} text raw YAML
 * @param {string} [source] optional label for error messages
 */
export function parseWorkflowString(text, source = '<inline>') {
    let parsed;
    try {
        parsed = YAML.parse(text);
    }
    catch (err) {
        return { ok: false, errors: [{ path: source, message: `YAML parse error: ${err.message}` }] };
    }
    if (parsed == null || typeof parsed !== 'object') {
        return {
            ok: false,
            errors: [{ path: source, message: 'workflow root must be a YAML mapping' }],
        };
    }
    const result = WorkflowSchema.safeParse(parsed);
    if (!result.success) {
        const errors = result.error.issues.map((issue) => ({
            path: issue.path.length === 0 ? '<root>' : issue.path.join('.'),
            message: issue.message,
        }));
        return { ok: false, errors };
    }
    const workflow = result.data;
    const errors = [];
    // Duplicate node ids
    const seen = new Map();
    for (let i = 0; i < workflow.nodes.length; i++) {
        const node = workflow.nodes[i];
        if (seen.has(node.id)) {
            errors.push({
                path: `nodes[${i}].id`,
                message: `duplicate node id "${node.id}" (first seen at nodes[${seen.get(node.id)}])`,
            });
        }
        else {
            seen.set(node.id, i);
        }
    }
    // Dangling depends_on references (also catch self-loop here for clarity)
    const nodeIds = new Set(workflow.nodes.map((n) => n.id));
    for (let i = 0; i < workflow.nodes.length; i++) {
        const node = workflow.nodes[i];
        for (let j = 0; j < (node.depends_on ?? []).length; j++) {
            const dep = node.depends_on[j];
            if (!nodeIds.has(dep)) {
                errors.push({
                    path: `nodes[${i}].depends_on[${j}]`,
                    message: `unknown node id "${dep}" referenced by node "${node.id}"`,
                });
            }
        }
    }
    // Duplicate input names
    const seenInputs = new Map();
    for (let i = 0; i < (workflow.inputs ?? []).length; i++) {
        const input = workflow.inputs[i];
        if (seenInputs.has(input.name)) {
            errors.push({
                path: `inputs[${i}].name`,
                message: `duplicate input name "${input.name}"`,
            });
        }
        else {
            seenInputs.set(input.name, i);
        }
    }
    if (errors.length > 0)
        return { ok: false, errors };
    return { ok: true, workflow };
}
//# sourceMappingURL=parse.mjs.map