import { createHash } from 'node:crypto'
import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { resolve } from 'node:path'
import { isDeepStrictEqual } from 'node:util'
import { unzipSync } from 'fflate'
import { createWebCadFacade } from '../src/facade/mockFacade'
import type { ColorListValue } from '../src/facade/types'

const root = resolve(new URL('..', import.meta.url).pathname)
const executable = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-desktop/bin/FreeCADCmd')
const sysroot = resolve(root, '.cache/freecad/sysroot')
const scriptPath = resolve(root, 'scripts/freecad-property-colorlist-roundtrip.py')
const outputDirectory = resolve(root, '.cache/freecad/property-colorlist-roundtrip')
const nativePath = resolve(outputDirectory, 'native-initial.FCStd')
const webPath = resolve(outputDirectory, 'web-edited.FCStd')
const resavedPath = resolve(outputDirectory, 'native-resaved.FCStd')
const reportPath = resolve(root, 'config/freecad-property-colorlist-roundtrip.json')
if (!existsSync(executable)) throw new Error(`FreeCAD PropertyColorList roundtrip executable is missing: ${executable}`)
await mkdir(outputDirectory, { recursive: true })

const runNative = (mode: 'create' | 'verify', path: string, resaved = '') => {
  const execution = spawnSync(executable, ['--python-path', resolve(sysroot, 'usr/lib/python3/dist-packages'), scriptPath], {
    cwd: root,
    encoding: 'utf8',
    timeout: 120_000,
    maxBuffer: 32 * 1024 * 1024,
    env: {
      ...process.env,
      FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_MODE: mode,
      FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_PATH: path,
      FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_RESAVED_PATH: resaved,
      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}` : ''}`,
    },
  })
  const output = `${execution.stdout || ''}\n${execution.stderr || ''}`
  const marker = 'FREECAD_PROPERTY_COLORLIST_ROUNDTRIP_RESULT='
  const line = output.split(/\r?\n/).find((candidate) => candidate.includes(marker))
  if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD PropertyColorList ${mode} probe failed with status ${execution.status}: ${execution.error?.message || output.trim()}`)
  return JSON.parse(line.slice(line.indexOf(marker) + marker.length))
}

const initial: ColorListValue = [[17 / 255, 34 / 255, 51 / 255, 68 / 255]]
const target: ColorListValue = [
  [51 / 255, 102 / 255, 153 / 255, 204 / 255],
  [17 / 255, 34 / 255, 51 / 255, 68 / 255],
]
const native = runNative('create', nativePath)
const nativeBytes = new Uint8Array(await readFile(nativePath))
const facade = createWebCadFacade({ runtimeMode: 'mock' })
const inspectedBefore = facade.project.fcstd.inspect(nativeBytes)
const property = (inspection: typeof inspectedBefore) => inspection.objects.find((object) => object.name === 'ColorListProbe')?.properties.find((candidate) => candidate.name === 'ColourList')
const resourcePath = property(inspectedBefore)?.resourcePath
if (!resourcePath) throw new Error('Native PropertyColorList archive has no external resource path.')
const editedBytes = facade.project.fcstd.rewriteColorList(nativeBytes, {
  objectName: 'ColorListProbe',
  propertyName: 'ColourList',
  value: target,
  expectedValue: initial,
})
const inspectedAfter = facade.project.fcstd.inspect(editedBytes)
await facade.project.dispose()
await writeFile(webPath, editedBytes)
const initialFiles = unzipSync(nativeBytes)
const editedFiles = unzipSync(editedBytes)
const opaquePaths = Object.keys(initialFiles).filter((path) => path.toLowerCase() !== 'document.xml' && path !== resourcePath)
const opaqueEntriesPreserved = opaquePaths.every((path) => Buffer.from(initialFiles[path]).equals(Buffer.from(editedFiles[path] ?? new Uint8Array())))
const editedResourceChanged = Boolean(editedFiles[resourcePath]) && !Buffer.from(initialFiles[resourcePath]).equals(Buffer.from(editedFiles[resourcePath]))
const withoutEditedProperty = (inspection: typeof inspectedBefore) => inspection.objects.map((object) => ({ ...object, properties: object.name === 'ColorListProbe' ? object.properties.filter((candidate) => candidate.name !== 'ColourList') : object.properties }))
const semanticObjectsPreserved = isDeepStrictEqual(withoutEditedProperty(inspectedBefore), withoutEditedProperty(inspectedAfter))
const nativeAfter = runNative('verify', webPath, resavedPath)
const resavedBytes = new Uint8Array(await readFile(resavedPath))
const approximatelyTarget = (value: unknown) => Array.isArray(value) && value.length === target.length && value.every((color, colorIndex) => Array.isArray(color) && color.length === 4 && color.every((channel, channelIndex) => typeof channel === 'number' && Math.abs(channel - target[colorIndex][channelIndex]) <= 1e-7))
const nativeOutputMatches = [nativeAfter.result.reopened, nativeAfter.result.resaved].every((snapshot: any) => approximatelyTarget(snapshot.object.value)) && isDeepStrictEqual(nativeAfter.result.reopened.object.value, nativeAfter.result.resaved.object.value)
const objectSetPreserved = isDeepStrictEqual(native.result.objectSet, nativeAfter.result.reopened.objectSet) && isDeepStrictEqual(native.result.objectSet, nativeAfter.result.resaved.objectSet)
const resourcePathPreserved = property(inspectedAfter)?.resourcePath === resourcePath
const zeroUnknownDrift = opaqueEntriesPreserved && editedResourceChanged && semanticObjectsPreserved && nativeOutputMatches && objectSetPreserved && resourcePathPreserved
const report = {
  schemaVersion: 1,
  status: zeroUnknownDrift ? 'pass' : 'fail',
  baselineId: 'freecad-1.1.1-property-colorlist-roundtrip',
  freecadVersion: native.freecadVersion,
  gitCommit: native.gitCommit,
  propertyType: 'App::PropertyColorList',
  archives: {
    nativeInitial: { path: '.cache/freecad/property-colorlist-roundtrip/native-initial.FCStd', bytes: nativeBytes.byteLength, sha256: createHash('sha256').update(nativeBytes).digest('hex') },
    webEdited: { path: '.cache/freecad/property-colorlist-roundtrip/web-edited.FCStd', bytes: editedBytes.byteLength, sha256: createHash('sha256').update(editedBytes).digest('hex') },
    nativeResaved: { path: '.cache/freecad/property-colorlist-roundtrip/native-resaved.FCStd', bytes: resavedBytes.byteLength, sha256: createHash('sha256').update(resavedBytes).digest('hex') },
  },
  nativeInitial: native.result,
  web: {
    before: property(inspectedBefore),
    after: property(inspectedAfter),
    resource: {
      path: resourcePath,
      pathPreserved: resourcePathPreserved,
      beforeBytes: initialFiles[resourcePath].byteLength,
      afterBytes: editedFiles[resourcePath]?.byteLength ?? 0,
      changed: editedResourceChanged,
    },
    objectSetPreserved,
    semanticObjectsPreserved,
    opaquePathsPreserved: opaquePaths.length,
    opaqueEntriesPreserved,
  },
  nativeAfter: nativeAfter.result,
  classification: {
    requestedValue: target,
    webValue: property(inspectedAfter)?.value,
    reopenedValue: nativeAfter.result.reopened.object.value,
    resavedValue: nativeAfter.result.resaved.object.value,
    nativeOutputMatches,
    unknownSemanticDrift: !zeroUnknownDrift,
    zeroUnknownDrift,
  },
}
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: report.status, output: 'config/freecad-property-colorlist-roundtrip.json', values: report.classification, resource: report.web.resource, opaqueEntriesPreserved }, null, 2))
