import {
  cpSync,
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  statSync
} from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
import {
  getReleaseArch,
  getReleaseArtifactName,
  getReleaseVersion,
  isBundleCefEnabled
} from './release-artifact-utils.mjs'
import {
  cleanExpandedWindowsApp,
  expandWindowsAppFromArchive
} from './expand-windows-installer.mjs'
import { stageWindowsDeeplink } from './build-windows-deeplink.mjs'
import { patchWindowsBinaries } from './patch-windows-binaries.mjs'
import { codesignMacAppBundle } from './codesign-macos-app.mjs'

const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const packageJson = JSON.parse(
  readFileSync(path.join(rootDir, 'package.json'), 'utf8')
)
const buildEnv = process.env.ELECTROBUN_BUILD_ENV || 'stable'
const targetOs =
  process.env.ELECTROBUN_OS ||
  (process.platform === 'darwin'
    ? 'macos'
    : process.platform === 'win32'
      ? 'win'
      : process.platform === 'linux'
        ? 'linux'
        : null)
const buildArch = getReleaseArch(process.env.ELECTROBUN_ARCH || process.arch)
const version =
  process.env.ELECTROBUN_APP_VERSION || getReleaseVersion(packageJson)
const bundleCef = isBundleCefEnabled()
const artifactDir =
  process.env.ELECTROBUN_ARTIFACT_DIR || path.join(rootDir, 'app_dist')
const identifier =
  process.env.ELECTROBUN_APP_IDENTIFIER || 'com.tombutcher.farmcontrol'
const artifactPrefix = `farmcontrol-${version}-`

function artifactName(arch, ext) {
  return getReleaseArtifactName(version, arch, ext, { cef: bundleCef })
}

function getBuildRoot() {
  const electrobunBuildDir = process.env.ELECTROBUN_BUILD_DIR
  if (!electrobunBuildDir) {
    return path.join(rootDir, 'build')
  }

  const baseName = path.basename(electrobunBuildDir)
  if (baseName.startsWith('stable-')) {
    return path.dirname(electrobunBuildDir)
  }

  return electrobunBuildDir
}

function walkFiles(dir) {
  const files = []
  if (!existsSync(dir)) {
    return files
  }

  for (const entry of readdirSync(dir)) {
    const fullPath = path.join(dir, entry)
    const stats = statSync(fullPath)
    if (stats.isDirectory()) {
      files.push(...walkFiles(fullPath))
    } else {
      files.push(fullPath)
    }
  }

  return files
}

function findByExtension(root, extension) {
  return walkFiles(root).find((filePath) =>
    filePath.toLowerCase().endsWith(extension.toLowerCase())
  )
}

