#!/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)
  lock="$content/.cosmos-cutover.lock"
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
  lock=/run/lock/cosmosluminosity-cutover.lock
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"

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
  local status_count=0 digest_count=0 status_value='' digest_value='' line
  [[ -f $path && ! -L $path ]] || fail "$kind evidence missing or not regular"
  assert_owner_mode "$path" "$evidence_uid"
  while IFS= read -r line || [[ -n $line ]]; do
    case $line in
      status=*)
        status_count=$((status_count + 1))
        status_value=${line#status=}
        ;;
      release_manifest_sha256=*)
        digest_count=$((digest_count + 1))
        digest_value=${line#release_manifest_sha256=}
        ;;
    esac
  done < "$path"
  [[ $status_count -eq 1 && $status_value == APPROVED ]] || fail "$kind evidence is not uniquely approved"
  [[ $digest_count -eq 1 && $digest_value == "$manifest_sha" ]] || fail "$kind evidence is not uniquely 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
}
move_noreplace() {
  local src=$1 dst=$2
  mv -T -n -- "$src" "$dst" || return 1
  [[ ! -e $src && ! -L $src ]]
}
rollback_mv() {
  local src=$1 dst=$2 label=$3
  if [[ ${CUTOVER_TEST_MODE:-0} == 1 && ${CUTOVER_ROLLBACK_FAILPOINT:-} == "$label" ]]; then
    return 1
  fi
  move_noreplace "$src" "$dst"
}
path_absent() { [[ ! -e $1 && ! -L $1 ]]; }

[[ -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'

exec 9>"$lock"
flock -n 9 || fail 'another cutover holds the lock'
for transaction_path in "$stage" "$old" "$routes_new" "$routes_old" "$failed_theme" "$failed_routes"; do
  path_absent "$transaction_path" || fail "transaction path already exists: $transaction_path"
done
if [[ ${CUTOVER_TEST_MODE:-0} == 1 ]]; then
  case ${CUTOVER_COLLISION_AFTER_PREFLIGHT:-} in
    old) mkdir -- "$old" ;;
    routes_old) mkdir -- "$routes_old" ;;
    '') ;;
    *) fail 'unknown collision injection' ;;
  esac
fi

cleanup_precommit() {
  rm -rf -- "$stage"
  rm -f -- "$routes_new"
}
rollback_needed=0
theme_backed_up=0
routes_backed_up=0
rollback() {
  local rc=${1:-1}
  local rollback_ok=1
  trap - ERR INT TERM EXIT
  set +e
  if (( rollback_needed )); then
    if (( theme_backed_up )); then
      if [[ ! -e $old || -L $old ]]; then
        rollback_ok=0
      else
        if [[ -e $live || -L $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
    fi
    if (( routes_backed_up )); then
      local routes_ok=1
      if [[ ! -e $routes_old || -L $routes_old ]]; then
        routes_ok=0
      else
        if [[ -e $routes || -L $routes ]]; then
          rollback_mv "$routes" "$failed_routes" routes_live_to_failed || routes_ok=0
        fi
        if (( routes_ok )); then
          rollback_mv "$routes_old" "$routes" routes_old_to_live || routes_ok=0
        fi
      fi
      (( routes_ok )) || rollback_ok=0
    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

if move_noreplace "$live" "$old"; then
  theme_backed_up=1
  rollback_needed=1
else
  fail 'theme backup destination collision'
fi
maybe_fail after_live_backup
move_noreplace "$stage" "$live" || fail 'theme install destination collision'
if move_noreplace "$routes" "$routes_old"; then
  routes_backed_up=1
else
  fail 'routes backup destination collision'
fi
maybe_fail after_routes_backup
move_noreplace "$routes_new" "$routes" || fail 'routes install destination collision'
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"
