#!/bin/sh
# gitlawb installer (macOS / Linux): gl, git-remote-gitlawb and gitlawb-node.
#
# Downloads a pre-built release archive from GitHub and installs it only if its
# SHA-256 is listed at https://twigpine.com/install-checksums.txt. That list is
# served by twigpine.com, not by GitHub, so a hijacked, renamed or look-alike
# GitHub repository cannot get a different binary installed on its own. The
# list also names the latest version, so no GitHub API call is needed.
#
# Usage:
#   curl -fsSL https://twigpine.com/install.sh | sh
#   curl -fsSL https://twigpine.com/install.sh | sh -s -- --version v0.7.1
#
# Plain POSIX sh on purpose: `curl ... | sh` runs dash on Debian and Ubuntu.
set -eu

REPO="${GITLAWB_RELEASE_REPO:-Twigpine/node}"
DOWNLOAD_BASE="${GITLAWB_DOWNLOAD_BASE:-https://github.com/${REPO}/releases/download}"
CHECKSUMS_URL="${GITLAWB_CHECKSUMS_URL:-https://twigpine.com/install-checksums.txt}"
INSTALL_DIR="${GITLAWB_INSTALL_DIR:-$HOME/.local/bin}"
VERSION_ARG="latest"

usage() {
  cat <<EOF
gitlawb installer

Options:
  --version <tag>   Install a specific release tag, for example v0.7.1.
  -h, --help        Show this help.

Environment:
  GITLAWB_INSTALL_DIR    Install directory (default: $HOME/.local/bin)
  GITLAWB_RELEASE_REPO   GitHub repo to download from (default: Twigpine/node)
  GITLAWB_DOWNLOAD_BASE  Release download base URL, for mirrors
                         (default: https://github.com/<repo>/releases/download)
  GITLAWB_CHECKSUMS_URL  Checksum list every download must match
                         (default: https://twigpine.com/install-checksums.txt)
EOF
}

while [ "$#" -gt 0 ]; do
  case "$1" in
    --version)
      if [ "$#" -lt 2 ]; then
        echo "error: --version requires a value" >&2
        exit 1
      fi
      VERSION_ARG="$2"
      shift 2
      ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      VERSION_ARG="$1"
      shift
      ;;
  esac
done

OS=$(uname -s)
ARCH=$(uname -m)

case "$OS" in
  Linux)  OS_NAME="linux" ;;
  Darwin) OS_NAME="darwin" ;;
  *)
    echo "error: unsupported OS: $OS" >&2
    echo "       please build from source: cargo install --git https://github.com/${REPO} gl git-remote-gitlawb" >&2
    exit 1
    ;;
esac

case "$ARCH" in
  x86_64)          ARCH_NAME="x86_64" ;;
  aarch64 | arm64) ARCH_NAME="aarch64" ;;
  *)
    echo "error: unsupported architecture: $ARCH" >&2
    exit 1
    ;;
esac

case "${OS_NAME}-${ARCH_NAME}" in
  linux-x86_64)   TARGET="x86_64-unknown-linux-musl" ;;
  linux-aarch64)  TARGET="aarch64-unknown-linux-musl" ;;
  darwin-x86_64)  TARGET="x86_64-apple-darwin" ;;
  darwin-aarch64) TARGET="aarch64-apple-darwin" ;;
esac

TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT

# The checksum list comes first: it names the latest version and is the only
# thing that can vouch for an archive.
echo "Fetching checksums from ${CHECKSUMS_URL}"
SUMS="$TMP_DIR/install-checksums.txt"
if ! curl -fsSL -o "$SUMS.raw" "$CHECKSUMS_URL"; then
  echo "error: could not download the checksum list: $CHECKSUMS_URL" >&2
  echo "       Nothing was installed." >&2
  exit 1
fi
tr -d '\r' < "$SUMS.raw" > "$SUMS"

if [ "$VERSION_ARG" = "latest" ]; then
  TAG=$(sed -n 's/^# latest: *\(v[0-9][0-9A-Za-z.+-]*\) *$/\1/p' "$SUMS" | head -n 1)
  if [ -z "$TAG" ]; then
    echo "error: the checksum list does not name a latest version: $CHECKSUMS_URL" >&2
    exit 1
  fi
else
  TAG="$VERSION_ARG"
fi

case "$TAG" in
  v*) VERSION="${TAG#v}" ;;
  *)  VERSION="$TAG"; TAG="v$TAG" ;;
esac
case "$VERSION" in
  "" | *[!0-9A-Za-z.+-]*)
    echo "error: not a release version: $TAG" >&2
    exit 1
    ;;
esac

PACKAGE_NAME="gitlawb-node-${VERSION}-${TARGET}"
ARCHIVE="${PACKAGE_NAME}.tar.gz"
DOWNLOAD_URL="${DOWNLOAD_BASE}/${TAG}/${ARCHIVE}"

