/**
 * depends-on.mjs — task dependency-graph helpers (C29).
 */
import { parseTaskRef } from './task-ref.mjs';
/**
 * Build a Map<task_id, depends_on[]> over all known tasks (active + completed)
 * in a loaded state object. Depends_on entries are kept as scoped refs where
 * available, but the cycle detector works on task_ids alone, so we normalize
 * to bare task_ids here. Cross-scope edges are preserved (the detector just
 * treats them as graph edges).
 */
export function buildDependsOnMap(state) {
    const map = new Map();
    const active = state?.active_tasks || {};
    for (const [id, task] of Object.entries(active)) {
        const deps = Array.isArray(task?.depends_on) ? task.depends_on : [];
        const normalized = deps
            .map((d) => {
            const parsed = parseTaskRef(d, task?.scope || 'project');
            return parsed ? parsed.task_id : null;
        })
            .filter(Boolean);
        map.set(id, normalized);
    }
    const completed = Array.isArray(state?.completed_tasks) ? state.completed_tasks : [];
    for (const task of completed) {
        if (!task?.id)
            continue;
        const deps = Array.isArray(task?.depends_on) ? task.depends_on : [];
        const normalized = deps
            .map((d) => {
            const parsed = parseTaskRef(d, task?.scope || 'project');
            return parsed ? parsed.task_id : null;
        })
            .filter(Boolean);
        if (!map.has(task.id))
            map.set(task.id, normalized);
    }
    return map;
}
/**
 * DFS-based cycle detector (mirror of src/helpers/cycle.ts detectCycle).
 * Given a depends_on adjacency map and a proposed new edge, returns a cycle
 * path (array of task_ids) if the edge would close one, or null otherwise.
 */
export function detectDependsOnCycle(dependsOnByTaskId, newEdge) {
    const adj = new Map();
    for (const [id, deps] of dependsOnByTaskId.entries()) {
        adj.set(id, [...deps]);
    }
    const existing = adj.get(newEdge.from) || [];
    if (!existing.includes(newEdge.to)) {
        adj.set(newEdge.from, [...existing, newEdge.to]);
    }
    const stack = [{ node: newEdge.to, path: [newEdge.to] }];
    const visited = new Set();
    while (stack.length > 0) {
        const { node, path } = stack.pop();
        if (node === newEdge.from)
            return [...path, newEdge.from];
        if (visited.has(node))
            continue;
        visited.add(node);
        const nexts = adj.get(node) || [];
        for (const next of nexts) {
            stack.push({ node: next, path: [...path, next] });
        }
    }
    return null;
}
/**
 * BFS over the reverse depends_on graph to collect the transitive set of
 * tasks blocked (directly or indirectly) by `targetId`. Used by
 * `task list --blocked-by`.
 */
export function collectBlockedByTaskIds(dependsOnByTaskId, targetId) {
    const reverse = new Map();
    for (const [id, deps] of dependsOnByTaskId.entries()) {
        for (const dep of deps) {
            const arr = reverse.get(dep) || [];
            arr.push(id);
            reverse.set(dep, arr);
        }
    }
    const visited = new Set();
    const out = [];
    const queue = [...(reverse.get(targetId) || [])];
    while (queue.length > 0) {
        const current = queue.shift();
        if (visited.has(current))
            continue;
        visited.add(current);
        out.push(current);
        for (const next of reverse.get(current) || []) {
            if (!visited.has(next))
                queue.push(next);
        }
    }
    return out;
}
//# sourceMappingURL=depends-on.mjs.map