function findMacAppBundle(arch) {
  const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`)
  if (!existsSync(platformDir)) {
    return null
  }

  for (const child of readdirSync(platformDir)) {
    if (child.endsWith('.app')) {
      return path.join(platformDir, child)
    }
  }

  return null
}

function findMacDmgSource(arch) {
  const prefixedArtifact = walkFiles(artifactDir).find(
    (filePath) =>
      filePath.includes(`stable-macos-${arch}`) &&
      filePath.toLowerCase().endsWith('.dmg')
  )
  if (prefixedArtifact) {
    return prefixedArtifact
  }

  const buildDmg = findByExtension(
    path.join(getBuildRoot(), `stable-macos-${arch}`),
    '.dmg'
  )
  if (buildDmg) {
    return buildDmg
  }

  return findByExtension(artifactDir, '.dmg')
}

function findWindowsInstallerFiles() {
  const buildRoot = getBuildRoot()
  if (!existsSync(buildRoot)) {
    return null
  }

  for (const entry of readdirSync(buildRoot)) {
    if (!entry.startsWith('stable-win-')) {
      continue
    }

    const platformDir = path.join(buildRoot, entry)
    const files = walkFiles(platformDir)
    const setupExe = files.find((filePath) => /-setup\.exe$/i.test(filePath))
    const setupArchive = files.find((filePath) =>
      /-setup\.tar\.zst$/i.test(filePath)
    )
    const setupMetadata = files.find((filePath) =>
      /-setup\.metadata\.json$/i.test(filePath)
    )
    const setupZip = files.find((filePath) => /-setup\.zip$/i.test(filePath))

    if (setupExe && setupArchive && setupMetadata) {
      return { setupExe, setupArchive, setupMetadata, setupZip }
    }
  }

  return null
}

function publishArtifact(sourcePath, arch, ext) {
  if (!sourcePath || !existsSync(sourcePath)) {
    throw new Error(
      `Missing source artifact for ${arch}.${ext}: ${sourcePath ?? 'not found'}`
    )
  }

  mkdirSync(artifactDir, { recursive: true })
  const destination = path.join(artifactDir, artifactName(arch, ext))
  cpSync(sourcePath, destination)
  console.log(`Published ${destination}`)
  return destination
}

function cleanStagingArtifacts(keepNames) {
  if (!existsSync(artifactDir)) {
    return
  }

  for (const entry of readdirSync(artifactDir)) {
    if (keepNames.includes(entry) || entry.startsWith(artifactPrefix)) {
      continue
    }

    if (
      entry.includes('stable-macos-') ||
      entry.includes('stable-win-') ||
      entry.endsWith('-update.json') ||
      entry.endsWith('.tar.gz')
    ) {
      rmSync(path.join(artifactDir, entry), { recursive: true, force: true })
    }
  }
}

const MAC_DMG_ASSETS_DIR = path.join(rootDir, 'packaging/macos/dmg')
const MAC_INSTALLER_ICONSET_PATH = path.join(rootDir, 'assets/installer.iconset')
const MAC_DMG_BACKGROUND_PATH = path.join(rootDir, 'assets/dmg/background.png')
const MAC_DMG_BACKGROUND_RETINA_PATH = path.join(
  rootDir,
  'assets/dmg/background@2x.png'
)
const MAC_DMG_APP_NAME = 'Farm Control.app'
const MAC_DMG_WINDOW_WIDTH = 540
const MAC_DMG_WINDOW_HEIGHT = 380

function findCommand(command) {
  const which = spawnSync('which', [command], { encoding: 'utf8' })
  if (which.status === 0 && which.stdout.trim()) {
    return which.stdout.trim()
  }

  return null
}

function buildInstallerIcns(destinationPath) {
  if (!existsSync(MAC_INSTALLER_ICONSET_PATH)) {
    throw new Error(
      `Installer iconset not found at ${MAC_INSTALLER_ICONSET_PATH}. Run \`bun run generate-app-icons\` first.`
    )
  }

  mkdirSync(path.dirname(destinationPath), { recursive: true })
  rmSync(destinationPath, { force: true })

  const iconutil = spawnSync(
    'iconutil',
    ['-c', 'icns', '-o', destinationPath, MAC_INSTALLER_ICONSET_PATH],
    { stdio: 'inherit' }
  )

  if (iconutil.status !== 0) {
    throw new Error(
      `iconutil failed to create installer icns with exit code ${iconutil.status ?? 1}`
    )
  }

  return destinationPath
}

function applyMacInstallerFileIcon(targetPath, icnsPath) {
  if (!existsSync(icnsPath)) {
    console.warn(
      `finalize-desktop-artifacts: installer icns not found at ${icnsPath}, skipping custom icon`
    )
    return false
  }

  const fileicon = findCommand('fileicon')
  if (fileicon) {
    const result = spawnSync(fileicon, ['set', targetPath, icnsPath], {
      stdio: 'inherit'
    })

    if (result.status === 0) {
      return true
    }
  }

  const escapedTarget = targetPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
  const escapedIcon = icnsPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
  const result = spawnSync(
    'osascript',
    [
      '-e',
      `tell application "Finder" to set icon of (POSIX file "${escapedTarget}") to (read (POSIX file "${escapedIcon}") as picture)`
    ],
    { stdio: 'inherit' }
  )

  if (result.status === 0) {
    return true
  }

  console.warn(
    'finalize-desktop-artifacts: could not apply installer icon; install fileicon or run on macOS with Finder access'
  )
  return false
}

