#!/usr/bin/env node

import { createHash } from "node:crypto"
import { execFileSync, execFile as execFileCb } from "node:child_process"
import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, renameSync, rmSync, statSync, readdirSync, chmodSync, cpSync } from "node:fs"
import { cp, readFile, writeFile, mkdir, rm, stat, readdir, rename } from "node:fs/promises"
import { createRequire } from "node:module"
import { basename, dirname, join, relative, resolve, delimiter } from "node:path"
import { homedir, tmpdir } from "node:os"
import { randomUUID } from "node:crypto"
import { promisify } from "node:util"

const execFileAsync = promisify(execFileCb)

// --- Platform guard ---
if (process.platform !== "darwin") {
  console.error("This script only runs on macOS.")
  process.exit(1)
}
if (process.arch !== "arm64") {
  console.error("This script only runs on Apple Silicon (arm64).")
  process.exit(1)
}

// --- Argument parsing ---
function parseArgs(argv) {
  const args = {}
  for (let i = 2; i < argv.length; i++) {
    if (argv[i].startsWith("--") && i + 1 < argv.length) {
      const key = argv[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())
      args[key] = argv[++i]
    }
  }
  return args
}

const args = parseArgs(process.argv)
const repoRoot = resolve(import.meta.dirname, "..", "..")

const runtimeDir = args.runtimeDir || join(repoRoot, "vendor", "openclaw-runtime")
const sourceConfigPath = args.sourceConfigPath || process.env.QJCLAW_MAC_OPENCLAW_CONFIG_PATH || join(homedir(), ".openclaw", "openclaw.json")
const sourceNode = args.sourceNode || process.env.QJCLAW_MAC_NODE || resolveCommand("node")
const sourcePythonRoot = args.sourcePythonRoot || process.env.QJCLAW_MAC_PYTHON_ROOT || null
const sourceOpenClawEntry = args.sourceOpenclawEntry || process.env.QJCLAW_MAC_OPENCLAW_ENTRY || resolveOpenClawEntry()
const sourceFfprobe = args.sourceFfprobe || process.env.QJCLAW_MAC_FFPROBE || null
const requirementsPath = args.requirementsPath || join(repoRoot, "build", "runtime", "python", "runtime-requirements.lock.txt")
const gatewayPort = args.gatewayPort ? parseInt(args.gatewayPort, 10) : 18889
const gatewayToken = args.gatewayToken || "qjc-bundled-runtime-token"

// --- Helpers ---
function resolveCommand(name) {
  try {
    const result = execFileSync("which", [name], { encoding: "utf8" }).trim()
    return result || null
  } catch {
    return null
  }
}

function resolveOpenClawEntry() {
  const cmd = resolveCommand("openclaw")
  if (!cmd) return null
  const cliDir = dirname(cmd)
  const candidate = join(cliDir, "node_modules", "openclaw", "openclaw.mjs")
  if (existsSync(candidate)) return candidate
  return null
}

function fileFingerprint(filePath, includeHash = false) {
  const st = statSync(filePath)
  const fp = { path: resolve(filePath), length: st.size, lastWriteTimeUtc: st.mtime.toISOString() }
  if (includeHash) {
    fp.hashSha256 = hashFileSha256(filePath)
  }
  return fp
}

function hashFileSha256(filePath) {
  const hash = createHash("sha256")
  const data = readFileSync(filePath)
  hash.update(data)
  return hash.digest("hex")
}

function computeMaterializationKey(inputs) {
  const serialized = JSON.stringify(inputs)
  return createHash("sha256").update(serialized).digest("hex")
}

function chmodPlusX(filePath) {
  const mode = statSync(filePath).mode
  chmodSync(filePath, mode | 0o111)
}

function validateExists(path, label) {
  if (!existsSync(path)) {
    throw new Error(`${label} not found at ${path}`)
  }
}

