#!/usr/bin/env bash
#
# CloudRadial AutomationAI - runner prerequisite installer (Linux)
#
# Installs every local tool a Linux workstation needs to install, scale, and
# upgrade an AutomationAI runner:
#
#   - PowerShell 7           (Microsoft package repository on x86_64; the
#                             official binary archive on arm64/arm32, which is
#                             all Microsoft publishes for those)
#   - Azure CLI              (Microsoft package repository)
#   - Bicep CLI              (official Microsoft binary -> /usr/local/bin/bicep)
#   - Az PowerShell modules  (Az.Accounts, Az.Resources, Az.KeyVault,
#                             Az.Websites, Az.CognitiveServices)
#
# Supports Debian/Ubuntu (apt) and RHEL/CentOS Stream/Fedora (dnf). Anything
# already present is left alone unless --force is passed. Safe to re-run. It
# does NOT sign you in to Azure and does NOT deploy anything.
#
# Usage:
#   chmod +x ./install-runner-prereqs-linux.sh
#   ./install-runner-prereqs-linux.sh [--skip-az-modules] [--force]
#
#   --skip-az-modules  Install the command-line tools only. The runner installer
#                      auto-installs the Az submodules it needs on first run.
#   --force            Reinstall/upgrade even when a tool is already present.
#
# Most steps use sudo and will prompt for your password.

set -euo pipefail

SKIP_AZ_MODULES=0
FORCE=0

# Az submodules the runner install/upgrade scripts actually load. Keep in step
# with infra/runner-sample/Deploy-Runner.ps1.
AZ_MODULES="'Az.Accounts','Az.Resources','Az.KeyVault','Az.Websites','Az.CognitiveServices'"

BICEP_INSTALL_PATH="/usr/local/bin/bicep"

# Where an arm64/arm32 tarball install of PowerShell lands. /usr/local/bin is
# the right place for a hand-installed binary and is on the default PATH of
# every supported distro; Microsoft's own docs symlink /usr/bin/pwsh, which
# would collide with the distro package on a machine that later gets one.
PWSH_PREFIX="/opt/microsoft/powershell"
PWSH_SYMLINK="/usr/local/bin/pwsh"

while [ $# -gt 0 ]; do
  case "$1" in
    --skip-az-modules) SKIP_AZ_MODULES=1 ;;
    --force)           FORCE=1 ;;
    -h|--help)
      # Print the header comment block and stop at the first non-comment line,
      # so the help text can't drift out of sync with a hard-coded line range.
      awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      echo "Usage: $0 [--skip-az-modules] [--force]" >&2
      exit 2
      ;;
  esac
  shift
done

step() { printf '\n==== %s ====\n' "$1"; }
ok()   { printf '  (OK) %s\n' "$1"; }
warn() { printf '  (!!) %s\n' "$1"; }
die()  { printf '\n%s\n' "$1" >&2; exit 1; }

have() { command -v "$1" >/dev/null 2>&1; }

# Prints the SHA-256 of $1 as lowercase hex, or nothing if no digest tool exists.
sha256_of() {
  if   have sha256sum; then sha256sum "$1" | awk '{print $1}'
  elif have shasum;    then shasum -a 256 "$1" | awk '{print $1}'
  elif have openssl;   then openssl dgst -sha256 "$1" | awk '{print $NF}'
  fi
}

printf '\nCloudRadial AutomationAI - runner prerequisite installer (Linux)\n'
printf -- '---------------------------------------------------------------\n'

if [ "$(uname -s)" != "Linux" ]; then
  die "This script is for Linux. On macOS use install-runner-prereqs-macos.sh; on Windows use Install-RunnerPrereqs.ps1."
fi

if have apt-get; then
  PKG=apt
elif have dnf; then
  PKG=dnf
else
  die "No supported package manager found (need apt-get or dnf). Install the tools manually: https://aka.ms/powershell-release, https://aka.ms/azcli, https://aka.ms/bicep-install"
fi

DISTRO_ID="unknown"; DISTRO_VERSION="unknown"
if [ -r /etc/os-release ]; then
  # shellcheck disable=SC1091
  . /etc/os-release
  DISTRO_ID="${ID:-unknown}"
  DISTRO_VERSION="${VERSION_ID:-unknown}"
fi
printf 'Detected %s %s on %s (package manager: %s).\n' "$DISTRO_ID" "$DISTRO_VERSION" "$(uname -m)" "$PKG"

have sudo || die "sudo is required but not installed."

