#!/usr/bin/env zx

const execSync = require('child_process').execSync

// Log symbols
const figuresDefault = {
  bullet: '●',
  circle: '◯',
  cross: '✖',
  lozenge: '◆',
  play: '▶',
  pointer: '❯',
  square: '◼',
  star: '★',
  tick: '✔'
}

const figuresFallback = {
  bullet: '■',
  circle: '□',
  cross: '×',
  lozenge: '♦',
  play: '►',
  pointer: '>',
  square: '■',
  star: '✶',
  tick: '√'
}

function isUnicodeSupported() {
  if (process.platform !== 'win32') {
    // Linux console (kernel)
    return process.env.TERM !== 'linux'
  }

  return (
    Boolean(process.env.CI) ||
    // Windows Terminal
    Boolean(process.env.WT_SESSION) ||
    // ConEmu and cmder
    process.env.ConEmuTask === '{cmd::Cmder}' ||
    process.env.TERM_PROGRAM === 'vscode' ||
    process.env.TERM === 'xterm-256color' ||
    process.env.TERM === 'alacritty'
  )
}

const figures = isUnicodeSupported() ? figuresDefault : figuresFallback

function log(type, label, msg) {
  let icon, message
  let fittedLabel = label
  while(fittedLabel.length < 14) {
    fittedLabel = fittedLabel.length % 2 === 1 ? fittedLabel + ' ' : ' ' + fittedLabel
  }
  if (type === 'info') {
    icon = `${chalk.cyanBright(figures.pointer)} ${chalk.bold.white.bgCyan(' ' + fittedLabel + ' ')}`
    message = wrapMessage(msg)
  } else if (type === 'star') {
    icon = `${chalk.yellow(figures.star)} ${chalk.bold.black.bgYellow(' ' + fittedLabel + ' ')}`
    message = wrapMessage(msg)
  } else if (type === 'success') {
    icon = `${chalk.greenBright(figures.play)} ${chalk.bold.white.bgGreenBright(' ' + fittedLabel + ' ')}`
    message = wrapMessage(msg)
  } else if (type === 'warn') {
    icon = `${chalk.yellowBright(figures.lozenge)} ${chalk.bold.black.bgYellowBright(' WARNING ')}`
    message = chalk.yellowBright(wrapMessage(msg))
  } else if (type === 'error') {
    icon = `${chalk.redBright(figures.cross)} ${chalk.black.bold.bgRedBright(' ERROR ')}`
    message = chalk.redBright(wrapMessage(msg))
  }
  const now = new Date()
  const outputMessage = `${icon} ${message} (${now})`
  console.log(outputMessage)
}

function locations(substring,string){
  var a=[],i=-1;
  while((i=string.indexOf(substring,i+1)) >= 0) a.push(i);
  return a;
}

function wrapMessage(msg) {
  const indexes = locations('`', msg)
  if (indexes.length > 3) {
    return msg.substring(0, indexes[0]) + chalk.bold.black.bgGray(' ' + msg.substring(indexes[0] + 1, indexes[1] + 1 - indexes[0]) + ' ') + msg.substring(indexes[1] + 1 - indexes[0]) + ' '
  } else {
    return msg
  }
}

function runCommand(spinnerTitle, command) {
  // execSync(command.includes('sudo') ? `sudo "$(which gum)" spin --spinner dot --title "${spinnerTitle}" -- ${command}` : `gum spin --spinner dot --title "${spinnerTitle}" -- ${command}`, {
  log('info', 'CMD', spinnerTitle)
  console.log(command)
  execSync(command, {
    stdio: 'inherit',
    shell: true,
    // Timeout of 140m
    timeout: 1000 * 60 * 140
  })
}

async function runSilentCommand(command) {
  execSync(`${command}`, { stdio: 'inherit', shell: true })
}

function fileExists(pathToFile) {
  return fs.existsSync(pathToFile)
}

function dirExists(pathToDir) {
  return fs.existsSync(pathToDir)
}

let installData
let installOrders = {}
let installMeta = {}
let binLinkRan = false
let chezmoiData = []
let installOrdersPre = []
let installOrdersPost = []
let installOrdersService = []
let installOrdersGroups = []
let installOrdersPorts = []
let installOrdersPlugins = []
let installOrdersBinLink = []
let brewUpdated, osType, osID, snapRefreshed

// Register the OS platform type
const osPlatformData = os.platform()
const osPlatform = osPlatformData === 'win32' ? 'windows' : osPlatformData

// Download the installation map
async function downloadInstallData() {
  const response = await fetch('https://github.com/megabyte-labs/install.doctor/raw/master/software.yml')
  if (response.ok) {
    log('info', 'Catalog Download', `Received ok response from download`)
    const text = await response.text()
    log('info', 'Catalog Download', `Parsing software.yml`)
    return YAML.parse(text, { maxAliasCount: -1 })
  } else {
    log('error', 'Catalog Download', `Failed to download the installation map`)
    log('info', 'Catalog Download', `Falling back to local version of software.yml`)
    const text = fs.readFileSync(process.env.HOME + '/.local/share/chezmoi/software.yml').toString()
    log('info', 'Catalog Download', `Parsing local software.yml file`)
    return YAML.parse(text, { maxAliasCount: -1 })
  }
}

// Download the installation map
async function getChezmoiData() {
  const text = fs.readFileSync(process.env.HOME + '/.local/share/chezmoi/home/.chezmoidata.yaml').toString()
  return YAML.parse(text, { maxAliasCount: -1 })
}

