const { spawn } = require('node:child_process')
const {
  createReadStream,
  createWriteStream,
  existsSync,
  closeSync,
  openSync,
  readFileSync,
  readSync,
  readdirSync,
  renameSync,
  rmSync,
  statSync,
  writeFileSync
} = require('node:fs')
const { once } = require('node:events')
const {
  basename,
  dirname,
  join,
  parse,
  relative,
  resolve,
  sep
} = require('node:path')
const { finished } = require('node:stream/promises')
const { Zip, ZipDeflate } = require('fflate')
const { sha256File } = require('./file-hash.cjs')

const root = join(__dirname, '..')
const packageJson = JSON.parse(
  readFileSync(join(root, 'package.json'), 'utf8')
)
const productName = packageJson.build?.productName ?? packageJson.name
const releaseRoot = join(root, 'dist', 'release')
const manifestName = 'release-manifest.json'
const portableMarkerName = '.goodbuddy-portable.json'
const portableRequiredFiles = [
  `${productName}.exe`,
  'resources/app.asar',
  'resources/icon.ico',
  'resources/tray-icon.png',
  'resources/runtimes/opencode/opencode.exe',
  'resources/runtimes/continue/package.json'
]
const maxPortableZipEntries = 50_000
const maxPortableCentralDirectoryBytes = 64 * 1024 * 1024
const ansiEscapeCharacter = String.fromCharCode(27)
const ansiSequenceSuffixPattern = /\[[0-9;]*[A-Za-z]/gu
const supportedArchitectures = new Set(['x64', 'arm64'])
const platformAliases = new Map([
  ['win', 'windows'],
  ['win32', 'windows'],
  ['windows', 'windows'],
  ['darwin', 'macos'],
  ['mac', 'macos'],
  ['macos', 'macos'],
  ['linux', 'linux']
])
const hostPlatforms = {
  win32: 'windows',
  darwin: 'macos',
  linux: 'linux'
}
const platformDefinitions = {
  windows: {
    builderFlag: '--win',
    defaultFormats: ['nsis', 'portable'],
    supportedFormats: ['nsis', 'portable'],
    unpackedPattern: /^win(?:-.+)?-unpacked$/u,
    executable: [`${productName}.exe`],
    runtimeExecutable: 'opencode.exe'
  },
  macos: {
    builderFlag: '--mac',
    defaultFormats: ['dmg', 'zip'],
    supportedFormats: ['dmg', 'zip'],
    unpackedPattern: /^mac(?:-.+)?$/u,
    executable: [
      `${productName}.app`,
      'Contents',
      'MacOS',
      productName
    ],
    runtimeExecutable: 'opencode'
  },
  linux: {
    builderFlag: '--linux',
    defaultFormats: ['AppImage', 'deb'],
    supportedFormats: ['AppImage', 'deb'],
    unpackedPattern: /^linux(?:-.+)?-unpacked$/u,
    executable: [packageJson.name],
    runtimeExecutable: 'opencode'
  }
}
const formatExtensions = {
  nsis: '.exe',
  portable: '.zip',
  dmg: '.dmg',
  zip: '.zip',
  AppImage: '.AppImage',
  deb: '.deb'
}

function normalizePlatform(value) {
  return platformAliases.get(String(value).toLowerCase())
}

function normalizeFormat(platform, value) {
  const definition = platformDefinitions[platform]
  return definition.supportedFormats.find(
    (candidate) => candidate.toLowerCase() === value.toLowerCase()
  )
}

function parseArguments(argv, environment = process) {
  const options = {
    platform: hostPlatforms[environment.platform],
    arch: environment.arch,
    formats: [],
    skipBuild: false,
    dryRun: false,
    help: false
  }
  for (let index = 0; index < argv.length; index += 1) {
    const argument = argv[index]
    if (argument === '--platform') {
      options.platform = normalizePlatform(argv[index + 1])
      index += 1
    } else if (argument === '--arch') {
      options.arch = argv[index + 1]
      index += 1
    } else if (argument === '--format') {
      const value = argv[index + 1]
      if (!value) {
        throw new Error('--format 缺少值')
      }
      options.formats.push(
        ...value.split(',').map((item) => item.trim()).filter(Boolean)
      )
      index += 1
    } else if (argument === '--skip-build') {
      options.skipBuild = true
    } else if (argument === '--dry-run') {
      options.dryRun = true
    } else if (argument === '--help' || argument === '-h') {
      options.help = true
    } else {
      throw new Error(`未知参数：${argument}`)
    }
  }
  if (options.help) {
    return options
  }
  if (!options.platform) {
    throw new Error('不支持当前系统，请显式指定 --platform')
  }
  if (!supportedArchitectures.has(options.arch)) {
    throw new Error(`不支持的架构：${options.arch}`)
  }
  const definition = platformDefinitions[options.platform]
  const requestedFormats =
    options.formats.length > 0
      ? options.formats
      : definition.defaultFormats
  options.formats = [...new Set(requestedFormats.map((format) => {
    const normalized = normalizeFormat(options.platform, format)
    if (!normalized) {
      throw new Error(
        `${options.platform} 不支持打包格式：${format}`
      )
    }
    return normalized
  }))]
  return options
}

function npmInvocation(environment = process.env) {
  if (environment.npm_execpath) {
    return {
      command: process.execPath,
      prefixArgs: [environment.npm_execpath]
    }
  }
  return {
    command: process.platform === 'win32' ? 'npm.cmd' : 'npm',
    prefixArgs: []
  }
}

function run(command, args, environment = process.env) {
  return new Promise((resolveRun, rejectRun) => {
    const child = spawn(command, args, {
      cwd: root,
      env: environment,
      shell: false,
      stdio: ['inherit', 'pipe', 'pipe'],
      windowsHide: true
    })
    let outputTail = ''
    const forward = (chunk, destination) => {
      destination.write(chunk)
      outputTail = `${outputTail}${chunk}`.slice(-12_000)
    }
    child.stdout.on('data', (chunk) => {
      forward(chunk, process.stdout)
    })
    child.stderr.on('data', (chunk) => {
      forward(chunk, process.stderr)
    })
    child.once('error', rejectRun)
    child.once('close', (code) => {
      if (code === 0) {
        resolveRun()
        return
      }
      const error = new Error(
        `命令执行失败（code ${code ?? 1}）：${command} ${args.join(' ')}`
      )
      error.outputTail = outputTail
      rejectRun(error)
    })
  })
}

function buildElectronBuilderArguments(options, outputDirectory) {
  const definition = platformDefinitions[options.platform]
  const builderFormats = [...new Set(
    options.formats.map((format) =>
      options.platform === 'windows' && format === 'portable'
        ? 'dir'
        : format
    )
  )]
  const builderArguments = [
    join(root, 'node_modules', 'electron-builder', 'cli.js'),
    definition.builderFlag,
    ...builderFormats,
    `--${options.arch}`,
    `--config.directories.output=${outputDirectory}`,
    '--publish',
    'never'
  ]
  if (
    options.platform === 'windows' &&
    options.formats.includes('nsis')
  ) {
    builderArguments.push(
      `--config.nsis.artifactName=${productName}-\${version}-windows-\${arch}-setup.\${ext}`
    )
  }
  return builderArguments
}

function detectBinaryArchitecture(buffer) {
  if (
    buffer.length >= 64 &&
    buffer[0] === 0x4d &&
    buffer[1] === 0x5a
  ) {
    const peOffset = buffer.readUInt32LE(0x3c)
    if (
      peOffset + 6 <= buffer.length &&
      buffer.toString('ascii', peOffset, peOffset + 4) === 'PE\0\0'
    ) {
      const machine = buffer.readUInt16LE(peOffset + 4)
      if (machine === 0x8664) {
        return 'x64'
      }
      if (machine === 0xaa64) {
        return 'arm64'
      }
    }
  }
  if (
    buffer.length >= 20 &&
    buffer[0] === 0x7f &&
    buffer.toString('ascii', 1, 4) === 'ELF'
  ) {
    const machine =
      buffer[5] === 2
        ? buffer.readUInt16BE(18)
        : buffer.readUInt16LE(18)
    if (machine === 62) {
      return 'x64'
    }
    if (machine === 183) {
      return 'arm64'
    }
  }
  if (buffer.length >= 8) {
    const littleMagic = buffer.readUInt32LE(0)
    const bigMagic = buffer.readUInt32BE(0)
    const cpuType =
      littleMagic === 0xfeedfacf
        ? buffer.readUInt32LE(4)
        : bigMagic === 0xfeedfacf
          ? buffer.readUInt32BE(4)
          : undefined
    if (cpuType === 0x01000007) {
      return 'x64'
    }
    if (cpuType === 0x0100000c) {
      return 'arm64'
    }
  }
  return undefined
}

function binaryArchitecture(filePath) {
  return detectBinaryArchitecture(readChunk(filePath, 4096))
}

function readChunk(filePath, length, position = 0) {
  const descriptor = openSync(filePath, 'r')
  try {
    const buffer = Buffer.alloc(length)
    const bytesRead = readSync(
      descriptor,
      buffer,
      0,
      length,
      position
    )
    return buffer.subarray(0, bytesRead)
  } finally {
    closeSync(descriptor)
  }
}

function findUnpackedDirectory(directory, platform) {
  const pattern = platformDefinitions[platform].unpackedPattern
  const candidates = readdirSync(directory, { withFileTypes: true })
    .filter((entry) => entry.isDirectory() && pattern.test(entry.name))
    .map((entry) => join(directory, entry.name))
  if (candidates.length !== 1) {
    throw new Error(
      `无法确定 ${platform} unpacked 目录：${candidates.join(', ') || '未生成'}`
    )
  }
  return candidates[0]
}

function resourceDirectory(unpackedDirectory, platform) {
  return platform === 'macos'
    ? join(
        unpackedDirectory,
        `${productName}.app`,
        'Contents',
        'Resources'
      )
    : join(unpackedDirectory, 'resources')
}

function assertFile(filePath, description) {
  if (!statSync(filePath, { throwIfNoEntry: false })?.isFile()) {
    throw new Error(`${description}缺失：${filePath}`)
  }
}

function verifyUnpackedOutput(directory, options) {
  const definition = platformDefinitions[options.platform]
  const unpackedDirectory = findUnpackedDirectory(
    directory,
    options.platform
  )
  const applicationExecutable = join(
    unpackedDirectory,
    ...definition.executable
  )
  const resources = resourceDirectory(
    unpackedDirectory,
    options.platform
  )
  const runtimeExecutable = join(
    resources,
    'runtimes',
    'opencode',
    definition.runtimeExecutable
  )
  assertFile(applicationExecutable, '应用主程序')
  assertFile(join(resources, 'app.asar'), '应用 ASAR')
  assertFile(runtimeExecutable, 'OpenCode Runtime')
  assertFile(
    join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
    'Continue Runtime'
  )
  for (const [filePath, label] of [
    [applicationExecutable, '应用主程序'],
    [runtimeExecutable, 'OpenCode Runtime']
  ]) {
    const actualArchitecture = binaryArchitecture(filePath)
    if (actualArchitecture !== options.arch) {
      throw new Error(
        `${label}架构错误：期望 ${options.arch}，实际 ${actualArchitecture ?? '未知'}`
      )
    }
  }
  return unpackedDirectory
}

function toArchivePath(rootDirectory, filePath) {
  return relative(rootDirectory, filePath).split(sep).join('/')
}

function listPortableFiles(rootDirectory) {
  const files = []
  const pending = [rootDirectory]
  while (pending.length > 0) {
    const directory = pending.pop()
    const entries = readdirSync(directory, { withFileTypes: true })
      .sort((left, right) => right.name.localeCompare(left.name))
    for (const entry of entries) {
      const filePath = join(directory, entry.name)
      if (entry.isSymbolicLink()) {
        throw new Error(`Portable 目录不能包含符号链接：${filePath}`)
      }
      if (entry.isDirectory()) {
        pending.push(filePath)
      } else if (entry.isFile()) {
        files.push(filePath)
        if (files.length > maxPortableZipEntries) {
          throw new Error(
            `Portable ZIP 文件数量超过限制：${files.length}`
          )
        }
      } else {
        throw new Error(`Portable 目录包含不支持的文件类型：${filePath}`)
      }
    }
  }
  return files.sort((left, right) =>
    toArchivePath(rootDirectory, left).localeCompare(
      toArchivePath(rootDirectory, right)
    )
  )
}

async function addFileToZip(
  zip,
  rootDirectory,
  filePath,
  waitForDrain
) {
  const input = new ZipDeflate(
    toArchivePath(rootDirectory, filePath),
    { level: 6 }
  )
  zip.add(input)
  const stream = createReadStream(filePath)
  try {
    for await (const chunk of stream) {
      input.push(
        new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength),
        false
      )
      await waitForDrain()
    }
    input.push(new Uint8Array(), true)
    await waitForDrain()
  } catch (error) {
    stream.destroy()
    throw error
  }
}

