/**
 * DSL graph structural validation.
 *
 * Used by WorkflowService.publish() to reject malformed or cyclic DSLs before
 * they become a published version. The worker's executeGraph still has a runtime
 * iteration cap as defense-in-depth, but with this validation in place the cap
 * should never trip in practice.
 *
 * Validates:
 * 1. nodes / edges are present and well-typed
 * 2. every edge references existing nodes
 * 3. node ids are unique
 * 4. there is at least one trigger node
 * 5. graph is a DAG (Kahn's algorithm — no cycles)
 *
 * Throws Error with a human-readable message on the first failure;
 * caller should map to BadRequestException.
 */
export interface DslGraphLike {
  nodes?: Array<{ id?: string; type?: string }>;
  edges?: Array<{ source?: string; target?: string }>;
}

export function validateDslGraph(dsl: unknown): void {
  if (!dsl || typeof dsl !== 'object') {
    throw new Error('DSL must be an object');
  }
  const g = dsl as DslGraphLike;

  if (!Array.isArray(g.nodes)) {
    throw new Error('DSL.nodes must be an array');
  }
  if (!Array.isArray(g.edges)) {
    throw new Error('DSL.edges must be an array');
  }
  if (g.nodes.length === 0) {
    throw new Error('DSL must contain at least one node');
  }

  // 1. node ids unique + non-empty
  const idSet = new Set<string>();
  for (const n of g.nodes) {
    if (!n || typeof n.id !== 'string' || n.id.length === 0) {
      throw new Error('Every node must have a non-empty string id');
    }
    if (idSet.has(n.id)) {
      throw new Error(`Duplicate node id: "${n.id}"`);
    }
    idSet.add(n.id);
    if (typeof n.type !== 'string' || n.type.length === 0) {
      throw new Error(`Node "${n.id}" must have a non-empty string type`);
    }
  }

  // 2. at least one trigger node
  const hasTrigger = g.nodes.some((n) => n.type!.endsWith('_trigger'));
  if (!hasTrigger) {
    throw new Error('DSL must contain at least one *_trigger node');
  }

  // 3. edges reference existing nodes; no self-loops
  for (const e of g.edges) {
    if (!e || typeof e.source !== 'string' || typeof e.target !== 'string') {
      throw new Error('Every edge must have string source and target');
    }
    if (!idSet.has(e.source)) {
      throw new Error(`Edge references unknown source node "${e.source}"`);
    }
    if (!idSet.has(e.target)) {
      throw new Error(`Edge references unknown target node "${e.target}"`);
    }
    if (e.source === e.target) {
      throw new Error(`Self-loop detected on node "${e.source}"`);
    }
  }

  // 4. cycle detection — Kahn's algorithm.
  // 注意：parallel/for_each 节点的"分支"在 worker 侧也是普通的 edge，因此用普通 DAG 校验即可。
  const indegree = new Map<string, number>();
  const adj = new Map<string, string[]>();
  for (const id of idSet) {
    indegree.set(id, 0);
    adj.set(id, []);
  }
  for (const e of g.edges) {
    indegree.set(e.target!, indegree.get(e.target!)! + 1);
    adj.get(e.source!)!.push(e.target!);
  }

  const queue: string[] = [];
  for (const [id, deg] of indegree) {
    if (deg === 0) queue.push(id);
  }

  let visited = 0;
  while (queue.length > 0) {
    const id = queue.shift()!;
    visited++;
    for (const next of adj.get(id) || []) {
      const d = indegree.get(next)! - 1;
      indegree.set(next, d);
      if (d === 0) queue.push(next);
    }
  }

  if (visited !== idSet.size) {
    const stuck = [...indegree.entries()]
      .filter(([, d]) => d > 0)
      .map(([id]) => id);
    throw new Error(
      `DSL contains a cycle. Nodes still in cycle or downstream of one: [${stuck.join(
        ', ',
      )}]`,
    );
  }
}