function ensureMacDmgAssets() {
  mkdirSync(MAC_DMG_ASSETS_DIR, { recursive: true })

  const voliconPath = path.join(MAC_DMG_ASSETS_DIR, 'volicon.icns')
  buildInstallerIcns(voliconPath)

  if (!existsSync(MAC_DMG_BACKGROUND_PATH)) {
    throw new Error(`DMG background not found at ${MAC_DMG_BACKGROUND_PATH}`)
  }

  if (!existsSync(MAC_DMG_BACKGROUND_RETINA_PATH)) {
    console.warn(
      `finalize-desktop-artifacts: retina DMG background not found at ${MAC_DMG_BACKGROUND_RETINA_PATH}`
    )
  }

  return {
    volicon: voliconPath,
    background: MAC_DMG_BACKGROUND_PATH
  }
}

function findCreateDmgCommand() {
  const which = spawnSync('which', ['create-dmg'], { encoding: 'utf8' })
  if (which.status === 0 && which.stdout.trim()) {
    return which.stdout.trim()
  }

  for (const candidate of [
    '/opt/homebrew/bin/create-dmg',
    '/usr/local/bin/create-dmg'
  ]) {
    if (existsSync(candidate)) {
      return candidate
    }
  }

  return null
}

function buildMacDmgWithCreateDmg(
  createDmg,
  dmgPath,
  sourceFolder,
  appBundleName,
  dmgAssets
) {
  rmSync(dmgPath, { force: true })

  const args = [
    '--volname',
    'Farm Control',
    '--window-pos',
    '200',
    '120',
    '--window-size',
    String(MAC_DMG_WINDOW_WIDTH),
    String(MAC_DMG_WINDOW_HEIGHT),
    '--icon-size',
    '100',
    '--icon',
    appBundleName,
    '130',
    '212',
    '--hide-extension',
    appBundleName,
    '--app-drop-link',
    '410',
    '212',
    '--format',
    'UDZO'
  ]

  if (dmgAssets.volicon) {
    args.push('--volicon', dmgAssets.volicon)
  }

  if (dmgAssets.background) {
    args.push('--background', dmgAssets.background)
  }

  args.push(dmgPath, sourceFolder)

  const result = spawnSync(createDmg, args, { stdio: 'inherit' })

  if (result.status !== 0) {
    throw new Error(`create-dmg failed with exit code ${result.status ?? 1}`)
  }

  if (!existsSync(dmgPath)) {
    throw new Error(`create-dmg did not produce ${dmgPath}`)
  }

  return dmgPath
}

function copyMacAppBundle(sourcePath, destinationPath) {
  rmSync(destinationPath, { recursive: true, force: true })
  mkdirSync(path.dirname(destinationPath), { recursive: true })

  const result = spawnSync('ditto', [sourcePath, destinationPath], {
    stdio: 'inherit'
  })

  if (result.status !== 0) {
    throw new Error(`ditto failed with exit code ${result.status ?? 1}`)
  }
}

function canUseDirectDmgSourceFolder(appBundlePath) {
  const platformDir = path.dirname(appBundlePath)
  const appName = path.basename(appBundlePath)
  const entries = readdirSync(platformDir).filter(
    (entry) => !entry.startsWith('.') && entry !== '.finalize-dmg-staging'
  )

  return entries.length === 1 && entries[0] === appName
}