// Creates the installOrders object which maps package managers to arrays of packages to install
let generateInstallOrderCount = 0
async function generateInstallOrders(pkgsToInstall) {
  const installerPreference = await OSTypeInstallerKey()
  const preferenceOrder = installData.installerPreference[installerPreference]
  const logStage = 'Install Orders'
  const packagesToInstall = pkgsToInstall
  const softwarePackages = installData.softwarePackages
  if (generateInstallOrderCount === 0) {
    log('info', logStage, `Installer preference category detected as ${installerPreference}`)
    log('info', logStage, `Preference order acquired:`)
    console.log('Preference order:', preferenceOrder)
  }
  generateInstallOrderCount++
  log('info', logStage, `New packages discovered for processing: ${pkgsToInstall} (${pkgsToInstall.length} items)`)
  pkgFor: for (let pkg of packagesToInstall) {
    let packageKey
    if (softwarePackages[pkg + ':' + osID]) {
      packageKey = pkg + ':' + osID
    } else if (softwarePackages[pkg + ':' + osType]) {
      packageKey = pkg + ':' + osType
    } else if (softwarePackages[pkg]) {
      packageKey = pkg
    } else {
      log('warn', logStage, `The package \`${pkg}\` was not found in the installation map`)
      process.env.DEBUG === 'true' && console.log('softwarePackages:', softwarePackages)
      console.log('pkg:', pkg)
      continue
    }
    let comparesRemaining = preferenceOrder.length
    for (let preference of preferenceOrder) {
      comparesRemaining--
      let currentSelector, doubleScoped, scopedPkgManager, scopedSystem, normalCheck
      if (
        softwarePackages[packageKey][preference + ':' + osID] ||
        softwarePackages[packageKey][preference + ':' + osType] ||
        softwarePackages[packageKey][preference]
      ) {
        // Handle the _when attribute
        currentSelector = 'when'
        doubleScoped =
          softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]
        scopedPkgManager = softwarePackages[packageKey]['_' + currentSelector + ':' + preference]
        scopedSystem =
          softwarePackages[packageKey]['_' + currentSelector + ':' + osID] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osType]
        normalCheck = softwarePackages[packageKey]['_' + currentSelector]
        if (doubleScoped) {
          try {
            await runSilentCommand(doubleScoped)
          } catch (e) {
            let pref
            if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) {
              pref = preference + ':' + osID
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) {
              pref = preference + ':' + osType
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) {
              pref = osID + ':' + preference
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) {
              pref = osType + ':' + preference
            }
            process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`)
            processPluginOrders(pkg)
            continue pkgFor
          }
        } else if (scopedPkgManager) {
          try {
            await runSilentCommand(scopedPkgManager)
          } catch (e) {
            const pref = preference
            process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`)
            processPluginOrders(pkg)
            continue pkgFor
          }
        } else if (scopedSystem) {
          try {
            await runSilentCommand(scopedSystem)
          } catch (e) {
            let pref
            if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) {
              pref = osID
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) {
              pref = osType
            }
            process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when:${pref} condition`)
            processPluginOrders(pkg)
            continue pkgFor
          }
        } else if (normalCheck) {
          try {
            await runSilentCommand(normalCheck)
          } catch (e) {
            process.env.DEBUG === 'true' && log('info', 'Filter', `${pkg} is being skipped because of the _when condition`)
            processPluginOrders(pkg)
            continue pkgFor
          }
        }

        // Handle the _bin attribute
        currentSelector = 'bin'
        doubleScoped =
          softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]
        scopedPkgManager = softwarePackages[packageKey]['_' + currentSelector + ':' + preference]
        scopedSystem =
          softwarePackages[packageKey]['_' + currentSelector + ':' + osID] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osType]
        normalCheck = softwarePackages[packageKey]['_' + currentSelector]
        if (doubleScoped) {
          const bin =
            typeof doubleScoped === 'string'
              ? which.sync(doubleScoped, { nothrow: true })
              : doubleScoped.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y)
          if (bin) {
            let pref
            if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) {
              pref = preference + ':' + osID
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) {
              pref = preference + ':' + osType
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) {
              pref = osID + ':' + preference
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) {
              pref = osType + ':' + preference
            }
            process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin:${pref})`)
            processPluginOrders(pkg)
            continue pkgFor
          } else {
            if (preference === 'cask' || preference === 'flatpak') {
              installOrdersBinLink.push({ package: packageKey, bin: doubleScoped, preference })
            }
          }
        } else if (scopedPkgManager) {
          const bin =
            typeof scopedPkgManager === 'string'
              ? which.sync(scopedPkgManager, { nothrow: true })
              : scopedPkgManager.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y)
          if (bin) {
            const pref = preference
            process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin:${pref})`)
            processPluginOrders(pkg)
            continue pkgFor
          } else {
            if (preference === 'cask' || preference === 'flatpak') {
              installOrdersBinLink.push({ package: packageKey, bin: scopedPkgManager, preference })
            }
          }
        } else if (scopedSystem) {
          const bin =
            typeof scopedSystem === 'string'
              ? which.sync(scopedSystem, { nothrow: true })
              : scopedSystem.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y)
          if (bin) {
            let pref
            if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) {
              pref = osID
            } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) {
              pref = osType
            }
            process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin:${pref})`)
            processPluginOrders(pkg)
            continue pkgFor
          } else {
            if (preference === 'cask' || preference === 'flatpak') {
              installOrdersBinLink.push({ package: packageKey, bin: scopedSystem, preference })
            }
          }
        } else if (normalCheck) {
          const bin =
            typeof normalCheck === 'string'
              ? which.sync(normalCheck, { nothrow: true })
              : normalCheck.map((x) => which.sync(x, { nothrow: true })).every((y) => !!y)
          if (bin) {
            process.env.DEBUG === 'true' && log('info', 'Filter', `${bin} already in PATH (via _bin)`)
            processPluginOrders(pkg)
            continue pkgFor
          } else {
            if (preference === 'cask' || preference === 'flatpak') {
              installOrdersBinLink.push({ package: packageKey, bin: normalCheck, preference })
            }
          }
        }

        // Handle the _app definition
        const appName = softwarePackages[packageKey]['_app']
        if (appName) {
          if(fileExists(`/Applications/${appName}`) || fileExists(`${process.env.HOME}/Applications/${appName}`)) {
            processPluginOrders(pkg)
            continue pkgFor
          }
        }

        // Handle the _deps attribute
        currentSelector = 'deps'
        doubleScoped =
          softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]
        scopedPkgManager = softwarePackages[packageKey]['_' + currentSelector + ':' + preference]
        scopedSystem =
          softwarePackages[packageKey]['_' + currentSelector + ':' + osID] ||
          softwarePackages[packageKey]['_' + currentSelector + ':' + osType]
        normalCheck = softwarePackages[packageKey]['_' + currentSelector]
        const dependenciesTag = 'Dependencies'
        if (doubleScoped) {
          let pref
          if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osID]) {
            pref = '_deps:' + preference + ':' + osID
            log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
            await generateInstallOrders(softwarePackages[packageKey][pref])
          } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + preference + ':' + osType]) {
            pref = '_deps:' + preference + ':' + osType
            log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
            await generateInstallOrders(softwarePackages[packageKey][pref])
          } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID + ':' + preference]) {
            pref = '_deps:' + osID + ':' + preference
            log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
            await generateInstallOrders(softwarePackages[packageKey][pref])
          } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType + ':' + preference]) {
            pref = '_deps:' + osType + ':' + preference
            log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
            await generateInstallOrders(softwarePackages[packageKey][pref])
          }
        } else if (scopedPkgManager) {
          const pref = '_deps:' + preference
          log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
          await generateInstallOrders(softwarePackages[packageKey][pref])
        } else if (scopedSystem) {
          let pref
          if (softwarePackages[packageKey]['_' + currentSelector + ':' + osID]) {
            pref = '_deps:' + osID
            log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
            await generateInstallOrders(softwarePackages[packageKey][pref])
          } else if (softwarePackages[packageKey]['_' + currentSelector + ':' + osType]) {
            pref = '_deps:' + osType
            log('info', dependenciesTag, `Installing dependencies for ${packageKey}.${pref}`)
            await generateInstallOrders(softwarePackages[packageKey][pref])
          }
        } else if (normalCheck) {
          log('info', dependenciesTag, `Installing dependencies for ${packageKey}.deps`)
          await generateInstallOrders(softwarePackages[packageKey]['_deps'])
        }
        if (softwarePackages[packageKey][preference + ':' + osID]) {
          await updateInstallMaps(
            preference,
            softwarePackages[packageKey],
            preference + ':' + osID,
            pkg,
            packageKey,
            softwarePackages
          )
          break
        } else if (softwarePackages[packageKey][preference + ':' + osType]) {
          await updateInstallMaps(
            preference,
            softwarePackages[packageKey],
            preference + ':' + osType,
            pkg,
            packageKey,
            softwarePackages
          )
          break
        } else if (softwarePackages[packageKey][preference]) {
          await updateInstallMaps(
            preference,
            softwarePackages[packageKey],
            preference,
            pkg,
            packageKey,
            softwarePackages
          )
          break
        }
      } else {
        if (!comparesRemaining) {
          log('info', 'No Match', `There was no match found for ${pkg} - it may be an OS-specific package`)
        }
      }
    }
  }
  if (generateInstallOrderCount === 1) {
    return installOrders
  } else {
    generateInstallOrderCount--
  }
}

function processPluginOrders(pkg) {
  const pluginMap = chezmoiData && chezmoiData.softwarePlugins && chezmoiData.softwarePlugins[pkg]
  if (pluginMap) {
    if (pluginMap.cmd && pluginMap.plugins) {
      installOrdersPlugins.push({ package: pkg, cmd: pluginMap.cmd, plugins: pluginMap.plugins })
    }
  }
}

// Update install, pre-hook, and post-hook objects
async function updateInstallMaps(preference, packages, scopedPreference, pkg, packageKey, softwarePackages) {
  const preHook = getHook(packages, 'pre', scopedPreference, preference)
  if (preHook) {
    installOrdersPre = installOrdersPre.concat(typeof preHook === 'string' ? [preHook] : preHook)
  }
  const postHook = getHook(packages, 'post', scopedPreference, preference)
  if (postHook) {
    installOrdersPost = installOrdersPost.concat(typeof postHook === 'string' ? [postHook] : postHook)
  }
  const serviceHook = getHook(packages, 'service', scopedPreference, preference)
  const serviceEnabledHook = getHook(packages, 'serviceEnabled', scopedPreference)
  if (serviceHook && serviceEnabledHook) {
    installOrdersService = installOrdersService.concat(typeof serviceHook === 'string' ? [serviceHook] : serviceHook)
  }
  const groupsHook = getHook(packages, 'groups', scopedPreference, preference)
  if (groupsHook) {
    installOrdersGroups = installOrdersGroups.concat(typeof groupsHook === 'string' ? [groupsHook] : groupsHook)
  }
  const portsHook = getHook(packages, 'ports', scopedPreference, preference)
  if (portsHook) {
    installOrdersPorts = installOrdersPorts.concat(typeof portsHook === 'string' ? [{
      packageKey,
      ports: portsHook
    }] : {
      packageKey,
      ports: portsHook})
  }
  processPluginOrders(pkg)
  if (!installOrders[preference]) {
    installOrders[preference] = []
  }
  log('info', 'Match', `Found a match for the package \`${pkg}\` (${packageKey} via ${scopedPreference})`)
  const newPackages = packages[scopedPreference]
  const newPkgs = typeof newPackages === 'string' ? [ newPackages ] : newPackages
  if (typeof newPackages === 'string') {
    installMeta[newPackages] = {
      preference,
      packages,
      scopedPreference,
      pkg,
      packageKey,
      softwarePackages
    }
  } else {
    for (const dataKey in newPackages) {
      installMeta[newPackages] = {
        preference,
        packages,
        scopedPreference,
        pkg,
        packageKey,
        softwarePackages
      }
    }
  }
  if (preference === 'snap' && softwarePackages[pkg]['_snapClassic'] === true) {
    if (!installOrders['snap-classic']) {
      installOrders['snap-classic'] = []
    }
    installOrders['snap-classic'] = installOrders['snap-classic'].concat(newPkgs)
  } else {
    installOrders[preference] = installOrders[preference].concat(newPkgs)
  }
}

// Get pre / post install hooks
function getHook(packages, hook, scopedPreference, preference) {
  const hookLabel = '_' + hook + ':'
  if (packages[hookLabel + scopedPreference]) {
    return packages[hookLabel + scopedPreference]
  } else if (packages[hookLabel + preference]) {
    return packages[hookLabel + preference]
  } else if (packages[hookLabel + osID]) {
    return packages
  } else if (packages[hookLabel + osType]) {
    return packages[hookLabel + osType]
  } else if (packages['_' + hook]) {
    return packages['_' + hook]
  }
}

