#!/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;
  if (type === "info") {
    icon = chalk.cyanBright(figures.pointer);
    message = chalk.gray.bold(msg);
  } else if (type === "star") {
    icon = chalk.yellowBright(figures.star);
    message = chalk.bold(msg);
  } else if (type === "success") {
    icon = chalk.greenBright(figures.play);
    message = chalk.bold(msg);
  } else if (type === "warn") {
    icon = `${chalk.yellowBright(
      figures.lozenge
    )} ${chalk.bold.black.bgYellowBright(" WARNING ")}`;
    message = chalk.yellowBright(msg);
  } else if (type === "error") {
    icon = `${chalk.redBright(figures.cross)} ${chalk.black.bold.bgRedBright(
      " ERROR "
    )}`;
    message = chalk.redBright(msg);
  }
  const outputMessage = `${icon} ${chalk.bold(label)} ${message}`;
  console.log(outputMessage);
}

let installData;
const installOrders = {};
const installOrdersPre = [];
const installOrdersPost = [];
const installOrdersSystemd = [];
let brewUpdated, osType, osID, snapRefreshed;

// Download the installation map
async function downloadInstallData() {
  const response = await fetch(
    "https://gitlab.com/megabyte-labs/misc/dotfiles/-/raw/master/.local/share/chezmoi/software.yml"
  );
  if (response.ok) {
    const text = await response.text();
    return YAML.parse(text);
  } 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");
    return YAML.parse(text);
  }
}

