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

const MSI_OLE_HEADER = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1])
const DEBUG_PREFIX = '[app-update][win-progress]'

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

const debugLog = () => {}

const decodeMsiLogBuffer = (buffer) => {
  if (!buffer?.length) return ''

  if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
    debugLog('decoded MSI log as UTF-16 LE (BOM)')
    return buffer.subarray(2).toString('utf16le')
  }

  const sample = buffer.subarray(0, Math.min(buffer.length, 64))
  const looksUtf16 =
    sample.length >= 4 &&
    sample.filter((byte) => byte === 0).length > sample.length / 4

  if (looksUtf16) {
    debugLog('decoded MSI log as UTF-16 LE (heuristic)')
    return buffer.toString('utf16le')
  }

  debugLog('decoded MSI log as UTF-8')
  return buffer.toString('utf8')
}

const formatMsiActionName = (actionName) => {
  const humanized = String(actionName)
    .replace(/([a-z])([A-Z])/g, '$1 $2')
    .replace(/_/g, ' ')
    .toLowerCase()
    .trim()

  if (!humanized) return 'Installing update...'

  return `${humanized.charAt(0).toUpperCase()}${humanized.slice(1)}...`
}

const parseWindowsInstallerProgress = (output) => {
  const lines = String(output || '').split(/\r?\n/)
  let percent = null
  let message = 'Installing update...'
  let totalTicks = 0
  let currentTicks = 0
  let actionStarts = 0
  let actionEnds = 0
  const matchedLines = []

  for (const line of lines) {
    const actionStart = line.match(/^Action start \d{2}:\d{2}:\d{2}: (.+?)\./)
    if (actionStart) {
      actionStarts += 1
      message = formatMsiActionName(actionStart[1])
      matchedLines.push(`action-start:${actionStart[1]}`)
    }

    const doingAction = line.match(/Doing action:\s*(.+)$/)
    if (doingAction && !actionStart) {
      message = formatMsiActionName(doingAction[1])
      matchedLines.push(`doing-action:${doingAction[1]}`)
    }

    if (/^Action ended \d{2}:\d{2}:\d{2}: .+?\. Return value \d+\./.test(line)) {
      actionEnds += 1
      matchedLines.push('action-ended')
    }

    const progressReset = line.match(/^\s*0\s+(\d+)\s+0(?:\s+\d+)?\s*$/)
    if (progressReset) {
      totalTicks = Number.parseInt(progressReset[1], 10) || 0
      currentTicks = 0
      matchedLines.push(`progress-reset:${totalTicks}`)
    }

    const progressIncrement = line.match(/^\s*2\s+(\d+)\s*$/)
    if (progressIncrement) {
      currentTicks += Number.parseInt(progressIncrement[1], 10) || 0
      matchedLines.push(`progress-increment:${progressIncrement[1]}`)
    }

    const progressAddition = line.match(/^\s*3\s+(\d+)\s*$/)
    if (progressAddition) {
      totalTicks += Number.parseInt(progressAddition[1], 10) || 0
      matchedLines.push(`progress-addition:${progressAddition[1]}`)
    }

    if (/Installation success or error status:\s*0\b/.test(line)) {
      percent = 100
      message = 'Installation complete. Restarting Farm Control...'
      matchedLines.push('install-success')
    }
  }

  if (percent !== 100) {
    if (totalTicks > 0) {
      percent = Math.min(99, Math.round((currentTicks / totalTicks) * 100))
    } else if (actionStarts > 0) {
      percent = Math.min(
        95,
        Math.max(5, Math.round((actionEnds / actionStarts) * 90))
      )
    }
  }

  return {
    percent,
    message,
    stats: {
      lineCount: lines.length,
      actionStarts,
      actionEnds,
      totalTicks,
      currentTicks,
      matchedLines: matchedLines.slice(-8)
    }
  }
}

const isWindowsInstallSuccessful = (output) =>
  /Installation success or error status:\s*0\b/.test(output) ||
  /MainEngineThread is returning 0\b/.test(output)

const isWindowsInstallFailed = (output) =>
  /Installation success or error status:\s*[1-9]\d*\b/.test(output) ||
  /MainEngineThread is returning [1-9]\d*\b/.test(output)

const isValidMsiPackage = async (filePath) => {
  const handle = await fs.open(filePath, 'r')
  try {
    const header = Buffer.alloc(MSI_OLE_HEADER.length)
    await handle.read(header, 0, header.length, 0)
    return header.equals(MSI_OLE_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)

  // Resolve to a canonical long path. Short 8.3 paths (e.g. ADMINI~1) break msiexec.
  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 isValidMsiPackage(resolvedPath))) {
    throw new Error(
      'Downloaded update is not a valid Windows Installer package. The file may be corrupted or incomplete.'
    )
  }

  return resolvedPath
}

