#!/usr/bin/env bash
set -uo pipefail;

####################################
# Ensure we can execute standalone #
####################################

function early_death() {
  echo "[FATAL] ${0}: ${1}" >&2;
  exit 1;
};

if [ -z "${TFENV_ROOT:-""}" ]; then
  # http://stackoverflow.com/questions/1055671/how-can-i-get-the-behavior-of-gnus-readlink-f-on-a-mac
  readlink_f() {
    local target_file="${1}";
    local file_name;

    while [ "${target_file}" != "" ]; do
      cd "$(dirname ${target_file})" || early_death "Failed to 'cd \$(dirname ${target_file})' while trying to determine TFENV_ROOT";
      file_name="$(basename "${target_file}")" || early_death "Failed to 'basename \"${target_file}\"' while trying to determine TFENV_ROOT";
      target_file="$(readlink "${file_name}")";
    done;

    echo "$(pwd -P)/${file_name}";
  };

  TFENV_ROOT="$(cd "$(dirname "$(readlink_f "${0}")")/.." && pwd)";
  [ -n "${TFENV_ROOT}" ] || early_death "Failed to 'cd \"\$(dirname \"\$(readlink_f \"${0}\")\")/..\" && pwd' while trying to determine TFENV_ROOT";
else
  TFENV_ROOT="${TFENV_ROOT%/}";
fi;
export TFENV_ROOT;

if [ -n "${TFENV_HELPERS:-""}" ]; then
  log 'debug' 'TFENV_HELPERS is set, not sourcing helpers again';
else
  [ "${TFENV_DEBUG:-0}" -gt 0 ] && echo "[DEBUG] Sourcing helpers from ${TFENV_ROOT}/lib/helpers.sh";
  if source "${TFENV_ROOT}/lib/helpers.sh"; then
    log 'debug' 'Helpers sourced successfully';
  else
    early_death "Failed to source helpers from ${TFENV_ROOT}/lib/helpers.sh";
  fi;
fi;

# Ensure libexec and bin are in $PATH
for dir in libexec bin; do
  case ":${PATH}:" in
    *:${TFENV_ROOT}/${dir}:*) log 'debug' "\$PATH already contains '${TFENV_ROOT}/${dir}', not adding it again";;
    *)
      log 'debug' "\$PATH does not contain '${TFENV_ROOT}/${dir}', prepending and exporting it now";
      export PATH="${TFENV_ROOT}/${dir}:${PATH}";
      ;;
  esac;
done;

#####################
# Begin Script Body #
#####################

[ "${#}" -gt 1 ] && log 'error' 'usage: tfenv install [<version>]';

declare requested="${1:-""}";

log debug "Resolving version with: tfenv-resolve-version ${requested}";
declare resolved;
resolved="$(tfenv-resolve-version ${requested})" || log 'error' "Failed to resolve ${requested} version";

declare version="${resolved%%\:*}";
declare regex="${resolved##*\:}";

[ -n "${version}" ] || log 'error' 'Version is not specified. This should not be possible as we default to latest';

log 'debug' "Processing install for version ${version}, using regex ${regex}";

# When resolving 'latest' with a version-number-only regex (e.g. latest:^0.11.
# from latest-allowed), exclude pre-release versions (alpha, beta, rc) which
# contain a hyphen. Regexes containing letters (e.g. latest:rc, latest:alpha)
# are intentionally targeting pre-releases and are not filtered.
if [ "${TFENV_SKIP_REMOTE_CHECK:-0}" -eq 0 ]; then
  if [[ "${version}" == "latest" && -n "${regex}" && ! "${regex}" =~ [a-zA-Z] ]]; then
    remote_version="$(tfenv-list-remote | grep -e "${regex}" | grep -v '-' | head -n 1)";
  else
    remote_version="$(tfenv-list-remote | grep -e "${regex}" | head -n 1)";
  fi;
  [ -n "${remote_version}" ] && version="${remote_version}" || log 'error' "No versions matching '${requested:-$version}' found in remote";