# Every download below uses curl, including on the pwsh-already-installed path
# that never reaches add_microsoft_repo. Minimal Debian/Ubuntu images ship
# without it. This script already installs packages, so bootstrap curl here
# rather than sending the operator away to install one package by hand.
if ! have curl; then
  warn "curl is not installed - installing it with $PKG first."
  if [ "$PKG" = "apt" ]; then
    if ! { sudo apt-get update && sudo apt-get install -y curl; }; then
      die "Could not install curl with apt-get. Install it manually ('sudo apt-get install -y curl'), then re-run this script."
    fi
  else
    if ! sudo dnf install -y curl; then
      die "Could not install curl with dnf. Install it manually ('sudo dnf install -y curl'), then re-run this script."
    fi
  fi
  hash -r 2>/dev/null || true
  if ! have curl; then
    die "curl still is not on the PATH after installing it with $PKG. Install it manually, then re-run this script."
  fi
  ok "curl installed."
fi

# Adds the Microsoft package repository once, for either package manager.
MS_REPO_ADDED=0
add_microsoft_repo() {
  [ "$MS_REPO_ADDED" -eq 1 ] && return 0

  if [ "$PKG" = "apt" ]; then
    printf '  Adding the Microsoft package repository...\n'
    sudo apt-get update
    sudo apt-get install -y curl wget apt-transport-https software-properties-common ca-certificates

    REPO_DISTRO="$DISTRO_ID"
    REPO_VERSION="$DISTRO_VERSION"
    # Debian and derivatives that are not Ubuntu use the debian config path.
    case "$REPO_DISTRO" in
      ubuntu|debian) : ;;
      linuxmint|pop) REPO_DISTRO="ubuntu"; REPO_VERSION="${UBUNTU_CODENAME:-$REPO_VERSION}" ;;
      *) REPO_DISTRO="ubuntu" ;;
    esac

    DEB_TMP="$(mktemp -d)"
    DEB_URL="https://packages.microsoft.com/config/${REPO_DISTRO}/${REPO_VERSION}/packages-microsoft-prod.deb"
    printf '  Fetching %s\n' "$DEB_URL"
    if ! curl -fsSL -o "$DEB_TMP/packages-microsoft-prod.deb" "$DEB_URL"; then
      rm -rf "$DEB_TMP"
      die "Could not fetch the Microsoft repository package for ${REPO_DISTRO} ${REPO_VERSION}.
Your distribution version may not be published at packages.microsoft.com.
Install PowerShell and the Azure CLI manually: https://aka.ms/powershell-release and https://aka.ms/azcli"
    fi
    sudo dpkg -i "$DEB_TMP/packages-microsoft-prod.deb"
    rm -rf "$DEB_TMP"
    sudo apt-get update
  else
    printf '  Adding the Microsoft package repository...\n'
    sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
    RPM_VERSION="${DISTRO_VERSION%%.*}"
    case "$DISTRO_ID" in
      fedora) RPM_URL="https://packages.microsoft.com/config/fedora/${RPM_VERSION}/packages-microsoft-prod.rpm" ;;
      *)      RPM_URL="https://packages.microsoft.com/config/rhel/${RPM_VERSION}/packages-microsoft-prod.rpm" ;;
    esac
    printf '  Using %s\n' "$RPM_URL"
    if ! sudo dnf install -y "$RPM_URL"; then
      die "Could not install the Microsoft repository package from $RPM_URL.
Your distribution version may not be published at packages.microsoft.com.
Install PowerShell and the Azure CLI manually: https://aka.ms/powershell-release and https://aka.ms/azcli"
    fi
  fi

  MS_REPO_ADDED=1
}