// Acquire OS type installer key (for the installerPreference data key)
async function OSTypeInstallerKey() {
  try {
    const apt = which.sync('apt-get', { nothrow: true })
    const dnf = which.sync('dnf', { nothrow: true })
    const freebsdPkg = which.sync('pkg', { nothrow: true })
    const freebsdVersion = which.sync('freebsd-version', { nothrow: true })
    const pacman = which.sync('pacman', { nothrow: true })
    const yum = which.sync('yum', { nothrow: true })
    const zypper = which.sync('zypper', { nothrow: true })
    if (apt) {
      return dirExists('/etc/ubuntu-advantage') ? 'ubuntu' : 'apt'
    } else if (dnf || yum) {
      return 'dnf'
    } else if (pacman) {
      return 'pacman'
    } else if (zypper) {
      return 'zypper'
    } else if (freebsdPkg && freebsdVersion) {
      return 'freebsd'
    } else {
      return dirExists('/Applications') && dirExists('/Library') ? 'darwin' : 'windows'
    }
  } catch (e) {
    log('error', 'OS Detection', 'There was an error determining the type of operating system')
    console.error(e)
  }
}

// Acquire OS type
async function OSType() {
  return dirExists('/Applications') && dirExists('/Library') ? 'darwin' : (fileExists('/etc/os-release') ? 'linux' : 'windows')
}

// Acquire release ID (for Linux)
async function releaseID() {
  const ID = await $`
    if [ -f /etc/os-release ]; then
      . /etc/os-release
      echo -n $ID
    fi
  `
  return ID.stdout
}

// Post-install hook
async function afterInstall(packageManager) {
  const logStage = 'Post-Install Package Manager'
  if (packageManager === 'appimage') {
  } else if (packageManager === 'ansible') {
  } else if (packageManager === 'apk') {
  } else if (packageManager === 'apt') {
    try {
      runCommand('Running apt-get autoclean', `sudo apt-get autoclean`)
      runCommand('Running apt-get autoremove', `sudo apt-get -y autoremove`)
    } catch (e) {
      log('error', logStage, 'Error cleaning up apt-get')
    }
  } else if (packageManager === 'basher') {
  } else if (packageManager === 'binary') {
  } else if (packageManager === 'brew' || packageManager === 'cask') {
    log('info', logStage, `Ensuring Homebrew cleanup is run`)
    await $`brew cleanup`
  } else if (packageManager === 'cargo') {
  } else if (packageManager === 'choco') {
  } else if (packageManager === 'crew') {
  } else if (packageManager === 'dnf') {
  } else if (packageManager === 'flatpak') {
  } else if (packageManager === 'gem') {
  } else if (packageManager === 'go') {
  } else if (packageManager === 'nix') {
  } else if (packageManager === 'npm') {
  } else if (packageManager === 'pacman') {
  } else if (packageManager === 'pipx') {
  } else if (packageManager === 'pkg') {
  } else if (packageManager === 'port') {
  } else if (packageManager === 'scoop') {
  } else if (packageManager === 'script') {
  } else if (packageManager === 'snap') {
  } else if (packageManager === 'whalebrew') {
  } else if (packageManager === 'winget') {
  } else if (packageManager === 'yay') {
  } else if (packageManager === 'zypper') {
  }
}

async function ensurePackage(dep) {
  const target = which.sync(dep, { nothrow: true })
  if (!target) {
    if (osType === 'linux') {
      const apk = which.sync('apk', { nothrow: true })
      const apt = which.sync('apt-get', { nothrow: true })
      const dnf = which.sync('dnf', { nothrow: true })
      const pkg = which.sync('pkg', { nothrow: true })
      const yum = which.sync('yum', { nothrow: true })
      const pacman = which.sync('pacman', { nothrow: true })
      const zypper = which.sync('zypper', { nothrow: true })
      if (apk) {
        await $`sudo apk add ${dep}`
      } else if (apt) {
        if (updateDone['apt-get'] !== true) {
          await beforeInstall('apt-get')
        }
        try {
          log('info', 'apt-get Installation', `Checking if ${dep} is already installed`)
          runCommand(
            `Checking if ${dep} is already installed via apt-get`,
            `dpkg -l ${dep} | grep -E '^ii' > /dev/null`
          )
          log('info', 'Filter', `${pkg} already installed via apt-get`)
        } catch (e) {
          runCommand(
            `Installing ${dep} via apt-get`,
            `sudo apt-get -o DPkg::Options::=--force-confdef install -y ${dep}`
          )
          log('success', 'Install', `Successfully installed ${pkg} via apt-get`)
        }
      } else if (dnf) {
        if (updateDone['dnf'] !== true) {
          await beforeInstall('dnf')
        }
        try {
          log('info', 'dnf Installation', `Checking if ${dep} is already installed`)
          await $`rpm -qa | grep '$'"${dep}-" > /dev/null`
        } catch (e) {
          log('info', 'dnf Installation', `Installing ${dep} since it is not already present on the system`)
          await $`sudo dnf install -y ${dep}`
        }
      } else if (yum) {
        if (updateDone['yum'] !== true) {
          await beforeInstall('yum')
        }
        try {
          log('info', 'YUM Installation', `Checking if ${dep} is already installed`)
          await $`rpm -qa | grep '$'"${dep}-" > /dev/null`
        } catch (e) {
          log('info', 'YUM Installation', `Installing ${dep} since it is not already present on the system`)
          await $`sudo yum install -y ${dep}`
        }
      } else if (pacman) {
        if (updateDone['pacman'] !== true) {
          await beforeInstall('pacman')
        }
        try {
          log('info', 'Pacman Installation', `Checking if ${dep} is already installed`)
          await $`pacman -Qs ${dep}`
        } catch (e) {
          log('info', 'Pacman Installation', `Installing ${dep} since it is not already present on the system`)
          await $`sudo pacman -Sy ${dep}`
        }
      } else if (zypper) {
        if (updateDone['zypper'] !== true) {
          await beforeInstall('zypper')
        }
        try {
          log('info', 'Zypper Installation', `Checking if ${dep} is already installed`)
          await $`rpm -qa | grep '$'"${dep}-" > /dev/null`
        } catch (e) {
          log('info', 'Zypper Installation', `Installing ${dep} since it is not already present on the system`)
          await $`sudo zypper install -y ${dep}`
        }
      } else if (pkg) {
        if (updateDone['pkg'] !== true) {
          await beforeInstall('pkg')
        }
        try {
          log('info', 'pkg Installation', `Checking if ${dep} is already installed`)
          await $`pkg info -Ix ${dep} > /dev/null`
        } catch (e) {
          log('info', 'pkg Installation', `Installing ${dep} since it is not already present on the system`)
          await $`sudo pkg install -y ${dep}`
        }
      }
    } else if (osType === 'darwin') {
      if (updateDone['brew'] !== true) {
        await beforeInstall('brew')
      }
      await $`brew install --quiet ${dep}`
    } else if (osType === 'windows') {
      if (updateDone['choco'] !== true) {
        await beforeInstall('choco')
      }
      await `choco install -y ${dep}`
    }
  }
}

