#!/bin/bash
set -eu
set -x
### --help Info: Нужно запускать через /opt/bin/update.sh
### --help Info: Скрипт проверяет необходимость update, все подготавливает, запускает hotfix
### --help Usage: update.sh [--version=...] [--branch=...] [--check] [--auto] [--list]
### --help Usage:           [--skip-patches|--skip-patches="ver1 ver2 ..."] [--background]
### --help Usage:           [--restore-on-fail] [--offline]
### --help Example: update.sh --auto

echo "Устаревшая версия обновления! Восспользуйтесь утилитами eva_admin для обновления"
exit 1


. /opt/fox_utils/crab_sys.sh

# if, чтобы работал запуск без параметров.
if [[ ${1:-} = --help || ${1:-} ]]; then
	sys::usage "$@"
fi

declare ARG_VERSION ARG_BRANCH ARG_CHECK ARG_AUTO ARG_LIST ARG_SKIP_PATCHES ARG_BACKGROUND
declare ARG_RESTORE_ON_FAIL ARG_OFFLINE
sys::arg_parse "$@"


# Будет конфликт с /opt/bin/update.sh, проверяем в нём
# if pidof -csxo %PPID "${0##*/}"; then
# 	echo "Already running" >&2
# 	exit 1
# fi

# TODO: !!! change branch(ARG_BRANCH) logic

# wrapper запускает update_prepare (скачивание, подготовка обновления),
# останавливает все сервисы в контейнере и запускает update_apply
declare UPDATE_SERVER UPDATE_URL UPDATE_BRANCH UPDATE_OFFLINE INSTALLATION_TYPE
declare EVA_DEPLOY_TYPE
. /opt/CONFIG

declare PRODUCT=evateam BRANCH="${UPDATE_BRANCH:-master}"
declare VERSION SCOPE_DIR VERSION_SCOPE INSTALLED_VERSION
declare IS_LOCAL_DIST='' DIST_CACHE_DIR=/var/cache/eva_dist
declare -a PATCHES_LIST
declare UPDATE_DIR  # /var/log/eva_update/<datetime>


if [[ ! ${UPDATE_URL:-} ]]; then
	UPDATE_URL="rsync://${UPDATE_SERVER:-updater.evateam.ru}/eva"
	echo "COMPAT: Set UPDATE_URL to ${UPDATE_URL}"
	/opt/fox_utils/crab_conf set UPDATE_URL "$UPDATE_URL" /opt/CONFIG
fi

if [[ $UPDATE_URL != *:* ]]; then
	IS_LOCAL_DIST=TRUE
	DIST_CACHE_DIR="$UPDATE_URL"
fi


run_hotfix() {
	# Миграция ssl
	if [ ! -d "/opt/etc/nginx/ssl" ]; then
		mkdir -p /opt/etc/nginx/
		cp -a /etc/nginx/ssl/ /opt/etc/nginx/ssl
		rm -rf --one-file-system /etc/nginx/ssl/
		ln -s /opt/etc/nginx/ssl /etc/nginx/ssl
	fi
	# Миграция letsencrypt
	if [ ! -d "/opt/etc/letsencrypt" ]; then
		mkdir -p /opt/etc/letsencrypt/
		cp -a /etc/letsencrypt /opt/etc/
		rm -rf --one-file-system /etc/letsencrypt/
		ln -s /opt/etc/letsencrypt /etc/letsencrypt
	fi
	# resolv.conf через конфиг
	if ! grep -q "DNS_SERVER1=" /opt/CONFIG; then
		/opt/fox_utils/crab_conf set DNS_SERVER1 "8.8.8.8" /opt/CONFIG
		/opt/fox_utils/crab_conf set DNS_SERVER2 "8.8.4.4" /opt/CONFIG
	fi
	# Фикс files.meta
	if [ ! -d /opt/files.meta ]; then
		mv /opt/crm/files.meta/ /opt/files.meta/
		ln -s /opt/files.meta /opt/crm/files.meta
	fi
	if [[ -z $UPDATE_BRANCH ]]; then
		UPDATE_BRANCH=master
		BRANCH="$UPDATE_BRANCH"
		/opt/fox_utils/crab_conf set UPDATE_BRANCH "$UPDATE_BRANCH" /opt/CONFIG
	fi
	# Фикс rdisk.custom
	mkdir -p /opt/rdisk/rdisk/custom/
	touch /opt/rdisk/rdisk/custom/config.py
	# На некоторых VM, прописана коробочная конфигурация rdisk, удалим её
	if [[ ${INSTALLATION_TYPE:-} = cloud ]]; then
		sed -i '/CONVERTER_URL *=.*/ d' /opt/rdisk/rdisk/custom/config.py
	fi
	# EMAIL_LOGIN в конфиг
	if ! grep -q "EMAIL_LOGIN=" /opt/CONFIG; then
		/opt/fox_utils/crab_conf set EMAIL_LOGIN "" /opt/CONFIG
	fi
	return 0
}


