import { DslNode, DslEdge, WorkflowDSL } from './types';

/**
 * DAG representation of a workflow DSL.
 * Provides topological ordering and graph traversal utilities.
 */
export interface GraphNode {
  node: DslNode;
  inEdges: DslEdge[];
  outEdges: DslEdge[];
}

export interface ExecutionLevel {
  nodes: DslNode[];
}

/**
 * Build a graph (adjacency list) from DSL nodes + edges.
 */
export function buildGraph(dsl: WorkflowDSL): Map<string, GraphNode> {
  const graph = new Map<string, GraphNode>();

  for (const node of dsl.nodes) {
    graph.set(node.id, { node, inEdges: [], outEdges: [] });
  }

  for (const edge of dsl.edges) {
    const sourceNode = graph.get(edge.source);
    const targetNode = graph.get(edge.target);
    if (sourceNode) sourceNode.outEdges.push(edge);
    if (targetNode) targetNode.inEdges.push(edge);
  }

  return graph;
}

/**
 * Find trigger nodes (nodes with category-type starting with *_trigger or no incoming edges).
 */
export function findTriggerNodes(dsl: WorkflowDSL): DslNode[] {
  const graph = buildGraph(dsl);
  const triggers: DslNode[] = [];

  for (const [, gn] of graph) {
    if (
      gn.node.type.endsWith('_trigger') ||
      gn.inEdges.length === 0
    ) {
      triggers.push(gn.node);
    }
  }

  return triggers;
}

/**
 * Get successor node IDs from a given node, optionally filtering by sourceHandle (for branches).
 */
export function getSuccessors(
  dsl: WorkflowDSL,
  nodeId: string,
  branchHandle?: string,
): string[] {
  return dsl.edges
    .filter((e) => {
      if (e.source !== nodeId) return false;
      if (branchHandle !== undefined) return e.sourceHandle === branchHandle;
      return true;
    })
    .map((e) => e.target);
}

/**
 * Topological sort using Kahn's algorithm.
 * Returns nodes in execution order. Nodes at the same "level" can be executed in parallel.
 */
export function topologicalSort(dsl: WorkflowDSL, startNodeIds: string[]): ExecutionLevel[] {
  const graph = buildGraph(dsl);
  const levels: ExecutionLevel[] = [];

  // Calculate in-degree (only count edges from nodes that are reachable from start)
  const reachable = getReachableNodes(dsl, startNodeIds);
  const inDegree = new Map<string, number>();

  for (const id of reachable) {
    inDegree.set(id, 0);
  }

  for (const edge of dsl.edges) {
    if (reachable.has(edge.source) && reachable.has(edge.target)) {
      inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
    }
  }

  // Start with provided start nodes (triggers already fired)
  let currentLevel = startNodeIds.filter((id) => reachable.has(id));

  while (currentLevel.length > 0) {
    const levelNodes: DslNode[] = [];
    const nextLevel: string[] = [];

    for (const nodeId of currentLevel) {
      const gn = graph.get(nodeId);
      if (gn) levelNodes.push(gn.node);

      // Decrease in-degree of successors
      const successors = dsl.edges
        .filter((e) => e.source === nodeId && reachable.has(e.target))
        .map((e) => e.target);

      for (const succ of successors) {
        const deg = (inDegree.get(succ) || 0) - 1;
        inDegree.set(succ, deg);
        if (deg === 0) {
          nextLevel.push(succ);
        }
      }
    }

    if (levelNodes.length > 0) {
      levels.push({ nodes: levelNodes });
    }

    currentLevel = [...new Set(nextLevel)];
  }

  return levels;
}

/**
 * Get all node IDs reachable from a set of start nodes via BFS.
 */
export function getReachableNodes(dsl: WorkflowDSL, startNodeIds: string[]): Set<string> {
  const visited = new Set<string>();
  const queue = [...startNodeIds];

  while (queue.length > 0) {
    const current = queue.shift()!;
    if (visited.has(current)) continue;
    visited.add(current);

    const successors = dsl.edges
      .filter((e) => e.source === current)
      .map((e) => e.target);

    for (const succ of successors) {
      if (!visited.has(succ)) queue.push(succ);
    }
  }

  return visited;
}

/**
 * Get the sub-graph (nodes + edges) reachable from given start nodes,
 * stopping at a set of boundary nodes.
 */
export function getSubGraph(
  dsl: WorkflowDSL,
  startNodeIds: string[],
  stopNodeIds: Set<string> = new Set(),
): WorkflowDSL {
  const visited = new Set<string>();
  const queue = [...startNodeIds];

  while (queue.length > 0) {
    const current = queue.shift()!;
    if (visited.has(current) || stopNodeIds.has(current)) continue;
    visited.add(current);

    const successors = dsl.edges
      .filter((e) => e.source === current)
      .map((e) => e.target);

    for (const succ of successors) {
      if (!visited.has(succ) && !stopNodeIds.has(succ)) queue.push(succ);
    }
  }

  return {
    nodes: dsl.nodes.filter((n) => visited.has(n.id)),
    edges: dsl.edges.filter((e) => visited.has(e.source) && visited.has(e.target)),
  };
}
