/**
 * ============================================================
 *  src/workflows/loader.ts —— workflow yaml 双源加载 + 最小校验
 * ------------------------------------------------------------
 *  - 扫描 cwd/workflows/ 与 packageRoot/workflows/（cwd 优先，复用
 *    getResourceSearchPaths 契约）
 *  - 一个坏 yaml 不应拖累其它工作流（catch 单文件错误，继续扫描）
 *  - 目录不存在 / 为空 → 静默返回 []（默认 T-A-O-R 不受影响）
 *
 *  P0 不引 AJV；下面手写 ≤80 行的最小校验器，只挡死硬性错误：
 *    - name / description / steps 必填且类型正确
 *    - steps 非空
 *    - 每个 step 必须有且只有一个动作字段（tool / skill / llm / type）
 *
 *  P1 切 AJV 做完整 schema。
 * ============================================================
 */
import { readFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { parse as parseYamlLib } from 'yaml';
import { getResourceSearchPaths } from '../../../src/plugin-sdk.js';
export class WorkflowLoadError extends Error {
    file;
    constructor(message, file) {
        super(message);
        this.file = file;
        this.name = 'WorkflowLoadError';
    }
}
/**
 * 扫描双源目录，返回所有 *.yaml / *.yml 解析后的 workflow。
 * 永远不抛；坏文件 console.warn 并跳过。
 */
export async function listWorkflows() {
    const out = [];
    const seen = new Set();
    const roots = getResourceSearchPaths('workflows', import.meta.url);
    for (const root of roots) {
        let entries;
        try {
            entries = await readdir(root);
        }
        catch {
            continue;
        }
        for (const f of entries) {
            if (!f.endsWith('.yaml') && !f.endsWith('.yml'))
                continue;
            const full = join(root, f);
            try {
                const def = await parseWorkflowFile(full);
                if (!seen.has(def.name)) {
                    seen.add(def.name);
                    out.push(def);
                }
            }
            catch (e) {
                // eslint-disable-next-line no-console
                console.warn(`[workflows] 忽略损坏的 yaml ${full}: ${e.message}`);
            }
        }
    }
    return out;
}
/** 按 name 查找。找不到返回 null —— 调用方决定如何提示用户。 */
export async function findWorkflowByName(name) {
    const all = await listWorkflows();
    return all.find((w) => w.name === name) ?? null;
}
/** 读单个 yaml 文件 → 解析 → 校验。失败抛 WorkflowLoadError。 */
export async function parseWorkflowFile(file) {
    let raw;
    try {
        raw = await readFile(file, 'utf8');
    }
    catch (e) {
        throw new WorkflowLoadError(`无法读取 ${file}: ${e.message}`, file);
    }
    let data;
    try {
        data = parseYamlLib(raw);
    }
    catch (e) {
        throw new WorkflowLoadError(`yaml 语法错误: ${e.message}`, file);
    }
    if (!data || typeof data !== 'object') {
        throw new WorkflowLoadError('yaml 根节点必须是对象', file);
    }
    validate(data, file);
    const def = data;
    def.__source = file;
    return def;
}
// ---------------- 手写最小校验 ----------------
function validate(obj, file) {
    if (typeof obj.name !== 'string' || obj.name.length === 0) {
        throw new WorkflowLoadError("缺少必填字段 'name'（string）", file);
    }
    if (typeof obj.description !== 'string' || obj.description.length === 0) {
        throw new WorkflowLoadError("缺少必填字段 'description'（string）", file);
    }
    if (!Array.isArray(obj.steps) || obj.steps.length === 0) {
        throw new WorkflowLoadError("缺少必填字段 'steps' 或为空数组", file);
    }
    validateSteps(obj.steps, file, '');
    if (obj.inputs !== undefined) {
        if (!Array.isArray(obj.inputs)) {
            throw new WorkflowLoadError("'inputs' 必须是数组", file);
        }
        for (const [i, item] of obj.inputs.entries()) {
            if (!item || typeof item !== 'object') {
                throw new WorkflowLoadError(`inputs[${i}] 必须是对象`, file);
            }
            const r = item;
            if (typeof r.name !== 'string' || r.name.length === 0) {
                throw new WorkflowLoadError(`inputs[${i}].name 必填`, file);
            }
            if (r.type !== undefined && r.type !== 'string' && r.type !== 'number' && r.type !== 'enum') {
                throw new WorkflowLoadError(`inputs[${i}].type 必须是 string/number/enum`, file);
            }
            if (r.type === 'enum' && (!Array.isArray(r.values) || r.values.length === 0)) {
                throw new WorkflowLoadError(`inputs[${i}].values 必填（type=enum 时）`, file);
            }
        }
    }
}
// ADR-05: 控制流 type 集合——这些节点的"动作"由 type 自身定义，
// 因此不能附带 tool/skill/llm 也不能配 output_schema / context_files / allowed_tools。
// 导出供「editor ↔ 后端」schema 一致性测试比对（test/editorSchemaSync.test.ts）。
// editor 因 D2 红线禁止 import 后端、手抄了同一份；后端在此演进时该测试会红，提示同步 editor。
export const CONTROL_FLOW_TYPES = new Set([
    'assert',
    'branch',
    'loop',
    'pause',
    'parallel',
    'vote',
]);
export const VALID_STEP_TYPES = new Set([
    'assert',
    'branch',
    'loop',
    'pause',
    'parallel',
    'vote',
]);
// 动作字段里「自带 kind 含义」的那几个（不含 type —— type 的 kind 由其值决定）。
// 单一来源：validateSteps 的 actionFields + test/editorSchemaSync.test.ts 都从这里取，
// 新增动作 kind 只改这一处（并同步 editor 手抄镜像 + 该测试快照）。
export const ACTION_KINDS = ['tool', 'skill', 'agent', 'llm'];
// onError 合法值（单一来源：onError 校验 + editor schema 同步测试）。
export const ON_ERROR_VALUES = ['halt', 'continue', 'recover'];
function validateSteps(steps, file, pathPrefix) {
    const seenIds = new Set();
    for (const [i, item] of steps.entries()) {
        const p = pathPrefix ? `${pathPrefix}.steps[${i}]` : `steps[${i}]`;
        if (!item || typeof item !== 'object') {
            throw new WorkflowLoadError(`${p} 必须是对象`, file);
        }
        const s = item;
        if (typeof s.id !== 'string' || s.id.length === 0) {
            throw new WorkflowLoadError(`${p}.id 必填`, file);
        }
        // id 唯一仅在同级 steps 数组内强制
        if (seenIds.has(s.id)) {
            throw new WorkflowLoadError(`${p}.id "${s.id}" 在当前 steps 列表中重复`, file);
        }
        seenIds.add(s.id);
        // type 枚举校验（提前于动作互斥，给更准确的错误信息）
        if (s.type !== undefined && (typeof s.type !== 'string' || !VALID_STEP_TYPES.has(s.type))) {
            throw new WorkflowLoadError(`${p}.type "${String(s.type)}" 非法；合法值: ${[...VALID_STEP_TYPES].join(' / ')}`, file);
        }
        // 动作字段恰好一个（ACTION_KINDS 单一来源 + 控制流 type）
        const actionFields = [...ACTION_KINDS, 'type'];
        const present = actionFields.filter((k) => s[k] !== undefined);
        if (present.length === 0) {
            throw new WorkflowLoadError(`${p} 必须有以下字段之一: ${actionFields.join(' / ')}`, file);
        }
        if (present.length > 1) {
            throw new WorkflowLoadError(`${p} 同时设置了 ${present.join(', ')}；只允许其中一个`, file);
        }
        // onError 枚举校验：runner 只认 halt/continue/recover，typo 会被静默当 halt，
        // 故在加载期拦掉，给作者明确错误。
        if (s.onError !== undefined &&
            !ON_ERROR_VALUES.includes(s.onError)) {
            throw new WorkflowLoadError(`${p}.onError "${String(s.onError)}" 非法；合法值: ${ON_ERROR_VALUES.join(' / ')}`, file);
        }
        // recover_hint 必须是字符串（仅 onError:'recover' 时有意义，但不强制配对）
        if (s.recover_hint !== undefined && typeof s.recover_hint !== 'string') {
            throw new WorkflowLoadError(`${p}.recover_hint 必须是 string`, file);
        }
        // 控制流字段一致性
        if (s.type === 'branch' && s.condition === undefined) {
            throw new WorkflowLoadError(`${p} type=branch 必须带 'condition'`, file);
        }
        if (s.type === 'loop') {
            if (s.over === undefined)
                throw new WorkflowLoadError(`${p} type=loop 必须带 'over'`, file);
            if (typeof s.as !== 'string' || s.as.length === 0) {
                throw new WorkflowLoadError(`${p} type=loop 必须带 'as'`, file);
            }
            if (!Array.isArray(s.do)) {
                throw new WorkflowLoadError(`${p} type=loop 必须带 'do' 数组`, file);
            }
            validateSteps(s.do, file, `${p}.do`);
        }
        if (s.type === 'assert' && s.condition === undefined) {
            throw new WorkflowLoadError(`${p} type=assert 必须带 'condition'`, file);
        }
        if (s.type === 'branch') {
            if (s.then !== undefined && !Array.isArray(s.then)) {
                throw new WorkflowLoadError(`${p}.then 必须是数组`, file);
            }
            if (s.else !== undefined && !Array.isArray(s.else)) {
                throw new WorkflowLoadError(`${p}.else 必须是数组`, file);
            }
            if (Array.isArray(s.then))
                validateSteps(s.then, file, `${p}.then`);
            if (Array.isArray(s.else))
                validateSteps(s.else, file, `${p}.else`);
        }
        // ---------------- ADR-05 新增校验 ----------------
        // parallel：必须有非空 fork[]，递归校验子步骤
        if (s.type === 'parallel') {
            if (!Array.isArray(s.fork) || s.fork.length === 0) {
                throw new WorkflowLoadError(`${p} type=parallel 必须有 fork: [...]（至少 1 个子步骤）`, file);
            }
            validateSteps(s.fork, file, `${p}.fork`);
        }
        else if (s.fork !== undefined) {
            throw new WorkflowLoadError(`${p}.fork 只能用在 type=parallel 节点上`, file);
        }
        // vote：voters_count (2~10) / rules / input 必填；threshold 可选但越界要拦
        if (s.type === 'vote') {
            const vc = s.voters_count;
            const rules = s.rules;
            const input = s.input;
            const missing = typeof vc !== 'number' ||
                !Number.isInteger(vc) ||
                vc < 2 ||
                vc > 10 ||
                typeof rules !== 'string' ||
                rules.length === 0 ||
                typeof input !== 'string' ||
                input.length === 0;
            if (missing) {
                throw new WorkflowLoadError(`${p} type=vote 要求 voters_count (2~10), rules, input 三个字段`, file);
            }
            if (s.threshold !== undefined) {
                const t = s.threshold;
                if (typeof t !== 'number' ||
                    !Number.isInteger(t) ||
                    t < 1 ||
                    t > vc) {
                    throw new WorkflowLoadError(`${p} threshold=${String(t)} 超出范围；必须是 1 <= threshold <= voters_count(${vc})`, file);
                }
            }
        }
        else {
            // 非 vote 节点不应携带 vote-only 字段
            for (const k of ['voters_count', 'rules', 'threshold']) {
                if (s[k] !== undefined) {
                    throw new WorkflowLoadError(`${p}.${k} 只能用在 type=vote 节点上`, file);
                }
            }
        }
        // output_schema：必须是 object；不能用在控制流节点上
        if (s.output_schema !== undefined) {
            if (typeof s.output_schema !== 'object' ||
                s.output_schema === null ||
                Array.isArray(s.output_schema)) {
                throw new WorkflowLoadError(`${p}.output_schema 必须是对象`, file);
            }
            if (typeof s.type === 'string' && CONTROL_FLOW_TYPES.has(s.type)) {
                throw new WorkflowLoadError(`${p}.output_schema 不能用在 type=${s.type} 控制流节点上`, file);
            }
            // P3：output_schema 在 skill 节点上是静默 no-op（execSkillStep 无视它）。
            // 与其悄悄丢弃，不如 loader 直接拒绝——仅 llm 节点支持 structured output。
            if (s.skill !== undefined) {
                throw new WorkflowLoadError(`${p}.output_schema 暂不支持 skill 节点（仅 llm 节点生效）`, file);
            }
        }
        // context_files：[{ path, hint? }]；不能用在控制流节点上
        if (s.context_files !== undefined) {
            if (!Array.isArray(s.context_files)) {
                throw new WorkflowLoadError(`${p}.context_files 必须是数组`, file);
            }
            for (const [j, cf] of s.context_files.entries()) {
                if (!cf || typeof cf !== 'object' || Array.isArray(cf)) {
                    throw new WorkflowLoadError(`${p}.context_files[${j}] 必须是 { path: string, hint?: string } 对象`, file);
                }
                const r = cf;
                if (typeof r.path !== 'string' || r.path.length === 0) {
                    throw new WorkflowLoadError(`${p}.context_files[${j}].path 必填（非空 string）`, file);
                }
                if (r.hint !== undefined && typeof r.hint !== 'string') {
                    throw new WorkflowLoadError(`${p}.context_files[${j}].hint 必须是 string`, file);
                }
            }
            if (typeof s.type === 'string' && CONTROL_FLOW_TYPES.has(s.type)) {
                throw new WorkflowLoadError(`${p}.context_files 不能用在 type=${s.type} 控制流节点上`, file);
            }
            // P3：context_files 同样是 skill 节点的静默 no-op（execSkillStep 无视它），
            // 仅 llm 节点经 miniReAct 生效——loader 直接拒绝，避免悄悄丢弃。
            if (s.skill !== undefined) {
                throw new WorkflowLoadError(`${p}.context_files 暂不支持 skill 节点（仅 llm 节点生效）`, file);
            }
        }
        // allowed_tools：string[]；不能用在控制流节点上
        if (s.allowed_tools !== undefined) {
            if (!Array.isArray(s.allowed_tools) ||
                !s.allowed_tools.every((x) => typeof x === 'string' && x.length > 0)) {
                throw new WorkflowLoadError(`${p}.allowed_tools 必须是非空 string 数组`, file);
            }
            if (typeof s.type === 'string' && CONTROL_FLOW_TYPES.has(s.type)) {
                throw new WorkflowLoadError(`${p}.allowed_tools 不能用在 type=${s.type} 控制流节点上`, file);
            }
            // P3：allowed_tools 与 context_files 配对，skill 节点上同样无效——一并拒绝。
            if (s.skill !== undefined) {
                throw new WorkflowLoadError(`${p}.allowed_tools 暂不支持 skill 节点（仅 llm 节点生效）`, file);
            }
        }
    }
}
