#!/usr/bin/env bash
set -euo pipefail

RELEASES_DIR="${OVERDECK_WEB_RELEASES_DIR:-$HOME/.local/share/overdeck/deploy/apps/web/.releases}"
CURRENT="${OVERDECK_WEB_CURRENT_LINK:-$RELEASES_DIR/current}"
PREVIOUS="${OVERDECK_WEB_PREVIOUS_LINK:-$RELEASES_DIR/previous}"
LOCK="${OVERDECK_WEB_RELEASE_LOCK:-$RELEASES_DIR/.release.lock}"
LOCK_WAIT="${OVERDECK_WEB_RELEASE_LOCK_WAIT:-30}"
READINESS_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/readiness.sh"
[[ -r "$READINESS_LIB" ]] || { printf '{"component":"web-release","status":"error","detail":"missing readiness helper: %s"}\n' "$READINESS_LIB" >&2; exit 1; }
# shellcheck disable=SC1090,SC1091
source "$READINESS_LIB"

fail() {
  printf '{"component":"web-release","status":"error","detail":"%s"}\n' "$1" >&2
  exit 1
}

release_path() {
  local candidate="$1" reject_symlink="${2:-1}" root release entry
  [[ "$reject_symlink" == 0 || ! -L "$candidate" ]] || fail "release path must not be a symlink: $candidate"
  root="$(realpath -e "$RELEASES_DIR")" || fail "release root not found: $RELEASES_DIR"
  release="$(realpath -e "$candidate")" || fail "release not found: $candidate"
  [[ "$release" == "$root"/* ]] || fail "release is outside release root: $release"
  entry="$release/server/entry.mjs"
  [[ -f "$entry" && ! -L "$entry" ]] || fail "release has no regular server entry: $release"
  [[ "$(realpath -e "$entry")" == "$entry" ]] || fail "release server entry traverses a symlink: $release"
  printf '%s\n' "$release"
}

point() {
  local link="$1" target="$2" next
  next="$(mktemp -p "$(dirname "$link")" ".$(basename "$link").next.XXXXXX")" || fail "cannot create pointer temporary"
  rm -f "$next"
  ln -s "$target" "$next" || { rm -f "$next"; fail "cannot create pointer: $link"; }
  mv -Tf "$next" "$link" || { rm -f "$next"; fail "cannot replace pointer: $link"; }
}

promote() {
  local release old=""
  release="$(release_path "$1")"
  if [[ -L "$CURRENT" ]]; then
    old="$(release_path "$CURRENT" 0)"
  elif [[ -e "$CURRENT" ]]; then
    fail "current pointer is not a symlink: $CURRENT"
  fi
  [[ -z "$old" ]] || point "$PREVIOUS" "$old"
  point "$CURRENT" "$release"
  printf '{"component":"web-release","status":"promoted","release":"%s"}\n' "$release"
}

verify_release() {
  local cmd="$1" timeout="$2" delay="$3" started now deadline remaining sleep_ms
  started=$(monotonic_milliseconds) || fail "cannot read monotonic readiness clock"
  deadline=$((started + timeout * 1000))
  while true; do
    if eval "$cmd"; then
      now=$(monotonic_milliseconds) || fail "cannot read monotonic readiness clock"
      printf '{"component":"web-release","status":"ready","elapsed_ms":%s,"deadline_ms":%s}\n' \
        "$((now - started))" "$((timeout * 1000))"
      return 0
    fi
    now=$(monotonic_milliseconds) || fail "cannot read monotonic readiness clock"
    (( now < deadline )) || return 1
    remaining=$((deadline - now))
    sleep_ms=$((delay * 1000))
    (( sleep_ms <= remaining )) || sleep_ms=$remaining
    readiness_sleep "$sleep_ms"
    now=$(monotonic_milliseconds) || fail "cannot read monotonic readiness clock"
    (( now < deadline )) || return 1
  done
}

activate() {
  local release old="" old_previous="" restart_cmd verify_cmd verify_timeout verify_delay
  release="$(release_path "$1")"
  restart_cmd="${OVERDECK_WEB_RELEASE_RESTART_CMD:-systemctl --user restart overdeck-web.service}"
  verify_cmd="${OVERDECK_WEB_RELEASE_VERIFY_CMD:-true}"
  verify_timeout="${OVERDECK_WEB_RELEASE_VERIFY_TIMEOUT:-1}"
  verify_delay="${OVERDECK_WEB_RELEASE_VERIFY_DELAY:-1}"
  [[ "$verify_timeout" =~ ^[1-9][0-9]*$ ]] || fail "verify timeout must be a positive integer"
  [[ "$verify_delay" =~ ^[1-9][0-9]*$ ]] || fail "verify delay must be a positive integer"
  [[ -L "$CURRENT" ]] && old="$(release_path "$CURRENT" 0)"
  [[ -L "$PREVIOUS" ]] && old_previous="$(release_path "$PREVIOUS" 0)"
  [[ -n "$old" ]] && point "$PREVIOUS" "$old"
  point "$CURRENT" "$release"
  if eval "$restart_cmd" && verify_release "$verify_cmd" "$verify_timeout" "$verify_delay"; then
    printf '{"component":"web-release","status":"activated","release":"%s"}\n' "$release"
    return 0
  fi
  if [[ -n "$old" ]]; then point "$CURRENT" "$old"; else rm -f "$CURRENT"; fi
  if [[ -n "$old_previous" ]]; then point "$PREVIOUS" "$old_previous"; else rm -f "$PREVIOUS"; fi
  eval "$restart_cmd" || fail "activation failed and restored release could not restart"
  verify_release "$verify_cmd" "$verify_timeout" "$verify_delay" || fail "activation failed and restored release is unhealthy"
  fail "activation failed; previous release and service health restored"
}

PRUNE_MIN_AGE="${OVERDECK_WEB_RELEASE_PRUNE_MIN_AGE:-3600}"
prune() {
  local keep_current="" keep_previous="" entry resolved mtime now pruned=0
  [[ "$PRUNE_MIN_AGE" =~ ^[0-9]+$ ]] || fail "prune min age must be a non-negative integer"
  [[ -L "$CURRENT" ]] && keep_current="$(release_path "$CURRENT" 0)"
  [[ -L "$PREVIOUS" ]] && keep_previous="$(release_path "$PREVIOUS" 0)"
  now="$(date +%s)"
  for entry in "$RELEASES_DIR"/.build-* "$RELEASES_DIR"/.deps-* "$RELEASES_DIR"/.bootstrap-*; do
    [[ -d "$entry" && ! -L "$entry" ]] || continue
    resolved="$(realpath -e "$entry")" || continue
    [[ "$resolved" != "$keep_current" && "$resolved" != "$keep_previous" ]] || continue
    mtime="$(stat -c %Y "$entry")" || fail "cannot stat retired release: $entry"
    (( now - mtime >= PRUNE_MIN_AGE )) || continue
    { chmod -R u+w "$entry" && rm -rf "$entry"; } || fail "cannot prune retired release: $entry"
    pruned=$((pruned + 1))
  done
  printf '{"component":"web-release","status":"pruned","count":%s}\n' "$pruned"
}

rollback() {
  [[ -L "$CURRENT" ]] || fail "no current release is recorded"
  [[ -L "$PREVIOUS" ]] || fail "no previous release is recorded"
  local current previous
  current="$(release_path "$CURRENT" 0)"
  previous="$(release_path "$PREVIOUS" 0)"
  point "$CURRENT" "$previous"
  point "$PREVIOUS" "$current"
  printf '{"component":"web-release","status":"rolled-back","release":"%s"}\n' "$previous"
}

mkdir -p "$RELEASES_DIR" || fail "cannot create release root: $RELEASES_DIR"
if [[ -n "${OVERDECK_WEB_RELEASE_LOCK_FD:-}" ]]; then
  lockfd="$OVERDECK_WEB_RELEASE_LOCK_FD"
  [[ "$lockfd" =~ ^[0-9]+$ && -e "/proc/$$/fd/$lockfd" ]] || fail "inherited release lock fd is invalid"
  [[ "$(stat -Lc '%d:%i' "/proc/$$/fd/$lockfd")" == "$(stat -Lc '%d:%i' "$LOCK")" ]] || fail "inherited release lock fd does not match $LOCK"
else
  exec {lockfd}>"$LOCK" || fail "cannot open release lock: $LOCK"
  flock -w "$LOCK_WAIT" "$lockfd" || fail "release lock timed out after ${LOCK_WAIT}s"
fi

case "${1:-}" in
  promote) [[ $# -eq 2 ]] || fail "usage: web-release promote RELEASE"; promote "$2" ;;
  activate) [[ $# -eq 2 ]] || fail "usage: web-release activate RELEASE"; activate "$2" ;;
  rollback) [[ $# -eq 1 ]] || fail "usage: web-release rollback"; rollback ;;
  prune) [[ $# -eq 1 ]] || fail "usage: web-release prune"; prune ;;
  *) fail "usage: web-release {promote RELEASE|activate RELEASE|rollback|prune}" ;;
esac