const startWindowsInstallerProgressWatch = (logPath, sendProgress) => {
  let installerOutput = ''
  let lastLogSize = 0
  let lastPercent = null
  let lastMessage = null
  let pollCount = 0

  const poll = async () => {
    pollCount += 1

    try {
      const stat = await fs.stat(logPath)
      if (stat.size === 0) {
        debugLog(`poll #${pollCount}: log exists but is empty`, { logPath })
        return
      }

      if (stat.size === lastLogSize) {
        debugLog(`poll #${pollCount}: no new log data`, {
          logPath,
          size: stat.size
        })
        return
      }

      const buffer = Buffer.alloc(stat.size)
      const handle = await fs.open(logPath, 'r')
      try {
        await handle.read(buffer, 0, stat.size, 0)
      } finally {
        await handle.close()
      }

      lastLogSize = stat.size
      installerOutput = decodeMsiLogBuffer(buffer)

      const { percent, message, stats } =
        parseWindowsInstallerProgress(installerOutput)
      const resolvedPercent = percent ?? lastPercent ?? 0
      const resolvedMessage = message || 'Installing update...'

      debugLog(`poll #${pollCount}: parsed installer log`, {
        logPath,
        size: stat.size,
        textLength: installerOutput.length,
        preview: installerOutput.slice(0, 240).replace(/\s+/g, ' '),
        parsed: stats,
        resolvedPercent,
        resolvedMessage
      })

      if (
        resolvedPercent !== lastPercent ||
        resolvedMessage !== lastMessage
      ) {
        debugLog(`poll #${pollCount}: sending progress update`, {
          percent: resolvedPercent,
          message: resolvedMessage
        })

        lastPercent = resolvedPercent
        lastMessage = resolvedMessage
        sendProgress( {
          phase: 'installing',
          percent: resolvedPercent,
          message: resolvedMessage
        })
      } else {
        debugLog(`poll #${pollCount}: progress unchanged, skipping UI update`, {
          percent: resolvedPercent,
          message: resolvedMessage
        })
      }
    } catch (error) {
      if (error?.code === 'ENOENT') {
        debugLog(`poll #${pollCount}: log file not created yet`, { logPath })
        return
      }

      console.error(`${DEBUG_PREFIX} installer log poll error:`, error)
    }
  }

  const intervalId = setInterval(() => {
    poll().catch((error) => {
      console.error(`${DEBUG_PREFIX} installer log poll error:`, error)
    })
  }, 300)

  return async () => {
    clearInterval(intervalId)
    await poll()
    debugLog('stopped progress watch', {
      logPath,
      finalSize: lastLogSize,
      textLength: installerOutput.length,
      pollCount
    })
    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')

  debugLog('prepared installer', {
    installerPath,
    resolvedPath,
    logPath
  })

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

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

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

  const stopProgressWatch = startWindowsInstallerProgressWatch(
    logPath,
    sendProgress
  )

  return new Promise((resolve, reject) => {
    let processOutput = ''
    const startedAt = Date.now()

    const installerArgs = [
      '/i',
      resolvedPath,
      '/qn',
      '/norestart',
      'ALLUSERS=2',
      'MSIINSTALLPERUSER=1',
      'REBOOT=ReallySuppress',
      '/L*v!',
      logPath
    ]

    debugLog('spawning msiexec', {
      args: installerArgs,
      elapsedMs: Date.now() - startedAt
    })

    const installerProcess = spawn('msiexec.exe', installerArgs, {
      stdio: ['ignore', 'pipe', 'pipe'],
      windowsHide: true
    })

    installerProcess.stdout?.on('data', (data) => {
      const text = data.toString('utf16le')
      processOutput += text
      debugLog('msiexec stdout chunk', {
        length: text.length,
        preview: text.slice(0, 200)
      })
    })

    installerProcess.stderr?.on('data', (data) => {
      const text = data.toString('utf16le')
      processOutput += text
      debugLog('msiexec stderr chunk', {
        length: text.length,
        preview: text.slice(0, 200)
      })
    })

    installerProcess.on('spawn', () => {
      debugLog('msiexec spawned', {
        pid: installerProcess.pid,
        elapsedMs: Date.now() - startedAt
      })
    })

    installerProcess.on('error', async (error) => {
      console.error(`${DEBUG_PREFIX} installer spawn error:`, error)
      const watchedOutput = await stopProgressWatch()

      debugLog('installer spawn failed', {
        watchedOutputLength: watchedOutput.length,
        processOutputLength: processOutput.length
      })

      const message = error?.message || 'Failed to start update installer.'
      sendProgress( {
        phase: 'error',
        percent: null,
        message
      })
      reject(error)
    })

    installerProcess.on('exit', async (code, signal) => {
      const watchedOutput = await stopProgressWatch()
      const output = watchedOutput || processOutput
      const finalParse = parseWindowsInstallerProgress(output)

      debugLog('msiexec exited', {
        code,
        signal,
        elapsedMs: Date.now() - startedAt,
        watchedOutputLength: watchedOutput.length,
        processOutputLength: processOutput.length,
        parsed: finalParse.stats,
        outputPreview: output.slice(0, 500).replace(/\s+/g, ' ')
      })

      debugLog('keeping install log', { logPath })

      if (code !== 0) {
        const message = getInstallErrorMessage(null, output)
        sendProgress( {
          phase: 'error',
          percent: null,
          message
        })
        reject(new Error(message))
        return
      }

      const succeeded =
        isWindowsInstallSuccessful(output) ||
        (code === 0 && !isWindowsInstallFailed(output))

      debugLog('install success evaluation', {
        succeeded,
        isSuccessful: isWindowsInstallSuccessful(output),
        isFailed: isWindowsInstallFailed(output),
        exitCode: code
      })

      if (!succeeded) {
        const message = getInstallErrorMessage(null, output)
        sendProgress( {
          phase: 'error',
          percent: null,
          message
        })
        reject(new Error(message))
        return
      }

      const { percent, message } = finalParse

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

      debugLog('installer completed successfully')
      resolve()
    })
  })
}