// Pre-install hook
const updateDone = {}
async function beforeInstall(packageManager) {
  updateDone[packageManager] = true
  const logStage = 'Pre-Install Package Manager'
  if (packageManager === 'appimage') {
    if (!fileExists(`${process.env.HOME}/Applications`)) {
      runSilentCommand(`mkdir -p "${process.env.HOME}/Applications"`)
    }
  } else if (packageManager === 'ansible') {
    log('info', logStage, `Temporarily enabling passwordless sudo for Ansible role installations`)
    await $`echo "$(whoami) ALL=(ALL:ALL) NOPASSWD: ALL # TEMPORARY FOR ANSIBLE INSTALL DOCTOR" | sudo tee -a /etc/sudoers`
    log('info', logStage, 'Running Ansible setup task so facts are cached')
    const unbuffer = which.sync('unbuffer', { nothrow: true })
    let unbufferPrefix = ''
    if (unbuffer) {
      unbufferPrefix = 'unbuffer'
    }
    if (osPlatform === 'darwin' || osPlatform === 'linux' || osPlatform === 'windows') {
      const capitalOsPlatform = osPlatform.charAt(0).toUpperCase() + osPlatform.slice(1)
      await $`ANSIBLE_CONFIG=${process.env.HOME}/.local/share/ansible/ansible.cfg ${unbufferPrefix} ansible 127.0.0.1 -e '{ ansible_connection: "local", ansible_become_user: "${process.env.USER}", ansible_user: "${process.env.USER}", ansible_family: "${capitalOsPlatform}", install_homebrew: False }' -m setup`
    } else {
      log('warn', 'Ansible', 'Unsupported platform - ' + osPlatform)
    }
  } else if (packageManager === 'apk') {
    await $`sudo apk update`
  } else if (packageManager === 'apt') {
    runCommand('Running apt-get update / upgrade', `sudo apt-get update && sudo apt-get upgrade -y`)
  } else if (packageManager === 'basher') {
  } else if (packageManager === 'binary') {
  } else if (packageManager === 'brew' || packageManager === 'cask') {
    if (!brewUpdated) {
      brewUpdated = true
      try {
        runCommand('Running brew update / upgrade', `brew update && brew upgrade --cask && brew upgrade`)
        runCommand('Running brew update', `brew update`)
        runCommand('Running brew upgrade', `brew upgrade`)
        if (osType === 'darwin'){
          runCommand('Running brew upgrade (Casks)', `brew upgrade --cask`)
        }
      } catch (e) {
        console.log(e)
        log('error', 'Homebrew', 'Failed running brew update / upgrade')
        log('info', 'Homebrew', 'Running brew tap --repair and trying again')
        try {
          runCommand('Repairing taps and retrying brew update / upgrade', 'export HOMEBREW_TEMP=/tmp && sudo rm -rf $(brew --cache) && brew tap --repair && brew update && brew upgrade --force --greedy && [[ $OSTYPE != "darwin"* ]] && brew upgrade --cask --greedy')
        } catch (e) {
          console.log(e)
          log('error', 'Homebrew', 'Failed both attempts to run brew update / upgrade')
        }
      }
    }
  } else if (packageManager === 'cargo') {
  } else if (packageManager === 'choco') {
  } else if (packageManager === 'crew') {
    await $`crew update`
  } else if (packageManager === 'dnf') {
    const dnf = which.sync('dnf', { nothrow: true })
    const yum = which.sync('yum', { nothrow: true })
    if (dnf) {
      runCommand('Running dnf update', `sudo dnf update -y`)
    } else if (yum) {
      runCommand('Running yum update', `sudo yum update -y`)
    }
  } else if (packageManager === 'flatpak') {
    // TODO - figure out why CentOS is failing to update
    // and then switch command below for `sudo flatpak update -y`
    runCommand('Running flatpak update', `. /etc/os-release; if [ "$ID" != "centos" ]; then sudo flatpak update -y; else echo "Skipping Flatpak update on CentOS"; fi`)
  } else if (packageManager === 'gem') {
  } else if (packageManager === 'go') {
  } else if (packageManager === 'nix') {
    runCommand('Running nix-channel --update', `nix-channel --update`)
  } else if (packageManager === 'npm') {
  } else if (packageManager === 'pacman') {
    runCommand('Running pacman update', `sudo pacman -Syu`)
  } else if (packageManager === 'pipx') {
  } else if (packageManager === 'pip') {
  } else if (packageManager === 'pkg') {
    await $`sudo pkg upgrade`
  } else if (packageManager === 'port') {
    const port = which.sync('port', { nothrow: true })
    if (port) {
      runCommand('Running port sync', `sudo port sync`)
    } else {
      log('error', 'Port Not Installed', 'Skipping sudo port sync step because port is not installed')
    }
  } else if (packageManager === 'scoop') {
    runCommand('Running scoop update', `scoop update`)
  } else if (packageManager === 'snap' || packageManager === 'snap-classic') {
    if (!snapRefreshed) {
      snapRefreshed = true
      runCommand('Ensuring snap is refreshed', `sudo snap refresh`)
    }
  } else if (packageManager === 'whalebrew') {
    if (osType === 'darwin') {
      const docker = which.sync('docker', { nothrow: true })
      if (!docker) {
        await $`brew install --cask --no-quarantine --quiet docker`
      }
      try {
        await $`sudo -c 'docker run --rm hello-world' - ${process.env.USER}`
      } catch (e) {
        log('warn', logStage, `The command \`docker run --rm hello-world\` failed`)
        try {
          log(
            'info',
            logStage,
            'Attempting to open `/Applications/Docker.app` (Docker Desktop for macOS). This should take about 30 seconds.'
          )
          const promises = [$`test -d /Applications/Docker.app`, $`rm -f ~/.docker && mkdir -p ~/.config/docker && open --background -a Docker --args --accept-license --unattended`]
          await Promise.all(promises)
          const gum = which.sync('gum', { nothrow: true })
          if (gum) {
            execSync('gum spin --spinner dot --title "Waiting for Docker Desktop to start up.." -- sleep 30', {
              stdio: 'inherit',
              shell: true
            })
          } else {
            await $`sleep 30`
          }
        } catch (e) {
          log('warn', logStage, `Docker Desktop appears to not be installed!`)
        }
      }
    }
  } else if (packageManager === 'winget') {
    runCommand('Running winget source update', `winget source update`)
  } else if (packageManager === 'yay') {
  } else if (packageManager === 'zypper') {
    runCommand('Running zypper update', `sudo zypper update`)
  }
}

async function ensureInstalled(bin, callback) {
  const logStage = 'Package Manager Install'
  const installed = which.sync(bin, { nothrow: true })
  if (installed) {
    log('info', logStage, `\`${bin}\` is available`)
  } else {
    log('warn', logStage, `\`${bin}\` is not installed!`)
    if (callback) {
      await callback
    } else {
      log('error', logStage, `There does not appear to be an installation method available for \`${bin}\``)
    }
  }
}

async function ensurePackageManagerAnsible() {
  await $`pipx install ansible-core`
  if (osType === 'darwin') {
    await $`pipx inject ansible-core PyObjC PyObjC-core`
  }
  await $`pipx inject ansible-core docker lxml netaddr pexpect python-vagrant pywinrm requests-credssp watchdog`
  await $`mkdir -p "$HOME/.cache/megabyte-labs"`
  await $`touch "$HOME/.cache/megabyte-labs/ansible-installed"`
  log('info', 'Package Manager Install', `Ansible and its supporting packages are now installed via pipx`)
}

