#!/usr/bin/env bash
# Release deployment for desfoto.de.
#
#   .ocauto/deploy <release-sha>
#
# Ships the committed static build to the production host, starts the stack
# behind the existing Traefik instance, and removes the obsolete
# "desfoto.de -> dennyschulz.de" redirect from the shared /srv/stack project.
#
# Rollback: the state before every release is snapshotted to
#   /home/denny/stacks/desfoto-releases/<stamp>-<sha>/
# including the shared stack file that the release modified. Restore it with
#   ROLLBACK_TO=<dir> .ocauto/deploy rollback
# The snapshot is written even for the very first release, when no site is live
# yet; rolling back to that snapshot then removes the new stack again and puts
# the previous /srv/stack configuration back.
set -euo pipefail

cd "$(dirname "$0")/.."
root="$(pwd)"
release_sha="${1:-}"
remote="${DESFOTO_REMOTE:-prod-main}"
live_dir="/home/denny/stacks/desfoto"
releases_dir="/home/denny/stacks/desfoto-releases"
stack_dir="/srv/stack"

log() { printf '[deploy] %s\n' "$1"; }
die() { printf '[deploy] ERROR: %s\n' "$1" >&2; exit 1; }

if [ "$release_sha" = "rollback" ]; then
  target="${ROLLBACK_TO:-}"
  [ -n "$target" ] || die "ROLLBACK_TO must name a snapshot directory"
  log "rolling back to $target"
  # shellcheck disable=SC2029
  ssh "$remote" "set -euo pipefail
    sudo test -d '$target'
    if sudo test -d '$target/site'; then
      sudo rsync -a --delete --chown=denny:denny '$target/site/' '$live_dir/site/'
      sudo cp '$target/nginx.conf' '$live_dir/nginx.conf'
      sudo cp '$target/compose.yml' '$live_dir/compose.yml'
      sudo cp '$target/compose.vps.yml' '$live_dir/compose.vps.yml'
      sudo -n -u denny bash -c \"cd '$live_dir' && docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans\"
    else
      echo '[deploy] initial-state snapshot: removing the desfoto stack again'
      if sudo test -d '$live_dir'; then
        sudo -n -u denny bash -c \"cd '$live_dir' && docker compose -f compose.yml -f compose.vps.yml down --remove-orphans\"
      fi
    fi
    if sudo test -f '$target/docker-compose.yml.stack-backup'; then
      echo '[deploy] restoring the previous shared stack configuration'
      sudo cp '$target/docker-compose.yml.stack-backup' '$stack_dir/docker-compose.yml'
      cd '$stack_dir'
      sudo docker compose up -d --no-deps landing
    fi"
  log "rollback started"
  exit 0
fi

[ -n "$release_sha" ] || die "usage: .ocauto/deploy <release-sha|rollback>"
[ "$(git rev-parse HEAD)" = "$release_sha" ] || die "HEAD does not match $release_sha"
[ -z "$(git status --porcelain -- site nginx.conf compose.yml compose.vps.yml)" ] \
  || die "the deployable files have uncommitted changes"

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

log "packing $release_sha"
tar -czf "$tmp/desfoto-$release_sha.tgz" -C "$root" site nginx.conf compose.yml compose.vps.yml

log "uploading"
scp -q "$tmp/desfoto-$release_sha.tgz" "$remote:/tmp/desfoto-$release_sha.tgz"

log "deploying on $remote"
# shellcheck disable=SC2029
ssh "$remote" "REMOTE_SHA='$release_sha' LIVE_DIR='$live_dir' RELEASES_DIR='$releases_dir' STACK_DIR='$stack_dir' bash -s" <<'REMOTE'
set -euo pipefail
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
snapshot="$RELEASES_DIR/$stamp-$REMOTE_SHA"
archive="/tmp/desfoto-$REMOTE_SHA.tgz"

# 1. Snapshot the current state so a rollback stays possible. This also runs for
#    the very first release (no site live yet) so ROLLBACK_TO always has a target.
sudo mkdir -p "$snapshot" "$RELEASES_DIR"
if sudo test -d "$LIVE_DIR"; then
  echo "[deploy] snapshot -> $snapshot"
  sudo cp -a "$LIVE_DIR/." "$snapshot/"
else
  echo "[deploy] no previous release; $snapshot records the initial state"
fi
sudo ln -sfn "$snapshot" "$RELEASES_DIR/previous"

