#!/usr/bin/env bash
#
# Deploys the admin panel on the server: pulls the branch, builds a new
# release folder, switches `current` to it and reloads PM2. If the new release doesn't answer, it switches back. Run as the cms user:
#
#   sudo -u cms -H bash /srv/secondline-cms/repo/deploy/deploy.sh
#   sudo -u cms -H bash .../deploy.sh --branch main
#   sudo -u cms -H bash .../deploy.sh --rollback    # back to the previous release
#
# Builds happen beside the live release, so the site keeps running until the
# switch. The admin panel has no database, so there are no migrations.
set -euo pipefail

CMS_ROOT="${CMS_ROOT:-/srv/secondline-cms}"
BRANCH="${BRANCH:-}"
KEEP_RELEASES="${KEEP_RELEASES:-3}"
ROLLBACK=0

while [[ $# -gt 0 ]]; do
  case "$1" in
    --branch) BRANCH="$2"; shift 2 ;;
    --rollback) ROLLBACK=1; shift ;;
    *) echo "Unknown option: $1" >&2; exit 1 ;;
  esac
done

REPO="$CMS_ROOT/repo"
# The app is at the root of the admin panel repository, or in admin-panel/
# when the website repository is cloned instead.
if [[ -f "$REPO/admin-panel/package.json" ]]; then APP_DIR="$REPO/admin-panel"; else APP_DIR="$REPO"; fi
RELEASES="$CMS_ROOT/releases"
SHARED="$CMS_ROOT/shared"
CURRENT="$CMS_ROOT/current"
APP_NAME="secondline-cms"
export CMS_ROOT

step() { printf '\n==> %s\n' "$*"; }

if [[ "$(id -un)" == "root" ]]; then
  echo "Run as the cms user: sudo -u cms -H bash $0" >&2
  exit 1
fi

# Points `current` at a release in one step, so it is never missing.
switch_to() {
  ln -sfn "$1" "$CURRENT.next"
  mv -Tf "$CURRENT.next" "$CURRENT"
}

reload_app() {
  pm2 startOrReload "$CURRENT/deploy/ecosystem.config.cjs" --update-env
  pm2 save >/dev/null
}

# Waits for the app to answer. /login renders without calling the CMS when
# there is no session cookie, so a slow CMS doesn't fail the deploy.
healthy() {
  for _ in $(seq 1 30); do
    if curl -fsS --max-time 5 -o /dev/null http://127.0.0.1:3200/login; then
      return 0
    fi
    sleep 2
  done
  return 1
}

# Release folders, newest first (their names start with a UTC timestamp).
releases_newest_first() {
  local dirs=("$RELEASES"/*/)
  [[ -d "${dirs[0]}" ]] || return 0
  printf '%s\n' "${dirs[@]%/}" | sort -r
}

previous_release() {
  local live dir
  live="$(readlink -f "$CURRENT" 2>/dev/null || true)"
  for dir in $(releases_newest_first); do
    if [[ "$dir" != "$live" ]]; then
      echo "$dir"
      return 0
    fi
  done
}

if [[ $ROLLBACK -eq 1 ]]; then
  target="$(previous_release)"
  if [[ -z "$target" ]]; then echo "No earlier release to go back to." >&2; exit 1; fi
  step "Rolling back to $(basename "$target")"
  switch_to "$target"
  reload_app
  healthy && echo "Live: $(basename "$target")" || { echo "The app isn't answering; see $CMS_ROOT/logs." >&2; exit 1; }
  exit 0
fi

[[ -f "$SHARED/.env" ]] || { echo "Missing $SHARED/.env (copy deploy/staging.env.example and fill it in)." >&2; exit 1; }
[[ -d "$REPO/.git" ]] || { echo "Missing $REPO (clone the repository there first)." >&2; exit 1; }

step "Pulling ${BRANCH:-the current branch}"
cd "$REPO"
git fetch --prune origin
BRANCH="${BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
git checkout -q "$BRANCH"
git reset -q --hard "origin/$BRANCH"
COMMIT="$(git rev-parse --short HEAD)"

RELEASE="$RELEASES/$(date -u +%Y%m%d%H%M%S)-$COMMIT"
step "Building release $(basename "$RELEASE")"
mkdir -p "$RELEASE"
rsync -a --delete --exclude node_modules --exclude .next --exclude '.env*' \
  --exclude storage --exclude wordpress-import --exclude .git "$APP_DIR/" "$RELEASE/"
ln -sfn "$SHARED/.env" "$RELEASE/.env"
# Import reports stay in one place across releases.
mkdir -p "$SHARED/wordpress-import"
ln -sfn "$SHARED/wordpress-import" "$RELEASE/wordpress-import"
echo "$COMMIT" > "$RELEASE/REVISION"

cd "$RELEASE"
# Dev dependencies are needed to build (Tailwind, TypeScript, tsx).
npm ci --include=dev --no-audit --no-fund
npm run build

step "Switching to the new release"
before="$(readlink -f "$CURRENT" 2>/dev/null || true)"
switch_to "$RELEASE"
reload_app

if ! healthy; then
  echo "The new release isn't answering; see $CMS_ROOT/logs/cms.error.log." >&2
  if [[ -n "$before" && -d "$before" ]]; then
    echo "Switching back to $(basename "$before")." >&2
    switch_to "$before"
    reload_app
  fi
  exit 1
fi

# Scheduled publishing now runs on the Laravel CMS. Remove the old cron job if an earlier release added it.
if crontab -l 2>/dev/null | grep -q 'publish-scheduled.sh'; then
  step "Removing the old scheduled-publishing cron job"
  crontab -l 2>/dev/null | grep -v 'publish-scheduled.sh' | crontab -
fi

step "Removing old releases (keeping $KEEP_RELEASES)"
live="$(readlink -f "$CURRENT")"
kept=0
for dir in $(releases_newest_first); do
  kept=$((kept + 1))
  if [[ $kept -gt $KEEP_RELEASES && "$dir" != "$live" ]]; then rm -rf "$dir"; fi
done

echo
echo "Live: $(basename "$RELEASE") ($BRANCH @ $COMMIT)"