function resolveOptionalBinarySource(explicitPath, candidatePaths = [], commandNames = []) {
  if (explicitPath) {
    const resolved = resolve(explicitPath)
    if (!existsSync(resolved)) {
      throw new Error(`Optional binary source was not found at ${resolved}`)
    }
    return resolved
  }

  for (const candidate of candidatePaths) {
    if (!candidate) continue
    const resolved = resolve(candidate)
    if (existsSync(resolved)) return resolved
  }

  for (const name of commandNames) {
    const found = resolveCommand(name)
    if (found && existsSync(found)) return resolve(found)
  }

  return null
}

function getImageioFfmpegBinaryPath(pythonDir) {
  const sitePackagesDir = join(pythonDir, "lib", "python*/site-packages/imageio_ffmpeg/binaries")
  // Use glob-style search
  const { globSync } = requireGlob()
  if (!globSync) {
    // Fallback: manual search
    return findImageioFfmpegManually(pythonDir)
  }
  const matches = globSync(join(pythonDir, "lib", "*", "site-packages", "imageio_ffmpeg", "binaries", "ffmpeg-*"))
  if (matches.length === 0) {
    throw new Error(`imageio-ffmpeg binaries directory not found under ${pythonDir}`)
  }
  // Filter for macOS binaries (not .exe)
  const macBinaries = matches.filter(f => !f.endsWith(".exe") && !f.endsWith(".zip"))
  if (macBinaries.length === 0) {
    throw new Error(`imageio-ffmpeg did not materialize a bundled macOS ffmpeg under ${pythonDir}`)
  }
  macBinaries.sort()
  return macBinaries[0]
}

function findImageioFfmpegManually(pythonDir) {
  const libDir = join(pythonDir, "lib")
  if (!existsSync(libDir)) {
    throw new Error(`Python lib directory not found at ${libDir}`)
  }
  for (const entry of readdirSync(libDir)) {
    const binariesDir = join(libDir, entry, "site-packages", "imageio_ffmpeg", "binaries")
    if (!existsSync(binariesDir)) continue
    const candidates = readdirSync(binariesDir)
      .filter(f => f.startsWith("ffmpeg-") && !f.endsWith(".exe") && !f.endsWith(".zip"))
      .sort()
    if (candidates.length > 0) {
      return join(binariesDir, candidates[0])
    }
  }
  throw new Error(`imageio-ffmpeg binaries not found under ${pythonDir}`)
}

function requireGlob() {
  try {
    return { globSync: require("glob").globSync }
  } catch {
    return { globSync: null }
  }
}

const require = createRequire(import.meta.url)

async function directoryMetrics(dirPath) {
  if (!existsSync(dirPath)) return { fileCount: 0, sizeBytes: 0 }
  const st = statSync(dirPath)
  if (!st.isDirectory()) return { fileCount: 1, sizeBytes: st.size }

  let fileCount = 0
  let sizeBytes = 0

  async function walk(dir) {
    for (const entry of await readdir(dir, { withFileTypes: true })) {
      const fullPath = join(dir, entry.name)
      if (entry.isDirectory()) {
        await walk(fullPath)
      } else if (entry.isFile()) {
        fileCount++
        const s = await stat(fullPath)
        sizeBytes += s.size
      }
    }
  }
  await walk(dirPath)
  return { fileCount, sizeBytes }
}

async function runtimePayloadStats(runtimeDirPath) {
  const rootMetrics = await directoryMetrics(runtimeDirPath)
  const topLevelBreakdown = []
  if (existsSync(runtimeDirPath)) {
    const entries = await readdir(runtimeDirPath, { withFileTypes: true })
    entries.sort((a, b) => a.name.localeCompare(b.name))
    for (const entry of entries) {
      const metrics = await directoryMetrics(join(runtimeDirPath, entry.name))
      topLevelBreakdown.push({
        path: entry.name,
        fileCount: metrics.fileCount,
        sizeBytes: metrics.sizeBytes,
      })
    }
  }
  return { fileCount: rootMetrics.fileCount, sizeBytes: rootMetrics.sizeBytes, topLevelBreakdown }
}

function relativePath(root, target) {
  return relative(root, target).replace(/\\/g, "/")
}

