/**
 * commands/wave.mjs — wave analyzer (C29).
 *
 * FRAMEWORK-BUG-033 ID-03 — accepts two input forms at the I/O boundary:
 *   1. Subtask-shaped plan (`{subtasks: [...]}`) — back-compat, byte-equivalent.
 *   2. Task-shaped plan (`{unit_kind: "task", tasks: [...]}`) — new fan-out
 *      input. Internal algorithm is unit-agnostic; only the input
 *      normalization + output envelope keys differ.
 */

import { existsSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { err, exitWith, ok } from '../util/result.mjs'
import { normalizeToUnits } from '../wfrun/unit-shape.mjs'

export function cmdWave(subcommand, projectDir, extraArgs) {
	if (!subcommand)
		return err('Usage: apt-tools wave <analyze> <project-dir> [--plan <path>] [--wave <N>]')

	switch (subcommand) {
		case 'analyze': {
			if (!projectDir)
				return err('Usage: apt-tools wave analyze <project-dir> [--plan <path>] [--wave <N>]')
			const targetDir = resolve(projectDir)

			// Determine plan path
			const planIdx = extraArgs.indexOf('--plan')
			const planPath =
				planIdx >= 0 && extraArgs[planIdx + 1]
					? resolve(extraArgs[planIdx + 1])
					: join(targetDir, 'implementation_plan.json')

			// Parse --wave N filter
			const waveIdx = extraArgs.indexOf('--wave')
			const waveFilter =
				waveIdx >= 0 && extraArgs[waveIdx + 1] ? parseInt(extraArgs[waveIdx + 1], 10) : null

			// Read and validate plan JSON (T-11-05: validate structure)
			if (!existsSync(planPath)) return err(`Plan file not found: ${planPath}`)
			let plan
			try {
				plan = JSON.parse(readFileSync(planPath, 'utf-8'))
			} catch (e) {
				return err(`Invalid JSON in plan file: ${e.message}`)
			}

			// FRAMEWORK-BUG-033 ID-03 — normalize subtask/task input shapes.
			const { units, unit_kind, error: shapeError } = normalizeToUnits(plan)
			if (shapeError) return err(shapeError)
			const isTaskUnit = unit_kind === 'task'

			// Validate each unit has required fields
			for (const u of units) {
				if (!u.id) return err(`${unit_kind} missing "id" field: ${JSON.stringify(u)}`)
				if (!Array.isArray(u.dependencies)) {
					return err(`${unit_kind} "${u.id}" missing "dependencies" array`)
				}
			}

			const unitMap = new Map()
			for (const u of units) {
				unitMap.set(u.id, u)
			}

			// Validate all dependencies reference existing units
			for (const u of units) {
				for (const dep of u.dependencies) {
					if (!unitMap.has(dep)) {
						return err(`${unit_kind} "${u.id}" depends on unknown ${unit_kind} "${dep}"`)
					}
				}
			}

			// Detect cycles using DFS with coloring (T-11-06: prevents infinite loops)
			const WHITE = 0,
				GRAY = 1,
				BLACK = 2
			const color = new Map()
			for (const u of units) color.set(u.id, WHITE)
			let cycleFound = null

			function dfs(id, path) {
				if (cycleFound) return
				color.set(id, GRAY)
				path.push(id)
				const u = unitMap.get(id)
				for (const dep of u.dependencies) {
					if (color.get(dep) === GRAY) {
						// Found cycle: extract from dep to current position
						const cycleStart = path.indexOf(dep)
						cycleFound = path.slice(cycleStart).concat(dep)
						return
					}
					if (color.get(dep) === WHITE) {
						dfs(dep, path)
					}
				}
				path.pop()
				color.set(id, BLACK)
			}

			for (const u of units) {
				if (color.get(u.id) === WHITE) {
					dfs(u.id, [])
				}
				if (cycleFound) break
			}

			if (cycleFound) {
				return exitWith({ error: 'Circular dependency detected', cycle: cycleFound }, 1)
			}

			// Wave assignment algorithm (D-12, D-13)
			const waveAssignment = new Map() // unit id -> wave number
			const assigned = new Set()

			// Wave 1: units with no dependencies
			for (const u of units) {
				if (u.dependencies.length === 0) {
					waveAssignment.set(u.id, 1)
					assigned.add(u.id)
				}
			}

			// Iteratively assign remaining units
			let currentWave = 1
			let progress = true
			while (assigned.size < units.length && progress) {
				progress = false
				currentWave++
				for (const u of units) {
					if (assigned.has(u.id)) continue
					// Check if ALL dependencies are assigned
					const allDepsAssigned = u.dependencies.every((dep) => assigned.has(dep))
					if (allDepsAssigned) {
						const maxDepWave = Math.max(...u.dependencies.map((dep) => waveAssignment.get(dep)))
						waveAssignment.set(u.id, maxDepWave + 1)
						assigned.add(u.id)
						progress = true
					}
				}
			}

			if (assigned.size < units.length) {
				const unassigned = units.filter((u) => !assigned.has(u.id)).map((u) => u.id)
				return err(
					`Could not assign all ${unit_kind}s to waves. Missing dependency or orphan ${unit_kind}s: ${unassigned.join(', ')}`,
				)
			}

			// Build wave groups
			const totalWaves = Math.max(...waveAssignment.values())
			const wavesMap = new Map()
			for (let w = 1; w <= totalWaves; w++) {
				// Output key per ID-03: subtask path keeps `subtasks`; task
				// path uses `tasks`. Bit-equivalence for the subtask path
				// is enforced by wave-engine-subtask-bit-equivalence.test.ts.
				wavesMap.set(
					w,
					isTaskUnit ? { wave: w, tasks: [], files: [] } : { wave: w, subtasks: [], files: [] },
				)
			}

			for (const u of units) {
				const w = waveAssignment.get(u.id)
				const group = wavesMap.get(w)
				if (isTaskUnit) group.tasks.push(u.id)
				else group.subtasks.push(u.id)
				if (Array.isArray(u.files)) {
					for (const f of u.files) {
						if (!group.files.includes(f)) {
							group.files.push(f)
						}
					}
				}
			}

			// File conflict check: files appearing in multiple units within same wave
			const fileConflicts = []
			for (const [w, group] of wavesMap) {
				const fileOwners = new Map()
				const groupIds = isTaskUnit ? group.tasks : group.subtasks
				for (const uId of groupIds) {
					const u = unitMap.get(uId)
					if (Array.isArray(u.files)) {
						for (const f of u.files) {
							if (!fileOwners.has(f)) fileOwners.set(f, [])
							fileOwners.get(f).push(uId)
						}
					}
				}
				for (const [file, owners] of fileOwners) {
					if (owners.length > 1) {
						fileConflicts.push(
							isTaskUnit ? { wave: w, file, tasks: owners } : { wave: w, file, subtasks: owners },
						)
					}
				}
			}

			// Build wave map. Subtask path: subtask_wave_map (back-compat).
			// Task path: unit_id_wave_map (new).
			const unitIdWaveMap = {}
			for (const [id, w] of waveAssignment) {
				unitIdWaveMap[id] = w
			}

			// Apply --wave N filter (D-16)
			let waves = Array.from(wavesMap.values())
			if (waveFilter !== null) {
				waves = waves.filter((w) => w.wave === waveFilter)
			}

			if (isTaskUnit) {
				return ok({
					status: 'ok',
					command: 'wave-analyze-task',
					unit_kind: 'task',
					total_waves: totalWaves,
					waves,
					file_conflicts: fileConflicts,
					unit_id_wave_map: unitIdWaveMap,
				})
			}

			// Back-compat subtask envelope — byte-equivalent to pre-refactor output.
			return ok({
				status: 'ok',
				command: 'wave-analyze',
				total_waves: totalWaves,
				waves,
				file_conflicts: fileConflicts,
				subtask_wave_map: unitIdWaveMap,
			})
		}
		default:
			return err(`Unknown wave subcommand: ${subcommand}. Available: analyze`)
	}
}