# Installs PowerShell 7 from the official Microsoft binary archive, for the
# architectures the package feeds do not serve. $1 is the PowerShell asset
# architecture ("arm64" or "arm32"). Returns non-zero instead of dying so the
# run continues to the Azure CLI, Bicep and the verification table.
install_pwsh_from_tarball() {
  PS_ARCH="$1"

  # The archive is self-contained .NET but still binds the distro's ICU and
  # OpenSSL at runtime, and pwsh will not start without them. Install by
  # DISCOVERED package name, not a pinned one: the sonames are release-specific
  # (libicu74 + libssl3t64 on Ubuntu 24.04, libicu70 + libssl3 on 22.04,
  # libicu72 on Debian 12), so a hard-coded list breaks on the next release.
  # Best-effort - most images already have both, and the smoke test below is
  # the real gate.
  printf '  Installing the runtime libraries PowerShell needs...\n'
  if [ "$PKG" = "apt" ]; then
    sudo apt-get update
    PWSH_DEPS="ca-certificates less locales"
    for PWSH_DEP_PATTERN in 'libicu[0-9]+' 'libssl3(t64)?' 'libunwind[0-9]+'; do
      PWSH_DEP="$(apt-cache pkgnames 2>/dev/null | grep -E "^${PWSH_DEP_PATTERN}\$" | sort -V | tail -1 || true)"
      [ -n "$PWSH_DEP" ] && PWSH_DEPS="$PWSH_DEPS $PWSH_DEP"
    done
    printf '  (%s)\n' "$PWSH_DEPS"
    # shellcheck disable=SC2086
    sudo apt-get install -y $PWSH_DEPS || warn "Could not install every runtime library; continuing."
  else
    sudo dnf install -y libicu openssl-libs || warn "Could not install every runtime library; continuing."
  fi

  # /releases/latest redirects to the newest STABLE release's tag page; read the
  # tag out of the redirect rather than calling api.github.com, which rate-limits
  # unauthenticated callers by IP and would fail behind a busy corporate NAT.
  PWSH_RELEASE_URL="$(curl -fsSL -o /dev/null -w '%{url_effective}' https://github.com/PowerShell/PowerShell/releases/latest || true)"
  PWSH_TAG=""
  case "$PWSH_RELEASE_URL" in
    */releases/tag/v*) PWSH_TAG="${PWSH_RELEASE_URL##*/}" ;;
  esac
  if [ -z "$PWSH_TAG" ]; then
    warn "Could not work out the latest PowerShell release from github.com."
    printf '       Install it by hand from the binary archive, then re-run this script:\n'
    printf '       https://learn.microsoft.com/powershell/scripting/install/install-other-linux\n'
    return 1
  fi

  PWSH_VERSION="${PWSH_TAG#v}"
  PWSH_ASSET="powershell-${PWSH_VERSION}-linux-${PS_ARCH}.tar.gz"
  PWSH_URL="https://github.com/PowerShell/PowerShell/releases/download/${PWSH_TAG}/${PWSH_ASSET}"

  PWSH_TMP="$(mktemp)"
  trap 'rm -f "$PWSH_TMP"' EXIT

  printf '  Downloading %s...\n' "$PWSH_ASSET"
  if ! curl -fsSL -o "$PWSH_TMP" "$PWSH_URL"; then
    rm -f "$PWSH_TMP"
    trap - EXIT
    warn "Could not download $PWSH_URL."
    printf '       Install PowerShell by hand from the binary archive, then re-run this\n'
    printf '       script: https://learn.microsoft.com/powershell/scripting/install/install-other-linux\n'
    return 1
  fi

  # Check the archive against the release's own hashes.sha256 manifest BEFORE it
  # reaches a privileged tar. Two quirks that make this less obvious than it
  # looks: the manifest is UTF-16LE with a BOM and CRLF line endings (a known,
  # recurring PowerShell release-pipeline artifact) which sha256sum cannot read,
  # and its filename column is prefixed with '*'. Deleting NUL/BOM bytes decodes
  # it for both encodings without needing iconv or python on the box.
  #
  # A MISMATCH aborts. Merely being unable to OBTAIN a hash warns and continues:
  # the manifest's encoding and format have changed before, and hard-failing
  # every arm64 install over a manifest quirk would cost more than this check is
  # worth on top of an HTTPS download from github.com.
  PWSH_HASH_TMP="$(mktemp)"
  trap 'rm -f "$PWSH_TMP" "$PWSH_HASH_TMP"' EXIT
  PWSH_EXPECTED=""
  if curl -fsSL -o "$PWSH_HASH_TMP" \
      "https://github.com/PowerShell/PowerShell/releases/download/${PWSH_TAG}/hashes.sha256"; then
    PWSH_EXPECTED="$(tr -d '\000\377\376\r' < "$PWSH_HASH_TMP" \
      | grep -F "$PWSH_ASSET" | awk '{print $1}' | head -1 | tr 'A-F' 'a-f' || true)"
  fi
  PWSH_ACTUAL="$(sha256_of "$PWSH_TMP" | tr 'A-F' 'a-f')"
  if [ -n "$PWSH_EXPECTED" ] && [ -n "$PWSH_ACTUAL" ]; then
    if [ "$PWSH_EXPECTED" != "$PWSH_ACTUAL" ]; then
      rm -f "$PWSH_TMP" "$PWSH_HASH_TMP"
      trap - EXIT
      warn "Checksum mismatch on $PWSH_ASSET - NOT installing it."
      printf '       expected %s\n' "$PWSH_EXPECTED"
      printf '       actual   %s\n' "$PWSH_ACTUAL"
      printf '       Re-run this script to download it again. If it keeps failing, install\n'
      printf '       PowerShell by hand: https://learn.microsoft.com/powershell/scripting/install/install-other-linux\n'
      return 1
    fi
    ok "Checksum of $PWSH_ASSET matches the release manifest."
  else
    warn "Could not verify $PWSH_ASSET against the release manifest; continuing with the HTTPS download."
  fi

  # Every step here is privileged and any of them can fail (a read-only /opt, a
  # truncated archive, no room). This function is called from an || list, which
  # disables 'set -e' inside it, so each command must be checked explicitly -
  # otherwise a failed tar would fall through to the smoke test, an EXISTING
  # pwsh symlink would satisfy it, and a failed install would report success.
  PWSH_DIR="${PWSH_PREFIX}/${PWSH_VERSION}"
  printf '  Installing to %s (requires sudo)...\n' "$PWSH_DIR"
  PWSH_INSTALL_OK=0
  {
    sudo mkdir -p "$PWSH_DIR" &&
    sudo tar zxf "$PWSH_TMP" -C "$PWSH_DIR" &&
    sudo chmod +x "$PWSH_DIR/pwsh" &&
    sudo mkdir -p "$(dirname "$PWSH_SYMLINK")" &&
    sudo ln -sf "$PWSH_DIR/pwsh" "$PWSH_SYMLINK"
  } && PWSH_INSTALL_OK=1

  rm -f "$PWSH_TMP" "$PWSH_HASH_TMP"
  trap - EXIT
  hash -r 2>/dev/null || true

  if [ "$PWSH_INSTALL_OK" -eq 0 ]; then
    warn "Could not unpack PowerShell into $PWSH_DIR."
    printf '       Check that you have sudo rights and free space, then re-run this script,\n'
    printf '       or install PowerShell by hand:\n'
    printf '       https://learn.microsoft.com/powershell/scripting/install/install-other-linux\n'
    return 1
  fi

  # An unpacked-but-unstartable pwsh (a missing ICU, almost always) would
  # otherwise look installed here and fail much later, inside the runner
  # installer. Prove it runs before calling this a success.
  # shellcheck disable=SC2016
  if ! "$PWSH_SYMLINK" -NoProfile -NonInteractive -Command '$PSVersionTable.PSVersion.ToString()' >/dev/null 2>&1; then
    warn "PowerShell unpacked to $PWSH_DIR but will not start."
    printf '       This is almost always a missing ICU or OpenSSL library. Install your\n'
    printf '       distribution'"'"'s libicu and libssl packages, then re-run this script:\n'
    printf '       https://learn.microsoft.com/powershell/scripting/install/install-other-linux\n'
    return 1
  fi

  return 0
}

