import { createHash } from 'node:crypto'
import { access, readFile, stat } from 'node:fs/promises'
import { execFileSync } from 'node:child_process'
import { resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const readJson = async (relative) => JSON.parse(await readFile(resolve(root, relative), 'utf8'))
const fail = (message) => { throw new Error(`FreeCAD desktop oracle: ${message}`) }
const config = await readJson('config/freecad-desktop-oracle.json')
const toolchain = await readJson('config/freecad-toolchain.json')
const source = resolve(root, '.cache/freecad/FreeCAD')
const build = resolve(root, config.buildDirectory)
const executable = resolve(root, `${config.installDirectory}/bin/FreeCAD`)
const probeReport = resolve(root, '.cache/freecad/reference-desktop.json')

const sourceRevision = execFileSync('git', ['-C', source, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
if (sourceRevision !== toolchain.source.commit) fail(`source revision ${sourceRevision} does not match ${toolchain.source.commit}.`)

const cache = await readFile(resolve(build, 'CMakeCache.txt'), 'utf8').catch(() => fail('CMakeCache.txt is missing; configure the desktop profile first.'))
const cacheValue = (name) => {
  const line = cache.split(/\r?\n/).find((entry) => entry.startsWith(`${name}:`))
  return line?.slice(line.indexOf('=') + 1)
}
const expectedOn = {
  AddonManager: 'BUILD_ADDONMGR', Assembly: 'BUILD_ASSEMBLY', BIM: 'BUILD_BIM', CAM: 'BUILD_CAM',
  Draft: 'BUILD_DRAFT', Fem: 'BUILD_FEM', Help: 'BUILD_HELP', Idf: 'BUILD_IDF', Import: 'BUILD_IMPORT',
  Inspection: 'BUILD_INSPECTION', Material: 'BUILD_MATERIAL', Measure: 'BUILD_MEASURE', Mesh: 'BUILD_MESH',
  MeshPart: 'BUILD_MESH_PART', OpenSCAD: 'BUILD_OPENSCAD', Part: 'BUILD_PART', PartDesign: 'BUILD_PART_DESIGN',
  Plot: 'BUILD_PLOT', Points: 'BUILD_POINTS', ReverseEngineering: 'BUILD_REVERSEENGINEERING', Robot: 'BUILD_ROBOT',
  Show: 'BUILD_SHOW', Sketcher: 'BUILD_SKETCHER', Spreadsheet: 'BUILD_SPREADSHEET', Start: 'BUILD_START',
  Surface: 'BUILD_SURFACE', TechDraw: 'BUILD_TECHDRAW', Test: 'BUILD_TEST', Tux: 'BUILD_TUX', Web: 'BUILD_WEB',
}
const expectedOff = {
  Cloud: 'BUILD_CLOUD', JtReader: 'BUILD_JTREADER', Sandbox: 'BUILD_SANDBOX',
}
if (cacheValue('BUILD_GUI') !== 'ON') fail('BUILD_GUI is not ON.')
for (const [module, key] of Object.entries(expectedOn)) if (cacheValue(key) !== 'ON') fail(`${module} requires ${key}=ON; got ${cacheValue(key) ?? '<missing>'}.`)
for (const [module, key] of Object.entries(expectedOff)) if (cacheValue(key) !== 'OFF') fail(`${module} requires ${key}=OFF; got ${cacheValue(key) ?? '<missing>'}.`)

await access(executable).catch(() => fail(`installed executable is missing: ${executable}`))
const versionOutput = execFileSync(executable, ['--console', '--version'], {
  cwd: root,
  encoding: 'utf8',
  env: {
    ...process.env,
    PYTHONPATH: `${resolve(root, config.sysrootDirectory, 'usr/lib/python3/dist-packages')}${process.env.PYTHONPATH ? `:${process.env.PYTHONPATH}` : ''}`,
    LD_LIBRARY_PATH: `${resolve(root, config.sysrootDirectory, 'usr/lib/x86_64-linux-gnu')}${process.env.LD_LIBRARY_PATH ? `:${process.env.LD_LIBRARY_PATH}` : ''}`,
  },
}).trim()
if (!versionOutput.includes('FreeCAD 1.1.1')) fail(`version output does not report FreeCAD 1.1.1: ${versionOutput}`)
const bytes = await readFile(executable)
const artifact = {
  path: `${config.installDirectory}/bin/FreeCAD`,
  sizeBytes: (await stat(executable)).size,
  sha256: createHash('sha256').update(bytes).digest('hex'),
  reportedVersion: versionOutput.split(/\r?\n/)[0],
}
if (config.artifact?.sha256 && config.artifact.sha256 !== artifact.sha256) fail(`artifact checksum mismatch: expected ${config.artifact.sha256}, got ${artifact.sha256}.`)

const report = await readJson('.cache/freecad/reference-desktop.json').catch(() => null)
if (!report) fail('34-module desktop probe report is missing; run probe:freecad-reference with FREECAD_ORACLE_PROFILE=desktop.')
if (report.baselineId !== config.baselineId || report.freecadVersion !== '1.1.1' || report.guiUp !== true || report.moduleCount !== 34 || report.modules?.length !== 34) {
  fail('desktop probe report is not a GUI-up 34-module FreeCAD 1.1.1 report.')
}
if (report.determinism?.schemaVersion !== 1 || report.determinism.normalizedProcessFields?.join(',') !== 'pointer-address,uuid,freecad-document-cache-run') fail('desktop probe does not declare deterministic process-field normalization.')
if (report.guiCommands?.stateProbe !== 'native-is-active-after-workbench-registration-with-Std_Expressions-qaction-guard' || report.guiCommands.stateCases?.join(',') !== 'noDocument,documentNoSelection,selectedPartBox' || report.guiCommands.directIsActiveBoundary !== 'Std_Expressions-only-qaction-guard-for-locked-1.1.1-null-qaction-crash' || report.guiCommands.stateCommandCount !== report.guiCommands.commands?.length) fail('desktop probe GUI state boundary is incomplete.')
const invalid = report.modules.filter((module) => !['compiled-importable', 'compiled-import-failure', 'gui-only-unprobeable', 'not-built'].includes(module.runtimeStatus))
if (invalid.length) fail(`probe contains invalid module statuses: ${invalid.map((module) => module.name).join(', ')}`)
const expectedObjects = ['Part::Box', 'Part::Cylinder', 'Part::Sphere', 'Part::Cone', 'Part::Feature', 'PartDesign::Feature', 'Sketcher::SketchObject']
if (!Array.isArray(report.objects) || report.objects.length !== expectedObjects.length) fail('desktop probe must include the seven core object fixtures.')
for (const typeId of expectedObjects) {
  const object = report.objects.find((candidate) => candidate.runtimeTypeId === typeId && candidate.typeId === typeId)
  if (!object?.available || !Array.isArray(object.properties)) fail(`runtime object fixture ${typeId} is missing or unavailable.`)
}
const propertyFlags = report.objects.flatMap((object) => object.properties || []).map((property) => property.status || [])
if (!propertyFlags.some((status) => status.includes('Hidden')) || !propertyFlags.some((status) => status.includes('Output'))) {
  fail('runtime property fixtures must include Hidden and Output status flags.')
}
for (const command of report.guiCommands?.commands ?? []) {
  if (!Array.isArray(command.registeredWorkbenches) || command.registeredWorkbenches.length === 0 || !Array.isArray(command.observations) || command.observations.length !== 1 || command.observations.some((observation) => !Object.hasOwn(observation, 'noDocument') || !Object.hasOwn(observation, 'documentNoSelection') || !Object.hasOwn(observation, 'selectedPartBox'))) {
    fail(`GUI command ${command.id || '<unknown>'} lacks no-document/document/selection observations.`)
  }
}
const runtimeObjects = report.runtimeObjects
if (!runtimeObjects?.available || runtimeObjects.candidateSource !== 'Document.supportedTypes' || !Array.isArray(runtimeObjects.types) || runtimeObjects.candidateCount !== runtimeObjects.types.length) {
  fail('complete Document.supportedTypes runtime object evidence is missing.')
}
if (runtimeObjects.availableCount + runtimeObjects.unavailableCount !== runtimeObjects.candidateCount) fail('runtime object summary counts are inconsistent.')
const runtimeTypeIds = new Set()
for (const candidate of runtimeObjects.types) {
  if (!candidate.typeId || runtimeTypeIds.has(candidate.typeId)) fail(`runtime object TypeId '${candidate.typeId || '<missing>'}' is invalid or duplicated.`)
  runtimeTypeIds.add(candidate.typeId)
  if (candidate.available) {
    if (candidate.probeStatus !== 'available' || typeof candidate.runtimeTypeId !== 'string' || !candidate.runtimeTypeId || !Array.isArray(candidate.properties)) fail(`${candidate.typeId} lacks runtime property metadata.`)
    if (candidate.properties.some((property) => !property.name || !property.typeId || !Array.isArray(property.status) || !Object.hasOwn(property, 'default'))) fail(`${candidate.typeId} has an incomplete property record.`)
  } else if (candidate.probeStatus !== 'unavailable' || !candidate.error) fail(`${candidate.typeId} lacks an explicit instantiation failure.`)
}
for (const typeId of expectedObjects) if (!runtimeTypeIds.has(typeId)) fail(`core TypeId ${typeId} is absent from Document.supportedTypes.`)
const runtimePropertyCount = runtimeObjects.types.reduce((total, candidate) => total + (candidate.properties?.length ?? 0), 0)

console.log(JSON.stringify({
  status: 'freecad-desktop-oracle-pass',
  baselineId: config.baselineId,
  sourceRevision,
  buildDirectory: config.buildDirectory,
  installDirectory: config.installDirectory,
  artifact,
  probe: {
    path: '.cache/freecad/reference-desktop.json',
    moduleCount: report.moduleCount,
    guiUp: report.guiUp,
    moduleStatusSummary: report.moduleStatusSummary,
    objectFixtureCount: report.objects.length,
    registeredObjectTypeCount: runtimeObjects.candidateCount,
    runtimeObjectCount: runtimeObjects.availableCount,
    unavailableObjectCount: runtimeObjects.unavailableCount,
    runtimePropertyCount,
  },
}, null, 2))