// Creates the installOrders object which maps package managers to arrays of packages to install
async function generateInstallOrders(pkgsToInstall) {
  const logStage = "Install Orders";
  const packagesToInstall = pkgsToInstall;
  const installerPreference = await OSTypeInstallerKey();
  log(
    "info",
    logStage,
    `Installer preference category detected as ${installerPreference}`
  );
  const preferenceOrder = installData.installerPreference[installerPreference];
  log("info", logStage, `Preference order acquired:`);
  console.log(preferenceOrder);
  const softwarePackages = installData.softwarePackages;
  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`
      );
      continue;
    }
    for (let preference of preferenceOrder) {
      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 $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;
            }
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _when:${pref} condition`
            );
            continue pkgFor;
          }
        } else if (scopedPkgManager) {
          try {
            await $scopedPkgManager;
          } catch (e) {
            const pref = preference;
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _when:${pref} condition`
            );
            continue pkgFor;
          }
        } else if (scopedSystem) {
          try {
            await $scopedSystem;
          } catch (e) {
            let pref;
            if (
              softwarePackages[packageKey]["_" + currentSelector + ":" + osID]
            ) {
              pref = osID;
            } else if (
              softwarePackages[packageKey]["_" + currentSelector + ":" + osType]
            ) {
              pref = osType;
            }
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _when:${pref} condition`
            );
            continue pkgFor;
          }
        } else if (normalCheck) {
          try {
            await $(normalCheck);
          } catch (e) {
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _when condition`
            );
            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 = which.sync(doubleScoped, { nothrow: true });
          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;
            }
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _bin:${pref} condition`
            );
            log("info", "Skipping Package", `${bin} is already in the PATH`);
            continue pkgFor;
          }
        } else if (scopedPkgManager) {
          const bin = which.sync(scopedPkgManager, { nothrow: true });
          if (bin) {
            const pref = preference;
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _bin:${pref} condition`
            );
            log("info", "Skipping Package", `${bin} is already in the PATH`);
            continue pkgFor;
          }
        } else if (scopedSystem) {
          const bin = which.sync(scopedSystem, { nothrow: true });
          if (bin) {
            let pref;
            if (
              softwarePackages[packageKey]["_" + currentSelector + ":" + osID]
            ) {
              pref = osID;
            } else if (
              softwarePackages[packageKey]["_" + currentSelector + ":" + osType]
            ) {
              pref = osType;
            }
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _bin:${pref} condition`
            );
            log("info", "Skipping Package", `${bin} is already in the PATH`);
            continue pkgFor;
          }
        } else if (normalCheck) {
          const bin = which.sync(normalCheck, { nothrow: true });
          if (bin) {
            log(
              "info",
              "Skipping Package",
              `${pkg} is being skipped because of the _bin condition`
            );
            log("info", "Skipping Package", `${bin} is already in the PATH`);
            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];
        if (doubleScoped) {
          let pref;
          if (
            softwarePackages[packageKey][
              "_" + currentSelector + ":" + preference + ":" + osID
            ]
          ) {
            pref = preference + ":" + osID;
            log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
            await installSoftware(softwarePackages[packageKey][pref])
          } else if (
            softwarePackages[packageKey][
              "_" + currentSelector + ":" + preference + ":" + osType
            ]
          ) {
            pref = preference + ":" + osType;
            log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
            await installSoftware(softwarePackages[packageKey][pref])
          } else if (
            softwarePackages[packageKey][
              "_" + currentSelector + ":" + osID + ":" + preference
            ]
          ) {
            pref = osID + ":" + preference;
            log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
            await installSoftware(softwarePackages[packageKey][pref])
          } else if (
            softwarePackages[packageKey][
              "_" + currentSelector + ":" + osType + ":" + preference
            ]
          ) {
            pref = osType + ":" + preference;
            log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
            await installSoftware(softwarePackages[packageKey][pref])
          }
        } else if (scopedPkgManager) {
          const pref = preference;
          log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
          await installSoftware(softwarePackages[packageKey][pref])
        } else if (scopedSystem) {
          let pref;
          if (
            softwarePackages[packageKey]["_" + currentSelector + ":" + osID]
          ) {
            pref = osID;
            log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
            await installSoftware(softwarePackages[packageKey][pref])
          } else if (
            softwarePackages[packageKey]["_" + currentSelector + ":" + osType]
          ) {
            pref = osType;
            log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.${pref}`);
            await installSoftware(softwarePackages[packageKey][pref])
          }
        } else if (normalCheck) {
          log("info", "Installing Dependencies", `Installing dependencies for ${packageKey}.deps`);
          await installSoftware(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;
        }
      }
    }
  }
  return installOrders;
}

// 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.concat(typeof preHook === "string" ? [preHook] : preHook);
  }
  const postHook = getHook(packages, "post", scopedPreference, preference);
  if (postHook) {
    installOrdersPost.concat(
      typeof postHook === "string" ? [postHook] : postHook
    );
  }
  const systemdHook = getHook(packages, "systemd", scopedPreference, preference)
  if (systemdHook) {
    installOrdersSystemd.concat(
      typeof systemdHook === "string" ? [systemdHook] : systemdHook
    );
  }
  if (!installOrders[preference]) {
    installOrders[preference] = [];
  }
  log(
    "info",
    "Install Orders",
    `Found a match for the package \`${pkg}\` (${packageKey} via ${scopedPreference})`
  );
  const newPackages = packages[scopedPreference];
  const newPkgs = typeof newPackages === "string" ? [newPackages] : newPackages;
  if (preference === 'snap' && softwarePackages["_snapClassic"] === true) {
    if (!installOrders[preference + '-classic']) {
      installOrders[preference + '-classic'] = [];
    }
    installOrders[preference + '-classic'] = installOrders[preference].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 "apt";
    } else if (dnf || yum) {
      return "dnf";
    } else if (pacman) {
      return "pacman";
    } else if (zypper) {
      return "zypper";
    } else if (freebsdPkg && freebsdVersion) {
      return "freebsd";
    } else {
      try {
        await $`test -d /Applications && test -d /Library`;
        return "darwin";
      } catch (e) {
        return "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() {
  try {
    await $`test -d /Applications && test -d /Library`;
    return "darwin";
  } catch (e) {
    try {
      await $`test -f /etc/os-release`;
      return "linux";
    } catch (e) {
      return "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") {
    log(
      "info",
      logStage,
      `Ensuring temporary passwordless sudo privileges used by Ansible are removed`
    );
    const gsed = which.sync("gsed", { nothrow: true });
    if (gsed) {
      await $`sudo gsed -i '/# TEMPORARY FOR ANSIBLE INSTALL/d' /etc/sudoers`;
    } else {
      await $`sudo sed -i '/# TEMPORARY FOR ANSIBLE INSTALL/d' /etc/sudoers`;
    }
  } else if (packageManager === "apk") {
  } else if (packageManager === "apt") {
    try {
      await $`sudo apt-get autoclean`
      await $`sudo apt-get autoremove`
    } catch (e) {
      log("error", logStage, 'Error cleaning up apt')
    }
  } else if (packageManager === "basher") {
  } else if (packageManager === "binary") {
  } else if (packageManager === "brew" || packageManager === "cask") {
  } 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", { 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) {
        $`sudo apk add ${dep}`;
      } else if (apt) {
        if (updateDone[packageManager] !== true) {
          await beforeInstall('apt-get')
        }
        await $`sudo apt-get install -y ${dep}`;
      } else if (dnf) {
        if (updateDone[packageManager] !== true) {
          await beforeInstall('dnf')
        }
        await $`sudo dnf install -y ${dep}`;
      } else if (yum) {
        if (updateDone[packageManager] !== true) {
          await beforeInstall('dnf')
        }
        await $`sudo yum install -y ${dep}`;
      } else if (pacman) {
        if (updateDone[packageManager] !== true) {
          await beforeInstall('pacman')
        }
        await $`sudo pacman -Sy ${dep}`;
      } else if (zypper) {
        if (updateDone[packageManager] !== true) {
          await beforeInstall('zypper')
        }
      await $`sudo zypper install -y ${dep}`;
      }
    } else if (osType === 'darwin') {
      if (updateDone['brew'] !== true) {
        await beforeInstall('brew')
      }
      await $`brew install ${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") {
  } 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" | 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'
    }
    await $`${unbufferPrefix} ansible 127.0.0.1 -vv -e '{ ansible_connection: "local", ansible_user: "${process.env.USER}", install_homebrew: False }' -m setup`;
  } else if (packageManager === "apk") {
    await $`sudo apk update`;
  } else if (packageManager === "apt") {
    await $`sudo apt-get update`;
  } else if (packageManager === "basher") {
  } else if (packageManager === "binary") {
  } else if (packageManager === "brew" || packageManager === "cask") {
    if (!brewUpdated) {
      brewUpdated = true
      await $`brew update`;
    }
  } 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) {
      await $`dnf check-update`;
    } else if (yum) {
      await $`yum check-update`;
    }
  } else if (packageManager === "flatpak") {
    await $`sudo flatpak update`;
  } else if (packageManager === "gem") {
  } else if (packageManager === "go") {
  } else if (packageManager === "nix") {
    await $`nix-channel --update`;
  } else if (packageManager === "npm") {
  } else if (packageManager === "pacman") {
    await $`sudo pacman -Syu`;
  } else if (packageManager === "pipx") {
  } else if (packageManager === "pkg") {
    await $`sudo pkg upgrade`;
  } else if (packageManager === "port") {
    const port = which.sync("port", { nothrow: true });
    if (port) {
      await $`sudo port sync`;
    } else {
      log(
        "error",
        "Port Not Installed",
        "Skipping sudo port sync step because port is not installed"
      );
    }
  } else if (packageManager === "scoop") {
    await $`scoop update`;
  } else if (packageManager === "snap" || packageManager === "snap-classic") {
    if (!snapRefreshed) {
      snapRefreshed = true
      await $`sudo snap refresh`;
    }
  } else if (packageManager === "whalebrew") {
    if (osType === "darwin") {
      const docker = which.sync("docker", { nothrow: true });
      if (!docker) {
        await $`brew install --cask docker`;
      }
      try {
        await $`docker run --rm hello-world`;
      } 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`,
            $`open /Applications/Docker.app`,
          ];
          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") {
    await $`winget source update`;
  } else if (packageManager === "yay") {
  } else if (packageManager === "zypper") {
    await $`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`;
  await $`pipx inject ansible PyObjC PyObjC-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 = "Package Manager Install";
  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 https://github.com/srevinsaju/zap/releases/download/continuous/zap-amd64 > /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)"
        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="$?"
          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") {
    await ensureInstalled(
      "cargo",
      $`
      # TODO Bash script that installs 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", { 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) {
        $`sudo apk add flatpak`;
      } else if (apt) {
        $`
          sudo apt install -y flatpak
          if [ -f /usr/bin/gnome-shell ]; then
            sudo apt install -y gnome-software-plugin-flatpak
          fi
          if [ -f /usr/bin/plasmashell ]; then
            sudo apt install -y plasmashell
          fi
        `;
      } 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`;
      }
      const flatpakPost = which.sync("flatpak", { nothrow: true });
      if (flatpakPost) {
        await $`flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo`;
      } else {
        log("error", logStage, `\`flatpak\` failed to install!`);
      }
      log(
        "info",
        logStage,
        `\`flatpak\` was installed. It may require a reboot to function correctly.`
      );
    }
  } else if (packageManager === "gem") {
    await ensureInstalled("gem", $`brew install ruby`);
  } else if (packageManager === "go") {
    await ensureInstalled("gem", $`brew install 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("npm", { nothrow: true });
    const node = which("node", { nothrow: true });
    const volta = which("volta", { nothrow: true });
    if (npm && node && volta) {
      log("info", logStage, `\`npm\`, \`node\`, and \`volta\` are available`);
    } else {
      if (!volta) {
        await $`brew install volta`;
      }
      await $`
        if [ -z "$VOLTA_HOME" ]; then
          volta setup
        fi
        export PATH="$VOLTA_HOME/bin:$PATH"
        volta install node
      `;
    }
  } else if (packageManager === "pacman") {
    await ensureInstalled("pacman", false);
  } else if (packageManager === "pipx") {
    await ensureInstalled("pipx", $`brew install pipx && 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) {
      await $`
          if [ -f /etc/apt/preferences.d/nosnap.pref ]; then
            sudo mv /etc/apt/preferences.d/nosnap.pref /etc/apt/nosnap.pref.bak
          fi
          sudo apt install -y snapd
        `;
      // TODO Following may be required on Kali -> https://snapcraft.io/docs/installing-snap-on-kali
      // systemctl enable --now snapd apparmor
    } else if (dnf) {
      await $`
          sudo dnf install -y snapd
          if [ ! -d /snap ]; then
            sudo ln -s /var/lib/snapd/snap /snap
          fi
        `;
    } else if (yum) {
      await $`
          sudo yum install -y snapd
          sudo systemctl enable --now snapd.socket
          if [ ! -d /snap ]; then
            sudo ln -s /var/lib/snapd/snap /snap
          fi
        `;
    } else if (pacman) {
      await $`
          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) {
      $`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 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, 3) === 'http' && pkg.slice(-8) === 'AppImage') {
            log("info", "AppImage Install", `Installing ${pkg} from its URL`)
            await $`zap install --from ${pkg}`
          } else if (pkg.includes("/")) {
            log("info", "AppImage Install", `Installing ${pkg} from a GitHub Release`)
            await $`zap install --github --from ${pkg}`
          } else {
            log("info", "AppImage Install", `Installing ${pkg} using the AppImage Catalog`)
            await $`zap install ${pkg}`
          }
        } catch (e) {
          log("error", "AppImage / Zap Failure", `There was an error using Zap to install ${pkg}`)
        }
      }
    } 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 === 'on' ? 'vv' : ''
          await $`${unbufferPrefix} ansible 127.0.0.1 -v${verboseMode} -e '{ ansible_connection: "local", ansible_user: "${process.env.USER}", install_homebrew: False }' -m include_role -a name=${pkg}`;
        } catch (e) {
          log(
            "error",
            "Ansible Role Failure",
            `There was an error installing ${pkg} with Ansible`
          );
        }
      }
    } else if (packageManager === "apk") {
      for (let pkg of packages) {
        try {
          await $`sudo apk add ${pkg}`;
        } catch (e) {
          log(
            "error",
            "APK Install Failure",
            `There was an error installing ${pkg} with apk`
          );
        }
      }
    } else if (packageManager === "apt") {
      for (let pkg of packages) {
        try {
          await $`sudo apt-get install -y ${pkg}`;
        } catch (e) {
          log(
            "error",
            "apt-get Failure",
            `There was an error installing ${pkg} with apt-get`
          );
        }
      }
    } else if (packageManager === "basher") {
      for (let pkg of packages) {
        try {
          await $`basher install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Basher Failure",
            `There was an error installing ${pkg} with basher`
          );
        }
      }
    } else if (packageManager === "binary") {
      for (let pkg of packages) {
        try {
          await $`TMP="$(mktemp)" && curl -sSL ${pkg} > "$TMP" && sudo mv "$TMP" /usr/local/src/${binName} && chmod +x /usr/local/src/${binName}`
        } catch (e) {
          log("error", "Binary Release Install", `There was an error installing the binary release for ${pkg}`)
        }
      }
    } else if (packageManager === "brew") {
      for (let pkg of packages) {
        try {
          await $`brew install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Homebrew Failure",
            `There was an error installing ${pkg} with brew`
          );
        }
      }
    } else if (packageManager === "cask") {
      for (let pkg of packages) {
        try {
          await $`brew install --cask ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Homebrew Cask Failure",
            `There was an error installing ${pkg} with Homebrew Cask`
          );
        }
      }
    } else if (packageManager === "cargo") {
      for (const pkg of packages) {
        try {
          await $`cargo install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Cargo Failure",
            `There was an error installing ${pkg} with Cargo`
          );
        }
      }
    } else if (packageManager === "choco") {
      for (let pkg of packages) {
        try {
          await $`choco install -y ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Chocolatey Failure",
            `There was an error installing ${pkg} with Chocolatey`
          );
        }
      }
    } else if (packageManager === "crew") {
    } else if (packageManager === "dnf") {
      const dnf = which.sync("dnf", { nothrow: true });
      const yum = which.sync("yum", { nothrow: true });
      if (dnf) {
        for (let pkg of packages) {
          try {
            await $`sudo dnf install -y ${pkg}`;
          } catch (e) {
            log(
              "error",
              "dnf Failure",
              `There was an error installing ${pkg} with dnf`
            );
          }
        }
      } else if (yum) {
        for (let pkg of packages) {
          try {
            await $`sudo yum install -y ${pkg}`;
          } catch (e) {
            log(
              "error",
              "yum Failure",
              `There was an error installing ${pkg} with yum`
            );
          }
        }
      }
    } else if (packageManager === "flatpak") {
      for (let pkg of packages) {
        try {
          await $`sudo flatpak install flathub ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Flatpak Failure",
            `There was an error installing ${pkg} with flatpak`
          );
        }
      }
    } else if (packageManager === "gem") {
      for (let pkg of packages) {
        try {
          await $`gem install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Gem Failure",
            `There was an error installing ${pkg} with gem`
          );
        }
      }
    } else if (packageManager === "go") {
      for (let pkg of packages) {
        try {
          await $`go install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Go Failure",
            `There was an error installing ${pkg} with go`
          );
        }
      }
    } else if (packageManager === "nix") {
    } else if (packageManager === "npm") {
      for (let pkg of packages) {
        try {
          await $`volta install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Volta Failure",
            `There was an error installing ${pkg} with volta`
          );
        }
      }
    } else if (packageManager === "pacman") {
      for (let pkg of packages) {
        try {
          await $`sudo pacman -Sy --noconfirm --needed ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Pacman Failure",
            `There was an error installing ${pkg} with pacman`
          );
        }
      }
    } else if (packageManager === "pipx") {
      for (let pkg of packages) {
        try {
          await $`pipx install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "PIPX Failure",
            `There was an error installing ${pkg} with pipx`
          );
        }
      }
    } else if (packageManager === "pkg") {
    } else if (packageManager === "port") {
      const port = which.sync("port", { nothrow: true });
      if (port) {
        for (let pkg of packages) {
          try {
            await $`sudo port install ${pkg}`;
          } catch (e) {
            log(
              "error",
              "Port Failure",
              `There was an error installing ${pkg} with port`
            );
          }
        }
      } 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 {
          await $`scoop install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Scoop Failure",
            `There was an error installing ${pkg} with scoop`
          );
        }
      }
    } else if (packageManager === "snap") {
      for (let pkg of packages) {
        // TODO _snapClassic
        try {
          await $`sudo snap install -y ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Snap Failure",
            `There was an error installing ${pkg} with snap`
          );
        }
      }
    } else if (packageManager === "script") {
      for (let pkg of packages) {
        try {
          await $`${pkg}`
        } catch(e) {
          log("error", "Script Install Failure", `There was an error running the script installation method for ${pkg}`)
        }
      }
    } else if (packageManager === "snap-classic") {
      for (let pkg of packages) {
        try {
          await $`sudo snap install --classic -y ${pkg}`
        } catch(e) {
          log("error", "Snap Failure", `There was an error installing ${pkg} with snap in classic mode`)
        }
      }
    } else if (packageManager === "whalebrew") {
      for (let pkg of packages) {
        try {
          await $`whalebrew install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Whalebrew Failure",
            `There was an error installing ${pkg} with whalebrew`
          );
        }
      }
    } else if (packageManager === "winget") {
      for (let pkg of packages) {
        try {
          await $`winget install ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Winget Failure",
            `There was an error installing ${pkg} with winget`
          );
        }
      }
    } else if (packageManager === "yay") {
      for (let pkg of packages) {
        try {
          await $`yay -Sy --noconfirm --needed ${pkg}`;
        } catch (e) {
          log(
            "error",
            "Yay Failure",
            `There was an error installing ${pkg} with yay`
          );
        }
      }
    } else if (packageManager === "zypper") {
      for (let pkg of packages) {
        try {
          await $`sudo zypper install -y ${packages}`;
        } catch (e) {
          log(
            "error",
            "Zypper Failure",
            `There was an error installing ${pkg} with zypper`
          );
        }
      }
    }
  } catch (e) {
    log(
      "error",
      logStage,
      `Possibly encountered an error while installing via \`${packageManager}\``
    );
    log("info", logStage, `Proceeding with the installation..`);
  }
}

