import { createHash } from 'node:crypto'
import { readFile, stat, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { inspectFcstdArchive, rewriteFcstdMetadataArchive, serializeFcstdMetadataArchive } from '../src/facade/fcstd'
import { DirectNativeOcctHistoryProvider, NATIVE_OCCT_HISTORY_PROTOCOL_VERSION } from '../src/facade/nativeHistoryProtocol'
import type { NativeStageNamingEvidence } from '../src/facade/nativeNamingEvidence'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from '../src/facade/nativeHistoryProvider'
import type { DocumentSnapshot, TopoRefValue } from '../src/facade/types'

const root = resolve(new URL('..', import.meta.url).pathname)
const dist = resolve(root, 'native/occt-history/dist')
const modulePath = resolve(dist, 'bitbybit-occt-history.js')
const reportPath = resolve(root, 'config/freecad-isomorphic-provenance-verification.json')
const artifactNames = ['bitbybit-occt-history.js', 'bitbybit-occt-history.wasm', 'bitbybit-occt-history.data']
const fail = (message: string): never => { throw new Error(`FreeCAD isomorphic provenance verification failed: ${message}`) }

const createModule = (await import(pathToFileURL(modulePath).href)).default
const module = await createModule({ locateFile: (path: string) => resolve(dist, path) })
const provider = new DirectNativeOcctHistoryProvider(module)

const shapeStep = (method: string, ...args: number[]) => {
  const shape = module[method](...args)
  try { return module.shapeToStep(shape) as string }
  finally { shape.delete?.() }
}

const namingCounts = (evidence: NativeStageNamingEvidence) => ({
  status: evidence.status,
  stable: evidence.mappedNames?.filter(({ relation }) => relation !== 'ambiguous').length ?? 0,
  ambiguous: evidence.mappedNames?.filter(({ relation }) => relation === 'ambiguous').length ?? 0,
  candidates: evidence.mappedNames?.filter(({ relation }) => relation === 'ambiguous').reduce((count, mapped) => count + (mapped.candidates?.length ?? 0), 0) ?? 0,
  stringHasherEntries: evidence.stringHasher ? ('entries' in evidence.stringHasher ? evidence.stringHasher.entries.length : 0) : 0,
  elementMaps: evidence.elementMap2 ? ('maps' in evidence.elementMap2 ? evidence.elementMap2.maps.length : 0) : 0,
})

const referencedUpstreamCandidateObjects = (records: NativeOcctHistoryResponse['records'], evidence: NativeStageNamingEvidence) => {
  const objects = new Set<string>()
  for (const record of records) {
    if (record.relation === 'deleted' || !['object', 'tool'].includes(record.sourceId ?? record.source)) continue
    const mapped = evidence.mappedNames?.find((entry) => entry.kind === record.kind && entry.resultIndex === record.sourceIndex)
    for (const candidate of mapped?.candidates ?? []) objects.add(candidate.objectId)
  }
  return objects
}

const commonObjectStep = shapeStep('makeBox', 10, 10, 10)
const commonToolStep = shapeStep('makeBox', 10, 10, 10)
const common = await provider.capture({
  protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
  requestId: 'isomorphic-common', documentId: 'isomorphic-doc', documentVersion: 1,
  operationId: 'common', resultObjectId: 'common-result', resultObjectTag: 401, operation: 'common',
  objectStep: commonObjectStep, toolStep: commonToolStep,
  inputs: [
    { inputId: 'object', objectId: 'symmetric-object', role: 'object', stageId: 'source-object', objectTag: 301, step: commonObjectStep },
    { inputId: 'tool', objectId: 'symmetric-tool', role: 'tool', stageId: 'source-tool', objectTag: 302, step: commonToolStep },
  ],
  stages: [{ stageId: 'common-stage', operation: 'common', inputIds: ['object', 'tool'], ordinal: 0 }],
}, new AbortController().signal)
const commonEvidence = common.history.namingEvidence ?? fail('symmetric Common omitted naming evidence')
const commonCounts = namingCounts(commonEvidence)
if (commonCounts.status !== 'ambiguous' || commonCounts.stable !== 0 || commonCounts.ambiguous < 1 || commonCounts.candidates < commonCounts.ambiguous * 2) fail('symmetric Common did not retain all native candidates')

const persistedCommon = JSON.parse(JSON.stringify(commonEvidence)) as NativeStageNamingEvidence
if (JSON.stringify(persistedCommon.mappedNames) !== JSON.stringify(commonEvidence.mappedNames)) fail('candidate sets changed across JSON persistence')

const cutObjectStep = shapeStep('makeBox', 10, 10, 10)
const cutToolStep = shapeStep('makeBox', 5, 5, 5)
const cut = await provider.capture({
  protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
  requestId: 'mixed-cut', documentId: 'isomorphic-doc', documentVersion: 2,
  operationId: 'cut', resultObjectId: 'cut-result', resultObjectTag: 501, operation: 'cut',
  objectStep: cutObjectStep, toolStep: cutToolStep,
  inputs: [
    { inputId: 'object', objectId: 'cut-object', role: 'object', stageId: 'cut-object-source', objectTag: 311, step: cutObjectStep },
    { inputId: 'tool', objectId: 'cut-tool', role: 'tool', stageId: 'cut-tool-source', objectTag: 312, step: cutToolStep },
  ],
  stages: [{ stageId: 'cut-stage', operation: 'cut', inputIds: ['object', 'tool'], ordinal: 0 }],
}, new AbortController().signal)
const cutEvidence = cut.history.namingEvidence ?? fail('mixed Cut omitted naming evidence')
const cutCounts = namingCounts(cutEvidence)
if (cutCounts.status !== 'ambiguous' || cutCounts.stable < 1 || cutCounts.ambiguous < 1 || cutCounts.elementMaps < 1) fail('mixed Cut did not combine private names with explicit ambiguity')

const cutResultStep = cut.history.resultStep ?? fail('mixed Cut omitted result STEP')
const downstream = await provider.capture({
  protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
  requestId: 'downstream-rotate', documentId: 'isomorphic-doc', documentVersion: 3,
  operationId: 'rotate', resultObjectId: 'rotate-result', resultObjectTag: 601, operation: 'rotate',
  objectStep: cutResultStep, axisOrigin: [0, 0, 0], direction: [0, 0, 1], angle: 15,
  inputs: [{ inputId: 'object', objectId: 'cut-result', role: 'object', stageId: 'cut-stage', objectTag: 501, step: cutResultStep, namingEvidence: JSON.parse(JSON.stringify(cutEvidence)) as NativeStageNamingEvidence }],
  stages: [{ stageId: 'rotate-stage', operation: 'rotate', inputIds: ['object'], ordinal: 0 }],
}, new AbortController().signal)
const downstreamEvidence = downstream.history.namingEvidence ?? fail('downstream Rotate omitted naming evidence')
const downstreamCounts = namingCounts(downstreamEvidence)
const downstreamCandidateObjects = new Set(downstreamEvidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])
const downstreamReferencedCandidateObjects = referencedUpstreamCandidateObjects(downstream.history.records, cutEvidence)
if (downstreamCounts.status !== 'ambiguous' || downstreamCounts.ambiguous < 1 || !downstreamCandidateObjects.has('cut-object') || !downstreamCandidateObjects.has('cut-tool')) fail('downstream feature collapsed upstream ambiguity')
if ([...downstreamReferencedCandidateObjects].some((objectId) => !downstreamCandidateObjects.has(objectId))) fail('downstream feature lost a referenced upstream candidate')

