import { existsSync } from 'node:fs'
import path from 'node:path'
import spawn from 'cross-spawn'
import semver from 'semver'
import { detectPackageManager } from './binary-upgrade'
import { notifyUpgradeToast } from './upgrade-toast'
import { resolveCliVersion } from './version'

declare const __CLI_VERSION__: string | undefined

const DEFAULT_PACKAGE_NAME = 'dimcode'

function resolveWrapperPackageVersion(): string | null {
  const v = (process.env.DIMCODE_NPM_PACKAGE_VERSION || '').trim()
  return v || null
}

function resolveUpgradePackageManager(): 'npm' | 'bun' {
  const v = (process.env.DIMCODE_NPM_PACKAGE_MANAGER || '').trim().toLowerCase()
  if (v === 'npm' || v === 'bun')
    return v
  return detectPackageManager()
}

const DEBUG_LOG_PATH = process.env.DIMCODE_DEBUG_LOG || './dimcode-debug.log'
const CHECK_TIMEOUT_MS = 5000
const autoInstallStartedVersions = new Set<string>()

async function fetchLatestVersion(
  packageName: string = DEFAULT_PACKAGE_NAME,
): Promise<string | null> {
  try {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS)

    const response = await fetch(`https://registry.npmjs.org/${packageName}`, {
      signal: controller.signal,
      headers: {
        Accept: 'application/json',
      },
    })

    clearTimeout(timeoutId)

    if (!response.ok) {
      throw new Error(`npm registry returned ${response.status}`)
    }

    const data = await response.json()
    const latestVersion = data['dist-tags']?.latest

    if (!latestVersion || typeof latestVersion !== 'string') {
      throw new Error('No latest version found in registry response')
    }

    return latestVersion
  }
  catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      throw new Error('npm registry request timed out')
    }
    throw error
  }
}

export async function checkNpmUpgrade(
  packageName: string = DEFAULT_PACKAGE_NAME,
): Promise<string | null> {
  await logUpgradeCheck('start', {
    disableAutoUpdate: process.env.DIMCODE_DISABLE_AUTOUPDATE === '1',
    autoUpdate: process.env.DIMCODE_AUTOUPDATE !== '0',
    cwd: safeCwd(),
    argv: safeArgv(),
    debugLogPath: DEBUG_LOG_PATH,
  })

  if (process.env.DIMCODE_DISABLE_AUTOUPDATE === '1') {
    await logUpgradeCheck('skip-disabled')
    return null
  }

  const resolvedVersion = resolveCliVersion()
  if (!resolvedVersion?.version) {
    await logUpgradeCheck('skip-missing-version')
    return null
  }

  const wrapperVersion
    = packageName !== DEFAULT_PACKAGE_NAME ? resolveWrapperPackageVersion() : null
  const current = wrapperVersion || resolvedVersion.version
  const hasInjectedVersion = resolvedVersion.source === 'injected'

  try {
    const latest = await fetchLatestVersion(packageName)

    await logUpgradeCheck('check-result', {
      current,
      latest,
      hasInjectedVersion,
      wrapperVersion,
    })

    if (!latest) {
      await logUpgradeCheck('error', {
        message: 'Failed to fetch latest version',
      })
      return null
    }

    if (semver.valid(current) && semver.valid(latest)) {
      if (semver.gte(current, latest)) {
        await logUpgradeCheck('no-update', { current, latest })
        return null
      }

      if (process.env.DIMCODE_AUTOUPDATE !== '0' && hasInjectedVersion) {
        if (autoInstallStartedVersions.has(latest)) {
          await logUpgradeCheck('autoupdate-skip-duplicate', {
            version: latest,
          })
          return null
        }
        autoInstallStartedVersions.add(latest)
        await logUpgradeCheck('update-available', {
          current,
          latest,
          autoUpdate: true,
        })
        startAutoInstall(latest, packageName)
        return `Update available: ${packageName}@${latest} (current: ${current}). Installing in background. Restart terminal after it finishes.`
      }

      await logUpgradeCheck('update-available', {
        current,
        latest,
        autoUpdate: false,
      })
      return `Update available: ${packageName}@${latest} (current: ${current})`
    }

    if (current === latest) {
      await logUpgradeCheck('no-update', { current, latest })
      return null
    }

    await logUpgradeCheck('update-available', {
      current,
      latest,
      autoUpdate: false,
    })
    return `Update available: ${packageName}@${latest} (current: ${current})`
  }
  catch (error) {
    await logUpgradeCheck('error', {
      message: error instanceof Error ? error.message : String(error),
    })
    return null
  }
}