function openExclusiveWriteStream(filePath) {
  const descriptor = openSync(filePath, 'wx')
  try {
    return createWriteStream(filePath, {
      fd: descriptor,
      autoClose: true
    })
  } catch (error) {
    closeSync(descriptor)
    rmSync(filePath, { force: true })
    throw error
  }
}

async function createPortableZip(
  unpackedDirectory,
  zipPath,
  dependencies = {}
) {
  const markerPath = join(unpackedDirectory, portableMarkerName)
  writeFileSync(
    markerPath,
    `${JSON.stringify({
      formatVersion: 1,
      productName,
      version: packageJson.version
    }, null, 2)}\n`,
    'utf8'
  )
  const portableFiles = listPortableFiles(unpackedDirectory)
  const output = (
    dependencies.openOutput ?? openExclusiveWriteStream
  )(zipPath)
  let zipError
  let pendingDrain
  let zipFinal = false
  const outputCompletion = finished(output).then(
    () => undefined,
    (error) => {
      zipError ??= error
    }
  )
  const waitForDrain = async () => {
    if (pendingDrain) {
      await pendingDrain
    }
    if (zipError) {
      throw zipError
    }
  }
  const zip = new Zip((error, chunk, final) => {
    if (error) {
      zipError ??= error
      output.destroy(error)
      return
    }
    try {
      if (!output.write(chunk) && !pendingDrain) {
        const drain = once(output, 'drain').then(
          () => undefined,
          (writeError) => {
            zipError ??= writeError
          }
        )
        const currentDrain = Promise.race([
          drain,
          outputCompletion
        ]).finally(() => {
          if (pendingDrain === currentDrain) {
            pendingDrain = undefined
          }
        })
        pendingDrain = currentDrain
      }
      if (final) {
        zipFinal = true
      }
    } catch (writeError) {
      zipError ??= writeError
      output.destroy(writeError)
    }
  })
  try {
    for (const filePath of portableFiles) {
      await addFileToZip(
        zip,
        unpackedDirectory,
        filePath,
        waitForDrain
      )
      if (zipError) {
        throw zipError
      }
    }
    zip.end()
    await waitForDrain()
    if (zipError) {
      throw zipError
    }
    if (!zipFinal) {
      throw new Error('Portable ZIP 未正常结束')
    }
    output.end()
    await outputCompletion
    if (zipError) {
      throw zipError
    }
  } catch (error) {
    zip.terminate()
    output.destroy()
    await outputCompletion
    if (!dependencies.openOutput) {
      rmSync(zipPath, { force: true })
    }
    throw error
  }
}

