#!/bin/bash
# Author: Christophe Casalegno / Brain 0verride
# Contact: christophe.casalegno@scalarx.com
# sxmdbsync
# Version 1.2
#
# Copyright (c) 2026 Christophe Casalegno
#
# This program is free software: you can redistribute it and/or modify
#
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>
#
# The license is available on this server here:
# https://www.christophe-casalegno.com/licences/gpl-3.0.txt

set -euo pipefail
IFS=$'\n\t'

SCRIPT_VERSION="1.2"
LOGFILE="${SXMDBSYNC_LOG:-$HOME/sxmdbsync.log}"
SSH_BIN="${SXMDBSYNC_SSH_BIN:-ssh}"
LOCKFILE=""
LOCAL_TEMP_CNF=""

method=""
sync_user=""
sync_host=""
sshport="65022"
dryrun=0
reset_target=1
auto_source_databases=0

local_db_user=""
local_db_pass=""
local_db_pass_set=0
local_db_pass_file=""
local_db_cnf=""
local_auth_mode=""

remote_db_user=""
remote_db_pass=""
remote_db_pass_set=0
remote_db_pass_file=""
remote_db_cnf=""
remote_auth_mode=""

# An environment-provided variable keeps its export attribute after a plain
# Bash assignment. Credentials loaded below must remain shell-local.
export -n local_db_user local_db_pass local_db_pass_set local_db_pass_file
export -n remote_db_user remote_db_pass remote_db_pass_set remote_db_pass_file
unset MYSQL_PWD

local_sql_bin=""
local_dump_bin=""
remote_sql_bin=""
remote_dump_bin=""

DUMP_COMMON_ARGS=()

SYSTEM_DATABASES_REGEX='^(information_schema|performance_schema|mysql|sys)$'

SSH_OPTIONS=()
DB_SOURCE_NAMES=()
DB_TARGET_NAMES=()

LOCAL_SQL_ARGS=()
LOCAL_DUMP_ARGS=()

function cleanup_local_temp_cnf()
{
	if [[ -n "$LOCAL_TEMP_CNF" ]]
	then
		rm -f -- "$LOCAL_TEMP_CNF"
		LOCAL_TEMP_CNF=""
	fi
}

trap cleanup_local_temp_cnf EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

function need_arg()
{
	local opt="$1"
	local val="${2:-}"

	if [[ -z "$val" || "$val" == -* ]]
	then
		echo "Error: missing value for $opt" >&2
		exit 2
	fi
}

function need_inline_arg()
{
	local opt="$1"
	local val="$2"

	if [[ -z "$val" ]]
	then
		echo "Error: missing value for $opt" >&2
		exit 2
	fi
}

function usage()
{
	cat <<'EOF_USAGE'
sxmdbsync version 1.2

sxmdbsync --method METHOD --user USER --host HOST [options]

Methods:
  antesync   sync local databases to the remote server over ssh
  retrosync  sync remote databases back to the local server over ssh

Required selection:
  --database DB[:TARGET]    select one database, optionally remapped on target
  --all-user-databases      sync all non-system databases from the source side

SSH options:
  --user, -u USER           remote ssh user
  --host, -H HOST           remote ssh host
  --port, -p PORT           remote ssh port (default: 65022)
  --ssh-option, -o OPT      extra ssh -o option (repeatable)

Local DB auth:
  --local-db-cnf FILE       local defaults-extra-file
  --local-db-user USER      local DB username
  --local-db-pass-file FILE read local DB password from a private file
  --local-db-pass PASS      legacy inline password (visible in process argv)

Remote DB auth:
  --remote-db-cnf FILE      remote defaults-extra-file path
  --remote-db-user USER     remote DB username
  --remote-db-pass-file FILE read remote DB password from a private local file
  --remote-db-pass PASS     legacy inline password (visible in process argv)

Sync behavior:
  --no-reset-target         import into existing target DB without dropping/recreating it
  --dry-run, -n             print the planned operations without executing them

Notes:
  - when run as root and no local DB auth is provided, sxmdbsync uses local root socket auth
  - when ssh user is root and no remote DB auth is provided, sxmdbsync uses remote root socket auth
  - when run as a non-root user with no explicit local auth, sxmdbsync falls back to ~/.my.cnf if present
  - when ssh user is non-root with no explicit remote auth, sxmdbsync falls back to ~/.my.cnf on the remote side if present
  - target reset is enabled by default for true sync semantics; target absence after DROP and presence after CREATE are verified before import
  - disable target reset only if your DB user cannot recreate the target schema
  - prefer --local-db-cnf / --remote-db-cnf or the password-file options over inline passwords

Examples:
  sxmdbsync --method retrosync --user root --host 192.0.2.10 --database sx
  sxmdbsync -m antesync -u root -H 192.0.2.10 -d sx --dry-run
  sxmdbsync --method retrosync --user root --host 192.0.2.10 --database olddb:newdb
  sxmdbsync --method antesync --user deploy --host 192.0.2.10 --all-user-databases --local-db-cnf ~/.my.cnf --remote-db-cnf /home/deploy/.my.cnf
EOF_USAGE
}

function version()
{
	echo "sxmdbsync version $SCRIPT_VERSION"
}

function log_ts()
{
	date '+%Y-%m-%d %H:%M:%S'
}

function init_logfile()
{
	local logdir=""

	if [[ -z "$LOGFILE" ]]
	then
		return
	fi

	logdir="$(dirname "$LOGFILE")"
	if ! mkdir -p "$logdir" 2>/dev/null || ! touch "$LOGFILE" 2>/dev/null
	then
		echo "Warning: cannot write log file ($LOGFILE), logging disabled" >&2
		LOGFILE=""
	fi
}

function log_event()
{
	local status="$1"
	local detail="$2"
	local ts=""

	[[ -z "$LOGFILE" ]] && return

	ts="$(log_ts)"
	if ! printf '%s - %s - %s\n' "$ts" "$status" "$detail" >> "$LOGFILE" 2>/dev/null
	then
		echo "Warning: cannot append to log file ($LOGFILE), logging disabled" >&2
		LOGFILE=""
	fi
}

function die()
{
	local msg="$1"
	echo "Error: $msg" >&2
	log_event "ERROR" "$msg"
	exit 1
}

function warn()
{
	local msg="$1"
	echo "Warning: $msg" >&2
	log_event "WARN" "$msg"
}

function info()
{
	local msg="$1"
	echo "$msg"
	log_event "INFO" "$msg"
}

function require_cmd()
{
	local cmd="$1"

	command -v "$cmd" >/dev/null 2>&1 || die "missing required command: $cmd"
}

