param(
  [string]$RuntimeDir,
  [string]$SourceConfigPath,
  [string]$SourceOpenClawEntry,
  [string]$SourceNodeExe,
  [string]$SourcePythonExe,
  [string]$SourceFfprobeExe,
  [string]$RequirementsPath,
  [int]$GatewayPort = 18889,
  [string]$GatewayToken = 'qjc-bundled-runtime-token'
)

$ErrorActionPreference = 'Stop'

function Get-FileFingerprint {
  param(
    [Parameter(Mandatory = $true)]
    [string]$Path,
    [switch]$IncludeHash
  )

  $item = Get-Item -LiteralPath $Path -ErrorAction Stop
  $fingerprint = [ordered]@{
    path = [System.IO.Path]::GetFullPath($Path)
    length = $item.Length
    lastWriteTimeUtc = $item.LastWriteTimeUtc.ToString('o')
  }

  if ($IncludeHash) {
    $fingerprint.hashSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
  }

  return $fingerprint
}

function Get-MaterializationKey {
  param(
    [Parameter(Mandatory = $true)]
    [hashtable]$Inputs
  )

  $serialized = $Inputs | ConvertTo-Json -Depth 20 -Compress
  $bytes = [System.Text.Encoding]::UTF8.GetBytes($serialized)
  $sha256 = [System.Security.Cryptography.SHA256Managed]::Create()
  try {
    $hashBytes = $sha256.ComputeHash($bytes)
  } finally {
    $sha256.Dispose()
  }
  return ([System.BitConverter]::ToString($hashBytes)).Replace('-', '').ToLowerInvariant()
}

function Test-RuntimePayloadReady {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeDir
  )

  $openClawPackageDir = Join-Path $RuntimeDir 'openclaw\package'
  $requiredPaths = @(
    (Join-Path $RuntimeDir 'node\node.exe'),
    (Join-Path $RuntimeDir 'openclaw\index.js'),
    (Join-Path $openClawPackageDir 'openclaw.mjs'),
    (Join-Path $openClawPackageDir 'package.json'),
    (Join-Path $RuntimeDir 'config\openclaw.json'),
    (Join-Path $RuntimeDir 'python\python.exe'),
    (Join-Path $RuntimeDir 'python\python-manifest.json'),
    (Join-Path $RuntimeDir 'python\runtime-requirements.lock.txt'),
    (Join-Path $RuntimeDir 'ffmpeg\bin\ffmpeg.exe'),
    (Join-Path $RuntimeDir 'ffmpeg\bin\ffprobe.exe'),
    (Join-Path $RuntimeDir 'playwright-browsers'),
    (Join-Path $RuntimeDir 'runtime-manifest.json'),
    (Join-Path $RuntimeDir 'README.md')
  )
  $requiredPaths += Get-RequiredWorkspaceTemplatePaths -OpenClawPackageDir $openClawPackageDir

  foreach ($requiredPath in $requiredPaths) {
    if (-not (Test-Path $requiredPath)) {
      return $false
    }
  }

  return $true
}