function readZipEntryNames(filePath) {
  const fileSize = statSync(filePath).size
  if (fileSize < 22) {
    throw new Error('Portable ZIP 缺少中央目录')
  }
  const endChunkSize = Math.min(fileSize, 65_557)
  const endChunkStart = fileSize - endChunkSize
  const endChunk = readChunk(
    filePath,
    endChunkSize,
    endChunkStart
  )
  let endOffset = -1
  for (let index = endChunk.length - 22; index >= 0; index -= 1) {
    if (
      endChunk.readUInt32LE(index) === 0x06054b50 &&
      index + 22 + endChunk.readUInt16LE(index + 20) ===
        endChunk.length
    ) {
      endOffset = index
      break
    }
  }
  if (endOffset < 0) {
    throw new Error('Portable ZIP 缺少中央目录')
  }
  const diskNumber = endChunk.readUInt16LE(endOffset + 4)
  const centralDisk = endChunk.readUInt16LE(endOffset + 6)
  const diskEntryCount = endChunk.readUInt16LE(endOffset + 8)
  const entryCount = endChunk.readUInt16LE(endOffset + 10)
  const centralSize = endChunk.readUInt32LE(endOffset + 12)
  const centralOffset = endChunk.readUInt32LE(endOffset + 16)
  const absoluteEndOffset = endChunkStart + endOffset
  if (
    diskNumber !== 0 ||
    centralDisk !== 0 ||
    diskEntryCount !== entryCount ||
    entryCount === 0xffff ||
    centralSize === 0xffffffff ||
    centralOffset === 0xffffffff ||
    entryCount < portableRequiredFiles.length + 1 ||
    entryCount > maxPortableZipEntries ||
    centralSize < 46 ||
    centralSize > maxPortableCentralDirectoryBytes ||
    centralOffset + centralSize !== absoluteEndOffset
  ) {
    throw new Error('Portable ZIP 中央目录无效')
  }
  const centralDirectory = readChunk(
    filePath,
    centralSize,
    centralOffset
  )
  const names = []
  let offset = 0
  for (let index = 0; index < entryCount; index += 1) {
    if (
      offset + 46 > centralDirectory.length ||
      centralDirectory.readUInt32LE(offset) !== 0x02014b50
    ) {
      throw new Error('Portable ZIP 中央目录条目无效')
    }
    const nameLength = centralDirectory.readUInt16LE(offset + 28)
    const extraLength = centralDirectory.readUInt16LE(offset + 30)
    const commentLength = centralDirectory.readUInt16LE(offset + 32)
    const entryLength = 46 + nameLength + extraLength + commentLength
    if (offset + entryLength > centralDirectory.length) {
      throw new Error('Portable ZIP 中央目录条目越界')
    }
    const name = centralDirectory
      .subarray(offset + 46, offset + 46 + nameLength)
      .toString(
        centralDirectory.readUInt16LE(offset + 8) & 0x0800
          ? 'utf8'
          : 'latin1'
      )
      .replaceAll('\\', '/')
    if (
      !name ||
      name.startsWith('/') ||
      /^[a-z]:\//iu.test(name) ||
      name.includes('\0') ||
      name.split('/').some((part) => part === '..')
    ) {
      throw new Error(`Portable ZIP 包含不安全路径：${name}`)
    }
    names.push(name)
    offset += entryLength
  }
  if (offset !== centralDirectory.length) {
    throw new Error('Portable ZIP 中央目录数量不一致')
  }
  return names
}