function trim()
{
	local s="$1"
	s="${s#"${s%%[![:space:]]*}"}"
	s="${s%"${s##*[![:space:]]}"}"
	printf '%s' "$s"
}

function shell_quote_squote()
{
	local value="$1"

	printf "'"
	while [[ "$value" == *"'"* ]]
	do
		printf "%s'\\\\''" "${value%%\'*}"
		value="${value#*\'}"
	done
	printf "%s'" "$value"
}

function sql_quote_squote()
{
	printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
}

function validate_host()
{
	local host="$1"

	[[ -n "$host" ]] || die "empty host is not allowed"
	[[ "$host" != -* ]] || die "invalid host ($host)"
	[[ "$host" =~ ^[A-Za-z0-9._:-]+$ ]] || die "invalid host ($host)"
}

function validate_ssh_user()
{
	local user="$1"

	[[ -n "$user" ]] || die "empty ssh user is not allowed"
	[[ "$user" =~ ^[A-Za-z_][A-Za-z0-9._-]*$ ]] || die "invalid ssh user ($user)"
}

function validate_db_name()
{
	local db="$1"

	[[ -n "$db" ]] || die "empty database name is not allowed"
	[[ "$db" =~ ^[A-Za-z0-9_.\$-]+$ ]] || die "invalid database name ($db)"
}

function validate_mysql_user()
{
	local user="$1"

	[[ -n "$user" ]] || die "empty mysql user is not allowed"
	[[ "$user" =~ ^[A-Za-z0-9_.\$@-]+$ ]] || die "invalid mysql user ($user)"
}

function validate_option_value()
{
	local opt="$1"
	local value="$2"

	[[ "$value" != *$'\n'* ]] || die "newline is not allowed in $opt"
	[[ "$value" != *$'\r'* ]] || die "carriage return is not allowed in $opt"
}

function secret_from_private_file()
{
	local file="$1"
	local label="$2"
	local mode=""
	local owner=""
	local value=""

	export -n value
	[[ ! -L "$file" && -f "$file" && -r "$file" ]] || die "$label must be a readable regular non-symlink file: $file"
	mode="$(stat -c '%a' "$file" 2>/dev/null)" || die "unable to read $label mode: $file"
	if (( (8#$mode & 077) != 0 ))
	then
		die "$label must not be group/world accessible: $file (mode $mode)"
	fi
	owner="$(stat -c '%u' "$file" 2>/dev/null)" || die "unable to read $label owner: $file"
	[[ "$owner" = "$EUID" ]] || die "$label owner must match the current user: $file"
	value="$(<"$file")"
	[[ -n "$value" && "$value" != *$'\n'* && "$value" != *$'\r'* ]] || die "$label must contain one non-empty line"
	printf '%s' "$value"
}

function mysql_option_escape()
{
	local value="$1"

	value="${value//\\/\\\\}"
	value="${value//\"/\\\"}"
	printf '%s' "$value"
}

function create_local_credentials_file()
{
	local user="$1"
	local pass="$2"
	local escaped_user=""
	local escaped_pass=""
	local oldumask=""

	export -n pass escaped_pass

	cleanup_local_temp_cnf
	escaped_user="$(mysql_option_escape "$user")"
	escaped_pass="$(mysql_option_escape "$pass")"
	oldumask="$(umask)"
	umask 077
	LOCAL_TEMP_CNF="$(mktemp "${TMPDIR:-/tmp}/stackx-sxmdbsync-local.XXXXXX.cnf")" || {
		umask "$oldumask"
		return 1
	}
	umask "$oldumask"

	if ! printf '[client]\nuser="%s"\npassword="%s"\n' "$escaped_user" "$escaped_pass" >"$LOCAL_TEMP_CNF"
	then
		cleanup_local_temp_cnf
		return 1
	fi
	chmod 0600 "$LOCAL_TEMP_CNF" || {
		cleanup_local_temp_cnf
		return 1
	}
}

function acquire_lock()
{
	local lockdir=""
	local lock_parent=""
	local current_uid=""
	local parent_owner=""
	local parent_mode=""
	local lockdir_owner=""
	local lockdir_mode=""
	local lockdir_fd=""
	local lockdir_path_id=""
	local lockdir_fd_id=""
	local lock_io_file=""
	local lock_path_id=""
	local lock_fd_id=""
	local lock_owner=""
	local lock_links=""
	local oldumask=""
	local noclobber_was_set=0
	local lockfd=""

	command -v flock >/dev/null 2>&1 || die "flock is required for sxmdbsync serialization"
	current_uid="$(id -u)"

	if [[ "$EUID" -eq 0 ]]
	then
		lock_parent="/run"
		lockdir="/run/stackx-locks"
	else
		if [[ -n "${XDG_RUNTIME_DIR:-}" && -d "$XDG_RUNTIME_DIR" && ! -L "$XDG_RUNTIME_DIR" ]]
		then
			lock_parent="${XDG_RUNTIME_DIR%/}"
		elif [[ -n "${HOME:-}" && -d "$HOME" && ! -L "$HOME" ]]
		then
			[[ ! -L "$HOME/.cache" ]] || die "private lock parent is a symlink ($HOME/.cache)"
			if [[ ! -e "$HOME/.cache" ]]
			then
				mkdir -m 0700 -- "$HOME/.cache" || die "cannot create private lock parent ($HOME/.cache)"
			fi
			lock_parent="$HOME/.cache"
		else
			die "no private runtime directory is available for the sxmdbsync lock"
		fi
		lockdir="$lock_parent/stackx-locks"
	fi

	[[ -d "$lock_parent" && ! -L "$lock_parent" ]] || die "invalid lock parent ($lock_parent)"
	parent_owner="$(stat -c %u -- "$lock_parent" 2>/dev/null)" || die "cannot stat lock parent ($lock_parent)"
	parent_mode="$(stat -c %a -- "$lock_parent" 2>/dev/null)" || die "cannot read lock parent mode ($lock_parent)"
	[[ "$parent_owner" = "$current_uid" ]] || die "lock parent is not owned by uid $current_uid ($lock_parent)"
	(( (8#$parent_mode & 0022) == 0 )) || die "lock parent is group/world writable ($lock_parent)"
	[[ ! -L "$lockdir" ]] || die "lock directory is a symlink ($lockdir)"
	if [[ ! -e "$lockdir" ]]
	then
		if [[ "$EUID" -eq 0 ]]
		then
			mkdir -m 0755 -- "$lockdir" || die "cannot create root lock directory ($lockdir)"
		else
			mkdir -m 0700 -- "$lockdir" || die "cannot create private lock directory ($lockdir)"
		fi
	fi
	[[ -d "$lockdir" && ! -L "$lockdir" ]] || die "invalid lock directory ($lockdir)"
	lockdir_owner="$(stat -c %u -- "$lockdir" 2>/dev/null)" || die "cannot stat lock directory ($lockdir)"
	lockdir_mode="$(stat -c %a -- "$lockdir" 2>/dev/null)" || die "cannot read lock directory mode ($lockdir)"
	[[ "$lockdir_owner" = "$current_uid" ]] || die "lock directory is not owned by uid $current_uid ($lockdir)"
	(( (8#$lockdir_mode & 0022) == 0 )) || die "lock directory is group/world writable ($lockdir)"
	exec {lockdir_fd}<"$lockdir" || die "cannot open lock directory ($lockdir)"
	lockdir_path_id="$(stat -c '%d:%i' -- "$lockdir" 2>/dev/null)" || die "cannot stat lock directory path ($lockdir)"
	lockdir_fd_id="$(stat -Lc '%d:%i' -- "/proc/self/fd/$lockdir_fd" 2>/dev/null)" || die "cannot stat lock directory descriptor ($lockdir)"
	[[ "$lockdir_path_id" = "$lockdir_fd_id" ]] || die "lock directory changed while opening ($lockdir)"
	LOCKFILE="$lockdir/sxmdbsync.lock"
	lock_io_file="/proc/self/fd/$lockdir_fd/sxmdbsync.lock"

	if [[ -L "$lock_io_file" ]]
	then
		die "lockfile is a symlink ($LOCKFILE)"
	fi

	if [[ -e "$lock_io_file" && ! -f "$lock_io_file" ]]
	then
		die "lockfile is not a regular file ($LOCKFILE)"
	fi

	oldumask="$(umask)"
	umask 077
	if [[ -e "$lock_io_file" ]]
	then
		exec {lockfd}<>"$lock_io_file" || lockfd=""
	else
		[[ -o noclobber ]] && noclobber_was_set=1
		set -o noclobber
		exec {lockfd}>"$lock_io_file" || lockfd=""
		[[ "$noclobber_was_set" = "1" ]] || set +o noclobber
		if [[ -z "$lockfd" && -f "$lock_io_file" && ! -L "$lock_io_file" ]]
		then
			exec {lockfd}<>"$lock_io_file" || lockfd=""
		fi
	fi
	umask "$oldumask"
	[[ -n "$lockfd" ]] || die "cannot open lock $LOCKFILE"
	lock_path_id="$(stat -c '%d:%i' -- "$lock_io_file" 2>/dev/null)" || die "cannot stat lock path ($LOCKFILE)"
	lock_fd_id="$(stat -Lc '%d:%i' -- "/proc/self/fd/$lockfd" 2>/dev/null)" || die "cannot stat lock descriptor ($LOCKFILE)"
	lock_owner="$(stat -Lc %u -- "/proc/self/fd/$lockfd" 2>/dev/null)" || die "cannot stat lock owner ($LOCKFILE)"
	lock_links="$(stat -Lc %h -- "/proc/self/fd/$lockfd" 2>/dev/null)" || die "cannot stat lock links ($LOCKFILE)"
	[[ "$lock_path_id" = "$lock_fd_id" && "$lock_owner" = "$current_uid" && "$lock_links" = "1" ]] || die "lock path/descriptor validation failed ($LOCKFILE)"
	chmod 0600 "/proc/self/fd/$lockfd" || die "cannot protect lock $LOCKFILE"
	if ! flock -n "$lockfd"
	then
		die "sxmdbsync is already running"
	fi
	[[ "$(stat -c '%d:%i' -- "$lockdir" 2>/dev/null)" = "$lockdir_fd_id" ]] || die "lock directory changed before lock publication ($lockdir)"
	[[ "$(stat -c '%d:%i' -- "$lock_io_file" 2>/dev/null)" = "$lock_fd_id" ]] || die "lockfile changed before lock publication ($LOCKFILE)"
	: > "/proc/self/fd/$lockfd"
	printf '%s\n' "$$" >&"$lockfd"
	exec {lockdir_fd}>&-
}

function detect_local_bins()
{
	if command -v mariadb >/dev/null 2>&1
	then
		local_sql_bin="mariadb"
	else
		if command -v mysql >/dev/null 2>&1
		then
			local_sql_bin="mysql"
		else
			die "unable to find mariadb/mysql in PATH"
		fi
	fi
}

function detect_local_dump_bin()
{
	if command -v mariadb-dump >/dev/null 2>&1
	then
		local_dump_bin="mariadb-dump"
	else
		if command -v mysqldump >/dev/null 2>&1
		then
			local_dump_bin="mysqldump"
		else
			die "unable to find mariadb-dump/mysqldump in PATH"
		fi
	fi
}

function resolve_local_auth()
{
	if [[ -n "$local_db_cnf" ]]
	then
		[[ -r "$local_db_cnf" ]] || die "local defaults file not readable: $local_db_cnf"
		local_auth_mode="cnf"
		return
	fi

	if [[ -n "$local_db_user" || "$local_db_pass_set" -eq 1 ]]
	then
		[[ -n "$local_db_user" ]] || die "--local-db-user is required when local credentials are provided"
		validate_mysql_user "$local_db_user"
		local_auth_mode="credentials"
		return
	fi

	if [[ "${EUID}" -eq 0 ]]
	then
		local_auth_mode="socket"
		return
	fi

	if [[ -r "$HOME/.my.cnf" ]]
	then
		local_db_cnf="$HOME/.my.cnf"
		local_auth_mode="cnf"
		return
	fi

	die "unable to resolve local DB auth, use --local-db-cnf or --local-db-user/--local-db-pass, or run as root"
}

function resolve_remote_auth()
{
	if [[ -n "$remote_db_cnf" ]]
	then
		remote_auth_mode="cnf"
		return
	fi

	if [[ -n "$remote_db_user" || "$remote_db_pass_set" -eq 1 ]]
	then
		[[ -n "$remote_db_user" ]] || die "--remote-db-user is required when remote credentials are provided"
		validate_mysql_user "$remote_db_user"
		remote_auth_mode="credentials"
		return
	fi

	if [[ "$sync_user" = "root" ]]
	then
		remote_auth_mode="socket"
		return
	fi

	remote_auth_mode="homecnf"
}

function infer_local_auth_mode_preview()
{
	if [[ -n "$local_db_cnf" ]]
	then
		printf '%s' "cnf"
	elif [[ -n "$local_db_user" || "$local_db_pass_set" -eq 1 ]]
	then
		printf '%s' "credentials"
	elif [[ "${EUID}" -eq 0 ]]
	then
		printf '%s' "socket"
	elif [[ -r "$HOME/.my.cnf" ]]
	then
		printf '%s' "cnf"
	else
		printf '%s' "unresolved"
	fi
}

function infer_remote_auth_mode_preview()
{
	if [[ -n "$remote_db_cnf" ]]
	then
		printf '%s' "cnf"
	elif [[ -n "$remote_db_user" || "$remote_db_pass_set" -eq 1 ]]
	then
		printf '%s' "credentials"
	elif [[ "$sync_user" = "root" ]]
	then
		printf '%s' "socket"
	else
		printf '%s' "homecnf"
	fi
}

function build_local_auth_args()
{
	LOCAL_SQL_ARGS=()
	LOCAL_DUMP_ARGS=()

	case "$local_auth_mode" in
		cnf)
			LOCAL_SQL_ARGS+=("--defaults-extra-file=$local_db_cnf")
			LOCAL_DUMP_ARGS+=("--defaults-extra-file=$local_db_cnf")
			;;
		credentials)
			create_local_credentials_file "$local_db_user" "$local_db_pass" || die "unable to create restricted local DB credentials"
			LOCAL_SQL_ARGS+=("--defaults-extra-file=$LOCAL_TEMP_CNF")
			LOCAL_DUMP_ARGS+=("--defaults-extra-file=$LOCAL_TEMP_CNF")
			;;
		socket)
			LOCAL_SQL_ARGS+=(--protocol=socket -uroot)
			LOCAL_DUMP_ARGS+=(--protocol=socket -uroot)
			;;
		*)
			die "unsupported local auth mode ($local_auth_mode)"
			;;
	esac
}

function remote_helper_body()
{
	cat <<'EOF_REMOTE'
set -euo pipefail
IFS=$'\n\t'
export -n REMOTE_DB_PASS REMOTE_TEMP_CNF pass escaped_pass
unset MYSQL_PWD
REMOTE_TEMP_CNF=""

function remote_cleanup_credentials_file()
{
	if [[ -n "$REMOTE_TEMP_CNF" ]]
	then
		rm -f -- "$REMOTE_TEMP_CNF"
		REMOTE_TEMP_CNF=""
	fi
}

function remote_mysql_option_escape()
{
	local value="$1"

	value="${value//\\/\\\\}"
	value="${value//\"/\\\"}"
	printf '%s' "$value"
}

function remote_create_credentials_file()
{
	local user="$1"
	local pass="$2"
	local escaped_user=""
	local escaped_pass=""
	local oldumask=""

	export -n pass escaped_pass

	remote_cleanup_credentials_file
	escaped_user="$(remote_mysql_option_escape "$user")"
	escaped_pass="$(remote_mysql_option_escape "$pass")"
	oldumask="$(umask)"
	umask 077
	REMOTE_TEMP_CNF="$(mktemp "${TMPDIR:-/tmp}/stackx-sxmdbsync-remote.XXXXXX.cnf")" || {
		umask "$oldumask"
		die_remote "unable to create restricted remote DB credentials"
	}
	umask "$oldumask"

	if ! printf '[client]\nuser="%s"\npassword="%s"\n' "$escaped_user" "$escaped_pass" >"$REMOTE_TEMP_CNF"
	then
		remote_cleanup_credentials_file
		die_remote "unable to write restricted remote DB credentials"
	fi
	chmod 0600 "$REMOTE_TEMP_CNF" || {
		remote_cleanup_credentials_file
		die_remote "unable to protect remote DB credentials"
	}
}

trap remote_cleanup_credentials_file EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

function die_remote()
{
	echo "REMOTE_ERROR:$1" >&2
	exit 1
}

function remote_require_cmd()
{
	command -v "$1" >/dev/null 2>&1 || die_remote "missing required command: $1"
}

function remote_detect_sql_bin()
{
	if command -v mariadb >/dev/null 2>&1
	then
		printf '%s' "mariadb"
	elif command -v mysql >/dev/null 2>&1
	then
		printf '%s' "mysql"
	else
		die_remote "unable to find mariadb/mysql"
	fi
}

function remote_detect_dump_bin()
{
	if command -v mariadb-dump >/dev/null 2>&1
	then
		printf '%s' "mariadb-dump"
	elif command -v mysqldump >/dev/null 2>&1
	then
		printf '%s' "mysqldump"
	else
		die_remote "unable to find mariadb-dump/mysqldump"
	fi
}

function remote_sql_quote_ident()
{
	local value="$1"
	value="${value//\`/\`\`}"
	printf '`%s`' "$value"
}

function remote_sql_quote_squote()
{
	printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"
}

function remote_fill_auth_args()
{
	REMOTE_SQL_ARGS=()
	REMOTE_DUMP_ARGS=()
	case "$REMOTE_AUTH_MODE" in
	cnf)
		[[ -r "$REMOTE_DB_CNF" ]] || die_remote "remote defaults file not readable: $REMOTE_DB_CNF"
		REMOTE_SQL_ARGS+=("--defaults-extra-file=$REMOTE_DB_CNF")
		REMOTE_DUMP_ARGS+=("--defaults-extra-file=$REMOTE_DB_CNF")
		;;
	credentials)
		remote_create_credentials_file "$REMOTE_DB_USER" "$REMOTE_DB_PASS"
		REMOTE_SQL_ARGS+=("--defaults-extra-file=$REMOTE_TEMP_CNF")
		REMOTE_DUMP_ARGS+=("--defaults-extra-file=$REMOTE_TEMP_CNF")
		;;
	socket)
		REMOTE_SQL_ARGS+=(--protocol=socket -uroot)
		REMOTE_DUMP_ARGS+=(--protocol=socket -uroot)
		;;
	homecnf)
		[[ -r "$HOME/.my.cnf" ]] || die_remote "remote ~/.my.cnf not readable for user $USER"
		REMOTE_SQL_ARGS+=("--defaults-extra-file=$HOME/.my.cnf")
		REMOTE_DUMP_ARGS+=("--defaults-extra-file=$HOME/.my.cnf")
		;;
	*)
		die_remote "unsupported remote auth mode: $REMOTE_AUTH_MODE"
		;;
	esac
}

function remote_fill_dump_args()
{
	REMOTE_DUMP_COMMON_ARGS=(
		--single-transaction
		--quick
		--hex-blob
		--triggers
		-R
		-E
		--add-drop-table
		--skip-add-drop-database
		--default-character-set=utf8mb4
		--max-allowed-packet=1G
	)

	if "$REMOTE_DUMP_BIN" --help 2>/dev/null | grep -q -- '--no-tablespaces'
	then
		REMOTE_DUMP_COMMON_ARGS+=(--no-tablespaces)
	fi
}

function remote_query_scalar()
{
	local query="$1"
	"$REMOTE_SQL_BIN" "${REMOTE_SQL_ARGS[@]}" -N -B -e "$query"
}

function remote_database_name()
{
	local db="$1"
	remote_query_scalar "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME=$(remote_sql_quote_squote "$db") LIMIT 1;"
}

function remote_reset_target_database()
{
	local db="$1"
	local charset="$2"
	local collation="$3"
	local exists=""
	local sql=""

	sql="DROP DATABASE IF EXISTS $(remote_sql_quote_ident "$db");"
	"$REMOTE_SQL_BIN" "${REMOTE_SQL_ARGS[@]}" -e "$sql"
	exists="$(remote_database_name "$db")"
	[[ -z "$exists" ]] || die_remote "remote target database still exists after reset drop: $db"

	sql="CREATE DATABASE $(remote_sql_quote_ident "$db")"
	if [[ -n "$charset" ]]
	then
		sql="$sql CHARACTER SET $(remote_sql_quote_ident "$charset")"
	fi
	if [[ -n "$collation" ]]
	then
		sql="$sql COLLATE $(remote_sql_quote_ident "$collation")"
	fi
	sql="$sql;"
	"$REMOTE_SQL_BIN" "${REMOTE_SQL_ARGS[@]}" -e "$sql"
	exists="$(remote_database_name "$db")"
	[[ "$exists" = "$db" ]] || die_remote "remote target database missing after reset create: $db"
}

function remote_ensure_target_database()
{
	local db="$1"
	local charset="$2"
	local collation="$3"
	local exists=""
	local sql=""

	exists="$(remote_database_name "$db")"
	if [[ -n "$exists" ]]
	then
		return 0
	fi

	sql="CREATE DATABASE $(remote_sql_quote_ident "$db")"
	if [[ -n "$charset" ]]
	then
		sql="$sql CHARACTER SET $(remote_sql_quote_ident "$charset")"
	fi
	if [[ -n "$collation" ]]
	then
		sql="$sql COLLATE $(remote_sql_quote_ident "$collation")"
	fi
	sql="$sql;"
	"$REMOTE_SQL_BIN" "${REMOTE_SQL_ARGS[@]}" -e "$sql"
	exists="$(remote_database_name "$db")"
	[[ "$exists" = "$db" ]] || die_remote "remote target database missing after create: $db"
}

remote_fill_auth_args

case "$ACTION" in
probe-sql)
	REMOTE_SQL_BIN="$(remote_detect_sql_bin)"
	printf '%s\n' "$REMOTE_SQL_BIN"
	;;
probe-dump)
	REMOTE_DUMP_BIN="$(remote_detect_dump_bin)"
	printf '%s\n' "$REMOTE_DUMP_BIN"
	;;
