// engine/spec-dependency-graph.ts — Build and analyze spec dependency graphs.
// Computes critical path, detects cycles, and generates Mermaid diagrams.
/**
 * Build a dependency graph from an array of specs.
 * Each spec's `dependencies` field lists the IDs it depends on.
 */
export function buildSpecDependencyGraph(specs) {
    const nodes = new Map();
    const edges = new Map();
    const specIds = new Set(specs.map((s) => s.id));
    for (const spec of specs) {
        nodes.set(spec.id, {
            specId: spec.id,
            title: spec.title,
            status: spec.status,
            estimate: spec.estimation.devHours,
        });
        // Only keep edges to specs that exist in the graph
        const validDeps = spec.dependencies.filter((d) => specIds.has(d));
        edges.set(spec.id, validDeps);
    }
    return { nodes, edges };
}
/**
 * Detect circular dependencies using DFS with a recursion stack.
 * Returns an array of cycles, each cycle being an array of spec IDs.
 */
export function detectCycles(graph) {
    const cycles = [];
    const visited = new Set();
    const inStack = new Set();
    const stack = [];
    function dfs(node) {
        if (inStack.has(node)) {
            const cycleStart = stack.indexOf(node);
            if (cycleStart !== -1) {
                cycles.push([...stack.slice(cycleStart), node]);
            }
            return;
        }
        if (visited.has(node)) {
            return;
        }
        visited.add(node);
        inStack.add(node);
        stack.push(node);
        for (const dep of graph.edges.get(node) ?? []) {
            dfs(dep);
        }
        stack.pop();
        inStack.delete(node);
    }
    for (const node of graph.nodes.keys()) {
        dfs(node);
    }
    return cycles;
}
/**
 * Topological sort in waves (levels). Each wave contains specs that can
 * execute in parallel once all previous waves are complete.
 * Returns empty array if cycles exist.
 */
export function getExecutionOrder(graph) {
    const inDegree = new Map();
    // Reverse edges: we need "blocked by" (dependsOn), which is what edges already represent
    // But for topo sort we need "who depends on me" to decrement in-degree
    const dependents = new Map();
    for (const id of graph.nodes.keys()) {
        inDegree.set(id, 0);
        dependents.set(id, []);
    }
    for (const [specId, deps] of graph.edges) {
        // specId depends on deps — so specId has in-degree = deps.length
        inDegree.set(specId, deps.length);
        for (const dep of deps) {
            const list = dependents.get(dep);
            if (list) {
                list.push(specId);
            }
        }
    }
    const waves = [];
    let remaining = new Set(graph.nodes.keys());
    while (remaining.size > 0) {
        // Collect nodes with in-degree 0
        const wave = [];
        for (const id of remaining) {
            if ((inDegree.get(id) ?? 0) === 0) {
                wave.push(id);
            }
        }
        if (wave.length === 0) {
            // Cycle detected — no nodes with in-degree 0
            break;
        }
        wave.sort(); // deterministic order
        waves.push(wave);
        // Remove wave nodes and decrement in-degrees
        for (const id of wave) {
            remaining.delete(id);
            for (const dependent of dependents.get(id) ?? []) {
                inDegree.set(dependent, (inDegree.get(dependent) ?? 1) - 1);
            }
        }
        // Create new Set to avoid mutation during iteration
        remaining = new Set(remaining);
    }
    return waves;
}
/**
 * Find the critical path — the longest chain by sum of estimates.
 * Uses dynamic programming on the DAG (assumes no cycles).
 */