function verifyPortableZip(filePath) {
  const entries = readZipEntryNames(filePath)
  const names = new Set(entries)
  if (names.size !== entries.length) {
    throw new Error('Portable ZIP 包含重复文件')
  }
  for (const required of [
    portableMarkerName,
    ...portableRequiredFiles
  ]) {
    if (!names.has(required)) {
      throw new Error(`Portable ZIP 缺少必要文件：${required}`)
    }
  }
}

function verifyArtifacts(directory, options) {
  const files = readdirSync(directory, { withFileTypes: true })
    .filter((entry) => entry.isFile())
    .map((entry) => entry.name)
  for (const format of options.formats) {
    const extension = formatExtensions[format]
    const candidates = files.filter((name) => name.endsWith(extension))
    const matches =
      options.platform === 'windows'
        ? candidates.filter((name) =>
            format === 'nsis'
              ? /-setup\.exe$/iu.test(name)
              : /-portable\.zip$/iu.test(name)
          )
        : candidates
    if (matches.length !== 1) {
      throw new Error(
        `${format} 产物数量错误：${matches.join(', ') || '未生成'}`
      )
    }
    verifyArtifactSignature(
      join(directory, matches[0]),
      format,
      options.arch
    )
    if (format === 'portable') {
      verifyPortableZip(join(directory, matches[0]))
    }
  }
}