# ----- PowerShell 7 -----------------------------------------------------------
step "PowerShell 7"

if have pwsh && [ "$FORCE" -eq 0 ]; then
  ok "PowerShell 7 (pwsh) is already installed."
else
  # Microsoft's Linux package feeds carry the 'powershell' package for x86_64
  # ONLY - verified against packages.microsoft.com: neither the Ubuntu prod deb
  # index (binary-arm64) nor the RHEL prod rpm index lists it for anything but
  # amd64/x86_64. The repo entry the config .deb installs still advertises
  # arm64, so apt does not say "no candidate" - it resolves the amd64 package
  # and then dies in dependency resolution ("powershell:amd64 : Depends:
  # libc6:amd64 but it is not installable"), which reads like a broken machine
  # rather than an unsupported architecture. Verified on Ubuntu 24.04 aarch64.
  # So on arm take the official binary archive, which Microsoft does publish
  # for arm64/arm32 - the operator gets the same one-shot install either way.
  ARCH="$(uname -m)"
  PWSH_OK=1
  case "$ARCH" in
    x86_64|amd64)
      add_microsoft_repo
      printf '  Installing PowerShell...\n'
      if [ "$PKG" = "apt" ]; then
        sudo apt-get install -y powershell
      else
        sudo dnf install -y powershell
      fi
      ;;
    aarch64|arm64)
      printf '  %s is not served by the Microsoft package feed - installing PowerShell from\n' "$ARCH"
      printf '  the official binary archive instead.\n'
      install_pwsh_from_tarball arm64 || PWSH_OK=0
      ;;
    armv7l|armv7|armhf)
      printf '  %s is not served by the Microsoft package feed - installing PowerShell from\n' "$ARCH"
      printf '  the official binary archive instead.\n'
      install_pwsh_from_tarball arm32 || PWSH_OK=0
      ;;
    *)
      warn "PowerShell 7 is not published for $ARCH by either the Microsoft package feed or the binary archive."
      printf '       Check https://learn.microsoft.com/powershell/scripting/install/install-other-linux\n'
      PWSH_OK=0
      ;;
  esac
  hash -r 2>/dev/null || true
  if [ "$PWSH_OK" -eq 0 ]; then
    warn "PowerShell 7 was not installed. The verification below shows what is still missing."
  elif have pwsh; then
    ok "PowerShell 7 installed."
  else
    warn "pwsh is still not on the PATH."
  fi
