/**
 * @module        pipelineConfig
 * @description   流水线 YAML 配置加载与校验
 *
 * @author        eddy
 * @organization  wykj
 * @ownership     wykj/eddy
 *
 * @created       2026-04-02
 * @updated       2026-04-03
 *
 * @role          config
 * @layer         core
 * @boundedContext pipeline
 */
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { basename, resolve } from 'node:path';
import { parse as parseYaml } from 'yaml';
// ─── Parsing ────────────────────────────────────────────────────
/**
 * Parse on_fail string into structured OnFail type.
 * Formats: "abort", "skip", "retry 3", "goto step_name"
 */
export function parseOnFail(raw) {
    if (!raw || raw === 'abort')
        return 'abort';
    if (raw === 'skip')
        return 'skip';
    const retryMatch = raw.match(/^retry\s+(\d+)$/);
    if (retryMatch)
        return { retry: parseInt(retryMatch[1], 10) };
    const gotoMatch = raw.match(/^goto\s+(\S+)$/);
    if (gotoMatch)
        return { goto: gotoMatch[1] };
    throw new Error(`Invalid on_fail value: "${raw}". Expected: abort, skip, retry N, or goto <step_name>`);
}
/**
 * Parse timeout string (e.g., "30m", "2h", "300s") into milliseconds.
 */
export function parseTimeout(raw) {
    if (!raw)
        return undefined;
    const match = raw.match(/^(\d+)(s|m|h)$/);
    if (!match)
        throw new Error(`Invalid timeout: "${raw}". Expected format: 30s, 5m, 2h`);
    const value = parseInt(match[1], 10);
    const unit = match[2];
    if (unit === 's')
        return value * 1000;
    if (unit === 'm')
        return value * 60 * 1000;
    return value * 3600 * 1000;
}
// ─── Validation ─────────────────────────────────────────────────
function validateStepsPipeline(config, filePath) {
    if (!config.steps || !Array.isArray(config.steps) || config.steps.length === 0) {
        throw new Error(`Pipeline "${config.name}" (${filePath}): mode "steps" requires at least one step`);
    }
    const names = new Set();
    for (const step of config.steps) {
        if (!step.name) {
            throw new Error(`Pipeline "${config.name}" (${filePath}): every step must have a "name" field`);
        }
        if (names.has(step.name)) {
            throw new Error(`Pipeline "${config.name}" (${filePath}): duplicate step name "${step.name}"`);
        }
        names.add(step.name);
        if (!step.agent && !step.run) {
            throw new Error(`Pipeline "${config.name}" (${filePath}): step "${step.name}" must have either "agent" or "run"`);
        }
        if (step.agent && step.run) {
            throw new Error(`Pipeline "${config.name}" (${filePath}): step "${step.name}" cannot have both "agent" and "run"`);
        }
        if (step.agent && !step.prompt && !step.prompt_file) {
            throw new Error(`Pipeline "${config.name}" (${filePath}): agent step "${step.name}" must have "prompt" or "prompt_file"`);
        }
        // Validate on_fail references
        if (step.on_fail) {
            const parsed = parseOnFail(step.on_fail);
            if (typeof parsed === 'object' && 'goto' in parsed) {
                // Can't validate goto target until all steps are parsed — defer
            }
        }
    }
    // Second pass: validate goto targets
    for (const step of config.steps) {
        if (step.on_fail) {
            const parsed = parseOnFail(step.on_fail);
            if (typeof parsed === 'object' && 'goto' in parsed && !names.has(parsed.goto)) {
                throw new Error(`Pipeline "${config.name}" (${filePath}): step "${step.name}" has on_fail goto "${parsed.goto}" but no step with that name exists`);
            }
        }
    }
}
// ─── Loading ────────────────────────────────────────────────────
/**
 * Get the pipelines directory path for a project.
 * Pipeline YAML files live in ~/.coral/projects/<name>/pipelines/.
 * Falls back to <cwd>/.sps/pipelines/ for legacy compatibility.
 */
export function getPipelinesDir(projectOrCwd) {
    if (projectOrCwd) {
        // Try ~/.coral/projects/<name>/pipelines/ first
        const coralDir = resolve(homedir(), '.coral', 'projects', projectOrCwd, 'pipelines');
        if (existsSync(coralDir))
            return coralDir;
        // Legacy fallback: <cwd>/.sps/pipelines/
        const legacyDir = resolve(projectOrCwd, '.sps', 'pipelines');
        if (existsSync(legacyDir))
            return legacyDir;
        // Default to new location (will be created)
        return coralDir;
    }
    return resolve(homedir(), '.coral', 'projects');
}
/**
 * List all available pipelines across all projects (or for a specific project).
 */
export function listPipelines(projectName) {
    const pipelines = [];
    const projectsDir = resolve(homedir(), '.coral', 'projects');
    // Collect pipeline dirs to scan
    const pipelineDirs = [];
    if (projectName) {
        const dir = getPipelinesDir(projectName);
        if (existsSync(dir))
            pipelineDirs.push(dir);
    }
    else if (existsSync(projectsDir)) {
        // Scan all projects
        for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const dir = resolve(projectsDir, entry.name, 'pipelines');
            if (existsSync(dir))
                pipelineDirs.push(dir);
        }
    }
    for (const dir of pipelineDirs) {
        const files = readdirSync(dir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
        for (const file of files) {
            const filePath = resolve(dir, file);
            try {
                const raw = readFileSync(filePath, 'utf-8');
                const parsed = parseYaml(raw);
                pipelines.push({
                    name: parsed.name || basename(file, file.endsWith('.yaml') ? '.yaml' : '.yml'),
                    description: parsed.description || '',
                    mode: parsed.mode || 'steps',
                    filePath,
                });
            }
            catch { /* skip */ }
        }
    }
    return pipelines;
}
/**
 * Load and validate a pipeline by name.
 * Searches .sps/pipelines/ for a matching YAML file.
 */
export function loadPipelineConfig(name, cwd) {
    const dir = getPipelinesDir(cwd);
    const candidates = [
        resolve(dir, `${name}.yaml`),
        resolve(dir, `${name}.yml`),
    ];
    let filePath = null;
    for (const c of candidates) {
        if (existsSync(c)) {
            filePath = c;
            break;
        }
    }
    if (!filePath) {
        const available = listPipelines(cwd);
        const names = available.map(p => p.name).join(', ');
        throw new Error(`Pipeline "${name}" not found in ${dir}\n` +
            (names ? `Available pipelines: ${names}` : 'No pipelines found. Create ~/.coral/projects/<project>/pipelines/<name>.yaml'));
    }
    const raw = readFileSync(filePath, 'utf-8');
    let parsed;
    try {
        parsed = parseYaml(raw);
    }
    catch (err) {
        throw new Error(`Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
    }
    if (!parsed || typeof parsed !== 'object') {
        throw new Error(`Pipeline file ${filePath} is empty or invalid`);
    }
    // Set defaults
    const config = {
        name: parsed.name || name,
        description: parsed.description,
        mode: parsed.mode || 'steps',
        ...parsed,
    };
    // Validate based on mode
    const mode = config.mode;
    if (mode === 'steps') {
        validateStepsPipeline(config, filePath);
    }
    else if (mode === 'project') {
        // Phase C validation — for now just accept
    }
    else {
        throw new Error(`Pipeline "${config.name}" (${filePath}): unknown mode "${mode}". Expected: steps or project`);
    }
    return config;
}
//# sourceMappingURL=pipelineConfig.js.map