import { createWriteStream, promises as fs } from 'node:fs'
import http from 'node:http'
import https from 'node:https'
import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { Utils } from 'electrobun/bun'
import { launchMacInstaller } from './macappupdate.js'
import { launchWindowsInstaller } from './winappupdate.js'
import { checkForDuplicateInstallations } from './check-duplicate-installations.js'
import { scheduleAppRestart } from './updater-runner.js'
import buildInfo from '../buildInfo.json'
import { version as packageVersion } from '../../package.json'
import { getAppSettings, setAppSettings } from './store.js'

const SUPPORTED_TARGETS = {
  darwin: {
    extension: '.pkg',
    osMatchers: ['darwin', 'mac', 'macos', 'osx']
  },
  win32: {
    extension: '.exe',
    osMatchers: ['win32', 'win', 'windows']
  }
}

const DEFAULT_UPDATE_ENGINE = 'native'

let runningUpdate = null

const getArtifactName = (artifact) =>
  String(artifact?.fileName || artifact?.relativePath || artifact?.url || '')

const normalizeArch = (arch) => {
  if (arch === 'x64' || arch === 'amd64') return 'x64'
  if (arch === 'arm64' || arch === 'aarch64') return 'arm64'
  return arch
}

const normalizeEngine = (engine) => {
  const value = String(engine || '')
    .trim()
    .toLowerCase()

  if (value === 'chromium' || value === 'cef') return 'chromium'
  if (value === 'native') return 'native'
  return null
}

const artifactIsChromium = (artifact) => {
  const explicit = String(
    artifact?.engine || artifact?.renderer || ''
  ).toLowerCase()

  if (explicit === 'cef' || explicit === 'chromium') return true
  if (explicit === 'native') return false

  const name = getArtifactName(artifact).toLowerCase()
  return /[-_.]cef(?:[-_.]|$)/.test(name)
}

const artifactMatchesEngine = (artifact, engine) => {
  const wantsChromium = normalizeEngine(engine) === 'chromium'
  return artifactIsChromium(artifact) === wantsChromium
}

const artifactMatchesPlatform = (artifact, target, platform, arch) => {
  const name = getArtifactName(artifact).toLowerCase()
  const normalizedArch = normalizeArch(arch)
  const artifactArch = normalizeArch(String(artifact?.arch || '').toLowerCase())
  const artifactPlatform = String(
    artifact?.platform || artifact?.os || artifact?.target || ''
  ).toLowerCase()

  if (!name.endsWith(target.extension)) return false
  if (!artifact?.url) return false

  const matchesArch =
    artifactArch === normalizedArch ||
    name.includes(`-${normalizedArch}`) ||
    name.includes(`_${normalizedArch}`) ||
    name.includes(`.${normalizedArch}.`) ||
    name.includes(normalizedArch)

  const matchesOs =
    !artifactPlatform ||
    target.osMatchers.includes(artifactPlatform) ||
    target.osMatchers.some((matcher) => name.includes(matcher)) ||
    (platform === 'darwin' && name.includes('mac')) ||
    (platform === 'win32' && name.includes('win'))

  return matchesArch && matchesOs
}

const selectUpdateArtifact = (
  update,
  platform = process.platform,
  arch = process.arch
) => {
  const target = SUPPORTED_TARGETS[platform]
  if (!target) {
    throw new Error(`App updates are not supported on ${platform}.`)
  }

  const engine = normalizeEngine(update?.engine) || DEFAULT_UPDATE_ENGINE
  const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : []
  const matchingArtifact = artifacts.find(
    (artifact) =>
      artifactMatchesPlatform(artifact, target, platform, arch) &&
      artifactMatchesEngine(artifact, engine)
  )

  if (!matchingArtifact) {
    const engineLabel = engine === 'chromium' ? 'Chromium (cef)' : 'Native'
    throw new Error(
      `No ${target.extension} ${engineLabel} update artifact found for ${platform}/${arch}.`
    )
  }

  return matchingArtifact
}

const getInstallErrorMessage = (error, output = '') => {
  const combined = `${output}\n${error?.message || ''}`.trim()

  if (
    /cancel/i.test(combined) ||
    /did not grant permission/i.test(combined) ||
    /user canceled/i.test(combined)
  ) {
    return 'Update installation was cancelled.'
  }

  if (/incorrect/i.test(combined)) {
    return 'The administrator password was incorrect.'
  }

  if (
    /1625/.test(combined) ||
    /forbidden by system policy/i.test(combined) ||
    /Non-assigned apps are disabled/i.test(combined)
  ) {
    return 'Update installation was blocked by system policy.'
  }

  return combined || 'Failed to install update.'
}

