import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { readFileSync } from 'node:fs'
import { dirname, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { decodeFcstdPathProperty, inspectFcstdArchive, rewriteFcstdMetadataArchive, rewriteFcstdPathProperty } from '../src/facade/fcstd'

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const executable = resolve(root, '.cache/freecad/install-desktop/bin/FreeCAD')
const probe = resolve(root, 'scripts/freecad-cam-path-oracle.py')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const outputDirectory = resolve(root, '.cache/freecad/cam-path-oracle')
const sourcePath = resolve(outputDirectory, 'native-path-source.FCStd')
const mutatedPath = resolve(outputDirectory, 'native-path-mutated.FCStd')
const preservedPath = resolve(outputDirectory, 'web-preserved.FCStd')
const webEditedPath = resolve(outputDirectory, 'web-edited-path.FCStd')
const webEditedPythonPath = resolve(outputDirectory, 'web-edited-python-path.FCStd')
const reportPath = resolve(root, 'config/freecad-cam-path-oracle.json')
const marker = 'FREECAD_CAM_PATH_RESULT='
let probeCounter = 0
const fail = (message: string): never => { throw new Error(`FreeCAD CAM Path oracle: ${message}`) }
const sha256 = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex')
const semanticSha256 = (value: unknown) => createHash('sha256').update(JSON.stringify(value)).digest('hex')

await mkdir(outputDirectory, { recursive: true })

const runtimeEnv = {
  ...process.env,
  PYTHONPATH: `${resolve(sysroot, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
  LD_LIBRARY_PATH: `${resolve(sysroot, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
  MATPLOTLIBRC: resolve(sysroot, 'usr/share/matplotlib/mpl-data/matplotlibrc'),
  MPLBACKEND: 'Agg',
}

const runProbe = (extraEnv: NodeJS.ProcessEnv) => {
  const resultFile = resolve(outputDirectory, `native-probe-result-${probeCounter++}.json`)
  const execution = spawnSync('xvfb-run', ['-a', executable, '--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), probe], {
    cwd: root,
    encoding: 'utf8',
    maxBuffer: 32 * 1024 * 1024,
    timeout: 120_000,
    env: { ...runtimeEnv, ...extraEnv, FREECAD_CAM_PATH_RESULT_FILE: resultFile },
  })
  const output = `${execution.stdout ?? ''}\n${execution.stderr ?? ''}`
  const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(marker))
  const resultFileAvailable = (() => { try { return readFileSync(resultFile, 'utf8') } catch { return '' } })()
  const qtTeardownSignal = execution.status !== 0 && /SIGSEGV|Segmentation fault|Shiboken::BindingManager/.test(output) && Boolean(line || resultFileAvailable)
  if (execution.error || (execution.status !== 0 && !qtTeardownSignal) || (!line && !resultFileAvailable)) fail(`native probe exited with ${execution.status}: ${execution.error?.message ?? output.trim()}`)
  const result = resultFileAvailable
    ? JSON.parse(resultFileAvailable) as Record<string, any>
    : JSON.parse(line!.slice(marker.length)) as Record<string, any>
  if (qtTeardownSignal && result.nativeRuntime) result.nativeRuntime.qtTaskPanelTeardown = 'isolated-pyside-wrapper-release-after-complete-marker'
  return result
}

const native = runProbe({
  FREECAD_CAM_PATH_SOURCE: sourcePath,
  FREECAD_CAM_PATH_MUTATED: mutatedPath,
})
if (native.freecadVersion !== '1.1.1' || native.gitCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') fail('native runtime does not match the locked FreeCAD 1.1.1 oracle')
if (native.nativeRuntime?.guiUp !== true || native.qtDynamicOracle?.activeWorkbench !== 'CAMWorkbench') fail('CAM Qt workbench did not activate in the GUI oracle')
if (native.profileAlgorithm?.pathPropertyType !== 'Path::PropertyPath' || native.profileAlgorithm?.cuttingCommandCount !== 20 || native.profileAlgorithm?.commandCount < 20) fail('native Profile operation did not reproduce the locked algorithm result')
if (native.profileAlgorithm?.variants?.length !== 2 || native.profileAlgorithm.variants.some((variant: any) => variant.commandCount !== 32 || variant.cuttingCommandCount !== 20 || !/^[0-9a-f]{64}$/.test(variant.commandSha256))) fail('native Profile compensation variants are incomplete')
const expectedHelixDirections: Record<string, [string, string]> = {
  'inside-conventional': ['CW', 'G2'],
  'outside-climb': ['CW', 'G2'],
  'inside-climb': ['CCW', 'G3'],
  'outside-conventional': ['CCW', 'G3'],
}
if (native.helixAlgorithm?.fixture !== 'Mod/CAM/CAMTests/test_holes00.fcstd' || native.helixAlgorithm?.pathPropertyType !== 'Path::PropertyPath' || native.helixAlgorithm?.toolDiameterMm !== 0.9 || native.helixAlgorithm?.baseSubElementCount !== 9 || native.helixAlgorithm?.scenarioCount !== 4) fail('native Helix fixture contract is incomplete')
if (native.helixAlgorithm.scenarios.some((scenario: any) => {
  const expected = expectedHelixDirections[scenario.id]
  return !expected || scenario.direction !== expected[0] || scenario.expectedDirection !== expected[0] || scenario.expectedArc !== expected[1] || JSON.stringify(scenario.arcNames) !== JSON.stringify([expected[1]]) || scenario.arcCommandCount !== 1260 || scenario.commandCount < 1400 || !/^[0-9a-f]{64}$/.test(scenario.commandSha256)
})) fail('native Helix direction or command evidence differs from the locked oracle')
const incompleteActions = native.qtDynamicOracle?.requiredCommands?.filter((command: any) => command.emptySelection?.registered !== true || command.selectedModel?.registered !== true || command.selectedJob?.registered !== true || command.selectedOperation?.registered !== true || command.emptyDocument?.registered !== true || command.selectedModel?.actionCount < 1) ?? []
if (incompleteActions.length > 0) fail(`required CAM QAction registration evidence is incomplete: ${JSON.stringify(incompleteActions)}`)

const original = new Uint8Array(await readFile(sourcePath))
const inspection = inspectFcstdArchive(original)
const nativePath = inspection.objects.find((object) => object.name === 'NativePath') ?? fail('NativePath is missing from Web FCStd inspection')
const pythonPath = inspection.objects.find((object) => object.name === 'PythonPath') ?? fail('PythonPath is missing from Web FCStd inspection')
const pathProperties = [nativePath, pythonPath].map((object) => object.properties.find((property) => property.name === 'Path'))
if (nativePath.typeId !== 'Path::Feature' || pythonPath.typeId !== 'Path::FeaturePython') fail('Web inspection did not preserve Path object TypeIds')
if (pathProperties.some((property) => property?.typeId !== 'Path::PropertyPath')) fail('Web inspection did not preserve Path::PropertyPath metadata')
if (!inspection.proxyDocument.readOnly || inspection.compatibility.blockedObjects < 1) fail('FeaturePython archive must remain a read-only proxy at the current Web codec boundary')

const preserved = rewriteFcstdMetadataArchive(original, inspection.proxyDocument)
if (sha256(original) !== sha256(preserved) || !Buffer.from(original).equals(Buffer.from(preserved))) fail('read-only Web rewrite was not byte-for-byte transparent')
await writeFile(preservedPath, preserved)
const reopened = runProbe({
  FREECAD_CAM_PATH_VERIFY_ONLY: '1',
  FREECAD_CAM_PATH_VERIFY_ARCHIVE: preservedPath,
})
if (reopened.verified !== true || reopened.objects?.some((object: any) => object.commandCount !== 5 || object.pathPropertyType !== 'Path::PropertyPath')) fail('FreeCAD could not reopen the Web-preserved Path archive')

const decodedPath = decodeFcstdPathProperty(original, 'NativePath')
if (decodedPath.commands.length !== 5 || decodedPath.resourcePath !== 'NativePath.nc') fail('Web Path::PropertyPath codec did not decode the native command resource')
const edited = rewriteFcstdPathProperty(original, {
  objectName: 'NativePath',
  value: { ...decodedPath, commands: [...decodedPath.commands, { name: 'M3', parameters: { S: 12000 } }] },
})
const editedDecoded = decodeFcstdPathProperty(edited, 'NativePath')
if (editedDecoded.commands.length !== 6 || editedDecoded.commands.at(-1)?.name !== 'M3' || editedDecoded.commands.at(-1)?.parameters.S !== 12000) fail('Web Path::PropertyPath codec did not persist the edited command')
await writeFile(webEditedPath, edited)
const editedReopened = runProbe({
  FREECAD_CAM_PATH_VERIFY_ONLY: '1',
  FREECAD_CAM_PATH_VERIFY_ARCHIVE: webEditedPath,
  FREECAD_CAM_PATH_EXPECT_NATIVE_COUNT: '6',
  FREECAD_CAM_PATH_EXPECT_PYTHON_COUNT: '5',
  FREECAD_CAM_PATH_EXPECT_NATIVE_LAST: 'M3',
})
if (editedReopened.verified !== true) fail('FreeCAD could not reopen the Web-edited native Path command resource')

const decodedPythonPath = decodeFcstdPathProperty(original, 'PythonPath', 'Path', { allowFeaturePython: true })
if (decodedPythonPath.commands.length !== 5 || decodedPythonPath.resourcePath !== 'PythonPath.nc') fail('Web FeaturePython Path::PropertyPath opt-in codec did not decode the native command resource')
const editedPython = rewriteFcstdPathProperty(original, {
  objectName: 'PythonPath',
  propertyName: 'Path',
  allowFeaturePython: true,
  value: { ...decodedPythonPath, commands: [...decodedPythonPath.commands, { name: 'M2', parameters: {} }] },
})
const editedPythonDecoded = decodeFcstdPathProperty(editedPython, 'PythonPath', 'Path', { allowFeaturePython: true })
if (editedPythonDecoded.commands.length !== 6 || editedPythonDecoded.commands.at(-1)?.name !== 'M2') fail('Web FeaturePython Path::PropertyPath codec did not persist the edited command')
await writeFile(webEditedPythonPath, editedPython)
const editedPythonReopened = runProbe({
  FREECAD_CAM_PATH_VERIFY_ONLY: '1',
  FREECAD_CAM_PATH_VERIFY_ARCHIVE: webEditedPythonPath,
  FREECAD_CAM_PATH_EXPECT_NATIVE_COUNT: '5',
  FREECAD_CAM_PATH_EXPECT_PYTHON_COUNT: '6',
  FREECAD_CAM_PATH_EXPECT_PYTHON_LAST: 'M2',
})
if (editedPythonReopened.verified !== true) fail('FreeCAD could not reopen the Web-edited FeaturePython Path command resource')

const mutated = new Uint8Array(await readFile(mutatedPath))
inspectFcstdArchive(mutated)
native.fcstdRoundTrip.source.semanticSha256 = semanticSha256(native.fcstdRoundTrip.sourceReopened)
native.fcstdRoundTrip.mutated.semanticSha256 = semanticSha256(native.fcstdRoundTrip.mutatedReopened)
reopened.archive.semanticSha256 = semanticSha256(reopened.objects)
delete native.fcstdRoundTrip.source.sha256
delete native.fcstdRoundTrip.mutated.sha256
delete reopened.archive.sha256
delete native.fcstdRoundTrip.source.byteLength
delete native.fcstdRoundTrip.mutated.byteLength
delete reopened.archive.byteLength

const relativePath = (path: string) => relative(root, path).replaceAll('\\', '/')
for (const archive of [native.fcstdRoundTrip.source, native.fcstdRoundTrip.mutated, reopened.archive]) archive.path = relativePath(archive.path)
native.nativeRuntime.pathModule = native.nativeRuntime.pathModule.startsWith(root) ? relativePath(native.nativeRuntime.pathModule) : native.nativeRuntime.pathModule

const report = {
  schemaVersion: 1,
  baselineId: 'freecad-1.1.1',
  sourceCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d',
  generatedBy: './npmw run probe:freecad-cam-path',
  checkedBy: './npmw run check:freecad-cam-path',
  runtime: native.nativeRuntime,
  profileAlgorithm: native.profileAlgorithm,
  helixAlgorithm: native.helixAlgorithm,
  qtDynamicOracle: native.qtDynamicOracle,
  fcstdRoundTrip: {
    ...native.fcstdRoundTrip,
    webInspection: {
      objectTypes: [nativePath.typeId, pythonPath.typeId],
      pathPropertyTypes: pathProperties.map((property) => property?.typeId),
      compatibility: inspection.compatibility,
      proxyReadOnly: inspection.proxyDocument.readOnly,
    },
    webTransparentPreservation: {
      archive: reopened.archive,
      byteExact: true,
      nativeReopenVerified: reopened.verified,
      objects: reopened.objects,
    },
    webEditablePathProperty: {
      objectName: 'NativePath',
      resourcePath: editedDecoded.resourcePath,
      sourceCommandCount: decodedPath.commands.length,
      editedCommandCount: editedDecoded.commands.length,
      lastCommand: editedDecoded.commands.at(-1),
      nativeReopenVerified: editedReopened.verified,
      featurePythonExecutionBlocked: true,
    },
    webEditableFeaturePythonPathProperty: {
      objectName: 'PythonPath',
      resourcePath: editedPythonDecoded.resourcePath,
      sourceCommandCount: decodedPythonPath.commands.length,
      editedCommandCount: editedPythonDecoded.commands.length,
      lastCommand: editedPythonDecoded.commands.at(-1),
      nativeReopenVerified: editedPythonReopened.verified,
      scriptExecution: 'blocked',
      optInRequired: true,
    },
  },
  claims: {
    nativeProfilePathAlgorithm: 'verified',
    nativeHelixPathAlgorithm: 'verified-four-direction-cases',
    qtCamWorkbenchDynamicOracle: 'verified',
    nativePathFeatureFcstdSaveOpenMutateSaveOpen: 'verified',
    nativeWebNativePathFcstdTransparentRoundTrip: 'verified',
    webEditablePathPropertyCodec: 'verified-native-path-feature',
    webEditableFeaturePythonPathResourceCodec: 'verified-safe-resource-only-opt-in',
  },
  boundary: {
    exactEvidence: 'FreeCAD 1.1.1 executes compensated/uncompensated Profile and four Helix direction cases, exposes live Qt CAM actions, and reopens both Path object types after native and byte-transparent Web round-trips.',
    remainingGap: 'FeaturePython Path::PropertyPath resource editing is now an explicit safe opt-in that never executes Python; FeaturePython operation metadata, recompute behavior and algorithms beyond the locked Profile/Helix scenarios remain unclaimed.',
    exactParityClaim: false,
  },
  status: 'verified-with-native-path-editable-subset',
}

await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({
  status: report.status,
  profileCommands: report.profileAlgorithm.commandCount,
  cuttingCommands: report.profileAlgorithm.cuttingCommandCount,
  helixScenarios: report.helixAlgorithm.scenarioCount,
  camCommands: report.qtDynamicOracle.camCommandCount,
  qtVersion: report.qtDynamicOracle.qtVersion,
  fcstdObjects: report.fcstdRoundTrip.webInspection.objectTypes,
  byteExact: report.fcstdRoundTrip.webTransparentPreservation.byteExact,
  report: relativePath(reportPath),
}, null, 2))
