"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DAGExecutor = void 0;
const os_1 = require("os");
const node_runner_js_1 = require("./node-runner.js");
const condition_evaluator_js_1 = require("./condition-evaluator.js");
const contract_validator_js_1 = require("./contract-validator.js");
class DAGExecutor {
    workspaceCwd;
    projectPath;
    graph;
    callbacks;
    pending;
    completed = new Set();
    failed = new Set();
    skipped = new Set();
    inFlight = new Set();
    nodeResults = {};
    runner;
    args = '';
    artifactsDir = (0, os_1.tmpdir)();
    initialized = false;
    inputsValidated = false;
    retryCounts = new Map();
    ancestorCache = new Map();
    delayFn;
    outputsMapCache = null;
    outputsMapDirty = true;
    yieldTimestamps = new Map();
    constructor(workspaceCwd, projectPath, graph, callbacks, options = {}) {
        this.workspaceCwd = workspaceCwd;
        this.projectPath = projectPath;
        this.graph = graph;
        this.callbacks = callbacks;
        this.runner = new node_runner_js_1.NodeRunner(workspaceCwd, projectPath);
        this.pending = new Set(graph.nodes.keys());
        this.delayFn = options.delayFn ?? ((ms) => new Promise(r => setTimeout(r, ms)));
    }
    static fromRecoveredState(workspaceCwd, projectPath, graph, callbacks, state, options = {}) {
        const executor = new DAGExecutor(workspaceCwd, projectPath, graph, callbacks, options);
        executor.pending = state.pending;
        executor.completed = state.completed;
        executor.failed = state.failed;
        executor.skipped = state.skipped;
        executor.nodeResults = state.nodeResults;
        executor.retryCounts = state.retryCounts;
        executor.args = state.args;
        executor.artifactsDir = state.artifactsPath;
        executor.initialized = true;
        executor.inputsValidated = true;
        return executor;
    }
    async step(input = {}) {
        // Initialize on first call
        if (!this.initialized && input.args !== undefined) {
            this.args = input.args;
            this.artifactsDir = input.artifactsDir ?? (0, os_1.tmpdir)();
            this.initialized = true;
        }
        // Validate inputs on first call
        if (!this.inputsValidated) {
            this.inputsValidated = true;
            if (this.graph.inputs) {
                const inputErrors = (0, contract_validator_js_1.validateInputs)(this.args, this.graph.inputs);
                if (inputErrors.length > 0) {
                    return {
                        type: 'failed',
                        error: `Input validation failed: ${inputErrors.join('; ')}`,
                    };
                }
            }
        }
        // Iterative loop replaces recursive this.step() calls to prevent stack overflow
        while (true) {
            // Absorb completed AI results
            if (input.completedResults) {
                for (const r of input.completedResults) {
                    this.inFlight.delete(r.nodeId);
                    // Validate output contract if defined
                    const node = this.graph.nodes.get(r.nodeId);
                    if (node?.outputFormat) {
                        if (!r.structuredOutput) {
                            const errorMsg = 'Output format required but no structured output received';
                            const yieldInfo = this.yieldTimestamps.get(r.nodeId);
                            this.yieldTimestamps.delete(r.nodeId);
                            this.nodeResults[r.nodeId] = {
                                status: 'failed',
                                error: errorMsg,
                                durationMs: yieldInfo ? Date.now() - yieldInfo.time : 0,
                                startedAt: yieldInfo?.startedAt,
                                completedAt: new Date().toISOString(),
                            };
                            this.markOutputsDirty();
                            this.failed.add(r.nodeId);
                            this.pending.delete(r.nodeId);
                            this.callbacks.onNodeFail(r.nodeId, errorMsg);
                            continue;
                        }
                        const validation = (0, contract_validator_js_1.validateOutputFormat)(r.structuredOutput, node.outputFormat);
                        if (!validation.valid) {
                            const errorMsg = `Output contract violation: ${validation.errors.join('; ')}`;
                            const yieldInfo = this.yieldTimestamps.get(r.nodeId);
                            this.yieldTimestamps.delete(r.nodeId);
                            this.nodeResults[r.nodeId] = {
                                status: 'failed',
                                error: errorMsg,
                                durationMs: yieldInfo ? Date.now() - yieldInfo.time : 0,
                                startedAt: yieldInfo?.startedAt,
                                completedAt: new Date().toISOString(),
                            };
                            this.markOutputsDirty();
                            this.failed.add(r.nodeId);
                            this.pending.delete(r.nodeId);
                            this.callbacks.onNodeFail(r.nodeId, errorMsg);
                            continue;
                        }
                    }
                    // Normal completion flow
                    const yieldInfo = this.yieldTimestamps.get(r.nodeId);
                    this.yieldTimestamps.delete(r.nodeId);
                    this.nodeResults[r.nodeId] = {
                        status: 'completed',
                        output: r.output,
                        structuredOutput: r.structuredOutput,
                        durationMs: yieldInfo ? Date.now() - yieldInfo.time : 0,
                        startedAt: yieldInfo?.startedAt,
                        completedAt: new Date().toISOString(),
                    };
                    this.markOutputsDirty();
                    this.completed.add(r.nodeId);
                    this.pending.delete(r.nodeId);
                    this.callbacks.onNodeComplete(r.nodeId, this.nodeResults[r.nodeId]);
                }
            }
            // Find ready nodes
            const ready = this.findReadyNodes();
            if (ready.length === 0 && this.pending.size === 0 && this.inFlight.size === 0) {
                if (this.failed.size > 0) {
                    return {
                        type: 'failed',
                        results: this.nodeResults,
                        error: `Nodes failed: ${[...this.failed].join(', ')}`,
                    };
                }
                return { type: 'done', results: this.nodeResults };
            }
            // Still waiting for in-flight AI results — don't skip or re-yield
            if (ready.length === 0 && this.inFlight.size > 0) {
                return { type: 'needs_ai', nodes: [] };
            }
            if (ready.length === 0 && this.pending.size > 0 && this.inFlight.size === 0) {
                for (const id of this.pending) {
                    this.skipped.add(id);
                    this.nodeResults[id] = { status: 'skipped' };
                    this.markOutputsDirty();
                    this.callbacks.onNodeSkip(id, 'blocked by failed dependency');
                }
                this.pending.clear();
                return {
                    type: 'failed',
                    results: this.nodeResults,
                    error: `Failed nodes: ${[...this.failed].join(', ')}`,
                };
            }
            // Evaluate conditions — intentionally unscoped so conditions can inspect any completed node
            const conditionFiltered = ready.filter(node => {
                try {
                    const shouldRun = (0, condition_evaluator_js_1.evaluateCondition)(node.when, this.buildOutputsMap());
                    if (!shouldRun) {
                        this.skipped.add(node.id);
                        this.pending.delete(node.id);
                        this.nodeResults[node.id] = { status: 'skipped' };
                        this.markOutputsDirty();
                        this.callbacks.onNodeSkip(node.id, `condition not met: ${node.when}`);
                        return false;
                    }
                    return true;
                }
                catch (err) {
                    const errorMsg = err instanceof Error ? err.message : String(err);
                    this.failed.add(node.id);
                    this.pending.delete(node.id);
                    this.nodeResults[node.id] = { status: 'failed', error: errorMsg, durationMs: 0 };
                    this.markOutputsDirty();
                    this.callbacks.onNodeFail(node.id, errorMsg);
                    return false;
                }
            });
            // Separate bash vs AI
            const bashNodes = conditionFiltered.filter(n => n.type === 'bash');
            const aiNodes = conditionFiltered.filter(n => n.type !== 'bash');
            // Execute bash nodes internally
            for (const node of bashNodes) {
                this.callbacks.onNodeStart(node.id);
                this.pending.delete(node.id);
                try {
                    const result = await this.runner.runBash(node, this.buildContext(node.id));
                    // Validate output contract if defined
                    if (node.outputFormat) {
                        if (!result.structuredOutput) {
                            throw new Error('Output format required but no structured output received');
                        }
                        const validation = (0, contract_validator_js_1.validateOutputFormat)(result.structuredOutput, node.outputFormat);
                        if (!validation.valid) {
                            throw new Error(`Output contract violation: ${validation.errors.join('; ')}`);
                        }
                    }
                    this.nodeResults[node.id] = result;
                    this.markOutputsDirty();
                    this.completed.add(node.id);
                    this.callbacks.onNodeComplete(node.id, result);
                }
                catch (error) {
                    const errorMsg = error instanceof Error ? error.message : String(error);
                    const result = { status: 'failed', error: errorMsg, durationMs: 0 };
                    this.nodeResults[node.id] = result;
                    this.markOutputsDirty();
                    // Loop support: retry if configured
                    if (node.loop) {
                        const retries = this.retryCounts.get(node.id) ?? 0;
                        if (retries < node.loop.maxRetries) {
                            this.retryCounts.set(node.id, retries + 1);
                            if (node.loop.backoffMs) {
                                await this.delayFn(node.loop.backoffMs * (retries + 1));
                            }
                            // Emit failure callback before reset so WAL records the failure
                            this.callbacks.onNodeFail(node.id, errorMsg, this.retryCounts.get(node.id));
                            // Emit reset callbacks for both target and loop node
                            this.callbacks.onNodeReset?.(node.loop.onFail);
                            this.callbacks.onNodeReset?.(node.id);
                            this.resetNode(node.id, node.loop.onFail, node.loop.injectError ? errorMsg : undefined);
                            input = {};
                            continue;
                        }
                    }
                    this.failed.add(node.id);
                    this.callbacks.onNodeFail(node.id, errorMsg, this.retryCounts.get(node.id));
                    // Error strategy: fail_workflow = skip all remaining, return immediately
                    if (node.errorStrategy === 'fail_workflow') {
                        for (const id of this.pending) {
                            this.skipped.add(id);
                            this.nodeResults[id] = { status: 'skipped' };
                            this.markOutputsDirty();
                            this.callbacks.onNodeSkip(id, `workflow failed at "${node.id}"`);
                        }
                        this.pending.clear();
                        return {
                            type: 'failed',
                            results: this.nodeResults,
                            error: `Workflow failed at node "${node.id}": ${errorMsg}`,
                        };
                    }
                }
            }
            // If only bash nodes were ready, loop to find next batch
            if (aiNodes.length === 0) {
                input = {};
                continue;
            }
            // Yield AI nodes
            const resolvedNodes = aiNodes.map(node => {
                this.callbacks.onNodeStart(node.id);
                this.pending.delete(node.id);
                this.inFlight.add(node.id);
                this.yieldTimestamps.set(node.id, { time: Date.now(), startedAt: new Date().toISOString() });
                const resolved = this.runner.resolveAIPrompt(node, this.buildContext(node.id));
                return {
                    nodeId: node.id,
                    description: resolved.description,
                    prompt: resolved.prompt,
                    skill: resolved.skill,
                    model: node.model ?? this.graph.defaultModel,
                };
            });
            return { type: 'needs_ai', nodes: resolvedNodes };
        } // end while(true)
    }
    findReadyNodes() {
        const ready = [];
        for (const nodeId of this.pending) {
            const node = this.graph.nodes.get(nodeId);
            const depsResolved = node.dependsOn.every(dep => this.completed.has(dep) || this.skipped.has(dep) || this.failed.has(dep));
            if (!depsResolved)
                continue;
            if (node.triggerRule === 'all_success') {
                const anyBlocking = node.dependsOn.some(dep => {
                    if (this.failed.has(dep)) {
                        const depNode = this.graph.nodes.get(dep);
                        return depNode.errorStrategy !== 'continue';
                    }
                    return this.skipped.has(dep);
                });
                if (anyBlocking) {
                    this.pending.delete(nodeId);
                    this.skipped.add(nodeId);
                    this.nodeResults[nodeId] = { status: 'skipped' };
                    this.markOutputsDirty();
                    this.callbacks.onNodeSkip(nodeId, 'dependency failed');
                    continue;
                }
            }
            else if (node.triggerRule === 'one_success') {
                const anySuccess = node.dependsOn.some(dep => this.completed.has(dep));
                if (!anySuccess) {
                    this.pending.delete(nodeId);
                    this.skipped.add(nodeId);
                    this.nodeResults[nodeId] = { status: 'skipped' };
                    this.markOutputsDirty();
                    this.callbacks.onNodeSkip(nodeId, 'no dependency succeeded');
                    continue;
                }
            }
            ready.push(node);
        }
        return ready;
    }
    resetNode(loopNodeId, targetNodeId, errorMsg) {
        this.inFlight.delete(targetNodeId);
        this.completed.delete(targetNodeId);
        this.failed.delete(targetNodeId);
        this.skipped.delete(targetNodeId);
        this.pending.add(targetNodeId);
        delete this.nodeResults[targetNodeId];
        this.markOutputsDirty();
        this.inFlight.delete(loopNodeId);
        this.completed.delete(loopNodeId);
        this.failed.delete(loopNodeId);
        this.pending.add(loopNodeId);
        delete this.nodeResults[loopNodeId];
        this.markOutputsDirty();
    }
    markOutputsDirty() {
        this.outputsMapDirty = true;
    }
    buildOutputsMap() {
        if (!this.outputsMapDirty && this.outputsMapCache)
            return this.outputsMapCache;
        const outputs = {};
        for (const [id, res] of Object.entries(this.nodeResults)) {
            outputs[id] = { output: res.output?.trim(), structuredOutput: res.structuredOutput };
        }
        this.outputsMapCache = outputs;
        this.outputsMapDirty = false;
        return outputs;
    }
    getAncestors(nodeId) {
        if (this.ancestorCache.has(nodeId))
            return this.ancestorCache.get(nodeId);
        const ancestors = new Set();
        const queue = [nodeId];
        while (queue.length > 0) {
            const current = queue.shift();
            const node = this.graph.nodes.get(current);
            if (!node)
                continue;
            for (const dep of node.dependsOn) {
                if (!ancestors.has(dep)) {
                    ancestors.add(dep);
                    queue.push(dep);
                }
            }
        }
        this.ancestorCache.set(nodeId, ancestors);
        return ancestors;
    }
    clearResults() {
        this.nodeResults = {};
        this.outputsMapCache = null;
        this.outputsMapDirty = true;
    }
    getNodeSummaries() {
        const summaries = {};
        for (const [id, result] of Object.entries(this.nodeResults)) {
            summaries[id] = {
                status: result.status,
                durationMs: result.durationMs,
                startedAt: result.startedAt,
                completedAt: result.completedAt,
                outputPreview: result.output?.substring(0, 200),
            };
        }
        for (const id of this.pending) {
            if (!summaries[id])
                summaries[id] = { status: 'pending' };
        }
        for (const id of this.inFlight) {
            if (!summaries[id])
                summaries[id] = { status: 'running' };
        }
        return summaries;
    }
    buildScopedOutputsMap(nodeId) {
        const ancestors = this.getAncestors(nodeId);
        const outputs = {};
        for (const id of ancestors) {
            const res = this.nodeResults[id];
            if (res) {
                outputs[id] = { output: res.output?.trim(), structuredOutput: res.structuredOutput };
            }
        }
        return outputs;
    }
    buildContext(forNodeId) {
        return {
            ARGUMENTS: this.args,
            ARTIFACTS_DIR: this.artifactsDir,
            nodes: this.buildScopedOutputsMap(forNodeId),
        };
    }
}
exports.DAGExecutor = DAGExecutor;
//# sourceMappingURL=dag-executor.js.map