fi;

dst_path="${TFENV_CONFIG_DIR}/versions/${version}";
if [ -f "${dst_path}/terraform" ]; then
  echo "Terraform v${version} is already installed";
  exit 0;
fi;

# Acquire a per-version lock to prevent concurrent installs of the same version.
# Uses mkdir as a portable atomic lock (works on Linux, macOS, and Windows/MSYS).
declare lockdir="${TFENV_CONFIG_DIR}/.install-lock-${version}";
declare lock_retries=0;
declare lock_max_retries=60;

cleanup_lock() {
  rmdir "${lockdir}" 2>/dev/null || true;
};

while ! mkdir "${lockdir}" 2>/dev/null; do
  if [ "${lock_retries}" -eq 0 ]; then
    log 'info' "Another process is installing Terraform v${version}. Waiting...";
  fi;
  lock_retries=$((lock_retries + 1));
  if [ "${lock_retries}" -ge "${lock_max_retries}" ]; then
    log 'warn' "Lock wait timeout after ${lock_max_retries}s. Removing stale lock and retrying.";
    rmdir "${lockdir}" 2>/dev/null || true;
    if ! mkdir "${lockdir}" 2>/dev/null; then
      log 'error' "Failed to acquire install lock for Terraform v${version}";
    fi;
    break;
  fi;
  # Check if the version appeared while we were waiting (another process finished)
  if [ -f "${dst_path}/terraform" ]; then
    echo "Terraform v${version} was installed by another process while waiting";
    exit 0;
  fi;
  sleep 1;
done;

# Ensure lock is released on exit (normal, error, or signal)
trap 'cleanup_lock' EXIT;
trap 'cleanup_lock; exit 1' INT TERM;

case "$(uname -s)" in
  Darwin*)
    kernel="darwin";
    ;;
  MINGW64*)
    kernel="windows";
    ;;
  MSYS*NT*)
    kernel="windows";
    ;;
  CYGWIN*NT*)
    kernel="windows";
    ;;
  FreeBSD*)
    kernel="freebsd";
    ;;
  *)
    kernel="linux";
    ;;
esac;

# Add support of ARM64 for Linux & Apple Silicon
case "$(uname -m)" in
  aarch64* | arm64*)
    case "${kernel}" in
      "linux")
        # There is no arm64 support for versions:
        # < 0.11.15
        # >= 0.12.0, < 0.12.30
        # >= 0.13.0, < 0.13.5
        if [[ "${version}" =~ 0\.(([0-9]|10))\.\d* ||
              "${version}" =~ 0\.11\.(([0-9]|1[0-4]))$ ||
              "${version}" =~ 0\.12\.(([0-9]|[1-2][0-9]))$ ||
              "${version}" =~ 0\.13\.[0-4]$
        ]]; then
          TFENV_ARCH="${TFENV_ARCH:-amd64}";
        else
          TFENV_ARCH="${TFENV_ARCH:-arm64}";
        fi;
      ;;
      "darwin")
        # No Apple Silicon builds before 1.0.2
        if [[ "${version}" =~ ^0\.[0-9]+\.[0-9]+ || "${version}" =~ ^1\.0\.[0-1]$
        ]]; then
          TFENV_ARCH="${TFENV_ARCH:-amd64}";
        else
          TFENV_ARCH="${TFENV_ARCH:-arm64}";
        fi;
      ;;
    esac;
    ;;
  *)
    TFENV_ARCH="${TFENV_ARCH:-amd64}";
    ;;
esac;

os="${kernel}_${TFENV_ARCH}"

keybase_bin="$(command -v keybase 2>/dev/null)";
shasum_bin="$(command -v shasum 2>/dev/null)";
sha256sum_bin="$(command -v sha256sum 2>/dev/null)";

TFENV_REMOTE="${TFENV_REMOTE:-https://releases.hashicorp.com}";
version_url="${TFENV_REMOTE}/terraform/${version}";

