#!/usr/bin/env bash
# Publish and atomically activate immutable per-component backend releases.
set -euo pipefail

BACKEND_ROOT="${OVERDECK_BACKEND_RELEASE_ROOT:-${XDG_STATE_HOME:-$HOME/.local/state}/overdeck/backend}"
LOCK_TIMEOUT="${OVERDECK_BACKEND_RELEASE_LOCK_TIMEOUT:-300}"

usage() {
  printf 'usage: %s publish <component> <sha> <staged-dir>\n' "$0" >&2
  printf '       %s seed <component> <sha>\n' "$0" >&2
  printf '       %s activate <component> <sha> <service> <verifier> [verifier-args...]\n' "$0" >&2
  printf '       %s select-inactive <component> <sha> <service>\n' "$0" >&2
  printf '       %s inspect <component> <service> <active|inactive>\n' "$0" >&2
  printf '       %s prune <component> [minimum-age-seconds]\n' "$0" >&2
  printf '       %s validate <component> <sha>\n' "$0" >&2
  exit 64
}

die() {
  printf 'backend-release: %s\n' "$1" >&2
  exit "${2:-1}"
}

validate_component() {
  [[ "$1" =~ ^[a-z0-9][a-z0-9-]*$ ]] || die "invalid component: $1" 64
}

validate_service() {
  [[ "$1" =~ ^[A-Za-z0-9@_.:-]+\.service$ ]] || die "invalid service: $1" 64
}

validate_sha() {
  [[ "$1" =~ ^[0-9a-f]{40}$ ]] || die "invalid full sha: $1" 64
}

component_root() {
  printf '%s/%s\n' "$BACKEND_ROOT" "$1"
}

release_path() {
  printf '%s/releases/%s\n' "$(component_root "$1")" "$2"
}

atomic_link() {
  local root=$1 target=$2 name=$3 tmp
  tmp="$root/.${name}.tmp.$$"
  rm -f -- "$tmp"
  ln -s -- "$target" "$tmp"
  mv -fT -- "$tmp" "$root/$name"
}

validated_link_target() {
  local component=$1 name=$2 root value sha
  root=$(component_root "$component")
  [[ -L "$root/$name" ]] || return 1
  value=$(readlink "$root/$name") || return 1
  [[ "$value" =~ ^releases/([0-9a-f]{40})$ ]] || return 1
  sha="${BASH_REMATCH[1]}"
  validate_release "$component" "$sha" >/dev/null || return 1
  printf '%s\n' "$sha"
}

validate_release() {
  local component=$1 sha=$2 release
  validate_component "$component"
  validate_sha "$sha"
  release=$(release_path "$component" "$sha")
  [[ -d "$release" && ! -L "$release" ]] || return 1
  python3 - "$release" "$component" "$sha" <<'PY'
import json
import os
import pathlib
import stat
import sys

root = pathlib.Path(sys.argv[1])
component, sha = sys.argv[2:]
try:
    manifest = json.loads((root / "release.json").read_text(encoding="utf-8"))
except (OSError, ValueError):
    raise SystemExit(1)
if manifest != {"schema": 1, "component": component, "target_sha": sha}:
    raise SystemExit(1)
resolved_root = root.resolve(strict=True)
if root.stat().st_mode & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH):
    raise SystemExit(1)
for directory, names, files in os.walk(root, followlinks=False):
    for name in [*names, *files]:
        path = pathlib.Path(directory) / name
        if path.is_symlink():
            try:
                target = path.resolve(strict=True)
                target.relative_to(resolved_root)
            except (OSError, ValueError):
                raise SystemExit(1)
            continue
        try:
            mode = path.stat().st_mode
        except OSError:
            raise SystemExit(1)
        if mode & (stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH):
            raise SystemExit(1)
PY
}

publish_release() {
  local component=$1 sha=$2 staged=$3 root staging_root release manifest
  validate_component "$component"
  validate_sha "$sha"
  root=$(component_root "$component")
  staging_root="$root/.staging"
  release=$(release_path "$component" "$sha")
  mkdir -p "$staging_root" "$root/releases"
  staging_root=$(realpath -e "$staging_root") \
    || die "cannot resolve staging root: $staging_root" 65
  [[ -d "$staged" && ! -L "$staged" ]] || die "staged release is not a directory" 65
  [[ "$(realpath -e "$staged")" == "$staging_root/"* ]] \
    || die "staged release must be inside $staging_root" 65
  if [[ -e "$release" || -L "$release" ]]; then
    validate_release "$component" "$sha" \
      || die "existing release is invalid: $release" 65
    chmod -R u+w "$staged"
    rm -rf -- "$staged"
    printf '%s\n' "$release"
    return
  fi
  manifest="$staged/release.json"
  printf '{"schema":1,"component":"%s","target_sha":"%s"}\n' \
    "$component" "$sha" >"$manifest"
  chmod -R a-w "$staged"
  # Some filesystems require the moved directory itself to remain owner-writable during
  # rename. The component lock prevents activation from observing this publication seam;
  # every descendant is already immutable before the atomic move.
  chmod u+w "$staged"
  mv -T -- "$staged" "$release"
  chmod a-w "$release"
  validate_release "$component" "$sha" \
    || die "published release failed validation: $release" 65
  printf '%s\n' "$release"
}

