#!/usr/bin/env bash
set -euo pipefail
BASE=$(cd -- "$(dirname -- "$0")" && pwd -P)
ROOT="$BASE/runtime"
VERSION=${1:?usage: deploy.sh VERSION JAR EXPECTED_SHA256}
ARTIFACT=${2:?JAR required}
EXPECTED=${3:?trusted SHA-256 required}
[[ $VERSION =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]{0,39}$ ]] || exit 2
[[ $EXPECTED =~ ^[0-9a-f]{64}$ ]] || exit 2
for command in docker curl sha256sum flock readlink mv; do command -v "$command" >/dev/null; done
test -f "$ARTIFACT"
test ! -L "$ROOT"
mkdir -p "$ROOT/releases"
exec 9>"$ROOT/deploy.lock"
flock -n 9 || { echo 'another deployment holds the lock' >&2; exit 3; }
if test -f "$ROOT/status"; then
  case "$(<"$ROOT/status")" in
    switching*|recovering*|recovery-failed*)
      echo 'unfinished recovery; inspect running state before continuing' >&2; exit 2;;
  esac
fi
PREVIOUS=''
if test -e "$ROOT/current" || test -L "$ROOT/current"; then
  test -L "$ROOT/current" || { echo 'current must be a symlink' >&2; exit 2; }
  PREVIOUS=$(readlink -f "$ROOT/current")
  [[ "$PREVIOUS" == "$ROOT/releases/"* ]] || exit 2
  test -f "$PREVIOUS/app.jar" && test -f "$PREVIOUS/version.txt"
fi
TARGET="$ROOT/releases/$VERSION"
test ! -e "$TARGET" || { echo 'release ID already exists; choose a new ID' >&2; exit 2; }
ACTUAL=$(sha256sum "$ARTIFACT"); ACTUAL=${ACTUAL%% *}
test "$ACTUAL" = "$EXPECTED" || { echo 'SHA-256 mismatch; no switch' >&2; exit 2; }
docker info >/dev/null
docker image inspect eclipse-temurin:25.0.4_7-jre >/dev/null
export RELEASE_DIR="$ROOT/releases/$VERSION"
docker compose -p ops-release-lab -f "$BASE/compose.yaml" config --quiet
STAGE=$(mktemp -d "$ROOT/releases/.staging-XXXXXX")
cp -- "$ARTIFACT" "$STAGE/app.jar"
COPIED=$(sha256sum "$STAGE/app.jar"); COPIED=${COPIED%% *}
test "$COPIED" = "$EXPECTED"
printf '%s\n' "$VERSION" > "$STAGE/version.txt"
printf '%s\n' "$EXPECTED" > "$STAGE/sha256.txt"
chmod 755 "$STAGE"
chmod 644 "$STAGE/app.jar" "$STAGE/version.txt" "$STAGE/sha256.txt"
mv -T -- "$STAGE" "$TARGET"
SWITCHED=0
state() { printf '%s\n' "$1" > "$ROOT/status.tmp"; mv -T "$ROOT/status.tmp" "$ROOT/status"; }
switch_to() {
  local link="$ROOT/.current-$$"
  ln -s -- "$1" "$link" && mv -Tf -- "$link" "$ROOT/current"
}
start_at() {
  RELEASE_DIR="$1" docker compose -p ops-release-lab -f "$BASE/compose.yaml" \
    up -d --force-recreate --no-deps --pull never app
}
healthy() {
  local expected_version="$1" body i
  for ((i=0;i<15;i++)); do
    if body=$(curl -q --noproxy '*' --fail --silent --show-error \
      --connect-timeout 1 --max-time 2 http://127.0.0.1:18088/health 2>/dev/null) \
      && test "$body" = "$expected_version"; then return 0; fi
    sleep 1
  done
  return 1
}
finish() {
  local rc=$?
  trap - EXIT INT TERM
  if (( rc == 0 || SWITCHED == 0 )); then exit "$rc"; fi
  set +e
  state "recovering failed=$VERSION previous=${PREVIOUS:-none}"
  if test -n "$PREVIOUS"; then
    local old
    old=$(<"$PREVIOUS/version.txt")
    if switch_to "$PREVIOUS" && start_at "$PREVIOUS" && healthy "$old"; then
      state "rolled-back failed=$VERSION active=$old"
      echo "deployment failed; restored $old" >&2
      exit 10
    fi
  else
    if RELEASE_DIR="$TARGET" docker compose -p ops-release-lab -f "$BASE/compose.yaml" stop app \
      && { test ! -L "$ROOT/current" || unlink "$ROOT/current"; }; then
      state "first-deploy-failed stopped=$VERSION"
      exit 10
    fi
  fi
  state "recovery-failed failed=$VERSION; manual inspection required"
  echo 'recovery failed; stop automatic deployment' >&2
  exit 20
}
trap finish EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
state "switching target=$VERSION previous=${PREVIOUS:-none}"
# Mark intent before the pointer operation so a signal in the gap still restores safely.
SWITCHED=1
switch_to "$TARGET"
start_at "$TARGET"
healthy "$VERSION"
state "success active=$VERSION sha256=$EXPECTED"
SWITCHED=0
echo "published $VERSION"