# 2. Unpack the new release.
echo "[deploy] unpacking $archive"
sudo mkdir -p "$LIVE_DIR"
sudo rm -rf "$LIVE_DIR/site"
sudo tar -xzf "$archive" -C "$LIVE_DIR"
sudo chown -R denny:denny "$LIVE_DIR"
sudo rm -f "$archive"
printf '%s\n' "$REMOTE_SHA" | sudo tee "$LIVE_DIR/RELEASE" >/dev/null

# 3. Start or update the stack. The live directory belongs to denny and the SSH
#    login cannot traverse /home/denny, so the directory change and the compose
#    command run together in one shell under the owning identity.
sudo -n -u denny bash -c "cd '$LIVE_DIR' && docker compose -f compose.yml -f compose.vps.yml up -d --remove-orphans"

# 4. Wait for the container health check.
for _ in $(seq 1 30); do
  status="$(sudo docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' desfoto-web-1 2>/dev/null || echo missing)"
  [ "$status" = "healthy" ] && break
  [ "$status" = "missing" ] && break
  sleep 2
done
echo "[deploy] desfoto-web-1: $status"
if [ "$status" != "healthy" ]; then
  echo "[deploy] ERROR: container is not healthy" >&2
  sudo docker logs --tail 40 desfoto-web-1 >&2 || true
  exit 1
fi

# 5. Remove the obsolete desfoto.de redirect from the shared stack project.
#    The new stack already owns desfoto.de through a higher Traefik priority,
#    this only deletes the now-dead labels so Traefik no longer advertises them.
#    Stripped are Traefik label lines that mention desfoto (the old routers
#    "desfoto-redirect"/"desfoto-redirect-http", the middleware
#    "desfoto-to-website" and any related key) plus the comment that grouped
#    them; every other line is copied verbatim. The result is validated as a
#    Compose project *before* it replaces the live file, and the replacement
#    itself is a single atomic rename.
stack_file="$STACK_DIR/docker-compose.yml"
if sudo grep -q 'desfoto' "$stack_file"; then
  echo "[deploy] removing obsolete desfoto redirect labels from $stack_file"
  sudo cp "$stack_file" "$STACK_DIR/docker-compose.yml.bak.$stamp"
  sudo cp "$stack_file" "$snapshot/docker-compose.yml.stack-backup"

  # The temp file lives next to the original (same filesystem, hidden name that
  # Compose ignores) so the final mv is atomic.
  stack_tmp="$(sudo mktemp "$STACK_DIR/.docker-compose.yml.desfoto.XXXXXX")"
  set +e
  # Strip only lines that are Traefik labels mentioning desfoto (list or map
  # syntax, key or value side) plus the comment that grouped the old redirect.
  # The generic "desfoto" substring filter of the previous version could have
  # deleted unrelated lines; this one is anchored to label lines.
  sudo awk '
    /^[[:space:]]*-?[[:space:]]*"?traefik\..*desfoto/ { next }
    /# desfoto\.de -> kanonische Fotografen-Website/ { next }
    { print }
  ' "$stack_file" | sudo tee "$stack_tmp" >/dev/null
  strip_status="${PIPESTATUS[0]}"
  set -e
  if [ "$strip_status" -ne 0 ]; then
    sudo rm -f "$stack_tmp"
    echo "[deploy] ERROR: cannot read $stack_file (awk exit $strip_status)" >&2
    exit 1
  fi
  sudo chown --reference="$stack_file" "$stack_tmp"
  sudo chmod --reference="$stack_file" "$stack_tmp"

  # Validate the stripped file as the same Compose project before touching the
  # live configuration. The project directory stays $STACK_DIR because the first
  # -f file lives there.
  validate=(docker compose -f "$stack_tmp")
  if sudo test -f "$STACK_DIR/docker-compose.override.yml"; then
    validate+=(-f "$STACK_DIR/docker-compose.override.yml")
  fi
  validate+=(config --quiet)
  if ! ( cd "$STACK_DIR" && sudo "${validate[@]}" ); then
    sudo rm -f "$stack_tmp"
    echo "[deploy] ERROR: stripped stack file is not valid; live file left untouched" >&2
    exit 1
  fi

  sudo mv "$stack_tmp" "$stack_file"
  remaining="$(sudo grep -n 'desfoto' "$stack_file" || true)"
  if [ -n "$remaining" ]; then
    echo "[deploy] WARNING: desfoto references remain in $stack_file:" >&2
    printf '%s\n' "$remaining" >&2
  fi

  ( cd "$STACK_DIR" && sudo docker compose up -d --no-deps landing )
  echo "[deploy] landing recreated without the desfoto redirect"
else
  echo "[deploy] obsolete redirect labels already absent"
fi

# 6. Record the release marker.
echo "[deploy] done: $REMOTE_SHA"
REMOTE

log "release $release_sha deployed"