with_lock() {
  local component=$1
  shift
  local root lock_fd
  root=$(component_root "$component")
  mkdir -p "$root"
  exec {lock_fd}>"$root/activation.lock"
  flock -w "$LOCK_TIMEOUT" "$lock_fd" \
    || die "activation lock timed out for $component" 22
  "$@"
}

seed_locked() {
  local component=$1 sha=$2 root
  root=$(component_root "$component")
  validate_release "$component" "$sha" \
    || die "cannot seed invalid release for $component at $sha" 65
  if [[ -e "$root/current" || -L "$root/current" ]]; then
    current=$(validated_link_target "$component" current) \
      || die "existing current link is invalid for $component" 65
    [[ "$current" == "$sha" ]] \
      || die "refusing to replace existing current for $component" 65
    return
  fi
  atomic_link "$root" "releases/$sha" current
}

select_inactive_locked() {
  local component=$1 sha=$2 service=$3 root old_sha=""
  root=$(component_root "$component")
  validate_release "$component" "$sha" \
    || die "candidate release is invalid for $component at $sha" 65
  systemctl --user is-active --quiet "$service" \
    && die "refusing to select $component while $service is active" 65
  if [[ -e "$root/current" || -L "$root/current" ]]; then
    old_sha=$(validated_link_target "$component" current) \
      || die "current link is invalid for $component" 65
  fi
  atomic_link "$root" "releases/$sha" current
  if [[ -n "$old_sha" && "$old_sha" != "$sha" ]]; then
    atomic_link "$root" "releases/$old_sha" previous
  fi
  printf 'backend-release: selected inactive %s at %s\n' "$component" "$sha"
}

inspect_locked() {
  local component=$1 service=$2 expected=$3 root current_sha="" previous_sha=""
  local loaded=false active=false enabled=false rollback_ready=false state=degraded degraded=0
  root=$(component_root "$component")

  systemctl --user cat "$service" >/dev/null 2>&1 && loaded=true || degraded=1
  systemctl --user is-active --quiet "$service" && active=true || true
  systemctl --user is-enabled --quiet "$service" && enabled=true || true

  if [[ -e "$root/current" || -L "$root/current" ]]; then
    current_sha=$(validated_link_target "$component" current) || degraded=1
  else
    degraded=1
  fi
  if [[ -e "$root/previous" || -L "$root/previous" ]]; then
    previous_sha=$(validated_link_target "$component" previous) || degraded=1
  fi
  if [[ -n "$current_sha" && -n "$previous_sha" && "$current_sha" != "$previous_sha" ]]; then
    rollback_ready=true
  fi

  case "$expected" in
    active)
      [[ "$loaded" == true && "$active" == true && "$enabled" == true && "$rollback_ready" == true ]] || degraded=1
      (( degraded == 0 )) && state=healthy
      ;;
    inactive)
      [[ "$loaded" == true && "$active" == false && "$enabled" == false ]] || degraded=1
      (( degraded == 0 )) && state=dark
      ;;
    *) die "invalid expected service state: $expected" 64 ;;
  esac

  CURRENT_SHA="$current_sha" PREVIOUS_SHA="$previous_sha" python3 - \
    "$component" "$service" "$expected" "$state" "$loaded" "$active" "$enabled" "$rollback_ready" <<'PY'
import json
import os
import sys

component, service, expected, state, loaded, active, enabled, rollback_ready = sys.argv[1:]
print(json.dumps({
    "schema": 1,
    "component": component,
    "service": service,
    "expected": expected,
    "state": state,
    "loaded": loaded == "true",
    "active": active == "true",
    "enabled": enabled == "true",
    "current_sha": os.environ.get("CURRENT_SHA") or None,
    "previous_sha": os.environ.get("PREVIOUS_SHA") or None,
    "rollback_ready": rollback_ready == "true",
}, separators=(",", ":")))
PY
  (( degraded == 0 ))
}

