import { defaultExecOptions, execRootSync } from "admina"
import { info, warning } from "ci-log"
import type { ExecaError } from "execa"
import { getAptEnv } from "./apt-env.js"
import { type AddAptKeyOptions, addAptKey } from "./apt-key.js"
import { addAptRepository } from "./apt-repository.js"
import { aptTimeout } from "./apt-timeout.js"
import { getApt } from "./get-apt.js"
import { initAptMemoized } from "./init-apt.js"
import { filterAndQualifyAptPackages } from "./qualify-install.js"
import { updateAptReposMemoized } from "./update.js"

/**
 * The information about an installation result
 */
export type InstallationInfo = {
  /** The install dir of the package (Defaults to `undefined`) */
  installDir?: string
  /** The bin dir of the package (Defaults to `/usr/bin`) */
  binDir: string
  /** The bin path of the package (Defaults to `undefined`) */
  bin?: string
}

/**
 * The information about an apt package
 */
export type AptPackage = {
  /** The name of the package */
  name: string
  /** The version of the package (optional) */
  version?: string
  /** The repository to add before installing the package (optional) */
  repository?: string
  /** The key to add before installing the package (optional) */
  key?: AddAptKeyOptions
  /**
   * If the given version is not available, fall back to the latest version
   * @default false
   */
  fallBackToLatest?: boolean
}

const retryErrors = [
  "E: Could not get lock",
  "dpkg: error processing archive",
  "dpkg: error: dpkg status database is locked by another process",
]

/**
 * Install a package using apt
 *
 * @param packages The packages to install (name, and optional info like version and repositories)
 * @param update Whether to update the package list before installing (Defaults to `false`)
 *
 * @returns The installation information
 *
 * @example
 * ```ts
 * await installAptPack([{ name: "ca-certificates" }, { name: "gnupg" }])
 * ```
 *
 * @example
 * ```ts
  await installAptPack([
    {
      name: "gcc",
      version,
      repository: "ppa:ubuntu-toolchain-r/test",
      key: { key: "1E9377A2BA9EF27F", fileName: "ubuntu-toolchain-r-test.gpg" },
    },
  ])
 * ```
 */
export async function installAptPack(packages: AptPackage[], update = false): Promise<InstallationInfo> {
  try {
    const apt: string = getApt()

    for (const { name, version } of packages) {
      info(`Installing ${name} ${version ?? ""} via ${apt}`)
    }

    if (update) {
      // Force update the repos
      updateAptReposMemoized.clear()
    }
    // Update the repos if needed
    updateAptReposMemoized(apt)

    // Add the repos if needed
    await addRepositories(apt, packages)

    const needToInstall = await filterAndQualifyAptPackages(packages, apt)

    if (needToInstall.length === 0) {
      info("All packages are already installed")
      return { binDir: "/usr/bin/" }
    }

    // Initialize apt if needed
    await initAptMemoized(apt)

    try {
      // Add the keys if needed
      await addAptKeys(packages)

      // Install
      execRootSync(apt, ["install", "--fix-broken", "-y", ...needToInstall], {
        ...defaultExecOptions,
        env: getAptEnv(apt),
      })
    } catch (err) {
      if (isExecaError(err)) {
        if (retryErrors.some((error) => typeof err.stderr === "string" && err.stderr.includes(error))) {
          warning(`Failed to install packages ${needToInstall}. Retrying...`)
          execRootSync(
            apt,
            ["install", "--fix-broken", "-y", "-o", aptTimeout, ...needToInstall],
            { ...defaultExecOptions, env: getAptEnv(apt) },
          )
        }
      } else {
        throw err
      }
    }

    return { binDir: "/usr/bin/" }
  } catch (err) {
    const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err)
    throw new Error(`Failed to install apt packages: ${msg}`)
  }
}

async function addRepositories(apt: string, packages: AptPackage[]) {
  const allRepositories = [...new Set(packages.flatMap((pack) => pack.repository ?? []))]
  await Promise.all(allRepositories.map((repo) => addAptRepository(repo, apt)))
}

async function addAptKeys(packages: AptPackage[]) {
  await Promise.all(packages.map(async (pack) => {
    if (pack.key !== undefined) {
      await addAptKey(pack.key)
    }
  }))
}

function isExecaError(err: unknown): err is ExecaError {
  return typeof (err as ExecaError).stderr === "string"
}
