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

# Provision a swap file so the service cgroups can actually reclaim anonymous
# memory. This is deliberately NOT part of the release hooks: it writes
# gigabytes to disk and edits /etc/fstab, so it is invoked explicitly
# (`pushy-systemd swap [size-mib]`) after an operator has confirmed disk space.
#
# Without swap a cgroup that crosses MemoryHigh cannot get back under it — the
# only reclaimable memory is page cache — and the kernel throttles every
# allocation from then on. configure-host-memory.sh compensates by narrowing the
# MemoryHigh/MemoryMax band, but swap is what restores the soft limit's actual
# purpose.

size_mib=${1:-}
swapfile=${SWAPFILE_PATH:-/swapfile}
proc_swaps=${SWAPFILE_PROC_SWAPS:-/proc/swaps}
fstab=${SWAPFILE_FSTAB:-/etc/fstab}
meminfo=${SWAPFILE_MEMINFO:-/proc/meminfo}
mkswap_bin=${SWAPFILE_MKSWAP_BIN:-mkswap}
swapon_bin=${SWAPFILE_SWAPON_BIN:-swapon}

usage() {
  cat <<'EOF'
usage: ensure-swapfile.sh [size-mib]

Create and activate a swap file when the host has none. Defaults to one eighth
of physical RAM, clamped to 2048-4096 MiB, matching the recommendation printed
by configure-host-memory.sh. Idempotent: exits successfully when active swap
already meets the requested size.
EOF
}

if [[ ${1:-} == -h || ${1:-} == --help ]]; then
  usage
  exit 0
fi

if [[ $EUID -ne 0 && ${SWAPFILE_ALLOW_NON_ROOT:-0} != 1 ]]; then
  echo "swap provisioning must run as root" >&2
  exit 1
fi

if [[ ! -r $meminfo ]]; then
  echo "cannot read physical memory information: $meminfo" >&2
  exit 1
fi
memory_total_mib=$(awk '$1 == "MemTotal:" { printf "%.0f", $2 / 1024; exit }' "$meminfo")
if [[ ! $memory_total_mib =~ ^[1-9][0-9]*$ ]]; then
  echo "failed to read MemTotal from $meminfo" >&2
  exit 1
fi

if [[ -z $size_mib ]]; then
  size_mib=$((memory_total_mib / 8))
  ((size_mib < 2048)) && size_mib=2048
  ((size_mib > 4096)) && size_mib=4096
fi
if [[ ! $size_mib =~ ^[1-9][0-9]*$ ]] || ((size_mib < 128)); then
  echo "size-mib must be an integer of at least 128" >&2
  exit 2
fi

active_swap_mib=0
if [[ -r $proc_swaps ]]; then
  active_swap_mib=$(awk 'NR > 1 { total += $3 } END { printf "%.0f", total / 1024 }' "$proc_swaps")
fi

if ((active_swap_mib >= size_mib)); then
  printf 'swapfile status=satisfied active_mib=%s requested_mib=%s\n' \
    "$active_swap_mib" "$size_mib"
  exit 0
fi

if [[ -e $swapfile ]]; then
  # Never resize or reuse a file we cannot prove is ours and inactive: doing so
  # on a live swap file corrupts whatever the kernel has paged out to it.
  if grep -qs "^${swapfile}[[:space:]]" "$proc_swaps"; then
    printf 'swapfile status=already-active path=%s active_mib=%s requested_mib=%s\n' \
      "$swapfile" "$active_swap_mib" "$size_mib"
    echo "note: $swapfile is smaller than requested; resize it manually after swapoff" >&2
    exit 0
  fi
  echo "refusing to overwrite existing file: $swapfile" >&2
  exit 1
fi

swapfile_dir=$(dirname "$swapfile")
available_mib=$(df -Pm "$swapfile_dir" | awk 'NR == 2 { print $4 }')
if [[ $available_mib =~ ^[0-9]+$ ]] && ((available_mib < size_mib + 1024)); then
  echo "not enough free space on $swapfile_dir: ${available_mib} MiB available, need $((size_mib + 1024)) MiB" >&2
  exit 1
fi

filesystem_type=$(df -PT "$swapfile_dir" 2>/dev/null | awk 'NR == 2 { print $2 }')
if [[ $filesystem_type == btrfs || $filesystem_type == zfs ]]; then
  echo "$filesystem_type needs a filesystem-specific swap setup; provision swap manually" >&2
  exit 1
fi

cleanup() {
  if [[ -e $swapfile ]] && ! grep -qs "^${swapfile}[[:space:]]" "$proc_swaps"; then
    rm -f "$swapfile"
  fi
}
trap cleanup ERR

install -m 0600 /dev/null "$swapfile"
if ! fallocate -l "${size_mib}M" "$swapfile" 2>/dev/null; then
  dd if=/dev/zero of="$swapfile" bs=1M count="$size_mib" status=none
fi
chmod 0600 "$swapfile"

if ! "$mkswap_bin" "$swapfile" >/dev/null; then
  # A fallocate'd file can be sparse or extent-mapped in a way mkswap rejects.
  rm -f "$swapfile"
  install -m 0600 /dev/null "$swapfile"
  dd if=/dev/zero of="$swapfile" bs=1M count="$size_mib" status=none
  "$mkswap_bin" "$swapfile" >/dev/null
fi
"$swapon_bin" "$swapfile"
trap - ERR

fstab_entry="$swapfile none swap sw 0 0"
fstab_changed=false
if [[ -f $fstab ]] && grep -qs "^${swapfile}[[:space:]]" "$fstab"; then
  :
else
  printf '%s\n' "$fstab_entry" >>"$fstab"
  fstab_changed=true
fi

printf 'swapfile status=created path=%s size_mib=%s fstab_changed=%s\n' \
  "$swapfile" "$size_mib" "$fstab_changed"
echo "re-run configure-host-memory.sh (or 'pushy-systemd restart') so the cgroup limits pick up the new swap state" >&2