check_disksize() {
	local avail required backups_size backup_size size patch_list_count version_size
	# Проверяем /opt, т.к. туда монтируем docker volume по умолчанию и бекап делаем в /opt/var/backup
	# TODO: проверять доступный размер у всех путей / /opt $DIST_CACHE_DIR /opt/var/backup
	set -o pipefail
	avail="$(df -P /opt | awk 'END {print $4/1024/1024}' | cut -d '.' -f 1)"
	set +o pipefail
	backup_size=0
	# make_dl_dir --dry-run не создает папку указываем любую
	set -o pipefail
	backups_size="$(make_dl_dir "/tmp/__test_disk_usage__" backup --dry-run | grep "Total file size:")"
	set +o pipefail
	while read -r size; do
		size="${size//[!0-9]/}"
		backup_size=$((backup_size+size))
	done <<< "$backups_size"
	# TODO: уточнить цифры, может их вычислять динамически: бэкап, размер версии * количество стейджей
	# у бекапа вместо коэффициентов учитывать реальный размер дампа postgresql
	if [[ ${INSTALLATION_TYPE:-} = cloud ]]; then
		required=$((backup_size * 15 / 10))
	else
		version_size="$(download_version "$VERSION" --dry-run | grep 'Total file size:' || true)"
		version_size="${version_size//[!0-9]/}"
		# Все версии скачены
		[ -z "$version_size" ] && version_size=0
		patch_list_count="${#PATCHES_LIST[@]}"
		required=$((backup_size * 15 / 10))
		required=$((required + version_size + version_size*patch_list_count*11/10))
	fi
	required=$((required/1024/1024/1024))
	if [ "$avail" -lt $((required * 9 / 10 )) ]; then
		# Специально пишем больше, чем проверяем
		echo "Нужно минимум $required Гб свободного места для запуска обновления!"
		exit 1
	fi
	return 0
}


# shellcheck disable=SC2120
version_scope() {
	set -e
	local branch="${1:-$UPDATE_BRANCH}"
	if [[
		$branch = integra
		|| $branch = devel
		|| $branch = release
		|| $branch = master
		]]; then
		echo public
	else
		echo "$branch"
	fi
	return 0
}


init_distr_cache() {
	#  sync: product/scope/... --exclude=builds
	#   т.е. все индексы доступных билдов, без самих билдов.
	#   если --exclude=builds/*/**, то будет список билдов для контроля индексов
	# rm -rf --one-file-system /var/cache/eva_dist
	rsync --stats --verbose --progress --checksum --timeout=60 --block-size=40507 \
		--archive --relative --exclude="/$PRODUCT/$VERSION_SCOPE/builds/**" \
		"${UPDATE_URL}/./$PRODUCT/$VERSION_SCOPE" \
		"$DIST_CACHE_DIR" >&2
	return 0
}


versions_list() {
	set -e
	local version
	set +x
	# shellcheck disable=SC2045
	for version in $(ls "$SCOPE_DIR/test/"); do
		[[ -h "$SCOPE_DIR/deleted/$version" ]] && continue
		if [[ $version > $INSTALLED_VERSION ]]; then
			echo "$version"  # "$(<"$SCOPE_DIR/test/$version/branch")"
		fi
	done
	echo
	echo "Patches:"
	# shellcheck disable=SC2045
	for version in $(ls "$SCOPE_DIR/patch/"); do
		if [[ $version > $INSTALLED_VERSION ]]; then
			echo "$version" patch  # "$(<"$SCOPE_DIR/patch/$version/branch")"
		fi
	done
	return 0
}