// Ensure the package manager is available
let packageManagerInstalled = {}
async function ensurePackageManager(packageManager) {
  const logStage = 'Pre-Reqs'
  log('info', logStage, `Ensuring \`${packageManager}\` is set up`)
  if (packageManagerInstalled[packageManager]) {
    return
  } else {
    packageManagerInstalled[packageManager] = true
  }
  if (packageManager === 'ansible') {
    await ensurePackageManager('pipx')
  }
  if (
    packageManager === 'gem' ||
    packageManager === 'go' ||
    packageManager === 'npm' ||
    packageManager === 'pipx' ||
    packageManager === 'whalebrew'
  ) {
    await ensurePackageManager('brew')
  }
  if (packageManager === 'appimage') {
    const zap = which.sync('zap', { nothrow: true })
    if (!zap) {
      log('info', 'Zap Installation', 'Installing Zap to handle AppImage installation')
      await ensurePackage('curl')
      await $`sudo curl -sSL --output /usr/local/bin/zap https://github.com/srevinsaju/zap/releases/download/continuous/zap-amd64`
      await $`sudo chmod +x /usr/local/bin/zap`
    }
  } else if (packageManager === 'ansible') {
    try {
      await $`test -f "$HOME/.cache/megabyte-labs/ansible-installed"`
      const ansible = which.sync('ansible', { nothrow: true })
      if (ansible) {
        log('info', logStage, `\`ansible\` and its supporting packages appear to be installed`)
      } else {
        await ensurePackageManagerAnsible()
      }
    } catch (e) {
      await ensurePackageManagerAnsible()
    }
  } else if (packageManager === 'apk') {
    await ensureInstalled('apk', false)
  } else if (packageManager === 'apt') {
    await ensureInstalled('apt', false)
  } else if (packageManager === 'basher') {
    await ensureInstalled(
      'basher',
      $`
      # TODO
      echo "Bash script that installs basher here"
    `
    )
  } else if (packageManager === 'binary') {
    await ensurePackage('curl')
  } else if (packageManager === 'bpkg') {
    await ensureInstalled(
      'bpkg',
      $`
      # TODO
      echo "Bash script that installs bpkg here"
    `
    )
  } else if (packageManager === 'brew' || packageManager === 'cask') {
    const brew = which.sync('brew', { nothrow: true })
    if (!brew) {
      await ensureInstalled(
        'brew',
        $`
        if command -v sudo > /dev/null && sudo -n true; then
          echo | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
          sudo chmod -R g-w "$(brew --prefix)/share"
        else
          log('info', logStage, 'Homebrew is not installed. Password may be required.')
          bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || BREW_EXIT_CODE="$?"
          sudo chmod -R g-w "$(brew --prefix)/share"
          if [ -n "$BREW_EXIT_CODE" ]; then
            if command -v brew > /dev/null; then
              log('warn', logStage, 'Homebrew was installed but part of the installation failed. Attempting to fix..')
              BREW_DIRS="share/man share/doc share/zsh/site-functions etc/bash_completion.d"
              for BREW_DIR in $BREW_DIRS; do
                if [ -d "$(brew --prefix)/$BREW_DIR" ]; then
                  sudo chown -R "$(whoami)" "$(brew --prefix)/$BREW_DIR"
                fi
              done
              brew update --force --quiet
            fi
          fi
        fi
      `
      )
    }
  } else if (packageManager === 'cargo') {
    const cargo = which.sync('cargo', { nothrow: true })
    if (!cargo) {
      if (osType === 'darwin') {
        const rustup = which.sync('rustup-init', { nothrow: true })
        if (!rustup) {
          await $`brew install --quiet rustup`
        }
        await $`rustup-init -y`
      } else if (osType === 'windows') {
      } else {
        await ensurePackage('cargo')
      }
    }
  } else if (packageManager === 'choco') {
    await ensureInstalled(
      'choco',
      $`
      powershell "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"
    `
    )
  } else if (packageManager === 'crew') {
    await ensureInstalled(
      'crew',
      $`
      # TODO Bash script that installs crew here
      # Source: https://github.com/chromebrew/chromebrew
      curl -Ls git.io/vddgY | bash
    `
    )
  } else if (packageManager === 'dnf') {
    const dnf = which.sync('dnf', { nothrow: true })
    const yum = which.sync('yum', { nothrow: true })
    if (dnf) {
      log('info', logStage, `\`dnf\` is available`)
    } else if (yum) {
      log('info', logStage, `\`yum\` is available`)
    } else {
      log('error', logStage, `Both \`dnf\` and \`yum\` are not available`)
    }
  } else if (packageManager === 'flatpak') {
    const flatpak = which.sync('flatpak', { nothrow: true })
    if (flatpak) {
      log('info', logStage, `\`flatpak\` is available`)
    } else {
      const apk = which.sync('apk', { nothrow: true })
      const apt = which.sync('apt-get', { nothrow: true })
      const dnf = which.sync('dnf', { nothrow: true })
      const yum = which.sync('yum', { nothrow: true })
      const pacman = which.sync('pacman', { nothrow: true })
      const zypper = which.sync('zypper', { nothrow: true })
      if (apk) {
        runCommand('Installing flatpak via apk', 'sudo apk add flatpak')
      } else if (apt) {
        runCommand('Installing flatpak via apt-get', 'sudo apt-get install -y flatpak')
        if (fileExists('/usr/bin/gnome-shell')) {
          runCommand(
            'Installing gnome-software-plugin-flatpak via apt-get',
            'sudo apt-get install -y gnome-software-plugin-flatpak'
          )
        }
        if (fileExists('/usr/bin/plasmashell')) {
          runCommand('Installing plasmashell via apt-get', 'sudo apt-get install -y plasmashell')
        }
      } else if (dnf) {
        await $`sudo dnf install -y flatpak`
      } else if (yum) {
        await $`sudo yum install -y flatpak`
      } else if (pacman) {
        await $`sudo pacman -Sy flatpak`
      } else if (zypper) {
        await $`sudo zypper install -y flatpak`
      }
      log('info', logStage, `\`flatpak\` was installed. It may require a reboot to function correctly.`)
    }
    const flatpakPost = which.sync('flatpak', { nothrow: true })
    if (flatpakPost) {
      await $`sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`
    } else {
      log('error', logStage, `\`flatpak\` failed to install!`)
    }
  } else if (packageManager === 'gem') {
    const gem = which.sync('gem', { nothrow: true })
    if (!gem) {
      await ensureInstalled('gem', $`brew install --quiet ruby`)
    }
  } else if (packageManager === 'go') {
    const go = which.sync('go', { nothrow: true })
    if (!go) {
      await ensureInstalled('go', $`brew install --quiet go`)
    }
  } else if (packageManager === 'nix') {
    await ensureInstalled(
      'nix',
      $`
      if [ -d /Applications ] && [ -d /Library ]; then
        sh <(curl -L https://nixos.org/nix/install)
      else
        sh <(curl -L https://nixos.org/nix/install) --daemon
      fi
    `
    )
  } else if (packageManager === 'npm') {
    const npm = which.sync('npm', { nothrow: true })
    const node = which.sync('node', { nothrow: true })
    const volta = which.sync('volta', { nothrow: true })
    if (npm && node && volta) {
      log('info', logStage, `\`npm\`, \`node\`, and \`volta\` are available`)
    } else {
      if (!volta) {
        await $`brew install --quiet volta`
      }
      await $`
        export VOLTA_HOME="\${XDG_DATA_HOME:-$HOME/.local/share}/volta"
        export PATH="$VOLTA_HOME/bin:$PATH"
        volta setup
        volta install node
      `
    }
    log('info', logStage, 'Ensuring Volt has Node.js runtime available')
    await $`export VOLTA_HOME="\${XDG_DATA_HOME:-$HOME/.local/share}/volta" && export PATH="$VOLTA_HOME/bin:$PATH" && if ! volta list 2>&1 | grep 'runtime node' > /dev/null; then volta install node; fi`
  } else if (packageManager === 'pacman') {
    await ensureInstalled('pacman', false)
  } else if (packageManager === 'pipx') {
    const pipx = which.sync('pipx', { nothrow: true })
    if (!pipx) {
      await ensureInstalled('pipx', $`brew install --quiet pipx`)
      await $`pipx ensurepath`
    }
  } else if (packageManager === 'pkg') {
    await ensureInstalled('pkg', false)
  } else if (packageManager === 'port') {
    const port = which.sync('port', { nothrow: true })
    if (!port) {
      log('info', logStage, `Installing ${packageManager}`)
      await ensureInstalled(
        'port',
        $`
          sudo mkdir -p /opt/mports
          cd /opt/mports
          sudo rm -rf macports-base
          sudo git clone https://github.com/macports/macports-base.git
          cd macports-base
          sudo git checkout v2.8.0
          sudo bash --noprofile --norc -c './configure --enable-readline && make && make install && make distclean'
          sudo port selfupdate
        `
      )
      log('info', logStage, `${packageManager} is now installed`)
    } else {
      log('info', logStage, `\`port\` is available`)
    }
  } else if (packageManager === 'scoop') {
    await ensureInstalled(
      'scoop',
      $`
      powershell 'Set-ExecutionPolicy RemoteSigned -Scope CurrentUser'
      powershell 'irm get.scoop.sh | iex
    `
    )
  } else if (packageManager === 'snap') {
    const apk = which.sync('apk', { nothrow: true })
    const apt = which.sync('apt-get', { nothrow: true })
    const dnf = which.sync('dnf', { nothrow: true })
    const yum = which.sync('yum', { nothrow: true })
    const pacman = which.sync('pacman', { nothrow: true })
    const zypper = which.sync('zypper', { nothrow: true })
    if (apt) {
      if (fileExists('/etc/apt/preferences.d/nosnap.pref')) {
        $`sudo mv /etc/apt/preferences.d/nosnap.pref /etc/apt/nosnap.pref.bak`
      }
      runCommand('Ensuring snapd is installed', `sudo apt-get install -y snapd`)
      // TODO Following may be required on Kali -> https://snapcraft.io/docs/installing-snap-on-kali
      // systemctl enable --now snapd apparmor
      runCommand('Enabling snapd service', `sudo systemctl enable snapd`)
      runCommand('Starting snapd service', `sudo systemctl start snapd`)
    } else if (dnf) {
      runCommand('Ensuring snapd is installed', `sudo dnf install -y snapd`)
      if (!fileExists('/snap')) {
        await $`sudo ln -s /var/lib/snapd/snap /snap`
      }
      runCommand('Enabling snapd service', `sudo systemctl enable snapd`)
      runCommand('Starting snapd service', `sudo systemctl start snapd`)
    } else if (yum) {
      runCommand('Ensuring snapd is installed', 'sudo yum install -y snapd')
      await $`sudo systemctl enable --now snapd.socket`
      if (!fileExists('/snap')) {
        $`sudo ln -s /var/lib/snapd/snap /snap`
      }
    } else if (pacman) {
      $`if [ -f /etc/arch-release ]; then sudo git clone https://aur.archlinux.org/snapd.git /usr/local/src/snapd && cd /usr/local/src/snapd && sudo makepkg -si; else sudo pacman -S snapd && sudo systemctl enable --now snapd.socket && if [ ! -d /snap ]; then sudo ln -s /var/lib/snapd/snap /snap; fi; fi`
    } else if (zypper) {
      // TODO See https://snapcraft.io/docs/installing-snap-on-opensuse
      await $`
         echo "TODO - Bash script that installs snap w/ zypper"
        `
    }
    const snap = which.sync('snap', { nothrow: true })
    if (snap) {
      runCommand('Check info for core snap package', `sudo snap info core`)
      // Required: https://snapcraft.io/docs/troubleshooting#heading--early
      runCommand('Ensuring snap is seeded', `sudo snap wait system seed.loaded`)
      runCommand('Ensuring snap core is installed', `sudo snap install core`)
    } else {
      log('warn', logStage, 'Snap installation sequence completed but the snap bin is still not available')
    }
  } else if (packageManager === 'script') {
  } else if (packageManager === 'whalebrew') {
    await ensureInstalled('whalebrew', $`brew install --quiet whalebrew`)
  } else if (packageManager === 'winget') {
    await ensureInstalled(
      'winget',
      $`
      echo "TODO - Script that installs winget here"
    `
    )
  } else if (packageManager === 'yay') {
    const yay = which.sync('yay', { nothrow: true })
    await $`sudo pacman -S --needed base-devel git`
    await $`
      if [ -d /usr/local/src ]; then
        git clone https://aur.archlinux.org/yay.git /usr/local/src/yay
        cd /usr/local/src/yay
        makepkg -si
      fi
    `
  } else if (packageManager === 'zypper') {
    await ensureInstalled('zypper', false)
  }
}

