import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { elementMap2SemanticDigest, parseElementMap2, validateElementMap2, writeElementMap2 } from '../src/facade/elementMap2.ts'
import { validateNativeNamingEvidence } from '../src/facade/nativeNamingEvidence.ts'
import { migrateStringHasherSchema, parseStringHasherTable, validateStringHasherTable, writeStringHasherTable } from '../src/facade/stringHasher.ts'
import { validateCompositeMutationEvidence } from './freecad-composite-mutation-evidence.mjs'

const root = resolve(new URL('..', import.meta.url).pathname)
const report = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-elementmap-oracle.json'), 'utf8'))
const resaveReport = JSON.parse(await readFile(resolve(root, 'config/freecad-composite-history-resave-verification.json'), 'utf8'))
if (report.schemaVersion !== 1 || report.baselineId !== 'freecad-1.1.1-composite-history-elementmap2' || report.freecadVersion !== '1.1.1' || report.status !== 'pass' || report.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('FreeCAD composite history ElementMap2 oracle baseline is invalid.')
if (report.summary?.cases !== 30 || report.summary?.passed !== 30 || report.summary?.failed !== 0 || report.summary?.stageCorrelations !== 219 || !(report.summary?.relationRecords > 0) || !Array.isArray(report.cases) || report.cases.length !== 30) throw new Error('FreeCAD composite oracle must contain exactly 30 passing cases and 219 correlated stages.')
if (resaveReport.schemaVersion !== 1 || resaveReport.baselineId !== report.baselineId || resaveReport.freecadVersion !== report.freecadVersion || resaveReport.gitCommit !== report.gitCommit || resaveReport.status !== 'pass' || resaveReport.summary?.cases !== 30 || resaveReport.summary?.passed !== 30 || resaveReport.summary?.failed !== 0 || resaveReport.summary?.roundtripNameDrift !== 0 || resaveReport.summary?.resaveNameDrift !== 0 || resaveReport.summary?.nativeDesktopResaveCases !== 30 || resaveReport.summary?.stageCorrelations !== 219 || resaveReport.summary?.relationRecords !== report.summary.relationRecords || !Array.isArray(resaveReport.cases) || resaveReport.cases.length !== 30) throw new Error('FreeCAD composite resave oracle must contain exactly 30 passing save/reopen/resave cases and 219 correlated stages.')
const reportIds = report.cases.map(({ id }) => id)
const resaveIds = resaveReport.cases.map(({ id }) => id)
if (new Set(reportIds).size !== 30 || JSON.stringify(resaveIds) !== JSON.stringify(reportIds)) throw new Error('FreeCAD composite and resave case identities are not joined one-to-one.')
const resaveById = new Map(resaveReport.cases.map((fixture) => [fixture.id, fixture]))
for (const fixture of resaveReport.cases) if (fixture.roundtripNameDrift !== 0 || fixture.resaveNameDrift !== 0 || fixture.nativeDesktopResaveCovered !== true) throw new Error(`FreeCAD composite fixture ${fixture.id} changed mapped names after native FreeCAD resave.`)
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)])) : value
const digest = (value) => createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')
const mutationEvidence = validateCompositeMutationEvidence(report)
let parsedResources = 0
let parsedStringHasherResources = 0
let namingEvidenceStages = 0
let namingEvidenceMissing = 0
let namingEvidenceBoundaryViolations = 0
let nativeEvidenceValidatedStages = 0
let nativeMappedNameStages = 0
let nativeIndexedNameStages = 0
let privateTokenEvidenceCompleteStages = 0
let internalBuilderEvidenceStages = 0
let internalBuilderEvidenceMissingStages = 0
let stageCorrelations = 0
let relationRecords = 0
let wrongBindings = 0
let unexplainedRelations = 0
for (const fixture of report.cases) {
  if (fixture.status !== 'pass' || !fixture.finalObject || !fixture.stages?.length) throw new Error(`FreeCAD composite fixture ${fixture.id} has no valid feature history stages.`)
  if (fixture.roundtripNameDrift !== 0) throw new Error(`FreeCAD composite fixture ${fixture.id} changed mapped names after FreeCAD reopen.`)
  const names = new Set()
  const stageNames = new Set(fixture.stages.map(({ name }) => name))
  const stageByName = new Map(fixture.stages.map((stage) => [stage.name, stage]))
  const dependencyClosure = (stageName, seen = new Set()) => {
    if (seen.has(stageName)) return seen
    seen.add(stageName)
    for (const linked of stageByName.get(stageName)?.linkedObjects ?? []) if (stageNames.has(linked)) dependencyClosure(linked, seen)
    return seen
  }
  const resaveFixture = resaveById.get(fixture.id)
  const correlations = resaveFixture?.stageCorrelations
  if (!correlations || !['initial', 'reopened', 'resaved'].every((phase) => Array.isArray(correlations[phase]) && correlations[phase].length === fixture.stages.length)) throw new Error(`FreeCAD composite fixture ${fixture.id} lacks complete initial/reopen/resave stage correlations.`)
  let historyStages = 0
  for (const [ordinal, stage] of fixture.stages.entries()) {
    if (!stage.shape?.valid || !stage.names?.length) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} has invalid topology.`)
    namingEvidenceStages += 1
    if (!['final-shape-only', 'opaque-preserved', 'native-evidence', 'ambiguous', 'missing'].includes(stage.namingEvidenceStatus)) namingEvidenceMissing += 1
    if (stage.namingEvidenceStatus === 'opaque-preserved' && !fixture.stringHasherResource?.text) namingEvidenceBoundaryViolations += 1
    if (!stage.nativeEvidence || stage.nativeEvidence.stageId !== stage.name || stage.nativeEvidence.resultObjectId !== stage.name) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} has incomplete native naming evidence identity.`)
    if (stage.nativeEvidence.status !== stage.namingEvidenceStatus) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} has inconsistent naming evidence status.`)
    const nativeEvidence = { ...stage.nativeEvidence }
    if (fixture.stringHasherResource?.text) nativeEvidence.stringHasher = migrateStringHasherSchema(parseStringHasherTable(fixture.stringHasherResource.text))
    const nativeValidation = validateNativeNamingEvidence(nativeEvidence)
    if (!nativeValidation.valid) throw new Error(`FreeCAD native naming evidence validation failed for ${fixture.id}/${stage.name}: ${nativeValidation.issues[0].path}: ${nativeValidation.issues[0].message}`)
    nativeEvidenceValidatedStages += 1
    if (stage.nativeEvidence.mappedNameApiEntries > 0) nativeMappedNameStages += 1
    if (stage.nativeEvidence.indexedNameApiEntries > 0) nativeIndexedNameStages += 1
    if (stage.nativeEvidence.privateTokenEvidenceComplete === true) {
      if (stage.nativeEvidence.mappedNameApiEntries !== stage.nativeEvidence.mappedNames.length) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} claims incomplete private token evidence as complete.`)
      privateTokenEvidenceCompleteStages += 1
    }
    if (stage.nativeEvidence.status === 'native-evidence' && stage.nativeEvidence.internalBuilderEvidence === true) internalBuilderEvidenceStages += 1
    else if (stage.nativeEvidence.status === 'native-evidence' && stage.nativeEvidence.internalBuilderEvidence !== true && /^(Part::(Fuse|Cut|Common|Extrusion|Revolution|Loft|Sweep|Fillet|Chamfer)|PartDesign::(Pad|Pocket|Revolution|Groove|AdditiveLoft|SubtractiveLoft|AdditivePipe|SubtractivePipe|Fillet|Chamfer|Draft|Thickness|Mirrored|MultiTransform|LinearPattern|PolarPattern|Hole))$/.test(stage.typeId || '')) internalBuilderEvidenceMissingStages += 1
    const stageHasHistory = stage.names.some((entry) => Array.isArray(entry.history) && entry.history.length > 0)
    if (stageHasHistory) historyStages += 1
    if (!Array.isArray(stage.linkedObjects) || !Array.isArray(stage.relations) || stage.relations.length !== stage.names.length || stage.relationDigest !== digest(stage.relations) || !/^[0-9a-f]{64}$/.test(stage.semanticNameDigest || '')) throw new Error(`FreeCAD composite fixture ${fixture.id}/${stage.name} lacks relation-level naming evidence.`)
    const expectedKey = `${fixture.id}:${String(ordinal).padStart(3, '0')}:${stage.name}`
    const phaseRecords = ['initial', 'reopened', 'resaved'].map((phase) => correlations[phase][ordinal])
    for (const record of phaseRecords) if (record.key !== expectedKey || record.ordinal !== ordinal || record.name !== stage.name || record.typeId !== stage.typeId || record.relationCount !== stage.relations.length) throw new Error(`FreeCAD composite fixture ${expectedKey} changed stage identity across native resave.`)
    if (phaseRecords.some((record) => record.relationDigest !== stage.relationDigest || record.semanticNameDigest !== stage.semanticNameDigest)) throw new Error(`FreeCAD composite fixture ${expectedKey} changed relation or semantic-name evidence across native resave.`)
    stageCorrelations += 1
    relationRecords += stage.relations.length
    const allowedSources = dependencyClosure(stage.name)
    allowedSources.delete(stage.name)
    let stageSourceRelations = 0
    for (const [relationIndex, relation] of stage.relations.entries()) {
      if (relation.resultName !== stage.names[relationIndex]?.name || !['generated', 'modified', 'preserved'].includes(relation.relation) || !Array.isArray(relation.sources)) unexplainedRelations += 1
      for (const source of relation.sources) {
        stageSourceRelations += 1
        if (!stageNames.has(source.sourceObject) || !/^(?:(?:Face|Edge|Vertex)\d+|#[a-zA-Z0-9]+:[0-9a-fA-F]+|#[0-9a-fA-F]+|g\d+(?:v\d+)?;SKT)$/.test(source.sourceElement || '')) unexplainedRelations += 1
        else if (!allowedSources.has(source.sourceObject)) wrongBindings += 1
      }
    }
    if (stage.nativeEvidence.internalBuilderEvidence === true && stageSourceRelations === 0) unexplainedRelations += 1
    for (const entry of stage.names) {
      if (names.has(`${stage.name}:${entry.name}`)) throw new Error(`Duplicate ElementMap name ${fixture.id}/${stage.name}/${entry.name}.`)
      names.add(`${stage.name}:${entry.name}`)
    }
  }
  if (historyStages === 0) throw new Error(`Missing FreeCAD getElementHistory chain for ${fixture.id}.`)
  if (fixture.stringHasherResource?.text !== null && fixture.stringHasherResource?.text !== undefined) {
    const table = migrateStringHasherSchema(parseStringHasherTable(fixture.stringHasherResource.text))
    const validation = validateStringHasherTable(table)
    if (!validation.valid) throw new Error(`StringHasher semantic validation failed for ${fixture.id}: ${validation.issues[0].path}: ${validation.issues[0].message}`)
    const reparsed = migrateStringHasherSchema(parseStringHasherTable(writeStringHasherTable(table)))
    if (JSON.stringify(reparsed) !== JSON.stringify(table)) throw new Error(`StringHasher semantic roundtrip changed native naming evidence for ${fixture.id}.`)
    parsedStringHasherResources += 1
  }
  for (const [path, resource] of Object.entries(fixture.elementMapResources || {})) {
    const document = parseElementMap2(resource.text)
    const validation = validateElementMap2(document)
    if (!validation.valid) throw new Error(`ElementMap2 semantic validation failed for ${fixture.id}/${path}: ${validation.issues[0].path}: ${validation.issues[0].message}`)
    const canonical = writeElementMap2(document)
    const reparsed = parseElementMap2(canonical)
    if (reparsed.maps.length !== document.maps.length || reparsed.postfixes.length !== document.postfixes.length) throw new Error(`ElementMap2 schema roundtrip changed counts for ${fixture.id}/${path}.`)
    if (elementMap2SemanticDigest(reparsed) !== elementMap2SemanticDigest(document)) throw new Error(`ElementMap2 semantic roundtrip changed native naming history for ${fixture.id}/${path}.`)
    parsedResources += 1
  }
}
if (stageCorrelations !== 219 || stageCorrelations !== report.summary.stageCorrelations || relationRecords !== report.summary.relationRecords) throw new Error(`FreeCAD stage-correlation totals drifted: stages=${stageCorrelations}, relations=${relationRecords}.`)
if (wrongBindings !== 0 || unexplainedRelations !== 0) throw new Error(`FreeCAD relation-level topology correlation failed: wrongBindings=${wrongBindings}, unexplainedRelations=${unexplainedRelations}.`)
if (parsedResources === 0) throw new Error('FreeCAD composite oracle did not capture any ElementMap2 resources.')
if (nativeIndexedNameStages !== namingEvidenceStages) throw new Error(`FreeCAD composite oracle must retain direct indexed-name evidence for every stage, found ${nativeIndexedNameStages}/${namingEvidenceStages}.`)
if (namingEvidenceMissing !== 0 || namingEvidenceBoundaryViolations !== 0) throw new Error(`FreeCAD naming evidence boundary is incomplete: missing=${namingEvidenceMissing}, violations=${namingEvidenceBoundaryViolations}.`)
console.log(JSON.stringify({ status: 'freecad-composite-history-elementmap-pass', cases: report.summary.cases, categoryCases: mutationEvidence.categoryCases, mutationCases: mutationEvidence.mutationCases, mutationPassed: mutationEvidence.mutationPassed, mutationStageRecords: mutationEvidence.mutationStageRecords, mutationPhaseStageRecords: mutationEvidence.mutationPhaseStageRecords, mutationFinalPropagationFailures: mutationEvidence.mutationFinalPropagationFailures, mutationStageRestoreFailures: mutationEvidence.mutationStageRestoreFailures, mutationNamingRestoreDriftCases: mutationEvidence.mutationNamingRestoreDriftCases, mutationNamingRestoreDriftStages: mutationEvidence.mutationNamingRestoreDriftStages, stageCorrelations, relationRecords, wrongBindings, unexplainedRelations, parsedResources, parsedStringHasherResources, namingEvidenceStages, nativeEvidenceValidatedStages, nativeIndexedNameStages, nativeMappedNameStages, indexedOnlyStages: nativeIndexedNameStages - nativeMappedNameStages, privateTokenEvidenceCompleteStages, namingEvidenceMissing, namingEvidenceBoundaryViolations, internalBuilderEvidenceStages, internalBuilderEvidenceMissingStages, nativeDesktopResaveCases: resaveReport.summary.nativeDesktopResaveCases, resaveNameDrift: resaveReport.summary.resaveNameDrift }, null, 2))