function startAutoInstall(
  version: string,
  packageName: string = DEFAULT_PACKAGE_NAME,
): void {
  try {
    void logUpgradeCheck('autoupdate-start', { version })
    const pm = resolveUpgradePackageManager()
    const { command, argsPrefix } = resolvePackageManagerCommand(pm)
    const proc = spawn(
      command,
      [...argsPrefix, 'install', '-g', `${packageName}@${version}`],
      {
        stdio: 'ignore',
        detached: true,
        windowsHide: process.platform === 'win32',
      },
    )
    proc.unref()
    let finished = false
    const finalize = (message: string) => {
      if (finished)
        return
      finished = true
      void logUpgradeCheck('autoupdate-finish', { version, message })
      notifyUpgradeToast(message)
    }
    proc.on('close', (code) => {
      if (code === 0) {
        finalize(
          `Update installed: ${packageName}@${version}. Restart terminal to use the new version.`,
        )
      }
      else {
        finalize(`Auto-update failed. Run: ${pm} install -g ${packageName}`)
      }
    })
    proc.on('error', () => {
      void logUpgradeCheck('autoupdate-error', {
        version,
        reason: 'process-error',
      })
      finalize(`Auto-update failed. Run: ${pm} install -g ${packageName}`)
    })
  }
  catch {
    const pm = resolveUpgradePackageManager()
    void logUpgradeCheck('autoupdate-error', { version, reason: 'spawn-throw' })
    notifyUpgradeToast(
      `Auto-update failed. Run: ${pm} install -g ${packageName}`,
    )
  }
}

async function logUpgradeCheck(
  _event: string,
  _data?: Record<string, unknown>,
): Promise<void> {
  // try {
  //   const [{ mkdir, appendFile }, path] = await Promise.all([
  //     import('node:fs/promises'),
  //     import('node:path'),
  //   ])
  //   const dir = await getDimcodeUserDir(process.env as Record<string, string | undefined>)
  //   await mkdir(dir, { recursive: true })
  //   const filePath = path.join(dir, UPGRADE_CHECK_LOG_FILE)
  //   const payload = {
  //     ts: new Date().toISOString(),
  //     event: _event,
  //     ..._data,
  //   }
  //   await appendFile(filePath, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', mode: 0o600 })
  //   await appendDebugLog(payload)
  // }
  // catch {
  //   // ignore
  // }
}

function safeCwd(): string | undefined {
  try {
    return process.cwd()
  }
  catch {
    return undefined
  }
}

function safeArgv(): string[] | undefined {
  try {
    return process.argv
  }
  catch {
    return undefined
  }
}

// resolveCliVersion moved to ./version.ts

export async function runNpmUpgradeCommand(
  argv: readonly string[],
  packageName: string = DEFAULT_PACKAGE_NAME,
): Promise<void> {
  const args = Array.from(argv)
  const quiet = args.includes('--quiet') || args.includes('--background')
  const checkOnly = args.includes('--check')
  const versionArg = firstNonFlag(args)
  const desired = versionArg && versionArg !== 'latest' ? versionArg : 'latest'

  await logUpgradeCheck('upgrade-command-start', {
    checkOnly,
    quiet,
    desired,
    cwd: safeCwd(),
    argv: safeArgv(),
  })

  if (process.env.DIMCODE_DISABLE_AUTOUPDATE === '1') {
    if (!quiet)
      console.error('Auto-update is disabled (DIMCODE_DISABLE_AUTOUPDATE=1)')
    await logUpgradeCheck('upgrade-command-skip-disabled')
    process.exitCode = 1
    return
  }

  const resolvedVersion = resolveCliVersion()
  if (!resolvedVersion?.version) {
    if (!quiet)
      console.error('Unable to determine current version')
    await logUpgradeCheck('upgrade-command-error', {
      message: 'Missing version',
    })
    process.exitCode = 1
    return
  }

  const wrapperVersion
    = packageName !== DEFAULT_PACKAGE_NAME ? resolveWrapperPackageVersion() : null
  const current = wrapperVersion || resolvedVersion.version

  try {
    const latest
      = desired === 'latest' ? await fetchLatestVersion(packageName) : desired

    if (!latest) {
      throw new Error('Failed to fetch version information')
    }

    await logUpgradeCheck('upgrade-command-check-result', {
      current,
      latest,
      desired,
      wrapperVersion,
    })

    if (semver.valid(current) && semver.valid(latest)) {
      if (checkOnly) {
        if (semver.gte(current, latest)) {
          if (!quiet)
            console.log(`Already up to date: ${current}`)
          await logUpgradeCheck('upgrade-command-no-update', {
            current,
            latest,
          })
          return
        }
        if (!quiet) {
          console.log(
            `Update available: ${packageName}@${latest} (current: ${current})`,
          )
        }
        await logUpgradeCheck('upgrade-command-update-available', {
          current,
          latest,
        })
        return
      }

      if (semver.gte(current, latest)) {
        if (!quiet)
          console.log(`Already up to date: ${current}`)
        await logUpgradeCheck('upgrade-command-no-update', { current, latest })
        return
      }

      await logUpgradeCheck('upgrade-command-install-start', {
        version: latest,
      })
      await installNpmPackage(latest, quiet, packageName)
      const message = `Update installed: ${packageName}@${latest}. Restart terminal to use the new version.`
      if (!quiet)
        console.log(message)
      notifyUpgradeToast(message)
      await logUpgradeCheck('upgrade-command-install-success', {
        version: latest,
      })
      return
    }

    if (current === latest) {
      if (!quiet)
        console.log(`Already up to date: ${current}`)
      await logUpgradeCheck('upgrade-command-no-update', { current, latest })
      return
    }

    if (checkOnly) {
      if (!quiet) {
        console.log(
          `Update available: ${packageName}@${latest} (current: ${current})`,
        )
      }
      await logUpgradeCheck('upgrade-command-update-available', {
        current,
        latest,
      })
      return
    }

    await logUpgradeCheck('upgrade-command-install-start', { version: latest })
    await installNpmPackage(latest, quiet, packageName)
    const message = `Update installed: ${packageName}@${latest}. Restart terminal to use the new version.`
    if (!quiet)
      console.log(message)
    notifyUpgradeToast(message)
    await logUpgradeCheck('upgrade-command-install-success', {
      version: latest,
    })
  }
  catch (error) {
    const msg = error instanceof Error ? error.message : String(error)
    const pm = resolveUpgradePackageManager()
    const message = `${pm} upgrade failed: ${msg}`
    if (!quiet)
      console.error(message)
    notifyUpgradeToast(message)
    await logUpgradeCheck('upgrade-command-error', {
      message: msg,
      error: error instanceof Error ? error.stack : String(error),
    })
    process.exitCode = 1
  }
}

