import { execSync } from 'node:child_process'
import fsp from 'node:fs/promises'
import path from 'node:path'

import AdmZip from 'adm-zip'
import fs from 'fs-extra'
import fetch from 'node-fetch'
import { extract } from 'tar'

import { downloadFile, getFetchOptions, getOpenlistArchMap, getRcloneArchMap, platformIdentifierMap } from './utils.js'

const cwd = process.cwd()
const target = process.argv.find(arg => arg.startsWith('--target='))?.slice(9)
const platformArch = target ? platformIdentifierMap[target] : `${process.platform}-${process.arch}`
const [platform, arch] = platformArch.split('-')
const isWin = platform === 'win32'

console.log(`platform: ${platform}, arch: ${arch}`)

const sidecarHost =
  target ||
  execSync('rustc -vV')
    .toString()
    .match(/(?<=host: ).+(?=\s*)/g)[0]

if (!getOpenlistArchMap[platformArch]) {
  throw new Error(`Unsupported target: ${platformArch}. Supported: ${Object.keys(getOpenlistArchMap).join(', ')}`)
}

// Rclone version management
let rcloneVersion = 'v1.70.3'
const rcloneVersionUrl = 'https://github.com/rclone/rclone/releases/latest/download/version.txt'

async function getLatestRcloneVersion() {
  try {
    const response = await fetch(rcloneVersionUrl, getFetchOptions())
    rcloneVersion = (await response.text()).trim().replace('rclone ', '') || '1.70.1'
    console.log(`Latest rclone version: ${rcloneVersion}`)
  } catch (error) {
    console.log('Error fetching latest rclone version:', error.message)
  }
}

// openlist version management
let openlistVersion = 'v4.0.8'

async function getLatestOpenlistVersion() {
  try {
    const response = await fetch(
      'https://api.github.com/repos/OpenListTeam/OpenList/releases/latest',
      getFetchOptions()
    )
    const data = await response.json()
    openlistVersion = data.tag_name || 'v4.0.8'
    console.log(`Latest OpenList version: ${openlistVersion}`)
  } catch (error) {
    console.log('Error fetching latest OpenList version:', error.message)
  }
}

const createBinaryInfo = (name, archMap, baseUrl, version = '') => {
  const zipName = archMap[platformArch]
  return {
    name,
    targetFile: `${name}-${sidecarHost}${isWin ? '.exe' : ''}`,
    exeFile: `${name}${isWin ? '.exe' : ''}`,
    zipFile: zipName,
    downloadURL: `${baseUrl}${version ? `/${version}` : ''}/${zipName}`
  }
}

// OpenList service URL
const serviceUrl = `https://github.com/OpenListTeam/openlist-desktop-service/releases/download/${sidecarHost}`

const getServiceInfo = exeName => {
  const ext = isWin ? '.exe' : ''
  const suffix = '-' + sidecarHost
  return {
    file: exeName + suffix + ext,
    downloadURL: `${serviceUrl}/${exeName}${ext}`
  }
}

const resolvePlugins = async () => {
  const pluginDir = path.join(process.env.APPDATA, 'Local/NSIS')
  await fs.mkdir(pluginDir, { recursive: true })
  await resolveSimpleServicePlugin(pluginDir)
  await resolveAccessControlPlugin(pluginDir)
}

const resolveSimpleServicePlugin = async pluginDir => {
  const url = 'https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip'
  const TEMP_DIR = path.join(cwd, 'temp')
  const tempDir = path.join(TEMP_DIR, 'SimpleSC')
  const tempZip = path.join(tempDir, 'NSIS_Simple_Service_Plugin_Unicode_1.30.zip')
  const tempDll = path.join(tempDir, 'SimpleSC.dll')
  const pluginPath = path.join(pluginDir, 'SimpleSC.dll')

  await fs.mkdir(tempDir, { recursive: true })
  if (fs.existsSync(pluginPath)) return

  try {
    if (!fs.existsSync(tempZip)) {
      await downloadFile(url, tempZip)
    }
    const zip = new AdmZip(tempZip)
    zip.extractAllTo(tempDir, true)
    await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
    console.log(`SimpleSC.dll copied to ${pluginPath}`)
  } finally {
    await fsp.rm(tempDir, { recursive: true, force: true })
  }
}