auth-check)
	REMOTE_SQL_BIN="$(remote_detect_sql_bin)"
	remote_query_scalar 'SELECT 1;'
	;;
list-databases)
	REMOTE_SQL_BIN="$(remote_detect_sql_bin)"
	remote_query_scalar 'SHOW DATABASES;'
	;;
db-meta)
	REMOTE_SQL_BIN="$(remote_detect_sql_bin)"
	remote_query_scalar "SELECT CONCAT_WS('|', DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME=$(remote_sql_quote_squote "$DB_NAME") LIMIT 1;"
	;;
dump-db)
	REMOTE_DUMP_BIN="$(remote_detect_dump_bin)"
	remote_fill_dump_args
	"$REMOTE_DUMP_BIN" "${REMOTE_DUMP_ARGS[@]}" "${REMOTE_DUMP_COMMON_ARGS[@]}" "$DB_NAME"
	;;
import-db)
	REMOTE_SQL_BIN="$(remote_detect_sql_bin)"
	if [[ "$RESET_TARGET" = "1" ]]
	then
		remote_reset_target_database "$TARGET_DB" "$TARGET_CHARSET" "$TARGET_COLLATION"
	else
		remote_ensure_target_database "$TARGET_DB" "$TARGET_CHARSET" "$TARGET_COLLATION"
	fi
	{
		printf 'SET FOREIGN_KEY_CHECKS=0;\nSET UNIQUE_CHECKS=0;\n'
		cat
		printf '\nSET UNIQUE_CHECKS=1;\nSET FOREIGN_KEY_CHECKS=1;\n'
	} | "$REMOTE_SQL_BIN" "${REMOTE_SQL_ARGS[@]}" "$TARGET_DB"
	;;