function removeOptionalLiteralPath(runtimeRoot, targetPath, removedPaths) {
  if (!existsSync(targetPath)) return false
  try { rmSync(targetPath, { recursive: true, force: true }) } catch { return false }
  if (existsSync(targetPath)) return false
  if (removedPaths) removedPaths.push(relativePath(runtimeRoot, targetPath))
  return true
}

function removeOptionalNamedDirectories(runtimeRoot, root, names) {
  const examples = []
  let removedCount = 0

  function walk(dir) {
    if (!existsSync(dir)) return
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue
      const fullPath = join(dir, entry.name)
      walk(fullPath)
      if (names.includes(entry.name)) {
        try { rmSync(fullPath, { recursive: true, force: true }) } catch { continue }
        if (!existsSync(fullPath)) {
          removedCount++
          if (examples.length < 12) examples.push(relativePath(runtimeRoot, fullPath))
        }
      }
    }
  }
  walk(root)
  return { removedCount, examples }
}

function removeOptionalFilesByPattern(runtimeRoot, root, patterns) {
  const examples = []
  let removedCount = 0

  function walk(dir) {
    if (!existsSync(dir)) return
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
      const fullPath = join(dir, entry.name)
      if (entry.isDirectory()) {
        walk(fullPath)
      } else if (entry.isFile()) {
        const name = entry.name
        if (patterns.some(p => matchSimplePattern(name, p))) {
          try { rmSync(fullPath, { force: true }) } catch { continue }
          if (!existsSync(fullPath)) {
            removedCount++
            if (examples.length < 12) examples.push(relativePath(runtimeRoot, fullPath))
          }
        }
      }
    }
  }
  walk(root)
  return { removedCount, examples }
}

function matchSimplePattern(name, pattern) {
  // Simple glob: only supports *.ext
  if (pattern.startsWith("*.")) {
    return name.endsWith(pattern.slice(1))
  }
  return name === pattern
}

function pruneOpenClawDocsPreservingTemplates(runtimeRoot, docsDir) {
  const preservedDirs = []
  if (!existsSync(docsDir)) return { pruned: false, preservedDirs: [] }

  const preserveRoot = join(dirname(docsDir), `.docs-preserve-${randomUUID()}`)
  const templateDirs = [
    join(docsDir, "reference", "templates"),
    join(docsDir, "zh-CN", "reference", "templates"),
  ]

  try {
    for (const templateDir of templateDirs) {
      if (!existsSync(templateDir)) continue
      const relDir = relativePath(docsDir, templateDir)
      const preservedPath = join(preserveRoot, relDir)
      mkdirSync(dirname(preservedPath), { recursive: true })
      cpSync(templateDir, preservedPath, { recursive: true })
    }

    rmSync(docsDir, { recursive: true, force: true })
    mkdirSync(docsDir, { recursive: true })

    for (const templateDir of templateDirs) {
      const relDir = relativePath(docsDir, templateDir)
      const preservedPath = join(preserveRoot, relDir)
      if (!existsSync(preservedPath)) continue
      mkdirSync(dirname(templateDir), { recursive: true })
      cpSync(preservedPath, templateDir, { recursive: true })
      preservedDirs.push(relativePath(runtimeRoot, templateDir))
    }
  } finally {
    if (existsSync(preserveRoot)) {
      rmSync(preserveRoot, { recursive: true, force: true })
    }
  }

  return { pruned: true, preservedDirs }
}

function getRequiredWorkspaceTemplatePaths(openClawPackageDir) {
  return [
    join(openClawPackageDir, "docs", "reference", "templates", "AGENTS.md"),
    join(openClawPackageDir, "docs", "reference", "templates", "BOOTSTRAP.md"),
    join(openClawPackageDir, "docs", "reference", "templates", "HEARTBEAT.md"),
    join(openClawPackageDir, "docs", "reference", "templates", "IDENTITY.md"),
    join(openClawPackageDir, "docs", "reference", "templates", "SOUL.md"),
    join(openClawPackageDir, "docs", "reference", "templates", "TOOLS.md"),
    join(openClawPackageDir, "docs", "reference", "templates", "USER.md"),
  ]
}