prune_locked() {
  local component=$1 minimum_age=$2 root releases current_sha="" previous_sha=""
  local now entry sha modified removed=0 retained=0 skipped=0
  root=$(component_root "$component")
  releases="$root/releases"
  [[ "$minimum_age" =~ ^[0-9]+$ ]] || die "invalid minimum release age: $minimum_age" 64
  [[ -d "$releases" && ! -L "$releases" ]] \
    || die "release directory is unavailable for $component" 65

  if [[ -e "$root/current" || -L "$root/current" ]]; then
    current_sha=$(validated_link_target "$component" current) \
      || die "current link is invalid for $component" 65
  fi
  if [[ -e "$root/previous" || -L "$root/previous" ]]; then
    previous_sha=$(validated_link_target "$component" previous) \
      || die "previous link is invalid for $component" 65
  fi

  now=$(date +%s)
  shopt -s nullglob
  for entry in "$releases"/*; do
    sha=$(basename "$entry")
    if [[ ! "$sha" =~ ^[0-9a-f]{40}$ || ! -d "$entry" || -L "$entry" ]]; then
      printf 'backend-release: skipped ambiguous release entry: %s\n' "$entry" >&2
      skipped=$((skipped + 1))
      continue
    fi
    if [[ "$sha" == "$current_sha" || "$sha" == "$previous_sha" ]]; then
      retained=$((retained + 1))
      continue
    fi
    if ! validate_release "$component" "$sha" >/dev/null; then
      printf 'backend-release: skipped invalid retired release: %s\n' "$entry" >&2
      skipped=$((skipped + 1))
      continue
    fi
    modified=$(stat -c %Y -- "$entry") || { skipped=$((skipped + 1)); continue; }
    if (( now - modified < minimum_age )); then
      retained=$((retained + 1))
      continue
    fi
    if chmod -R u+w -- "$entry" && rm -rf -- "$entry"; then
      removed=$((removed + 1))
    else
      printf 'backend-release: could not remove retired release: %s\n' "$entry" >&2
      skipped=$((skipped + 1))
    fi
  done
  shopt -u nullglob
  printf '{"schema":1,"component":"%s","status":"%s","removed":%d,"retained":%d,"skipped":%d,"minimum_age_seconds":%d}\n' \
    "$component" "$([[ "$skipped" == 0 ]] && printf pruned || printf degraded)" \
    "$removed" "$retained" "$skipped" "$minimum_age"
  (( skipped == 0 ))
}

activate_locked() {
  local component=$1 sha=$2 service=$3 verifier=$4
  shift 4
  local root old_sha="" candidate_rc rollback_rc
  root=$(component_root "$component")
  validate_release "$component" "$sha" \
    || die "candidate release is invalid for $component at $sha" 65
  if [[ -e "$root/current" || -L "$root/current" ]]; then
    old_sha=$(validated_link_target "$component" current) \
      || die "current link is invalid for $component" 65
  elif systemctl --user is-active --quiet "$service"; then
    die "active $service has no rollback release" 65
  fi

  atomic_link "$root" "releases/$sha" current
  candidate_rc=0
  systemctl --user restart "$service" || candidate_rc=$?
  if (( candidate_rc == 0 )); then
    "$verifier" "$@" "$component" "$sha" || candidate_rc=$?
  fi
  if (( candidate_rc == 0 )); then
    if [[ -n "$old_sha" && "$old_sha" != "$sha" ]]; then
      atomic_link "$root" "releases/$old_sha" previous
    fi
    printf 'backend-release: activated %s at %s\n' "$component" "$sha"
    return
  fi

  if [[ -z "$old_sha" ]]; then
    rm -f -- "$root/current"
    die "candidate failed and no prior release exists for $component (rc=$candidate_rc)" 20
  fi
  atomic_link "$root" "releases/$old_sha" current
  rollback_rc=0
  systemctl --user restart "$service" || rollback_rc=$?
  if (( rollback_rc == 0 )); then
    "$verifier" "$@" "$component" "$old_sha" || rollback_rc=$?
  fi
  if (( rollback_rc == 0 )); then
    die "candidate rejected; restored $component at $old_sha" 20
  fi
  die "candidate failed and rollback verification failed for $component (candidate_rc=$candidate_rc, rollback_rc=$rollback_rc)" 21
}

command=${1:-}
case "$command" in
  publish)
    (($# == 4)) || usage
    validate_component "$2"; validate_sha "$3"
    with_lock "$2" publish_release "$2" "$3" "$4"
    ;;
  validate)
    (($# == 3)) || usage
    validate_release "$2" "$3"
    ;;
  seed)
    (($# == 3)) || usage
    validate_component "$2"; validate_sha "$3"
    with_lock "$2" seed_locked "$2" "$3"
    ;;
  select-inactive)
    (($# == 4)) || usage
    validate_component "$2"; validate_sha "$3"; validate_service "$4"
    with_lock "$2" select_inactive_locked "$2" "$3" "$4"
    ;;
  inspect)
    (($# == 4)) || usage
    validate_component "$2"; validate_service "$3"
    with_lock "$2" inspect_locked "$2" "$3" "$4"
    ;;
  prune)
    (($# == 2 || $# == 3)) || usage
    validate_component "$2"
    with_lock "$2" prune_locked "$2" "${3:-86400}"
    ;;
  activate)
    (($# >= 5)) || usage
    validate_component "$2"; validate_sha "$3"; validate_service "$4"
    with_lock "$2" activate_locked "${@:2}"
    ;;
  *) usage ;;
esac