*)
	die_remote "unsupported remote action: $ACTION"
	;;
esac
EOF_REMOTE
}

function build_remote_command()
{
	local action="$1"
	local db_name="${2:-}"
	local target_db="${3:-}"
	local target_charset="${4:-}"
	local target_collation="${5:-}"
	local script_body=""

	script_body="$(remote_helper_body)"

	cat <<EOF_REMOTE_CMD
ACTION=$(shell_quote_squote "$action")
DB_NAME=$(shell_quote_squote "$db_name")
TARGET_DB=$(shell_quote_squote "$target_db")
TARGET_CHARSET=$(shell_quote_squote "$target_charset")
TARGET_COLLATION=$(shell_quote_squote "$target_collation")
RESET_TARGET=$(shell_quote_squote "$reset_target")
REMOTE_AUTH_MODE=$(shell_quote_squote "$remote_auth_mode")
REMOTE_DB_USER=$(shell_quote_squote "$remote_db_user")
REMOTE_DB_PASS=$(shell_quote_squote "$remote_db_pass")
REMOTE_DB_PASS_SET=$(shell_quote_squote "$remote_db_pass_set")
REMOTE_DB_CNF=$(shell_quote_squote "$remote_db_cnf")
$script_body
EOF_REMOTE_CMD
}

