/**
 * wfrun/dag.mjs — Topological wave grouping + cycle detection for C17 workflows.
 *
 * resolveWaves(nodes) returns `{ok: true, waves}` (Node[][] — wave = set of
 * nodes with all deps satisfied by earlier waves) or `{ok: false, cycle}` when
 * the graph is cyclic. Nodes within a wave are sorted by `id` for determinism.
 *
 * detectNodeCycle(nodes) is exposed for direct use — it mirrors
 * src/cli/graph/depends-on.mjs detectDependsOnCycle but walks a node-id
 * adjacency list (no task_id scoping).
 */

/**
 * Build a Map<id, string[]> of forward edges (id → deps it waits on).
 * @param {Array<{id: string, depends_on?: string[]}>} nodes
 */
function buildAdj(nodes) {
	const adj = new Map()
	for (const node of nodes) {
		adj.set(node.id, [...(node.depends_on ?? [])])
	}
	return adj
}

/**
 * DFS-based cycle detector. Returns the offending path (array of ids ending
 * at the start of the cycle) or `null` if the graph is acyclic.
 * @param {Array<{id: string, depends_on?: string[]}>} nodes
 * @returns {string[] | null}
 */
export function detectNodeCycle(nodes) {
	const adj = buildAdj(nodes)
	const WHITE = 0
	const GRAY = 1
	const BLACK = 2
	const color = new Map()
	for (const id of adj.keys()) color.set(id, WHITE)

	const stack = []
	let cycle = null

	function visit(u) {
		if (cycle) return
		color.set(u, GRAY)
		stack.push(u)
		for (const v of adj.get(u) ?? []) {
			if (cycle) return
			const c = color.get(v)
			if (c === GRAY) {
				// v is on current stack → cycle from v to current u then back to v
				const idx = stack.indexOf(v)
				cycle = [...stack.slice(idx), v]
				return
			}
			if (c === WHITE) visit(v)
		}
		stack.pop()
		color.set(u, BLACK)
	}

	// Iterate ids in stable order for deterministic cycle reporting.
	const ids = [...adj.keys()].sort()
	for (const id of ids) {
		if (cycle) break
		if (color.get(id) === WHITE) visit(id)
	}
	return cycle
}

/**
 * Kahn's algorithm wave grouping. Each wave is the set of nodes whose deps
 * are entirely satisfied by earlier waves. Within a wave, nodes are sorted
 * by `id`.
 * @param {Array<{id: string, depends_on?: string[]}>} nodes
 * @returns {{ok: true, waves: any[][]} | {ok: false, cycle: string[]}}
 */
export function resolveWaves(nodes) {
	const cycle = detectNodeCycle(nodes)
	if (cycle) return { ok: false, cycle }

	const byId = new Map(nodes.map((n) => [n.id, n]))
	const remainingDeps = new Map()
	for (const n of nodes) {
		remainingDeps.set(n.id, new Set(n.depends_on ?? []))
	}

	const waves = []
	const placed = new Set()
	while (placed.size < nodes.length) {
		const ready = []
		for (const n of nodes) {
			if (placed.has(n.id)) continue
			const deps = remainingDeps.get(n.id)
			let allPlaced = true
			for (const d of deps) {
				if (!placed.has(d)) {
					allPlaced = false
					break
				}
			}
			if (allPlaced) ready.push(n)
		}
		if (ready.length === 0) {
			// Should be unreachable — cycle detected above.
			return { ok: false, cycle: [...nodes.filter((n) => !placed.has(n.id)).map((n) => n.id)] }
		}
		ready.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
		waves.push(ready.map((n) => byId.get(n.id)))
		for (const n of ready) placed.add(n.id)
	}
	return { ok: true, waves }
}
