/**
 * @module        services/PipelineService
 * @description   Pipeline 控制 + yaml CRUD service
 *
 * @layer         services
 *
 * 职责：
 *   - start：spawn 后台 supervisor（唯一允许用 ProcessSpawner 的 service）
 *   - stop：注入 PipelineExecutor.stop 调 CLI 停止（Phase 3 会接到 Domain 内部实现）
 *   - reset：注入 PipelineExecutor.reset
 *   - status：读 supervisor.pid 判进程活
 *   - pipelines yaml CRUD：list / get / write / create / delete / switch active
 */
import { createHash } from 'node:crypto';
import { resolve } from 'node:path';
import YAML from 'yaml';
import { domainError } from '../shared/errors.js';
import { err, ok } from '../shared/result.js';
import { activePipelineFile, logsDir, PipelineFilenameRe, pipelineFile, pipelinesDir, projectDir, supervisorPidFile, } from '../shared/runtimePaths.js';
export class PipelineService {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    /** 启动 pipeline —— 已在跑直接返当前 pid。 */
    async start(project) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!this.deps.fs.exists(projectDir(project))) {
            return err(projectNotFound(project));
        }
        const existing = this.readSupervisorPid(project);
        if (existing) {
            return ok({ status: 'running', pid: existing });
        }
        if (!this.deps.fs.exists(logsDir(project))) {
            this.deps.fs.mkdir(logsDir(project), { recursive: true });
        }
        // v0.50.8：启动前先回收遗孤卡片 —— 上一轮被 stop/crash 的卡，slot 还 active 但
        // tick 进程没了，MonitorEngine 会把它打 STALE-RUNTIME，下一轮又被 SKIP，最后
        // 链式升级到 ACK-TIMEOUT / NEEDS-FIX，卡在 Inprogress 永远跑不起来。
        // 这里一次性清 label + 移回 Todo，让下一个 tick 能重新 prepare 派发。
        // executor 缺失时跳过（单元测试场景），线上容器总会注入。
        if (this.deps.executor) {
            try {
                await this.deps.executor.recoverOrphans(project);
            }
            catch {
                /* 回收失败不阻塞 start —— 坏情况也就退化到老行为 */
            }
        }
        const date = this.deps.clock.nowIso().slice(0, 10);
        const logPath = resolve(logsDir(project), `console-tick-${date}.log`);
        try {
            const child = this.deps.spawner.spawnSupervisor({
                args: ['tick', project],
                logPath,
            });
            const pid = child.pid ?? null;
            // v0.50.7：写 supervisor.pid 文件 —— ProjectService.isPipelineRunning 靠它判活。
            // 历史上 sps tick 进程不自己写这个文件（v0.44 Console 引入时的设计缺陷，
            // 从来没有生效过 pipeline running 检测）。Service 层一口气把 spawn + pid
            // 登记都做了，对外原子。
            if (pid !== null && pid > 0) {
                try {
                    this.deps.fs.writeFileAtomic(supervisorPidFile(project), `${pid}\n`);
                }
                catch {
                    /* pid 文件写失败不阻塞启动，下一次 poll 会补偿 */
                }
            }
            this.deps.events.emit({
                type: 'pipeline.started',
                project,
                pid: pid ?? 0,
                ts: this.deps.clock.now(),
            });
            return ok({ status: 'running', pid });
        }
        catch (cause) {
            return err(domainError('external', 'PIPELINE_SPAWN_FAIL', 'Failed to start pipeline', {
                cause,
                details: { message: cause instanceof Error ? cause.message : String(cause) },
            }));
        }
    }
    async stop(project) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!this.deps.executor) {
            return err(domainError('internal', 'EXECUTOR_MISSING', 'PipelineExecutor not injected'));
        }
        try {
            await this.deps.executor.stop(project);
        }
        catch (cause) {
            return err(domainError('external', 'PIPELINE_STOP_FAIL', 'Failed to stop pipeline', {
                cause,
                details: { message: cause instanceof Error ? cause.message : String(cause) },
            }));
        }
        // 清 supervisor.pid —— pipeline 已经停了，配对的文件也该清
        try {
            const pidPath = supervisorPidFile(project);
            if (this.deps.fs.exists(pidPath)) {
                this.deps.fs.unlink(pidPath);
            }
        }
        catch {
            /* best effort */
        }
        this.deps.events.emit({
            type: 'pipeline.stopped',
            project,
            ts: this.deps.clock.now(),
        });
        return ok(undefined);
    }
    async reset(project, opts = {}) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!this.deps.executor) {
            return err(domainError('internal', 'EXECUTOR_MISSING', 'PipelineExecutor not injected'));
        }
        try {
            await this.deps.executor.reset(project, opts);
        }
        catch (cause) {
            return err(domainError('external', 'PIPELINE_RESET_FAIL', 'Failed to reset pipeline', {
                cause,
            }));
        }
        return ok(undefined);
    }
    async status(project) {
        if (!isValidProject(project))
            return err(invalidProject());
        const pid = this.readSupervisorPid(project);
        return ok({ status: pid ? 'running' : 'idle', pid });
    }
    // ─── Pipeline yaml CRUD ──────────────────────────────────────────
    async listPipelines(project) {
        if (!isValidProject(project))
            return err(invalidProject());
        const dir = pipelinesDir(project);
        if (!this.deps.fs.exists(dir)) {
            return ok({ active: null, available: [] });
        }
        let files;
        try {
            files = this.deps.fs
                .readDir(dir)
                .filter((e) => e.isFile && e.name.endsWith('.yaml'))
                .map((e) => e.name)
                .sort();
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINES_READ_FAIL', 'Failed to read pipelines directory', { cause }));
        }
        const activePath = activePipelineFile(project);
        let activeHash = null;
        if (this.deps.fs.exists(activePath)) {
            try {
                const content = this.deps.fs.readFile(activePath);
                activeHash = createHash('sha256').update(content).digest('hex');
            }
            catch {
                /* ignore */
            }
        }
        const available = files
            .filter((f) => f !== 'project.yaml')
            .map((name) => {
            const full = resolve(dir, name);
            let isActive = false;
            try {
                const content = this.deps.fs.readFile(full);
                isActive = activeHash !== null && createHash('sha256').update(content).digest('hex') === activeHash;
            }
            catch {
                /* ignore */
            }
            return { name, isActive };
        });
        return ok({ active: activeHash ? 'project.yaml' : null, available });
    }
    async readPipeline(project, filename) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!PipelineFilenameRe.test(filename)) {
            return err(domainError('validation', 'INVALID_FILENAME', 'Invalid pipeline filename'));
        }
        const path = pipelineFile(project, filename);
        if (!this.deps.fs.exists(path)) {
            return err(domainError('not-found', 'PIPELINE_NOT_FOUND', 'Pipeline file not found'));
        }
        let content;
        try {
            content = this.deps.fs.readFile(path);
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINE_READ_FAIL', 'Failed to read pipeline', { cause }));
        }
        const etag = hashEtag(content);
        let parsed = null;
        let parseError = null;
        try {
            parsed = YAML.parse(content);
        }
        catch (e) {
            parseError = e instanceof Error ? e.message : String(e);
        }
        return ok({
            content,
            etag,
            parsed,
            parseError,
            isActive: filename === 'project.yaml',
        });
    }
    async writePipeline(project, filename, content, etag) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!PipelineFilenameRe.test(filename)) {
            return err(domainError('validation', 'INVALID_FILENAME', 'Invalid pipeline filename'));
        }
        if (!etag) {
            return err(domainError('validation', 'ETAG_REQUIRED', 'etag is required'));
        }
        const path = pipelineFile(project, filename);
        if (!this.deps.fs.exists(path)) {
            return err(domainError('not-found', 'PIPELINE_NOT_FOUND', 'Pipeline file not found'));
        }
        let current;
        try {
            current = this.deps.fs.readFile(path);
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINE_READ_FAIL', 'Failed to read pipeline', { cause }));
        }
        if (hashEtag(current) !== etag) {
            return err(domainError('conflict', 'PIPELINE_ETAG_MISMATCH', 'Pipeline was modified by someone else — please reload'));
        }
        try {
            YAML.parse(content);
        }
        catch (e) {
            return err(domainError('validation', 'YAML_INVALID', 'Invalid YAML syntax', {
                details: { message: e instanceof Error ? e.message : String(e) },
            }));
        }
        try {
            this.deps.fs.writeFileAtomic(path, content);
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINE_WRITE_FAIL', 'Failed to write pipeline', { cause }));
        }
        return ok({ etag: hashEtag(content) });
    }
    async createPipeline(project, input) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!PipelineFilenameRe.test(input.name)) {
            return err(domainError('validation', 'INVALID_FILENAME', 'Invalid filename'));
        }
        const dir = pipelinesDir(project);
        if (!this.deps.fs.exists(dir)) {
            return err(domainError('not-found', 'PIPELINES_DIR_NOT_FOUND', 'pipelines directory not found'));
        }
        const path = pipelineFile(project, input.name);
        if (this.deps.fs.exists(path)) {
            return err(domainError('conflict', 'PIPELINE_EXISTS', 'Pipeline already exists'));
        }
        const template = input.template ?? 'blank';
        let content;
        if (template === 'sample') {
            const src = resolve(dir, 'sample.yaml.example');
            if (!this.deps.fs.exists(src)) {
                return err(domainError('not-found', 'SAMPLE_NOT_FOUND', 'Sample template not found'));
            }
            content = this.deps.fs.readFile(src);
        }
        else if (template === 'active') {
            const src = activePipelineFile(project);
            if (!this.deps.fs.exists(src)) {
                return err(domainError('not-found', 'ACTIVE_NOT_FOUND', 'No active pipeline'));
            }
            content = this.deps.fs.readFile(src);
        }
        else {
            content = `mode: project\n\nstages:\n  - name: develop\n    on_complete: "move_card Done"\n    on_fail:\n      action: "label NEEDS-FIX"\n      halt: true\n`;
        }
        try {
            this.deps.fs.writeFileAtomic(path, content);
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINE_WRITE_FAIL', 'Failed to write pipeline', { cause }));
        }
        return ok({ name: input.name, content, etag: hashEtag(content) });
    }
    async deletePipeline(project, filename) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!PipelineFilenameRe.test(filename)) {
            return err(domainError('validation', 'INVALID_FILENAME', 'Invalid filename'));
        }
        if (filename === 'project.yaml') {
            return err(domainError('conflict', 'CANNOT_DELETE_ACTIVE', 'Cannot delete the active pipeline — switch to another one first'));
        }
        if (filename === 'sample.yaml.example') {
            return err(domainError('conflict', 'CANNOT_DELETE_SAMPLE', 'The sample template cannot be deleted'));
        }
        const path = pipelineFile(project, filename);
        if (!this.deps.fs.exists(path)) {
            return err(domainError('not-found', 'PIPELINE_NOT_FOUND', 'Pipeline not found'));
        }
        try {
            this.deps.fs.unlink(path);
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINE_DELETE_FAIL', 'Failed to delete pipeline', { cause }));
        }
        return ok(undefined);
    }
    /** 切换 active pipeline —— 复制指定文件到 project.yaml。 */
    async switchActive(project, filename) {
        if (!isValidProject(project))
            return err(invalidProject());
        if (!PipelineFilenameRe.test(filename) || filename === 'project.yaml') {
            return err(domainError('validation', 'INVALID_FILENAME', 'Invalid pipeline filename or cannot be project.yaml'));
        }
        if (this.readSupervisorPid(project)) {
            return err(domainError('conflict', 'PIPELINE_RUNNING', 'Pipeline is running — stop it before switching'));
        }
        const src = pipelineFile(project, filename);
        if (!this.deps.fs.exists(src)) {
            return err(domainError('not-found', 'PIPELINE_NOT_FOUND', 'Source pipeline not found'));
        }
        try {
            const content = this.deps.fs.readFile(src);
            this.deps.fs.writeFileAtomic(activePipelineFile(project), content);
        }
        catch (cause) {
            return err(domainError('internal', 'PIPELINE_SWITCH_FAIL', 'Failed to switch pipeline', { cause }));
        }
        return ok({ activePipeline: filename });
    }
    // ─── 内部 ─────────────────────────────────────────────────────────
    readSupervisorPid(project) {
        const path = supervisorPidFile(project);
        if (!this.deps.fs.exists(path))
            return null;
        try {
            const raw = this.deps.fs.readFile(path);
            const pid = Number.parseInt(raw.trim(), 10);
            if (!Number.isFinite(pid) || pid <= 0)
                return null;
            try {
                process.kill(pid, 0);
                return pid;
            }
            catch {
                return null;
            }
        }
        catch {
            return null;
        }
    }
}
// ─── helpers ──────────────────────────────────────────────────────
function hashEtag(content) {
    return createHash('sha256').update(content).digest('hex').slice(0, 16);
}
function isValidProject(project) {
    return typeof project === 'string' && /^[a-zA-Z0-9_-]+$/.test(project);
}
function invalidProject() {
    return domainError('validation', 'INVALID_PROJECT_NAME', 'Invalid project name');
}
function projectNotFound(name) {
    return domainError('not-found', 'PROJECT_NOT_FOUND', `Project ${name} not found`);
}
//# sourceMappingURL=PipelineService.js.map