/**
 * Directed Acyclic Graph (DAG) utilities
 * Used to analyze and order workflow steps based on dependencies
 */

import { Step } from '../types/workflow';

export interface DAGNode {
  id: string;
  step: Step;
  dependencies: string[];
  dependents: string[];
}

export class DAG {
  private nodes: Map<string, DAGNode>;

  constructor() {
    this.nodes = new Map();
  }

  /**
   * Add a step to the DAG
   */
  addStep(step: Step): void {
    if (this.nodes.has(step.id)) {
      throw new Error(`Duplicate step ID: ${step.id}`);
    }

    const node: DAGNode = {
      id: step.id,
      step,
      dependencies: step.depends_on || [],
      dependents: [],
    };

    this.nodes.set(step.id, node);
  }

  /**
   * Build the complete DAG from steps
   */
  build(steps: Step[]): void {
    // First pass: add all nodes
    for (const step of steps) {
      this.addStep(step);
    }

    // Second pass: build dependent relationships and validate dependencies
    for (const node of this.nodes.values()) {
      for (const depId of node.dependencies) {
        const depNode = this.nodes.get(depId);
        if (!depNode) {
          throw new Error(
            `Step "${node.id}" depends on unknown step "${depId}"`
          );
        }
        depNode.dependents.push(node.id);
      }
    }

    // Validate no circular dependencies
    this.detectCycles();
  }

  /**
   * Detect circular dependencies using DFS
   */
  private detectCycles(): void {
    const visited = new Set<string>();
    const recursionStack = new Set<string>();

    const visit = (nodeId: string, path: string[]): void => {
      if (recursionStack.has(nodeId)) {
        const cycle = [...path, nodeId].join(' -> ');
        throw new Error(`Circular dependency detected: ${cycle}`);
      }

      if (visited.has(nodeId)) {
        return;
      }

      visited.add(nodeId);
      recursionStack.add(nodeId);

      const node = this.nodes.get(nodeId);
      if (node) {
        for (const depId of node.dependencies) {
          visit(depId, [...path, nodeId]);
        }
      }

      recursionStack.delete(nodeId);
    };

    for (const nodeId of this.nodes.keys()) {
      if (!visited.has(nodeId)) {
        visit(nodeId, []);
      }
    }
  }

  /**
   * Get topologically sorted steps (dependencies first)
   */
  topologicalSort(): Step[] {
    const sorted: Step[] = [];
    const visited = new Set<string>();
    const temp = new Set<string>();

    const visit = (nodeId: string): void => {
      if (visited.has(nodeId)) {
        return;
      }

      if (temp.has(nodeId)) {
        throw new Error(`Circular dependency detected at node: ${nodeId}`);
      }

      temp.add(nodeId);

      const node = this.nodes.get(nodeId);
      if (node) {
        // Visit dependencies first
        for (const depId of node.dependencies) {
          visit(depId);
        }

        temp.delete(nodeId);
        visited.add(nodeId);
        sorted.push(node.step);
      }
    };

    for (const nodeId of this.nodes.keys()) {
      if (!visited.has(nodeId)) {
        visit(nodeId);
      }
    }

    return sorted;
  }

  /**
   * Get steps that have no dependencies (entry points)
   */
  getEntryPoints(): Step[] {
    const entryPoints: Step[] = [];

    for (const node of this.nodes.values()) {
      if (node.dependencies.length === 0) {
        entryPoints.push(node.step);
      }
    }

    return entryPoints;
  }

  /**
   * Get steps that have no dependents (exit points)
   */
  getExitPoints(): Step[] {
    const exitPoints: Step[] = [];

    for (const node of this.nodes.values()) {
      if (node.dependents.length === 0) {
        exitPoints.push(node.step);
      }
    }

    return exitPoints;
  }

  /**
   * Get all steps that depend on a given step
   */
  getDependents(stepId: string): Step[] {
    const node = this.nodes.get(stepId);
    if (!node) {
      throw new Error(`Step not found: ${stepId}`);
    }

    return node.dependents
      .map(id => this.nodes.get(id)?.step)
      .filter((step): step is Step => step !== undefined);
  }

  /**
   * Get all steps that a given step depends on
   */
  getDependencies(stepId: string): Step[] {
    const node = this.nodes.get(stepId);
    if (!node) {
      throw new Error(`Step not found: ${stepId}`);
    }

    return node.dependencies
      .map(id => this.nodes.get(id)?.step)
      .filter((step): step is Step => step !== undefined);
  }

  /**
   * Get execution levels (steps that can run in parallel)
   */
  getExecutionLevels(): Step[][] {
    const levels: Step[][] = [];
    const visited = new Set<string>();
    const nodeMap = new Map(this.nodes);

    while (visited.size < this.nodes.size) {
      const currentLevel: Step[] = [];
      const currentLevelIds: string[] = [];

      // Find all nodes whose dependencies have been visited
      for (const [nodeId, node] of nodeMap.entries()) {
        if (visited.has(nodeId)) {
          continue;
        }

        const allDepsVisited = node.dependencies.every(depId =>
          visited.has(depId)
        );

        if (allDepsVisited) {
          currentLevel.push(node.step);
          currentLevelIds.push(nodeId);
        }
      }

      if (currentLevel.length === 0 && visited.size < this.nodes.size) {
        throw new Error('Unable to resolve execution order - possible circular dependency');
      }

      // Mark all nodes in this level as visited AFTER finding all of them
      for (const nodeId of currentLevelIds) {
        visited.add(nodeId);
      }

      if (currentLevel.length > 0) {
        levels.push(currentLevel);
      }
    }

    return levels;
  }

  /**
   * Get the node for a step
   */
  getNode(stepId: string): DAGNode | undefined {
    return this.nodes.get(stepId);
  }

  /**
   * Get all nodes
   */
  getAllNodes(): DAGNode[] {
    return Array.from(this.nodes.values());
  }
}