function invokeRuntimePayloadCleanup(runtimeRoot, openClawPackageDir, pythonDir) {
  const removedLiteralPaths = []
  const docsCleanup = pruneOpenClawDocsPreservingTemplates(runtimeRoot, join(openClawPackageDir, "docs"))

  const literalTargets = [
    join(openClawPackageDir, "README.md"),
    join(openClawPackageDir, "README-header.png"),
    join(openClawPackageDir, "CHANGELOG.md"),
    join(pythonDir, "Doc"),
    join(pythonDir, "Tools"),
    join(pythonDir, "lib", "test"),
    join(pythonDir, "lib", "idlelib"),
    join(pythonDir, "lib", "tkinter"),
    join(pythonDir, "lib", "turtledemo"),
    join(pythonDir, "lib", "ensurepip"),
    join(pythonDir, "lib", "site-packages", "setuptools"),
    join(pythonDir, "lib", "site-packages", "wheel"),
  ]
  for (const target of literalTargets) {
    removeOptionalLiteralPath(runtimeRoot, target, removedLiteralPaths)
  }

  const pythonCacheDirs = removeOptionalNamedDirectories(runtimeRoot, pythonDir, ["__pycache__"])
  const sourceMapFiles = removeOptionalFilesByPattern(runtimeRoot, openClawPackageDir, ["*.map"])
  const pythonCompiledFiles = removeOptionalFilesByPattern(runtimeRoot, pythonDir, ["*.pyc", "*.pyo"])

  return {
    cleanupRulesVersion: 3,
    prunedOpenClawDocs: docsCleanup.pruned,
    preservedWorkspaceTemplateDirs: docsCleanup.preservedDirs,
    removedLiteralPaths,
    removedLiteralPathCount: removedLiteralPaths.length,
    removedPycacheDirectories: pythonCacheDirs,
    removedSourceMapFiles: sourceMapFiles,
    removedCompiledPythonFiles: pythonCompiledFiles,
  }
}

function testRuntimePayloadReady(runtimeDirPath) {
  const openClawPackageDir = join(runtimeDirPath, "openclaw", "package")
  const requiredPaths = [
    join(runtimeDirPath, "node", "bin", "node"),
    join(runtimeDirPath, "openclaw", "index.js"),
    join(openClawPackageDir, "openclaw.mjs"),
    join(openClawPackageDir, "package.json"),
    join(runtimeDirPath, "config", "openclaw.json"),
    join(runtimeDirPath, "python", "bin", "python3"),
    join(runtimeDirPath, "python", "python-manifest.json"),
    join(runtimeDirPath, "python", "runtime-requirements.lock.txt"),
    join(runtimeDirPath, "ffmpeg", "bin", "ffmpeg"),
    join(runtimeDirPath, "ffmpeg", "bin", "ffprobe"),
    join(runtimeDirPath, "playwright-browsers"),
    join(runtimeDirPath, "runtime-manifest.json"),
    join(runtimeDirPath, "README.md"),
    ...getRequiredWorkspaceTemplatePaths(openClawPackageDir),
  ]
  return requiredPaths.every(p => existsSync(p))
}