type ChainSource = { objectId: string; objectTag: number; stageId: string; step: string; evidence: NativeStageNamingEvidence }
type ChainDefinition = { operation: NativeOcctHistoryOperation; parameters: Record<string, unknown> }
const chainStages = [{
  operation: 'cut',
  historyRecords: cut.history.records.length,
  ...cutCounts,
  candidateObjects: [...new Set(cutEvidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])].sort(),
  persistedInputEvidence: false,
}, {
  operation: 'rotate',
  historyRecords: downstream.history.records.length,
  ...downstreamCounts,
  candidateObjects: [...downstreamCandidateObjects].sort(),
  referencedInputCandidateObjects: [...downstreamReferencedCandidateObjects].sort(),
  referencedCandidateObjectsRetained: true,
  persistedInputEvidence: true,
}]
const chainResponses = [cut, downstream]
let chainSource: ChainSource = {
  objectId: 'rotate-result', objectTag: 601, stageId: 'rotate-stage',
  step: downstream.history.resultStep ?? fail('downstream Rotate omitted result STEP'), evidence: downstreamEvidence,
}
let previousHasherEntries = downstreamCounts.stringHasherEntries
const chainDefinitions: ChainDefinition[] = [
  { operation: 'fillet', parameters: { radius: 0.2 } },
  { operation: 'mirrored', parameters: { axisOrigin: [0, 0, 0], direction: [1, 0, 0] } },
  { operation: 'linear-pattern', parameters: { direction: [3, 0, 0] } },
]
for (const [index, definition] of chainDefinitions.entries()) {
  const documentVersion = index + 4
  const stageId = `${definition.operation}-stage`
  const resultObjectId = `${definition.operation}-result`
  const resultObjectTag = 701 + index
  const persistedInputEvidence = JSON.parse(JSON.stringify(chainSource.evidence)) as NativeStageNamingEvidence
  const response = await provider.capture({
    protocolVersion: NATIVE_OCCT_HISTORY_PROTOCOL_VERSION,
    requestId: `long-chain-${definition.operation}`, documentId: 'isomorphic-doc', documentVersion,
    operationId: definition.operation, resultObjectId, resultObjectTag, operation: definition.operation,
    objectStep: chainSource.step,
    inputs: [{ inputId: 'object', objectId: chainSource.objectId, role: 'object', stageId: chainSource.stageId, objectTag: chainSource.objectTag, step: chainSource.step, namingEvidence: persistedInputEvidence }],
    stages: [{ stageId, operation: definition.operation, inputIds: ['object'], ordinal: 0 }],
    ...definition.parameters,
  } as Parameters<typeof provider.capture>[0], new AbortController().signal)
  const evidence = response.history.namingEvidence ?? fail(`${definition.operation} omitted naming evidence`)
  const counts = namingCounts(evidence)
  const candidateObjects = new Set(evidence.mappedNames?.flatMap(({ candidates }) => candidates?.map(({ objectId }) => objectId) ?? []) ?? [])
  const referencedInputCandidateObjects = referencedUpstreamCandidateObjects(response.history.records, persistedInputEvidence)
  if (counts.status !== 'ambiguous' || counts.stable < 1 || counts.ambiguous < 1 || counts.elementMaps < 1) fail(`${definition.operation} did not preserve mixed stable/ambiguous naming evidence`)
  if ([...referencedInputCandidateObjects].some((objectId) => !candidateObjects.has(objectId))) fail(`${definition.operation} lost a referenced upstream candidate`)
  if (counts.stringHasherEntries < previousHasherEntries) fail(`${definition.operation} regressed the restored StringHasher table`)
  previousHasherEntries = counts.stringHasherEntries
  chainStages.push({ operation: definition.operation, historyRecords: response.history.records.length, ...counts, candidateObjects: [...candidateObjects].sort(), referencedInputCandidateObjects: [...referencedInputCandidateObjects].sort(), referencedCandidateObjectsRetained: true, persistedInputEvidence: true })
  chainResponses.push(response)
  chainSource = { objectId: resultObjectId, objectTag: resultObjectTag, stageId, step: response.history.resultStep ?? fail(`${definition.operation} omitted result STEP`), evidence }
}
const persistedFinalEvidence = JSON.parse(JSON.stringify(chainSource.evidence)) as NativeStageNamingEvidence
if (JSON.stringify(persistedFinalEvidence.mappedNames) !== JSON.stringify(chainSource.evidence.mappedNames)) fail('long-chain naming evidence changed across final JSON persistence')