async function updateSystemd(service) {
  const logStage = 'Systemd Service'
  if(osType === 'linux') {
    const systemd = which.sync('systemctl', { nothrow: true })
    if (systemd) {
      try {
        log("info", logStage, `Starting / enabling the ${service} service`)
        await $`sudo systemctl enable --now ${service}`
      } catch (e) {
        log("error", logStage, `There was an error starting / enabling the ${service} service`)
        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 {
        log("info", logStage, `Starting / enabling the ${service} service with Homebrew`)
        await $`brew services start ${service}`
      } 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`)
    }
  }
}

// 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();
  log("info", "Install Orders", `Calculating the install orders`);
  await generateInstallOrders(pkgsToInstall ? pkgsToInstall : process.argv.slice(3));
  log(
    "info",
    "Ensure Package Manager Installed",
    `Ensuring any package managers that will be used are installed / configured`
  );
  const packageManagers = Object.keys(installOrders);
  for (const packageManager of packageManagers) {
    await ensurePackageManager(packageManager);
  }
  try {
    for (const key in installOrders) {
      console.log(key)
      installOrders[key] = [...new Set(installOrders[key])];
    }
    log("info", "Install Orders", `The install orders were generated:`);
  } catch (e) {
    log("error", "Install Orders", `There was an error reducing the duplicates in the install orders`)
    console.error(e)
  }
  console.log(installOrders);
  log(
    "info",
    "Package Manager Pre-Install",
    `Running package manager pre-installation steps`
  );
  for (const packageManager of packageManagers) {
    await beforeInstall(packageManager);
  }
  log(
    "info",
    "Package Pre-Install",
    `Running package-specific pre-installation steps`
  );
  for (const script of installOrdersPre) {
    await $`${script}`;
  }
  log("info", "Package Install", `Installing the packages`);
  for (const packageManager of packageManagers) {
    const asyncOrders = [];
    asyncOrders.push(
      installPackageList(packageManager, installOrders[packageManager])
    );
    await Promise.all(asyncOrders);
  }
  log(
    "info",
    "Package Post-Install",
    `Running package-specific post-installation steps`
  );
  for (const service of installOrdersSystemd) {
    await updateSystemd(service);
  }
  for (const script of installOrdersPost) {
    await $`${script}`;
  }
  log(
    "info",
    "Package Manager Post-Install",
    `Running package manager post-installation steps`
  );
  for (const packageManager of packageManagers) {
    await afterInstall(packageManager);
  }
  log("success", "Installation Complete", `Done!`);
}

// Start the main process
await installSoftware(false);