#!/usr/bin/env bash
#
# Server half of `npm run deploy:prod`. Do not run by hand: scripts/deploy-prod.sh
# pipes it over SSH as   bash -s -- <mode> <app-dir> <branch> <sha> <archive>
#
#   release   move the server's code to <sha> (the commit that was built), put
#             the uploaded build in place, restart pm2 and check the site
#             answers - and undo all of it automatically if it does not
#   rollback  swap back to the previous release (running it twice swaps back)
#
# The swap is two renames, so the live folder is never half-written, and the
# previous build is kept in $BUILD_DIR-bkp with its commit in .deploy-prev.

set -euo pipefail

MODE="$1" APP_DIR="$2" BRANCH="$3" SHA="${4:-}" ARCHIVE="${5:-}"

# Per-app settings - the only lines that differ between the two repos.
BUILD_DIR="build"
PM2_NAME="wedig-strapi"
HEALTH_URL="http://localhost:${PORT:-1337}/_health"

NEW_DIR="$BUILD_DIR-new"
BACKUP_DIR="$BUILD_DIR-bkp"
OLD_BACKUP_DIR="$BUILD_DIR-bkp-old"

step() { printf '\n\033[1;36m[server] %s\033[0m\n' "$*"; }
fail() { printf '\n\033[1;31m[server] STOPPED: %s\033[0m\n' "$*" >&2; exit 1; }

# Over `ssh host bash -s` the shell is non-interactive, and Ubuntu's .bashrc
# stops before the nvm lines - so node, npm and pm2 can be missing from PATH
# even though they work when you log in. Load them the way a login would.
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# shellcheck disable=SC1091
[[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh" >/dev/null
export PATH="$PATH:/usr/local/bin:$HOME/.npm-global/bin:$HOME/.local/bin"
for tool in git node npm pm2 curl tar; do
  command -v "$tool" >/dev/null || fail "'$tool' is not available to non-interactive SSH on the server (see DEPLOY.md, troubleshooting)."
done

cd "$APP_DIR"

healthy() {
  for _ in $(seq 1 ${HEALTH_TRIES:-60}); do
    curl -fsS -o /dev/null "$HEALTH_URL" && return 0
    sleep 2
  done
  return 1
}

restart() {
  step "Restarting $PM2_NAME"
  pm2 restart "$PM2_NAME" >/dev/null
  pm2 ls | grep -E "$PM2_NAME" || true
}

# Swap the live build with the backup (used by rollback and by auto-undo).
swap_back() {
  [[ -d "$BACKUP_DIR" ]] || fail "No $BACKUP_DIR on the server - nothing to swap back to."
  rm -rf "$NEW_DIR"
  [[ -d "$BUILD_DIR" ]] && mv "$BUILD_DIR" "$NEW_DIR"
  mv "$BACKUP_DIR" "$BUILD_DIR"
  [[ -d "$NEW_DIR" ]] && mv "$NEW_DIR" "$BACKUP_DIR"
  return 0
}

if [[ "$MODE" == "rollback" ]]; then
  [[ -f .deploy-prev ]] || fail "No .deploy-prev on the server - nothing to roll back to."
  prev="$(cat .deploy-prev)"
  current="$(git rev-parse HEAD)"
  step "Rolling back: $(git log --oneline -1 "$current")  ->  $(git log --oneline -1 "$prev")"
  git reset -q --keep "$prev"
  swap_back
  echo "$current" > .deploy-prev
  restart
  healthy || fail "Rolled back, but the app is not answering on $HEALTH_URL. Check: pm2 logs $PM2_NAME --lines 100"
  printf '\033[1;32m[server] Rolled back and answering.\033[0m (Run rollback again to return.)\n'
  exit 0
fi

[[ "$MODE" == "release" ]] || fail "Unknown mode '$MODE'."
[[ -f "$ARCHIVE" ]] || fail "Uploaded build $ARCHIVE not found."
trap 'rm -f "$ARCHIVE"' EXIT   # never leave the upload behind, whatever happens

step "Checking the server copy"
[[ "$(git branch --show-current)" == "$BRANCH" ]] || fail "Server is on '$(git branch --show-current)', expected '$BRANCH'."
if [[ -n "$(git status --porcelain --untracked-files=no)" ]]; then
  git status --short --untracked-files=no
  fail "Code was changed directly on the server (above). Commit it to GitLab or discard it, then deploy."
fi

step "Moving the code to the commit that was built"
git fetch -q origin "$BRANCH"
git merge-base --is-ancestor "$SHA" "origin/$BRANCH" || fail "$SHA is not on origin/$BRANCH - push it first."
before="$(git rev-parse HEAD)"
git merge -q --ff-only "$SHA" || fail "Server history has diverged from GitLab; not merging automatically."
git log --oneline "$before..HEAD" | sed 's/^/  + /'
[[ "$before" == "$SHA" ]] && echo "  (code already at $(git log --oneline -1 | cut -c1-60))"

# The lockfile only changes when real dependencies do (package.json also
# changes for scripts), and npm ci under a running app is not free.
if ! git diff --quiet "$before" HEAD -- package-lock.json; then
  step "Dependencies changed - npm ci"
  npm ci || { git reset -q --keep "$before"; fail "npm ci failed; code put back. The running app was not touched."; }
fi

step "Unpacking the new build next to the live one ($NEW_DIR)"
rm -rf "$NEW_DIR" && mkdir "$NEW_DIR"
tar -xzf "$ARCHIVE" -C "$NEW_DIR" --strip-components=1 || { git reset -q --keep "$before"; fail "Could not unpack the build; nothing changed."; }

# Two renames, so visitors never see a half-copied folder:
#   live -> backup, new -> live.
# The backup from the previous deploy is parked until the new release has
# proved itself, so a failed release can put everything back as it was.
step "Swapping: $BUILD_DIR -> $BACKUP_DIR, $NEW_DIR -> $BUILD_DIR"
prev_pointer="$(cat .deploy-prev 2>/dev/null || true)"
rm -rf "$OLD_BACKUP_DIR"
[[ -d "$BACKUP_DIR" ]] && mv "$BACKUP_DIR" "$OLD_BACKUP_DIR"
[[ -d "$BUILD_DIR" ]] && mv "$BUILD_DIR" "$BACKUP_DIR"
mv "$NEW_DIR" "$BUILD_DIR"
echo "$before" > .deploy-prev   # the commit that goes with $BACKUP_DIR

restart
step "Checking $HEALTH_URL"
if healthy; then
  rm -rf "$OLD_BACKUP_DIR"
  printf '\033[1;32m[server] Live: %s\033[0m\n' "$(git log --oneline -1)"
  exit 0
fi

printf '\033[1;31m[server] Not answering after the restart - undoing the release.\033[0m\n' >&2
git reset -q --keep "$before"
rm -rf "$BUILD_DIR"
[[ -d "$BACKUP_DIR" ]] && mv "$BACKUP_DIR" "$BUILD_DIR"
[[ -d "$OLD_BACKUP_DIR" ]] && mv "$OLD_BACKUP_DIR" "$BACKUP_DIR"
if [[ -n "$prev_pointer" ]]; then echo "$prev_pointer" > .deploy-prev; else rm -f .deploy-prev; fi
restart
healthy && fail "Release undone; the previous version is live again. Check: pm2 logs $PM2_NAME --lines 100"
fail "Release undone but the app still is not answering. Check: pm2 logs $PM2_NAME --lines 100"