# "<sha256>  <file>" (or "<sha256> *<file>"), as sha256sum writes it.
EXPECTED=$(awk -v f="$ARCHIVE" '
  ($2 == f || $2 == "*" f) && length($1) == 64 && $1 ~ /^[0-9A-Fa-f]+$/ { print tolower($1); exit }
' "$SUMS")
if [ -z "$EXPECTED" ]; then
  echo "error: ${ARCHIVE} is not in the checksum list at ${CHECKSUMS_URL}" >&2
  echo "       Refusing to install a build the list does not vouch for." >&2
  LISTED=$(awk -v pre="gitlawb-node-" -v suf="-${TARGET}.tar.gz" '
    { n = $2; sub(/^\*/, "", n) }
    index(n, pre) == 1 && length(n) > length(pre) + length(suf) &&
      substr(n, length(n) - length(suf) + 1) == suf {
      printf "%s v%s", sep, substr(n, length(pre) + 1, length(n) - length(pre) - length(suf)); sep = ","
    }' "$SUMS")
  if [ -n "$LISTED" ]; then
    echo "       Listed for ${TARGET}:${LISTED}" >&2
  fi
  exit 1
fi

echo "Installing gitlawb ${TAG} for ${OS_NAME}/${ARCH_NAME}"
echo "  Archive:  ${ARCHIVE}"
echo "  Into:     ${INSTALL_DIR}"
echo ""

echo "Downloading..."
if ! curl -fSL --progress-bar -o "$TMP_DIR/$ARCHIVE" "$DOWNLOAD_URL"; then
  echo ""
  echo "error: download failed: $DOWNLOAD_URL" >&2
  echo "       Check https://github.com/${REPO}/releases for available builds." >&2
  exit 1
fi

sha256_of() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  elif command -v openssl >/dev/null 2>&1; then
    openssl dgst -sha256 "$1" | awk '{print $NF}'
  else
    return 1
  fi
}

echo "Verifying checksum..."
if ! ACTUAL=$(sha256_of "$TMP_DIR/$ARCHIVE"); then
  echo "error: no SHA-256 tool found (need sha256sum, shasum or openssl). Nothing was installed." >&2
  exit 1
fi
ACTUAL=$(printf '%s' "$ACTUAL" | tr 'A-F' 'a-f')
if [ "$ACTUAL" != "$EXPECTED" ]; then
  echo "error: checksum mismatch for ${ARCHIVE}" >&2
  echo "  expected: $EXPECTED (${CHECKSUMS_URL})" >&2
  echo "  actual:   $ACTUAL (${DOWNLOAD_URL})" >&2
  echo "The download is corrupted or is not the published build. Nothing was installed." >&2
  exit 1
fi
echo "  checksum OK"

echo "Extracting..."
tar -xzf "$TMP_DIR/$ARCHIVE" -C "$TMP_DIR"
PACKAGE_DIR="$TMP_DIR/$PACKAGE_NAME"
if [ ! -d "$PACKAGE_DIR" ]; then
  PACKAGE_DIR=$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)
fi
if [ ! -d "$PACKAGE_DIR" ]; then
  echo "error: could not find extracted package directory" >&2
  exit 1
fi

mkdir -p "$INSTALL_DIR"

INSTALLED=""
for BIN in gl git-remote-gitlawb gitlawb-node; do
  if [ -f "$PACKAGE_DIR/$BIN" ]; then
    install -m 755 "$PACKAGE_DIR/$BIN" "$INSTALL_DIR/$BIN.new"
    mv -f "$INSTALL_DIR/$BIN.new" "$INSTALL_DIR/$BIN"
    INSTALLED="$INSTALLED $BIN"
  fi
done

if [ -z "$INSTALLED" ]; then
  echo "error: no installable binaries found in $ARCHIVE" >&2
  exit 1
fi

echo ""
echo "Installed gitlawb ${TAG}"
for BIN in $INSTALLED; do
  echo "  ${BIN} -> ${INSTALL_DIR}/${BIN}"
done
echo ""

if echo ":$PATH:" | grep -q ":${INSTALL_DIR}:"; then
  echo "Run:"
  echo "  gl doctor"
  echo "  gl quickstart"
else
  SHELL_NAME=$(basename "${SHELL:-bash}")
  case "$SHELL_NAME" in
    zsh)  RC="$HOME/.zshrc" ;;
    fish) RC="$HOME/.config/fish/config.fish" ;;
    *)    RC="$HOME/.bashrc" ;;
  esac

  echo "Add ${INSTALL_DIR} to your PATH:"
  echo ""
  if [ "$SHELL_NAME" = "fish" ]; then
    echo "  fish_add_path ${INSTALL_DIR}"
  else
    echo "  echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ${RC}"
    echo "  source ${RC}"
  fi
  echo ""
  echo "Then run:"
  echo "  gl doctor"
  echo "  gl quickstart"
fi

# oh-my-zsh's default git plugin aliases gl='git pull', which silently shadows
# the gl binary in every interactive zsh (aliases beat PATH). Detect the common
# setup and say so now, at install time, instead of letting the first `gl`
# print git's baffling "fatal: not a git repository".
if [ -f "$HOME/.zshrc" ] && [ -d "$HOME/.oh-my-zsh/plugins/git" ] \
  && grep -Eq '^[[:space:]]*plugins=\(([^)]*[[:space:]])?git([[:space:]][^)]*)?\)' "$HOME/.zshrc" \
  && ! grep -Eq '^([^#]*[[:space:];&|])?unalias[[:space:]]+([^;&|#]*[[:space:]])?gl([[:space:]]|$)' "$HOME/.zshrc"; then
  echo ""
  echo "NOTE: oh-my-zsh's git plugin aliases gl='git pull', which will shadow"
  echo "the gl command in interactive shells. To use gl by name, run:"
  echo ""
  echo "  echo 'unalias gl 2>/dev/null' >> ~/.zshrc && source ~/.zshrc"
fi

echo ""
echo "Docs: https://explorer.twigpine.com/docs"