// Installs a list of packages via the specified package manager
async function installPackageList(packageManager, packages) {
  const logStage = 'Package Install'
  try {
    if (packageManager === 'appimage') {
      for (let pkg of packages) {
        try {
          if (pkg.substring(0, 4) === 'http') {
            log('info', 'AppImage Install', `Installing ${pkg} from its URL`)
            runCommand('Installing ${pkg} via zap', `zap install --select-first -q --from ${pkg}`)
          } else if ((pkg.match(/\//g) || []).length === 1) {
            log('info', 'AppImage Install', `Installing ${pkg} from a GitHub Release`)
            runCommand('Installing ${pkg} via zap', `zap install --select-first -q --github --from ${pkg}`)
          } else {
            log('info', 'AppImage Install', `Installing ${pkg} using the AppImage Catalog`)
            runCommand('Installing ${pkg} via zap', `zap install --select-first -q ${pkg}`)
          }
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error using Zap to install ${pkg}`)
          console.error(e)
        }
      }
      log('warn', 'Install', 'Zap installs might fail - this is expected. Waiting on fixes to Zap upstream project')
    } else if (packageManager === 'ansible') {
      for (let pkg of packages) {
        try {
          const unbuffer = which.sync('unbuffer', { nothrow: true })
          let unbufferPrefix = ''
          if (unbuffer) {
            unbufferPrefix = 'unbuffer'
          }
          const verboseMode = process.env.DEBUG_MODE === 'true' ? 'vv' : ''
          if (osPlatform === 'darwin' || osPlatform === 'linux' || osPlatform === 'windows') {
            const capitalOsPlatform = osPlatform.charAt(0).toUpperCase() + osPlatform.slice(1)
            await $`ANSIBLE_CONFIG=${process.env.HOME}/.local/share/ansible/ansible.cfg ansible 127.0.0.1 -v${verboseMode} -e '{ ansible_connection: "local", ansible_become_user: "root", ansible_user: "${process.env.USER}", ansible_family: "${capitalOsPlatform}", install_homebrew: False }' -m include_role -a name=${pkg}`
            log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
          } else {
            log('warn', 'Ansible', 'Unsupported platform - ' + osPlatform)
          }
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with Ansible`)
        }
      }
    } else if (packageManager === 'apk') {
      for (let pkg of packages) {
        try {
          runCommand('Installing ${pkg} via apk', `sudo apk add ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with apk`)
          console.error(e)
        }
      }
    } else if (packageManager === 'apt') {
      for (let pkg of packages) {
        try {
          if (pkg.startsWith('http') && pkg.endsWith('.deb')) {
            runCommand(
              `Downloading and installing ${pkg}`,
              `TMP="$(mktemp)" && curl -sSL ${pkg} -o "$TMP" && sudo dpkg -i "$TMP"`
            )
          } else {
            runCommand(
              `Installing ${pkg} via ${packageManager}`,
              `sudo DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Options::=--force-confdef install -y ${pkg}`
            )
          }
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with apt-get`)
          console.error(e)
        }
      }
    } else if (packageManager === 'basher') {
      for (let pkg of packages) {
        try {
          await $`basher install ${pkg}`
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with basher`)
          console.error(e)
        }
      }
    } else if (packageManager === 'binary') {
      for (let pkg of packages) {
        try {
          const bins = installData.softwarePackages.filter((x) => x.appimage === pkg)
          if (bins && bins[0]) {
            const binName = bins[0]['_bin']
            await $`TMP="$(mktemp)" && curl -sSL ${pkg} > "$TMP" && sudo mv "$TMP" /usr/local/src/${binName} && chmod +x /usr/local/src/${binName}`
          }
        } catch (e) {
          log('error', 'Install', `There was an error installing the binary release for ${pkg}`)
          console.error(e)
        }
      }
    } else if (packageManager === 'brew') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `brew install --quiet ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with brew`)
          console.error(e)
        }
      }
    } else if (packageManager === 'cask') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `brew install --cask --no-quarantine --quiet ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with Homebrew Cask`)
          console.error(e)
        }
      }
    } else if (packageManager === 'cargo') {
      for (const pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `cargo install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with Cargo`)
          console.error(e)
        }
      }
    } else if (packageManager === 'choco') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `choco install -y ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with Chocolatey`)
          console.error(e)
        }
      }
    } else if (packageManager === 'crew') {
    } else if (packageManager === 'dnf') {
      const dnf = which.sync('dnf', { nothrow: true })
      const yum = which.sync('yum', { nothrow: true })
      for (let pkg of packages) {
        if (dnf) {
          try {
            runCommand(`Installing ${pkg} via ${packageManager}`, `sudo dnf install -y ${pkg}`)
            log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
          } catch (e) {
            log('error', 'Install', `There was an error installing ${pkg} with dnf`)
            console.error(e)
          }
        } else if (yum) {
          try {
            runCommand(`Installing ${pkg} via ${packageManager}`, `sudo yum install -y ${pkg}`)
            log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
          } catch (e) {
            log('error', 'Install', `There was an error installing ${pkg} with yum`)
            console.error(e)
          }
        }
      }
    } else if (packageManager === 'flatpak') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `sudo flatpak install -y flathub ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with flatpak`)
          console.error(e)
        }
      }
    } else if (packageManager === 'gem') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `gem install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with gem`)
          console.error(e)
        }
      }
    } else if (packageManager === 'go') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `go install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with go`)
          console.error(e)
        }
      }
    } else if (packageManager === 'nix') {
    } else if (packageManager === 'npm') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `volta install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with volta`)
          console.error(e)
        }
      }
    } else if (packageManager === 'pacman') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `sudo pacman -Sy --noconfirm --needed ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with pacman`)
          console.error(e)
        }
      }
    } else if (packageManager === 'pipx') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `pipx install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('warn', 'Install', `There was an error installing ${pkg} with pipx, re-attempting using pip3`)
          console.error(e)
          try {
            runCommand(`Installing ${pkg} via pip3`, `pip3 install ${pkg}`)
            log('success', 'Install', `${pkg} successfully installed via pip3`)
          } catch (e) {
            log('error', 'Install', `There was an error installing ${pkg} with both pipx and pip3`)
            console.error(e)
          }
        }
      }
    } else if (packageManager === 'pip') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `pip3 install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('warn', 'Install', `There was an error installing ${pkg} with pip3`)
          console.error(e)
        }
      }
    } else if (packageManager === 'pkg-darwin') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `TMP="$(mktemp)" && curl -sSL "${pkg}" > "$TMP" && sudo installer -pkg "$TMP" -target /`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with the system installer`)
          console.error(e)
        }
      }
    } else if (packageManager === 'port') {
      const port = which.sync('port', { nothrow: true })
      if (port) {
        for (let pkg of packages) {
          try {
            runCommand(`Installing ${pkg} via ${packageManager}`, `sudo port install ${pkg}`)
            log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
          } catch (e) {
            log('error', 'Install', `There was an error installing ${pkg} with port`)
            console.error(e)
          }
        }
      } else {
        log(
          'error',
          'Port Not Installed',
          `Unable to install with port because it is not installed. Skipping installation of ${packages}`
        )
      }
    } else if (packageManager === 'scoop') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `scoop install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with scoop`)
          console.error(e)
        }
      }
    } else if (packageManager === 'snap') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `sudo snap install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with snap`)
          console.error(e)
        }
      }
    } else if (packageManager === 'script') {
      for (let pkg of packages) {
        try {
          await $`bash -c ${pkg}`
        } catch (e) {
          log('error', 'Install', `There was an error running the script installation method for ${pkg}`)
          console.error(e)
        }
      }
    } else if (packageManager === 'snap-classic') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `sudo snap install --classic ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with snap in classic mode`)
          console.error(e)
        }
      }
    } else if (packageManager === 'whalebrew') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `whalebrew install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with whalebrew`)
          console.error(e)
        }
      }
    } else if (packageManager === 'winget') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `winget install ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with winget`)
          console.error(e)
        }
      }
    } else if (packageManager === 'yay') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `yay -Sy --noconfirm --needed ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with yay`)
          console.error(e)
        }
      }
    } else if (packageManager === 'zypper') {
      for (let pkg of packages) {
        try {
          runCommand(`Installing ${pkg} via ${packageManager}`, `sudo zypper install -y ${pkg}`)
          log('success', 'Install', `${pkg} successfully installed via ${packageManager}`)
        } catch (e) {
          log('error', 'Install', `There was an error installing ${pkg} with zypper`)
          console.error(e)
        }
      }
    }
  } catch (e) {
    log('error', logStage, `Possibly encountered an error while installing via \`${packageManager}\``)
    log('info', logStage, `Proceeding with the installation..`)
  }
}

async function addUserGroup(group) {
  const logStage = 'Users / Groups'
  log('info', logStage, `Ensuring the ${group} group / user is added`)
  runCommand(`Creating the ${group} user / group`, `sudo "${process.env.HOME}/.local/bin/add-usergroup" "${process.env.USER}" "${group}"`)
}

/**
 * Adds the rules specified in the `_ports` key of each entry in the `software.yml` file.
 *
 * @param rule {packageKey: string, ports: any} Firewall rule in the form of 8888/tcp or 9999/udp. Can also be the XML file name stored in ~/.config/firewall/etc/firewalld/services.
 */
