// Run with Node 24.14.1 from the pinned checkout, after build.sh.
// The parent creates three independent Node processes and records their cells.
import { spawnSync } from 'node:child_process'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { cpus } from 'node:os'
import { median } from './summarize.mjs'
// The campaign at this commit already defines the input series, its seed, and
// the LTTB threshold. Importing them keeps one input protocol for the library
// instead of two, so these cells and the campaign's are directly comparable.
// Resolves when this file sits in article-tools/ inside the pinned checkout.
import { makeSeries, SEED, THRESHOLD } from '../benchmarks/campaign/protocol.mjs'

const SESSIONS = 3
const BLOCKS = 9
const WARMUP_CALLS = 5

const root = resolve(process.argv[2])
if (process.argv[3] !== '--child') {
  // build.sh records the toolchain that produced the modules under `root`.
  // Probing PATH here could describe a different rustc than the one that ran.
  const toolchainPath = resolve(root, 'toolchain.json')
  let toolchain
  try {
    toolchain = JSON.parse(readFileSync(toolchainPath, 'utf8'))
  } catch (error) {
    throw new Error(`Cannot read ${toolchainPath}; run build.sh first (${error.message})`)
  }
  const sessions = []
  for (let session = 0; session < SESSIONS; session++) {
    const child = spawnSync(
      process.execPath,
      [fileURLToPath(import.meta.url), root, '--child', String(session)],
      { encoding: 'utf8' }
    )
    if (child.status !== 0) throw new Error(child.stderr || 'Benchmark child failed')
    sessions.push(JSON.parse(child.stdout))
  }
  process.stdout.write(
    JSON.stringify(
      {
        measuredAt: new Date().toISOString(),
        node: process.version,
        v8: process.versions.v8,
        platform: process.platform,
        arch: process.arch,
        // os.cpus() is empty where the platform exposes no CPU information.
        cpu: cpus()[0]?.model ?? 'unknown',
        ...toolchain,
        boundary:
          'Node wasm-bindgen wrappers: input allocation/copy, Rust work, output copy/free, full output checksum',
        seed: SEED,
        threshold: THRESHOLD,
        input: 'benchmarks/campaign/protocol.mjs makeSeries: seeded random walk, monotone x',
        warmupCalls: WARMUP_CALLS,
        blocks: BLOCKS,
        sessions,
      },
      null,
      2
    ) + '\n'
  )
} else {
  const require = createRequire(import.meta.url)
  const modules = Object.fromEntries(
    ['scalar', 'simd'].map(variant => [
      variant,
      {
        downsample: require(resolve(root, variant, 'vizcrush_downsample.js')),
        aggregate: require(resolve(root, variant, 'vizcrush_aggregate.js')),
      },
    ])
  )
  let sink = 0
  const consume = values => {
    let sum = 0
    for (let i = 0; i < values.length; i++) sum += values[i] * ((i % 7) + 1)
    if (!Number.isFinite(sum)) throw new Error('Non-finite checksum')
    sink += sum
  }
  // Parity gate: equal lengths and an elementwise difference of exactly 0,
  // or the cell is not timed at all. The recorded value is the measured maximum.
  const parity = (label, simd, scalar) => {
    if (simd.length !== scalar.length) {
      throw new Error(
        `${label}: output lengths differ, simd ${simd.length} vs scalar ${scalar.length}`
      )
    }
    let maxAbsDiff = 0
    for (let i = 0; i < simd.length; i++) {
      maxAbsDiff = Math.max(maxAbsDiff, Math.abs(simd[i] - scalar[i]))
    }
    if (maxAbsDiff !== 0) throw new Error(`${label}: outputs differ (maxAbsDiff=${maxAbsDiff})`)
    return maxAbsDiff
  }
  const cells = []
  const session = Number(process.argv[4])
  for (const size of [1_000_000, 10_000_000]) {
    const { x, y } = makeSeries(size, SEED)
    for (const kernel of ['lttb', 'compute_stats']) {
      const run = variant =>
        kernel === 'lttb'
          ? modules[variant].downsample.lttb(x, y, THRESHOLD)
          : modules[variant].aggregate.compute_stats(y)
      const maxAbsDiff = parity(`${kernel} at ${size}`, run('simd'), run('scalar'))
      for (let i = 0; i < WARMUP_CALLS; i++)
        for (const variant of ['scalar', 'simd']) consume(run(variant))
      const samples = { scalar: [], simd: [] }
      const calls = size === 1_000_000 ? 20 : 3
      for (let block = 0; block < BLOCKS; block++) {
        const order = (session + block) % 2 ? ['simd', 'scalar'] : ['scalar', 'simd']
        for (const variant of order) {
          const start = performance.now()
          for (let call = 0; call < calls; call++) consume(run(variant))
          samples[variant].push((performance.now() - start) / calls)
        }
      }
      cells.push({
        size,
        kernel,
        callsPerBlock: calls,
        maxAbsDiff,
        samples,
        mediansMs: { scalar: median(samples.scalar), simd: median(samples.simd) },
        simdOverScalar: median(samples.simd) / median(samples.scalar),
      })
    }
  }
  if (!Number.isFinite(sink)) throw new Error('Benchmark sink is not finite')
  process.stdout.write(JSON.stringify({ session, benchmarkSink: sink, cells }))
}
