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

const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const [plan, sourceInventory, entrypointInventory, packageJson, compatibility, platformCoverage] = await Promise.all([
  load('config/freecad-web-exact-parity-plan.json'),
  load('config/freecad-source-inventory.json'),
  load('config/freecad-entrypoint-inventory.json'),
  load('package.json'),
  load('config/compatibility-matrix.json'),
  load('config/platform-module-coverage.json'),
])

const fail = (message) => { throw new Error(`FreeCAD Web exact-parity plan: ${message}`) }
const requireText = (value, label) => {
  if (typeof value !== 'string' || !value.trim()) fail(`${label} must be a non-empty string.`)
}
const requireTextList = (value, label, allowEmpty = false) => {
  if (!Array.isArray(value) || (!allowEmpty && value.length === 0)) fail(`${label} must be ${allowEmpty ? 'an' : 'a non-empty'} array.`)
  value.forEach((entry, index) => requireText(entry, `${label}[${index}]`))
}

const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
if (plan.schemaVersion !== 1 || plan.baseline?.freecadVersion !== '1.1.1' || plan.baseline?.freecadCommit !== lockedCommit) fail('baseline is not the locked FreeCAD 1.1.1 reference.')
if (sourceInventory.baseline?.commit !== lockedCommit || entrypointInventory.baseline?.commit !== lockedCommit || platformCoverage.baseline?.commit !== lockedCommit) fail('an input inventory uses a different FreeCAD baseline.')
if (plan.claim?.exact !== false) fail('system exact must remain false while exact-parity tasks are open.')
requireText(plan.claim?.scope, 'claim.scope')
requireText(plan.claim?.reason, 'claim.reason')

if (!Array.isArray(plan.hardBoundaries) || plan.hardBoundaries.length !== 3) fail('exactly three native-evidence hard boundaries are required.')
for (const [index, boundary] of plan.hardBoundaries.entries()) requireText(boundary, `hardBoundaries[${index}]`)
const boundaryText = plan.hardBoundaries.join(' ').toLowerCase()
for (const token of ['mappednameref', 'builder-stage', 'isomorphic']) if (!boundaryText.includes(token)) fail(`hard boundaries do not retain the ${token} rule.`)

const expectedProgramIds = Array.from({ length: 10 }, (_, index) => `EX${String(index + 1).padStart(2, '0')}`)
if (!Array.isArray(plan.programs) || JSON.stringify(plan.programs.map((program) => program.id)) !== JSON.stringify(expectedProgramIds)) fail('programs must contain ordered EX01 through EX10.')
const taskById = new Map()
const statuses = new Set(['pending', 'in_progress', 'completed', 'blocked'])
const priorities = new Set(['P0', 'P1', 'P2'])
for (const program of plan.programs) {
  requireText(program.title, `${program.id}.title`)
  if (!Array.isArray(program.tasks) || program.tasks.length === 0) fail(`${program.id}.tasks must be non-empty.`)
  for (const task of program.tasks) {
    requireText(task.id, `${program.id}.task.id`)
    requireText(task.title, `${task.id}.title`)
    if (taskById.has(task.id)) fail(`duplicate task ${task.id}.`)
    if (!priorities.has(task.priority)) fail(`${task.id} has invalid priority ${task.priority}.`)
    if (!statuses.has(task.status)) fail(`${task.id} has invalid status ${task.status}.`)
    requireTextList(task.deliverables, `${task.id}.deliverables`)
    requireTextList(task.acceptance, `${task.id}.acceptance`)
    requireTextList(task.evidence, `${task.id}.evidence`)
    requireTextList(task.exactBlockedBy, `${task.id}.exactBlockedBy`, task.status === 'completed')
    if (!Array.isArray(task.dependencies)) fail(`${task.id}.dependencies must be an array.`)
    if (task.status === 'completed' && task.exactBlockedBy.length !== 0) fail(`${task.id} is completed but still has exact blockers.`)
    if (task.status !== 'completed' && task.exactBlockedBy.length === 0) fail(`${task.id} is open without an explicit exact blocker.`)
    for (const evidence of task.evidence) if (!packageJson.scripts?.[evidence]) fail(`${task.id} references missing evidence script ${evidence}.`)
    taskById.set(task.id, task)
  }
}
if (taskById.size < 40) fail('the exact plan is not detailed enough to cover the full FreeCAD scope.')
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, chain = []) => {
  if (visitState.get(taskId) === 'done') return
  if (visitState.get(taskId) === 'visiting') fail(`task dependency cycle: ${[...chain, taskId].join(' -> ')}.`)
  visitState.set(taskId, 'visiting')
  for (const dependency of taskById.get(taskId).dependencies) visit(dependency, [...chain, taskId])
  visitState.set(taskId, 'done')
}
for (const taskId of taskById.keys()) visit(taskId)

