/**
 * TaskGraphBuilder — converts task definitions into a TaskGraph IR.
 *
 * The builder is the first stage of the compilation pipeline:
 *   GeneralizedTaskDefinitions → TaskGraphBuilder → TaskGraph → PromptfooCompiler → YAML
 *
 * Responsibilities:
 * - Accept tasks from any source (TS, YAML, Content Lake)
 * - Apply area/tag/mode filtering
 * - Resolve inter-task dependencies into edges
 * - Validate the graph is a DAG (reject cycles)
 * - Assign execution priority via topological sort
 *
 * @see packages/core/src/types/task-graph.ts — TaskGraph types
 * @see docs/archive/exec-plans/architecture-overhaul/phase-2-config-compiler.md
 */
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
 * Build a TaskGraph from task definitions.
 *
 * 1. Filters tasks by area, tags, task IDs, and status
 * 2. Creates TaskNodes with resolved variables
 * 3. Discovers dependency edges from task metadata
 * 4. Validates the graph is acyclic
 * 5. Assigns topological priority
 */
export function buildTaskGraph(options) {
    const warnings = [];
    const filteredOut = [];
    // Step 1: Filter tasks
    const filtered = filterTasks(options.tasks, options.filter, filteredOut);
    if (filtered.length === 0) {
        return {
            graph: null,
            warnings: ["No tasks matched the filter criteria"],
            filteredOut,
        };
    }
    // Step 2: Create nodes
    const nodes = new Map();
    for (const task of filtered) {
        const node = taskToNode(task);
        if (nodes.has(node.taskId)) {
            warnings.push(`Duplicate task ID "${node.taskId}" — later definition wins`);
        }
        nodes.set(node.taskId, node);
    }
    // Step 3: Discover edges from dependency metadata
    const edges = discoverEdges(filtered, nodes, warnings);
    // Step 4: Validate acyclicity
    const cycleError = detectCycle(nodes, edges);
    if (cycleError) {
        throw new Error(`Task graph contains a cycle: ${cycleError.join(" → ")}. ` +
            "Task graphs must be directed acyclic graphs (DAGs).");
    }
    // Step 5: Assign topological priority
    assignPriority(nodes, edges);
    // Step 6: Build fixture map (empty for now — Phase 2d fills this)
    const fixtures = new Map();
    const graph = {
        compilationTarget: options.compilationTarget ?? "promptfoo",
        edges,
        fixtures,
        nodes,
    };
    return { graph, warnings, filteredOut };
}
// ---------------------------------------------------------------------------
// Filtering
// ---------------------------------------------------------------------------
function filterTasks(tasks, filter, filteredOut) {
    return tasks.filter((task) => {
        // Status filter — always applied (even without explicit filter options)
        const status = task.status ?? "active";
        const isTargetedById = filter?.taskIds && filter.taskIds.includes(task.id);
        if (status === "archived") {
            filteredOut.push(task.id);
            return false;
        }
        if (status === "paused" && !isTargetedById) {
            filteredOut.push(task.id);
            return false;
        }
        if (status === "draft" && !isTargetedById && !filter?.includeDrafts) {
            filteredOut.push(task.id);
            return false;
        }
        // Remaining filters only apply when an explicit filter is provided
        if (!filter)
            return true;
        // Area filter — GeneralizedTaskDefinition uses `area` (not `featureArea`)
        const taskArea = task.area ?? "";
        if (filter.areas &&
            filter.areas.length > 0 &&
            !filter.areas.map((a) => a.toLowerCase()).includes(taskArea.toLowerCase())) {
            filteredOut.push(task.id);
            return false;
        }
        // Task ID filter
        if (filter.taskIds &&
            filter.taskIds.length > 0 &&
            !filter.taskIds.includes(task.id)) {
            filteredOut.push(task.id);
            return false;
        }
        // Tag filter
        if (filter.tags &&
            filter.tags.length > 0 &&
            (!task.tags || !task.tags.some((t) => filter.tags.includes(t)))) {
            filteredOut.push(task.id);
            return false;
        }
        return true;
    });
}
// ---------------------------------------------------------------------------
// Node creation
// ---------------------------------------------------------------------------
function taskToNode(task) {
    // GeneralizedTaskDefinition uses prompt.text/prompt.template instead of taskPrompt,
    // and prompt.vars instead of extraVars
    const promptText = task.prompt?.text ?? task.prompt?.template ?? "";
    const promptVars = task.prompt?.vars ?? {};
    const envelope = {
        declarations: [],
        provenance: {},
        values: {
            ...(promptText ? { task: promptText } : {}),
            ...promptVars,
        },
    };
    return {
        dependsOn: [],
        mode: task.mode,
        priority: 0,
        resolvedPrompt: promptText,
        resolvedVariables: envelope,
        taskId: task.id,
    };
}
// ---------------------------------------------------------------------------
// Edge discovery
// ---------------------------------------------------------------------------
/**
 * Discover dependency edges from task metadata.
 *
 * Looks for explicit `dependsOn` arrays in prompt.vars (the generalized
 * equivalent of the old extraVars convention).
 * Future phases will add implicit deps from fixture sharing, data flow, etc.
 */