// --- Main ---
async function main() {
  // Resolve source Python root
  let resolvedSourcePythonRoot = sourcePythonRoot
  if (!resolvedSourcePythonRoot) {
    const pythonCmd = resolveCommand("python3")
    if (pythonCmd) {
      // Walk up to find the Python root (the parent of bin/)
      resolvedSourcePythonRoot = dirname(dirname(pythonCmd))
    }
  }

  // Resolve source ffprobe
  const resolvedSourceFfprobe = resolveOptionalBinarySource(
    sourceFfprobe,
    [
      join(homedir(), "ffmpeg", "current", "bin", "ffprobe"),
      "/opt/homebrew/bin/ffprobe",
      "/usr/local/bin/ffprobe",
    ],
    ["ffprobe"]
  )

  if (!resolvedSourceFfprobe) {
    throw new Error(
      "ffprobe is required for bundled video analysis workflows. " +
      "Install FFmpeg with ffprobe (e.g. brew install ffmpeg), or pass --source-ffprobe /path/to/ffprobe."
    )
  }

  // Validate all sources
  validateExists(sourceConfigPath, "OpenClaw config")
  validateExists(sourceNode, "Node executable")
  validateExists(sourceOpenClawEntry, "OpenClaw entry")
  validateExists(requirementsPath, "Runtime requirements lock file")
  validateExists(resolvedSourcePythonRoot, "Python root directory")
  validateExists(join(resolvedSourcePythonRoot, "bin", "python3"), "Python executable (bin/python3)")

  const sourceOpenClawDir = dirname(sourceOpenClawEntry)
  const sourceOpenClawPackageJsonPath = join(sourceOpenClawDir, "package.json")
  validateExists(sourceOpenClawDir, "OpenClaw package directory")
  validateExists(sourceOpenClawPackageJsonPath, "OpenClaw package.json")

  // Compute materialization key for caching
  const materializationInputs = {
    schemaVersion: 6,
    gatewayPort,
    gatewayToken,
    sourceConfig: fileFingerprint(sourceConfigPath, true),
    sourceNodeExe: fileFingerprint(sourceNode),
    sourcePythonExe: fileFingerprint(join(resolvedSourcePythonRoot, "bin", "python3")),
    sourceOpenClawEntry: fileFingerprint(sourceOpenClawEntry),
    sourceOpenClawPackageJson: fileFingerprint(sourceOpenClawPackageJsonPath, true),
    requirements: fileFingerprint(requirementsPath, true),
    sourceFfprobeExe: resolvedSourceFfprobe ? fileFingerprint(resolvedSourceFfprobe) : null,
  }
  const materializationKey = computeMaterializationKey(materializationInputs)

  // Check cache
  const existingManifestPath = join(runtimeDir, "runtime-manifest.json")
  if (existsSync(existingManifestPath) && testRuntimePayloadReady(runtimeDir)) {
    try {
      const existingManifest = JSON.parse(readFileSync(existingManifestPath, "utf8"))
      if (existingManifest.materializationKey === materializationKey) {
        console.log(`Reusing bundled runtime payload at ${runtimeDir} (materialization key match)`)
        console.log(JSON.stringify(buildRuntimeSummary(runtimeDir, existingManifest, true), null, 2))
        return
      }
    } catch (e) {
      console.warn(`Existing runtime manifest could not be reused: ${e.message}`)
    }
  }

  // Create staging directory
  const stagingDir = join(dirname(runtimeDir), `openclaw-runtime.staging.${randomUUID()}`)
  const nodeBinDir = join(stagingDir, "node", "bin")
  const openclawDir = join(stagingDir, "openclaw")
  const openclawPackageDir = join(openclawDir, "package")
  const configDir = join(stagingDir, "config")
  const pythonDir = join(stagingDir, "python")
  const ffmpegBinDir = join(stagingDir, "ffmpeg", "bin")
  const playwrightBrowsersDir = join(stagingDir, "playwright-browsers")

  const payloadNodeExe = join(nodeBinDir, "node")
  const payloadFfmpegExe = join(ffmpegBinDir, "ffmpeg")
  const payloadFfprobeExe = join(ffmpegBinDir, "ffprobe")
  const payloadPythonExe = join(pythonDir, "bin", "python3")
  const wrapperPath = join(openclawDir, "index.js")
  const configPath = join(configDir, "openclaw.json")
  const pythonManifestPath = join(pythonDir, "python-manifest.json")
  const pythonRequirementsCopy = join(pythonDir, "runtime-requirements.lock.txt")
  const manifestPath = join(stagingDir, "runtime-manifest.json")
  const payloadReadmePath = join(stagingDir, "README.md")

  if (existsSync(stagingDir)) {
    rmSync(stagingDir, { recursive: true, force: true })
  }

  try {
    mkdirSync(nodeBinDir, { recursive: true })
    mkdirSync(openclawDir, { recursive: true })
    mkdirSync(configDir, { recursive: true })
    mkdirSync(ffmpegBinDir, { recursive: true })
    mkdirSync(playwrightBrowsersDir, { recursive: true })

    // --- Copy Node ---
    console.log(`Copying Node from ${sourceNode}`)
    copyFileSync(sourceNode, payloadNodeExe)
    chmodPlusX(payloadNodeExe)

    // --- Copy OpenClaw package ---
    console.log(`Copying OpenClaw package from ${sourceOpenClawDir}`)
    cpSync(sourceOpenClawDir, openclawPackageDir, { recursive: true })

    // --- Copy and rewrite config ---
    const config = JSON.parse(readFileSync(sourceConfigPath, "utf8"))
    if (!config.gateway) config.gateway = {}
    if (!config.gateway.auth) config.gateway.auth = {}
    config.gateway.mode = "local"
    config.gateway.bind = "loopback"
    config.gateway.port = gatewayPort
    config.gateway.auth.mode = "token"
    config.gateway.auth.token = gatewayToken
    writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8")

    // --- Copy Python ---
    console.log(`Copying bundled Python runtime from ${resolvedSourcePythonRoot}`)
    cpSync(resolvedSourcePythonRoot, pythonDir, { recursive: true })
    if (!existsSync(payloadPythonExe)) {
      throw new Error(`Bundled Python executable not found at ${payloadPythonExe}`)
    }
    chmodPlusX(payloadPythonExe)

    // --- Upgrade pip ---
    console.log("Upgrading pip in bundled Python runtime")
    execFileSync(payloadPythonExe, ["-m", "pip", "install", "--disable-pip-version-check", "--upgrade", "pip"], {
      stdio: "inherit",
    })

    // --- Install requirements ---
    console.log(`Installing locked runtime dependencies from ${requirementsPath}`)
    execFileSync(payloadPythonExe, ["-m", "pip", "install", "--disable-pip-version-check", "-r", requirementsPath], {
      stdio: "inherit",
    })

    // --- Install Playwright Chromium ---
    console.log(`Installing bundled Playwright Chromium into ${playwrightBrowsersDir}`)
    execFileSync(payloadPythonExe, ["-m", "playwright", "install", "chromium"], {
      stdio: "inherit",
      env: { ...process.env, PLAYWRIGHT_BROWSERS_PATH: playwrightBrowsersDir },
    })

    // --- Copy requirements lock file ---
    copyFileSync(requirementsPath, pythonRequirementsCopy)

    // --- Generate Python manifest ---
    const manifestScript = `
import importlib.metadata as metadata
import json
import pathlib
import sys

requirements_path = pathlib.Path(sys.argv[1])
requested = []
for line in requirements_path.read_text(encoding='utf-8').splitlines():
    line = line.strip()
    if not line or line.startswith('#'):
        continue
    package_name = line.split('==', 1)[0].strip()
    requested.append({
        'name': package_name,
        'version': metadata.version(package_name)
    })

payload = {
    'pythonVersion': sys.version.split()[0],
    'requestedPackages': requested,
    'resolvedPackages': sorted(
        [
            {'name': dist.metadata['Name'], 'version': dist.version}
            for dist in metadata.distributions()
            if dist.metadata['Name']
        ],
        key=lambda item: item['name'].lower(),
    ),
}
print(json.dumps(payload))
`.trim()
    const pythonManifestJson = execFileSync(payloadPythonExe, ["-c", manifestScript, pythonRequirementsCopy], {
      encoding: "utf8",
    })
    const pythonManifestProbe = JSON.parse(pythonManifestJson)
    const materializedAt = new Date().toISOString()
    const pythonManifest = {
      pythonVersion: pythonManifestProbe.pythonVersion,
      executable: "bin/python3",
      requirementsPath: "runtime-requirements.lock.txt",
      requestedPackages: pythonManifestProbe.requestedPackages,
      resolvedPackages: pythonManifestProbe.resolvedPackages,
      materializedAt,
    }
    writeFileSync(pythonManifestPath, JSON.stringify(pythonManifest, null, 2), "utf8")

    // --- Copy ffmpeg from imageio-ffmpeg ---
    const imageioFfmpegSource = getImageioFfmpegBinaryPath(pythonDir)
    console.log(`Copying ffmpeg from ${imageioFfmpegSource}`)
    copyFileSync(imageioFfmpegSource, payloadFfmpegExe)
    chmodPlusX(payloadFfmpegExe)
    if (!existsSync(payloadFfmpegExe)) {
      throw new Error(`Bundled ffmpeg executable was not materialized at ${payloadFfmpegExe}`)
    }

    // --- Copy ffprobe ---
    console.log(`Copying ffprobe from ${resolvedSourceFfprobe}`)
    copyFileSync(resolvedSourceFfprobe, payloadFfprobeExe)
    chmodPlusX(payloadFfprobeExe)

    // --- Read OpenClaw package info ---
    const openClawPackageJson = existsSync(sourceOpenClawPackageJsonPath)
      ? JSON.parse(readFileSync(sourceOpenClawPackageJsonPath, "utf8"))
      : null

    // --- Write runtime-manifest.json ---
    const manifest = {
      platform: "darwin",
      arch: "arm64",
      bundledNodeExecutable: "node/bin/node",
      packagedOpenClawEntry: "openclaw/package/openclaw.mjs",
      packagedSkillsRoot: "openclaw/package/skills",
      defaultConfigPath: "config/openclaw.json",
      pythonExecutable: "python/bin/python3",
      pythonManifestPath: "python/python-manifest.json",
      requirementsPath: "python/runtime-requirements.lock.txt",
      ffmpegExecutable: "ffmpeg/bin/ffmpeg",
      ffprobeExecutable: "ffmpeg/bin/ffprobe",
      playwrightBrowsersPath: "playwright-browsers",
      gatewayPort,
      gatewayToken,
      materializedAt,
      materializationKey,
      materializationInputs,
      pythonVersion: pythonManifest.pythonVersion,
      openClawVersion: openClawPackageJson?.version ?? null,
      installedPythonPackages: pythonManifest.requestedPackages.map(p => `${p.name}==${p.version}`),
    }
    writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8")

    // --- Write openclaw/index.js wrapper ---
    const wrapper = `const { readFileSync } = require('node:fs');
const path = require('node:path');
const { pathToFileURL } = require('node:url');

async function main() {
  const runtimeDir = path.join(__dirname, '..');
  const manifestPath = path.join(runtimeDir, 'runtime-manifest.json');
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
  const entryRelative = typeof manifest.packagedOpenClawEntry === 'string' ? manifest.packagedOpenClawEntry.trim() : '';
  if (!entryRelative) {
    throw new Error('runtime-manifest.json is missing packagedOpenClawEntry');
  }

  const entryPath = path.resolve(runtimeDir, entryRelative);
  process.argv[1] = entryPath;
  await import(pathToFileURL(entryPath).href);
}

main().catch((error) => {
  const message = error instanceof Error ? error.stack || error.message : String(error);
  console.error(message);
  process.exit(1);
});
`
    writeFileSync(wrapperPath, wrapper, "utf8")

    // --- Write README ---
    const readme = `# Bundled Runtime Payload

Immutable packaged payload under \`vendor/openclaw-runtime/\` includes:

- \`node/bin/node\`
- \`openclaw/index.js\`
- \`openclaw/package/openclaw.mjs\`
- \`config/openclaw.json\`
- \`python/bin/python3\`
- \`python/python-manifest.json\`
- \`python/runtime-requirements.lock.txt\`
- \`ffmpeg/bin/ffmpeg\`
- \`ffmpeg/bin/ffprobe\`
- \`playwright-browsers/\`

Mutable runtime data lives outside the installer payload and should be created under Electron \`userData/runtime/\`.

The payload is considered ready only when the Node entry, OpenClaw package, Python executable, Python manifest, FFmpeg/FFprobe tools, and locked Python imports all validate successfully on the target machine.
`
    writeFileSync(payloadReadmePath, readme.trimStart(), "utf8")

    // --- Cleanup ---
    const payloadStatsBeforeCleanup = await runtimePayloadStats(stagingDir)
    const cleanupSummary = invokeRuntimePayloadCleanup(stagingDir, openclawPackageDir, pythonDir)
    const payloadStatsAfterCleanup = await runtimePayloadStats(stagingDir)
    const cleanupBytesRemoved = payloadStatsBeforeCleanup.sizeBytes - payloadStatsAfterCleanup.sizeBytes
    const cleanupFilesRemoved = payloadStatsBeforeCleanup.fileCount - payloadStatsAfterCleanup.fileCount
    console.log(`Runtime payload cleanup removed ${cleanupFilesRemoved} files and ${cleanupBytesRemoved} bytes`)

    // --- Update manifest with stats ---
    manifest.sizeBytes = payloadStatsAfterCleanup.sizeBytes
    manifest.fileCount = payloadStatsAfterCleanup.fileCount
    manifest.topLevelBreakdown = payloadStatsAfterCleanup.topLevelBreakdown
    manifest.payloadStats = {
      beforeCleanup: payloadStatsBeforeCleanup,
      afterCleanup: payloadStatsAfterCleanup,
      bytesRemoved: cleanupBytesRemoved,
      filesRemoved: cleanupFilesRemoved,
    }
    manifest.cleanupSummary = cleanupSummary
    writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf8")

    // --- Validate required files ---
    const requiredPaths = [
      payloadNodeExe,
      wrapperPath,
      join(openclawPackageDir, "openclaw.mjs"),
      join(openclawPackageDir, "package.json"),
      configPath,
      payloadPythonExe,
      pythonManifestPath,
      payloadFfmpegExe,
      payloadFfprobeExe,
      manifestPath,
      payloadReadmePath,
      ...getRequiredWorkspaceTemplatePaths(openclawPackageDir),
    ]
    for (const rp of requiredPaths) {
      if (!existsSync(rp)) {
        throw new Error(`Bundled runtime materialization failed; missing ${rp}`)
      }
    }

    // --- Atomic swap ---
    if (existsSync(runtimeDir)) {
      rmSync(runtimeDir, { recursive: true, force: true })
    }
    renameSync(stagingDir, runtimeDir)

    console.log(JSON.stringify(buildRuntimeSummary(runtimeDir, manifest, false), null, 2))
  } finally {
    if (stagingDir && existsSync(stagingDir)) {
      rmSync(stagingDir, { recursive: true, force: true })
    }
  }
}

