import { createHash } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import { resolve } from 'node:path'
import { validateCompositeMutationEvidence } from './freecad-composite-mutation-evidence.mjs'

const root = resolve(new URL('..', import.meta.url).pathname)
const load = (path) => readFile(resolve(root, path), 'utf8').then(JSON.parse)
const fail = (message) => { throw new Error(`FreeCAD TSN stage correlation: ${message}`) }
const [desktop, production, productionRegistry, composite, resave, mutations, recoveredNaming, productionDriftClassification, orderedPairClassification] = await Promise.all([
  load('config/freecad-tsn-stage-correlation-oracle.json'),
  load('config/freecad-isomorphic-provenance-verification.json'),
  load('config/chrome-freecad-naming-production-verification.json'),
  load('config/freecad-composite-history-elementmap-oracle.json'),
  load('config/freecad-composite-history-resave-verification.json'),
  load('config/freecad-parameter-mutation-report.json'),
  load('config/freecad-recovered-naming-classification.json'),
  load('config/freecad-production-drift-classification.json'),
  load('config/freecad-ordered-operation-pair-classification.json'),
])
const operations = ['cut', 'rotate', 'fillet', 'mirrored', 'linear-pattern']
if (desktop.schemaVersion !== 1 || desktop.baselineId !== 'freecad-1.1.1-tsn-stage-correlation' || desktop.freecadVersion !== '1.1.1' || desktop.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' || desktop.status !== 'pass') fail('desktop oracle is not locked to FreeCAD 1.1.1.')
const harnessPath = resolve(root, desktop.harness?.path ?? '')
const [harnessContent, harnessBytes] = await Promise.all([readFile(harnessPath), stat(harnessPath).then(({ size }) => size)])
if (desktop.harness?.path !== 'scripts/freecad-tsn-stage-correlation-oracle.py' || desktop.harness.bytes !== harnessBytes || desktop.harness.sha256 !== createHash('sha256').update(harnessContent).digest('hex')) fail('desktop oracle is stale for its executable harness.')
if (JSON.stringify(desktop.operations) !== JSON.stringify(operations) || JSON.stringify(production.cases?.longChain?.operations) !== JSON.stringify(operations)) fail('desktop and production five-stage operation order differs.')
if (desktop.stages?.length !== 5 || desktop.reopenedStages?.length !== 5 || desktop.resavedStages?.length !== 5) fail('desktop save/reopen/resave stage count is incomplete.')
for (let index = 0; index < operations.length; index += 1) {
  const before = desktop.stages[index]
  const reopened = desktop.reopenedStages[index]
  const resaved = desktop.resavedStages[index]
  if (before.operation !== operations[index] || reopened.operation !== before.operation || resaved.operation !== before.operation || !before.shape?.valid || before.mappedNameCount < 1 || before.historyRelations < 1) fail(`desktop stage ${operations[index]} has no valid topology association.`)
  if (before.mappedNameDigest !== reopened.mappedNameDigest || before.mappedNameDigest !== resaved.mappedNameDigest) fail(`desktop stage ${operations[index]} changed mapped names after save/reopen/resave.`)
}
if (desktop.summary?.wrongBindings !== 0 || desktop.summary?.unexplainedRelations !== 0 || desktop.summary?.roundtripNameDrift !== 0 || desktop.summary?.resaveNameDrift !== 0) fail(`desktop zero-difference metrics failed: ${JSON.stringify(desktop.summary)}`)
if (desktop.nativeDesktopResaveCovered !== true || desktop.summary?.mutationCases !== 5 || desktop.summary?.mutationPassed !== 5 || desktop.mutations?.length !== 5) fail('desktop cross-feature mutation/resave corpus is incomplete.')
for (const [index, mutation] of desktop.mutations.entries()) if (mutation.operation !== operations[index] || mutation.status !== 'pass' || mutation.parameterChanged !== true || mutation.downstreamShapeChanged !== true || mutation.restoredExactly !== true) fail(`desktop ${operations[index]} mutation did not edit, recompute and restore the chain.`)
if (composite.summary?.cases !== 30 || composite.summary?.passed !== 30 || resave.summary?.nativeDesktopResaveCases !== 30 || resave.summary?.roundtripNameDrift !== 0 || resave.summary?.resaveNameDrift !== 0) fail('the locked 30-case native desktop resave corpus is incomplete.')
const compositeMutation = validateCompositeMutationEvidence(composite)
const driftCases = composite.cases.filter((fixture) => fixture.mutation.metrics.namingRestorationDriftStages > 0)
const driftById = new Map(driftCases.map((fixture) => [fixture.id, fixture]))
const recoveredNamingClassifications = recoveredNaming.classifications || []
if (recoveredNaming.baseline?.freecadVersion !== desktop.freecadVersion || recoveredNaming.baseline?.commit !== desktop.gitCommit || recoveredNamingClassifications.length !== new Set(recoveredNamingClassifications.map(({ caseId }) => caseId)).size) fail('recovered naming classifications have invalid baseline or duplicate cases.')
let classifiedNamingDriftStages = 0
for (const entry of recoveredNamingClassifications) {
  const fixture = driftById.get(entry.caseId)
  if (!fixture || !['stable_semantics', 'allowed_evolution', 'implementation_defect'].includes(entry.classification) || entry.replayStable !== true) fail(`recovered naming classification ${entry.caseId} is invalid.`)
  classifiedNamingDriftStages += fixture.mutation.metrics.namingRestorationDriftStages
}
const unclassifiedNamingDriftCases = compositeMutation.mutationNamingRestoreDriftCases - recoveredNamingClassifications.length
const unclassifiedNamingDriftStages = compositeMutation.mutationNamingRestoreDriftStages - classifiedNamingDriftStages
const resaveById = new Map((resave.cases || []).map((fixture) => [fixture.id, fixture]))
let compositeCaseReconciliations = 0
if (resaveById.size !== compositeMutation.mutationCases || resave.cases?.length !== compositeMutation.mutationCases) fail('the 30-case resave report is not joined one-to-one with mutation evidence.')
for (const fixture of composite.cases) {
  const resaveFixture = resaveById.get(fixture.id)
  if (!resaveFixture || resaveFixture.roundtripNameDrift !== 0 || resaveFixture.resaveNameDrift !== 0 || resaveFixture.nativeDesktopResaveCovered !== true) fail(`${fixture.id} lacks a passing native resave transaction.`)
  for (const phase of ['initial', 'reopened', 'resaved']) {
    const records = resaveFixture.stageCorrelations?.[phase]
    if (!Array.isArray(records) || records.length !== fixture.stages.length) fail(`${fixture.id}/${phase} lacks all stage correlations.`)
    for (const [ordinal, stage] of fixture.stages.entries()) {
      const record = records[ordinal]
      const key = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${stage.name}`
      if (record.key !== key || record.ordinal !== ordinal || record.name !== stage.name || record.typeId !== stage.typeId || record.relationDigest !== stage.relationDigest || record.semanticNameDigest !== stage.semanticNameDigest) fail(`${key}/${phase} changed its locked stage evidence.`)
    }
  }
  compositeCaseReconciliations += 1
}
if (compositeCaseReconciliations !== 30 || compositeMutation.mutationStageRecords !== 219 || compositeMutation.mutationStageRestoreFailures !== 0 || compositeMutation.mutationFinalPropagationFailures !== 0) fail('the 30-case mutation corpus is not fully reconciled.')

const mutationFamilyByOperation = new Map(mutations.families.flatMap((family) => (family.operationTypes || []).map((operation) => [operation, family])))
const nativeMutationFamilies = { cut: 'Part::Cut', fillet: 'Part::Fillet', mirrored: 'PartDesign::Mirrored', 'linear-pattern': 'PartDesign::LinearPattern' }
const nativeMutationCorrelation = operations.map((operation) => {
  const requiredFamily = nativeMutationFamilies[operation]
  const family = requiredFamily ? mutations.families.find(({ id }) => id === requiredFamily) : mutationFamilyByOperation.get(operation)
  return { operation, familyId: family?.id ?? null, complete: family?.complete === true }
})
const missingNativeMutationOperations = nativeMutationCorrelation.filter(({ complete }) => !complete).map(({ operation }) => operation)
const proxyStageOperations = desktop.stages.filter(({ nativeBuilder }) => nativeBuilder !== true).map(({ operation }) => operation)
if (JSON.stringify(proxyStageOperations) !== '["rotate","mirrored","linear-pattern"]' || desktop.summary?.nativeBuilderStages !== 2 || desktop.summary?.proxyStages !== 3 || desktop.exactCorrelationReady !== false) fail('desktop native-builder/proxy boundary drifted.')

const registeredOperations = productionRegistry.operations || []
const productionDriftMutations = desktop.mutations.filter(({ restoreTopologyDriftStages }) => restoreTopologyDriftStages > 0)
const productionDriftByOperation = new Map(productionDriftMutations.map((mutation) => [mutation.operation, mutation]))
const productionDriftClassifications = productionDriftClassification.classifications || []
if (productionDriftClassification.baseline?.freecadVersion !== desktop.freecadVersion || productionDriftClassification.baseline?.commit !== desktop.gitCommit || productionDriftClassifications.length !== new Set(productionDriftClassifications.map(({ operation }) => operation)).size) fail('production drift classifications have invalid baseline or duplicate operations.')
let classifiedProductionDriftStages = 0
for (const entry of productionDriftClassifications) {
  const mutation = productionDriftByOperation.get(entry.operation)
  if (!mutation || !['stable_semantics', 'allowed_evolution', 'implementation_defect'].includes(entry.classification) || entry.replayStable !== true) fail(`production drift classification ${entry.operation} is invalid.`)
  classifiedProductionDriftStages += mutation.restoreTopologyDriftStages
}
const unclassifiedProductionDriftCases = productionDriftMutations.length - productionDriftClassifications.length
const unclassifiedProductionDriftStages = desktop.summary.mutationRestoreTopologyDriftStages - classifiedProductionDriftStages
const orderedPairClassifications = orderedPairClassification.classifications || []
if (orderedPairClassification.baseline?.freecadVersion !== desktop.freecadVersion || orderedPairClassification.baseline?.commit !== desktop.gitCommit || orderedPairClassifications.length !== new Set(orderedPairClassifications.map(({ pair }) => pair)).size) fail('ordered operation pair classifications have invalid baseline or duplicate pairs.')
for (const entry of orderedPairClassifications) if (!['accepted', 'rejected'].includes(entry.classification) || entry.nativeDecision !== entry.classification) fail(`ordered operation pair classification ${entry.pair} is invalid.`)
const coveredTransitions = [...new Set([...operations.slice(1).map((operation, index) => `${operations[index]}->${operation}`), ...orderedPairClassifications.map(({ pair }) => pair)])]
const orderedOperationPairs = registeredOperations.length * registeredOperations.length
const blockers = [
  `Native desktop builder stages missing from the sequential chain: ${proxyStageOperations.join(', ')}`,
  `Native parameter-mutation family missing for production operation: ${missingNativeMutationOperations.join(', ')}`,
  `${unclassifiedProductionDriftCases}/${productionDriftMutations.length} production restore-topology cases and ${unclassifiedProductionDriftStages}/${desktop.summary.mutationRestoreTopologyDriftStages} downstream stages remain unclassified; ${productionDriftClassifications.length} ${productionDriftClassifications.length === 1 ? 'case is' : 'cases are'} classified from an independent native replay`,
  `${unclassifiedNamingDriftCases}/${compositeMutation.mutationNamingRestoreDriftCases} native naming-drift cases and ${unclassifiedNamingDriftStages}/${compositeMutation.mutationNamingRestoreDriftStages} recovered naming stages remain unclassified; ${recoveredNamingClassifications.length} ${recoveredNamingClassifications.length === 1 ? 'case is' : 'cases are'} classified from an independent native replay`,
  `Only ${coveredTransitions.length}/${orderedOperationPairs} ordered production operation pairs are classified and replayed; ${orderedOperationPairs - coveredTransitions.length} remain unclassified for type compatibility`,
]
console.log(JSON.stringify({
  status: 'freecad-tsn-stage-correlation-fail-closed',
  exactPromotionReady: false,
  productionStages: production.cases.longChain.stages.length,
  desktopStages: desktop.summary.stages,
  nativeBuilderStages: desktop.summary.nativeBuilderStages,
  proxyStageOperations,
  compositeMutations: { cases: compositeMutation.mutationCases, passed: compositeMutation.mutationPassed, caseReconciliations: compositeCaseReconciliations, categoryCases: compositeMutation.categoryCases, stageRecords: compositeMutation.mutationStageRecords, phaseStageRecords: compositeMutation.mutationPhaseStageRecords, finalPropagationFailures: compositeMutation.mutationFinalPropagationFailures, geometryRestoreFailures: compositeMutation.mutationStageRestoreFailures, namingRestoreDriftCases: compositeMutation.mutationNamingRestoreDriftCases, namingRestoreDriftStages: compositeMutation.mutationNamingRestoreDriftStages, classifiedNamingDriftCases: recoveredNamingClassifications.length, classifiedNamingDriftStages, unclassifiedNamingDriftCases, unclassifiedNamingDriftStages },
  mutations: { cases: desktop.summary.mutationCases, passed: desktop.summary.mutationPassed, nativeCorrelated: nativeMutationCorrelation.filter(({ complete }) => complete).length, missingNativeMutationOperations, restoreTopologyDriftCases: desktop.summary.mutationRestoreTopologyDriftCases, restoreTopologyDriftStages: desktop.summary.mutationRestoreTopologyDriftStages, classifiedRestoreTopologyDriftCases: productionDriftClassifications.length, classifiedRestoreTopologyDriftStages: classifiedProductionDriftStages, unclassifiedRestoreTopologyDriftCases: unclassifiedProductionDriftCases, unclassifiedRestoreTopologyDriftStages: unclassifiedProductionDriftStages },
  transitions: { registeredOperations: registeredOperations.length, orderedOperationPairs, covered: coveredTransitions.length, unclassified: orderedOperationPairs - coveredTransitions.length, coveredTransitions },
  nativeDesktopResaveCases: resave.summary.nativeDesktopResaveCases + 1,
  wrongBindings: desktop.summary.wrongBindings,
  unexplainedRelations: desktop.summary.unexplainedRelations,
  roundtripNameDrift: desktop.summary.roundtripNameDrift,
  resaveNameDrift: desktop.summary.resaveNameDrift,
  blockers,
}, null, 2))