const getDownloadUrl = (url, redirectCount = 0) =>
  new Promise((resolve, reject) => {
    if (redirectCount > 5) {
      reject(new Error('Too many redirects while downloading update.'))
      return
    }

    const parsedUrl = new URL(url)
    const client = parsedUrl.protocol === 'https:' ? https : http
    const request = client.get(parsedUrl, (response) => {
      const location = response.headers.location

      if (response.statusCode >= 300 && response.statusCode < 400 && location) {
        response.resume()
        resolve(
          getDownloadUrl(
            new URL(location, parsedUrl).toString(),
            redirectCount + 1
          )
        )
        return
      }

      resolve({ response, url: parsedUrl.toString() })
    })

    request.on('error', reject)
  })

const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
  const { response } = await getDownloadUrl(artifact.url)

  if (response.statusCode < 200 || response.statusCode >= 300) {
    response.resume()
    throw new Error(`Update download failed with HTTP ${response.statusCode}.`)
  }

  const totalBytes =
    Number.parseInt(response.headers['content-length'], 10) || 0
  let downloadedBytes = 0

  await new Promise((resolve, reject) => {
    const output = createWriteStream(destinationPath)

    response.on('data', (chunk) => {
      downloadedBytes += chunk.length
      const percent = totalBytes
        ? Math.round((downloadedBytes / totalBytes) * 100)
        : null

      sendProgress({
        phase: 'downloading',
        percent,
        downloadedBytes,
        totalBytes,
        message: totalBytes
          ? `Downloading update (${percent}%)`
          : 'Downloading update'
      })
    })

    response.on('error', reject)
    output.on('error', reject)
    output.on('finish', resolve)
    response.pipe(output)
  })
}

const getRunningEngine = (mainWindow) =>
  mainWindow?.renderer === 'cef' ? 'chromium' : 'native'

const formatFullAppVersion = (version, buildNumber) => {
  const normalizedVersion = String(version || '')
    .trim()
    .replace(/^v/i, '')

  if (!normalizedVersion) return null

  const build = String(
    buildNumber || process.env.BUILD_NUMBER || process.env.VITE_BUILD_NUMBER || ''
  ).trim()
  const buildSuffix =
    !build || build === 'dev' ? 'dev' : build.startsWith('b') ? build : `b${build}`

  return `v${normalizedVersion}-${buildSuffix}`
}

export const getRunningAppVersion = () =>
  formatFullAppVersion(packageVersion, buildInfo?.buildNumber)

const getRunningAppState = (mainWindow, settings) => ({
  version: getRunningAppVersion(),
  branch: settings?.appUpdateRunningBranch || null,
  engine: getRunningEngine(mainWindow)
})

// Snapshot the running version/branch/engine so the next launch can tell
// whether an update actually completed.
const persistCurrentAppState = async (mainWindow) => {
  try {
    const settings = await getAppSettings()
    await setAppSettings({
      ...settings,
      current: getRunningAppState(mainWindow, settings)
    })
  } catch (error) {
    console.warn('[app-update] Failed to persist current app state.', error)
  }
}

// Ignore missing values: a field only counts as changed when it was recorded
// both before and after the update.
const stateValueChanged = (previous, next) =>
  Boolean(previous) && Boolean(next) && previous !== next

const formatEngineLabel = (engine) => {
  const normalized = normalizeEngine(engine)
  if (normalized === 'chromium') return 'Chromium'
  if (normalized === 'native') return 'Native'
  return String(engine || 'Unknown')
}

export const getUpdateInstalledMessage = ({ changes, current } = {}) => {
  if (!current) return 'Farm Control was successfully updated.'

  const versionChanged = Boolean(changes?.version && current.version)
  const branchChanged = Boolean(changes?.branch && current.branch)
  const engineChanged = Boolean(changes?.engine && current.engine)

  if (!versionChanged && !branchChanged && !engineChanged) {
    const version = current.version || getRunningAppVersion()
    return version
      ? `Farm Control was successfully updated to version ${version}.`
      : 'Farm Control was successfully updated.'
  }

  const segments = []

  if (versionChanged) {
    segments.push({ type: 'version', text: `version ${current.version}` })
  }
  if (branchChanged) {
    segments.push({ type: 'branch', text: `the ${current.branch} branch` })
  }
  if (engineChanged) {
    segments.push({
      type: 'engine',
      text: `the ${formatEngineLabel(current.engine)} engine`
    })
  }

  if (segments.length === 1) {
    const [segment] = segments
    if (segment.type === 'engine') {
      return `Farm Control was successfully updated to use ${segment.text}.`
    }
    return `Farm Control was successfully updated to ${segment.text}.`
  }

  let message = `Farm Control was successfully updated to ${segments[0].text}`

  for (const segment of segments.slice(1)) {
    if (segment.type === 'branch') {
      message += ` on ${segment.text}`
    } else if (segment.type === 'engine') {
      message += ` using ${segment.text}`
    } else {
      message += ` and ${segment.text}`
    }
  }

  return `${message}.`
}