function ssh_target()
{
	printf '%s@%s' "$sync_user" "$sync_host"
}

function ssh_base_args()
{
	local opt=""
	local -a args
	args=("-p" "$sshport")
	for opt in "${SSH_OPTIONS[@]}"
	do
		args+=("-o" "$opt")
	done
	printf '%s\n' "${args[@]}"
}

function remote_payload_runner()
{
	cat <<'EOF_REMOTE_RUNNER'
set -euo pipefail
export -n payload payload_file
unset MYSQL_PWD
payload=""
payload_file=""

function cleanup_payload_file()
{
	if [[ -n "$payload_file" ]]
	then
		rm -f -- "$payload_file"
		payload_file=""
	fi
}

trap cleanup_payload_file EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

IFS= read -r payload || {
	echo "REMOTE_ERROR:missing command payload" >&2
	exit 1
}
payload_file="$(mktemp "${TMPDIR:-/tmp}/stackx-sxmdbsync-payload.XXXXXX.sh")"
chmod 0600 "$payload_file"
printf '%s' "$payload" | base64 --decode >"$payload_file" || {
	echo "REMOTE_ERROR:invalid command payload" >&2
	exit 1
}

set +e
bash "$payload_file"
rc=$?
set -e
exit "$rc"
EOF_REMOTE_RUNNER
}