const resolveAccessControlPlugin = async pluginDir => {
  const url = 'https://nsis.sourceforge.io/mediawiki/images/4/4a/AccessControl.zip'
  const TEMP_DIR = path.join(cwd, 'temp')
  const tempDir = path.join(TEMP_DIR, 'AccessControl')
  const tempZip = path.join(tempDir, 'AccessControl.zip')
  const tempDll = path.join(tempDir, 'Plugins', 'AccessControl.dll')
  const pluginPath = path.join(pluginDir, 'Plugins', 'x86-unicode', 'AccessControl.dll')
  const pluginPathB = path.join(pluginDir, 'AccessControl.dll')

  await fs.mkdir(tempDir, { recursive: true })
  if (fs.existsSync(pluginPath)) return

  try {
    if (!fs.existsSync(tempZip)) {
      await downloadFile(url, tempZip)
    }
    const zip = new AdmZip(tempZip)
    zip.extractAllTo(tempDir, true)

    let sourcePath = tempDll
    if (!fs.existsSync(sourcePath)) {
      const altPaths = [
        path.join(tempDir, 'AccessControl.dll'),
        path.join(tempDir, 'Plugins', 'i386-unicode', 'AccessControl.dll')
      ]
      for (const altPath of altPaths) {
        if (fs.existsSync(altPath)) {
          sourcePath = altPath
          break
        }
      }
    }
    if (fs.existsSync(sourcePath)) {
      await fsp.cp(sourcePath, pluginPath, { recursive: true, force: true })
      await fsp.cp(sourcePath, pluginPathB, { recursive: true, force: true })
      console.log(`AccessControl.dll copied to ${pluginPath}`)
    } else {
      console.warn('AccessControl.dll not found in the extracted archive')
    }
  } finally {
    await fsp.rm(tempDir, { recursive: true, force: true })
  }
}

async function resolveSidecar(binInfo) {
  const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo
  const binaryDir = path.join(cwd, 'src-tauri', 'binary')
  const binaryPath = path.join(binaryDir, targetFile)

  await fs.mkdir(binaryDir, { recursive: true })
  const zipPath = path.join(binaryDir, zipFile)

  try {
    await downloadFile(downloadURL, zipPath)

    if (zipFile.endsWith('.zip')) {
      const zip = new AdmZip(zipPath)
      zip.extractAllTo(binaryDir, true)

      if (name === 'rclone') {
        const extractedDir = path.join(binaryDir, zipFile.replace('.zip', ''))
        await fs.rename(path.join(extractedDir, exeFile), binaryPath)
        await fs.remove(extractedDir)
      } else {
        await fs.rename(path.join(binaryDir, exeFile), binaryPath)
      }
    } else {
      await extract({ cwd: binaryDir, file: zipPath })
      await fs.rename(path.join(binaryDir, exeFile), binaryPath)
    }
    await fs.remove(zipPath)
    await fs.chmod(binaryPath, 0o755)
  } catch (err) {
    console.error(`Error preparing "${name}":`, err.message)
    await fs.rm(binaryPath, { recursive: true, force: true })
    throw err
  }
}

async function resolveService(resourceInfo, isChmod = true, defaultMode = 0o755) {
  const { file, downloadURL } = resourceInfo
  const resourceDir = path.join(cwd, 'src-tauri', 'binary')
  const resourcePath = path.join(resourceDir, file)

  await fs.mkdir(resourceDir, { recursive: true })

  try {
    await downloadFile(downloadURL, resourcePath)
    if (isChmod) {
      await fs.chmod(resourcePath, defaultMode)
    }
    console.log(`"${file}" downloaded to ${resourcePath}`)
  } catch (err) {
    console.error(`Error preparing "${file}":`, err.message)
    await fs.rm(resourcePath, { recursive: true, force: true })
    throw err
  }
}

async function retryTask(name, fn, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      console.log(`task::${name} try ${i}`)
      await fn()
      return
    } catch (err) {
      console.log(`task::${name} try ${i} ==`, err.message)
      if (i === maxRetries - 1) throw err
    }
  }
}

async function main() {
  await getLatestOpenlistVersion()
  await retryTask('openlist', () =>
    resolveSidecar(
      createBinaryInfo(
        'openlist',
        getOpenlistArchMap,
        'https://github.com/OpenListTeam/OpenList/releases/download',
        openlistVersion
      )
    )
  )

  await retryTask('rclone', async () => {
    await getLatestRcloneVersion()
    await resolveSidecar(
      createBinaryInfo('rclone', getRcloneArchMap(rcloneVersion), `https://downloads.rclone.org`, rcloneVersion)
    )
  })
  if (isWin) {
    await resolvePlugins()
  }
  await resolveService(getServiceInfo('install-openlist-service'))
  await resolveService(getServiceInfo('openlist-desktop-service'))
  await resolveService(getServiceInfo('uninstall-openlist-service'))
}

main().catch(console.log)