fi

# ----- Azure CLI --------------------------------------------------------------
step "Azure CLI"

if have az && [ "$FORCE" -eq 0 ]; then
  ok "Azure CLI (az) is already installed."
else
  printf '  Installing the Azure CLI...\n'
  if [ "$PKG" = "apt" ]; then
    # Microsoft's maintained installer for Debian/Ubuntu. It configures the
    # azure-cli repository and installs the package. Download it to a file and
    # then run the file, rather than piping the response straight into `sudo
    # bash`: a piped shell starts executing bytes as they arrive, so a truncated
    # or interrupted transfer can run half a script as root. (This does not
    # authenticate the script - it removes the partial-execution hazard.)
    AZ_TMP="$(mktemp)"
    trap 'rm -f "$AZ_TMP"' EXIT
    curl -fsSL -o "$AZ_TMP" https://aka.ms/InstallAzureCLIDeb
    sudo bash "$AZ_TMP"
    rm -f "$AZ_TMP"
    trap - EXIT
  else
    add_microsoft_repo
    sudo dnf install -y azure-cli
  fi
  hash -r 2>/dev/null || true
  if have az; then ok "Azure CLI installed."; else warn "az is still not on the PATH."; fi
fi

# ----- Bicep CLI --------------------------------------------------------------
# The runner deploys through Az PowerShell (New-AzResourceGroupDeployment
# -TemplateFile main.bicep), and Az PowerShell only ever looks for a standalone
# 'bicep' on the PATH. The Azure CLI's private copy (az bicep) does NOT satisfy
# it, so install the standalone binary even on a machine that has az bicep.
step "Bicep CLI"

if have bicep && [ "$FORCE" -eq 0 ]; then
  ok "Bicep CLI (bicep) is already installed at $(command -v bicep)."
else
  IS_MUSL=0
  if have ldd && ldd --version 2>&1 | head -1 | grep -qi musl; then IS_MUSL=1; fi
  if [ -f /etc/alpine-release ]; then IS_MUSL=1; fi

  case "$(uname -m)" in
    x86_64|amd64)
      if [ "$IS_MUSL" -eq 1 ]; then BICEP_ASSET="bicep-linux-musl-x64"; else BICEP_ASSET="bicep-linux-x64"; fi
      ;;
    aarch64|arm64)
      BICEP_ASSET="bicep-linux-arm64"
      ;;
    *)
      die "Unsupported architecture $(uname -m) for the Bicep CLI. Install it manually: https://aka.ms/bicep-install"
      ;;
  esac

  BICEP_TMP="$(mktemp)"
  trap 'rm -f "$BICEP_TMP"' EXIT

  printf '  Downloading %s from the official Bicep release...\n' "$BICEP_ASSET"
  curl -fsSL -o "$BICEP_TMP" "https://github.com/Azure/bicep/releases/latest/download/$BICEP_ASSET"
  chmod +x "$BICEP_TMP"

  printf '  Installing to %s (requires sudo)...\n' "$BICEP_INSTALL_PATH"
  sudo mkdir -p "$(dirname "$BICEP_INSTALL_PATH")"
  sudo cp "$BICEP_TMP" "$BICEP_INSTALL_PATH"
  sudo chmod +x "$BICEP_INSTALL_PATH"

  rm -f "$BICEP_TMP"
  trap - EXIT

  hash -r 2>/dev/null || true
  if have bicep; then ok "Bicep CLI installed."; else warn "bicep is still not on the PATH ($BICEP_INSTALL_PATH may not be in PATH)."; fi
fi

# ----- Az PowerShell modules --------------------------------------------------
step "Az PowerShell modules"