const validateCoverage = (actual, expected, label) => {
  const actualIds = Object.keys(actual ?? {}).sort()
  const expectedIds = [...expected].sort()
  if (JSON.stringify(actualIds) !== JSON.stringify(expectedIds)) fail(`${label} coverage does not match the locked inventory.`)
  for (const [id, taskIds] of Object.entries(actual)) {
    if (!Array.isArray(taskIds) || taskIds.length === 0) fail(`${label} ${id} has no task coverage.`)
    for (const taskId of taskIds) if (!taskById.has(taskId)) fail(`${label} ${id} references unknown task ${taskId}.`)
  }
}
validateCoverage(plan.moduleCoverage, sourceInventory.modules.map((module) => module.name), 'module')
validateCoverage(plan.entrypointCoverage?.formats, entrypointInventory.formats.map((format) => format.id), 'format')
validateCoverage(plan.entrypointCoverage?.entrypoints, entrypointInventory.entrypoints.map((entrypoint) => entrypoint.id), 'entrypoint')

const finalTask = taskById.get(plan.promotionGate?.finalTask)
if (!finalTask || finalTask.id !== 'EX-REL-01' || finalTask.status === 'completed') fail('EX-REL-01 must remain open until all exact tasks close.')
if (plan.promotionGate?.requiredExactModules !== sourceInventory.modules.length) fail('promotion gate must require every locked module to be exact.')
requireTextList(plan.promotionGate?.requiredZeroMetrics, 'promotionGate.requiredZeroMetrics')
requireTextList(plan.promotionGate?.requiredEvidenceClasses, 'promotionGate.requiredEvidenceClasses')
if (new Set(plan.promotionGate.requiredZeroMetrics).size !== plan.promotionGate.requiredZeroMetrics.length) fail('promotion metrics must be unique.')
if (new Set(plan.promotionGate.requiredEvidenceClasses).size !== plan.promotionGate.requiredEvidenceClasses.length) fail('promotion evidence classes must be unique.')

const systemEvaluation = compatibility.systemExactEvaluation
if (systemEvaluation?.exact !== false || systemEvaluation?.featureExactCount !== 0 || !Array.isArray(systemEvaluation.blockers) || systemEvaluation.blockers.length !== 2) fail('compatibility matrix must retain the current non-exact evaluation and two remaining exact blockers.')
if (platformCoverage.modules.some((module) => module.level === 'exact')) fail('platform coverage cannot contain exact modules before their exact tasks complete.')

const tasks = [...taskById.values()]
const statusCounts = Object.fromEntries([...statuses].map((status) => [status, tasks.filter((task) => task.status === status).length]))
const priorityCounts = Object.fromEntries([...priorities].map((priority) => [priority, tasks.filter((task) => task.priority === priority).length]))
const matrixTaskStatus = systemEvaluation.taskStatus
if (systemEvaluation.plan !== 'config/freecad-web-exact-parity-plan.json' || systemEvaluation.promotionTask !== finalTask.id) fail('compatibility matrix does not point at the exact-parity plan and final promotion task.')
if (matrixTaskStatus?.completed !== statusCounts.completed || matrixTaskStatus?.inProgress !== statusCounts.in_progress || matrixTaskStatus?.pending !== statusCounts.pending || matrixTaskStatus?.blocked !== statusCounts.blocked) fail('compatibility matrix exact task counts are stale.')
console.log(JSON.stringify({
  status: 'freecad-web-exact-parity-plan-pass',
  systemExact: false,
  programs: plan.programs.length,
  tasks: tasks.length,
  statusCounts,
  priorityCounts,
  modules: Object.keys(plan.moduleCoverage).length,
  formats: Object.keys(plan.entrypointCoverage.formats).length,
  entrypoints: Object.keys(plan.entrypointCoverage.entrypoints).length,
  hardBoundaries: plan.hardBoundaries.length,
  finalGate: finalTask.status,
}, null, 2))