async function buildMacDmg(appBundlePath, arch) {
  const createDmg = findCreateDmgCommand()
  if (!createDmg) {
    throw new Error(
      'create-dmg not found. Install with: brew install create-dmg'
    )
  }

  const appBundleName = path.basename(appBundlePath)
  if (appBundleName !== MAC_DMG_APP_NAME) {
    console.warn(
      `finalize-desktop-artifacts: expected ${MAC_DMG_APP_NAME}, found ${appBundleName}`
    )
  }

  const dmgPath = path.join(artifactDir, artifactName(arch, 'dmg'))

  mkdirSync(artifactDir, { recursive: true })

  const stagingDir = path.join(
    path.dirname(appBundlePath),
    '.finalize-dmg-staging'
  )
  const useDirectSource = canUseDirectDmgSourceFolder(appBundlePath)
  const sourceFolder = useDirectSource
    ? path.dirname(appBundlePath)
    : stagingDir

  if (!useDirectSource) {
    rmSync(stagingDir, { recursive: true, force: true })
    mkdirSync(stagingDir, { recursive: true })
    const stagedAppPath = path.join(stagingDir, path.basename(appBundlePath))
    copyMacAppBundle(appBundlePath, stagedAppPath)
    codesignMacAppBundle(stagedAppPath)
  }

  let builtDmgPath
  const dmgAssets = ensureMacDmgAssets()

  try {
    builtDmgPath = buildMacDmgWithCreateDmg(
      createDmg,
      dmgPath,
      sourceFolder,
      appBundleName,
      dmgAssets
    )
    applyMacInstallerFileIcon(builtDmgPath, dmgAssets.volicon)
  } finally {
    if (!useDirectSource) {
      rmSync(stagingDir, { recursive: true, force: true })
    }
  }

  console.log(`Published ${builtDmgPath}`)
  return builtDmgPath
}

function cleanMacBuildDir(arch) {
  const platformDir = path.join(getBuildRoot(), `stable-macos-${arch}`)
  if (existsSync(platformDir)) {
    rmSync(platformDir, { recursive: true, force: true })
    console.log(`Removed build output ${platformDir}`)
  }
}

function buildMacPkg(appBundlePath, arch) {
  const pkgPath = path.join(artifactDir, artifactName(arch, 'pkg'))

  const result = spawnSync(
    'pkgbuild',
    [
      '--component',
      appBundlePath,
      '--install-location',
      '/Applications',
      '--identifier',
      identifier,
      '--version',
      version,
      pkgPath
    ],
    { stdio: 'inherit' }
  )

  if (result.status !== 0) {
    throw new Error(`pkgbuild failed with exit code ${result.status ?? 1}`)
  }

  console.log(`Published ${pkgPath}`)
  return pkgPath
}

function stageWindowsDeeplinkScript(appDir) {
  return stageWindowsDeeplink(appDir)
}

function cleanWinBuildDir(arch) {
  const platformDir = path.join(getBuildRoot(), `stable-win-${arch}`)
  if (existsSync(platformDir)) {
    rmSync(platformDir, { recursive: true, force: true })
    console.log(`Removed build output ${platformDir}`)
  }
}

function buildWindowsNsis(appDir, arch) {
  const scriptPath = path.join(rootDir, 'scripts/build-windows-nsis.ps1')
  const exePath = path.join(artifactDir, artifactName(arch, 'exe'))
  const buildInfoPath = path.join(rootDir, 'src/buildInfo.json')
  const buildInfo = existsSync(buildInfoPath)
    ? JSON.parse(readFileSync(buildInfoPath, 'utf8'))
    : {}
  const buildNumber =
    process.env.BUILD_NUMBER ||
    process.env.VITE_BUILD_NUMBER ||
    buildInfo.buildNumber ||
    'dev'
  const powershell = process.env.SystemRoot
    ? path.join(
        process.env.SystemRoot,
        'System32',
        'WindowsPowerShell',
        'v1.0',
        'powershell.exe'
      )
    : 'powershell.exe'

  const result = spawnSync(
    powershell,
    [
      '-NoProfile',
      '-ExecutionPolicy',
      'Bypass',
      '-File',
      scriptPath,
      '-AppDir',
      appDir,
      '-OutputExe',
      exePath,
      '-Version',
      version,
      '-BuildNumber',
      buildNumber
    ],
    { stdio: 'inherit' }
  )

  if (result.status !== 0) {
    throw new Error(
      `build-windows-nsis.ps1 failed with exit code ${result.status ?? 1}`
    )
  }

  const minInstallerBytes = 10 * 1024 * 1024
  const installerSize = statSync(exePath).size
  if (installerSize < minInstallerBytes) {
    throw new Error(
      `Windows installer ${path.basename(exePath)} is only ${(installerSize / 1024).toFixed(1)} KiB; expected at least ${minInstallerBytes / (1024 * 1024)} MiB`
    )
  }

  console.log(`Published ${exePath}`)
  return exePath
}