check_need_update() {
	if [[ ${ARG_VERSION:-} ]]; then
		if [[ ! -h $SCOPE_DIR/test/$ARG_VERSION ]]; then
			echo "Версии $ARG_VERSION нет на сервере обновления" >&2
			echo -e "Доступны версии:\n$(ls -1 "$SCOPE_DIR/test/")" >&2
			exit 1
		elif [[ -h $SCOPE_DIR/deleted/$ARG_VERSION ]]; then
			echo "Версия $ARG_VERSION удалена, обновление не возможно." >&2
			exit 1
		fi
		VERSION="$ARG_VERSION"
	elif [[ -h "$SCOPE_DIR/official/$BRANCH/last" ]]; then
		VERSION=$(readlink "$SCOPE_DIR/official/$BRANCH/last")
		VERSION=${VERSION##*/}
	else
		# На сервере нет последнего офишела
		VERSION=
	fi
	echo "Версия updater: ${VERSION}; локально: ${INSTALLED_VERSION}" >&2
	if [[ "${VERSION}" > "${INSTALLED_VERSION}" ]]; then
		echo "На updater более свежая версия, нужно обновление!" >&2
		if [[ ${ARG_CHECK:-} ]]; then
			echo "$VERSION"
			exit 0
		fi
	else
		echo "Обновление не требуется" >&2
		exit 0
	fi
	return 0
}


make_dl_dir() {
	set -e
	local dir="$1" type="${2:-}" dry_run=${3:-}
	if [ -z "$dry_run" ]; then
		# Делаем папку похожую на скачиваемый билд, чтобы минимизировать скачивание.
		# Пока просто копируем систему как шаблон
		mkdir -p "$dir"
		# TODO !!! echo "Проверяем, что для этой версии есть eva_root"
		echo "Инициализируем eva_root из локального рута"
	else
		echo "Проверяем размер бекапа eva_root"
	fi
	# --verbose --progress
	if [[ $type == backup ]]; then
		/usr/bin/rsync $dry_run \
			--exclude="$DIST_CACHE_DIR" \
			--exclude=/opt --exclude=/dev --exclude=/mnt --exclude=/proc \
			--exclude=/run --exclude=/sys --exclude=/tmp --exclude=/var/log \
			--stats --block-size=40507 -c --timeout=60 -a --delete \
			/ "$dir/eva_root/"
	else
		/usr/bin/rsync \
			--exclude=/version --exclude="$DIST_CACHE_DIR" \
			--exclude=/opt --exclude=/dev --exclude=/mnt --exclude=/proc \
			--exclude=/run --exclude=/sys --exclude=/tmp --exclude=/var/log \
			--exclude=/etc/hostname --exclude=/etc/hosts --exclude=/etc/resolv.conf \
			--stats --block-size=40507 -c --timeout=60 -a --delete \
			/ "$dir/eva_root/"
	fi
	if [ -z "$dry_run" ]; then
		echo "Инициализируем eva_opt из локального рута"
	else
		echo "Проверяем размер бекапа eva_opt"
	fi
	if [[ $type == backup ]]; then
		# дуль копии бд, но без этой папки будет сложно сделать restore
		# Backup files?
		/usr/bin/rsync $dry_run \
			--include=/var/lib --exclude=/var --exclude=/files --exclude=/crm/backup \
			--stats --block-size=40507 -c --timeout=60 -a --delete \
			/opt/ "$dir/eva_opt/"
	else
		/usr/bin/rsync \
			--exclude=/var --exclude=/files --exclude=/etc --exclude=/crm/backup \
			--stats --block-size=40507 -c --timeout=60 -a --delete \
			/opt/ "$dir/eva_opt/"
	fi
	return 0
}


calc_patches_list() {
	PATCHES_LIST=()
	local version
	# shellcheck disable=SC2045
	for version in $(ls "$SCOPE_DIR/patch/"); do
		if ! [[ $version =~ ^([0-9]{2}\.){3}[0-9]{4}$ ]]; then
			echo "Skip unknown file $version" >&2
			continue
		fi

		if ! [[
			$version > $INSTALLED_VERSION
			&& $version < $VERSION
			]]; then
			continue
		fi

		if [[ ${ARG_SKIP_PATCHES:-} ]] \
			&& [[
			$ARG_SKIP_PATCHES = TRUE
			|| " $ARG_SKIP_PATCHES " = *" $version "*
			]]; then
			echo "Skip $version due --skip-patches" >&2
			continue
		fi

		echo "Add intermediate patch version $version" >&2
		PATCHES_LIST+=("$version")
	done
	return 0
}


download_version() {
	set -e
	local version="$1" dry_run="${2:-}"
	if [[ -d "$SCOPE_DIR/builds/${version}" ]]; then
		echo "Version $version already downloaded" >&2
		return 0
	fi
	echo
	echo "Do download $version" >&2
	local dl_dir="$SCOPE_DIR/builds/${version}.dl"
	[ -z "$dry_run" ] && make_dl_dir "$dl_dir"
	# --verbose --progress
	rsync $dry_run --stats --checksum --timeout=60 --block-size=40507 \
		--archive --delete \
		--exclude='/docker_image.tgz' \
		"${UPDATE_URL}/$PRODUCT/$VERSION_SCOPE/builds/$version/" "$dl_dir"
	[ -z "$dry_run" ] && mv "$dl_dir" "$SCOPE_DIR/builds/${version}"
	return 0
}


run_update_download() {
	# Нужно скачать все промежуточные патчи, и саму версию.
	# Пока качаем всё сразу, но если будут проблемы с местом,
	#   то можно качать и накатывать по-одному.
	# Качаем "поверх" предыдущей версии, чтобы снизить объём.
	echo "Загрузим необходимые для обновления версии"
	local version
	# download patches
	for version in "${PATCHES_LIST[@]}"; do
		echo "DOWNLOAD $version" > "$UPDATE_DIR/status"
		download_version "$version"
	done
	# download target version
	echo "DOWNLOAD $VERSION" > "$UPDATE_DIR/status"
	download_version "$VERSION"
	return 0
}


run_update_apply() {
	echo "Применяем обновление"
	echo "Останавливаем сервисы"
	echo "APPLY services stop" > "$UPDATE_DIR/status"
	/opt/bin/services stop

	local version dist_dir="$SCOPE_DIR/builds/$VERSION" patch_dist_dir

	for version in "${PATCHES_LIST[@]}"; do
		echo "APPLY $version" > "$UPDATE_DIR/status"
		patch_dist_dir="$SCOPE_DIR/builds/$version"
		/opt/bin/update_scripts/update_apply.sh "$patch_dist_dir" \
			--patch-for="$dist_dir"
	done
	echo "APPLY $VERSION" > "$UPDATE_DIR/status"
	/opt/bin/update_scripts/update_apply.sh "$dist_dir"

	echo "Запускаю сервисы"
	echo "APPLY services start" > "$UPDATE_DIR/status"
	/opt/bin/services stop || true
	/opt/bin/services start
	return 0
}


db_list() {
	set -e
	if [[ ${INSTALLATION_TYPE:-box} = box ]]; then
		echo accountdb evacrmdb
	else
		echo evacrmdb
	fi
	return 0
}


# TODO внешние скрипты для бэкапа.
#   где размещать бэкап/ресторе, в bin или bin/update_scripts?
create_full_backup() {
	echo "Создаем резервную копию обновлением"
	echo "Сохраним БД"
	local backup_tmp_dir backup_dir db backup_name
	backup_name="$(date +%Y-%m-%d_%H%M%S__%s__update)"
	backup_dir="/opt/var/backup/$backup_name"
	backup_tmp_dir="/opt/var/backup/__incomplete__$backup_name"
	if [[ ${INSTALLATION_TYPE:-} = cloud && -d /opt/var/backup/last_update ]]; then
		mv "$(readlink -f /opt/var/backup/last_update)" "$backup_tmp_dir"
	else
		mkdir -p "$backup_tmp_dir"
	fi
	/etc/init.d/postgresql start
	for db in $(db_list); do
		echo "Сохраним БД $db"
		pg_dump -U postgres -Fc -f "${backup_tmp_dir}/$db.pg" "$db"
	done
	/etc/init.d/postgresql stop
	echo "Сохраним файлы системы"  # Надо ли рут сохранять?
	make_dl_dir "$backup_tmp_dir" backup
	mv "$backup_tmp_dir" "$backup_dir"
	# TODO: /opt/files and other exclude system configs?
	ln -sf --no-target-directory "$backup_name" /opt/var/backup/last_update
	ln -sf --no-target-directory "$backup_name" /opt/var/backup/last
	return 0
}


wrap_exec() {
	# go to background if needed
	UPDATE_DIR="/var/log/eva_update/$(date +%Y-%m-%d_%H-%M-%S)"
	if [[ ${ARG_BACKGROUND:-} ]]; then
		run_update "$@" </dev/null &
		disown
		local pid=$!
		echo "$VERSION ${UPDATE_DIR##*/}"
		echo "Run update process in background pid=$pid, show progress in $UPDATE_DIR" >&2
		exit 0
	else
		run_update "$@"
	fi
	return 0
}


run_update() {
	# init UPDATE_DIR, redirect output
	# cmd, pid, log, ret_code, versions: cur, target, patch_list, status, TODO: progress
	# TODO: nginx stub pages
	mkdir -p "$UPDATE_DIR"
	ln -sf --no-target-directory "$UPDATE_DIR" /var/log/eva_update/current
	echo "$0 $*" >"$UPDATE_DIR/cmd"
	echo "$$" >"$UPDATE_DIR/pid"
	echo "$INSTALLED_VERSION" > "$UPDATE_DIR/version_src"
	echo "$VERSION" > "$UPDATE_DIR/version_dst"
	echo "${PATCHES_LIST[*]}" > "$UPDATE_DIR/version_patches"
	echo "STARTED" > "$UPDATE_DIR/status"
	if [[ ! ${ARG_BACKGROUND:-} ]]; then
		tail -F --pid=$$ "$UPDATE_DIR/log" &
	fi
	exec &> "$UPDATE_DIR/log"

	# Run Backup
	if [[ ! $IS_LOCAL_DIST ]]; then
		echo "DOWNLOAD" > "$UPDATE_DIR/status"
		run_update_download
	fi
	echo "BACKUP" > "$UPDATE_DIR/status"
	create_full_backup
	set +e  # чтобы получить результат сабшела
	(
		set -e
		echo "APPLY" > "$UPDATE_DIR/status"
		run_update_apply
		echo "Применим конфигурацию."
		echo "CONFIGURE" > "$UPDATE_DIR/status"
		if [[ ${INSTALLATION_TYPE:-} = cloud ]]; then
			/opt/bin/eva_configure --account-sync-users
		else
			/opt/bin/eva_configure
			# Не хочется падать из-за возможных проблем на другом сервере.
			/opt/bin/eva_configure --account-sync-users || true
		fi
		echo "Удалим загруженные данные версий."
		rm -rf --one-file-system /var/cache/eva_dist
	)
	local ret_code=$?
	set -e
	echo "$ret_code" > "$UPDATE_DIR/ret_code"
	if [[ $ret_code = 0 ]]; then
		echo "SUCCESS" > "$UPDATE_DIR/status"
	else
		echo "FAIL" > "$UPDATE_DIR/status"
		if [[ ${ARG_RESTORE_ON_FAIL:-} ]]; then
			/opt/bin/eva_restore.sh last_update || true
		fi
		return 1
	fi
	if [[ -x /opt/custom/update_hook ]]; then
		/opt/custom/update_hook "$INSTALLED_VERSION" "$VERSION" "${PATCHES_LIST[*]}" "$@"
	fi
	return 0
}


main() {
	if [ "${EVA_DEPLOY_TYPE:-}" = 'box' ]; then
		echo "Для обновления Docker-контейнера используйте набор утилит eva-admin"
		exit 1
	fi

	run_hotfix
	INSTALLED_VERSION=0
	[[ -f /opt/eva_version ]] && INSTALLED_VERSION=$(</opt/eva_version)
	# Хак совместимости для старого формата версии
	if [[ $INSTALLED_VERSION =~ ^[0-9]+$ ]]; then
		echo "Old version format: $INSTALLED_VERSION" >&2
		INSTALLED_VERSION=$(
		printf "%02d.%02d.%02d.%04d\n" \
			"$((101030144 / 100000000 % 100))" \
			"$((101030144 / 1000000 % 100))" \
			"$((101030144 / 10000 % 100))" \
			"$((101030144 % 10000))")
		echo "Converted to: $INSTALLED_VERSION" >&2
	fi
	VERSION_SCOPE=$(version_scope)
	SCOPE_DIR="$DIST_CACHE_DIR/$PRODUCT/$(version_scope)"
	# если dist локальный или обновление offline, то ничего не качаем
	if [[ ! $IS_LOCAL_DIST && ! ${ARG_OFFLINE:-} && ! ${UPDATE_OFFLINE:-} ]]; then
		init_distr_cache
	fi
	if [[ ${ARG_LIST:-} ]]; then
		echo "Доступные для обновления версии:" >&2
		set -o pipefail
		versions_list | sort
		set +o pipefail
		exit 0
	fi
	check_need_update
	calc_patches_list
	echo
	echo "Обновление до $VERSION, промежуточные версии: ${PATCHES_LIST[*]}" >&2
	check_disksize
	echo "$$" > /var/run/update_running
	# delme когда старых версий без ватчдога не будет
	if [[ -x /usr/local/sbin/watchdog ]]; then
		/usr/local/sbin/watchdog stop
	fi
	wrap_exec "$@"
	rm -f /var/run/update_running
	return 0
}


main "$@"

exit 0