function buildRuntimeSummary(runtimeDirPath, manifest, cacheHit) {
  return {
    ok: true,
    cacheHit,
    platform: manifest.platform ?? null,
    arch: manifest.arch ?? null,
    materializationKey: manifest.materializationKey,
    runtimeDir: runtimeDirPath,
    nodeExecutable: join(runtimeDirPath, "node", "bin", "node"),
    openClawEntry: join(runtimeDirPath, "openclaw", "index.js"),
    openClawPackageDir: join(runtimeDirPath, "openclaw", "package"),
    configPath: join(runtimeDirPath, "config", "openclaw.json"),
    manifestPath: join(runtimeDirPath, "runtime-manifest.json"),
    pythonExecutable: join(runtimeDirPath, "python", "bin", "python3"),
    pythonManifestPath: join(runtimeDirPath, "python", "python-manifest.json"),
    requirementsPath: join(runtimeDirPath, "python", "runtime-requirements.lock.txt"),
    ffmpegExecutable: join(runtimeDirPath, "ffmpeg", "bin", "ffmpeg"),
    ffprobeExecutable: join(runtimeDirPath, "ffmpeg", "bin", "ffprobe"),
    playwrightBrowsersPath: join(runtimeDirPath, "playwright-browsers"),
    gatewayPort: manifest.gatewayPort,
    gatewayToken: manifest.gatewayToken,
    installedPythonPackages: manifest.installedPythonPackages,
    sizeBytes: manifest.sizeBytes ?? null,
    fileCount: manifest.fileCount ?? null,
    topLevelBreakdown: manifest.topLevelBreakdown ?? [],
    payloadStats: manifest.payloadStats ?? null,
    cleanupSummary: manifest.cleanupSummary ?? null,
  }
}

main().catch((error) => {
  console.error(error instanceof Error ? error.stack || error.message : String(error))
  process.exit(1)
})
