import { spawn } from 'node:child_process'
import { createHash } from 'node:crypto'
import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

export const developmentServiceDirectoryEnvironment =
  'CLASH_VERGE_DEV_SERVICE_DIR'
const developmentServiceInstallerEnvironment =
  'CLASH_VERGE_DEV_SERVICE_INSTALLER'

const scriptDirectory = dirname(fileURLToPath(import.meta.url))
const repositoryRoot = resolve(scriptDirectory, '..')
const serviceRepository = resolve(
  repositoryRoot,
  '..',
  'clash-verge-service-ipc',
)
const serviceManifest = join(serviceRepository, 'Cargo.toml')
const developmentReceipt = join(
  repositoryRoot,
  'target',
  'development-service.json',
)

function executable(name, platform) {
  return join(
    serviceRepository,
    'target',
    'debug',
    platform === 'win32' ? `${name}.exe` : name,
  )
}

function run(command, args, options = {}) {
  return new Promise((resolvePromise, reject) => {
    const child = spawn(command, args, {
      env: options.env ?? process.env,
      stdio: options.stdio ?? 'inherit',
      windowsHide: true,
    })
    child.once('error', reject)
    child.once('exit', (code, signal) => {
      if (code === 0) {
        resolvePromise()
        return
      }
      const error = new Error(
        `${command} failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`,
      )
      error.exitCode = code
      reject(error)
    })
  })
}

async function hashFiles(paths) {
  const hash = createHash('sha256')
  for (const path of paths) {
    hash.update(await readFile(path))
  }
  return hash.digest('hex')
}

async function readInstalledFingerprint() {
  try {
    const parsed = JSON.parse(await readFile(developmentReceipt, 'utf8'))
    return typeof parsed.fingerprint === 'string' ? parsed.fingerprint : null
  } catch {
    return null
  }
}

async function writeInstalledFingerprint(fingerprint) {
  await mkdir(dirname(developmentReceipt), { recursive: true })
  await writeFile(
    developmentReceipt,
    `${JSON.stringify({ fingerprint }, null, 2)}\n`,
    'utf8',
  )
}

export function windowsElevationInvocation(
  installer,
  environment = process.env,
) {
  const script = [
    "$ErrorActionPreference = 'Stop'",
    `$installer = [Environment]::GetEnvironmentVariable('${developmentServiceInstallerEnvironment}', 'Process')`,
    "if ([string]::IsNullOrWhiteSpace($installer)) { throw 'Development service installer path is missing' }",
    '$child = Start-Process -FilePath $installer -Verb RunAs -Wait -PassThru -WindowStyle Hidden',
    'exit $child.ExitCode',
  ].join('; ')
  return {
    command: 'powershell.exe',
    args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
    env: {
      ...environment,
      [developmentServiceInstallerEnvironment]: installer,
    },
  }
}

async function elevateInstaller(installer, platform) {
  if (platform === 'win32') {
    const invocation = windowsElevationInvocation(installer)
    return run(invocation.command, invocation.args, { env: invocation.env })
  }

  if (platform === 'darwin') {
    const script =
      'on run argv\nset toolPath to item 1 of argv\nset groupId to item 2 of argv\ndo shell script "CLASH_VERGE_SERVICE_GID=" & quoted form of groupId & " " & quoted form of toolPath with administrator privileges\nend run'
    return run('osascript', ['-e', script, installer, String(process.getgid())])
  }

  if (typeof process.getuid === 'function' && process.getuid() === 0) {
    return run(installer, [], {
      env: {
        ...process.env,
        CLASH_VERGE_SERVICE_GID: String(process.getgid()),
      },
    })
  }
  try {
    await run('pkexec', [installer])
  } catch (error) {
    if (error.code !== 'ENOENT' && error.exitCode !== 127) throw error
    await run('sudo', [installer])
  }
}

export async function prepareDevelopmentService({
  platform = process.platform,
} = {}) {
  await access(serviceManifest)
  await run('cargo', [
    'build',
    '--manifest-path',
    serviceManifest,
    '--features',
    'standalone,client,development-channel',
    '--bins',
  ])

  const service = executable('clash-verge-service', platform)
  await access(service)
  return dirname(service)
}

export async function ensureDevelopmentService({
  platform = process.platform,
} = {}) {
  const serviceDirectory = await prepareDevelopmentService({ platform })
  const service = join(
    serviceDirectory,
    platform === 'win32' ? 'clash-verge-service.exe' : 'clash-verge-service',
  )
  const installer = join(
    serviceDirectory,
    platform === 'win32'
      ? 'clash-verge-service-install.exe'
      : 'clash-verge-service-install',
  )
  const driver = join(
    serviceDirectory,
    platform === 'win32'
      ? 'service-integration-driver.exe'
      : 'service-integration-driver',
  )
  const fingerprint = await hashFiles([service, installer])
  const installedFingerprint = await readInstalledFingerprint()

  if (installedFingerprint === fingerprint) {
    try {
      await run(driver, ['probe'], { stdio: 'ignore' })
      return serviceDirectory
    } catch {
      // The expected binary is installed but unavailable; the repair tool owns recovery.
    }
  }

  try {
    await elevateInstaller(installer, platform)
  } catch (error) {
    if (error.exitCode !== 75) throw error
  }
  await run(driver, ['ready'])
  await writeInstalledFingerprint(fingerprint)
  return serviceDirectory
}