function buildWindowsMsi(setupExePath, arch) {
  const scriptPath = path.join(rootDir, 'scripts/build-windows-msi.ps1')
  const msiPath = path.join(artifactDir, artifactName(arch, 'msi'))
  const powershell = process.env.SystemRoot
    ? path.join(
        process.env.SystemRoot,
        'System32',
        'WindowsPowerShell',
        'v1.0',
        'powershell.exe'
      )
    : 'powershell.exe'

  const result = spawnSync(
    powershell,
    [
      '-NoProfile',
      '-ExecutionPolicy',
      'Bypass',
      '-File',
      scriptPath,
      '-SetupExe',
      setupExePath,
      '-OutputMsi',
      msiPath,
      '-Version',
      version
    ],
    { stdio: 'inherit' }
  )

  if (result.status !== 0) {
    throw new Error(
      `build-windows-msi.ps1 failed with exit code ${result.status ?? 1}`
    )
  }

  console.log(`Published ${msiPath}`)
  return msiPath
}

if (buildEnv === 'dev') {
  console.log('finalize-desktop-artifacts: skipping dev build')
  process.exit(0)
}

async function main() {
  console.log(
    `finalize-desktop-artifacts: os=${targetOs} arch=${buildArch} cef=${bundleCef} version=${version}`
  )

  if (targetOs === 'macos') {
    const appBundle = findMacAppBundle(buildArch)

    if (!appBundle) {
      console.log(
        `finalize-desktop-artifacts: no macOS ${buildArch} app bundle found, skipping`
      )
      process.exit(0)
    }

    codesignMacAppBundle(appBundle)

    const existingDmg = findMacDmgSource(buildArch)
    const dmgPath = existingDmg
      ? publishArtifact(existingDmg, buildArch, 'dmg')
      : await buildMacDmg(appBundle, buildArch)

    const published = [dmgPath, buildMacPkg(appBundle, buildArch)]

    cleanStagingArtifacts(published.map((filePath) => path.basename(filePath)))
    cleanMacBuildDir(buildArch)
    return
  }

  if (targetOs === 'win') {
    const arch = getReleaseArch(process.env.ELECTROBUN_ARCH || 'x64')
    const installerFiles = findWindowsInstallerFiles()

    if (!installerFiles?.setupArchive) {
      throw new Error(
        'Could not find the Windows setup archive (.tar.zst) to expand'
      )
    }

    const platformDir = path.dirname(installerFiles.setupArchive)
    const appDir = expandWindowsAppFromArchive(
      installerFiles.setupArchive,
      platformDir
    )
    stageWindowsDeeplinkScript(appDir)
    patchWindowsBinaries(appDir)

    let published
    try {
      const setupExe = buildWindowsNsis(appDir, arch)
      published = [setupExe, buildWindowsMsi(setupExe, arch)]

      if (installerFiles.setupZip) {
        published.push(publishArtifact(installerFiles.setupZip, arch, 'zip'))
      }
    } finally {
      cleanExpandedWindowsApp(platformDir)
    }

    cleanStagingArtifacts(published.map((filePath) => path.basename(filePath)))
    cleanWinBuildDir(arch)
    return
  }

  console.log(
    'finalize-desktop-artifacts: no desktop packaging configured for',
    targetOs ?? 'unknown target'
  )
  process.exit(0)
}

await main()