function verifyArtifactSignature(filePath, format, arch) {
  if (format === 'nsis') {
    if (readChunk(filePath, 2).toString('ascii') !== 'MZ') {
      throw new Error(`${format} 产物不是有效的 Windows PE 文件`)
    }
    return
  }
  if (format === 'portable' || format === 'zip') {
    const signature = readChunk(filePath, 4).toString('hex')
    if (
      !['504b0304', '504b0506', '504b0708'].includes(signature)
    ) {
      throw new Error('zip 产物不是有效的 ZIP 文件')
    }
    return
  }
  if (format === 'dmg') {
    const size = statSync(filePath).size
    if (
      size < 512 ||
      readChunk(filePath, 4, size - 512).toString('ascii') !==
        'koly'
    ) {
      throw new Error('dmg 产物缺少 UDIF 尾部')
    }
    return
  }
  if (format === 'AppImage') {
    if (
      detectBinaryArchitecture(readChunk(filePath, 4096)) !== arch
    ) {
      throw new Error(`AppImage 产物架构不是 ${arch}`)
    }
    return
  }
  if (
    format === 'deb' &&
    readChunk(filePath, 8).toString('ascii') !== '!<arch>\n'
  ) {
    throw new Error('deb 产物不是有效的 ar 归档')
  }
}

