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

SCRATCH_LABEL=buildbox-scratch
SCRATCH_MNT=/var/lib/buildbox
SCRATCH_USER=user
SCRATCH_BEGIN='# >>> buildbox scratch disk, declared by host-config/apply.sh >>>'
SCRATCH_END='# <<< buildbox scratch disk <<<'

scratch_fstab() {
  cat <<'FSTAB'
LABEL=buildbox-scratch  /var/lib/buildbox  ext4  defaults,noatime,nofail,x-systemd.device-timeout=10s  0  2
/var/lib/buildbox/builds        /home/user/builds                  none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
/var/lib/buildbox/pnpm-store    /home/user/.local/share/pnpm       none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
/var/lib/buildbox/npm           /home/user/.npm                    none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
/var/lib/buildbox/cargo-registry /home/user/.cargo/registry        none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
/var/lib/buildbox/dev-tools     /home/user/.dev-tools              none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
/var/lib/buildbox/runs          /home/user/runs                    none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
/var/lib/buildbox/playwright    /home/user/.cache/ms-playwright    none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0
FSTAB
  local runner target name runner_home="${SCRATCH_RUNNER_HOME:-/home/user}"
  for runner in "$runner_home"/actions-runner-*; do
    [ -d "$runner" ] || continue
    [ ! -L "$runner" ] || { echo "scratch-bind.sh: runner path is a symlink: $runner" >&2; return 1; }
    target="$runner/_work"
    [ ! -L "$target" ] || { echo "scratch-bind.sh: runner work path is a symlink: $target" >&2; return 1; }
    name="${runner##*/}"
    [[ "$name" =~ ^actions-runner-[A-Za-z0-9._-]+$ ]] \
      || { printf 'scratch-bind.sh: invalid runner name: %q\n' "$name" >&2; return 1; }
    printf '/var/lib/buildbox/runner-work/%s  %s  none  bind,nofail,x-systemd.requires=/var/lib/buildbox  0 0\n' "$name" "$target"
  done
}

scratch_binds() { scratch_fstab | awk 'NR > 1 { print $1, $2 }'; }
scratch_user_systemctl() { runuser -u "$SCRATCH_USER" -- env "XDG_RUNTIME_DIR=/run/user/$(id -u "$SCRATCH_USER")" systemctl --user "$@"; }
scratch_mkdir_user() {
  local d="$1"
  [ -d "$d" ] && return 0
  scratch_mkdir_user "$(dirname "$d")"
  install -d -o "$SCRATCH_USER" -g "$SCRATCH_USER" -m 0755 "$d"
}
scratch_missing_paths() {
  LC_ALL=C comm -23 <(cd "$1" && find . -mindepth 1 -printf '%P\n' | LC_ALL=C sort) \
                    <(cd "$2" && find . -mindepth 1 -printf '%P\n' | LC_ALL=C sort)
}

scratch_fstab_converge() {
  local new verify
  new=$(mktemp)
  awk -v b="$SCRATCH_BEGIN" -v e="$SCRATCH_END" '
    $0 == b { skip = 1 } !skip { print } $0 == e { skip = 0 }' /etc/fstab >"$new"
  { printf '%s\n' "$SCRATCH_BEGIN"; scratch_fstab; printf '%s\n' "$SCRATCH_END"; } >>"$new"
  if ! verify=$(findmnt --verify --tab-file "$new" 2>&1); then
    echo "scratch-bind.sh: rejected fstab candidate" >&2
    printf '%s\n' "$verify" >&2
    rm -f "$new"
    return 1
  fi
  install -m 0644 -o root -g root "$new" /etc/fstab
  rm -f "$new"
  systemctl daemon-reload
  echo "SCRATCH fstab $(scratch_fstab | grep -c .) entries"
}

scratch_migrate_one() {
  local src="$1" target="$2" stash="$2.migrated" missing
  if [ ! -e "$stash" ]; then
    mountpoint -q "$target" && return 0
    if [ -d "$target" ] && [ -n "$(ls -A "$target" 2>/dev/null)" ]; then
      mv "$target" "$stash"
    else
      rmdir "$target" 2>/dev/null || true
      scratch_mkdir_user "$target"
      mount --bind "$src" "$target"
      echo "SCRATCH bind  $target (nothing to migrate)"
      return 0
    fi
  fi
  if ! mountpoint -q "$target"; then
    rsync -aHAX --delete "$stash/" "$src/"
    scratch_mkdir_user "$target"
    mount --bind "$src" "$target"
  fi
  missing=$(scratch_missing_paths "$stash" "$target")
  if [ -n "$missing" ]; then
    rsync -aHAX --ignore-existing "$stash/" "$target/"
    missing=$(scratch_missing_paths "$stash" "$target")
  fi
  if [ -n "$missing" ]; then
    echo "scratch-bind.sh: $stash holds paths missing from $target; leaving both in place" >&2
    head -20 <<<"$missing" >&2
    return 1
  fi
  rm -rf "$stash"
  echo "SCRATCH bind  $target (migrated)"
}

apply_mounts() {
  local src target pending="" rc=0 system_units user_units
  [ "$(id -u)" = 0 ] || { echo "scratch-bind.sh: must run as root" >&2; return 2; }
  [ "$(findmnt -no TARGET --mountpoint "$SCRATCH_MNT")" = "$SCRATCH_MNT" ] || { echo "scratch-bind.sh: scratch mount absent" >&2; return 1; }
  [ "$(findmnt -no FSTYPE --mountpoint "$SCRATCH_MNT")" = ext4 ] || { echo "scratch-bind.sh: scratch is not ext4" >&2; return 1; }
  [ "$(lsblk -no LABEL "$(findmnt -no SOURCE --mountpoint "$SCRATCH_MNT")")" = "$SCRATCH_LABEL" ] || { echo "scratch-bind.sh: scratch label mismatch" >&2; return 1; }

  while read -r src target; do
    scratch_mkdir_user "$src"
    scratch_mkdir_user "$target"
    if ! mountpoint -q "$target" || [ -e "$target.migrated" ]; then pending+="$src $target"$'\n'; fi
  done < <(scratch_binds)
  scratch_fstab_converge
  [ -n "$pending" ] || { echo 'SCRATCH already converged'; return 0; }

  system_units=$(systemctl list-units --type=service --state=active --no-legend --plain 'actions.runner.*' | awk '{print $1}')
  user_units=$(scratch_user_systemctl list-units --type=service --state=active --no-legend --plain '*runner*' | awk '{print $1}')
  resume() {
    local resume_rc=0
    if [ -n "$user_units" ]; then scratch_user_systemctl start $user_units || resume_rc=1; fi
    if [ -n "$system_units" ]; then systemctl start $system_units || resume_rc=1; fi
    return "$resume_rc"
  }
  trap 'resume' EXIT
  if [ -n "$system_units" ]; then systemctl stop $system_units; fi
  if [ -n "$user_units" ]; then scratch_user_systemctl stop $user_units; fi
  while read -r src target; do
    [ -n "$src" ] || continue
    scratch_migrate_one "$src" "$target" || rc=1
  done <<<"$pending"
  resume || rc=1
  trap - EXIT
  return "$rc"
}

case "${1:-apply}" in
  --scratch-fstab) scratch_fstab ;;
  apply) apply_mounts ;;
  *) echo 'usage: scratch-bind.sh [apply|--scratch-fstab]' >&2; exit 2 ;;
esac