# Thanks for the inconsistency in 0.12-alpha, Hashicorp(!)
if [[ "${version}" =~ 0.12.0-alpha[3-9] ]]; then
  tarball_name="terraform_${version}_terraform_${version}_${os}.zip";
else
  tarball_name="terraform_${version}_${os}.zip";
fi;

shasums_name="terraform_${version}_SHA256SUMS";
shasums_signing_key_postfix=".72D7468F";
shasums_sig="${shasums_name}${shasums_signing_key_postfix}.sig";

log 'info' "Installing Terraform v${version}";

# Create a local temporary directory for downloads
tmpdir_arg="-t";

if mktemp --help 2>&1 | grep -- '--tmpdir' >/dev/null; then
  tmpdir_arg="--tmpdir";
fi;

download_tmp="$(mktemp -d ${tmpdir_arg} tfenv_download.XXXXXX)" || log 'error' "Unable to create temporary download directory (mktemp -d ${tmpdir_arg} tfenv_download.XXXXXX). Working Directory is: $(pwd)";

# Clean it up in case of error
cleanup_download() { rm -rf "${download_tmp}"; }
trap 'cleanup_download; cleanup_lock' EXIT;
trap 'cleanup_download; cleanup_lock; exit 1' INT TERM;

declare curl_progress="";
case "${TFENV_CURL_OUTPUT:-2}" in
  '2')
    log 'debug' 'Setting curl progress bar with "-#"';
    curl_progress="-#";
    ;;
  '1')
    log 'debug' 'Using default curl output';
    curl_progress="";
    ;;
  '0')
    log 'debug' 'Running curl silently with "-s"';
    curl_progress="-s";
    ;;
  *)
    log 'error' 'TFENV_CURL_OUTPUT specified, but not with a supported value ([0,1,2])';
    ;;
esac;

log 'info' "Downloading release tarball from ${version_url}/${tarball_name}";
curlw ${curl_progress} -f -L -o "${download_tmp}/${tarball_name}" "${version_url}/${tarball_name}" || log 'error' 'Tarball download failed';
log 'info' "Downloading SHA hash file from ${version_url}/${shasums_name}";
curlw -s -f -L -o "${download_tmp}/${shasums_name}" "${version_url}/${shasums_name}" || log 'error' 'SHA256 hashes download failed';

download_signature() {
  log 'info' "Downloading SHA hash signature file from ${version_url}/${shasums_sig}";
  curlw -s -f -L \
    -o "${download_tmp}/${shasums_sig}" \
    "${version_url}/${shasums_sig}" \
    && log 'debug' "SHA256SUMS signature file downloaded successfully to ${download_tmp}/${shasums_sig}" \
    || log 'error' 'SHA256SUMS signature download failed';
};

# If on MacOS with Homebrew, use GNU grep
# This allows keybase login detection to work on Mac,
# and is required to be able to detect terraform version
# from "required_version" setting in "*.tf" files
check_dependencies;

# Verify signature if verification mechanism (keybase, gpg, etc) is present
if [[ -f "${TFENV_CONFIG_DIR}/use-gnupg" ]]; then
  # GnuPG uses the user's keyring, and any web-of-trust or local signatures or
  # anything else they have setup.  This is the crazy-powerful mode which is
  # overly confusing to newcomers.  We don't support it without the user creating
  # the file use-gnupg, optionally with directives in it.
  gnupg_command="$(sed -E -n -e 's/^binary: *//p' <"${TFENV_CONFIG_DIR}/use-gnupg")";
  [[ -n "${gnupg_command}" ]] || gnupg_command=gpg;

  download_signature;
  # Deliberately unquoted command, in case caller has something fancier in "use-gnupg".
  # Also, don't use batch mode.  If someone specifies GnuPG, let them deal with any prompting.
  ${gnupg_command} \
    --verify "${download_tmp}/${shasums_sig}" \
    "${download_tmp}/${shasums_name}" \
    || log 'error' 'PGP signature rejected by GnuPG';