const ambiguousRef: TopoRefValue = { schemaVersion: 1, objectId: 'Source', kind: 'face', persistentId: 'Face1', topologyVersion: 2, generation: 1, status: 'ambiguous', candidates: ['Face1', 'Face2'] }
const fcstdDocument: DocumentSnapshot = {
  id: 'fcstd-ambiguous', label: 'FCStd ambiguity boundary', version: 1, dirty: false, readOnly: false, units: 'mm',
  tree: [{ id: 'Source', label: 'Source', type: 'feature' }, { id: 'Consumer', label: 'Consumer', type: 'feature' }],
  objects: [
    { id: 'Source', typeId: 'Part::Feature', properties: [] },
    { id: 'Consumer', typeId: 'Part::Feature', properties: [{ name: 'Support', label: 'Support', group: 'Links', scope: 'data', type: 'App::PropertyLinkSub', value: { schemaVersion: 1, objectId: 'Source', subElements: [ambiguousRef] } }] },
  ],
  dependencies: [{ sourceId: 'Consumer', targetId: 'Source', relation: 'topo-ref', propertyName: 'Support', reference: 'Face1' }],
  recompute: { generation: 0, status: 'idle', objectStates: { Source: 'clean', Consumer: 'clean' }, dirtyObjects: [], order: [], errors: [] },
}
let fcstdAmbiguityRejected = false
try { serializeFcstdMetadataArchive(fcstdDocument) }
catch (error) { fcstdAmbiguityRejected = /stable topology status/.test(error instanceof Error ? error.message : String(error)) }
if (!fcstdAmbiguityRejected) fail('FCStd writer did not reject an ambiguous LinkSub')

