#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

APP_NAME="nextcloud-borg-backup"
APP_VERSION="2.0.0"

CONFIG_FILE="${NCB_CONFIG:-${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/config}"

die() {
  printf 'ERROR: %s\n' "$*" >&2
  exit 1
}

warn() {
  printf 'WARN: %s\n' "$*" >&2
}

info() {
  printf '%s\n' "$*"
}

have() {
  command -v "$1" >/dev/null 2>&1
}

shell_quote() {
  printf '%q' "$1"
}

usage() {
  cat <<'EOF'
nextcloud-borg-backup - Borg backup verified cloud-to-NAS backup and restore helper

Usage:
  nextcloud-borg-backup backup [--if-due]
  nextcloud-borg-backup health [--notify]
  nextcloud-borg-backup init
  nextcloud-borg-backup list
  nextcloud-borg-backup info [ARCHIVE]
  nextcloud-borg-backup search PATTERN [ARCHIVE]
  nextcloud-borg-backup mount [ARCHIVE] [MOUNT_DIR]
  nextcloud-borg-backup umount [MOUNT_DIR]
  nextcloud-borg-backup restore [ARCHIVE] [PATH] [DEST_DIR]
  nextcloud-borg-backup check [--verify-data]
  nextcloud-borg-backup prune
  nextcloud-borg-backup status
  nextcloud-borg-backup break-lock

Environment:
  NCB_CONFIG=/path/to/config
EOF
}

load_config() {
  [[ -r "$CONFIG_FILE" ]] || die "Config not readable: $CONFIG_FILE"
  # shellcheck disable=SC1090
  source "$CONFIG_FILE"

  : "${SOURCE_DIR:=${HOME}/Nextcloud}"
  : "${REPO_DIR:=/net/thor/volume1/NetBackup/Nextcloud/borg-repo}"
  : "${ARCHIVE_PREFIX:=nextcloud}"
  : "${BORG_BIN:=borg}"
  : "${BORG_ENCRYPTION:=repokey-blake2}"
  : "${BORG_PASSPHRASE_FILE:=${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/passphrase}"
  : "${BORG_COMPRESSION:=zstd,6}"
  : "${BORG_REMOTE_PATH:=borg}"
  : "${BORG_FILES_CACHE:=}"
  : "${RETENTION_WITHIN:=4w}"
  : "${PRUNE_AFTER_BACKUP:=true}"
  : "${COMPACT_AFTER_PRUNE:=true}"
  : "${REPOSITORY_CHECK_AFTER_BACKUP:=true}"
  : "${FULL_CHECK_INTERVAL_DAYS:=7}"
  : "${FULL_CHECK_VERIFY_DATA:=false}"
  : "${ALLOW_EMPTY_SOURCE:=false}"
  : "${MIN_FREE_REPO_GB:=5}"
  : "${PRECHECK_TIMEOUT_SECONDS:=60}"
  : "${LOG_DIR:=${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}/logs}"
  : "${STATE_DIR:=${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}}"
  : "${RESTORE_DIR:=${HOME}/Nextcloud-Restore}"
  : "${MOUNT_DIR:=${XDG_RUNTIME_DIR:-/tmp}/${APP_NAME}-mount}"
  : "${EXCLUDE_FILE:=${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/excludes}"
  : "${EXCLUDE_CACHES:=true}"
  : "${ONE_FILE_SYSTEM:=true}"
  : "${LOCK_FILE:=${STATE_DIR}/${APP_NAME}.lock}"

  export BORG_RELOCATED_REPO_ACCESS_IS_OK="${BORG_RELOCATED_REPO_ACCESS_IS_OK:-yes}"
  export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK="${BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK:-yes}"
  export BORG_REMOTE_PATH
  [[ -n "$BORG_FILES_CACHE" ]] && export BORG_FILES_CACHE

  if [[ -f "$BORG_PASSPHRASE_FILE" ]]; then
    export BORG_PASSCOMMAND="cat $(shell_quote "$BORG_PASSPHRASE_FILE")"
  fi
}

require_command() {
  have "$1" || die "Required command missing: $1"
}

ensure_dirs() {
  mkdir -p "$LOG_DIR" "$STATE_DIR"
}

with_lock() {
  exec 9>"$LOCK_FILE"
  flock -n 9 || die "Another ${APP_NAME} run is already active: $LOCK_FILE"
}

repo_parent() {
  dirname "$REPO_DIR"
}

check_passphrase_for_encryption() {
  case "$BORG_ENCRYPTION" in
    none|authenticated*) return 0 ;;
  esac
  [[ -r "$BORG_PASSPHRASE_FILE" ]] || die "BORG_PASSPHRASE_FILE missing/read-protected: $BORG_PASSPHRASE_FILE"
}

