#!/usr/bin/env bash
set -Eeuo pipefail
umask 022

: "${RELEASE:?RELEASE is required}"
: "${STAMP:?STAMP is required}"
: "${SELLER_EVIDENCE_FILE:?SELLER_EVIDENCE_FILE is required}"
: "${EMAIL_MX_EVIDENCE_FILE:?EMAIL_MX_EVIDENCE_FILE is required}"

if [[ ${CUTOVER_TEST_MODE:-0} == 1 ]]; then
  content=${CUTOVER_TEST_CONTENT:?CUTOVER_TEST_CONTENT is required in test mode}
  compose_file=${CUTOVER_TEST_COMPOSE_FILE:-/dev/null}
  base_url=${CUTOVER_TEST_BASE_URL:-https://www.cosmosluminosity.com}
  evidence_uid=$(id -u)
  content_user=$(id -un)
  content_group=$(id -gn)
else
  [[ $(id -u) -eq 0 ]] || { echo 'production cutover must run as root' >&2; exit 1; }
  content=/opt/cosmosluminosity/content
  compose_file=/opt/cosmosluminosity/docker-compose.yml
  base_url=https://www.cosmosluminosity.com
  evidence_uid=0
  content_user=spazio
  content_group=spazio
fi

release=$(realpath -e -- "$RELEASE")
manifest="$release/PAYLOAD_SHA256SUMS"
manifest_pin="$release/PAYLOAD_MANIFEST_SHA256"
candidate="$release/candidate"
parent="$content/themes"
live="$parent/clean"
stage="$parent/.clean-stage-$STAMP"
old="$parent/.clean-before-$STAMP"
settings="$content/settings"
routes="$settings/routes.yaml"
routes_new="$settings/.routes-$STAMP.new"
routes_old="$settings/.routes-$STAMP.before"
failed_theme="$parent/.clean-failed-$STAMP"
failed_routes="$settings/.routes-$STAMP.failed"
lock="$content/.cosmos-cutover.lock"

fail() { printf 'CUTOVER_FAIL: %s\n' "$*" >&2; exit 1; }
assert_owner_mode() {
  local path=$1 expected_uid=$2
  local uid mode
  uid=$(stat -c '%u' -- "$path")
  mode=$(stat -c '%a' -- "$path")
  [[ $uid == "$expected_uid" ]] || fail "unexpected owner: $path"
  (( (8#$mode & 8#022) == 0 )) || fail "group/world writable: $path"
}
verify_attestation() {
  local path=$1 kind=$2 manifest_sha=$3
  [[ -f $path && ! -L $path ]] || fail "$kind evidence missing or not regular"
  assert_owner_mode "$path" "$evidence_uid"
  grep -Fxq 'status=APPROVED' "$path" || fail "$kind evidence is not approved"
  grep -Fxq "release_manifest_sha256=$manifest_sha" "$path" || fail "$kind evidence is not bound to this release"
}
assert_content_tree() {
  local bad
  bad=$(find "$1" -xdev \( ! -user "$content_user" -o ! -group "$content_group" \) -print -quit)
  [[ -z $bad ]] || fail "ownership check failed: $bad"
}
assert_frozen_release() {
  local bad
  bad=$(find "$release" -xdev \( -type l -o ! -uid "$evidence_uid" -o -perm /222 \) -print -quit)
  [[ -z $bad ]] || fail "release is not immutable and owner-pinned: $bad"
}
restart_ghost() {
  if [[ ${CUTOVER_TEST_MODE:-0} == 1 ]]; then
    if [[ ${CUTOVER_FAILPOINT:-} == restart && ${ROLLING_BACK:-0} != 1 ]]; then
      return 1
    fi
    return 0
  fi
  docker compose -f "$compose_file" restart ghost >/dev/null
  docker compose -f "$compose_file" ps --status running --services | grep -Fxq ghost
}
healthcheck() {
  local path
  if [[ ${CUTOVER_TEST_MODE:-0} == 1 ]]; then
    if [[ ${CUTOVER_FAILPOINT:-} == healthcheck && ${ROLLING_BACK:-0} != 1 ]]; then
      return 1
    fi
    return 0
  fi
  for path in / /en/ /webinar/ /en/webinar/ /terms/ /en/terms/ /refund-policy/ /en/refund-policy/; do
    curl --fail --silent --show-error --max-time 20 "$base_url$path" >/dev/null
  done
}
maybe_fail() {
  [[ ${CUTOVER_TEST_MODE:-0} == 1 && ${CUTOVER_FAILPOINT:-} == "$1" ]] && fail "injected failure at $1"
  return 0
}
rollback_mv() {
  local src=$1 dst=$2 label=$3
  if [[ ${CUTOVER_TEST_MODE:-0} == 1 && ${CUTOVER_ROLLBACK_FAILPOINT:-} == "$label" ]]; then
    return 1
  fi
  mv -T -- "$src" "$dst"
}

[[ -d $candidate/theme && -f $candidate/routes.yaml ]] || fail 'candidate is incomplete'
[[ -f $manifest && -f $manifest_pin ]] || fail 'payload manifest is incomplete'
[[ ! -L $release && ! -L $candidate && ! -L $candidate/theme ]] || fail 'release paths must not be symlinks'
assert_frozen_release
expected_pin=$(awk 'NF {print $1; exit}' "$manifest_pin")
[[ $expected_pin =~ ^[0-9a-f]{64}$ ]] || fail 'invalid manifest pin'
actual_pin=$(sha256sum "$manifest" | awk '{print $1}')
[[ $actual_pin == "$expected_pin" ]] || fail 'payload manifest pin mismatch'
(
  cd "$release"
  sha256sum --strict -c PAYLOAD_SHA256SUMS >/dev/null
)
manifest_sha=$actual_pin
verify_attestation "$SELLER_EVIDENCE_FILE" seller "$manifest_sha"
verify_attestation "$EMAIL_MX_EVIDENCE_FILE" email-mx "$manifest_sha"

[[ -d $live && -f $routes ]] || fail 'live theme/routes missing'
[[ -d $parent && -d $settings ]] || fail 'content layout missing'
[[ ! -e $stage && ! -e $old && ! -e $routes_new && ! -e $routes_old && ! -e $failed_theme && ! -e $failed_routes ]] || fail 'transaction paths already exist'

exec 9>"$lock"
flock -n 9 || fail 'another cutover holds the lock'

cleanup_precommit() {
  rm -rf -- "$stage"
  rm -f -- "$routes_new"
}
rollback_needed=0
rollback() {
  local rc=${1:-1}
  local rollback_ok=1
  trap - ERR INT TERM EXIT
  set +e
  if (( rollback_needed )); then
    if [[ -e $old ]]; then
      if [[ -e $live ]]; then
        rollback_mv "$live" "$failed_theme" theme_live_to_failed || rollback_ok=0
      fi
      if (( rollback_ok )); then
        rollback_mv "$old" "$live" theme_old_to_live || rollback_ok=0
      fi
    fi
    if [[ -e $routes_old ]]; then
      if [[ -e $routes ]]; then
        rollback_mv "$routes" "$failed_routes" routes_live_to_failed || rollback_ok=0
      fi
      if (( rollback_ok )); then
        rollback_mv "$routes_old" "$routes" routes_old_to_live || rollback_ok=0
      fi
    fi
    if (( rollback_ok )); then
      chown -R "$content_user:$content_group" -- "$live" "$routes" 2>/dev/null || rollback_ok=0
      assert_content_tree "$live" 2>/dev/null || rollback_ok=0
      [[ $(stat -c '%U:%G' "$routes" 2>/dev/null) == "$content_user:$content_group" ]] || rollback_ok=0
      ROLLING_BACK=1 restart_ghost >/dev/null 2>&1 || rollback_ok=0
      ROLLING_BACK=1 healthcheck >/dev/null 2>&1 || rollback_ok=0
    fi
  else
    cleanup_precommit
  fi
  if (( rollback_needed )); then
    if (( rollback_ok )); then
      printf 'ROLLBACK=VERIFIED\n' >&2
    else
      printf 'ROLLBACK=FAILED\n' >&2
      exit 70
    fi
  else
    printf 'ROLLBACK=NOT_NEEDED\n' >&2
  fi
  exit "$rc"
}
trap 'rollback $?' ERR INT TERM EXIT

cp -a -- "$candidate/theme" "$stage"
install -o "$content_user" -g "$content_group" -m 0644 -- "$candidate/routes.yaml" "$routes_new"
chown -R "$content_user:$content_group" -- "$stage"
assert_content_tree "$stage"
cmp -- "$candidate/routes.yaml" "$routes_new"
diff -qr --no-dereference "$candidate/theme" "$stage" >/dev/null
maybe_fail after_stage

rollback_needed=1
mv -- "$live" "$old"
maybe_fail after_live_backup
mv -- "$stage" "$live"
mv -- "$routes" "$routes_old"
maybe_fail after_routes_backup
mv -- "$routes_new" "$routes"
assert_content_tree "$live"
[[ $(stat -c '%U:%G' "$routes") == "$content_user:$content_group" ]] || fail 'routes ownership check failed'
cmp -- "$candidate/routes.yaml" "$routes"
diff -qr --no-dereference "$candidate/theme" "$live" >/dev/null
restart_ghost
maybe_fail after_restart
healthcheck

rollback_needed=0
trap - ERR INT TERM EXIT
printf 'CUTOVER=PASS\nRELEASE_MANIFEST_SHA256=%s\nOLD_THEME=%s\nOLD_ROUTES=%s\n' "$manifest_sha" "$old" "$routes_old"
