#!/bin/sh
#
# borg-apiscp-restore: restore ApisCP data from a Borg repository.
#
# Borg stores archived paths relative (leading "/" stripped) and preserves
# POSIX ACLs and xattrs natively, so restore is a plain `borg extract` with
# `--strip-components` to rebase the archived path under a staging target. No
# ACL/xattr replay is needed (that is the kopia engine's job, not Borg's).
#
# Archive naming (from the engine): siteN-shadow-{now}, siteN-info-{now},
# siteN-db-{now}, _system-{now}, _custom-<slug>-{now}. "newest" means the most
# recent archive of a given prefix.
#
# Usage:
#   borg-apiscp-restore list [PREFIX]
#       List archives, optionally filtered to a prefix glob (e.g. "site1").
#   borg-apiscp-restore list-json PREFIX
#       Raw `borg list --json` for archives matching PREFIX-* (for the module).
#   borg-apiscp-restore site SITE TARGET [--subpath P ...]
#       Restore SITE's newest shadow (+ info) into TARGET/{shadow,info}. With
#       --subpath, restore only those paths (relative to the site root).
#   borg-apiscp-restore account SITE TARGET
#       Restore a whole account: files (shadow+info) plus all databases (newest
#       SITE-db archive) into TARGET, databases under TARGET/databases/.
#   borg-apiscp-restore system TARGET
#       Restore the newest _system archive into TARGET.
#   borg-apiscp-restore restore-db SITE ENGINE DB TARGET
#       Restore one database dump (DB.sql) from the newest SITE-db archive into
#       TARGET/ (non-destructive).
#   borg-apiscp-restore import-db SITE ENGINE DB
#       DESTRUCTIVE: restore the newest dump of SITE's DB and import it into the
#       live database via ApisCP's native, site-context import.
#   borg-apiscp-restore owner-files SITE [--subpath P ...]
#   borg-apiscp-restore owner-account SITE
#   borg-apiscp-restore owner-restore-db SITE ENGINE DB
#   borg-apiscp-restore owner-import-db SITE ENGINE DB
#       Site-owner variants: restore INTO the site's own filesystem under
#       /.borg-restore/, chowned to the site owner; print the in-site path.
#       SITE is supplied by the trusted site-owner module (auth context).

set -u

_self_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
for _cand in \
	"${APISCP_BORG_LIB:-}" \
	"$_self_dir/../lib/apiscp-borg-common.sh" \
	/usr/local/lib/apiscp-borg/apiscp-borg-common.sh \
	/usr/lib/apiscp-borg/apiscp-borg-common.sh; do
	[ -n "$_cand" ] && [ -r "$_cand" ] && { . "$_cand"; _lib_loaded=1; break; }
done
[ "${_lib_loaded:-0}" = 1 ] || { echo "FATAL: cannot find apiscp-borg-common.sh" >&2; exit 1; }

CONFIG_FILE="${APISCP_BORG_CONFIG:-/etc/apiscp-borg/config}"
# shellcheck source=/dev/null
[ -r "$CONFIG_FILE" ] && . "$CONFIG_FILE"
: "${BORG_REPO:=}"
: "${BORG_RSH:=ssh -o BatchMode=yes}"
: "${VIRTBASE:=/home/virtual}"
: "${APNSCP_CMD:=/usr/local/apnscp/bin/cmd}"
export BORG_REPO BORG_RSH
[ -n "${BORG_PASSPHRASE:-}" ] && export BORG_PASSPHRASE
# Every real command needs a repository; help/usage does not.
case "${1:-}" in
	-h|--help|help|"") ;;
	*) [ -n "$BORG_REPO" ] || akp_die "BORG_REPO is not set (edit $CONFIG_FILE)" ;;
esac

usage() { awk 'NR>=2 && /^set -u/{exit} NR>=2{sub(/^# ?/,""); print}' "$0"; exit "${1:-2}"; }