function Get-RelativePath {
  param(
    [Parameter(Mandatory = $true)]
    [string]$Root,
    [Parameter(Mandatory = $true)]
    [string]$Path
  )

  $normalizedRoot = [System.IO.Path]::GetFullPath($Root).TrimEnd('\', '/')
  $normalizedPath = [System.IO.Path]::GetFullPath($Path)
  $rootPrefix = $normalizedRoot + '\'
  if ($normalizedPath.StartsWith($rootPrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
    return $normalizedPath.Substring($rootPrefix.Length).Replace('\', '/')
  }

  $rootUri = [System.Uri]($rootPrefix.Replace('\', '/'))
  $pathUri = [System.Uri]($normalizedPath.Replace('\', '/'))
  return [System.Uri]::UnescapeDataString($rootUri.MakeRelativeUri($pathUri).ToString()).Replace('\', '/')
}

function Get-DirectoryMetrics {
  param(
    [Parameter(Mandatory = $true)]
    [string]$Path
  )

  if (-not (Test-Path $Path)) {
    return [ordered]@{
      fileCount = 0
      sizeBytes = [int64]0
    }
  }

  $item = Get-Item -LiteralPath $Path -ErrorAction Stop
  if (-not $item.PSIsContainer) {
    return [ordered]@{
      fileCount = 1
      sizeBytes = [int64]$item.Length
    }
  }

  $files = Get-ChildItem -LiteralPath $Path -Recurse -File -Force -ErrorAction SilentlyContinue
  $sizeBytes = ($files | Measure-Object -Property Length -Sum).Sum
  if ($null -eq $sizeBytes) {
    $sizeBytes = 0
  }

  return [ordered]@{
    fileCount = @($files).Count
    sizeBytes = [int64]$sizeBytes
  }
}

function Get-RuntimePayloadStats {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeDir
  )

  $rootMetrics = Get-DirectoryMetrics -Path $RuntimeDir
  $topLevelBreakdown = @()
  if (Test-Path $RuntimeDir) {
    $topLevelBreakdown = @(
      Get-ChildItem -LiteralPath $RuntimeDir -Force -ErrorAction SilentlyContinue |
        Sort-Object Name |
        ForEach-Object {
          $metrics = Get-DirectoryMetrics -Path $_.FullName
          [ordered]@{
            path = Get-RelativePath -Root $RuntimeDir -Path $_.FullName
            fileCount = $metrics.fileCount
            sizeBytes = [int64]$metrics.sizeBytes
          }
        }
    )
  }

  return [ordered]@{
    fileCount = $rootMetrics.fileCount
    sizeBytes = [int64]$rootMetrics.sizeBytes
    topLevelBreakdown = $topLevelBreakdown
  }
}

function Resolve-OptionalBinarySource {
  param(
    [string]$ExplicitPath,
    [string[]]$CandidatePaths = @(),
    [string[]]$CommandNames = @()
  )

  if ($ExplicitPath) {
    $resolvedExplicit = [System.IO.Path]::GetFullPath($ExplicitPath)
    if (-not (Test-Path $resolvedExplicit)) {
      throw "Optional binary source was not found at $resolvedExplicit"
    }
    return $resolvedExplicit
  }

  foreach ($candidatePath in $CandidatePaths) {
    if (-not $candidatePath) {
      continue
    }
    $resolvedCandidate = [System.IO.Path]::GetFullPath($candidatePath)
    if (Test-Path $resolvedCandidate) {
      return $resolvedCandidate
    }
  }

  foreach ($commandName in $CommandNames) {
    $command = Get-Command $commandName -ErrorAction SilentlyContinue
    if ($command -and $command.Source -and (Test-Path $command.Source)) {
      return [System.IO.Path]::GetFullPath($command.Source)
    }
  }

  return $null
}

function Get-ImageioFfmpegBinaryPath {
  param(
    [Parameter(Mandatory = $true)]
    [string]$PythonDir
  )

  $binariesDir = Join-Path $PythonDir 'Lib\site-packages\imageio_ffmpeg\binaries'
  if (-not (Test-Path $binariesDir)) {
    throw "imageio-ffmpeg binaries directory was not found at $binariesDir"
  }

  $candidate = Get-ChildItem -LiteralPath $binariesDir -File -Filter 'ffmpeg-*.exe' -ErrorAction SilentlyContinue |
    Sort-Object Name |
    Select-Object -First 1
  if (-not $candidate) {
    throw "imageio-ffmpeg did not materialize a bundled ffmpeg executable under $binariesDir"
  }

  return $candidate.FullName
}

function Remove-OptionalLiteralPath {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeRoot,
    [Parameter(Mandatory = $true)]
    [string]$Path,
    [System.Collections.IList]$RemovedPaths
  )

  if (-not (Test-Path $Path)) {
    return $false
  }

  Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue
  if (Test-Path $Path) {
    return $false
  }

  if ($null -ne $RemovedPaths) {
    $RemovedPaths.Add((Get-RelativePath -Root $RuntimeRoot -Path $Path)) | Out-Null
  }
  return $true
}

function Remove-OptionalNamedDirectories {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeRoot,
    [Parameter(Mandatory = $true)]
    [string]$Root,
    [Parameter(Mandatory = $true)]
    [string[]]$Names
  )

  $examples = [System.Collections.Generic.List[string]]::new()
  $removedCount = 0
  $candidates = @(
    Get-ChildItem -LiteralPath $Root -Recurse -Directory -Force -ErrorAction SilentlyContinue |
      Where-Object { $Names -contains $_.Name } |
      Sort-Object FullName -Descending
  )

  foreach ($candidate in $candidates) {
    if (-not (Test-Path $candidate.FullName)) {
      continue
    }
    Remove-Item -LiteralPath $candidate.FullName -Recurse -Force -ErrorAction SilentlyContinue
    if (-not (Test-Path $candidate.FullName)) {
      $removedCount += 1
      if ($examples.Count -lt 12) {
        $examples.Add((Get-RelativePath -Root $RuntimeRoot -Path $candidate.FullName)) | Out-Null
      }
    }
  }

  return [ordered]@{
    removedCount = $removedCount
    examples = @($examples)
  }
}

function Remove-OptionalFilesByPattern {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeRoot,
    [Parameter(Mandatory = $true)]
    [string]$Root,
    [Parameter(Mandatory = $true)]
    [string[]]$Patterns
  )

  $examples = [System.Collections.Generic.List[string]]::new()
  $removedCount = 0
  $candidates = @(
    Get-ChildItem -LiteralPath $Root -Recurse -File -Force -ErrorAction SilentlyContinue |
      Where-Object {
        $name = $_.Name
        foreach ($pattern in $Patterns) {
          if ($name -like $pattern) {
            return $true
          }
        }
        return $false
      }
  )

  foreach ($candidate in $candidates) {
    if (-not (Test-Path $candidate.FullName)) {
      continue
    }
    Remove-Item -LiteralPath $candidate.FullName -Force -ErrorAction SilentlyContinue
    if (-not (Test-Path $candidate.FullName)) {
      $removedCount += 1
      if ($examples.Count -lt 12) {
        $examples.Add((Get-RelativePath -Root $RuntimeRoot -Path $candidate.FullName)) | Out-Null
      }
    }
  }

  return [ordered]@{
    removedCount = $removedCount
    examples = @($examples)
  }
}

function Get-RequiredWorkspaceTemplatePaths {
  param(
    [Parameter(Mandatory = $true)]
    [string]$OpenClawPackageDir
  )

  return @(
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\AGENTS.md'),
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\BOOTSTRAP.md'),
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\HEARTBEAT.md'),
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\IDENTITY.md'),
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\SOUL.md'),
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\TOOLS.md'),
    (Join-Path $OpenClawPackageDir 'docs\reference\templates\USER.md')
  )
}

function Prune-OpenClawDocsPreservingTemplates {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeRoot,
    [Parameter(Mandatory = $true)]
    [string]$DocsDir
  )

  $preservedDirs = [System.Collections.Generic.List[string]]::new()
  if (-not (Test-Path $DocsDir)) {
    return [ordered]@{
      pruned = $false
      preservedDirs = @()
    }
  }

  $preserveRoot = Join-Path $RuntimeRoot ('.docs-preserve-' + [guid]::NewGuid().ToString('N'))
  $templateDirs = @(
    (Join-Path $DocsDir 'reference\templates'),
    (Join-Path $DocsDir 'zh-CN\reference\templates')
  )

  try {
    foreach ($templateDir in $templateDirs) {
      if (-not (Test-Path $templateDir)) {
        continue
      }

      $relativeDir = Get-RelativePath -Root $DocsDir -Path $templateDir
      $preservedPath = Join-Path $preserveRoot $relativeDir
      $preservedParent = Split-Path $preservedPath -Parent
      if ($preservedParent) {
        New-Item -ItemType Directory -Force -Path $preservedParent | Out-Null
      }
      Copy-Item -LiteralPath $templateDir -Destination $preservedPath -Recurse -Force
    }

    Remove-Item -LiteralPath $DocsDir -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Force -Path $DocsDir | Out-Null

    foreach ($templateDir in $templateDirs) {
      $relativeDir = Get-RelativePath -Root $DocsDir -Path $templateDir
      $preservedPath = Join-Path $preserveRoot $relativeDir
      if (-not (Test-Path $preservedPath)) {
        continue
      }

      $restoreParent = Split-Path $templateDir -Parent
      if ($restoreParent) {
        New-Item -ItemType Directory -Force -Path $restoreParent | Out-Null
      }
      Copy-Item -LiteralPath $preservedPath -Destination $templateDir -Recurse -Force
      $preservedDirs.Add((Get-RelativePath -Root $RuntimeRoot -Path $templateDir)) | Out-Null
    }
  } finally {
    if (Test-Path $preserveRoot) {
      Remove-Item -LiteralPath $preserveRoot -Recurse -Force -ErrorAction SilentlyContinue
    }
  }

  return [ordered]@{
    pruned = $true
    preservedDirs = @($preservedDirs)
  }
}

function Invoke-RuntimePayloadCleanup {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeRoot,
    [Parameter(Mandatory = $true)]
    [string]$OpenClawPackageDir,
    [Parameter(Mandatory = $true)]
    [string]$PythonDir
  )

  $removedLiteralPaths = [System.Collections.Generic.List[string]]::new()
  $docsCleanup = Prune-OpenClawDocsPreservingTemplates -RuntimeRoot $RuntimeRoot -DocsDir (Join-Path $OpenClawPackageDir 'docs')

  foreach ($targetPath in @(
    (Join-Path $OpenClawPackageDir 'README.md'),
    (Join-Path $OpenClawPackageDir 'README-header.png'),
    (Join-Path $OpenClawPackageDir 'CHANGELOG.md'),
    (Join-Path $PythonDir 'Doc'),
    (Join-Path $PythonDir 'Tools'),
    (Join-Path $PythonDir 'Lib\test'),
    (Join-Path $PythonDir 'Lib\idlelib'),
    (Join-Path $PythonDir 'Lib\tkinter'),
    (Join-Path $PythonDir 'Lib\turtledemo'),
    (Join-Path $PythonDir 'Lib\ensurepip'),
    (Join-Path $PythonDir 'Lib\site-packages\setuptools'),
    (Join-Path $PythonDir 'Lib\site-packages\wheel')
  )) {
    [void](Remove-OptionalLiteralPath -RuntimeRoot $RuntimeRoot -Path $targetPath -RemovedPaths $removedLiteralPaths)
  }

  $pythonCacheDirs = Remove-OptionalNamedDirectories -RuntimeRoot $RuntimeRoot -Root $PythonDir -Names @('__pycache__')
  $sourceMapFiles = Remove-OptionalFilesByPattern -RuntimeRoot $RuntimeRoot -Root $OpenClawPackageDir -Patterns @('*.map')
  $pythonCompiledFiles = Remove-OptionalFilesByPattern -RuntimeRoot $RuntimeRoot -Root $PythonDir -Patterns @('*.pyc', '*.pyo')

  return [ordered]@{
    cleanupRulesVersion = 3
    prunedOpenClawDocs = [bool]$docsCleanup.pruned
    preservedWorkspaceTemplateDirs = @($docsCleanup.preservedDirs)
    removedLiteralPaths = @($removedLiteralPaths)
    removedLiteralPathCount = $removedLiteralPaths.Count
    removedPycacheDirectories = $pythonCacheDirs
    removedSourceMapFiles = $sourceMapFiles
    removedCompiledPythonFiles = $pythonCompiledFiles
  }
}

function New-RuntimeSummary {
  param(
    [Parameter(Mandatory = $true)]
    [string]$RuntimeDir,
    [Parameter(Mandatory = $true)]
    [psobject]$Manifest,
    [bool]$CacheHit = $false
  )

  return [PSCustomObject]@{
    ok = $true
    cacheHit = $CacheHit
    materializationKey = $Manifest.materializationKey
    runtimeDir = $RuntimeDir
    nodeExecutable = (Join-Path $RuntimeDir 'node\node.exe')
    openClawEntry = (Join-Path $RuntimeDir 'openclaw\index.js')
    openClawPackageDir = (Join-Path $RuntimeDir 'openclaw\package')
    configPath = (Join-Path $RuntimeDir 'config\openclaw.json')
    manifestPath = (Join-Path $RuntimeDir 'runtime-manifest.json')
    pythonExecutable = (Join-Path $RuntimeDir 'python\python.exe')
    pythonManifestPath = (Join-Path $RuntimeDir 'python\python-manifest.json')
    requirementsPath = (Join-Path $RuntimeDir 'python\runtime-requirements.lock.txt')
    ffmpegExecutable = (Join-Path $RuntimeDir 'ffmpeg\bin\ffmpeg.exe')
    ffprobeExecutable = (Join-Path $RuntimeDir 'ffmpeg\bin\ffprobe.exe')
    playwrightBrowsersPath = (Join-Path $RuntimeDir 'playwright-browsers')
    gatewayPort = $Manifest.gatewayPort
    gatewayToken = $Manifest.gatewayToken
    installedPythonPackages = $Manifest.installedPythonPackages
    sizeBytes = if ($null -ne $Manifest.sizeBytes) { [int64]$Manifest.sizeBytes } else { $null }
    fileCount = if ($null -ne $Manifest.fileCount) { [int]$Manifest.fileCount } else { $null }
    topLevelBreakdown = if ($null -ne $Manifest.topLevelBreakdown) { $Manifest.topLevelBreakdown } else { @() }
    payloadStats = if ($null -ne $Manifest.payloadStats) { $Manifest.payloadStats } else { $null }
    cleanupSummary = if ($null -ne $Manifest.cleanupSummary) { $Manifest.cleanupSummary } else { $null }
  }
}

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $RuntimeDir) {
  $RuntimeDir = Join-Path $repoRoot 'vendor\openclaw-runtime'
}
if (-not $SourceConfigPath) {
  $SourceConfigPath = Join-Path $env:USERPROFILE '.openclaw\openclaw.json'
}
if (-not $SourceNodeExe) {
  $nodeCommand = Get-Command node -ErrorAction Stop
  $SourceNodeExe = $nodeCommand.Source
}
if (-not $SourcePythonExe) {
  $pythonCommand = Get-Command python -ErrorAction Stop
  $SourcePythonExe = $pythonCommand.Source
}
if (-not $RequirementsPath) {
  $RequirementsPath = Join-Path $repoRoot 'build\runtime\python\runtime-requirements.lock.txt'
}
if (-not $SourceOpenClawEntry) {
  $openclawCommand = Get-Command openclaw.cmd -ErrorAction SilentlyContinue
  if (-not $openclawCommand) {
    $openclawCommand = Get-Command openclaw -ErrorAction SilentlyContinue
  }
  if (-not $openclawCommand) {
    throw 'Unable to locate a local openclaw CLI installation.'
  }

  $cliDir = Split-Path $openclawCommand.Source -Parent
  $candidate = Join-Path $cliDir 'node_modules\openclaw\openclaw.mjs'
  if (-not (Test-Path $candidate)) {
    throw "Unable to locate openclaw.mjs next to $($openclawCommand.Source)"
  }
  $SourceOpenClawEntry = $candidate
}

$RuntimeDir = [System.IO.Path]::GetFullPath($RuntimeDir)
$SourceConfigPath = [System.IO.Path]::GetFullPath($SourceConfigPath)
$SourceNodeExe = [System.IO.Path]::GetFullPath($SourceNodeExe)
$SourceOpenClawEntry = [System.IO.Path]::GetFullPath($SourceOpenClawEntry)
$SourcePythonExe = [System.IO.Path]::GetFullPath($SourcePythonExe)
$RequirementsPath = [System.IO.Path]::GetFullPath($RequirementsPath)
$SourceOpenClawDir = Split-Path $SourceOpenClawEntry -Parent
$SourcePythonDir = Split-Path $SourcePythonExe -Parent
$SourceOpenClawPackageJsonPath = Join-Path $SourceOpenClawDir 'package.json'
$ResolvedSourceFfprobeExe = Resolve-OptionalBinarySource `
  -ExplicitPath $SourceFfprobeExe `
  -CandidatePaths @(
    (Join-Path $env:USERPROFILE 'ffmpeg\current\bin\ffprobe.exe'),
    (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages\Gyan.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg\bin\ffprobe.exe'),
    (Join-Path $env:LOCALAPPDATA 'Microsoft\WinGet\Packages\BtbN.FFmpeg_Microsoft.Winget.Source_8wekyb3d8bbwe\ffmpeg\bin\ffprobe.exe'),
    (Join-Path $env:ProgramFiles 'ffmpeg\bin\ffprobe.exe'),
    'C:\ffmpeg\bin\ffprobe.exe'
  ) `
  -CommandNames @('ffprobe')

if (-not $ResolvedSourceFfprobeExe) {
  throw "ffprobe.exe is required for bundled Douyin/video analysis workflows. Install FFmpeg with ffprobe, place it at $env:USERPROFILE\ffmpeg\current\bin\ffprobe.exe, or pass -SourceFfprobeExe C:\path\to\ffprobe.exe."
}

if (-not (Test-Path $SourceConfigPath)) {
  throw "OpenClaw config not found at $SourceConfigPath"
}
if (-not (Test-Path $SourceNodeExe)) {
  throw "Node executable not found at $SourceNodeExe"
}
if (-not (Test-Path $SourceOpenClawEntry)) {
  throw "OpenClaw entry not found at $SourceOpenClawEntry"
}
if (-not (Test-Path $SourcePythonExe)) {
  throw "Python executable not found at $SourcePythonExe"
}
if (-not (Test-Path $RequirementsPath)) {
  throw "Runtime requirements lock file not found at $RequirementsPath"
}
if (-not (Test-Path $SourceOpenClawDir)) {
  throw "OpenClaw package directory not found at $SourceOpenClawDir"
}
if (-not (Test-Path $SourceOpenClawPackageJsonPath)) {
  throw "OpenClaw package.json not found at $SourceOpenClawPackageJsonPath"
}
if (-not (Test-Path $SourcePythonDir)) {
  throw "Python installation directory not found at $SourcePythonDir"
}

$materializationInputs = [ordered]@{
  schemaVersion = 5
  gatewayPort = $GatewayPort
  gatewayToken = $GatewayToken
  sourceConfig = Get-FileFingerprint -Path $SourceConfigPath -IncludeHash
  sourceNodeExe = Get-FileFingerprint -Path $SourceNodeExe
  sourcePythonExe = Get-FileFingerprint -Path $SourcePythonExe
  sourceOpenClawEntry = Get-FileFingerprint -Path $SourceOpenClawEntry
  sourceOpenClawPackageJson = Get-FileFingerprint -Path $SourceOpenClawPackageJsonPath -IncludeHash
  requirements = Get-FileFingerprint -Path $RequirementsPath -IncludeHash
  sourceFfprobeExe = if ($ResolvedSourceFfprobeExe) { Get-FileFingerprint -Path $ResolvedSourceFfprobeExe } else { $null }
}
$materializationKey = Get-MaterializationKey -Inputs $materializationInputs
$existingManifestPath = Join-Path $RuntimeDir 'runtime-manifest.json'
if ((Test-Path $existingManifestPath) -and (Test-RuntimePayloadReady -RuntimeDir $RuntimeDir)) {
  try {
    $existingManifest = Get-Content $existingManifestPath -Raw | ConvertFrom-Json
    if ($existingManifest.materializationKey -eq $materializationKey) {
      Write-Host "Reusing bundled runtime payload at $RuntimeDir (materialization key match)"
      New-RuntimeSummary -RuntimeDir $RuntimeDir -Manifest $existingManifest -CacheHit $true | ConvertTo-Json -Depth 20
      exit 0
    }
  } catch {
    Write-Warning "Existing runtime manifest could not be reused: $($_.Exception.Message)"
  }
}

$stagingDir = Join-Path (Split-Path $RuntimeDir -Parent) ('openclaw-runtime.staging.' + ([guid]::NewGuid().ToString('N')))
$nodeDir = Join-Path $stagingDir 'node'
$openclawDir = Join-Path $stagingDir 'openclaw'
$openclawPackageDir = Join-Path $openclawDir 'package'
$configDir = Join-Path $stagingDir 'config'
$pythonDir = Join-Path $stagingDir 'python'
$ffmpegDir = Join-Path $stagingDir 'ffmpeg'
$ffmpegBinDir = Join-Path $ffmpegDir 'bin'
$playwrightBrowsersDir = Join-Path $stagingDir 'playwright-browsers'
$manifestPath = Join-Path $stagingDir 'runtime-manifest.json'
$wrapperPath = Join-Path $openclawDir 'index.js'
$configPath = Join-Path $configDir 'openclaw.json'
$pythonManifestPath = Join-Path $pythonDir 'python-manifest.json'
$pythonRequirementsCopy = Join-Path $pythonDir 'runtime-requirements.lock.txt'
$payloadFfmpegExe = Join-Path $ffmpegBinDir 'ffmpeg.exe'
$payloadFfprobeExe = Join-Path $ffmpegBinDir 'ffprobe.exe'
$payloadReadmePath = Join-Path $stagingDir 'README.md'
$payloadPythonExe = Join-Path $pythonDir 'python.exe'
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)

if (Test-Path $stagingDir) {
  Remove-Item -Recurse -Force $stagingDir
}

try {
  New-Item -ItemType Directory -Force -Path $nodeDir, $openclawDir, $configDir, $ffmpegBinDir, $playwrightBrowsersDir | Out-Null
  Copy-Item -Path $SourceNodeExe -Destination (Join-Path $nodeDir 'node.exe') -Force

  Write-Host "Copying OpenClaw package from $SourceOpenClawDir"
  Copy-Item -Path $SourceOpenClawDir -Destination $openclawPackageDir -Recurse -Force

  $config = Get-Content $SourceConfigPath -Raw | ConvertFrom-Json
  if (-not $config.gateway) {
    $config | Add-Member -NotePropertyName gateway -NotePropertyValue ([pscustomobject]@{})
  }
  if (-not $config.gateway.auth) {
    $config.gateway | Add-Member -NotePropertyName auth -NotePropertyValue ([pscustomobject]@{})
  }
  $config.gateway.mode = 'local'
  $config.gateway.bind = 'loopback'
  $config.gateway.port = $GatewayPort
  $config.gateway.auth.mode = 'token'
  $config.gateway.auth.token = $GatewayToken
  [System.IO.File]::WriteAllText($configPath, ($config | ConvertTo-Json -Depth 100), $utf8NoBom)

  Write-Host "Copying bundled Python runtime from $SourcePythonDir"
  Copy-Item -Path $SourcePythonDir -Destination $pythonDir -Recurse -Force
  if (-not (Test-Path $payloadPythonExe)) {
    throw "Bundled Python executable not found at $payloadPythonExe"
  }

  Write-Host 'Upgrading pip in bundled Python runtime'
  & $payloadPythonExe -m pip install --disable-pip-version-check --upgrade pip
  if ($LASTEXITCODE -ne 0) {
    throw 'Failed to upgrade pip in bundled Python runtime.'
  }

  Write-Host "Installing locked runtime dependencies from $RequirementsPath"
  & $payloadPythonExe -m pip install --disable-pip-version-check -r $RequirementsPath
  if ($LASTEXITCODE -ne 0) {
    throw 'Failed to install bundled Python dependencies.'
  }

  $env:PLAYWRIGHT_BROWSERS_PATH = $playwrightBrowsersDir
  Write-Host "Installing bundled Playwright Chromium into $playwrightBrowsersDir"
  & $payloadPythonExe -m playwright install chromium
  if ($LASTEXITCODE -ne 0) {
    throw 'Failed to install bundled Playwright Chromium.'
  }

  Copy-Item -Path $RequirementsPath -Destination $pythonRequirementsCopy -Force

  $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))
"@
  $pythonManifestJson = & $payloadPythonExe -c $manifestScript $pythonRequirementsCopy
  if ($LASTEXITCODE -ne 0) {
    throw 'Failed to inspect bundled Python manifest.'
  }

  $pythonManifestProbe = $pythonManifestJson | ConvertFrom-Json
  $materializedAt = (Get-Date).ToString('o')
  $pythonManifest = [ordered]@{
    pythonVersion = $pythonManifestProbe.pythonVersion
    executable = 'python.exe'
    requirementsPath = 'runtime-requirements.lock.txt'
    requestedPackages = @($pythonManifestProbe.requestedPackages)
    resolvedPackages = @($pythonManifestProbe.resolvedPackages)
    materializedAt = $materializedAt
  }
  [System.IO.File]::WriteAllText($pythonManifestPath, ($pythonManifest | ConvertTo-Json -Depth 100), $utf8NoBom)

  $imageioFfmpegSource = Get-ImageioFfmpegBinaryPath -PythonDir $pythonDir
  Copy-Item -LiteralPath $imageioFfmpegSource -Destination $payloadFfmpegExe -Force
  if (-not (Test-Path $payloadFfmpegExe)) {
    throw "Bundled ffmpeg executable was not materialized at $payloadFfmpegExe"
  }
  if ($ResolvedSourceFfprobeExe) {
    Copy-Item -LiteralPath $ResolvedSourceFfprobeExe -Destination $payloadFfprobeExe -Force
  }

  $openClawPackageJsonPath = Join-Path $openclawPackageDir 'package.json'
  $openClawPackage = if (Test-Path $openClawPackageJsonPath) {
    Get-Content $openClawPackageJsonPath -Raw | ConvertFrom-Json
  } else {
    $null
  }

  $manifest = [ordered]@{
    bundledNodeExecutable = 'node/node.exe'
    packagedOpenClawEntry = 'openclaw/package/openclaw.mjs'
    packagedSkillsRoot = 'openclaw/package/skills'
    defaultConfigPath = 'config/openclaw.json'
    pythonExecutable = 'python/python.exe'
    pythonManifestPath = 'python/python-manifest.json'
    requirementsPath = 'python/runtime-requirements.lock.txt'
    ffmpegExecutable = 'ffmpeg/bin/ffmpeg.exe'
    ffprobeExecutable = 'ffmpeg/bin/ffprobe.exe'
    playwrightBrowsersPath = 'playwright-browsers'
    gatewayPort = $GatewayPort
    gatewayToken = $GatewayToken
    materializedAt = $materializedAt
    materializationKey = $materializationKey
    materializationInputs = $materializationInputs
    pythonVersion = $pythonManifest.pythonVersion
    openClawVersion = if ($openClawPackage) { $openClawPackage.version } else { $null }
    installedPythonPackages = @($pythonManifest.requestedPackages | ForEach-Object { "$($_.name)==$($_.version)" })
  }
  [System.IO.File]::WriteAllText($manifestPath, ($manifest | ConvertTo-Json -Depth 100), $utf8NoBom)

  $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);
});
"@
  [System.IO.File]::WriteAllText($wrapperPath, $wrapper, $utf8NoBom)

  $readme = @'
# Bundled Runtime Payload

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

- `node/node.exe`
- `openclaw/index.js`
- `openclaw/package/openclaw.mjs`
- `config/openclaw.json`
- `python/python.exe`
- `python/python-manifest.json`
- `python/runtime-requirements.lock.txt`
- `ffmpeg/bin/ffmpeg.exe`
- `ffmpeg/bin/ffprobe.exe`
- `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.
'@
  [System.IO.File]::WriteAllText($payloadReadmePath, $readme.TrimStart(), $utf8NoBom)

  $payloadStatsBeforeCleanup = Get-RuntimePayloadStats -RuntimeDir $stagingDir
  $cleanupSummary = Invoke-RuntimePayloadCleanup -RuntimeRoot $stagingDir -OpenClawPackageDir $openclawPackageDir -PythonDir $pythonDir
  $payloadStatsAfterCleanup = Get-RuntimePayloadStats -RuntimeDir $stagingDir
  $cleanupBytesRemoved = [int64]($payloadStatsBeforeCleanup.sizeBytes - $payloadStatsAfterCleanup.sizeBytes)
  $cleanupFilesRemoved = [int]($payloadStatsBeforeCleanup.fileCount - $payloadStatsAfterCleanup.fileCount)
  Write-Host ("Runtime payload cleanup removed {0} files and {1} bytes" -f $cleanupFilesRemoved, $cleanupBytesRemoved)

  $manifest['sizeBytes'] = [int64]$payloadStatsAfterCleanup.sizeBytes
  $manifest['fileCount'] = [int]$payloadStatsAfterCleanup.fileCount
  $manifest['topLevelBreakdown'] = $payloadStatsAfterCleanup.topLevelBreakdown
  $manifest['payloadStats'] = [ordered]@{
    beforeCleanup = $payloadStatsBeforeCleanup
    afterCleanup = $payloadStatsAfterCleanup
    bytesRemoved = $cleanupBytesRemoved
    filesRemoved = $cleanupFilesRemoved
  }
  $manifest['cleanupSummary'] = $cleanupSummary
  [System.IO.File]::WriteAllText($manifestPath, ($manifest | ConvertTo-Json -Depth 100), $utf8NoBom)

  $requiredPaths = @(
    (Join-Path $nodeDir 'node.exe'),
    $wrapperPath,
    (Join-Path $openclawPackageDir 'openclaw.mjs'),
    $openClawPackageJsonPath,
    $configPath,
    $payloadPythonExe,
    $pythonManifestPath,
    $payloadFfmpegExe,
    $payloadFfprobeExe,
    $manifestPath,
    $payloadReadmePath
  )
  $requiredPaths += Get-RequiredWorkspaceTemplatePaths -OpenClawPackageDir $openclawPackageDir
  foreach ($requiredPath in $requiredPaths) {
    if (-not (Test-Path $requiredPath)) {
      throw "Bundled runtime materialization failed; missing $requiredPath"
    }
  }

  if (Test-Path $RuntimeDir) {
    Remove-Item -Recurse -Force $RuntimeDir
  }
  Move-Item -Path $stagingDir -Destination $RuntimeDir
  $stagingDir = $null

  New-RuntimeSummary -RuntimeDir $RuntimeDir -Manifest ([pscustomobject]$manifest) | ConvertTo-Json -Depth 20
} finally {
  if ($stagingDir -and (Test-Path $stagingDir)) {
    Remove-Item -Recurse -Force $stagingDir -ErrorAction SilentlyContinue
  }
}