borg_repo_exists() {
  [[ -d "$REPO_DIR" && -f "$REPO_DIR/config" ]]
}

init_repo() {
  require_command "$BORG_BIN"
  check_passphrase_for_encryption
  timeout 60 test -d "$(repo_parent)" || die "NAS parent unavailable; mount NAS before init"
  local actual_source
  actual_source="$(findmnt --direction backward --first-only -n -o SOURCE -T "$(repo_parent)")"
  [[ "$actual_source" == "${EXPECTED_NAS_SOURCE:-thor:/volume1/NetBackup}" ]] || die "Wrong NAS backing mount: $actual_source"
  if borg_repo_exists; then
    info "-- Borg repository already exists: $REPO_DIR"
    return 0
  fi
  info "-- Initializing Borg repository: $REPO_DIR"
  "$BORG_BIN" init --encryption="$BORG_ENCRYPTION" "$REPO_DIR"
}

archive_names() {
  "$BORG_BIN" list --short --glob-archives "${ARCHIVE_PREFIX}-*" "$REPO_DIR"
}

latest_archive() {
  archive_names | tail -n 1
}

prune_repo() {
  info "-- Pruning archives with retention: keep-within $RETENTION_WITHIN"
  "$BORG_BIN" prune --list --show-rc --glob-archives "${ARCHIVE_PREFIX}-*" --keep-within "$RETENTION_WITHIN" --keep-last 7 "$REPO_DIR"
}

compact_repo() {
  info "-- Compacting repository"
  "$BORG_BIN" compact "$REPO_DIR"
}

check_repo() {
  local verify_data="${1:-false}"
  if [[ "$verify_data" == "true" ]]; then
    info "-- Running full Borg check with data verification"
    "$BORG_BIN" check --verify-data "$REPO_DIR"
  else
    info "-- Running Borg repository check"
    "$BORG_BIN" check --repository-only "$REPO_DIR"
  fi
}

cmd_runner() {
  load_config
  export REPO_DIR ARCHIVE_PREFIX BORG_BIN BORG_COMPRESSION BORG_PASSPHRASE_FILE
  export RETENTION_WITHIN PRUNE_AFTER_BACKUP COMPACT_AFTER_PRUNE
  export STATE_DIR LOG_DIR MIN_FREE_REPO_GB EXCLUDE_FILE LOCK_FILE
  export FULL_CHECK_INTERVAL_DAYS FULL_CHECK_VERIFY_DATA
  # New settings are optional in existing configs, and inherited by the runner.
  export REMOTE="${REMOTE:-Nextcloud:}"
  export STAGING_DIR="${STAGING_DIR:-${HOME}/.local/share/nextcloud-borg-backup/staging}"
  export EXPECTED_NAS_SOURCE="${EXPECTED_NAS_SOURCE:-thor:/volume1/NetBackup}"
  export EXPECTED_NAS_FSTYPE="${EXPECTED_NAS_FSTYPE:-nfs4}"
  export BACKUP_INTERVAL_HOURS="${BACKUP_INTERVAL_HOURS:-24}"
  export MAX_BACKUP_AGE_HOURS="${MAX_BACKUP_AGE_HOURS:-36}"
  export MIN_SOURCE_FILES="${MIN_SOURCE_FILES:-1}"
  export MAX_SOURCE_DROP_PERCENT="${MAX_SOURCE_DROP_PERCENT:-20}"
  export MAX_STAGE_DELETES="${MAX_STAGE_DELETES:-1000}"
  export MIN_FREE_STAGE_GB="${MIN_FREE_STAGE_GB:-5}"
  export COMMAND_TIMEOUT_SECONDS="${COMMAND_TIMEOUT_SECONDS:-21600}"
  export NCB_RUNNER="${NCB_RUNNER:-${HOME}/.local/libexec/nextcloud-borg-runner.py}"
  exec python3 "$NCB_RUNNER" "$@"
}

cmd_backup() { cmd_runner backup "$@"; }

cmd_list() {
  load_config
  require_command "$BORG_BIN"
  "$BORG_BIN" list "$REPO_DIR"
}

cmd_info() {
  load_config
  require_command "$BORG_BIN"
  local archive="${1:-}"
  [[ -n "$archive" ]] || archive="$(latest_archive)"
  [[ -n "$archive" ]] || die "No archive found"
  "$BORG_BIN" info "${REPO_DIR}::${archive}"
}