function run_remote_command()
{
	local action="$1"
	local db_name="${2:-}"
	local target_db="${3:-}"
	local target_charset="${4:-}"
	local target_collation="${5:-}"
	local command_string=""
	local command_payload=""
	local runner=""
	local -a ssh_args

	export -n command_string command_payload

	command_string="$(build_remote_command "$action" "$db_name" "$target_db" "$target_charset" "$target_collation")"
	command_payload="$(printf '%s' "$command_string" | base64 | tr -d '\n')"
	runner="$(remote_payload_runner)"
	mapfile -t ssh_args < <(ssh_base_args)

	if [[ "$action" = "import-db" ]]
	then
		{
			printf '%s\n' "$command_payload"
			cat
		} | "$SSH_BIN" "${ssh_args[@]}" "$(ssh_target)" "bash -c $(shell_quote_squote "$runner")"
	else
		printf '%s\n' "$command_payload" | "$SSH_BIN" "${ssh_args[@]}" "$(ssh_target)" "bash -c $(shell_quote_squote "$runner")"
	fi
}

function verify_remote_sql_bin()
{
	if [[ -z "$remote_sql_bin" ]]
	then
		remote_sql_bin="$(run_remote_command probe-sql)"
		[[ -n "$remote_sql_bin" ]] || die "unable to detect remote SQL client binary"
	fi
}

function verify_remote_dump_bin()
{
	if [[ -z "$remote_dump_bin" ]]
	then
		remote_dump_bin="$(run_remote_command probe-dump)"
		[[ -n "$remote_dump_bin" ]] || die "unable to detect remote dump client binary"
	fi
}

function check_local_auth()
{
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -N -B -e 'SELECT 1;' >/dev/null
}

function check_remote_auth()
{
	run_remote_command auth-check >/dev/null
}

function parse_database_mapping()
{
	local spec="$1"
	local source_db=""
	local target_db=""

	[[ -n "$spec" ]] || die "empty database mapping is not allowed"
	if [[ "$spec" == *:* ]]
	then
		source_db="${spec%%:*}"
		target_db="${spec#*:}"
	else
		source_db="$spec"
		target_db="$spec"
	fi

	source_db="$(trim "$source_db")"
	target_db="$(trim "$target_db")"
	validate_db_name "$source_db"
	validate_db_name "$target_db"

	DB_SOURCE_NAMES+=("$source_db")
	DB_TARGET_NAMES+=("$target_db")
}