function firstNonFlag(argv: readonly string[]): string | null {
  for (const a of argv) {
    if (!a.startsWith('-'))
      return a
  }
  return null
}

async function installNpmPackage(
  version: string,
  quiet: boolean,
  packageName: string = DEFAULT_PACKAGE_NAME,
): Promise<void> {
  return new Promise((resolve, reject) => {
    const pm = resolveUpgradePackageManager()
    const { command, argsPrefix } = resolvePackageManagerCommand(pm)
    const proc = spawn(
      command,
      [...argsPrefix, 'install', '-g', `${packageName}@${version}`],
      {
        stdio: quiet ? 'ignore' : 'inherit',
        windowsHide: process.platform === 'win32',
      },
    )

    proc.on('close', (code) => {
      if (code === 0) {
        resolve()
      }
      else {
        reject(new Error(`${pm} install exited with code ${code}`))
      }
    })

    proc.on('error', (error) => {
      reject(error)
    })
  })
}

type PackageManager = ReturnType<typeof detectPackageManager>
type ResolvedPackageManagerCommand = Readonly<{
  command: string
  argsPrefix: string[]
}>

function resolvePackageManagerCommand(
  pm: PackageManager,
): ResolvedPackageManagerCommand {
  if (process.platform !== 'win32')
    return { command: pm, argsPrefix: [] }

  if (pm === 'bun')
    return resolveBunCommand()

  return resolveNpmCommand()
}

function resolveBunCommand(): ResolvedPackageManagerCommand {
  const execPath = String(process.execPath ?? '').trim()
  if (execPath) {
    const base = path.basename(execPath).toLowerCase()
    if (base.startsWith('bun') && existsSync(execPath))
      return { command: execPath, argsPrefix: [] }
  }

  const bunHome = String(process.env.BUN_INSTALL ?? '').trim()
  if (bunHome) {
    const bunExe = path.join(bunHome, 'bin', 'bun.exe')
    if (existsSync(bunExe))
      return { command: bunExe, argsPrefix: [] }
  }

  return { command: 'bun', argsPrefix: [] }
}

function resolveNpmCommand(): ResolvedPackageManagerCommand {
  const execPath = String(process.execPath ?? '').trim()
  const execDir = execPath ? path.dirname(execPath) : ''

  if (execDir) {
    const npmCmd = path.join(execDir, 'npm.cmd')
    if (existsSync(npmCmd))
      return { command: npmCmd, argsPrefix: [] }

    const npmCliCandidates = [
      path.join(execDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
      path.resolve(
        execDir,
        '..',
        'lib',
        'node_modules',
        'npm',
        'bin',
        'npm-cli.js',
      ),
    ]
    if (execPath) {
      for (const npmCli of npmCliCandidates) {
        if (existsSync(npmCli))
          return { command: execPath, argsPrefix: [npmCli] }
      }
    }
  }

  const appData = String(process.env.APPDATA ?? '').trim()
  if (appData) {
    const npmCmd = path.join(appData, 'npm', 'npm.cmd')
    if (existsSync(npmCmd))
      return { command: npmCmd, argsPrefix: [] }
  }

  const programFiles = String(process.env.ProgramFiles ?? '').trim()
  if (programFiles) {
    const npmCmd = path.join(programFiles, 'nodejs', 'npm.cmd')
    if (existsSync(npmCmd))
      return { command: npmCmd, argsPrefix: [] }
  }

  const programFilesX86 = String(process.env['ProgramFiles(x86)'] ?? '').trim()
  if (programFilesX86) {
    const npmCmd = path.join(programFilesX86, 'nodejs', 'npm.cmd')
    if (existsSync(npmCmd))
      return { command: npmCmd, argsPrefix: [] }
  }

  return { command: 'npm', argsPrefix: [] }
}