export function findCriticalPath(graph) {
    const waves = getExecutionOrder(graph);
    if (waves.length === 0) {
        return [];
    }
    // Flatten waves to get topological order
    const topoOrder = waves.flat();
    // dist[id] = longest path ending at id (by estimate sum)
    const dist = new Map();
    const prev = new Map();
    for (const id of topoOrder) {
        const nodeEstimate = graph.nodes.get(id)?.estimate ?? 0;
        dist.set(id, nodeEstimate);
        prev.set(id, null);
    }
    // Process in topological order
    for (const id of topoOrder) {
        const currentDist = dist.get(id) ?? 0;
        // For each spec that depends on this one (dependents)
        for (const [specId, deps] of graph.edges) {
            if (deps.includes(id)) {
                const specEstimate = graph.nodes.get(specId)?.estimate ?? 0;
                const newDist = currentDist + specEstimate;
                if (newDist > (dist.get(specId) ?? 0)) {
                    dist.set(specId, newDist);
                    prev.set(specId, id);
                }
            }
        }
    }
    // Find the node with maximum distance
    let maxNode = '';
    let maxDist = -1;
    for (const [id, d] of dist) {
        if (d > maxDist) {
            maxDist = d;
            maxNode = id;
        }
    }
    if (maxNode === '') {
        return [];
    }
    // Reconstruct path
    const path = [];
    let current = maxNode;
    while (current !== null) {
        path.unshift(current);
        current = prev.get(current) ?? null;
    }
    return path;
}
/**
 * Generate a Mermaid flowchart diagram of the dependency graph.
 * Highlights the critical path in red.
 */
export function generateMermaidDiagram(graph, criticalPath) {
    const lines = ['graph TD'];
    const criticalSet = new Set(criticalPath);
    // Define nodes
    for (const [id, node] of graph.nodes) {
        const label = `${id}: ${node.title}`;
        const safeLabel = label.replace(/"/g, "'");
        if (criticalSet.has(id)) {
            lines.push(`  ${id}["${safeLabel}"]:::critical`);
        }
        else {
            lines.push(`  ${id}["${safeLabel}"]`);
        }
    }
    // Define edges
    for (const [specId, deps] of graph.edges) {
        for (const dep of deps) {
            const isCriticalEdge = criticalSet.has(specId) && criticalSet.has(dep);
            if (isCriticalEdge) {
                lines.push(`  ${dep} ==> ${specId}`);
            }
            else {
                lines.push(`  ${dep} --> ${specId}`);
            }
        }
    }
    // Style critical path
    if (criticalPath.length > 0) {
        lines.push('  classDef critical fill:#ff6b6b,stroke:#c92a2a,color:#fff');
    }
    // Status-based styling
    lines.push('  classDef done fill:#51cf66,stroke:#2f9e44,color:#fff');
    lines.push('  classDef inprogress fill:#ffd43b,stroke:#f08c00,color:#333');
    for (const [id, node] of graph.nodes) {
        if (criticalSet.has(id)) {
            continue; // already styled
        }
        if (node.status === 'done' || node.status === 'approved') {
            lines.push(`  class ${id} done`);
        }
        else if (node.status === 'implementing') {
            lines.push(`  class ${id} inprogress`);
        }
    }
    return lines.join('\n');
}
/**
 * Full analysis: build graph, detect cycles, compute critical path,
 * execution waves, and generate Mermaid diagram.
 */
export function analyzeSpecDependencies(specs) {
    const graph = buildSpecDependencyGraph(specs);
    const cycles = detectCycles(graph);
    const executionWaves = getExecutionOrder(graph);
    const criticalPath = findCriticalPath(graph);
    let criticalPathHours = 0;
    for (const id of criticalPath) {
        criticalPathHours += graph.nodes.get(id)?.estimate ?? 0;
    }
    let totalEdges = 0;
    for (const deps of graph.edges.values()) {
        totalEdges += deps.length;
    }
    const mermaidDiagram = generateMermaidDiagram(graph, criticalPath);
    return {
        criticalPath,
        criticalPathHours,
        cycles,
        executionWaves,
        mermaidDiagram,
        totalSpecs: graph.nodes.size,
        totalEdges,
    };
}
//# sourceMappingURL=spec-dependency-graph.js.map