#!/usr/bin/env bash

set -Eeuo pipefail
umask 077

remote_path="${1:-}"
release_sha="${2:-}"
release_owner="${3:-}"
lease_token="${4:-}"
delay_seconds="${5:-0}"

case "$remote_path" in
  /*) ;;
  *) echo "Release guard requires an absolute application path." >&2; exit 1 ;;
esac
remote_path="${remote_path%/}"
test -n "$remote_path"
test "$remote_path" != "/"
printf '%s' "$release_sha" | grep -Eq '^[a-f0-9]{40}$'
printf '%s' "$release_owner" | grep -Eq '^[0-9]+-[0-9]+$'
printf '%s' "$lease_token" | grep -Eq '^[a-f0-9]{64}$'
printf '%s' "$delay_seconds" | grep -Eq '^[0-9]{1,4}$'

state_path="$remote_path/.deploy/deploy-$release_owner.state"
release_path="$remote_path/.deploy/releases/$release_sha"
rollback_path="$remote_path/.deploy/rollbacks/$release_owner"
prior_checkout_path="$remote_path/.deploy/prior-checkout-$release_owner.state"
environment_backup_path="$remote_path/.deploy/env-backups/$release_owner.env"
worker_script="$release_path/scripts/start-production-queue-worker.sh"
scheduler_script="$release_path/scripts/start-production-scheduler.sh"
lease_path="$remote_path/.deploy/current-release.lease"
owner_lease_path="$remote_path/.deploy/leases/$release_owner.lease"
current_release_path="$remote_path/.deploy/current-release"
prior_release_path="$remote_path/.deploy/prior-release-$release_owner.state"
expected_lease="$(printf 'owner=%s\nrelease=%s\ntoken=%s' \
  "$release_owner" "$release_sha" "$lease_token")"

owns_release_lease() {
  [ -f "$lease_path" ] \
    && [ ! -L "$lease_path" ] \
    && [ -f "$owner_lease_path" ] \
    && [ ! -L "$owner_lease_path" ] \
    && [ "$lease_path" -ef "$owner_lease_path" ] \
    && [ "$(cat "$owner_lease_path")" = "$expected_lease" ]
}

assert_release_lease() {
  if ! owns_release_lease; then
    echo "Release guard $release_owner is superseded and will not touch production."
    exit 0
  fi
}

write_state() {
  local state="$1"
  local temporary_path="$state_path.tmp.$$"

  assert_release_lease
  printf '%s\n' "$state" > "$temporary_path"
  chmod 0600 "$temporary_path"
  mv "$temporary_path" "$state_path"
}

release_owned_lease() {
  assert_release_lease
  test "$lease_path" -ef "$owner_lease_path"
  rm "$lease_path"
  rm -f "$owner_lease_path"
}

discard_environment_backup() {
  if [ -e "$environment_backup_path" ]; then
    assert_release_lease
    test -f "$environment_backup_path"
    test ! -L "$environment_backup_path"
    shred --force --remove "$environment_backup_path" \
      || rm -f -- "$environment_backup_path"
  fi
}

discard_environment_temporaries() {
  local temporary_path

  for temporary_path in \
    "$environment_backup_path.tmp" \
    "$remote_path/.env.release-tmp" \
    "$remote_path/.env.session-encryption-tmp" \
    "$remote_path/.env.guard-$release_owner.tmp"; do
    if [ -e "$temporary_path" ]; then
      assert_release_lease
      test -f "$temporary_path"
      test ! -L "$temporary_path"
      shred --force --remove "$temporary_path" || rm -f -- "$temporary_path"
    fi
  done
}

restore_environment_backup() {
  local temporary_path="$remote_path/.env.guard-$release_owner.tmp"

  if [ ! -e "$environment_backup_path" ]; then
    return
  fi

  assert_release_lease
  test -f "$environment_backup_path"
  test ! -L "$environment_backup_path"
  cp -- "$environment_backup_path" "$temporary_path"
  chmod 0600 "$temporary_path"
  mv -f "$temporary_path" "$remote_path/.env"
  shred --force --remove "$environment_backup_path" \
    || rm -f -- "$environment_backup_path"
}

prior_release() {
  local value

  test -f "$prior_release_path"
  value="$(cat "$prior_release_path")"
  case "$value" in
    none) printf '%s' none ;;
    *)
      printf '%s' "$value" | grep -Eq '^[0-9]+-[0-9]+ [a-f0-9]{40}$'
      printf '%s' "$value"
      ;;
  esac
}

assert_pre_promotion_release_marker() {
  local previous current

  previous="$(prior_release)"
  current="$(cat "$current_release_path" 2>/dev/null || printf 'none')"
  case "$current" in
    "$previous"|"$release_owner $release_sha"|none) ;;
    *)
      echo "Release guard refuses an unexpected current-release owner: $current" >&2
      exit 1
      ;;
  esac
}

restore_prior_release_marker() {
  local previous temporary_path

  assert_release_lease
  assert_pre_promotion_release_marker
  previous="$(prior_release)"
  if [ "$previous" = none ]; then
    rm -f "$current_release_path"
    return
  fi

  temporary_path="$current_release_path.tmp.$$"
  printf '%s\n' "$previous" > "$temporary_path"
  chmod 0600 "$temporary_path"
  mv "$temporary_path" "$current_release_path"
}

assert_promoted_release_marker() {
  assert_release_lease
  test -f "$current_release_path"
  test ! -L "$current_release_path"
  if [ "$(cat "$current_release_path")" != "$release_owner $release_sha" ]; then
    echo "Release guard refuses to alter a different current release." >&2
    exit 1
  fi
}

restore_prior_checkout() {
  local prior_checkout

  if [ ! -f "$prior_checkout_path" ]; then
    return
  fi

  prior_checkout="$(cat "$prior_checkout_path")"
  case "$prior_checkout" in
    on|off) ;;
    *) echo "Release guard found an invalid prior checkout state." >&2; exit 1 ;;
  esac

  assert_release_lease
  test -f "$release_path/artisan"
  test -f "$release_path/vendor/autoload.php"
  (
    # The live tree may already have been restored to the previous release,
    # whose Artisan console does not know the new feature-flag command. Use
    # the immutable release candidate to restore the shared database setting.
    cd "$release_path"
    timeout --signal=TERM --kill-after=10s 60 \
      php artisan wholesale:checkout \
      "$([ "$prior_checkout" = on ] && printf enable || printf disable)"
  )
}

if [ "$delay_seconds" -gt 0 ]; then
  sleep "$delay_seconds"
fi

# This ownership proof precedes every read that could lead to a mutation.
# A watchdog from an older run/attempt exits successfully once a newer owner
# has atomically acquired the global hard link.
assert_release_lease
state="$(cat "$state_path" 2>/dev/null || printf 'none')"
discard_environment_temporaries

case "$state" in
  healthy)
    assert_promoted_release_marker
    discard_environment_backup
    release_owned_lease
    exit 0
    ;;
  rolled-back|failed-forward|pre-maintenance-restored)
    discard_environment_backup
    release_owned_lease
    exit 0
    ;;
  armed|configuring|configured|prepared|rollback-ready)
    restore_environment_backup
    restore_prior_checkout
    write_state pre-maintenance-restored
    release_owned_lease
    exit 0
    ;;
  maintenance-requested|maintenance)
    ;;
  promoted|available|checkout-enabled|checkout-disabled)
    # Once new code has reached the live tree, preserve its payment, webhook,
    # account-claim and order-status handlers. Shopify webhooks do not need to
    # be rolled back because their callback remains compatible with this code.
    assert_promoted_release_marker
    cd "$remote_path"
    timeout --signal=TERM --kill-after=10s 60 php artisan wholesale:checkout disable || true
    assert_promoted_release_marker
    timeout --signal=TERM --kill-after=10s 120 php artisan down --render='errors::503' || true
    timeout --signal=TERM --kill-after=10s 120 php artisan config:clear
    timeout --signal=TERM --kill-after=10s 120 php artisan route:clear
    timeout --signal=TERM --kill-after=10s 120 php artisan view:clear
    timeout --signal=TERM --kill-after=10s 120 php artisan event:clear
    timeout --signal=TERM --kill-after=10s 120 php artisan config:cache
    timeout --signal=TERM --kill-after=10s 120 php artisan route:cache
    timeout --signal=TERM --kill-after=10s 120 php artisan view:cache
    timeout --signal=TERM --kill-after=10s 120 php artisan event:cache
    timeout --signal=TERM --kill-after=15s 180 php artisan release:preflight --expect-checkout=off
    timeout --signal=TERM --kill-after=10s 120 php artisan queue:restart
    test -f "$worker_script"
    test -f "$scheduler_script"
    timeout --signal=TERM --kill-after=10s 120 \
      bash "$worker_script" "$remote_path" default
    assert_promoted_release_marker
    timeout --signal=TERM --kill-after=10s 120 php artisan up
    timeout --signal=TERM --kill-after=20s 180 \
      bash "$scheduler_script" "$remote_path"
    write_state failed-forward
    discard_environment_backup
    release_owned_lease
    exit 0
    ;;
  *)
    echo "Release guard refuses unknown state: $state" >&2
    exit 1
    ;;
esac

assert_release_lease
assert_pre_promotion_release_marker
test -f "$rollback_path/artisan"
test -f "$rollback_path/vendor/autoload.php"

cd "$remote_path"
timeout --signal=TERM --kill-after=10s 60 php artisan wholesale:checkout disable || true
assert_release_lease
timeout --signal=TERM --kill-after=10s 120 php artisan down --render='errors::503' || true
timeout --signal=TERM --kill-after=30s 600 rsync -a --delete \
  --exclude='.deploy' --exclude='.env' --exclude='.env.*' \
  --exclude='storage' --exclude='public/storage' \
  "$rollback_path/" "$remote_path/"

restore_environment_backup
restore_prior_release_marker
timeout --signal=TERM --kill-after=10s 120 php artisan config:clear
timeout --signal=TERM --kill-after=10s 120 php artisan route:clear
timeout --signal=TERM --kill-after=10s 120 php artisan view:clear
timeout --signal=TERM --kill-after=10s 120 php artisan event:clear
timeout --signal=TERM --kill-after=10s 120 php artisan migrate:status
timeout --signal=TERM --kill-after=10s 120 php artisan route:list > /dev/null
restore_prior_checkout
timeout --signal=TERM --kill-after=10s 120 php artisan queue:restart
if [ -f "$worker_script" ]; then
  timeout --signal=TERM --kill-after=10s 120 \
    bash "$worker_script" "$remote_path" default
fi
assert_release_lease
timeout --signal=TERM --kill-after=10s 120 php artisan up
if [ -f "$scheduler_script" ]; then
  timeout --signal=TERM --kill-after=20s 180 \
    bash "$scheduler_script" "$remote_path"
fi
write_state rolled-back
release_owned_lease
