import { spawnSync } from 'node:child_process'
import { readFile, writeFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { loadGoldenManifest } from './freecad-golden-contract.mjs'

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const manifest = await loadGoldenManifest(resolve(root, 'fixtures/freecad-golden/manifest.json'))
const booleanScenarios = manifest.scenarios.filter(({ scenario }) => ['fuse', 'cut', 'common'].includes(scenario.operation.type))
const freecadCommand = process.env.FREECAD_CMD || resolve(root, '.cache/freecad/install-native/bin/FreeCADCmd')
const nativeFactory = (await import('../native/occt-history/dist/bitbybit-occt-history.js')).default
const native = await nativeFactory()
const bitbybitFactory = (await import('@bitbybit-dev/occt/bitbybit-dev-occt/bitbybit-dev-occt.js')).default
const bitbybit = await bitbybitFactory({ locateFile: (path) => new URL(`../node_modules/@bitbybit-dev/occt/bitbybit-dev-occt/${path}`, import.meta.url).href })

const translation = (operation) => operation.placement?.translation || [0, 0, 0]

const buildShape = (module, operation) => {
  const [x, y, z] = translation(operation)
  if (operation.type === 'box') return module.MakeBoxFromPntAndDims(new module.gp_Pnt(x, y, z), operation.length, operation.width, operation.height)
  if (operation.type === 'sphere') return new module.BRepPrimAPI_MakeSphere(new module.gp_Pnt(x, y, z), operation.radius).Shape()
  const axis = new module.gp_Ax2(new module.gp_Pnt(x, y, z), new module.gp_Dir(0, 0, 1))
  if (operation.type === 'cylinder') return new module.BRepPrimAPI_MakeCylinder(axis, operation.radius, operation.height).Shape()
  if (operation.type === 'cone') return new module.BRepPrimAPI_MakeCone(axis, operation.radius1, operation.radius2, operation.height, (operation.angle || 360) * Math.PI / 180).Shape()
  const base = buildShape(module, operation.base)
  const tool = buildShape(module, operation.tool)
  const algorithm = operation.type === 'fuse' ? new module.BRepAlgoAPI_Fuse(base, tool) : operation.type === 'cut' ? new module.BRepAlgoAPI_Cut(base, tool) : new module.BRepAlgoAPI_Common(base, tool)
  algorithm.Build()
  if (!algorithm.IsDone()) throw new Error(`Bitbybit Boolean ${operation.type} did not complete.`)
  return algorithm.Shape()
}

const bitbybitSummary = (shape) => {
  const box = bitbybit.GetBoundingBox(shape)
  const validation = JSON.parse(bitbybit.BRepGraphValidate(shape))
  const graph = JSON.parse(bitbybit.BRepGraphAnalyze(shape))
  return {
    shapeType: graph.compounds > 0 ? 'Compound' : graph.solids === 1 ? 'Solid' : 'Shape', isNull: shape.IsNull(), isValid: validation.valid === true,
    solids: graph.solids, faces: graph.faces, edges: graph.edges, vertices: graph.vertices,
    volume: bitbybit.ComputeVolumeProperties(shape).Mass, area: bitbybit.ComputeSurfaceProperties(shape).Mass,
    boundingBox: { min: [box.XMin, box.YMin, box.ZMin], max: [box.XMax, box.YMax, box.ZMax] },
  }
}

const runFreecad = (entry) => {
  const execution = spawnSync(freecadCommand, [resolve(root, manifest.driver)], { cwd: root, encoding: 'utf8', timeout: 120000, maxBuffer: 10 * 1024 * 1024, env: { ...process.env, FREECAD_GOLDEN_SCENARIO: entry.file } })
  const line = `${execution.stdout || ''}\n${execution.stderr || ''}`.split(/\r?\n/).find((candidate) => candidate.startsWith('FREECAD_GOLDEN_RESULT='))
  if (execution.error || execution.status !== 0 || !line) throw new Error(`FreeCAD oracle failed for ${entry.id}: ${execution.error?.message || execution.stderr || execution.stdout}`)
  return JSON.parse(line.slice('FREECAD_GOLDEN_RESULT='.length))
}

const compare = (expected, actual, tolerance) => {
  const differences = []
  const known = []
  const scalar = (path, received, target, epsilon) => {
    if (!Number.isFinite(received) || Math.abs(received - target) > epsilon) differences.push(`${path}: expected ${target} +/- ${epsilon}, received ${received}`)
  }
  for (const key of ['isNull', 'isValid', 'solids']) if (actual[key] !== expected[key]) differences.push(`${key}: expected ${expected[key]}, received ${actual[key]}`)
  if (actual.shapeType !== expected.shapeType) known.push(`shapeType differs (${expected.shapeType} vs ${actual.shapeType})`)
  for (const key of ['faces', 'edges', 'vertices']) if (actual[key] !== expected[key]) differences.push(`${key}: expected ${expected[key]}, received ${actual[key]}`)
  scalar('volume', actual.volume, expected.volume, tolerance.scalar * 10)
  scalar('area', actual.area, expected.area, tolerance.scalar * 10)
  for (const axis of ['min', 'max']) for (let i = 0; i < 3; i += 1) scalar(`boundingBox.${axis}[${i}]`, actual.boundingBox[axis][i], expected.boundingBox[axis][i], tolerance.linear * 10)
  return { differences, known }
}

const reports = []
for (const entry of booleanScenarios) {
  const scenario = entry.scenario
  const oracle = runFreecad(entry)
  const bitbybitResult = bitbybitSummary(buildShape(bitbybit, scenario.operation))
  const nativeShape = (() => {
    const build = (operation) => {
      const [x, y, z] = translation(operation)
      if (operation.type === 'box') return native.makeBoxPlaced(operation.length, operation.width, operation.height, x, y, z)
      if (operation.type === 'cylinder') return native.makeCylinder(operation.radius, operation.height, x, y, z)
      if (operation.type === 'sphere') return native.makeSphere(operation.radius, x, y, z)
      if (operation.type === 'cone') return native.makeCone(operation.radius1, operation.radius2, operation.height, x, y, z)
      const base = build(operation.base); const tool = build(operation.tool)
      return native.booleanHistory(base, tool, operation.type)
    }
    return build(scenario.operation)
  })()
  const nativeResponse = nativeShape?.summary ? nativeShape : null
  const nativeSummary = nativeResponse ? nativeResponse.summary : native.shapeSummary(nativeShape)
  const bitbybitDiff = compare(oracle, bitbybitResult, scenario.tolerance)
  const nativeDiff = compare(oracle, nativeSummary, scenario.tolerance)
  reports.push({ id: entry.id, operation: scenario.operation.type, oracle: { freecadVersion: oracle.freecadVersion, expected: scenario.expected }, bitbybit: { summary: bitbybitResult, differences: bitbybitDiff }, native: { summary: nativeSummary, historyRecordCount: nativeResponse?.records?.length || 0, differences: nativeDiff } })
}

const report = { schemaVersion: 1, baselineId: manifest.baselineId, freecadCommit: '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d', nativeOcctVersion: native.occtVersion(), scenarioCount: reports.length, unknownDifferencesFail: true, reports }
const output = resolve(root, 'config/freecad-bitbybit-native-threeway.json')
await writeFile(output, `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify({ status: reports.some((item) => item.bitbybit.differences.differences.length || item.native.differences.differences.length) ? 'differences-found' : 'pass', scenarioCount: reports.length, output, sha256: createHash('sha256').update(JSON.stringify(report)).digest('hex') }, null, 2))
if (reports.some((item) => item.bitbybit.differences.differences.length || item.native.differences.differences.length)) process.exit(1)
