import { spawn } from 'child_process'
import { promises as fs } from 'fs'
import os from 'os'
import path from 'path'

const PE_MZ_HEADER = Buffer.from([0x4d, 0x5a]) // "MZ"
const COPY_PROGRESS_START = 5
const COPY_PROGRESS_END = 90
const COPY_PROGRESS_RANGE = COPY_PROGRESS_END - COPY_PROGRESS_START

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

const formatBytes = (bytes) => {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
  return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
}

const parseWindowsInstallerProgress = (output) => {
  const lines = String(output || '').split(/\r?\n/)
  let percent = null
  let message = 'Installing update...'
  let copyTotalBytes = 0
  let copiedBytes = 0

  for (const line of lines) {
    if (line.startsWith('installer:PHASE:')) {
      message = line.slice('installer:PHASE:'.length).trim() || message
    } else if (line.startsWith('installer:STATUS:')) {
      const status = line.slice('installer:STATUS:'.length).trim()
      if (status) message = status
    } else if (line.startsWith('installer:%')) {
      const value = Number.parseFloat(line.slice('installer:%'.length))
      if (Number.isFinite(value)) {
        percent = Math.min(100, Math.round(value <= 1 ? value * 100 : value))
      }
    } else if (line.startsWith('installer:COPY_TOTAL:')) {
      const value = Number.parseInt(
        line.slice('installer:COPY_TOTAL:'.length),
        10
      )
      if (Number.isFinite(value) && value > 0) {
        copyTotalBytes = value
        copiedBytes = 0
      }
    } else if (line.startsWith('installer:COPY_FILE:')) {
      const copyFile = line.match(/^installer:COPY_FILE:(\d+):(.*)$/)
      if (copyFile) {
        const fileBytes = Number.parseInt(copyFile[1], 10)
        var fileName = copyFile[2].trim()
        if (fileName.includes('\\')) {
          fileName = fileName.split('\\').pop()
        }

        if (Number.isFinite(fileBytes)) {
          copiedBytes += fileBytes
          if (copyTotalBytes > 0) {
            percent = Math.min(
              COPY_PROGRESS_END,
              COPY_PROGRESS_START +
                Math.round(
                  (copiedBytes / copyTotalBytes) * COPY_PROGRESS_RANGE
                )
            )
          }
        }

        if (fileName) {
          message = `Copying ${fileName} (${formatBytes(fileBytes)})`
        }
      }
    } else if (
      line.startsWith('installer: ') &&
      !line.startsWith('installer:PHASE:') &&
      !line.startsWith('installer:STATUS:') &&
      !line.startsWith('installer:%') &&
      !line.startsWith('installer:COPY_')
    ) {
      const text = line.slice('installer: '.length).trim()
      if (text) message = text
    }
  }

  return { percent, message }
}

const isWindowsInstallSuccessful = (output) =>
  /installer: The install was successful\./i.test(output)

const isWindowsInstallFailed = (output) =>
  /installer: The install failed/i.test(output)

const isValidNsisInstaller = async (filePath) => {
  const handle = await fs.open(filePath, 'r')
  try {
    const header = Buffer.alloc(PE_MZ_HEADER.length)
    await handle.read(header, 0, header.length, 0)
    return header.equals(PE_MZ_HEADER)
  } finally {
    await handle.close()
  }
}

export const prepareInstallerPath = async (installerPath) => {
  const fileName = path.basename(installerPath)
  const updateDir = path.join(
    os.homedir(),
    'AppData',
    'Local',
    'FarmControl',
    'Updates'
  )
  await fs.mkdir(updateDir, { recursive: true })

  const stablePath = path.join(updateDir, fileName)
  await fs.copyFile(installerPath, stablePath)

  const resolvedPath = await fs.realpath(stablePath)
  const stats = await fs.stat(resolvedPath)

  if (!stats.isFile() || stats.size === 0) {
    throw new Error('Update installer file is missing or empty.')
  }

  if (!(await isValidNsisInstaller(resolvedPath))) {
    throw new Error(
      'Downloaded update is not a valid Windows installer. The file may be corrupted or incomplete.'
    )
  }

  return resolvedPath
}