if [ "$SKIP_AZ_MODULES" -eq 1 ]; then
  warn "Skipped (--skip-az-modules). The runner installer will install them on first run."
elif ! have pwsh; then
  warn "pwsh is not available, so the Az modules cannot be installed."
  printf '       Open a new terminal, run pwsh, then: Install-Module Az -Scope CurrentUser\n'
else
  printf '  Installing the Az submodules the runner scripts use (CurrentUser scope).\n'
  printf '  This is a one-time install and can take several minutes.\n'
  # $true/$false are PowerShell literals for the pwsh command below, not bash vars.
  # shellcheck disable=SC2016
  if [ "$FORCE" -eq 1 ]; then FORCE_PS='$true'; else FORCE_PS='$false'; fi
  if pwsh -NoProfile -NonInteractive -Command "
\$ErrorActionPreference = 'Stop'
\$required = @($AZ_MODULES)
\$forceInstall = $FORCE_PS
foreach (\$m in \$required) {
    if (-not \$forceInstall -and (Get-Module -ListAvailable -Name \$m)) {
        Write-Host \"  (OK) \$m already installed.\"
        continue
    }
    Write-Host \"  Installing \$m ...\"
    Install-Module -Name \$m -Scope CurrentUser -Repository PSGallery -Force -AllowClobber -Confirm:\$false -ErrorAction Stop
    Write-Host \"  (OK) \$m installed.\"
}
"; then
    ok "Az PowerShell modules are installed."
  else
    # Never let a PSGallery hiccup abort the run under `set -e`: the operator
    # would lose the verification table AND the remediation text that follows.
    # The runner installer retries the Az submodules on its first run anyway.
    warn "Az module install did not complete. The verification below shows what is still missing."
  fi
fi

# ----- Verification -----------------------------------------------------------
step "Verification"

MISSING=0

if have pwsh; then
  # $PSVersionTable is a PowerShell variable evaluated by pwsh, not by bash.
  # shellcheck disable=SC2016
  printf '  PowerShell 7 (pwsh)   : %s\n' "$(pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' 2>/dev/null || echo 'installed')"
else
  printf '  PowerShell 7 (pwsh)   : MISSING\n'; MISSING=1
fi

if have az; then
  printf '  Azure CLI (az)        : %s\n' "$(az version --output tsv --query '"azure-cli"' 2>/dev/null || echo 'installed')"
else
  printf '  Azure CLI (az)        : MISSING\n'; MISSING=1
fi

if have bicep; then
  printf '  Bicep CLI (bicep)     : %s\n' "$(bicep --version 2>/dev/null | head -1 || echo 'installed')"
else
  printf '  Bicep CLI (bicep)     : MISSING\n'; MISSING=1
fi

if [ "$SKIP_AZ_MODULES" -eq 0 ]; then
  if have pwsh; then
    AZ_MISSING="$(pwsh -NoProfile -NonInteractive -Command "@($AZ_MODULES) | Where-Object { -not (Get-Module -ListAvailable -Name \$_) } | ForEach-Object { \$_ }" 2>/dev/null | tr -d '\r' | tr '\n' ' ' | sed 's/ *$//')"
    if [ -z "$AZ_MISSING" ]; then
      printf '  Az PowerShell modules : all present\n'
    else
      printf '  Az PowerShell modules : MISSING: %s\n' "$AZ_MISSING"; MISSING=1
    fi
  else
    printf '  Az PowerShell modules : MISSING (no pwsh)\n'; MISSING=1
  fi
fi

printf '\n'

if [ "$MISSING" -ne 0 ]; then
  printf 'Some prerequisites are still missing.\n\n'
  printf 'What to do next:\n'
  printf '  1. Open a NEW terminal window. A newly installed tool is not on the PATH of a\n'
  printf '     shell that was already open.\n'
  printf '  2. Re-run this script. It is safe to run repeatedly.\n'
  printf '  3. If a tool still will not install, follow the manual steps in\n'
  printf '     "Runner prerequisites and Azure requirements" on the CloudRadial support site.\n\n'
  exit 1
fi

printf 'All local prerequisites are installed.\n\n'
printf 'Next steps:\n'
printf '  1. Start PowerShell 7:                pwsh\n'
printf '  2. Sign in to Azure (Az PowerShell):  Connect-AzAccount\n'
printf '  3. Extract your runner setup package and run:  ./Install-AutomationsRunner.ps1\n\n'
printf 'Connect-AzAccount is a separate sign-in from "az login" - the runner installer\n'
printf 'uses the Az PowerShell one.\n\n'