# Number of path components in $1 (leading/trailing slashes ignored).
_strip_for() { printf '%s' "$1" | sed 's#^/*##; s#/*$##' | awk -F/ '{print NF}'; }
# Relative (leading-slash-stripped) form of a path, as Borg stores it.
_rel() { printf '%s' "$1" | sed 's#^/*##'; }
# Newest archive name for a prefix (PREFIX-*), or empty.
_newest() { akp_borg list --glob-archives "$1-*" --last 1 --format '{archive}{NL}' 2>/dev/null | head -1; }

cmd_list() {
	if [ -n "${1:-}" ]; then akp_borg list --glob-archives "$1-*"; else akp_borg list; fi
}
cmd_list_json() {
	[ -n "${1:-}" ] || akp_die "list-json: PREFIX required"
	akp_borg list --json --glob-archives "$1-*"
}

# Extract SITE's shadow/info into TARGET (optionally only --subpath paths).
_extract_site_files() {
	_site="$1"; _target="$2"; shift 2
	_subs=""
	while [ $# -gt 0 ]; do case "$1" in --subpath) _subs="$_subs${2:-}
"; shift 2 ;; *) shift ;; esac; done
	mkdir -p "$_target" || akp_die "cannot create $_target"
	_sc=$(_strip_for "$VIRTBASE/$_site")
	_any=0
	for _sub in shadow info; do
		_arch=$(_newest "$_site-$_sub")
		[ -n "$_arch" ] || continue
		if [ -n "$_subs" ] && [ "$_sub" = shadow ]; then
			# Selected paths only (relative to the site root, under shadow/).
			_oifs=$IFS; IFS='
'
			for _sp in $_subs; do
				[ -n "$_sp" ] || continue
				case "$_sp" in /*|*..*) akp_warn "skip unsafe subpath $_sp"; continue ;; esac
				if ( cd "$_target" && akp_borg extract --numeric-ids --strip-components "$_sc" "::$_arch" "$(_rel "$VIRTBASE/$_site")/shadow/$_sp" ); then
					_any=1; akp_log "restored $_sub/$_sp"
				else akp_warn "failed subpath $_sub/$_sp"; fi
			done
			IFS=$_oifs
		else
			if ( cd "$_target" && akp_borg extract --numeric-ids --strip-components "$_sc" "::$_arch" ); then
				_any=1; akp_log "restored $_sub ($_arch)"
			else akp_warn "failed to restore $_sub ($_arch)"; fi
		fi
	done
	return $(( _any == 1 ? 0 : 1 ))
}

cmd_site() {
	_site="${1:-}"; _target="${2:-}"; shift 2 2>/dev/null || usage 2
	case "$_site" in site[0-9]*) ;; *) akp_die "site: invalid site '$_site'" ;; esac
	[ -n "$_target" ] || usage 2
	_extract_site_files "$_site" "$_target" "$@" || akp_die "site: nothing restored for $_site"
	echo "Restored $_site to $_target. Review, then copy back into place." >&2
}

# Extract all databases (newest SITE-db archive) into TARGET/databases.
_extract_site_dbs() {
	_site="$1"; _target="$2"
	_arch=$(_newest "$_site-db")
	[ -n "$_arch" ] || { akp_warn "no database archive for $_site"; return 1; }
	mkdir -p "$_target/databases" || return 1
	_sc=$(_strip_for "$VIRTBASE/$_site/fst/tmp/.apiscp-borg-db")
	( cd "$_target/databases" && akp_borg extract --strip-components "$_sc" "::$_arch" ) \
		&& akp_log "restored databases ($_arch) -> $_target/databases" \
		|| { akp_warn "failed to restore databases for $_site"; return 1; }
}

cmd_account() {
	_site="${1:-}"; _target="${2:-}"
	case "$_site" in site[0-9]*) ;; *) akp_die "account: invalid site '$_site'" ;; esac
	[ -n "$_target" ] || usage 2
	mkdir -p "$_target" || akp_die "account: cannot create $_target"
	_extract_site_files "$_site" "$_target" || akp_die "account: no file archives for $_site"
	_extract_site_dbs "$_site" "$_target" || akp_warn "account: no databases restored (files only)"
	echo "Restored account $_site to $_target (files + databases under databases/)." >&2
}

cmd_system() {
	_target="${1:-}"; [ -n "$_target" ] || usage 2
	_arch=$(_newest "_system")
	[ -n "$_arch" ] || akp_die "system: no _system archive found"
	mkdir -p "$_target" || akp_die "system: cannot create $_target"
	( cd "$_target" && akp_borg extract --numeric-ids "::$_arch" ) || akp_die "system: extract failed"
	akp_log "restored system archive $_arch -> $_target"
	echo "Restored system paths to $_target. Review, then copy back into place." >&2
}

# restore-db SITE ENGINE DB TARGET: extract one dump to TARGET/DB.sql.
cmd_restore_db() {
	_site="${1:-}"; _eng="${2:-}"; _db="${3:-}"; _target="${4:-}"
	case "$_site" in site[0-9]*) ;; *) akp_die "restore-db: invalid site '$_site'" ;; esac
	case "$_eng" in mysql|pgsql) ;; *) akp_die "restore-db: ENGINE must be mysql or pgsql" ;; esac
	case "$_db" in ''|*[!A-Za-z0-9_-]*) akp_die "restore-db: invalid DB name '$_db'" ;; esac
	[ -n "$_target" ] || akp_die "restore-db: TARGET required"
	_arch=$(_newest "$_site-db"); [ -n "$_arch" ] || akp_die "restore-db: no database archive for $_site"
	mkdir -p "$_target" || akp_die "restore-db: cannot create $_target"
	_sc=$(_strip_for "$VIRTBASE/$_site/fst/tmp/.apiscp-borg-db/$_eng")
	_relpath="$(_rel "$VIRTBASE/$_site")/fst/tmp/.apiscp-borg-db/$_eng/$_db.sql"
	( cd "$_target" && akp_borg extract --strip-components "$_sc" "::$_arch" "$_relpath" ) \
		|| akp_die "restore-db: could not extract $_eng/$_db.sql from $_arch"
	[ -f "$_target/$_db.sql" ] || akp_die "restore-db: $_db.sql not found in archive"
	akp_log "restored $_eng db $_db for $_site to $_target/$_db.sql"
	printf '%s\n' "$_target/$_db.sql"
}

# import-db SITE ENGINE DB: restore newest dump and import into the live DB.
cmd_import_db() {
	_site="${1:-}"; _eng="${2:-}"; _db="${3:-}"
	case "$_site" in site[0-9]*) ;; *) akp_die "import-db: invalid site '$_site'" ;; esac
	case "$_eng" in mysql|pgsql) ;; *) akp_die "import-db: ENGINE must be mysql or pgsql" ;; esac
	case "$_db" in ''|*[!A-Za-z0-9_-]*) akp_die "import-db: invalid DB name '$_db'" ;; esac
	_dom=$(akp_site_domain "$_site"); [ -n "$_dom" ] || akp_die "import-db: no domain for $_site"
	_tmp=$(mktemp -d) || akp_die "import-db: cannot stage"
	cmd_restore_db "$_site" "$_eng" "$_db" "$_tmp" >/dev/null
	# ApisCP import resolves the path inside the site fs namespace: stage the dump
	# under the site fst and import via the site-relative path.
	_srel="/tmp/.apiscp-borg-import"; _fst=$(akp_site_fst "$_site")
	[ -d "$_fst" ] || { rm -rf "$_tmp"; akp_die "import-db: site fst not found ($_fst)"; }
	_stage="$_fst$_srel"; mkdir -p "$_stage" || { rm -rf "$_tmp"; akp_die "import-db: cannot stage under $_stage"; }
	cp "$_tmp/$_db.sql" "$_stage/$_db.sql" || { rm -rf "$_tmp" "$_stage"; akp_die "import-db: cannot stage dump"; }
	chmod 0644 "$_stage/$_db.sql" 2>/dev/null || true
	akp_log "importing $_eng db $_db into $_site ($_dom) (DESTRUCTIVE)"
	_rc=0; "$APNSCP_CMD" -d "$_dom" "$_eng:import" "$_db" "$_srel/$_db.sql" || _rc=1
	rm -rf "$_stage" "$_tmp"
	[ "$_rc" = 0 ] || akp_die "import-db: import of $_db failed"
	akp_log "imported $_eng db $_db into $_site"
}

# --- site-owner variants: restore into the site's own fs, chowned ------------

_owner_target() { printf '%s%s' "$(akp_site_fst "$1")" "/.borg-restore/$2-$(date +%s)"; }

cmd_owner_files() {
	_site="${1:-}"; [ $# -ge 1 ] && shift || usage 2
	case "$_site" in site[0-9]*) ;; *) akp_die "owner-files: invalid site '$_site'" ;; esac
	_fst=$(akp_site_fst "$_site"); [ -d "$_fst" ] || akp_die "owner-files: site fst not found"
	_rel="/.borg-restore/files-$(date +%s)"; _target="$_fst$_rel"
	_extract_site_files "$_site" "$_target" "$@" || { rm -rf "$_target"; akp_die "owner-files: nothing restored"; }
	akp_owner_chown "$_site" "$_fst/.borg-restore" || true
	printf '%s\n' "$_rel"
}

cmd_owner_account() {
	_site="${1:-}"
	case "$_site" in site[0-9]*) ;; *) akp_die "owner-account: invalid site '$_site'" ;; esac
	_fst=$(akp_site_fst "$_site"); [ -d "$_fst" ] || akp_die "owner-account: site fst not found"
	_rel="/.borg-restore/account-$(date +%s)"; _target="$_fst$_rel"
	mkdir -p "$_target" || akp_die "owner-account: cannot create restore dir"
	_extract_site_files "$_site" "$_target" || { rm -rf "$_target"; akp_die "owner-account: no file archives"; }
	_extract_site_dbs "$_site" "$_target" || akp_warn "owner-account: no databases restored"
	akp_owner_chown "$_site" "$_fst/.borg-restore" || true
	printf '%s\n' "$_rel"
}

cmd_owner_restore_db() {
	_site="${1:-}"; _eng="${2:-}"; _db="${3:-}"
	case "$_site" in site[0-9]*) ;; *) akp_die "owner-restore-db: invalid site '$_site'" ;; esac
	_fst=$(akp_site_fst "$_site"); [ -d "$_fst" ] || akp_die "owner-restore-db: site fst not found"
	_rel="/.borg-restore/db-$(date +%s)"; _target="$_fst$_rel"
	cmd_restore_db "$_site" "$_eng" "$_db" "$_target" >/dev/null
	akp_owner_chown "$_site" "$_fst/.borg-restore" || true
	printf '%s/%s.sql\n' "$_rel" "$_db"
}

cmd_owner_import_db() {
	_site="${1:-}"; _eng="${2:-}"; _db="${3:-}"
	case "$_site" in site[0-9]*) ;; *) akp_die "owner-import-db: invalid site '$_site'" ;; esac
	cmd_import_db "$_site" "$_eng" "$_db"
}

case "${1:-}" in
	list)            shift; cmd_list "$@" ;;
	list-json)       shift; cmd_list_json "$@" ;;
	site)            shift; cmd_site "$@" ;;
	account)         shift; cmd_account "$@" ;;
	system)          shift; cmd_system "$@" ;;
	restore-db)      shift; cmd_restore_db "$@" ;;
	import-db)       shift; cmd_import_db "$@" ;;
	owner-files)     shift; cmd_owner_files "$@" ;;
	owner-account)   shift; cmd_owner_account "$@" ;;
	owner-restore-db) shift; cmd_owner_restore_db "$@" ;;
	owner-import-db)  shift; cmd_owner_import_db "$@" ;;
	-h|--help|help|"") usage 0 ;;
	*) echo "unknown command: $1" >&2; usage 2 ;;
esac