async function writeManifest(directory, options) {
  const entries = readdirSync(directory, { withFileTypes: true })
    .filter(
      (entry) => entry.isFile() && entry.name !== manifestName
    )
    .sort((left, right) => left.name.localeCompare(right.name))
  const files = []
  for (const entry of entries) {
    const filePath = join(directory, entry.name)
    files.push({
      name: entry.name,
      size: statSync(filePath).size,
      sha256: await sha256File(filePath)
    })
  }
  const manifest = {
    formatVersion: 1,
    productName,
    version: packageJson.version,
    platform: options.platform,
    arch: options.arch,
    formats: options.formats,
    files
  }
  writeFileSync(
    join(directory, manifestName),
    `${JSON.stringify(manifest, null, 2)}\n`,
    'utf8'
  )
  return manifest
}

function assertReplaceableOutput(directory, options) {
  if (!existsSync(directory)) {
    return
  }
  if (readdirSync(directory).length === 0) {
    return
  }
  try {
    const manifest = JSON.parse(
      readFileSync(join(directory, manifestName), 'utf8')
    )
    if (
      manifest.productName === productName &&
      manifest.formatVersion === 1 &&
      manifest.platform === options.platform &&
      manifest.arch === options.arch
    ) {
      return
    }
  } catch {
    // The explicit error below describes the safe recovery path.
  }
  throw new Error(
    `拒绝覆盖未识别的发布目录：${directory}`
  )
}

function replaceOutput(stagingDirectory, destination, options) {
  const backup = `${destination}.previous-${process.pid}`
  assertReplaceableOutput(destination, options)
  rmSync(backup, { recursive: true, force: true })
  if (existsSync(destination)) {
    renameSync(destination, backup)
  }
  try {
    renameSync(stagingDirectory, destination)
  } catch (error) {
    if (!existsSync(destination) && existsSync(backup)) {
      renameSync(backup, destination)
    }
    throw error
  }
  rmSync(backup, { recursive: true, force: true })
}

function safeReleasePath(filePath) {
  const resolved = resolve(filePath)
  if (
    resolved === parse(resolved).root ||
    resolved === root ||
    resolved === dirname(releaseRoot)
  ) {
    throw new Error(`拒绝使用不安全的发布目录：${resolved}`)
  }
  return resolved
}

function summarizeCommandOutput(value) {
  const lines = String(value)
    .replaceAll(ansiEscapeCharacter, '')
    .replace(ansiSequenceSuffixPattern, '')
    .replaceAll('\r', '\n')
    .split('\n')
    .map((line) => line.trim())
    .filter(
      (line) =>
        line &&
        !line.includes('duplicate dependency references')
    )
    .map((line) => line.slice(0, 800))
  const important = lines.filter((line) =>
    /error|failed|fatal|cannot|unable|enoent|codesign|hdiutil|exit code/iu.test(
      line
    )
  )
  return [
    ...new Set([
      ...lines.slice(0, 8),
      ...important,
      ...lines.slice(-14)
    ])
  ].join('\n').slice(-3_000)
}

