import { createHash } from 'node:crypto'
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const sha256 = (value) => createHash('sha256').update(value).digest('hex')
const run = (command, args, options = {}) => {
  const result = spawnSync(command, args, { encoding: 'utf8', ...options })
  if (result.error || result.status !== 0) throw new Error(`${command} failed: ${result.error?.message || result.stderr || result.stdout}`)
  return result
}
const artifactInfo = async (path) => {
  const bytes = await readFile(resolve(root, path))
  const metadata = await stat(resolve(root, path))
  return { bytes: bytes.byteLength, sha256: sha256(bytes), mode: metadata.mode.toString(8) }
}

const camoticsRevision = execFileSync('git', ['-C', resolve(root, 'CAMotics'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
const cbangRevision = execFileSync('git', ['-C', resolve(root, '.cache/camotics-native/cbang'), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
const camoticsVersionProbe = run(resolve(root, 'CAMotics/camotics'), ['--version'])
const tplVersionProbe = run(resolve(root, 'CAMotics/tplang'), ['--version'])
const camoticsVersion = `${camoticsVersionProbe.stdout}${camoticsVersionProbe.stderr}`.trim()
const tplVersion = `${tplVersionProbe.stdout}${tplVersionProbe.stderr}`.trim()
if (camoticsVersion !== tplVersion) throw new Error(`CAMotics/TPL version mismatch: ${camoticsVersion}/${tplVersion}`)

const tplTests = run(resolve(root, 'CAMotics/tests/testHarness'), ['--no-color'], { cwd: resolve(root, 'CAMotics/tests/tplTests') })
const passed = Number(tplTests.stdout.match(/^Passed\s+(\d+)/m)?.[1] ?? -1)
const failed = Number(tplTests.stdout.match(/^Failed\s+(\d+)/m)?.[1] ?? -1)
if (passed < 0 || failed < 0) throw new Error('Could not parse CAMotics TPL test output.')

const tplExample = run(resolve(root, 'CAMotics/tplang'), [resolve(root, 'CAMotics/examples/box/box.tpl')])
const tplExampleBytes = Buffer.from(tplExample.stdout)
const guiDir = await mkdtemp(join(tmpdir(), 'camotics-native-gui-'))
let guiSmoke
try {
  const gui = spawnSync('xvfb-run', ['-a', '-s', '-screen 0 1280x800x24 +extension GLX', 'bash', '-lc', `
    set -u
    smoke_dir="$CAMOTICS_SMOKE_DIR"
    export XDG_CONFIG_HOME="$smoke_dir/config" XDG_CACHE_HOME="$smoke_dir/cache" XDG_DATA_HOME="$smoke_dir/data" LIBGL_ALWAYS_SOFTWARE=1
    mkdir -p "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_DATA_HOME"
    "$CAMOTICS_BINARY" >"$smoke_dir/stdout" 2>"$smoke_dir/stderr" &
    pid=$!
    found=0
    for _ in $(seq 1 100); do
      if xdotool search --onlyvisible --name "^CAMotics$" >/dev/null 2>&1; then found=1; break; fi
      sleep 0.1
    done
    if [ "$found" -eq 1 ]; then
      wid=$(xdotool search --onlyvisible --name "^CAMotics$" | head -n1)
      xwininfo -id "$wid" >"$smoke_dir/xwininfo"
    fi
    alive=0
    kill -0 "$pid" 2>/dev/null && alive=1
    kill -TERM "$pid" 2>/dev/null || true
    wait "$pid" 2>/dev/null || true
    printf '%s\n' "$found" >"$smoke_dir/found"
    printf '%s\n' "$alive" >"$smoke_dir/alive"
  `], {
    encoding: 'utf8',
    env: { ...process.env, CAMOTICS_SMOKE_DIR: guiDir, CAMOTICS_BINARY: resolve(root, 'CAMotics/camotics') },
    timeout: 30_000,
  })
  if (gui.error || gui.status !== 0) throw new Error(`Xvfb GUI smoke failed: ${gui.error?.message || gui.stderr || gui.stdout}`)
  const found = (await readFile(join(guiDir, 'found'), 'utf8')).trim() === '1'
  const alive = (await readFile(join(guiDir, 'alive'), 'utf8')).trim() === '1'
  const startupStdout = await readFile(join(guiDir, 'stdout'), 'utf8')
  const startupStderr = await readFile(join(guiDir, 'stderr'), 'utf8')
  const xwininfo = found ? await readFile(join(guiDir, 'xwininfo'), 'utf8') : ''
  const numberFrom = (label) => Number(xwininfo.match(new RegExp(`^  ${label}:\\s+(\\d+)`, 'm'))?.[1] ?? -1)
  const mapState = xwininfo.match(/^  Map State:\s+(.+)$/m)?.[1]?.trim() ?? ''
  guiSmoke = {
    status: found && alive && mapState === 'IsViewable' && !/Fatal error|FATAL/i.test(startupStderr) ? 'pass' : 'fail',
    processAliveBeforeTerminate: alive,
    browserRuntime: false,
    window: { title: xwininfo.match(/^xwininfo: Window id: \S+ "([^"]+)"$/m)?.[1] ?? '', width: numberFrom('Width'), height: numberFrom('Height'), depth: numberFrom('Depth'), mapState },
    startupBannerSha256: sha256(Buffer.from(startupStdout)),
    startupStderrSha256: sha256(Buffer.from(startupStderr)),
  }
} finally {
  await rm(guiDir, { recursive: true, force: true })
}
if (guiSmoke.status !== 'pass') throw new Error(`CAMotics GUI smoke did not pass: ${JSON.stringify(guiSmoke)}`)

const artifacts = {}
for (const path of ['CAMotics/camotics', 'CAMotics/tplang']) artifacts[path] = await artifactInfo(path)
const cliArtifacts = {}
for (const path of ['CAMotics/camsim', 'CAMotics/gcodetool', 'CAMotics/planner', 'CAMotics/build/camotics.so']) cliArtifacts[path] = await artifactInfo(path)
const cliSmoke = {}
for (const path of ['CAMotics/camsim', 'CAMotics/gcodetool', 'CAMotics/planner']) {
  const probe = run(resolve(root, path), ['--help'])
  cliSmoke[path] = { exitCode: probe.status, containsUsage: `${probe.stdout}${probe.stderr}`.includes('Usage:') }
}
const config = {
  schemaVersion: 2,
  status: 'pass',
  source: {
    path: 'CAMotics',
    repository: 'https://github.com/CauldronDevelopmentLLC/CAMotics.git',
    revision: camoticsRevision,
    version: camoticsVersion,
  },
  cbang: {
    path: '.cache/camotics-native/cbang',
    repository: 'https://github.com/CauldronDevelopmentLLC/cbang.git',
    revision: cbangRevision,
    version: '1.7.2',
    v8PointerCompression: false,
  },
  build: {
    mode: 'release-native-qt5-tpl-cli',
    compiler: execFileSync('g++', ['--version'], { encoding: 'utf8' }).split('\n')[0],
    sconsVersion: execFileSync('scons', ['--version'], { encoding: 'utf8' }).split('\n')[0],
    nodeV8PackageVersion: execFileSync('dpkg-query', ['-W', '-f=${Version}', 'libnode-dev'], { encoding: 'utf8' }).trim(),
    qtVersion: execFileSync('qmake', ['-query', 'QT_VERSION'], { encoding: 'utf8' }).trim(),
    options: ['strict=0', 'cxxstd=c++17', 'v8_compress_pointers=0'],
    withGui: true,
    withTpl: true,
    browserExecutable: false,
  },
  artifacts,
  cliArtifacts,
  cliSmoke,
  tplTests: { command: 'cd CAMotics/tests/tplTests && ../testHarness --no-color', passed, failed, outputSha256: sha256(Buffer.from(tplTests.stdout)) },
  tplExample: { input: 'CAMotics/examples/box/box.tpl', bytes: tplExampleBytes.byteLength, lines: tplExample.stdout.trimEnd().split('\n').length, sha256: sha256(tplExampleBytes) },
  guiSmoke,
  pipelineBoundary: {
    workspaceNativeGuiAvailable: true,
    workspaceNativeTplAvailable: true,
    workspaceNativeCliAvailable: true,
    browserNativeGuiAvailable: false,
    browserNativeTplAvailable: false,
    nativeRole: 'host-native-qt5-tpl-cli-sidecar',
    generationAuthority: 'camotics-source-stage',
    canonicalGcodeParser: 'linuxcnc-wasm',
    camoticsParsesCanonicalPipelineGcode: false,
    order: ['cad', 'opencamlib-wasm', 'camotics-source-stage', 'gcode', 'linuxcnc-wasm-parse-execute'],
  },
}
await writeFile(resolve(root, 'config/camotics-native-artifact.json'), `${JSON.stringify(config, null, 2)}\n`)
console.log(JSON.stringify(config, null, 2))