async function addFirewallRule(rule) {
  try {
    const logStage = 'Firewall'
    const packageName = installData.softwarePackages[rule.packageKey] && installData.softwarePackages[rule.packageKey]._name
    const packageDesc = installData.softwarePackages[rule.packageKey] && installData.softwarePackages[rule.packageKey]._desc
    log('info', logStage, `Ensuring the ${rule.packageKey} rule is added since the _ports key is defined`)
    if (osType === 'linux') {
      const firewallCmd = which.sync('firewall-cmd', { nothrow: true })
      // const ufw = which.sync('ufw', { nothrow: true })
      if (firewallCmd) {
        const serviceFile = `${process.env.HOME}/.config/firewall/etc/firewalld/services/${rule.packageKey}.xml`
        if (fileExists(serviceFile)) {
          log('info', logStage, `Service file found at ${serviceFile} - using it to apply firewall-cmd configuration`)
          runCommand(`Copying over ${serviceFile} file to /etc/firewalld/services`, `sudo cp -f "${serviceFile}" "/etc/firewalld/services/${rule.packageKey}.xml"`)
          runCommand(`Adding the ${rule.packageKey} firewall-cmd service`, `sudo firewall-cmd --add-service=${rule.packageKey} --permanent`)
        } else {
          if (typeof rule.ports === 'string') {
            runCommand(`Adding the ${rule.packageKey} ${rule.ports} rule to the firewall configuration`, `sudo firewall-cmd --add-port=${rule.ports} --permanent`)
          } else {
            for (const port of rule.ports) {
              if (typeof port === 'string') {
                runCommand(`Adding the ${rule.packageKey} ${rule.ports} rule to the firewall configuration`, `sudo firewall-cmd --add-port=${rule.ports} --permanent`)
              } else if (port.port && port.proto) {
                runCommand(`Adding the ${rule.packageKey} ${port.port}/${port.proto} rule to the firewall configuration`, `sudo firewall-cmd --add-port=${port.port}/${port.proto} --permanent`)
              } else {
                log('error', logStage, `Unable to parse the firewall definition for ${rule.packageKey}`)
              }
            }
          }
        }
      } else {
        log('error', logStage, `The firewall-cmd executable is not present on the system so the firewall cannot be configured`)
      }
    } else if (osType === 'darwin') {
      const socketFilterFw = '/usr/libexec/ApplicationFirewall/socketfilterfw'
      const serviceFile = `${process.env.HOME}/.config/firewall/darwin/${rule.packageKey}.sh`
      if (fileExists(serviceFile)) {
        runCommand(`Executing the matching ${serviceFile} service file`, `sudo bash "${serviceFile}"`)
      } else {
        if (typeof rule.ports === 'string') {
          log('error', logStage, `_ports rules that are equal to strings are not yet implemented on macOS (package: ${rule.packageKey})`)
        } else {
          for (const port of rule.ports) {
            if (typeof port === 'string') {
              log('error', logStage, `_ports rules that are equal to strings are not yet implemented on macOS (package: ${rule.packageKey})`)
            } else if (port.port && port.proto) {
              if (packageDesc) {
                runCommand(`Adding new service with description populated for ${rule.packageKey}`, `${socketFilterFw} --add --service "${packageName ? packageName : rule.packageKey}" --setglobaldescription "${packageDesc} --getglobalstate"`)
              } else {
                runCommand(`Adding new service for ${rule.packageKey}`, `${socketFilterFw} --add --service "${packageName ? packageName : rule.packageKey}" --getglobalstate`)
              }
              runCommand(`Adding firewall rule for ${rule.packageKey}`, `${socketFilterFw} --add --service "${packageName ? packageName : rule.packageKey}" --port ${port.port} --protocol ${port.proto}`)
            } else {
              log('error', logStage, `Unable to parse the firewall definition for ${rule.packageKey}`)
            }
          }
        }
      }
    } else if (osType === 'windows') {
      log('warn', logStage, `Windows support not yet added`)
    } else {
      log('warn', logStage, `Unknown operating system type`)
    }
  } catch (e) {
    console.log(e)
    log('error', 'Bin', `Error configuring firewall settings for ${rule.packageKey}`)
  }
}

async function updateService(service) {
  const logStage = 'Service Service'
  if (osType === 'linux') {
    const systemctl = which.sync('systemctl', { nothrow: true })
    const brew = which.sync('brew', { nothrow: true })
    if (systemctl) {
      try {
        runCommand(`Starting / enabling ${service} with systemctl`, `sudo systemctl enable --now ${service}`)
        log('success', logStage, `Started / enabled the ${service} service`)
      } catch (e) {
        log('info', logStage, `There was an error starting / enabling the ${service} service with systemd`)
        try {
          if (brew) {
            if (typeof service === 'array') {
              service.forEach(x => {
                runCommand(`Starting / enabling object array ${x.name} with Homebrew`, `${x.sudo ? 'sudo brew' : 'brew'} services restart ${x.name}`)
                log('success', logStage, `Started / enabled the ${x.name} service with Homebrew`)
              })
            } else if (typeof service === 'object') {
              runCommand(`Starting / enabling object ${service.name} with Homebrew`, `${service.sudo ? 'sudo brew' : 'brew'} services restart ${service.name}`)
              log('success', logStage, `Started / enabled the ${service.name} service with Homebrew`)
            } else {
              runCommand(`Starting / enabling ${service} with Homebrew`, `brew services restart ${service}`)
              log('success', logStage, `Started / enabled the ${service} service with Homebrew`)
            }
          } else {
            log('error', logStage, `Unable to start service with systemd and Homebrew is not available`)
          }
        } catch (err) {
          log('error', logStage, `Unable to start service with both systemd and Homebrew`)
          log('info', logStage, `systemd error`)
          console.error(e)
          log('info', logStage, `brew services error`)
          console.error(e)
        }
      }
    } else {
      log(
        'warn',
        logStage,
        `The systemctl command is not available so applications with services cannot be started / enabled`
      )
    }
  } else if (osType === 'darwin') {
    const brew = which.sync('brew', { nothrow: true })
    if (brew) {
      try {
        if (typeof service === 'array') {
          service.forEach(x => {
            runCommand(`Starting / enabling object array ${x.name} with Homebrew`, `${x.sudo ? 'sudo brew' : 'brew'} services restart ${x.name}`)
            log('success', logStage, `Started / enabled the ${x.name} service with Homebrew`)
          })
        } else if (typeof service === 'object') {
          runCommand(`Starting / enabling object ${service.name} with Homebrew`, `${service.sudo ? 'sudo brew' : 'brew'} services restart ${service.name}`)
          log('success', logStage, `Started / enabled the ${service.name} service with Homebrew`)
        } else {
          runCommand(`Starting / enabling ${service} with Homebrew`, `brew services restart ${service}`)
          log('success', logStage, `Started / enabled the ${service} service with Homebrew`)
        }
      } catch (e) {
        log('error', logStage, `There was an error starting / enabling the ${service} Homebrew service`)
        console.error(e)
      }
    } else {
      log('warn', logStage, `Homebrew is not available - skipping service start command`)
    }
  }
}

/**
 * Filter that resolves when all asynchronous filter actions are done
 */
const asyncFilter = async (arr, predicate) => {
  const results = await Promise.all(arr.map(predicate))

  return arr.filter((_v, index) => results[index])
}

async function pruneInstallOrders(installOrders) {
  const newOrders = Object.assign({}, installOrders)
  log('info', 'Filter', 'Removing packages from installOrders that are already installed')
  for (const pkgManager in installOrders) {
    log('info', 'Filter', `Filtering the ${pkgManager} installOrders`)
    console.log('List to be filtered:', newOrders[pkgManager])
    if (pkgManager === 'appimage') {
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          await runSilentCommand(`zap list | grep "${pkg}" > /dev/null`)
          return false
        } catch (e) {
          return true
        }
      })
    } else if (pkgManager === 'apt') {
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          await runSilentCommand(`dpkg -l ${pkg} | grep -E '^ii' > /dev/null`)
          return false
        } catch (e) {
          return true
        }
      })
    } else if (pkgManager === 'brew') {
      let newVal = newOrders[pkgManager]
      const pkgTmp = '/tmp/brew-list-install-doctor'
      runCommand(`Populating temporary file with list of Homebrew packages`, `brew list > ${pkgTmp}`)
      for (const pkg of newOrders[pkgManager]) {
        try {
          await $`cat ${pkgTmp} | grep '^${pkg}$'`
          log('info', 'Filter', 'Filtering list')
          newVal = newVal.filter((x) => x !== pkg)
          console.log('New list', newVal)
        } catch (e) {
          // Do nothing
        }
      }
      newOrders[pkgManager] = newVal
    } else if (pkgManager === 'dnf') {
      const dnf = which.sync('dnf', { nothrow: true })
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          if (dnf) {
            await runSilentCommand(`rpm -qa | grep '$'"${pkg}-" > /dev/null`)
          } else {
            await runSilentCommand(`rpm -qa | grep '$'"${pkg}-" > /dev/null`)
          }
          return false
        } catch (e) {
          return true
        }
      })
    } else if (pkgManager === 'flatpak') {
      const flatpakInstallation = await $`flatpak --installations`
      const flatpakDir = flatpakInstallation.stdout.replace('\n', '')
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          await runSilentCommand(`test -d ${flatpakDir}/app/${pkg}`)
          return false
        } catch (e) {
          return true
        }
      })
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          await runSilentCommand(`flatpak info ${pkg} > /dev/null`)
          return false
        } catch (e) {
          return true
        }
      })
    } else if (pkgManager === 'pacman') {
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          await runSilentCommand(`pacman -Qs ${pkg} > /dev/null`)
          return false
        } catch (e) {
          return true
        }
      })
    } else if (pkgManager === 'snap' || pkgManager === 'snap-classic') {
      newOrders[pkgManager] = await asyncFilter(newOrders[pkgManager], async (pkg) => {
        try {
          await runSilentCommand(`snap list ${pkg} | grep ${pkg} > /dev/null`)
          return false
        } catch (e) {
          return true
        }
      })
    }
    log('info', 'Filter', `Finished filtering ${pkgManager}`)
    console.log('Filtered list:', newOrders[pkgManager])
  }
  return newOrders
}