const stableRef: TopoRefValue = { schemaVersion: 1, objectId: 'Source', kind: 'face', persistentId: 'Face1', topologyVersion: 5, generation: 5, status: 'stable' }
const stableFcstdDocument: DocumentSnapshot = {
  ...fcstdDocument,
  id: 'fcstd-stable', label: 'FCStd stable LinkSub boundary', version: 5,
  objects: fcstdDocument.objects.map((object) => object.id !== 'Consumer' ? object : {
    ...object,
    properties: object.properties.map((property) => property.name !== 'Support' ? property : { ...property, value: { schemaVersion: 1, objectId: 'Source', subElements: [stableRef] } }),
  }),
}
const stableArchive = serializeFcstdMetadataArchive(stableFcstdDocument)
const stableInspection = inspectFcstdArchive(stableArchive)
const nativeLinkSub = stableInspection.objects.find(({ name }) => name === 'Consumer')?.properties.find(({ name }) => name === 'Support')
if (JSON.stringify(nativeLinkSub?.subElements) !== '["Face1"]') fail('FCStd writer did not emit the stable native LinkSub name')
const rewrittenStableArchive = rewriteFcstdMetadataArchive(stableArchive, stableInspection.proxyDocument)
const rewrittenStableLinkSub = inspectFcstdArchive(rewrittenStableArchive).objects.find(({ name }) => name === 'Consumer')?.properties.find(({ name }) => name === 'Support')
if (JSON.stringify(rewrittenStableLinkSub?.subElements) !== '["Face1"]') fail('FCStd inspect/rewrite changed the stable native LinkSub name')

for (const response of [common, ...chainResponses]) (response.history as typeof response.history & { result?: { delete?(): void } }).result?.delete?.()

const artifacts = await Promise.all(artifactNames.map(async (name) => {
  const path = resolve(dist, name)
  const [content, bytes] = await Promise.all([readFile(path), stat(path).then(({ size }) => size)])
  return { name, bytes, sha256: createHash('sha256').update(content).digest('hex') }
}))
const harnessPath = resolve(root, 'scripts/run-freecad-isomorphic-provenance.ts')
const harnessContent = await readFile(harnessPath)
const report = {
  schemaVersion: 1, status: 'pass', runtime: 'node', implementation: 'freecad-linked', productionWorkerLinked: true,
  cases: {
    symmetricCommon: { ...commonCounts, persistedCandidateSets: true },
    mixedCut: cutCounts,
    downstreamRotate: { ...downstreamCounts, candidateObjects: [...downstreamCandidateObjects].sort() },
    longChain: {
      operations: chainStages.map(({ operation }) => operation),
      stages: chainStages,
      persistedFinalEvidence: true,
      referencedCandidateObjectsRetained: true,
      stringHasherMonotonic: true,
    },
  },
  fcstdAmbiguityRejected,
  fcstdStableLinkRoundtrip: { subElements: rewrittenStableLinkSub?.subElements, inspectRewritePreserved: true, nativeDesktopResaveCovered: false },
  artifacts,
  harness: { path: 'scripts/run-freecad-isomorphic-provenance.ts', bytes: harnessContent.length, sha256: createHash('sha256').update(harnessContent).digest('hex') },
  exactBlocker: 'exhaustive cross-feature naming and native FreeCAD FCStd save-reopen-resave corpus',
  systemExact: false,
  generatedAt: new Date().toISOString(),
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