function printHelp() {
  console.log(`${productName} 跨平台发布构建

用法：
  npm run release:package -- --platform <windows|macos|linux> --arch <x64|arm64>

参数：
  --format <列表>  覆盖默认格式，逗号分隔
  --skip-build     复用已有 out 生产构建
  --dry-run        仅显示目标与 electron-builder 参数

默认格式：
  windows: nsis, portable (ZIP)
  macos:   dmg, zip
  linux:   AppImage, deb`)
}

async function main(argv = process.argv.slice(2)) {
  const options = parseArguments(argv)
  if (options.help) {
    printHelp()
    return
  }
  const targetName = `${options.platform}-${options.arch}`
  const destination = safeReleasePath(join(releaseRoot, targetName))
  const stagingDirectory = safeReleasePath(
    join(releaseRoot, `.stage-${targetName}-${process.pid}`)
  )
  const builderArguments = buildElectronBuilderArguments(
    options,
    stagingDirectory
  )
  if (options.dryRun) {
    console.log(JSON.stringify({
      target: targetName,
      formats: options.formats,
      output: destination,
      command: process.execPath,
      arguments: builderArguments
    }, null, 2))
    return
  }
  const hostPlatform = hostPlatforms[process.platform]
  if (options.platform !== hostPlatform) {
    throw new Error(
      `${options.platform} 包必须在对应系统构建，当前系统为 ${hostPlatform ?? process.platform}`
    )
  }

  rmSync(stagingDirectory, { recursive: true, force: true })
  try {
    if (!options.skipBuild) {
      const npm = npmInvocation()
      await run(
        npm.command,
        [...npm.prefixArgs, 'run', 'build']
      )
    }
    await run(
      process.execPath,
      builderArguments,
      {
        ...process.env,
        CSC_IDENTITY_AUTO_DISCOVERY:
          process.env.CSC_IDENTITY_AUTO_DISCOVERY ?? 'false'
      }
    )
    const unpackedDirectory = verifyUnpackedOutput(
      stagingDirectory,
      options
    )
    if (
      options.platform === 'windows' &&
      options.formats.includes('portable')
    ) {
      await createPortableZip(
        unpackedDirectory,
        join(
          stagingDirectory,
          `${productName}-${packageJson.version}-windows-${options.arch}-portable.zip`
        )
      )
    }
    verifyArtifacts(stagingDirectory, options)
    rmSync(unpackedDirectory, { recursive: true, force: true })
    const manifest = await writeManifest(stagingDirectory, options)
    replaceOutput(stagingDirectory, destination, options)
    console.log(`发布包构建完成：${destination}`)
    for (const file of manifest.files) {
      console.log(
        `${basename(file.name)}  ${file.size} bytes  sha256:${file.sha256}`
      )
    }
  } finally {
    rmSync(stagingDirectory, { recursive: true, force: true })
  }
}

module.exports = {
  assertReplaceableOutput,
  buildElectronBuilderArguments,
  createPortableZip,
  detectBinaryArchitecture,
  normalizePlatform,
  parseArguments,
  platformDefinitions,
  replaceOutput,
  verifyArtifacts,
  verifyArtifactSignature,
  verifyPortableZip,
  writeManifest
}

if (require.main === module) {
  main().catch((error) => {
    console.error(error)
    if (process.env.GITHUB_ACTIONS === 'true') {
      const details = `${error.message}\n${summarizeCommandOutput(error.outputTail ?? '')}`
        .replace(
          /((?:authorization|_authToken|api[_-]?key|password)\s*[:=]\s*)\S+/giu,
          '$1[redacted]'
        )
        .replaceAll('%', '%25')
        .replaceAll('\r', '%0D')
        .replaceAll('\n', '%0A')
      console.error(
        `::error title=Release packaging failed::${details}`
      )
    }
    process.exitCode = 1
  })
}