const startWindowsInstallerProgressWatch = (
  logPath,
  sendProgress,
  { onInstallSuccessful, onInstallFailed, onStalled, stallTimeoutMs }
) => {
  let installerOutput = ''
  let offset = 0
  let lastPercent = null
  let lastMessage = null
  let outcomeNotified = false
  let lastActivityAt = Date.now()

  const poll = async () => {
    try {
      const stat = await fs.stat(logPath)
      if (stat.size <= offset) return

      const handle = await fs.open(logPath, 'r')
      try {
        const buffer = Buffer.alloc(stat.size - offset)
        await handle.read(buffer, 0, buffer.length, offset)
        offset = stat.size
        installerOutput += buffer.toString('utf8')
        lastActivityAt = Date.now()

        const { percent, message } =
          parseWindowsInstallerProgress(installerOutput)
        const resolvedMessage = message || 'Installing update...'

        if (percent !== lastPercent || resolvedMessage !== lastMessage) {
          lastPercent = percent
          lastMessage = resolvedMessage
          sendProgress({
            phase: 'installing',
            percent,
            message: resolvedMessage
          })
        }

        if (!outcomeNotified) {
          if (isWindowsInstallFailed(installerOutput)) {
            outcomeNotified = true
            onInstallFailed?.(installerOutput)
          } else if (isWindowsInstallSuccessful(installerOutput)) {
            outcomeNotified = true
            onInstallSuccessful?.(installerOutput)
          }
        }
      } finally {
        await handle.close()
      }
    } catch (error) {
      if (error?.code !== 'ENOENT') {
        console.error('[app-update] installer log poll error:', error)
      }
    }
  }

  const intervalId = setInterval(() => {
    // The installer runs detached (outside our job object), so a dead
    // installer only shows up as the log going quiet before any outcome.
    if (
      !outcomeNotified &&
      stallTimeoutMs > 0 &&
      Date.now() - lastActivityAt > stallTimeoutMs
    ) {
      outcomeNotified = true
      onStalled?.(installerOutput)
      return
    }

    poll().catch((error) => {
      console.error('[app-update] installer log poll error:', error)
    })
  }, 200)

  return async () => {
    clearInterval(intervalId)
    await poll()
    return installerOutput
  }
}

export const launchWindowsInstaller = async (
  mainWindow,
  installerPath,
  webContents,
  { sendProgress, getInstallErrorMessage }
) => {
  const resolvedPath = await prepareInstallerPath(installerPath)
  const logPath = path.join(path.dirname(resolvedPath), 'install.log')

  sendProgress({
    phase: 'installing',
    percent: 0,
    message: 'Installing update...'
  })

  await fs.unlink(logPath).catch(() => {})

  // Allow file handles from the download/copy to settle before the installer opens.
  await sleep(2000)

  if (mainWindow && !mainWindow.isDestroyed?.()) {
    mainWindow.focus?.()
    mainWindow.show?.()
  }

  return new Promise((resolve, reject) => {
    let settled = false

    const settleSuccess = (output) => {
      if (settled) return
      settled = true

      const { percent, message } = parseWindowsInstallerProgress(output)

      sendProgress({
        phase: 'installing',
        percent: percent ?? 100,
        message: message || 'Installation complete. Restarting Farm Control...'
      })

      resolve()
    }

    const settleFailure = (message) => {
      if (settled) return
      settled = true

      sendProgress({
        phase: 'error',
        percent: null,
        message
      })
      reject(new Error(message))
    }

    const stopProgressWatch = startWindowsInstallerProgressWatch(
      logPath,
      sendProgress,
      {
        stallTimeoutMs: 3 * 60 * 1000,
        onInstallSuccessful: (output) => {
          settleSuccess(output)
        },
        onInstallFailed: (output) => {
          settleFailure(getInstallErrorMessage(null, output))
        },
        onStalled: (output) => {
          settleFailure(
            getInstallErrorMessage(
              new Error('The update installer stopped responding.'),
              output
            )
          )
        }
      }
    )

    const refreshInstallerOutput = async () => {
      return await stopProgressWatch()
    }

    // Silent NSIS install.
    // /S = silent (https://nsis.sourceforge.io/Reference/SilentInstall)
    // /UPDATE = in-app update (stage into Farm Control.new; swap after exit)
    // /RESTARTFC = installer waits for this process to exit, swaps folders, relaunches
    // /PARENTPID = exact process to wait for (CEF may outlive the launcher)
    // /LOG= = progress log (installer:% / PHASE / STATUS / COPY_TOTAL / COPY_FILE)
    const installerCommandLine = [
      `"${resolvedPath}"`,
      '/S',
      '/UPDATE',
      '/RESTARTFC',
      `/PARENTPID=${process.pid}`,
      `/LOG="${logPath}"`
    ].join(' ')

    // This process runs inside a Windows job object (CEF/launcher) that kills
    // every child process when the app exits — a directly spawned installer
    // (even with detached: true) inherits the job and dies mid folder-swap the
    // moment the app quits. Creating the process via WMI (Win32_Process.Create)
    // parents it to the WMI provider host, outside our job, so it survives.
    const quoteForPowershell = (value) => `'${value.replace(/'/g, "''")}'`

    const spawnerProcess = spawn(
      'powershell.exe',
      [
        '-NoProfile',
        '-NonInteractive',
        '-WindowStyle',
        'Hidden',
        '-Command',
        `$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${quoteForPowershell(installerCommandLine)} }; exit $result.ReturnValue`
      ],
      {
        stdio: 'ignore',
        windowsHide: true
      }
    )

    spawnerProcess.on('error', async (error) => {
      const output = await refreshInstallerOutput()
      console.error('[app-update] installer launch error:', error)
      settleFailure(getInstallErrorMessage(error, output))
    })

    spawnerProcess.on('exit', async (code) => {
      // PowerShell exits as soon as the installer is created; from here the
      // log watcher owns success/failure. Only a non-zero code (WMI create
      // failed) means the installer never started.
      if (code === 0 || settled) return

      const output = await refreshInstallerOutput()
      settleFailure(
        getInstallErrorMessage(
          new Error(`Failed to launch the update installer (code ${code}).`),
          output
        )
      )
    })
  })
}