function discoverEdges(tasks, nodes, warnings) {
    const edges = [];
    for (const task of tasks) {
        // Check for explicit dependencies in prompt.vars (was extraVars.dependsOn)
        const deps = task.prompt?.vars?.dependsOn;
        if (Array.isArray(deps)) {
            for (const dep of deps) {
                if (typeof dep !== "string")
                    continue;
                if (!nodes.has(dep)) {
                    warnings.push(`Task "${task.id}" depends on "${dep}" which is not in the graph — ` +
                        "dependency ignored (task may have been filtered out)");
                    continue;
                }
                edges.push({ from: dep, to: task.id, type: "ordering" });
                const node = nodes.get(task.id);
                if (node && !node.dependsOn.includes(dep)) {
                    node.dependsOn.push(dep);
                }
            }
        }
    }
    return edges;
}
// ---------------------------------------------------------------------------
// Cycle detection — Kahn's algorithm (topological sort)
// ---------------------------------------------------------------------------
/**
 * Detect cycles in the task graph using Kahn's algorithm.
 *
 * @returns null if acyclic, or the cycle path as a string array
 */
export function detectCycle(nodes, edges) {
    // Build in-degree map
    const inDegree = new Map();
    const adjacency = new Map();
    for (const id of nodes.keys()) {
        inDegree.set(id, 0);
        adjacency.set(id, []);
    }
    for (const edge of edges) {
        adjacency.get(edge.from).push(edge.to);
        inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
    }
    // Start with all zero-in-degree nodes
    const queue = [];
    for (const [id, deg] of inDegree) {
        if (deg === 0)
            queue.push(id);
    }
    let visited = 0;
    while (queue.length > 0) {
        const current = queue.shift();
        visited++;
        for (const neighbor of adjacency.get(current) ?? []) {
            const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
            inDegree.set(neighbor, newDeg);
            if (newDeg === 0)
                queue.push(neighbor);
        }
    }
    if (visited === nodes.size)
        return null;
    // Find cycle participants (nodes with remaining in-degree > 0)
    const cycleNodes = [...inDegree.entries()]
        .filter(([, deg]) => deg > 0)
        .map(([id]) => id);
    // Reconstruct a cycle path for the error message
    return reconstructCyclePath(cycleNodes, adjacency);
}
/**
 * Reconstruct a human-readable cycle path from cycle participants.
 */
function reconstructCyclePath(cycleNodes, adjacency) {
    if (cycleNodes.length === 0)
        return [];
    const inCycle = new Set(cycleNodes);
    const start = cycleNodes[0];
    const path = [start];
    const visited = new Set();
    let current = start;
    // Follow edges within the cycle to produce a readable path
    while (true) {
        visited.add(current);
        const next = (adjacency.get(current) ?? []).find((n) => inCycle.has(n) && (!visited.has(n) || n === start));
        if (!next)
            break;
        path.push(next);
        if (next === start)
            break; // Completed the cycle
        current = next;
    }
    return path;
}
// ---------------------------------------------------------------------------
// Topological priority assignment
// ---------------------------------------------------------------------------
/**
 * Assign execution priority via topological order.
 * Lower priority = earlier execution.
 */
function assignPriority(nodes, edges) {
    const inDegree = new Map();
    const adjacency = new Map();
    for (const id of nodes.keys()) {
        inDegree.set(id, 0);
        adjacency.set(id, []);
    }
    for (const edge of edges) {
        adjacency.get(edge.from).push(edge.to);
        inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
    }
    const queue = [];
    for (const [id, deg] of inDegree) {
        if (deg === 0)
            queue.push(id);
    }
    let priority = 0;
    while (queue.length > 0) {
        // Process all nodes at the current level (same priority)
        const levelSize = queue.length;
        for (let i = 0; i < levelSize; i++) {
            const current = queue.shift();
            const node = nodes.get(current);
            if (node)
                node.priority = priority;
            for (const neighbor of adjacency.get(current) ?? []) {
                const newDeg = (inDegree.get(neighbor) ?? 1) - 1;
                inDegree.set(neighbor, newDeg);
                if (newDeg === 0)
                    queue.push(neighbor);
            }
        }
        priority++;
    }
}