let completedUpdateResult = null

export const checkForCompletedUpdate = async (mainWindow) => {
  // Cache per process so repeated renderer calls (e.g. remounts) get the same
  // answer instead of a false negative after `current` has been rewritten.
  if (completedUpdateResult) return completedUpdateResult

  try {
    const settings = await getAppSettings()
    const previous =
      settings?.current && typeof settings.current === 'object'
        ? settings.current
        : null
    const current = getRunningAppState(mainWindow, settings)

    await setAppSettings({ ...settings, current })

    const changes = {
      version: stateValueChanged(previous.version, current.version),
      branch: stateValueChanged(previous.branch, current.branch),
      engine: stateValueChanged(
        normalizeEngine(previous.engine),
        normalizeEngine(current.engine)
      )
    }

    const updated =
      Boolean(previous) &&
      (changes.version || changes.branch || changes.engine)

    const duplicates = checkForDuplicateInstallations()

    completedUpdateResult = {
      updated,
      previous,
      current,
      changes,
      message: updated
        ? getUpdateInstalledMessage({ changes, current })
        : null,
      duplicates
    }
  } catch (error) {
    console.warn('[app-update] Failed to check for a completed update.', error)
    completedUpdateResult = {
      updated: false,
      previous: null,
      current: null,
      duplicates: checkForDuplicateInstallations()
    }
  }

  return completedUpdateResult
}

const persistInstalledUpdateSettings = async (update) => {
  try {
    const settings = await getAppSettings()
    const engine =
      normalizeEngine(update?.engine) ||
      normalizeEngine(settings?.appUpdateEngine) ||
      DEFAULT_UPDATE_ENGINE

    await setAppSettings({
      ...settings,
      appUpdateEngine: engine,
      ...(update?.branch ? { appUpdateRunningBranch: update.branch } : {})
    })
  } catch (error) {
    console.warn(
      '[app-update] Failed to persist installed update settings.',
      error
    )
  }
}

const launchInstallerAndRestart = async (
  mainWindow,
  installerPath,
  sendProgress,
  update
) => {
  const installerHelpers = { sendProgress, getInstallErrorMessage }

  if (process.platform === 'darwin') {
    await launchMacInstaller(
      mainWindow,
      installerPath,
      sendProgress,
      installerHelpers
    )
  } else if (process.platform === 'win32') {
    await launchWindowsInstaller(
      mainWindow,
      installerPath,
      sendProgress,
      installerHelpers
    )
  } else {
    throw new Error(`App updates are not supported on ${process.platform}.`)
  }

  await persistInstalledUpdateSettings(update)

  if (process.platform === 'darwin') {
    scheduleAppRestart()
  }

  // Give the UI a moment to show completion before the app exits.
  await new Promise((resolve) => setTimeout(resolve, 500))
  Utils.quit()
}

const runAppUpdate = async (mainWindow, update, sendProgress) => {
  await persistCurrentAppState(mainWindow)

  const artifact = selectUpdateArtifact(update)
  const tempDirectory = await fs.mkdtemp(
    path.join(os.tmpdir(), 'farmcontrol-update-')
  )
  const artifactName = path.basename(getArtifactName(artifact))
  const installerPath = path.join(tempDirectory, artifactName)

  sendProgress({
    phase: 'preparing',
    percent: 0,
    artifact,
    message: 'Preparing update download'
  })

  await downloadArtifact(artifact, installerPath, sendProgress)

  sendProgress({
    phase: 'downloaded',
    percent: 100,
    downloadedBytes: null,
    totalBytes: null,
    artifact,
    message: 'Update downloaded'
  })

  await launchInstallerAndRestart(
    mainWindow,
    installerPath,
    sendProgress,
    update
  )
}

export function startAppUpdate(mainWindow, update, sendProgress) {
  if (runningUpdate) return runningUpdate

  runningUpdate = runAppUpdate(mainWindow, update, sendProgress)
    .then(() => ({ ok: true }))
    .catch((error) => {
      sendProgress({
        phase: 'error',
        percent: null,
        message: error?.message || 'Failed to update app.'
      })
      throw error
    })
    .finally(() => {
      runningUpdate = null
    })

  return runningUpdate
}
