#!/usr/bin/env bash

set -o pipefail

function usage() {
  local full=${1:-false}
  cat <<EOF
Usage: $SCRIPT [-h] [-H] [-c config] command [OPTIONS]

Helper script for proxmox-backup-client.

Options:
  -h, --help           show this help message and exit
  -H, --full-help      show this help message and all commands of proxmox-backup-client
                       if it is available
  -c, --config CONFIG  path to config file, default: $ETC_DIR/config

Commands:

backup                               run backup and print output to screen
backup-cron                          run backup and suppress output on success
list                                 list groups, latest snapshots and archives
snapshot-list [group]                list snapshots, sizes and archives
catalog-shell [snapshot [archive]]   run interactive shell to explore arhive contents
                                     and restore files/dirs selectively
mount [snapshot [archive [target]]]  mount snapshot archive locally
install [--update]                   install/update proxmox-backup-client and pdb dependencies

EOF
  ! $full && cat <<EOF
Other commands and options are passed to proxmox-backup-client.
See \`$SCRIPT -H\` for a full help message including all available commands.

EOF
}

function full_usage() {
  usage true
  [ ! -x "$PBC" ] && echo "Error: $PBC is not executable!" >&2 && return 254
  echo "Commands of proxmox-backup-client:"
  echo
  "$PBC" help | tail -n +3
}

function pbc-version() {
  local vers_info rc
  vers_info=$("$PBC" version)
  rc=$?
  (( rc != 0 )) && echo "Error running $PBC version ... exited with rc=$rc" >&2 && return $rc
  echo "$vers_info" | grep client
  rc=$?
  (( rc != 0 )) && echo "Error reading client version, output was:" >&2 && echo "$vers_info" >&2
  return $rc
}

function pbc-backup() { "$PBC" backup --ns "$PBS_NAMESPACE" "${BACKUP[@]}" "$@"; }

function pbc-list() { "$PBC" list --ns "$PBS_NAMESPACE" "$@"; }

function pbc-snapshot-list() { "$PBC" snapshot list --ns "$PBS_NAMESPACE" "$@"; }

function pbc-files() { "$PBC" files --ns "$PBS_NAMESPACE" "$@"; }

function pbc-catalog-shell() { "$PBC" catalog shell --ns "$PBS_NAMESPACE" "$@"; }

function pbc-mount() { "$PBC" mount --ns "$PBS_NAMESPACE" "$@"; }

function get_group_names() {
  pbc-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])"'
}

function get_snapshot_names() {
  pbc-snapshot-list --output-format json | jq -r '.[] | "\(.["backup-type"])/\(.["backup-id"])/\(.["backup-time"] | todateiso8601)"'
}

function get_snapshot_files() {
  pbc-files "$1" --output-format json | jq -r '.[] | select(.filename | endswith(".pxar.didx")) | .filename | rtrimstr(".didx")' | sort
}

function apt_get_install() {
  local package=$1
  echo "Installing $package using package manager ..."
  DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$package" >/dev/null
  local rc=$?
  (( rc != 0 )) && echo "Error installing package!" >&2
  return $rc
}

WORK_DIR=''
function cleanup_work_dir() {
  [ -n "$WORK_DIR" ] && [ -d "$WORK_DIR" ] && rm -r "$WORK_DIR"
  trap - EXIT
}

function install_proxmox_backup_client() {
  local update=${1:-false}
  local version

  [ -f "$PBC" ] && local installed=true || local installed=false
  ! $installed && update=false

  if $installed && ! $update; then
    version=$(pbc-version) || return 10
    echo "Info: skip installation of proxmox-backup-client, already installed ($version)"
    return 0
  fi

  if $update && dpkg -S "$PBC" &>/dev/null; then
    echo "Info: skip update of proxmox-backup-client, package is managed by the package manager"
    return 0
  fi

  if apt-cache show proxmox-backup-client &>/dev/null; then
    apt_get_install proxmox-backup-client
    return $?
  fi

  echo "Info: proxmox-backup-client is not available from any configured apt repository"
  echo "Info: add the Proxmox pbs-client repository to install a signed, apt-managed package"

  $update && local action='Updating' || local action='Installing'
  echo "$action proxmox-backup-client-static binary ..."

  local pkgfile=$(curl -fsSL "$PBC_DOWNLOAD_URL/" | grep proxmox-backup-client-static_ | sed 's#.*href="##g;s#".*##g;' | sort --version-sort | tail -n 1)
  [ -z "$pkgfile" ] && echo "Unable to determine current package file!" >&2 && return 30

  WORK_DIR=$(mktemp -d)
  trap cleanup_work_dir EXIT

  echo "Info: downloading $PBC_DOWNLOAD_URL/$pkgfile ..."
  ! curl -fsSL -o "$WORK_DIR/$pkgfile" "$PBC_DOWNLOAD_URL/$pkgfile" && echo "Error downloading file!" >&2 && return 40

  echo "Info: extracting $pkgfile ..."
  ! dpkg-deb -x "$WORK_DIR/$pkgfile" "$WORK_DIR/" && echo "Error extracting file!" >&2 && return 50

  echo "Info: installing proxmox-backup-client static binary to $PBC ..."
  ! install -m 0755 -D "$WORK_DIR/usr/bin/proxmox-backup-client" "$PBC" && echo "Error installing binary!" >&2 && return 60

  version=$(pbc-version) || return $?
  $update && action='updated' || action='installed'
  echo "Info: successfully $action proxmox-backup-$version"

  cleanup_work_dir
}


function arr_contains() {
  local search=$1
  shift
  [ -z "$search" ] && return 1
  while (( $# > 0 )); do
    [ "$1" == "$search" ] && return 0
    shift
  done
  return 1
}

function may_select() {
  local value=$1
  shift

  (( $# == 0 )) && echo "Error: no values to select" >&2 && return 1

  if [ -z "$value" ]; then
    if (( $# == 1 )); then
      value=$1
    elif [ ! -t 0 ]; then
      echo "Error: no value given and stdin is not a terminal" >&2
      return 1
    else
      local sorted
      readarray -t sorted < <(printf '%s\n' "$@" | sort)
      select value in "${sorted[@]}"; do
        [ -n "$value" ] && break
      done
    fi
  elif ! arr_contains "$value" "$@"; then
    return 1
  fi

  [ -z "$value" ] && return 1
  echo "$value"
}

function select_group() {
  local groups raw
  raw=$(get_group_names) || { echo "Error: failed to query PBS!" >&2; return 1; }
  [ -z "$raw" ] && return 2
  readarray -t groups <<< "$raw"

  PS3="Snapshot group [1-${#groups[@]}]: "
  GROUP=$(may_select "$1" "${groups[@]}") || { echo "Error: invalid group" >&2; return 1; }
}

function select_snapshot_and_archive() {
  local snapshots archives raw
  raw=$(get_snapshot_names) || { echo "Error: failed to query PBS!" >&2; return 1; }
  [ -z "$raw" ] && return 2
  readarray -t snapshots <<< "$raw"

  PS3="Snapshot [1-${#snapshots[@]}]: "
  SNAPSHOT=$(may_select "$1" "${snapshots[@]}") || { echo "Error: invalid snapshot" >&2; return 1; }

  raw=$(get_snapshot_files "$SNAPSHOT") || { echo "Error: failed to query PBS!" >&2; return 1; }
  [ -z "$raw" ] && return 3
  readarray -t archives <<< "$raw"

  PS3="Archive [1-${#archives[@]}]: "
  ARCHIVE=$(may_select "$2" "${archives[@]}") || { echo "Error: invalid archive" >&2; return 1; }
}


SCRIPT_PATH=$(realpath -s "$0")
SCRIPT_DIR=$(dirname "$(realpath "$0")")
SCRIPT=$(basename "$SCRIPT_PATH")

PBC=$(command -v proxmox-backup-client)
PBC_DOWNLOAD_URL='http://download.proxmox.com/debian/pbs-client/dists/trixie/main/binary-amd64'
ETC_DIR="/etc/$SCRIPT"

full_help=false
update_pbc=false

action=''
cfgfile="$ETC_DIR/config"
args=()

while (( $# > 0 )); do
  opt=$1
  shift
  if [ -z "$action" ]; then
    case "$opt" in
      -c|--config)
        cfgfile=$1
        shift
        [ -z "$cfgfile" ] && echo "Error: missing argument for $opt" >&2 && usage >&2 && exit 255
      ;;
      -h|--help)
        usage
        exit 0
      ;;
      -H|--full-help)
        full_help=true
      ;;
      -*)
        echo "Error: invalid option: $opt" >&2
        echo >&2
        usage >&2
        exit 255
      ;;
      *)
        action=$opt
      ;;
    esac
  elif [ "$action" == "install" ]; then
    case "$opt" in
      -u|--update)
        update_pbc=true
      ;;
      -*)
        echo "Error: invalid option: $opt" >&2
        echo >&2
        usage >&2
        exit 255
      ;;
      *)
        echo "Error: invalid argument: $opt" >&2
        echo >&2
        usage >&2
        exit 255
      ;;
    esac
  else
    args+=("$opt")
  fi
done


if [ -f "$cfgfile" ]; then
  perms=$(stat -c '%a' "$cfgfile")
  (( 8#$perms & 0022 )) && echo "Error: $cfgfile is writable by group or others!" >&2 && exit 202
  (( 8#$perms & 0007 )) && echo "Warning: $cfgfile is readable by others!" >&2
fi

if $full_help; then
  [ -f "$cfgfile" ] && source "$cfgfile"
  PBC=${PBC:-/usr/local/bin/proxmox-backup-client}
  full_usage
  exit $?
fi

[ -z "$action" ] && usage >&2 && exit 255

if [ "$action" == 'install' ]; then
  if [ -f "$cfgfile" ]; then
    source "$cfgfile" || exit $?
  fi
  PBC=${PBC:-/usr/local/bin/proxmox-backup-client}
  for exe in apt-cache apt-get dpkg dpkg-deb curl; do
    ! command -v "$exe" >/dev/null && echo "Error: $exe executable not found!" >&2 && exit 200
  done
else
  [ ! -f "$cfgfile" ] && echo "Error: $cfgfile: no such file" >&2 && exit 201
  [ ! -r "$cfgfile" ] && echo "Error: $cfgfile: not readable" >&2 && exit 201
  source "$cfgfile" || exit $?
  PBC=${PBC:-/usr/local/bin/proxmox-backup-client}
  [ ! -f "$PBC" ] && echo "Error: $PBC: no such file" >&2 && exit 200
  [ ! -x "$PBC" ] && echo "Error: $PBC: not executable" >&2 && exit 200
  ! command -v jq >/dev/null && echo "Error: jq executable not found!" >&2 && exit 200
fi

export PBS_REPOSITORY="$PBS_USER@$PBS_SERVER:$PBS_DATASTORE"
export PBS_PASSWORD="$PBS_PASSWORD"

case "$action" in
  backup)
    pbc-backup "${args[@]}" || exit $?
  ;;
  backup-cron)
    output=$(pbc-backup "${args[@]}" 2>&1)
    rc=$?
    (( rc != 0 )) && echo "$output" >&2
    exit $rc
  ;;
  list)
    pbc-list "${args[@]}" || exit $?
  ;;
  snapshot-list)
    select_group "${args[0]}"
    case $? in
      0) ;;
      2) echo "no snapshot groups found"; exit 0 ;;
      *) exit 1 ;;
    esac

    echo
    pbc-snapshot-list "$GROUP" "${args[@]:1}" || exit $?
  ;;
  catalog-shell)
    select_snapshot_and_archive "${args[0]}" "${args[1]}"
    case $? in
      0) ;;
      2) echo "no snapshots found"; exit 0 ;;
      3) echo "no archives found"; exit 0 ;;
      *) exit 1 ;;
    esac

    echo
    pbc-catalog-shell "$SNAPSHOT" "$ARCHIVE" || exit $?
  ;;
  mount)
    select_snapshot_and_archive "${args[0]}" "${args[1]}"
    case $? in
      0) ;;
      2) echo "no snapshots found"; exit 0 ;;
      3) echo "no archives found"; exit 0 ;;
      *) exit 1 ;;
    esac

    target=${args[2]}

    if [ -z "$target" ]; then
      while [ -z "$target" ]; do
        echo -n "Target path: "
        read -r target
        [ -d "$target" ] && break
        echo -e "\nError: $target: no such directory\n" >&2
        target=''
      done
    elif [ ! -d "$target" ]; then
      echo "Error: $target: no such directory" >&2
      exit 1
    fi

    echo
    pbc-mount "$SNAPSHOT" "$ARCHIVE" "$target" || exit $?
  ;;
  install)
    (( EUID != 0 )) && echo "Error: installation only works as root!" >&2 && exit 1

    install_proxmox_backup_client "$update_pbc" || exit $?

    if dpkg -s jq &>/dev/null; then
      $update_pbc && echo "Info: skip update of jq, package is managed by package manager" || echo "Info: jq is already installed"
    else
      apt_get_install jq || exit $?
    fi

    if [ ! -d "$ETC_DIR" ]; then
      echo "Creating config directory $ETC_DIR ..."
      ! mkdir -p "$ETC_DIR" && echo "Error creating directory!" >&2 && exit 3
    fi

    if [ -f "$SCRIPT_DIR/config.example" ]; then
      if $update_pbc || [ ! -f "$ETC_DIR/config.example" ]; then
        echo "Install example config to $ETC_DIR/config.example ..."
        ! install -m 0640 "$SCRIPT_DIR/config.example" "$ETC_DIR/config.example" && echo "Error installing file!" >&2 && exit 4
      fi
    fi

    echo "Info: successfully installed pbc dependencies"
  ;;
  *)
    "$PBC" "$action" "${args[@]}" || exit $?
  ;;
esac
