// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * DEPENDENCY RESOLVER
 *
 * Handles dependency graph operations for execution plans.
 * Supports topological sorting and circular dependency detection.
 */
import { createLogger } from '../../common/logger.js';
const logger = createLogger({ component: 'dependency-resolver' });
/**
 * Error thrown when circular dependencies are detected
 */
export class CircularDependencyError extends Error {
    cycle;
    constructor(cycle, message) {
        super(message ?? `Circular dependency detected: ${cycle.join(' -> ')}`);
        this.cycle = cycle;
        this.name = 'CircularDependencyError';
    }
}
/**
 * Error thrown when dependencies reference unknown steps
 */
export class InvalidDependencyError extends Error {
    stepId;
    unknownDep;
    constructor(stepId, unknownDep) {
        super(`Step "${stepId}" depends on unknown step "${unknownDep}"`);
        this.stepId = stepId;
        this.unknownDep = unknownDep;
        this.name = 'InvalidDependencyError';
    }
}
/**
 * DependencyResolver - Manages step dependencies and execution ordering
 */
export class DependencyResolver {
    /**
     * Validate that all dependencies reference existing steps
     */
    validateDependencies(steps, dependencies) {
        const stepIds = new Set(steps.map(s => s.id));
        for (const [stepId, deps] of Object.entries(dependencies)) {
            // Check if the step exists (optional - may define deps for steps not yet added)
            if (!stepIds.has(stepId)) {
                logger.warn({ stepId }, 'Dependencies defined for non-existent step');
            }
            // Check if all dependencies exist
            for (const dep of deps) {
                if (!stepIds.has(dep)) {
                    throw new InvalidDependencyError(stepId, dep);
                }
            }
        }
    }
    /**
     * Get execution order using topological sort (Kahn's algorithm)
     * Throws CircularDependencyError if cycles are detected.
     */
    getExecutionOrder(stepIds, dependencies) {
        if (stepIds.length === 0) {
            return [];
        }
        // Build in-degree map and adjacency list
        const inDegree = new Map();
        const adjacency = new Map();
        // Initialize all nodes
        for (const id of stepIds) {
            inDegree.set(id, 0);
            adjacency.set(id, []);
        }
        // Build graph from dependencies
        // If A depends on B, then B -> A (B must complete before A)
        for (const [stepId, deps] of Object.entries(dependencies)) {
            if (!inDegree.has(stepId))
                continue;
            for (const dep of deps) {
                if (!inDegree.has(dep))
                    continue;
                // Add edge from dependency to dependent
                adjacency.get(dep).push(stepId);
                inDegree.set(stepId, inDegree.get(stepId) + 1);
            }
        }
        // Find all nodes with no incoming edges
        const queue = [];
        for (const [id, degree] of inDegree) {
            if (degree === 0) {
                queue.push(id);
            }
        }
        const result = [];
        while (queue.length > 0) {
            const current = queue.shift();
            result.push(current);
            // For each neighbor, reduce in-degree
            for (const neighbor of adjacency.get(current) ?? []) {
                const newDegree = inDegree.get(neighbor) - 1;
                inDegree.set(neighbor, newDegree);
                if (newDegree === 0) {
                    queue.push(neighbor);
                }
            }
        }
        // If we didn't process all nodes, there's a cycle
        if (result.length !== stepIds.length) {
            const cycle = this.findCycle(stepIds, dependencies);
            throw new CircularDependencyError(cycle);
        }
        return result;
    }
    /**
     * Find a cycle in the dependency graph using DFS
     */
    findCycle(stepIds, dependencies) {
        const state = new Map();
        const parent = new Map();
        for (const id of stepIds) {
            state.set(id, 'unvisited');
            parent.set(id, null);
        }
        // Build adjacency (dependency -> dependent)
        const adjacency = new Map();
        for (const id of stepIds) {
            adjacency.set(id, []);
        }
        for (const [stepId, deps] of Object.entries(dependencies)) {
            if (!adjacency.has(stepId))
                continue;
            for (const dep of deps) {
                if (adjacency.has(dep)) {
                    adjacency.get(dep).push(stepId);
                }
            }
        }
        // DFS to find cycle
        const dfs = (node, path) => {
            state.set(node, 'visiting');
            path.push(node);
            for (const neighbor of adjacency.get(node) ?? []) {
                const neighborState = state.get(neighbor);
                if (neighborState === 'visiting') {
                    // Found a cycle - extract the cycle path
                    const cycleStart = path.indexOf(neighbor);
                    const cycle = path.slice(cycleStart);
                    cycle.push(neighbor); // Complete the cycle
                    return cycle;
                }
                if (neighborState === 'unvisited') {
                    const result = dfs(neighbor, path);
                    if (result)
                        return result;
                }
            }
            state.set(node, 'visited');
            path.pop();
            return null;
        };
        for (const id of stepIds) {
            if (state.get(id) === 'unvisited') {
                const cycle = dfs(id, []);
                if (cycle)
                    return cycle;
            }
        }
        // Shouldn't reach here if there's actually a cycle
        return ['unknown'];
    }
    /**
     * Get execution levels for parallel execution
     * Steps in the same level have no dependencies between them
     */
    getExecutionLevels(stepIds, dependencies) {
        if (stepIds.length === 0) {
            return [];
        }
        // Get topological order first (validates no cycles)
        const order = this.getExecutionOrder(stepIds, dependencies);
        // Calculate level for each node
        const levels = new Map();
        for (const id of order) {
            const deps = dependencies[id] ?? [];
            if (deps.length === 0) {
                levels.set(id, 0);
            }
            else {
                // Level is max of dependency levels + 1
                const maxDepLevel = Math.max(...deps.map(dep => levels.get(dep) ?? 0));
                levels.set(id, maxDepLevel + 1);
            }
        }
        // Group by level
        const maxLevel = Math.max(...levels.values());
        const result = [];
        for (let i = 0; i <= maxLevel; i++) {
            const levelNodes = order.filter(id => levels.get(id) === i);
            if (levelNodes.length > 0) {
                result.push(levelNodes);
            }
        }
        return result;
    }
    /**
     * Build reverse dependency map (step -> steps that depend on it)
     */
    buildReverseDependencyMap(stepIds, dependencies) {
        const reverse = {};
        // Initialize all steps
        for (const id of stepIds) {
            reverse[id] = [];
        }
        // Build reverse map
        for (const [stepId, deps] of Object.entries(dependencies)) {
            for (const dep of deps) {
                if (reverse[dep]) {
                    reverse[dep].push(stepId);
                }
            }
        }
        return reverse;
    }
    /**
     * Get all transitive dependencies of a step
     */
    getTransitiveDependencies(stepId, dependencies) {
        const result = new Set();
        const queue = [...(dependencies[stepId] ?? [])];
        const visited = new Set();
        while (queue.length > 0) {
            const current = queue.shift();
            if (visited.has(current))
                continue;
            visited.add(current);
            result.add(current);
            for (const dep of dependencies[current] ?? []) {
                if (!visited.has(dep)) {
                    queue.push(dep);
                }
            }
        }
        return result;
    }
    /**
     * Get all transitive dependents of a step (steps that depend on it)
     */
    getTransitiveDependents(stepId, stepIds, dependencies) {
        const reverse = this.buildReverseDependencyMap(stepIds, dependencies);
        const result = new Set();
        const queue = [...(reverse[stepId] ?? [])];
        const visited = new Set();
        while (queue.length > 0) {
            const current = queue.shift();
            if (visited.has(current))
                continue;
            visited.add(current);
            result.add(current);
            for (const dep of reverse[current] ?? []) {
                if (!visited.has(dep)) {
                    queue.push(dep);
                }
            }
        }
        return result;
    }
    /**
     * Check if adding a dependency would create a cycle
     */
    wouldCreateCycle(stepId, newDependency, stepIds, dependencies) {
        // A cycle would be created if newDependency transitively depends on stepId
        const transitiveDeps = this.getTransitiveDependencies(newDependency, dependencies);
        return transitiveDeps.has(stepId);
    }
    /**
     * Get the critical path (longest path through the graph)
     * Returns step IDs in the critical path and total duration
     */
    getCriticalPath(steps, dependencies) {
        if (steps.length === 0) {
            return { path: [], duration: 0 };
        }
        const order = this.getExecutionOrder(steps.map(s => s.id), dependencies);
        const durations = new Map();
        for (const step of steps) {
            durations.set(step.id, step.estimatedDuration ?? 0);
        }
        // Calculate longest path to each node
        const longestPath = new Map();
        const predecessor = new Map();
        for (const id of order) {
            predecessor.set(id, null);
            const deps = dependencies[id] ?? [];
            if (deps.length === 0) {
                longestPath.set(id, durations.get(id));
            }
            else {
                let maxPrev = 0;
                let maxPredecessor = null;
                for (const dep of deps) {
                    const depPath = longestPath.get(dep) ?? 0;
                    if (depPath > maxPrev) {
                        maxPrev = depPath;
                        maxPredecessor = dep;
                    }
                }
                longestPath.set(id, maxPrev + durations.get(id));
                predecessor.set(id, maxPredecessor);
            }
        }
        // Find the node with longest path
        let maxNode = order[0];
        let maxDuration = longestPath.get(maxNode) ?? 0;
        for (const id of order) {
            const pathLen = longestPath.get(id) ?? 0;
            if (pathLen > maxDuration) {
                maxDuration = pathLen;
                maxNode = id;
            }
        }
        // Reconstruct path
        const path = [];
        let current = maxNode;
        while (current) {
            path.unshift(current);
            current = predecessor.get(current) ?? null;
        }
        return { path, duration: maxDuration };
    }
}
/**
 * Create a new dependency resolver instance
 */
export function createDependencyResolver() {
    return new DependencyResolver();
}
//# sourceMappingURL=dependency.js.map