async function installPlugins(pluginData) {
  if (pluginData.cmd && pluginData.plugins && pluginData.plugins.length) {
    const pluginWhen = pluginData.when
    console.log('Plugin when condition:', pluginWhen)
    try {
      if (pluginWhen) {
        runCommand(`Checking when condition for ${pluginData.package} plugin - ${plugin}`, pluginWhen)
      }
      for (const plugin of pluginData.plugins) {
        try {
          const pluginCmd = pluginData.cmd.replace(/{PLUGIN}/g, plugin)
          try {
            try {
              runCommand(`Installing ${pluginData.package} plugin - ${plugin}`, pluginCmd)
              log('success', 'Plugin', `Successfully installed ${pluginData.package} plugin - ${plugin}`)
            } catch (e) {
              log('info', 'Plugin', `Failed to install ${pluginData.package} plugin - ${plugin}`)
              console.error(e)
            }
          } catch (e) {
            log('info', 'Plugin', `Skipping ${pluginData.package} plugin installs due to failed when condition - ${pluginWhen}`)
            break
          }
        } catch (e) {
          log('error', 'Plugin', `Failed to install ${pluginData.package} plugin due to an unknown reason - ${plugin}`)
          console.error(e)
        }
      }
    } catch (e) {
      log('info', 'Plugin', 'Failed to pass when condition')
    }
  }
  if (pluginData.update) {
    const pluginDataPackage = pluginData.package
    if (pluginDataPackage) {
      runCommand('Updating ' + pluginDataPackage + ' plugins', pluginData.update)
    }
  }
}

async function linkBin(installOrdersBinLink) {
  let flatpakInstallation, flatpakDir
  const softwarePackages = installData.softwarePackages
  const flatpak = which.sync('flatpak', { nothrow: true })
  if (flatpak) {
    flatpakInstallation = await $`flatpak --installations`
    flatpakDir = flatpakInstallation.stdout.replace('\n', '')
  }
  for (const binLink of installOrdersBinLink) {
    const pkg = softwarePackages[binLink.package][binLink.preference]
    if (typeof pkg === 'string') {
      if (binLink.bin !== false) {
        if (!which.sync(binLink.bin, { nothrow: true })) {
          if (binLink.preference === 'flatpak' && flatpak) {
            try {
              runCommand(
                `Adding bin link for ${pkg} (${binLink.bin})`,
                `bash -c 'test -d ${flatpakDir}/app/${pkg} && mkdir -p "${process.env.HOME}/.local/bin/flatpak" && echo "flatpak run ${pkg} \\\$*" > "${process.env.HOME}/.local/bin/flatpak/${binLink.bin}" && chmod +x  "${process.env.HOME}/.local/bin/flatpak/${binLink.bin}"'`
              )
              log('success', 'Bin', `Linked ~/.local/bin/flatpak/${binLink.bin} to the ${pkg} Flatpak`)
            } catch (e) {
              log('warn', 'Bin', `Expected flatpak directory not available - ${flatpakDir}/app/${pkg}`)
            }
          } else if (softwarePackages[binLink.package]['_app']) {
            try {
              const appName = softwarePackages[binLink.package]['_app']
              log('info', 'Bin', `Checking for existence of ${appName} application in /Applications and ~/Applications`)
              if (fileExists(`/Applications/${appName}`)) {
                runCommand(
                  `Adding shortcut bin link for ${binLink.package}`,
                  `bash -c 'mkdir -p "${process.env.HOME}/.local/bin/cask" && echo "open \"/Applications/${appName}\" \\\$*" > "${process.env.HOME}/.local/bin/cask/${binLink.bin}" && chmod +x "${process.env.HOME}/.local/bin/cask/${binLink.bin}"'`
                )
              } else if(fileExists(`${process.env.HOME}/Applications/${appName}`)) {
                runCommand(
                  `Adding shortcut bin link for ${binLink.package}`,
                  `bash -c 'mkdir -p "${process.env.HOME}/.local/bin/cask" && echo "open \"${process.env.HOME}/Applications/${appName}\" \\\$*" > "${process.env.HOME}/.local/bin/cask/${binLink.bin}" && chmod +x "${process.env.HOME}/.local/bin/cask/${binLink.bin}"'`
                )
              } else {
                log('warn', 'Bin', `Expected Homebrew cask directory not found - ${pkg}`)
              }
            } catch (e) {
              console.log(e)
              log('warn', 'Bin', `Error creating bin shortcut link for ${pkg}`)
            }
          }
        } else {
          log('info', 'Bin', `Link already exists for ${binLink.package}`)
        }
      } else {
        log('info', 'Bin', `Skipping ${binLink.package} because the _bin is equal to false`)
      }
    } else {
      log('info', 'Bin', `Skipping ${binLink.package} because there was more than one _bin value`)
    }
  }
}

// main process
async function installSoftware(pkgsToInstall) {
  osType = await OSType()
  osID = osType
  if (osType === 'linux') {
    osID = await releaseID()
  }
  log('info', 'Catalog Download', `Fetching the latest version of the installation map`)
  installData = await downloadInstallData()
  chezmoiData = await getChezmoiData()
  log('info', 'Filter', `Calculating the install orders`)
  await generateInstallOrders(pkgsToInstall ? pkgsToInstall : process.argv.slice(3))
  const packageManagers = Object.keys(installOrders)
  packageManagers.length && log('info', 'Pre-Reqs', `Ensuring any package managers that will be used are installed / configured`)
  for (const packageManager of packageManagers) {
    await ensurePackageManager(packageManager)
  }
  try {
    for (const key in installOrders) {
      installOrders[key] = [...new Set(installOrders[key])]
    }
    log('info', 'Install', `The install orders were generated:`)
  } catch (e) {
    log('error', 'Filter', `There was an error reducing the duplicates in the install orders`)
    console.error(e)
  }
  installOrders = await pruneInstallOrders(installOrders)
  delete installOrders._deps
  console.log('Install orders:', installOrders)
  packageManagers.length && log('info', 'Pre-Reqs', `Running package manager pre-installation steps`)
  for (const packageManager of packageManagers) {
    await beforeInstall(packageManager)
  }
  installOrdersPre.length && log('info', 'Pre-Install', `Running package-specific pre-installation steps`)
  for (const script of installOrdersPre) {
    const timeoutAvail = which.sync('timeout', { nothrow: true })
    if (timeoutAvail) {
      await $`${['timeout', '1000', 'bash', '-c', script]}`
    } else {
      await $`${['bash', '-c', script]}`
    }
  }
  installOrdersGroups.length && log('info', 'Users / Groups', `Adding groups / users`)
  for (const group of installOrdersGroups) {
    await addUserGroup(group)
  }
  packageManagers.length && log('info', 'Install', `Installing the packages`)
  for (const packageManager of packageManagers) {
    const asyncOrders = []
    asyncOrders.push(installPackageList(packageManager, installOrders[packageManager]))
    await Promise.all(asyncOrders)
  }
  installOrdersPorts.length && log('info', 'Firewall', 'Configuring firewall exceptions')
  for (const firewallRule of installOrdersPorts) {
    await addFirewallRule(firewallRule)
  }
  installOrdersService.length && log('info', 'Post-Install', `Running package-specific post-installation steps`)
  for (const service of installOrdersService) {
    await updateService(service)
  }
  if (!binLinkRan) {
    binLinkRan = true
    log('info', 'Bin', 'Linking bin aliases to their installed packages')
    await linkBin(installOrdersBinLink)
  }
  for (const script of installOrdersPost) {
    try {
      log('info', 'Post Hook', script)
      const timeoutAvail = which.sync('timeout', { nothrow: true })
      if (timeoutAvail) {
        await $`${['timeout', '1000', 'bash', '-c', script]}`
      } else {
        await $`${['bash', '-c', script]}`
      }
    } catch(e) {
      log('info', 'Post-Install Hook', 'Encountered error while running post-install hook')
    }
  }
  installOrdersPlugins.length && log('info', 'Plugin', 'Installing package-specific plugins')
  for (const plugin of installOrdersPlugins) {
    await installPlugins(plugin)
  }
  packageManagers.length && log('info', 'Post-Install', `Running package manager post-installation steps`)
  for (const packageManager of packageManagers) {
    await afterInstall(packageManager)
  }
  log('success', 'Complete', `Done!`)
}

// Start the main process
await installSoftware(false)