"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RunStore = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const crypto_1 = require("crypto");
const wal_js_1 = require("./wal.js");
const snapshot_js_1 = require("./snapshot.js");
const recovery_js_1 = require("./recovery.js");
const SNAPSHOT_INTERVAL = 25;
class RunStore {
    baseDir;
    wals = new Map();
    runs = new Map();
    appendCount = new Map();
    retryCounts = new Map();
    constructor(baseDir) {
        this.baseDir = baseDir;
        (0, fs_1.mkdirSync)(baseDir, { recursive: true });
    }
    create(workflow, args, workspacePath, artifactsPath, projectPath, workflowYaml, branchName) {
        const runId = `run-${(0, crypto_1.randomBytes)(6).toString('hex')}`;
        const run = {
            runId, workflow, workflowYaml, status: 'running', args,
            workspacePath, artifactsPath, projectPath, branchName,
            startedAt: new Date().toISOString(), nodes: {},
        };
        this.runs.set(runId, run);
        const wal = this.getWAL(runId);
        wal.append({
            type: 'run:create',
            data: { workflow, args, workflowYaml, workspacePath, artifactsPath, projectPath, branchName },
        });
        return run;
    }
    updateNode(runId, nodeId, result, opts) {
        const run = this.getRun(runId);
        run.nodes[nodeId] = { ...run.nodes[nodeId], ...result };
        if (result.status) {
            const wal = this.getWAL(runId);
            const eventType = this.statusToEventType(result.status);
            if (eventType) {
                const data = {};
                if (result.output != null) {
                    data.output = wal.persistOutput(nodeId, result.output);
                }
                if (result.structuredOutput != null) {
                    data.structuredOutput = wal.persistStructuredOutput(nodeId, result.structuredOutput);
                }
                if (result.error != null)
                    data.error = result.error;
                if (result.startedAt)
                    data.startedAt = result.startedAt;
                if (result.completedAt)
                    data.completedAt = result.completedAt;
                if (result.durationMs !== undefined)
                    data.durationMs = result.durationMs;
                if (opts?.retryCount !== undefined) {
                    data.retryCount = opts.retryCount;
                    this.retryCounts.set(`${runId}:${nodeId}`, opts.retryCount);
                }
                wal.append({ type: eventType, nodeId, data: Object.keys(data).length > 0 ? data : undefined });
                this.maybeSnapshot(runId);
            }
        }
    }
    save(runId) {
        // In WAL-based store, save is a no-op — state is persisted via WAL append
        // Kept for API compatibility
    }
    complete(runId) {
        const run = this.getRun(runId);
        run.status = 'completed';
        run.completedAt = new Date().toISOString();
        const wal = this.getWAL(runId);
        wal.append({ type: 'run:complete' });
        this.writeSnapshotNow(runId);
    }
    fail(runId) {
        const run = this.getRun(runId);
        run.status = 'failed';
        run.completedAt = new Date().toISOString();
        const wal = this.getWAL(runId);
        wal.append({ type: 'run:fail' });
        this.writeSnapshotNow(runId);
    }
    resetNode(runId, nodeId) {
        const run = this.getRun(runId);
        delete run.nodes[nodeId];
        const wal = this.getWAL(runId);
        wal.append({ type: 'node:reset', nodeId });
    }
    load(runId) {
        if (this.runs.has(runId))
            return this.runs.get(runId);
        // Try to recover from WAL
        const runDir = (0, path_1.join)(this.baseDir, runId);
        if (!(0, fs_1.existsSync)(runDir))
            return undefined;
        const state = (0, recovery_js_1.recoverState)(runDir);
        if (!state)
            return undefined;
        const run = {
            runId: state.runId || runId,
            workflow: state.workflow,
            workflowYaml: state.workflowYaml,
            status: state.runStatus ?? 'running',
            args: state.args,
            workspacePath: state.workspacePath,
            artifactsPath: state.artifactsPath,
            projectPath: state.projectPath,
            branchName: state.branchName,
            startedAt: state.startedAt,
            completedAt: state.completedAt,
            nodes: state.nodeResults,
        };
        this.runs.set(runId, run);
        return run;
    }
    recover(runId) {
        const runDir = (0, path_1.join)(this.baseDir, runId);
        return (0, recovery_js_1.recoverState)(runDir);
    }
    listAll() {
        const runs = [];
        if (!(0, fs_1.existsSync)(this.baseDir))
            return runs;
        for (const dir of (0, fs_1.readdirSync)(this.baseDir)) {
            if (!dir.startsWith('run-'))
                continue;
            const run = this.load(dir);
            if (run)
                runs.push(run);
        }
        return runs;
    }
    listResumable() {
        const results = [];
        if (!(0, fs_1.existsSync)(this.baseDir))
            return results;
        for (const dir of (0, fs_1.readdirSync)(this.baseDir)) {
            if (!dir.startsWith('run-'))
                continue;
            const runDir = (0, path_1.join)(this.baseDir, dir);
            // Try snapshot first (fast path)
            const snap = (0, snapshot_js_1.loadSnapshot)(runDir);
            if (snap) {
                if (snap.status !== 'running') {
                    continue; // Terminal status in snapshot — skip
                }
                // Check WAL for terminal events after this snapshot
                if (wal_js_1.WAL.hasTerminalAfter(runDir, snap.walSeq)) {
                    continue; // Run finished after snapshot — skip
                }
                results.push({
                    runId: snap.runId,
                    workflow: snap.workflow,
                    status: snap.status,
                    startedAt: snap.startedAt,
                });
                continue;
            }
            // Fall back to WAL replay for runs without snapshots
            if ((0, fs_1.existsSync)((0, path_1.join)(runDir, 'wal.jsonl'))) {
                const state = (0, recovery_js_1.recoverState)(runDir);
                if (state && (!state.runStatus || state.runStatus === 'running')) {
                    results.push({
                        runId: state.runId || dir,
                        workflow: state.workflow,
                        status: 'running',
                        startedAt: state.startedAt,
                    });
                }
            }
        }
        return results;
    }
    deleteRun(runId) {
        const runDir = (0, path_1.join)(this.baseDir, runId);
        if (!(0, fs_1.existsSync)(runDir))
            return;
        // Phase 1: write .cleanup marker (crash-safety net)
        (0, fs_1.writeFileSync)((0, path_1.join)(runDir, '.cleanup'), '', { mode: 0o600 });
        // Phase 2: delete directory
        (0, fs_1.rmSync)(runDir, { recursive: true, force: true });
        // Clear in-memory state
        this.wals.delete(runId);
        this.runs.delete(runId);
        this.appendCount.delete(runId);
        this.retryCounts.delete(runId);
    }
    sweepMarked() {
        if (!(0, fs_1.existsSync)(this.baseDir))
            return 0;
        let count = 0;
        for (const dir of (0, fs_1.readdirSync)(this.baseDir)) {
            if (!dir.startsWith('run-'))
                continue;
            if ((0, fs_1.existsSync)((0, path_1.join)(this.baseDir, dir, '.cleanup'))) {
                (0, fs_1.rmSync)((0, path_1.join)(this.baseDir, dir), { recursive: true, force: true });
                this.wals.delete(dir);
                this.runs.delete(dir);
                this.appendCount.delete(dir);
                this.retryCounts.delete(dir);
                count++;
            }
        }
        return count;
    }
    listFailed() {
        const results = [];
        if (!(0, fs_1.existsSync)(this.baseDir))
            return results;
        for (const dir of (0, fs_1.readdirSync)(this.baseDir)) {
            if (!dir.startsWith('run-'))
                continue;
            const run = this.load(dir);
            if (run && run.status === 'failed') {
                results.push({
                    runId: run.runId,
                    workflow: run.workflow,
                    status: run.status,
                    startedAt: run.startedAt,
                });
            }
        }
        return results;
    }
    snapshotSync(runId) {
        this.writeSnapshotNow(runId);
    }
    // ── Run History ──
    /** Capture a summary of the run and append to history.jsonl (call before deleteRun). */
    appendHistory(runId) {
        const run = this.runs.get(runId);
        if (!run)
            return;
        const failedNodes = Object.entries(run.nodes)
            .filter(([, r]) => r.status === 'failed')
            .map(([id]) => id);
        const entry = {
            runId: run.runId,
            workflow: run.workflow,
            status: run.status,
            args: run.args,
            startedAt: run.startedAt,
            completedAt: run.completedAt ?? new Date().toISOString(),
            durationMs: run.completedAt
                ? new Date(run.completedAt).getTime() - new Date(run.startedAt).getTime()
                : Date.now() - new Date(run.startedAt).getTime(),
            nodeCount: Object.keys(run.nodes).length,
            failedNodes,
        };
        const historyPath = (0, path_1.join)(this.baseDir, '..', 'history.jsonl');
        (0, fs_1.appendFileSync)(historyPath, JSON.stringify(entry) + '\n', 'utf-8');
    }
    /** Read all history entries. Most recent last. */
    listHistory(limit = 50) {
        const historyPath = (0, path_1.join)(this.baseDir, '..', 'history.jsonl');
        if (!(0, fs_1.existsSync)(historyPath))
            return [];
        const lines = (0, fs_1.readFileSync)(historyPath, 'utf-8').trim().split('\n').filter(Boolean);
        const entries = [];
        for (const line of lines) {
            try {
                entries.push(JSON.parse(line));
            }
            catch { /* skip corrupt lines */ }
        }
        return entries.slice(-limit);
    }
    getWAL(runId) {
        let wal = this.wals.get(runId);
        if (!wal) {
            const runDir = (0, path_1.join)(this.baseDir, runId);
            (0, fs_1.mkdirSync)(runDir, { recursive: true });
            wal = new wal_js_1.WAL(runDir);
            this.wals.set(runId, wal);
        }
        return wal;
    }
    getRun(runId) {
        const run = this.runs.get(runId);
        if (!run)
            throw new Error(`Run not found: ${runId}`);
        return run;
    }
    statusToEventType(status) {
        switch (status) {
            case 'running': return 'node:start';
            case 'completed': return 'node:complete';
            case 'failed': return 'node:fail';
            case 'skipped': return 'node:skip';
            default: return null;
        }
    }
    maybeSnapshot(runId) {
        const count = (this.appendCount.get(runId) ?? 0) + 1;
        this.appendCount.set(runId, count);
        if (count >= SNAPSHOT_INTERVAL) {
            this.writeSnapshotNow(runId);
            this.appendCount.set(runId, 0);
        }
    }
    writeSnapshotNow(runId) {
        const run = this.runs.get(runId);
        if (!run)
            return;
        const wal = this.wals.get(runId);
        if (!wal)
            return;
        const snapshot = {
            runId: run.runId,
            workflow: run.workflow,
            workflowYaml: run.workflowYaml,
            status: run.status,
            walSeq: wal.currentSeq,
            args: run.args,
            workspacePath: run.workspacePath,
            artifactsPath: run.artifactsPath,
            projectPath: run.projectPath,
            branchName: run.branchName,
            startedAt: run.startedAt,
            completedAt: run.completedAt,
            nodes: {},
        };
        for (const [nodeId, result] of Object.entries(run.nodes)) {
            const retryCount = this.retryCounts.get(`${runId}:${nodeId}`);
            snapshot.nodes[nodeId] = {
                status: result.status,
                outputRef: result.output != null ? wal.getOutputRef(nodeId, result.output) : undefined,
                structuredOutputRef: result.structuredOutput != null
                    ? wal.getStructuredOutputRef(nodeId, result.structuredOutput)
                    : undefined,
                startedAt: result.startedAt,
                completedAt: result.completedAt,
                error: result.error,
                retryCount,
            };
        }
        (0, snapshot_js_1.writeSnapshot)((0, path_1.join)(this.baseDir, runId), snapshot);
    }
}
exports.RunStore = RunStore;
//# sourceMappingURL=run-store.js.map