elif [[ -f "${TFENV_CONFIG_DIR}/use-gpgv" ]]; then
  # gpgv is a much simpler interface to verification, but does require that the
  # key have been downloaded and marked trusted.
  # We don't force the caller to trust the tfenv repo's copy of their key, they
  # have to choose to make that trust decision.
  gpgv_command="$(sed -E -n -e 's/^binary: *//p' <"${TFENV_CONFIG_DIR}/use-gpgv")";
  trust_tfenv="$(sed -E -n -e 's/^trust.?tfenv: *//p' <"${TFENV_CONFIG_DIR}/use-gpgv")";
  [[ -n "${gpgv_command}" ]] || gpgv_command=gpgv;

  download_signature;
  if [[ "${trust_tfenv}" == 'yes' ]]; then
    ${gpgv_command} \
      --keyring "${TFENV_ROOT}/share/hashicorp-keys.pgp" \
      "${download_tmp}/${shasums_sig}" \
      "${download_tmp}/${shasums_name}" \
      || log 'error' 'PGP signature rejected';
  else
    ${gpgv_command} \
      "${download_tmp}/${shasums_sig}" \
      "${download_tmp}/${shasums_name}" \
      || log 'error' 'PGP signature rejected';
  fi;
elif [[ -n "${keybase_bin}" && -x "${keybase_bin}" ]]; then
  grep -Eq '^Logged in:[[:space:]]*yes' <("${keybase_bin}" status);
  keybase_logged_in="${?}";
  grep -Fq hashicorp <("${keybase_bin}" list-following);
  keybase_following_hc="${?}";

  if [[ "${keybase_logged_in}" -ne 0 || "${keybase_following_hc}" -ne 0 ]]; then
    log 'warn' 'Unable to verify OpenPGP signature unless logged into keybase and following hashicorp';
  else
    download_signature;
    "${keybase_bin}" pgp verify \
      -S hashicorp \
      -d "${download_tmp}/${shasums_sig}" \
      -i "${download_tmp}/${shasums_name}" \
      && log 'debug' 'SHA256SUMS signature matched' \
      || log 'error' 'SHA256SUMS signature does not match!';
  fi;
else
  # Warning about this avoids an unwarranted sense of confidence in the SHA check
  log 'warn' "Not instructed to use Local PGP (${TFENV_CONFIG_DIR}/use-{gpgv,gnupg}) & No keybase install found, skipping OpenPGP signature verification";
fi;

if [[ -n "${shasum_bin}" && -x "${shasum_bin}" ]]; then
  (
    cd "${download_tmp}";
    "${shasum_bin}" \
      -a 256 \
      -s \
      -c <(grep -F "${tarball_name}" "${shasums_name}")
  ) && log 'info' 'SHA256 hash matched!' \
    || log 'error' 'SHA256 hash does not match!';
elif [[ -n "${sha256sum_bin}" && -x "${sha256sum_bin}" ]]; then
  (
    cd "${download_tmp}";
    "${sha256sum_bin}" \
      -c <(grep -F "${tarball_name}" "${shasums_name}")
  ) && log 'info' 'SHA256 hash matched!' \
    || log 'error' 'SHA256 hash does not match!';
else
  # Lack of shasum deserves a proper warning
  log 'warn' 'No shasum tool available. Skipping SHA256 hash validation';
fi;

mkdir -p "${dst_path}" || log 'error' "Failed to make directory ${dst_path}";

declare unzip_output;
unzip_output="$(unzip -o "${download_tmp}/${tarball_name}" -d "${dst_path}")" || log 'error' 'Tarball unzip failed';
while IFS= read -r unzip_line; do
 log 'info' "${unzip_line}";
done < <(printf '%s\n' "${unzip_output}");

log 'info' "Installation of terraform v${version} successful. To make this your default version, run 'tfenv use ${version}'";