cmd_search() {
  load_config
  require_command "$BORG_BIN"
  local pattern="${1:-}"
  local archive="${2:-}"
  [[ -n "$pattern" ]] || die "Missing search pattern"

  if [[ -n "$archive" ]]; then
    "$BORG_BIN" list --short "${REPO_DIR}::${archive}" | grep -i -- "$pattern" || true
    return 0
  fi

  local found=0
  while IFS= read -r archive; do
    [[ -n "$archive" ]] || continue
    while IFS= read -r path; do
      printf '%s\t%s\n' "$archive" "$path"
      found=1
    done < <("$BORG_BIN" list --short "${REPO_DIR}::${archive}" | grep -i -- "$pattern" || true)
  done < <(archive_names)
  [[ "$found" -eq 1 ]] || warn "No matches found for: $pattern"
}

cmd_mount() {
  load_config
  require_command "$BORG_BIN"
  local archive="${1:-}"
  local mount_dir="${2:-$MOUNT_DIR}"
  mkdir -p "$mount_dir"
  if [[ -n "$archive" ]]; then
    info "-- Mounting archive $archive at $mount_dir"
    "$BORG_BIN" mount "${REPO_DIR}::${archive}" "$mount_dir"
  else
    info "-- Mounting repository archive overview at $mount_dir"
    "$BORG_BIN" mount "$REPO_DIR" "$mount_dir"
  fi
  info "-- Mounted at: $mount_dir"
}

cmd_umount() {
  load_config
  local mount_dir="${1:-$MOUNT_DIR}"
  if have borg; then
    "$BORG_BIN" umount "$mount_dir" 2>/dev/null && return 0
  fi
  if have fusermount3; then
    fusermount3 -u "$mount_dir"
  elif have fusermount; then
    fusermount -u "$mount_dir"
  else
    umount "$mount_dir"
  fi
}

normalize_restore_path() {
  local path="$1"
  path="${path#/}"
  path="${path#./}"
  printf '%s\n' "$path"
}

cmd_restore() {
  load_config
  require_command "$BORG_BIN"
  local archive="${1:-}"
  local restore_path="${2:-}"
  local dest_dir="${3:-}"

  [[ -n "$archive" ]] || archive="$(latest_archive)"
  [[ -n "$archive" ]] || die "No archive found"
  [[ -n "$dest_dir" ]] || dest_dir="${RESTORE_DIR}/${archive}"
  mkdir -p "$dest_dir"

  info "-- Restoring from archive: $archive"
  info "-- Destination: $dest_dir"
  (
    cd "$dest_dir"
    if [[ -n "$restore_path" ]]; then
      restore_path="$(normalize_restore_path "$restore_path")"
      "$BORG_BIN" extract "${REPO_DIR}::${archive}" "$restore_path"
    else
      "$BORG_BIN" extract "${REPO_DIR}::${archive}"
    fi
  )
  info "-- Restore finished: $dest_dir"
}

cmd_check() {
  load_config
  ensure_dirs
  with_lock
  require_command "$BORG_BIN"
  local verify_data=false
  [[ "${1:-}" == "--verify-data" ]] && verify_data=true
  check_repo "$verify_data"
}

cmd_status() {
  load_config
  require_command "$BORG_BIN"
  info "Config: $CONFIG_FILE"
  info "Source: $SOURCE_DIR"
  info "Repository: $REPO_DIR"
  info "Retention: $RETENTION_WITHIN"
  info "Latest archive: $(latest_archive || true)"
  info
  "$BORG_BIN" info "$REPO_DIR"
}

cmd_break_lock() {
  load_config
  require_command "$BORG_BIN"
  "$BORG_BIN" break-lock "$REPO_DIR"
}

main() {
  local cmd="${1:-}"
  [[ $# -gt 0 ]] && shift || true

  case "$cmd" in
    backup) cmd_backup "$@" ;;
    health) cmd_runner health "$@" ;;
    init) load_config; ensure_dirs; with_lock; init_repo "$@" ;;
    list) cmd_list "$@" ;;
    info) cmd_info "$@" ;;
    search) cmd_search "$@" ;;
    mount) cmd_mount "$@" ;;
    umount|unmount) cmd_umount "$@" ;;
    restore) cmd_restore "$@" ;;
    check) cmd_check "$@" ;;
    prune) load_config; ensure_dirs; with_lock; prune_repo "$@"; compact_repo ;;
    status) cmd_status "$@" ;;
    break-lock|unlock) load_config; ensure_dirs; with_lock; cmd_break_lock "$@" ;;
    -h|--help|help|"") usage ;;
    --version|version) printf '%s %s\n' "$APP_NAME" "$APP_VERSION" ;;
    *) usage >&2; die "Unknown command: $cmd" ;;
  esac
}

main "$@"
