import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const plan = JSON.parse(await readFile(resolve(root, 'config/freecad-execution-plan.json'), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD execution plan: ${message}`) }
const requireText = (value, label) => { if (typeof value !== 'string' || !value.trim()) fail(`${label} must be a non-empty string.`) }
const requireTextList = (value, label) => {
  if (!Array.isArray(value) || value.length === 0) fail(`${label} must be a non-empty array.`)
  value.forEach((entry, index) => requireText(entry, `${label}[${index}]`))
}
const uniqueIds = (items, label) => {
  if (!Array.isArray(items) || items.length === 0) fail(`${label} must be a non-empty array.`)
  const ids = new Set()
  for (const item of items) {
    requireText(item?.id, `${label} id`)
    if (ids.has(item.id)) fail(`duplicate ${label} id '${item.id}'.`)
    ids.add(item.id)
  }
  return ids
}

if (plan.schemaVersion !== 1) fail(`unsupported schemaVersion '${plan.schemaVersion}'.`)
if (plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.freecadCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('baseline does not match the locked FreeCAD reference.')

const laneIds = uniqueIds(plan.lanes, 'lane')
const gateIds = uniqueIds(plan.gates, 'gate')
const programIds = uniqueIds(plan.programs, 'program')
const requiredPrograms = Array.from({ length: 10 }, (_, index) => `P${String(index + 1).padStart(2, '0')}`)
if (requiredPrograms.some((id) => !programIds.has(id)) || plan.programs.length !== requiredPrograms.length) fail('programs must contain exactly P01 through P10.')

const taskById = new Map()
const taskProgram = new Map()
const statuses = new Set(['pending', 'in_progress', 'completed', 'blocked'])
for (const program of plan.programs) {
  requireText(program.title, `${program.id}.title`)
  if (!laneIds.has(program.ownerLane)) fail(`${program.id} uses unknown owner lane '${program.ownerLane}'.`)
  if (!gateIds.has(program.gate)) fail(`${program.id} uses unknown gate '${program.gate}'.`)
  if (!Array.isArray(program.dependencies)) fail(`${program.id}.dependencies must be an array.`)
  for (const dependency of program.dependencies) if (!programIds.has(dependency)) fail(`${program.id} depends on unknown program '${dependency}'.`)
  if (!Array.isArray(program.tasks) || program.tasks.length === 0) fail(`${program.id} must contain tasks.`)
  for (const task of program.tasks) {
    requireText(task.id, `${program.id} task id`)
    if (taskById.has(task.id)) fail(`duplicate task id '${task.id}'.`)
    requireText(task.title, `${task.id}.title`)
    if (!statuses.has(task.status)) fail(`${task.id} has invalid status '${task.status}'.`)
    if (!Array.isArray(task.dependencies)) fail(`${task.id}.dependencies must be an array.`)
    requireTextList(task.deliverables, `${task.id}.deliverables`)
    requireTextList(task.acceptance, `${task.id}.acceptance`)
    taskById.set(task.id, task)
    taskProgram.set(task.id, program.id)
  }
}

for (const task of taskById.values()) for (const dependency of task.dependencies) if (!taskById.has(dependency)) fail(`${task.id} depends on unknown task '${dependency}'.`)

const visitState = new Map()
const visit = (taskId, path = []) => {
  const state = visitState.get(taskId)
  if (state === 'done') return
  if (state === 'visiting') fail(`task dependency cycle: ${[...path, taskId].join(' -> ')}.`)
  visitState.set(taskId, 'visiting')
  for (const dependency of taskById.get(taskId).dependencies) visit(dependency, [...path, taskId])
  visitState.set(taskId, 'done')
}
for (const taskId of taskById.keys()) visit(taskId)

for (const gate of plan.gates) {
  requireText(gate.title, `${gate.id}.title`)
  requireTextList(gate.requiredPrograms, `${gate.id}.requiredPrograms`)
  for (const programId of gate.requiredPrograms) if (!programIds.has(programId)) fail(`${gate.id} references unknown program '${programId}'.`)
}
for (const program of plan.programs) {
  const gate = plan.gates.find((candidate) => candidate.id === program.gate)
  if (!gate.requiredPrograms.includes(program.id)) fail(`${program.gate} does not require ${program.id}.`)
}

requireTextList(plan.rules, 'rules')
const statusCounts = Object.fromEntries([...statuses].map((status) => [status, [...taskById.values()].filter((task) => task.status === status).length]))
console.log(JSON.stringify({ status: 'execution-plan-pass', programs: plan.programs.length, lanes: plan.lanes.length, gates: plan.gates.length, tasks: taskById.size, statusCounts, taskPrograms: new Set(taskProgram.values()).size }, null, 2))