function ensure_unique_target_mappings()
{
	local idx=0
	local jdx=0

	for (( idx=0; idx<${#DB_TARGET_NAMES[@]}; idx++ ))
	do
		for (( jdx=idx+1; jdx<${#DB_TARGET_NAMES[@]}; jdx++ ))
		do
			if [[ "${DB_TARGET_NAMES[$idx]}" = "${DB_TARGET_NAMES[$jdx]}" ]]
			then
				die "duplicate target database mapping detected (${DB_TARGET_NAMES[$idx]})"
			fi
		done
	done
}

function list_local_user_databases()
{
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -N -B -e 'SHOW DATABASES;' | grep -Ev "$SYSTEM_DATABASES_REGEX" || true
}

function list_remote_user_databases()
{
	run_remote_command list-databases | grep -Ev "$SYSTEM_DATABASES_REGEX" || true
}

function populate_source_databases_if_needed()
{
	local db=""

	if [[ "$auto_source_databases" -eq 0 ]]
	then
		[[ ${#DB_SOURCE_NAMES[@]} -gt 0 ]] || die "at least one --database or --all-user-databases is required"
		return
	fi

	if [[ ${#DB_SOURCE_NAMES[@]} -gt 0 ]]
	then
		die "--database and --all-user-databases cannot be used together"
	fi

	if [[ "$method" = "antesync" ]]
	then
		while IFS= read -r db
		do
			[[ -n "$db" ]] || continue
			validate_db_name "$db"
			DB_SOURCE_NAMES+=("$db")
			DB_TARGET_NAMES+=("$db")
		done < <(list_local_user_databases)
	else
		while IFS= read -r db
		do
			[[ -n "$db" ]] || continue
			validate_db_name "$db"
			DB_SOURCE_NAMES+=("$db")
			DB_TARGET_NAMES+=("$db")
		done < <(list_remote_user_databases)
	fi

	[[ ${#DB_SOURCE_NAMES[@]} -gt 0 ]] || die "no user databases found on the source side"
}

function get_local_db_meta()
{
	local db="$1"
	local query=""

	query="SELECT CONCAT_WS('|', DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME=$(sql_quote_squote "$db") LIMIT 1;"
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -N -B -e "$query"
}

function get_remote_db_meta()
{
	local db="$1"
	run_remote_command db-meta "$db"
}

function split_db_meta()
{
	local meta="$1"
	local out_charset_var="$2"
	local out_collation_var="$3"
	local charset=""
	local collation=""

	charset="${meta%%|*}"
	collation="${meta#*|}"
	if [[ "$meta" != *"|"* ]]
	then
		charset=""
		collation=""
	fi

	printf -v "$out_charset_var" '%s' "$charset"
	printf -v "$out_collation_var" '%s' "$collation"
}

function sql_quote_ident()
{
	local value="$1"
	value="${value//\`/\`\`}"
	printf '`%s`' "$value"
}

function local_database_name()
{
	local db="$1"
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -N -B -e "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME=$(sql_quote_squote "$db") LIMIT 1;"
}

function local_reset_target_database()
{
	local db="$1"
	local charset="$2"
	local collation="$3"
	local exists=""
	local sql=""

	sql="DROP DATABASE IF EXISTS $(sql_quote_ident "$db");"
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -e "$sql"
	exists="$(local_database_name "$db")"
	[[ -z "$exists" ]] || die "local target database still exists after reset drop: $db"

	sql="CREATE DATABASE $(sql_quote_ident "$db")"
	if [[ -n "$charset" ]]
	then
		sql="$sql CHARACTER SET $(sql_quote_ident "$charset")"
	fi
	if [[ -n "$collation" ]]
	then
		sql="$sql COLLATE $(sql_quote_ident "$collation")"
	fi
	sql="$sql;"
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -e "$sql"
	exists="$(local_database_name "$db")"
	[[ "$exists" = "$db" ]] || die "local target database missing after reset create: $db"
}

function local_ensure_target_database()
{
	local db="$1"
	local charset="$2"
	local collation="$3"
	local exists=""
	local sql=""

	exists="$(local_database_name "$db")"
	if [[ -n "$exists" ]]
	then
		return 0
	fi

	sql="CREATE DATABASE $(sql_quote_ident "$db")"
	if [[ -n "$charset" ]]
	then
		sql="$sql CHARACTER SET $(sql_quote_ident "$charset")"
	fi
	if [[ -n "$collation" ]]
	then
		sql="$sql COLLATE $(sql_quote_ident "$collation")"
	fi
	sql="$sql;"
	"$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" -e "$sql"
	exists="$(local_database_name "$db")"
	[[ "$exists" = "$db" ]] || die "local target database missing after create: $db"
}

function local_import_stream_to_db()
{
	local target_db="$1"
	local target_charset="$2"
	local target_collation="$3"

	if [[ "$reset_target" -eq 1 ]]
	then
		local_reset_target_database "$target_db" "$target_charset" "$target_collation"
	else
		local_ensure_target_database "$target_db" "$target_charset" "$target_collation"
	fi

	{
		printf 'SET FOREIGN_KEY_CHECKS=0;\nSET UNIQUE_CHECKS=0;\n'
		cat
		printf '\nSET UNIQUE_CHECKS=1;\nSET FOREIGN_KEY_CHECKS=1;\n'
	} | "$local_sql_bin" "${LOCAL_SQL_ARGS[@]}" "$target_db"
}

function local_dump_db()
{
	local source_db="$1"
	"$local_dump_bin" "${LOCAL_DUMP_ARGS[@]}" "${DUMP_COMMON_ARGS[@]}" "$source_db"
}

function remote_dump_db()
{
	local source_db="$1"
	run_remote_command dump-db "$source_db"
}

function log_pair_result()
{
	local source_db="$1"
	local target_db="$2"
	local status="$3"
	log_event "$status" "$method:$source_db->$target_db"
}

function print_planned_operations()
{
	local idx=0
	local source_role=""
	local target_role=""
	local source_auth_desc=""
	local target_auth_desc=""

	if [[ "$method" = "antesync" ]]
	then
		source_role="local"
		target_role="remote"
	else
		source_role="remote"
		target_role="local"
	fi

	source_auth_desc="local/$local_auth_mode"
	target_auth_desc="remote/$remote_auth_mode"
	if [[ "$method" = "retrosync" ]]
	then
		source_auth_desc="remote/$remote_auth_mode"
		target_auth_desc="local/$local_auth_mode"
	fi

	echo "sxmdbsync dry-run"
	echo "method=$method ssh=${sync_user}@${sync_host}:$sshport reset_target=$reset_target"
	echo "source_auth=$source_auth_desc target_auth=$target_auth_desc"
	for (( idx=0; idx<${#DB_SOURCE_NAMES[@]}; idx++ ))
	do
		echo "plan[$((idx + 1))]=$source_role:${DB_SOURCE_NAMES[$idx]} -> $target_role:${DB_TARGET_NAMES[$idx]}"
	done
}

function sync_antesync_pair()
{
	local source_db="$1"
	local target_db="$2"
	local meta=""
	local target_charset=""
	local target_collation=""

	meta="$(get_local_db_meta "$source_db")"
	[[ -n "$meta" ]] || die "source database metadata not found locally for $source_db"
	split_db_meta "$meta" target_charset target_collation

	if local_dump_db "$source_db" | run_remote_command import-db "$source_db" "$target_db" "$target_charset" "$target_collation"
	then
		info "antesync OK: $source_db -> $target_db"
		log_pair_result "$source_db" "$target_db" "OK"
	else
		die "antesync failed: $source_db -> $target_db"
	fi
}

function sync_retrosync_pair()
{
	local source_db="$1"
	local target_db="$2"
	local meta=""
	local target_charset=""
	local target_collation=""

	meta="$(get_remote_db_meta "$source_db")"
	[[ -n "$meta" ]] || die "source database metadata not found remotely for $source_db"
	split_db_meta "$meta" target_charset target_collation

	if remote_dump_db "$source_db" | local_import_stream_to_db "$target_db" "$target_charset" "$target_collation"
	then
		info "retrosync OK: $source_db -> $target_db"
		log_pair_result "$source_db" "$target_db" "OK"
	else
		die "retrosync failed: $source_db -> $target_db"
	fi
}

function validate_runtime()
{
	[[ "$method" = "antesync" || "$method" = "retrosync" ]] || die "invalid method ($method), expected antesync|retrosync"
	validate_ssh_user "$sync_user"
	validate_host "$sync_host"
	[[ "$sshport" =~ ^[0-9]+$ ]] || die "invalid ssh port ($sshport)"
	[[ "$sshport" -ge 1 && "$sshport" -le 65535 ]] || die "invalid ssh port ($sshport)"

	if [[ -n "$local_db_user" ]]
	then
		validate_mysql_user "$local_db_user"
	fi
	if [[ -n "$remote_db_user" ]]
	then
		validate_mysql_user "$remote_db_user"
	fi
	if [[ "$local_db_pass_set" -eq 1 ]]
	then
		validate_option_value "--local-db-pass" "$local_db_pass"
	fi
	if [[ "$remote_db_pass_set" -eq 1 ]]
	then
		validate_option_value "--remote-db-pass" "$remote_db_pass"
	fi
	if [[ -n "$local_db_cnf" ]]
	then
		validate_option_value "--local-db-cnf" "$local_db_cnf"
	fi
	if [[ -n "$remote_db_cnf" ]]
	then
		validate_option_value "--remote-db-cnf" "$remote_db_cnf"
	fi
	for opt in "${SSH_OPTIONS[@]}"
	do
		validate_option_value "--ssh-option" "$opt"
	done
}

function load_password_files()
{
	if [[ -n "$local_db_pass_file" ]]
	then
		[[ "$local_db_pass_set" -eq 0 ]] || die "--local-db-pass-file cannot be combined with --local-db-pass"
		local_db_pass="$(secret_from_private_file "$local_db_pass_file" "local DB password file")"
		local_db_pass_set=1
	fi
	if [[ -n "$remote_db_pass_file" ]]
	then
		[[ "$remote_db_pass_set" -eq 0 ]] || die "--remote-db-pass-file cannot be combined with --remote-db-pass"
		remote_db_pass="$(secret_from_private_file "$remote_db_pass_file" "remote DB password file")"
		remote_db_pass_set=1
	fi
}

function prepare_dump_common_args()
{
	DUMP_COMMON_ARGS=(
		--single-transaction
		--quick
		--hex-blob
		--triggers
		-R
		-E
		--add-drop-table
		--skip-add-drop-database
		--default-character-set=utf8mb4
		--max-allowed-packet=1G
	)

	if "$local_dump_bin" --help 2>/dev/null | grep -q -- '--no-tablespaces'
	then
		DUMP_COMMON_ARGS+=(--no-tablespaces)
	fi
}

function main()
{
	local idx=0

	if [[ $# -eq 0 ]]
	then
		usage
		exit 0
	fi

	while [[ $# -gt 0 ]]
	do
		case "$1" in
			--method=*)
				need_inline_arg "--method" "${1#*=}"
				method="${1#*=}"
				;;
			--method|-m)
				need_arg "$1" "${2:-}"
				shift
				method="$1"
				;;
			--user=*)
				need_inline_arg "--user" "${1#*=}"
				sync_user="${1#*=}"
				;;
			--user|-u)
				need_arg "$1" "${2:-}"
				shift
				sync_user="$1"
				;;
			--host=*)
				need_inline_arg "--host" "${1#*=}"
				sync_host="${1#*=}"
				;;
			--host|-H)
				need_arg "$1" "${2:-}"
				shift
				sync_host="$1"
				;;
			--port=*)
				need_inline_arg "--port" "${1#*=}"
				sshport="${1#*=}"
				;;
			--port|-p)
				need_arg "$1" "${2:-}"
				shift
				sshport="$1"
				;;
			--database=*)
				need_inline_arg "--database" "${1#*=}"
				parse_database_mapping "${1#*=}"
				;;
			--database|-d)
				need_arg "$1" "${2:-}"
				shift
				parse_database_mapping "$1"
				;;
			--all-user-databases|-a)
				auto_source_databases=1
				;;
			--local-db-user=*)
				need_inline_arg "--local-db-user" "${1#*=}"
				local_db_user="${1#*=}"
				;;
			--local-db-user)
				need_arg "$1" "${2:-}"
				shift
				local_db_user="$1"
				;;
			--local-db-pass=*)
				local_db_pass="${1#*=}"
				local_db_pass_set=1
				;;
			--local-db-pass)
				need_arg "$1" "${2:-}"
				shift
				local_db_pass="$1"
				local_db_pass_set=1
				;;
			--local-db-pass-file=*)
				need_inline_arg "--local-db-pass-file" "${1#*=}"
				local_db_pass_file="${1#*=}"
				;;
			--local-db-pass-file)
				need_arg "$1" "${2:-}"
				shift
				local_db_pass_file="$1"
				;;
			--local-db-cnf=*)
				need_inline_arg "--local-db-cnf" "${1#*=}"
				local_db_cnf="${1#*=}"
				;;
			--local-db-cnf)
				need_arg "$1" "${2:-}"
				shift
				local_db_cnf="$1"
				;;
			--remote-db-user=*)
				need_inline_arg "--remote-db-user" "${1#*=}"
				remote_db_user="${1#*=}"
				;;
			--remote-db-user)
				need_arg "$1" "${2:-}"
				shift
				remote_db_user="$1"
				;;
			--remote-db-pass=*)
				remote_db_pass="${1#*=}"
				remote_db_pass_set=1
				;;
			--remote-db-pass)
				need_arg "$1" "${2:-}"
				shift
				remote_db_pass="$1"
				remote_db_pass_set=1
				;;
			--remote-db-pass-file=*)
				need_inline_arg "--remote-db-pass-file" "${1#*=}"
				remote_db_pass_file="${1#*=}"
				;;
			--remote-db-pass-file)
				need_arg "$1" "${2:-}"
				shift
				remote_db_pass_file="$1"
				;;
			--remote-db-cnf=*)
				need_inline_arg "--remote-db-cnf" "${1#*=}"
				remote_db_cnf="${1#*=}"
				;;
			--remote-db-cnf)
				need_arg "$1" "${2:-}"
				shift
				remote_db_cnf="$1"
				;;
			--ssh-option=*)
				need_inline_arg "--ssh-option" "${1#*=}"
				SSH_OPTIONS+=("${1#*=}")
				;;
			--ssh-option|-o)
				need_arg "$1" "${2:-}"
				shift
				SSH_OPTIONS+=("$1")
				;;
			--no-reset-target)
				reset_target=0
				;;
			--dry-run|-n)
				dryrun=1
				;;
			--help|-h|help)
				usage
				exit 0
				;;
			--version|-V|-v|version)
				version
				exit 0
				;;
			*)
				echo "Error: unknown option ($1)" >&2
				usage
				exit 2
				;;
		esac
		shift
	done

	load_password_files
	validate_runtime
	init_logfile
	if [[ "$local_db_pass_set" -eq 1 && -z "$local_db_pass_file" ]]
	then
		warn "--local-db-pass exposes the password through process argv; prefer --local-db-pass-file or --local-db-cnf"
	fi
	if [[ "$remote_db_pass_set" -eq 1 && -z "$remote_db_pass_file" ]]
	then
		warn "--remote-db-pass exposes the password through process argv; prefer --remote-db-pass-file or --remote-db-cnf"
	fi
	acquire_lock
	require_cmd "$SSH_BIN"
	require_cmd base64

	if [[ "$dryrun" -eq 1 ]]
	then
		local_auth_mode="$(infer_local_auth_mode_preview)"
		remote_auth_mode="$(infer_remote_auth_mode_preview)"
		if [[ "$auto_source_databases" -eq 1 ]]
		then
			if [[ "$method" = "antesync" ]]
			then
				detect_local_bins
				resolve_local_auth
				build_local_auth_args
				check_local_auth
				populate_source_databases_if_needed
			else
				resolve_remote_auth
				verify_remote_sql_bin
				check_remote_auth
				populate_source_databases_if_needed
			fi
		fi
		ensure_unique_target_mappings
		print_planned_operations
		exit 0
	fi

	detect_local_bins
	resolve_local_auth
	resolve_remote_auth
	build_local_auth_args
	check_local_auth
	verify_remote_sql_bin
	check_remote_auth
	populate_source_databases_if_needed
	ensure_unique_target_mappings

	if [[ "$method" = "antesync" ]]
	then
		detect_local_dump_bin
		prepare_dump_common_args
	elif [[ "$method" = "retrosync" ]]
	then
		verify_remote_dump_bin
	fi

	for (( idx=0; idx<${#DB_SOURCE_NAMES[@]}; idx++ ))
	do
		if [[ "$method" = "antesync" ]]
		then
			sync_antesync_pair "${DB_SOURCE_NAMES[$idx]}" "${DB_TARGET_NAMES[$idx]}"
		else
			sync_retrosync_pair "${DB_SOURCE_NAMES[$idx]}" "${DB_TARGET_NAMES[$idx]}"
		fi
	done

	info "sxmdbsync completed successfully"
}

main "$@"
