#!/bin/sh
#################################################################################################################
# █████╗ ███████╗██╗   ██╗███████╗██╗    ██╗██████╗ ████████╗   ███╗   ███╗███████╗██████╗ ██╗     ██╗███╗   ██╗#
#██╔══██╗██╔════╝██║   ██║██╔════╝██║    ██║██╔══██╗╚══██╔══╝   ████╗ ████║██╔════╝██╔══██╗██║     ██║████╗  ██║#
#███████║███████╗██║   ██║███████╗██║ █╗ ██║██████╔╝   ██║█████╗██╔████╔██║█████╗  ██████╔╝██║     ██║██╔██╗ ██║#
#██╔══██║╚════██║██║   ██║╚════██║██║███╗██║██╔══██╗   ██║╚════╝██║╚██╔╝██║██╔══╝  ██╔══██╗██║     ██║██║╚██╗██║#
#██║  ██║███████║╚██████╔╝███████║╚███╔███╔╝██║  ██║   ██║      ██║ ╚═╝ ██║███████╗██║  ██║███████╗██║██║ ╚████║#
#╚═╝  ╚═╝╚══════╝ ╚═════╝ ╚══════╝ ╚══╝╚══╝ ╚═╝  ╚═╝   ╚═╝      ╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝╚══════╝╚═╝╚═╝  ╚═══╝#
# █████╗ ██████╗  ██████╗ ██╗   ██╗ █████╗ ██████╗ ██████╗ ██╗  ██╗ ██████╗ ███╗   ███╗███████╗                 #
#██╔══██╗██╔══██╗██╔════╝ ██║   ██║██╔══██╗██╔══██╗██╔══██╗██║  ██║██╔═══██╗████╗ ████║██╔════╝                 #
#███████║██║  ██║██║  ███╗██║   ██║███████║██████╔╝██║  ██║███████║██║   ██║██╔████╔██║█████╗                   #
#██╔══██║██║  ██║██║   ██║██║   ██║██╔══██║██╔══██╗██║  ██║██╔══██║██║   ██║██║╚██╔╝██║██╔══╝                   #
#██║  ██║██████╔╝╚██████╔╝╚██████╔╝██║  ██║██║  ██║██████╔╝██║  ██║╚██████╔╝██║ ╚═╝ ██║███████╗                 #
#╚═╝  ╚═╝╚═════╝  ╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚═╝  ╚═╝ ╚═════╝ ╚═╝     ╚═╝╚══════╝                 #
#██╗███╗   ██╗███████╗████████╗ █████╗ ██╗     ██╗     ███████╗██████╗     Author:                              #
#██║████╗  ██║██╔════╝╚══██╔══╝██╔══██╗██║     ██║     ██╔════╝██╔══██╗          SomeWhereOverTheRainBow        #
#██║██╔██╗ ██║███████╗   ██║   ███████║██║     ██║     █████╗  ██████╔╝    Contributors:                        #
#██║██║╚██╗██║╚════██║   ██║   ██╔══██║██║     ██║     ██╔══╝  ██╔══██╗          JackYaz,gspannu,thelonelycoder #
#██║██║ ╚████║███████║   ██║   ██║  ██║███████╗███████╗███████╗██║  ██║    v2.6.0                               #
#╚═╝╚═╝  ╚═══╝╚══════╝   ╚═╝   ╚═╝  ╚═╝╚══════╝╚══════╝╚══════╝╚═╝  ╚═╝                                         #
#################################################################################################################

export LC_ALL=C
export PATH="/sbin:/bin:/usr/sbin:/usr/bin${PATH:+:$PATH}"

AI_VERSION="v2.6.0"
export AI_VERSION
readonly ADGUARD_METADATA_FILE="/tmp/AGH_STATIC_METADATA_$$.txt"
readonly ADGUARDHOME_WAIT_TIMEOUT="60"
readonly ADGUARDHOME_READY_TIMEOUT="180"
readonly ADDON_DIR="/jffs/addons/AdGuardHome.d"
readonly BASE_DIR="/opt/etc"
readonly TARG_DIR="${BASE_DIR}/AdGuardHome"
readonly AGH_FILE="${TARG_DIR}/AdGuardHome"
readonly CONF_FILE="${TARG_DIR}/.config"
readonly YAML_FILE="${AGH_FILE}.yaml"
readonly YAML_BAK="${YAML_FILE}.bak"
readonly YAML_ERR="${YAML_FILE}.err"
readonly YAML_ORI="${TARG_DIR}/.AdGuardHome.yaml.ori"
readonly ROLLBACK_RESULT_FILE="${BASE_DIR}/.AdGuardHome.rollback_result"
readonly BLOCKLIST_ANALYZER_FILE="${TARG_DIR}/blocklist_analyzer.py"
readonly BLOCKLIST_ANALYZER_URL="https://gist.githubusercontent.com/graysky2/8035291d1bf87b8fe3693668965337e1/raw/a4be7655095d6ff880c2f3748964b825d7c45bd2/blocklilst_analyzer.py"
readonly BLOCKLIST_ANALYZER_SHA256_DEFAULT="77c21b6faeb7fea86b0729996dec5cc20d2d309fe8c6946c7371dd64134a6e5f"
BLOCKLIST_ANALYZER_SHA256=""
SCRIPT_LOC=""
SETUP_YAML_TMP_FILE=""
BLOCKLIST_YAML_TMP_FILE=""

BOLD="$(printf "\033[1m")" || true
readonly BOLD
NORM="$(printf "\033[0m")" || true
readonly NORM
INFO="$(printf "%s" "${BOLD} Info: ${NORM}")" || true
readonly INFO
ERROR="$(printf "%s" "${BOLD} *** Error: ${NORM}")" || true
readonly ERROR
WARNING="$(printf "%s" "${BOLD} * Warning: ${NORM}")" || true
readonly WARNING
INPUT="$(printf "%s" "${BOLD} => ${NORM}")" || true
readonly INPUT

# Functions are grouped by purpose; names are sorted alpha-numerically within each group.

# Bootstrap and package helpers

ai_have_cmd() {
	which "$1" >/dev/null 2>&1
}

rollback_result_write() {
	local context result detail marker_dir tmp_file
	context="$1"
	result="$2"
	detail="${3:-}"
	ROLLBACK_RESULT_UPDATED="1"
	marker_dir="${ROLLBACK_RESULT_FILE%/*}"
	[ -d "${marker_dir}" ] || mkdir -p "${marker_dir}" 2>/dev/null || return 0
	tmp_file="${ROLLBACK_RESULT_FILE}.$$"
	{
		printf 'time=%s\n' "$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null)"
		printf 'context=%s\n' "${context}"
		printf 'result=%s\n' "${result}"
		[ -z "${detail}" ] || printf 'detail=%s\n' "${detail}"
	} >"${tmp_file}" 2>/dev/null && mv -f "${tmp_file}" "${ROLLBACK_RESULT_FILE}" 2>/dev/null
	rm -f "${tmp_file}" 2>/dev/null
	chmod 600 "${ROLLBACK_RESULT_FILE}" 2>/dev/null
}

rollback_result_summary() {
	[ -s "${ROLLBACK_RESULT_FILE}" ] || return 1
	awk '
		index($0, "=") {
			key = substr($0, 1, index($0, "=") - 1)
			value = substr($0, index($0, "=") + 1)
		}
		key == "time" { time = value }
		key == "context" { context = value }
		key == "result" { result = value }
		key == "detail" { detail = value }
		END {
			if (result == "") exit 1
			printf "%s%s%s%s%s", result, context ? " during " context : "", detail ? ": " detail : "", time ? " (" time ")" : "", ""
		}
	' "${ROLLBACK_RESULT_FILE}"
}

rollback_result_notice() {
	local summary
	summary="$(rollback_result_summary 2>/dev/null)" || return 0
	PTXT "${WARNING} Last rollback result: ${summary}"
}

rollback_result_needs_attention() {
	local result
	[ -f "${ROLLBACK_RESULT_FILE}" ] || return 1
	result="$(awk -F= '$1 == "result" { print substr($0, 8); exit }' "${ROLLBACK_RESULT_FILE}" 2>/dev/null)"
	case "${result}" in
		rollback\ failed | rollback\ partial | rollback\ unavailable | restore-failed | replace-failed | yaml-quarantine-failed | staged-yaml-quarantine-failed | failed:* | *-failed)
			return 0
			;;
	esac
	return 1
}

entware_available() {
	[ -d /opt ] || return 1
	[ -x /opt/bin/opkg ] || return 1
	[ -f /opt/etc/opkg.conf ] || [ -f /opt/etc/init.d/rc.unslung ] || return 1
	/opt/bin/opkg --version >/dev/null 2>&1
}

installer_script_path() {
	local BASE DIR PATH_VALUE RESOLVED
	PATH_VALUE="$1"
	if ai_have_cmd readlink; then
		RESOLVED="$(readlink -f "${PATH_VALUE}" 2>/dev/null)" || RESOLVED=""
		if [ -n "${RESOLVED}" ]; then
			PTXT "${RESOLVED}"
			return 0
		fi
	fi
	case "${PATH_VALUE}" in
		/*) ;;
		*) PATH_VALUE="${PWD}/${PATH_VALUE}" ;;
	esac
	BASE="${PATH_VALUE##*/}"
	DIR="${PATH_VALUE%/*}"
	[ -n "${BASE}" ] && [ -d "${DIR}" ] || return 1
	DIR="$(cd "${DIR}" 2>/dev/null && pwd -P)" || return 1
	PTXT "${DIR}/${BASE}"
}

cleanup_api_files() {
	rm -f "${ADGUARD_METADATA_FILE}"
}

clear_screen() {
	ai_have_cmd clear && clear
	return 0
}

ensure_bcrypt_tool() {
	if bcrypt_tool_available; then
		return 0
	fi
	case "$(/bin/uname -m)" in
		"aarch64" | "arm64")
			ensure_opkg_package go || return 1
			;;
		"armv7l" | *)
			if opkg_pkg_available go_nohf; then
				ensure_opkg_package go_nohf || return 1
			else
				ensure_opkg_package go || return 1
			fi
			;;
	esac
	export GOROOT="/opt/bin/go"
	export GOBIN="/opt/bin/"
	GO_CACHE_DIR="/tmp/AdGuardHome-go-build-cache"
	GO_PATH_DIR="/tmp/AdGuardHome-go-path"
	[ -x /opt/bin/go/bin/go ] || return 1
	mkdir -p "${GO_CACHE_DIR}" "${GO_PATH_DIR}" || return 1
	env -i LC_ALL="${LC_ALL:-C}" PATH="/sbin:/bin:/usr/sbin:/usr/bin:/opt/sbin:/opt/bin:/opt/usr/sbin:/opt/usr/bin:/opt/bin/go/bin" HOME="${GO_PATH_DIR}" GOCACHE="${GO_CACHE_DIR}" GOPATH="${GO_PATH_DIR}" GOROOT="${GOROOT}" GOBIN="${GOBIN}" /opt/bin/go/bin/go install gophers.dev/cmds/bcrypt-tool@latest >/dev/null 2>&1 || return 1
	bcrypt_tool_available
}

bcrypt_tool_available() {
	local hash
	[ -x /opt/bin/bcrypt-tool ] || return 1
	hash="$(/opt/bin/bcrypt-tool hash preflight 10 2>/dev/null)" || return 1
	case "${hash}" in
		\$2a\$10\$*) return 0 ;;
	esac
	return 1
}

ensure_opkg_package() {
	_pkg="$1"
	case "${_pkg}" in
		"" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.+-]*)
			return 1
			;;
	esac
	if opkg_pkg_installed "${_pkg}"; then
		return 0
	fi

	if ! opkg_clean_env install "${_pkg}" >/dev/null 2>&1; then
		return 1
	fi
	opkg_pkg_installed "${_pkg}"
}

sha256sum_available() {
	if ai_have_cmd sha256sum && sha256sum /dev/null >/dev/null 2>&1; then
		return 0
	fi
	if [ -x /opt/bin/sha256sum ] && /opt/bin/sha256sum /dev/null >/dev/null 2>&1; then
		return 0
	fi
	if [ -x /bin/busybox ] && /bin/busybox sha256sum /dev/null >/dev/null 2>&1; then
		return 0
	fi
	if ai_have_cmd busybox && busybox sha256sum /dev/null >/dev/null 2>&1; then
		return 0
	fi
	if ai_have_cmd openssl && openssl dgst -sha256 /dev/null >/dev/null 2>&1; then
		return 0
	fi
	return 1
}

ensure_sha256sum_tool() {
	if sha256sum_available; then
		return 0
	fi
	ptxt_warn "Neither sha256sum nor OpenSSL SHA-256 support was found in PATH; checksum verification is required."
	PTXT "${INFO} Some Asuswrt-Merlin firmware builds do not include sha256sum."
	PTXT "${INFO} Installing Entware package: opkg install coreutils-sha256sum"
	ptxt_step "Installing Entware coreutils-sha256sum package."
	if ! ensure_opkg_package coreutils-sha256sum; then
		ptxt_fail "Unable to install Entware coreutils-sha256sum."
		return 1
	fi
	if sha256sum_available; then
		ptxt_ok "SHA-256 checksum support is available."
		return 0
	fi
	ptxt_fail "coreutils-sha256sum was installed, but sha256sum is still unavailable."
	return 1
}

ensure_password_hash_tool() {
	ensure_opkg_package python3 || true
	ensure_opkg_package python3-bcrypt || true
	if python_bcrypt_available; then
		return 0
	fi
	ensure_bcrypt_tool
}

hash_password_python() {
	_password="$1"
	PTXT -n "${_password}" | /opt/bin/python3 -c 'import sys, bcrypt; password = sys.stdin.buffer.read(); print(bcrypt.hashpw(password, bcrypt.gensalt(prefix=b"2a", rounds=10)).decode("ascii"))'
}

opkg_pkg_available() {
	_pkg="$1"
	opkg_clean_env list "${_pkg}" 2>/dev/null | awk -v pkg="${_pkg}" '$1 == pkg { found = 1; exit } END { exit(found ? 0 : 1) }'
}

opkg_pkg_installed() {
	_pkg="$1"
	opkg_clean_env list-installed 2>/dev/null | awk -v pkg="${_pkg}" '$1 == pkg { found = 1; exit } END { exit(found ? 0 : 1) }'
}

opkg_clean_env() {
	[ -x /opt/bin/opkg ] || return 127
	env -i LC_ALL="${LC_ALL:-C}" PATH="/sbin:/bin:/usr/sbin:/usr/bin:/opt/sbin:/opt/bin:/opt/usr/sbin:/opt/usr/bin" /opt/bin/opkg "$@"
}

installer_cleanup_tmp_file() {
	case "$1:$2" in
		setup:"${YAML_ORI}.new.$$" | blocklist:"${YAML_FILE}.blocklists.$$.tmp")
			rm -f "$2" 2>/dev/null
			;;
	esac
}

on_installer_exit() {
	cleanup_api_files
	installer_cleanup_tmp_file setup "${SETUP_YAML_TMP_FILE:-}"
	installer_cleanup_tmp_file blocklist "${BLOCKLIST_YAML_TMP_FILE:-}"
	# Restore pre-check DNS settings only when exiting without a local adguardhome installation.
	if [ ! -f "${AGH_FILE}" ]; then
		check_dns_environment 1
	fi
}

python_bcrypt_available() {
	[ -x /opt/bin/python3 ] && /opt/bin/python3 -c 'import bcrypt' >/dev/null 2>&1
}

remove_conflicting_apache() {
	if opkg_pkg_installed apache || opkg_pkg_installed apache-utils; then
		opkg_clean_env flag user apache apache-utils >/dev/null 2>&1
		opkg_clean_env remove apache apache-utils --force-removal-of-dependent-packages >/dev/null 2>&1
	fi
}

# HTTP/download helpers

curl_common_args() {
	if [ -z "${CURL_COMMON_ARGS_SET:-}" ]; then
		CURL_COMMON_ARGS=""
		curl_has_option '--retry' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --retry 5"
		curl_has_option '--connect-timeout' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --connect-timeout 25"
		curl_has_option '--retry-delay' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --retry-delay 5"
		curl_has_option '--max-time' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --max-time $((5 * 25))"
		curl_has_option '--retry-connrefused' && CURL_COMMON_ARGS="${CURL_COMMON_ARGS} --retry-connrefused"
		CURL_COMMON_ARGS_SET="1"
	fi
	PTXT -n "${CURL_COMMON_ARGS}"
}

curl_has_option() {
	curl_help | grep -q -e "$1"
}

curl_help() {
	if [ -z "${CURL_HELP_CACHE_SET:-}" ]; then
		CURL_HELP_CACHE="$(curl --help all 2>&1 || curl --help 2>&1)"
		CURL_HELP_CACHE_SET="1"
	fi
	PTXT "${CURL_HELP_CACHE}"
}

curl_insecure_arg() {
	if [ -z "${CURL_INSECURE_ARG_SET:-}" ]; then
		CURL_INSECURE_ARG=""
		curl_has_option '--insecure' && CURL_INSECURE_ARG=" -k"
		CURL_INSECURE_ARG_SET="1"
	fi
	PTXT -n "${CURL_INSECURE_ARG}"
}

http_get_file() {
	local URL OUT HEADERS INSECURE CURL_ARGS WGET_ARGS
	URL="$1"
	OUT="$2"
	HEADERS="${3:-}"
	INSECURE="${4:-}"
	if ai_have_cmd curl; then
		CURL_ARGS="$(curl_common_args)"
		[ "${INSECURE}" = "insecure" ] && CURL_ARGS="${CURL_ARGS}$(curl_insecure_arg)"
		if [ -n "${HEADERS}" ]; then
			curl ${CURL_ARGS} -D "${HEADERS}" -f -sL "${URL}" -o "${OUT}"
		else
			curl ${CURL_ARGS} -f -sL "${URL}" -o "${OUT}"
		fi
	elif ai_have_cmd wget; then
		WGET_ARGS="$(wget_common_args)"
		[ "${INSECURE}" = "insecure" ] && WGET_ARGS="${WGET_ARGS}$(wget_insecure_arg)"
		if [ -n "${HEADERS}" ] && wget_has_option '--server-response'; then
			wget ${WGET_ARGS} --server-response -q -O "${OUT}" "${URL}" 2>"${HEADERS}"
		else
			[ -n "${HEADERS}" ] && : >"${HEADERS}"
			wget ${WGET_ARGS} -q -O "${OUT}" "${URL}"
		fi
	else
		return 127
	fi
}

http_get_stdout() {
	local URL INSECURE CURL_ARGS WGET_ARGS
	URL="$1"
	INSECURE="${2:-}"
	if ai_have_cmd curl; then
		CURL_ARGS="$(curl_common_args)"
		[ "${INSECURE}" = "insecure" ] && CURL_ARGS="${CURL_ARGS}$(curl_insecure_arg)"
		curl ${CURL_ARGS} -f -sL "${URL}"
	elif ai_have_cmd wget; then
		WGET_ARGS="$(wget_common_args)"
		[ "${INSECURE}" = "insecure" ] && WGET_ARGS="${WGET_ARGS}$(wget_insecure_arg)"
		wget ${WGET_ARGS} -q -O - "${URL}"
	else
		return 127
	fi
}

http_probe() {
	local URL PROBE_TIMEOUT CURL_ARGS WGET_ARGS
	URL="$1"
	PROBE_TIMEOUT="${2:-}"
	if ai_have_cmd curl; then
		CURL_ARGS="$(curl_common_args)"
		if [ -n "${PROBE_TIMEOUT}" ]; then
			curl_has_option '--retry' && CURL_ARGS="${CURL_ARGS} --retry 0"
			curl_has_option '--connect-timeout' && CURL_ARGS="${CURL_ARGS} --connect-timeout ${PROBE_TIMEOUT}"
			curl_has_option '--max-time' && CURL_ARGS="${CURL_ARGS} --max-time ${PROBE_TIMEOUT}"
		fi
		curl ${CURL_ARGS} -f -sL -I -o /dev/null "${URL}"
	elif ai_have_cmd wget; then
		WGET_ARGS="$(wget_common_args)"
		if [ -n "${PROBE_TIMEOUT}" ]; then
			wget_has_option '--tries' && WGET_ARGS="${WGET_ARGS} --tries=1"
			wget_has_option '--timeout' && WGET_ARGS="${WGET_ARGS} --timeout=${PROBE_TIMEOUT}"
			wget_has_option '--waitretry' && WGET_ARGS="${WGET_ARGS} --waitretry=0"
		fi
		if wget_has_option '--spider'; then
			wget ${WGET_ARGS} -q --spider "${URL}"
		else
			wget ${WGET_ARGS} -q -O /dev/null "${URL}"
		fi
	else
		return 127
	fi
}

wget_common_args() {
	if [ -z "${WGET_COMMON_ARGS_SET:-}" ]; then
		WGET_COMMON_ARGS=""
		wget_has_option '--no-cache' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --no-cache"
		wget_has_option '--no-cookies' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --no-cookies"
		wget_has_option '--tries' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --tries=5"
		wget_has_option '--timeout' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --timeout=25"
		wget_has_option '--waitretry' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --waitretry=5"
		wget_has_option '--retry-connrefused' && WGET_COMMON_ARGS="${WGET_COMMON_ARGS} --retry-connrefused"
		WGET_COMMON_ARGS_SET="1"
	fi
	PTXT -n "${WGET_COMMON_ARGS}"
}

wget_has_option() {
	wget_help | grep -q -e "$1"
}

wget_help() {
	if [ -z "${WGET_HELP_CACHE_SET:-}" ]; then
		WGET_HELP_CACHE="$(wget --help 2>&1)"
		WGET_HELP_CACHE_SET="1"
	fi
	PTXT "${WGET_HELP_CACHE}"
}

wget_insecure_arg() {
	if [ -z "${WGET_INSECURE_ARG_SET:-}" ]; then
		WGET_INSECURE_ARG=""
		wget_has_option '--no-check-certificate' && WGET_INSECURE_ARG=" --no-check-certificate"
		WGET_INSECURE_ARG_SET="1"
	fi
	PTXT -n "${WGET_INSECURE_ARG}"
}

# Console and config helpers

_quote() {
	PTXT "$1" | sed 's/[]\/()$*.^|[]/\\&/g'
}

PTXT() {
	case "$1" in
		-n)
			shift
			while [ $# -gt 0 ]; do
				printf "%s" "$1"
				shift
			done
			;;
		*)
			while [ $# -gt 0 ]; do
				printf "%s\n" "$1"
				shift
			done
			;;
	esac
}

ptxt_phase() {
	PTXT "${INFO} ===================================================="
	PTXT "${INFO} PHASE: $1"
	PTXT "${INFO} ===================================================="
}

ptxt_step() {
	PTXT "${INFO} -> $1"
}

ptxt_ok() {
	PTXT "${INFO} OK: $1"
}

ptxt_warn() {
	PTXT "${WARNING} WARN: $1"
}

ptxt_fail() {
	PTXT "${ERROR} FAIL: $1"
}

SCRIPT_LOC="$(installer_script_path "$0")" || {
	PTXT "Unable to resolve installer path: $0" >&2
	exit 1
}
readonly SCRIPT_LOC

branch_is_safe() {
	case "$1" in
		"" | /* | */ | *..* | *//* | *\\* | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._/-]*)
			return 1
			;;
	esac
	return 0
}

conf_value() {
	[ -f "${CONF_FILE}" ] || return 1
	awk -v KEY="$1" '
		index($0, KEY "=") == 1 {
			VALUE = substr($0, length(KEY) + 2)
			gsub(/^"|"$/, "", VALUE)
			print VALUE
			exit
		}
	' "${CONF_FILE}"
}

conf_has_key() {
	[ -f "${CONF_FILE}" ] || return 1
	grep -q "^$1=" "${CONF_FILE}"
}

write_conf_if_absent() {
	conf_has_key "$1" && return 0
	write_conf "$1" "$2"
}

configure_runtime_defaults() {
	local NETCHECK_MODE
	case "${1:-}" in
		new-install)
			case "${2:-}" in
				1 | YES | yes | Yes) NETCHECK_MODE="wan" ;;
				*) NETCHECK_MODE="lan" ;;
			esac
			write_conf_if_absent ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "\"1\"" || return 1
			write_conf_if_absent ADGUARD_NETCHECK_MODE "\"${NETCHECK_MODE}\"" || return 1
			write_conf_if_absent ADGUARD_PROC_OPTIMIZE "\"YES\"" || return 1
			write_conf_if_absent ADGUARD_PROC_PROFILE "\"balanced\"" || return 1
			ptxt_ok "Runtime defaults saved: DNS port refusal enabled, netcheck mode ${NETCHECK_MODE}, performance profile balanced."
			;;
		upgrade)
			write_conf_if_absent ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "\"0\"" || return 1
			write_conf_if_absent ADGUARD_NETCHECK_MODE "\"legacy\"" || return 1
			write_conf_if_absent ADGUARD_PROC_OPTIMIZE "\"YES\"" || return 1
			write_conf_if_absent ADGUARD_PROC_PROFILE "\"aggressive\"" || return 1
			PTXT "${INFO} Existing runtime defaults were retained for compatibility." \
				"${INFO} Legacy values remain active unless you migrate with:" \
				"${INFO}   sh installer migrate-runtime-defaults --yes"
			;;
	esac
}

md5_is_valid() {
	case "$1" in
		"" | *[!0123456789abcdefABCDEF]*) return 1 ;;
	esac
	[ "${#1}" -eq 32 ]
}

sha256_is_valid() {
	case "$1" in
		"" | *[!0123456789abcdefABCDEF]*) return 1 ;;
	esac
	[ "${#1}" -eq 64 ]
}

file_md5() {
	local FILE MD5_OUTPUT MD5_VALUE
	FILE="$1"
	ai_have_cmd md5sum || return 1
	MD5_OUTPUT="$(md5sum "${FILE}")" || return 1
	MD5_VALUE="$(PTXT "${MD5_OUTPUT}" | awk 'NF {print $1; found = 1; exit} END {if (!found) exit 1}')" || return 1
	md5_is_valid "${MD5_VALUE}" || return 1
	PTXT "${MD5_VALUE}"
}

file_sha256() {
	local FILE SHA256_OUTPUT SHA256_VALUE
	FILE="$1"
	SHA256_OUTPUT=""
	if ai_have_cmd sha256sum; then
		SHA256_OUTPUT="$(sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT=""
	fi
	if [ -z "${SHA256_OUTPUT}" ] && [ -x /opt/bin/sha256sum ]; then
		SHA256_OUTPUT="$(/opt/bin/sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT=""
	fi
	if [ -z "${SHA256_OUTPUT}" ] && [ -x /bin/busybox ]; then
		SHA256_OUTPUT="$(/bin/busybox sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT=""
	fi
	if [ -z "${SHA256_OUTPUT}" ] && ai_have_cmd busybox; then
		SHA256_OUTPUT="$(busybox sha256sum "${FILE}" | awk '{printf "%s", $1}')" || SHA256_OUTPUT=""
	fi
	if [ -z "${SHA256_OUTPUT}" ] && ai_have_cmd openssl; then
		SHA256_OUTPUT="$(openssl dgst -sha256 "${FILE}" | awk '{printf "%s", $2}')" || SHA256_OUTPUT=""
	fi
	if ! sha256_is_valid "${SHA256_OUTPUT}"; then
		PTXT "${ERROR} Invalid SHA-256 checksum."
		return 1
	else
		SHA256_VALUE="${SHA256_OUTPUT}"
		PTXT -n "${SHA256_VALUE}"
	fi
}

ipv4_is_valid() {
	PTXT "$1" | awk -F. '
		NF != 4 { exit 1 }
		{
			for (i = 1; i <= 4; i++) {
				if ($i !~ /^[0-9][0-9]*$/ || $i < 0 || $i > 255) exit 1
			}
		}
	'
}

ipv4_is_private() {
	ipv4_is_valid "$1" || return 1
	PTXT "$1" | awk -F. '
		$1 == 10 || $1 == 127 || ($1 == 172 && $2 >= 16 && $2 <= 31) || ($1 == 192 && $2 == 168) { found = 1 }
		END { exit(found ? 0 : 1) }
	'
}

port_is_valid() {
	case "$1" in
		"" | *[!0123456789]*) return 1 ;;
	esac
	[ "$1" -ge 3000 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
}

runtime_port_is_valid() {
	case "$1" in
		"" | *[!0123456789]*) return 1 ;;
	esac
	[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ] 2>/dev/null
}

web_port_in_use() {
	netstat -nl 2>/dev/null | awk -v port=":$1" '
		$4 ~ port "$" {
			found = 1
		}
		END {
			exit(found ? 0 : 1)
		}
	'
}

web_port_owned_by_agh() {
	netstat -nlp 2>/dev/null | awk -v port=":$1" '
		$0 ~ /^(tcp)6?[[:space:]]+/ && $4 ~ port "$" && $0 ~ "/AdGuardHome([[:space:]]|$)" {
			found = 1
		}
		END {
			exit(found ? 0 : 1)
		}
	'
}

installer_dependencies_available() {
	local REQUIRED_COMMAND
	for REQUIRED_COMMAND in awk chmod chown cp cut grep kill ln md5sum mkdir mv netstat nvram pidof rm sed service tar; do
		if ! ai_have_cmd "${REQUIRED_COMMAND}"; then
			PTXT "${ERROR} Required command is unavailable: ${REQUIRED_COMMAND}" >&2
			return 1
		fi
	done
	if ! entware_available; then
		PTXT "${ERROR} Entware is not available. Install or mount Entware before running this installer." >&2
		return 1
	fi
	return 0
}

preflight_action_requires_entware() {
	case "${1:-install}" in
		status | preflight)
			return 1
			;;
		*)
			return 0
			;;
	esac
}

preflight_action_requires_jq() {
	case "${1:-install}" in
		*)
			return 1
			;;
	esac
}

preflight_action_requires_password_hash() {
	case "${1:-install}" in
		"" | install | reconfigure | changepw | 3 | 4)
			return 0
			;;
	esac
	return 1
}

preflight_action_requires_sha256() {
	case "${1:-install}" in
		"" | install | update | restore | blocklists | unusedblocklists | 9)
			return 0
			;;
	esac
	return 1
}

preflight_action_requires_timezone_column() {
	case "${1:-install}" in
		"" | install | reconfigure | restore | 4)
			return 0
			;;
	esac
	return 1
}

preflight_action_requires_jffs_ready() {
	case "${1:-install}" in
		"" | install | reconfigure | 4)
			return 0
			;;
	esac
	return 1
}

preflight_action_requires_router_eligibility() {
	case "${1:-install}" in
		status | preflight)
			return 1
			;;
	esac
	return 0
}

preflight_action_requires_downloader() {
	case "${1:-install}" in
		status | preflight)
			return 1
			;;
	esac
	return 0
}

preflight_action_requires_service_tools() {
	case "${1:-install}" in
		status | preflight)
			return 1
			;;
	esac
	return 0
}

preflight_action_requires_cru() {
	case "${1:-install}" in
		status | preflight)
			return 1
			;;
	esac
	return 0
}

preflight_action_requires_firewall_tools() {
	case "${1:-install}" in
		status | preflight)
			return 1
			;;
	esac
	return 0
}

preflight_check_jffs_ready() {
	local jffs2_format result
	jffs2_format="$(nvram get jffs2_format 2>/dev/null)" || jffs2_format=""
	result="OK"
	PTXT "preflight.jffs.required=yes"
	PTXT "preflight.jffs.format_pending=${jffs2_format:-missing}"
	if [ "${jffs2_format}" = "1" ]; then
		result="FAIL"
	fi
	PTXT "preflight.jffs.result=${result}"
	[ "${result}" = "OK" ]
}

preflight_check_router_eligibility() {
	local arch model odmpid os result sw_mode
	result="OK"
	PTXT "preflight.router.required=yes"
	if [ -z "${ROUTER_MODEL:-}" ]; then
		odmpid="$(nvram get odmpid 2>/dev/null)" || odmpid=""
		[ -z "${odmpid}" ] && model="$(nvram get productid 2>/dev/null)" || model="${odmpid}"
	else
		model="${ROUTER_MODEL}"
	fi
	os="$(/bin/uname 2>/dev/null)" || os=""
	case "${model}" in
		*AC*)
			case "${model}" in
				*RT-AC86* | *RT-AC2900*) arch="$(/bin/uname -m 2>/dev/null)" || arch="" ;;
				*) arch="armv7" ;;
			esac
			;;
		*)
			arch="$(/bin/uname -m 2>/dev/null)" || arch=""
			;;
	esac
	sw_mode="$(nvram get sw_mode 2>/dev/null)" || sw_mode=""
	PTXT "preflight.router.model=${model:-missing}"
	PTXT "preflight.router.sw_mode=${sw_mode:-missing}"
	if [ -z "${model}" ]; then
		PTXT "preflight.router.model.result=FAIL"
		result="FAIL"
	else
		PTXT "preflight.router.model.result=OK"
	fi
	if [ "${sw_mode}" = "1" ]; then
		PTXT "preflight.router.mode.result=OK"
	else
		PTXT "preflight.router.mode.result=FAIL"
		result="FAIL"
	fi
	case "${os}" in
		Linux)
			PTXT "preflight.router.os=linux"
			PTXT "preflight.router.os.result=OK"
			;;
		*)
			PTXT "preflight.router.os=${os:-missing}"
			PTXT "preflight.router.os.result=FAIL"
			result="FAIL"
			;;
	esac
	case "${arch}" in
		aarch64 | arm64 | armv7l | armv7)
			PTXT "preflight.router.arch=${arch}"
			PTXT "preflight.router.arch.result=OK"
			;;
		*)
			PTXT "preflight.router.arch=${arch:-missing}"
			PTXT "preflight.router.arch.result=FAIL"
			result="FAIL"
			;;
	esac
	if [ "${result}" = "OK" ]; then
		PTXT "preflight.router.result=OK"
		return 0
	fi
	PTXT "preflight.router.result=FAIL"
	return 1
}

preflight_check_path() {
	local description path result which_path
	description="$1"
	path="$2"
	result="FAIL"
	which_path="$(which "${path##*/}" 2>/dev/null)" || which_path=""
	if [ -x "${path}" ]; then
		result="OK"
	fi
	PTXT "preflight.${description}.required=${path}"
	PTXT "preflight.${description}.which=${which_path:-missing}"
	PTXT "preflight.${description}.result=${result}"
	[ "${result}" = "OK" ]
}

preflight_check_command() {
	local command_name result which_path
	command_name="$1"
	result="FAIL"
	which_path="$(which "${command_name}" 2>/dev/null)" || which_path=""
	if [ -n "${which_path}" ] && [ -x "${which_path}" ]; then
		result="OK"
	fi
	PTXT "preflight.stock.${command_name}.required=yes"
	PTXT "preflight.stock.${command_name}.which=${which_path:-missing}"
	PTXT "preflight.stock.${command_name}.result=${result}"
	[ "${result}" = "OK" ]
}

preflight_check_stock_commands() {
	local failed required_command
	failed="0"
	for required_command in awk chmod chown cp cut grep kill ln md5sum mkdir mv netstat pidof rm sed tar; do
		preflight_check_command "${required_command}" || failed="1"
	done
	[ "${failed}" = "0" ]
}

preflight_check_downloader() {
	local curl_path wget_path result
	curl_path="$(which curl 2>/dev/null)" || curl_path=""
	wget_path="$(which wget 2>/dev/null)" || wget_path=""
	result="FAIL"
	if [ -x /usr/sbin/curl ] || [ -x /usr/sbin/wget ]; then
		result="OK"
	fi
	PTXT "preflight.downloader.required=/usr/sbin/curl or /usr/sbin/wget"
	PTXT "preflight.downloader.curl.which=${curl_path:-missing}"
	PTXT "preflight.downloader.curl.stock=$([ -x /usr/sbin/curl ] && PTXT OK || PTXT missing)"
	PTXT "preflight.downloader.wget.which=${wget_path:-missing}"
	PTXT "preflight.downloader.wget.stock=$([ -x /usr/sbin/wget ] && PTXT OK || PTXT missing)"
	PTXT "preflight.downloader.result=${result}"
	[ "${result}" = "OK" ]
}

preflight_check_entware() {
	local result
	result="FAIL"
	if entware_available; then
		result="OK"
	fi
	PTXT "preflight.entware.required=yes"
	PTXT "preflight.entware.opt_dir=$([ -d /opt ] && PTXT OK || PTXT missing)"
	PTXT "preflight.entware.opkg=$([ -x /opt/bin/opkg ] && PTXT OK || PTXT missing)"
	PTXT "preflight.entware.config=$({ [ -f /opt/etc/opkg.conf ] || [ -f /opt/etc/init.d/rc.unslung ]; } && PTXT OK || PTXT missing)"
	PTXT "preflight.entware.result=${result}"
	[ "${result}" = "OK" ]
}

preflight_check_entware_package() {
	local pkg result
	pkg="$1"
	result="MISSING"
	if opkg_pkg_installed "${pkg}"; then
		result="OK"
	fi
	PTXT "preflight.entware.package.${pkg}.required=yes"
	PTXT "preflight.entware.package.${pkg}.result=${result}"
	[ "${result}" = "OK" ] || PTXT "preflight.entware.package.${pkg}.install_hint=opkg install ${pkg}"
	[ "${result}" = "OK" ]
}

preflight_check_jq() {
	local allow_entware jq_path result
	allow_entware="$1"
	jq_path="$(which jq 2>/dev/null)" || jq_path=""
	result="FAIL"
	if [ -x /usr/bin/jq ]; then
		result="OK"
	fi
	PTXT "preflight.jq.required=yes"
	PTXT "preflight.jq.which=${jq_path:-missing}"
	PTXT "preflight.jq.stock=$([ -x /usr/bin/jq ] && PTXT OK || PTXT missing)"
	if [ "${allow_entware}" = "1" ]; then
		[ "${result}" = "OK" ] || [ ! -x /opt/bin/jq ] || result="OK"
		PTXT "preflight.jq.entware=$([ -x /opt/bin/jq ] && PTXT OK || PTXT missing)"
		PTXT "preflight.jq.install_hint=opkg install jq"
	else
		PTXT "preflight.jq.entware=not_checked"
	fi
	PTXT "preflight.jq.result=${result}"
	[ "${result}" = "OK" ]
}

preflight_check_password_hash_support() {
	local failed result
	failed="0"
	result="MISSING"
	if python_bcrypt_available || bcrypt_tool_available; then
		result="OK"
	fi
	PTXT "preflight.entware.password_hash.required=yes"
	PTXT "preflight.entware.password_hash.result=${result}"
	if [ "${result}" != "OK" ]; then
		PTXT "preflight.entware.password_hash.install_hint=opkg install python3 python3-bcrypt"
		preflight_check_entware_package python3 || failed="1"
		preflight_check_entware_package python3-bcrypt || failed="1"
	fi
	[ "${failed}" = "0" ] && [ "${result}" = "OK" ]
}

preflight_check_sha256_support() {
	if sha256sum_available; then
		PTXT "preflight.sha256.required=yes"
		PTXT "preflight.sha256.result=OK"
		PTXT "preflight.entware.package.coreutils-sha256sum.required=no"
		return 0
	fi
	PTXT "preflight.sha256.required=yes"
	PTXT "preflight.sha256.result=MISSING"
	preflight_check_entware_package coreutils-sha256sum || true
	return 1
}

preflight_check_timezone_column() {
	if [ -x /opt/bin/column ]; then
		PTXT "preflight.entware.column.required=yes"
		PTXT "preflight.entware.column.result=OK"
		return 0
	fi
	PTXT "preflight.entware.column.required=yes"
	PTXT "preflight.entware.column.result=MISSING"
	preflight_check_entware_package column || true
	return 1
}

preflight() {
	local action entware_available_now entware_required failed
	action="${1:-install}"
	entware_available_now="0"
	entware_required="0"
	failed="0"
	PTXT "preflight.version=${AI_VERSION}"
	PTXT "preflight.action=${action}"
	PTXT "preflight.safe_to_paste=yes"
	PTXT "preflight.path=${PATH}"
	preflight_check_path "sh" "/bin/sh" || failed="1"
	preflight_check_path "nvram" "/bin/nvram" || failed="1"
	if preflight_action_requires_downloader "${action}"; then
		preflight_check_downloader || failed="1"
	else
		PTXT "preflight.downloader.required=no"
		PTXT "preflight.downloader.result=SKIP"
	fi
	if preflight_action_requires_service_tools "${action}"; then
		preflight_check_path "service" "/sbin/service" || failed="1"
	else
		PTXT "preflight.service.required=no"
		PTXT "preflight.service.result=SKIP"
	fi
	if preflight_action_requires_cru "${action}"; then
		preflight_check_path "cru" "/usr/sbin/cru" || failed="1"
	else
		PTXT "preflight.cru.required=no"
		PTXT "preflight.cru.result=SKIP"
	fi
	if preflight_action_requires_firewall_tools "${action}"; then
		preflight_check_path "iptables" "/usr/sbin/iptables" || failed="1"
		preflight_check_path "ip6tables" "/usr/sbin/ip6tables" || failed="1"
	else
		PTXT "preflight.iptables.required=no"
		PTXT "preflight.iptables.result=SKIP"
		PTXT "preflight.ip6tables.required=no"
		PTXT "preflight.ip6tables.result=SKIP"
	fi
	preflight_check_stock_commands || failed="1"
	if preflight_action_requires_jffs_ready "${action}"; then
		preflight_check_jffs_ready || failed="1"
	else
		PTXT "preflight.jffs.required=no"
		PTXT "preflight.jffs.result=SKIP"
	fi
	if preflight_action_requires_router_eligibility "${action}"; then
		preflight_check_router_eligibility || failed="1"
	else
		PTXT "preflight.router.required=no"
		PTXT "preflight.router.result=SKIP"
	fi
	if preflight_action_requires_entware "${action}"; then
		entware_required="1"
		if preflight_check_entware; then
			entware_available_now="1"
		else
			failed="1"
		fi
	else
		PTXT "preflight.entware.required=no"
		PTXT "preflight.entware.result=SKIP"
	fi
	if preflight_action_requires_jq "${action}"; then
		preflight_check_jq "${entware_required}" || failed="1"
	else
		PTXT "preflight.jq.required=no"
		PTXT "preflight.jq.result=SKIP"
	fi
	if [ "${entware_required}" = "1" ] && [ "${entware_available_now}" = "1" ]; then
		if preflight_action_requires_sha256 "${action}"; then
			preflight_check_sha256_support || failed="1"
		else
			PTXT "preflight.sha256.required=no"
			PTXT "preflight.sha256.result=SKIP"
		fi
		if preflight_action_requires_password_hash "${action}"; then
			preflight_check_password_hash_support || failed="1"
		else
			PTXT "preflight.entware.password_hash.required=no"
			PTXT "preflight.entware.password_hash.result=SKIP"
		fi
		if preflight_action_requires_timezone_column "${action}"; then
			preflight_check_timezone_column || failed="1"
		else
			PTXT "preflight.entware.column.required=no"
			PTXT "preflight.entware.column.result=SKIP"
		fi
	elif [ "${entware_required}" = "1" ]; then
		PTXT "preflight.entware.dependent_checks=SKIP_ENTWARE_MISSING"
	fi
	if [ "${failed}" = "0" ]; then
		PTXT "preflight.result=PASS"
		return 0
	fi
	PTXT "preflight.result=FAIL"
	return 1
}

sanitize_branch() {
	local CANDIDATE
	CANDIDATE="$1"
	if branch_is_safe "${CANDIDATE}"; then
		PTXT "${CANDIDATE}"
	else
		PTXT "${WARNING} Ignoring unsafe installer branch ${CANDIDATE}, using master instead." >&2
		PTXT "master"
	fi
}

# AdGuardHome process helpers

adguard_pid_list_has_new_pid() {
	local new_pid old_pids
	old_pids=" ${1:-} "
	shift
	for new_pid in "$@"; do
		case "${old_pids}" in
			*" ${new_pid} "*) ;;
			*)
				return 0
				;;
		esac
	done
	return 1
}

adguard_service_status_after_action() {
	local action elapsed maxwait old_pids status_message transition_message
	action="${1:-restart}"
	old_pids="${2:-}"
	elapsed="0"
	maxwait="${ADGUARDHOME_WAIT_TIMEOUT}"
	[ "${maxwait}" -le "5" ] || maxwait="5"
	case "${action}" in
		stop | kill)
			status_message="Waiting for AdGuardHome to stop cleanly..."
			transition_message="Stopping..."
			;;
		start)
			status_message="Waiting for AdGuardHome to report running state after start..."
			transition_message="Restarting..."
			;;
		restart | *)
			status_message="Waiting for AdGuardHome to report running state after restart..."
			transition_message="Restarting..."
			;;
	esac
	PTXT "${INFO} ${status_message}"
	case "${action}" in
		stop | kill)
			while [ "$(agh_process_count)" -ge "1" ]; do
				if [ "${elapsed}" -ge "${maxwait}" ]; then
					PTXT "${INFO} ${transition_message}"
					return 1
				fi
				sleep 1s
				elapsed="$((elapsed + 1))"
			done
			;;
		restart)
			while ! { agh_is_running && { [ -z "${old_pids}" ] || adguard_pid_list_has_new_pid "${old_pids}" "$(pidof AdGuardHome 2>/dev/null)"; }; }; do
				if [ "${elapsed}" -ge "${maxwait}" ]; then
					PTXT "${INFO} ${transition_message}"
					return 1
				fi
				sleep 1s
				elapsed="$((elapsed + 1))"
			done
			;;
		*)
			while ! agh_is_running; do
				if [ "${elapsed}" -ge "${maxwait}" ]; then
					PTXT "${INFO} ${transition_message}"
					return 1
				fi
				sleep 1s
				elapsed="$((elapsed + 1))"
			done
			;;
	esac
	agh_check
	return 0
}

valid_adguardhome_username() {
	case "${1:-}" in
		"" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-]*)
			return 1
			;;
		*)
			return 0
			;;
	esac
}

valid_adguardhome_password_input() {
	[ -n "${1:-}" ] || return 1
	[ "${1}" = "${2:-}" ] || return 1
	case "${1}" in
		" "* | *" " | *[[:cntrl:]]*)
			return 1
			;;
	esac
	return 0
}

AdGuardHome_authen() {
	local YAML_TARGET="${2:-${YAML_ORI}}"
	local YAML_VALIDATE="${3:-1}"
	if [ -z "${PW1}" ] || [ -z "${PW2}" ]; then
		local USERNAME
		PTXT -n "${INPUT} Please enter AdGuardHome username${NORM}: "
		if ! IFS= read -r USERNAME; then
			return 1
		fi
	fi
	if ! valid_adguardhome_username "${USERNAME:-}"; then
		PTXT "${ERROR} Invalid AdGuardHome username. Accepted characters: letters, digits, dot (.), underscore (_), and hyphen (-)."
		return 1
	fi
	local PW1 PW2
	PTXT -n "${INPUT} Please enter AdGuardHome password${NORM}: "
	local PASSWORD_ECHO_DISABLED=0
	if stty -echo 2>/dev/null; then
		PASSWORD_ECHO_DISABLED=1
	fi
	if ! IFS= read -r PW1; then
		[ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null
		PTXT " "
		return 1
	fi
	[ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null
	PTXT " "
	PTXT -n "${INPUT} Please reenter AdGuardHome password${NORM}: "
	PASSWORD_ECHO_DISABLED=0
	if stty -echo 2>/dev/null; then
		PASSWORD_ECHO_DISABLED=1
	fi
	if ! IFS= read -r PW2; then
		[ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null
		PTXT " "
		return 1
	fi
	[ "${PASSWORD_ECHO_DISABLED}" -eq 0 ] || stty echo 2>/dev/null
	PTXT " "
	if ! valid_adguardhome_password_input "${PW1}" "${PW2}"; then
		PTXT "${ERROR} Password entered incorrectly!"
		AdGuardHome_authen "$1" "${YAML_TARGET}" "${YAML_VALIDATE}"
		return
	fi
	remove_conflicting_apache
	if ! ensure_password_hash_tool; then
		PTXT "${ERROR} Unable to install a password hashing tool from Entware."
		end_op_message 1
		return
	fi

	local PW1_ENCRYPTED
	PW1_ENCRYPTED=""
	if python_bcrypt_available; then
		PW1_ENCRYPTED="$(hash_password_python "${PW1}")" || PW1_ENCRYPTED=""
	fi
	if [ -z "${PW1_ENCRYPTED}" ] && [ -f "/opt/bin/bcrypt-tool" ]; then
		PW1_ENCRYPTED="$(/opt/bin/bcrypt-tool hash "${PW1}" 10)" || PW1_ENCRYPTED=""
	fi
	case "${PW1_ENCRYPTED}" in
		\$2a\$10\$*) ;;
		*)
			PTXT "${ERROR} Password could not be set!" "${ERROR} Please contact dev."
			end_op_message 1
			return
			;;
	esac
	if [ "$1" -eq 0 ]; then
		yaml_nvars_delete "- name:" "${YAML_FILE}"
		yaml_nvars_delete "  password:" "${YAML_FILE}"
		yaml_nvars_insert "users:" "\  password: ${PW1_ENCRYPTED}" "${YAML_FILE}"
		yaml_nvars_insert "users:" "- name: ${USERNAME}" "${YAML_FILE}"
		check_AdGuardHome_yaml "${YAML_FILE}"
		ptxt_step "Waiting for AdGuardHome post-install readiness checks."
		if ! agh_complete_startup; then
			end_op_message 1
			return
		fi
		end_op_message 0
	else
		PTXT "users:" \
			"- name: ${USERNAME}" \
			"  password: ${PW1_ENCRYPTED}" >>"${YAML_TARGET}"
		if [ "${YAML_VALIDATE}" -ne 0 ] && ! check_AdGuardHome_yaml "${YAML_TARGET}"; then
			return 1
		fi
	fi
}

agh_check() {
	/opt/etc/init.d/S99AdGuardHome check
}

agh_config_valid() {
	[ -x "${AGH_FILE}" ] || return 1
	[ -f "${YAML_FILE}" ] || return 1
	"${AGH_FILE}" --check-config -c "${YAML_FILE}" --no-check-update -l /dev/null >/dev/null 2>&1
}

agh_dns_bound() {
	local socket_table
	pidof AdGuardHome >/dev/null 2>&1 || return 2
	socket_table="$(netstat -nlp 2>/dev/null)" || return 1
	PTXT "${socket_table}" | awk '
		$0 ~ /^(tcp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			if ($0 ~ "/AdGuardHome([[:space:]]|$)") tcp_found = 1
		}
		$0 ~ /^(udp)6?[[:space:]]+/ && $0 ~ /:53[[:space:]]/ {
			if ($0 ~ "/AdGuardHome([[:space:]]|$)") udp_found = 1
		}
		END { exit(tcp_found && udp_found ? 0 : 1) }
	'
}

agh_web_port() {
	local conf_port yaml_port
	yaml_port="$(awk -F: '/^[[:space:]]*address:[[:space:]]*/ { print $NF; exit }' "${YAML_FILE}" 2>/dev/null | sed 's/[^0-9].*$//')"
	if runtime_port_is_valid "${yaml_port}"; then
		PTXT "${yaml_port}"
		return 0
	fi
	conf_port="$(awk -F= '/^ADGUARD_WEBUI_PORT=/ { print $2; exit }' "${CONF_FILE}" 2>/dev/null | sed -e 's/^"//' -e 's/"$//')"
	if port_is_valid "${conf_port}"; then
		PTXT "${conf_port}"
		return 0
	fi
	return 1
}

agh_web_bound() {
	local web_port
	web_port="$(agh_web_port)" || return 1
	web_port_owned_by_agh "${web_port}"
}

agh_log_start_failure() {
	ADGUARDHOME_START_FAILURE_LOGGED=1
	if ! agh_is_running; then
		PTXT "${ERROR} AdGuardHome startup failed: process is not running."
		return 0
	fi
	if ! agh_dns_bound; then
		PTXT "${ERROR} AdGuardHome startup failed: process is running but DNS is not bound."
		return 0
	fi
	if ! agh_web_bound; then
		PTXT "${ERROR} AdGuardHome startup failed: WebUI port is unavailable."
		return 0
	fi
	if ! agh_config_valid; then
		PTXT "${ERROR} AdGuardHome startup failed: configuration check failed."
		return 0
	fi
	ADGUARDHOME_START_FAILURE_LOGGED=
	return 1
}

agh_startup_check() {
	agh_is_running || return 1
	agh_dns_bound || return 1
	agh_web_bound || return 1
	agh_config_valid || return 1
	return 0
}

agh_startup_ready() {
	local elapsed maxwait
	elapsed="0"
	maxwait="${ADGUARDHOME_READY_TIMEOUT}"
	while ! agh_startup_check; do
		if ! agh_is_running; then
			agh_log_start_failure
			return 1
		fi
		if [ "${elapsed}" -ge "${maxwait}" ]; then
			agh_log_start_failure
			return 1
		fi
		sleep 1s
		elapsed="$((elapsed + 1))"
	done
	return 0
}

agh_complete_startup() {
	ptxt_phase "Starting AdGuardHome service."
	agh_stop
	ADGUARDHOME_START_FAILURE_LOGGED=
	if ! agh_start; then
		[ "${ADGUARDHOME_START_FAILURE_LOGGED:-}" = "1" ] || agh_start_error
		return 1
	fi
	ptxt_step "Waiting for AdGuardHome readiness check to complete."
	ADGUARDHOME_START_FAILURE_LOGGED=
	if ! agh_restart; then
		[ "${ADGUARDHOME_START_FAILURE_LOGGED:-}" = "1" ] || agh_start_error
		return 1
	fi
	ptxt_ok "AdGuardHome setup is complete."
	return 0
}

agh_is_running() {
	[ -n "$(pidof AdGuardHome)" ]
}

agh_prepare_binary_replace() {
	if { [ -f "${AGH_FILE}" ] || [ -e "/opt/sbin/AdGuardHome" ]; } && [ "$(agh_process_count)" -ge "1" ]; then
		ptxt_step "Stopping AdGuardHome service before replacing ${AGH_FILE}."
		if ! agh_stop; then
			ptxt_fail "Unable to safely stop AdGuardHome service before replacing ${AGH_FILE}."
			return 1
		fi
	fi
	return 0
}

agh_monitor_count() {
	local count pid
	count="0"
	for pid in $(pidof AdGuardHome.sh S99AdGuardHome rc.func.AdGuardHome 2>/dev/null); do
		if awk '{ print }' "/proc/${pid}/cmdline" 2>/dev/null | grep -q 'monitor-start'; then
			count="$((count + 1))"
		fi
	done
	PTXT "${count}"
}

agh_process_count() {
	local daemon_count monitor_count
	daemon_count="$(pidof AdGuardHome 2>/dev/null | wc -w)"
	monitor_count="$(agh_monitor_count)"
	PTXT "$((daemon_count + monitor_count))"
}

doctor_status() {
	local next
	next="${3:-}"
	[ "$1" = "FAIL" ] && DOCTOR_FAILED="1"
	case "$1" in
		WARN | FAIL)
			[ -n "${next}" ] || next="Review the installer output and repair manually."
			PTXT "[$1] $2 | Next: ${next}"
			;;
		*)
			PTXT "[$1] $2"
			;;
	esac
}

doctor_fix_msg() {
	PTXT "[OK] fixed: $1"
}

doctor_file_state() {
	local path mode expected
	path="$1"
	expected="$2"
	if [ ! -e "${path}" ]; then
		doctor_status WARN "${path} missing" "Re-run install or restore ${path}."
		return 1
	fi
	mode="$(ls -ld "${path}" 2>/dev/null | awk '{ print $1 }')"
	if [ -n "${expected}" ] && [ "${mode}" != "${expected}" ]; then
		doctor_status WARN "${path} mode ${mode}, expected ${expected}" "Run: sh installer doctor --fix"
		return 1
	fi
	doctor_status OK "${path} present${mode:+ (${mode})}"
	return 0
}

doctor_managed_script_state() {
	local operation script_path expected_line
	script_path="$1"
	operation="$2"
	expected_line="[ -x ${ADDON_DIR}/AdGuardHome.sh ] && ${ADDON_DIR}/AdGuardHome.sh ${operation}"
	if [ ! -f "${script_path}" ]; then
		doctor_status WARN "${script_path} managed hook missing" "Re-run install to recreate the managed hook."
		return 1
	fi
	if grep -q -F "${expected_line}" "${script_path}" 2>/dev/null; then
		if [ ! -x "${script_path}" ]; then
			doctor_status WARN "${script_path} managed hook present but not executable" "Run: sh installer doctor --fix"
			return 1
		fi
		doctor_status OK "${script_path} managed hook present and executable"
		return 0
	fi
	doctor_status WARN "${script_path} managed hook missing expected AdGuardHome call" "Re-run install or add the managed AdGuardHome hook line."
	return 1
}

doctor_dns53_state() {
	local dns53 owners
	dns53="$(netstat -nlp 2>/dev/null | awk '$0 ~ /:53[[:space:]]/ { print; found = 1 } END { exit(found ? 0 : 1) }')"
	if agh_dns_bound; then
		doctor_status OK "DNS port 53 TCP and UDP are owned by AdGuardHome"
		return 0
	fi
	if [ -n "${dns53}" ]; then
		owners="$(PTXT "${dns53}" | awk '{ print $NF }' | sort | uniq | tr '\n' ' ' | sed 's/[[:space:]]*$//')"
		doctor_status WARN "DNS port 53 TCP and UDP are not both owned by AdGuardHome${owners:+ (owners: ${owners})}" "Inspect DNS settings; do not use --fix for DNS changes."
		return 1
	fi
	doctor_status FAIL "DNS port 53 is not listening" "Start AdGuardHome or restore dnsmasq DNS service manually."
	return 1
}

doctor_fix_permissions() {
	[ -d "${TARG_DIR}" ] && ensure_adguardhome_directory_permissions >/dev/null 2>&1 && doctor_fix_msg "AdGuardHome directory permissions"
	[ -f "/opt/etc/init.d/S99AdGuardHome" ] && chmod 755 "/opt/etc/init.d/S99AdGuardHome" 2>/dev/null && doctor_fix_msg "/opt/etc/init.d/S99AdGuardHome executable bit"
	[ -f "/opt/etc/init.d/rc.func.AdGuardHome" ] && chmod 644 "/opt/etc/init.d/rc.func.AdGuardHome" 2>/dev/null && doctor_fix_msg "/opt/etc/init.d/rc.func.AdGuardHome permissions"
	[ -f "${ADDON_DIR}/AdGuardHome.sh" ] && chmod 755 "${ADDON_DIR}/AdGuardHome.sh" 2>/dev/null && doctor_fix_msg "${ADDON_DIR}/AdGuardHome.sh executable bit"
	[ -f "/jffs/scripts/init-start" ] && chmod 755 "/jffs/scripts/init-start" 2>/dev/null && doctor_fix_msg "/jffs/scripts/init-start executable bit"
	[ -f "/jffs/scripts/services-stop" ] && chmod 755 "/jffs/scripts/services-stop" 2>/dev/null && doctor_fix_msg "/jffs/scripts/services-stop executable bit"
	[ -f "/jffs/scripts/dnsmasq.postconf" ] && chmod 755 "/jffs/scripts/dnsmasq.postconf" 2>/dev/null && doctor_fix_msg "/jffs/scripts/dnsmasq.postconf executable bit"
	[ -f "/jffs/scripts/firewall-start" ] && chmod 755 "/jffs/scripts/firewall-start" 2>/dev/null && doctor_fix_msg "/jffs/scripts/firewall-start executable bit"
	[ -f "${YAML_FILE}" ] && chmod 600 "${YAML_FILE}" 2>/dev/null && doctor_fix_msg "${YAML_FILE} permissions"
	[ -f "${CONF_FILE}" ] && chmod 600 "${CONF_FILE}" 2>/dev/null && doctor_fix_msg "${CONF_FILE} permissions"
}

doctor_pid_file_is_active() {
	local marker_pid
	marker_pid="$(sed -n '1s/[[:space:]].*$//p' "$1" 2>/dev/null)"
	case "${marker_pid}" in
		"" | *[!0-9]*) return 1 ;;
	esac
	[ -d "/proc/${marker_pid}" ]
}

doctor_run_lock_is_active() {
	local lock_file pid_file status
	lock_file="/tmp/AdGuardHome.lock"
	pid_file="/tmp/AdGuardHome/pid"
	if [ -f "${pid_file}" ] && doctor_pid_file_is_active "${pid_file}"; then
		return 0
	fi
	if ai_have_cmd flock && [ -e "${lock_file}" ]; then
		(
			exec 9<>"${lock_file}" || exit 1
			flock -n 9 >/dev/null 2>&1
			status="$?"
			[ "${status}" -eq 0 ] && flock -u 9 >/dev/null 2>&1
			exit "${status}"
		)
		status="$?"
		[ "${status}" -ne 0 ] && return 0
	fi
	return 1
}

doctor_fix_safe() {
	local link_target stale
	doctor_fix_permissions
	link_target="$(readlink "/opt/sbin/AdGuardHome" 2>/dev/null)"
	if [ -f "${AGH_FILE}" ] && { [ ! -L "/opt/sbin/AdGuardHome" ] || [ "${link_target}" != "${AGH_FILE}" ]; }; then
		mkdir -p "/opt/sbin" 2>/dev/null && ln -sf "${AGH_FILE}" "/opt/sbin/AdGuardHome" 2>/dev/null && doctor_fix_msg "/opt/sbin/AdGuardHome symlink"
	fi
	for stale in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock /tmp/AdGuardHome.lock /tmp/AdGuardHome /tmp/AdGuardHome.pid /opt/var/run/AdGuardHome.pid /tmp/AdGuardHome.*.tmp "${TARG_DIR}"/.*.$$.* "${TARG_DIR}"/*.tmp.$$; do
		[ -e "${stale}" ] || continue
		case "${stale}" in
			/tmp/AdGuardHome.dnsmasq.handoff | /tmp/AdGuardHome.dnsmasq.lock | /tmp/AdGuardHome.dns-handoff/active | /tmp/AdGuardHome.dns-handoff/lock)
				doctor_pid_file_is_active "${stale}" && continue
				[ "$(agh_monitor_count)" -eq 0 ] || continue
				;;
			/tmp/AdGuardHome.lock | /tmp/AdGuardHome)
				doctor_run_lock_is_active && continue
				;;
			/opt/var/run/AdGuardHome.pid | /tmp/AdGuardHome.pid)
				doctor_pid_file_is_active "${stale}" && continue
				;;
			/tmp/AdGuardHome/pid)
				doctor_run_lock_is_active && continue
				;;
			/tmp/AdGuardHome.*.tmp)
				[ -f "${stale}" ] || continue
				;;
		esac
		rm -rf "${stale}" 2>/dev/null && doctor_fix_msg "removed stale ${stale}"
	done
}

doctor_show_nvram_dns() {
	local key value
	if ! ai_have_cmd nvram; then
		doctor_status WARN "nvram command unavailable; DNS NVRAM values not checked" "Run doctor on the router where nvram is available."
		return 0
	fi
	for key in dnspriv_enable dns_local_cache dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x dnsfilter_enable_x dnsfilter_mode dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 lan_dnsenable_x lan_dns1_x lan_dns2_x wan_dnsenable_x wan_dns1_x wan_dns2_x; do
		value="$(nvram get "${key}" 2>/dev/null)"
		case "${key}:${value}" in
			dnsfilter_enable_x:0 | dnsfilter_enable_x:)
				doctor_status OK "nvram ${key}=${value}"
				;;
			dnsfilter_enable_x:*)
				doctor_status WARN "nvram ${key}=${value}; DNSFilter may redirect client DNS" "Review DNSFilter settings manually; --fix will not change NVRAM."
				;;
			*)
				doctor_status OK "nvram ${key}=${value}"
				;;
		esac
	done
}

doctor() {
	local count fix ipset_file link_target marker port version branch backup_file entware_ready
	DOCTOR_FAILED="0"
	fix="0"
	case "${1:-}" in
		--fix) fix="1" ;;
		"") ;;
		*)
			doctor_status FAIL "usage: sh installer doctor [--fix]" "Run: sh installer doctor [--fix]"
			return 1
			;;
	esac
	entware_ready="0"
	if entware_available; then
		entware_ready="1"
		doctor_status OK "Entware is available"
	else
		doctor_status FAIL "Entware is not available" "Mount or install Entware, then rerun doctor."
	fi
	if [ "${fix}" = "1" ]; then
		if [ "${entware_ready}" = "1" ]; then
			doctor_fix_safe
		else
			doctor_status WARN "skipping --fix repairs because Entware is not available" "Mount or install Entware before using --fix."
		fi
	fi
	doctor_file_state "${TARG_DIR}" "drwx------" || true
	if [ -f "${ROLLBACK_RESULT_FILE}" ]; then
		if rollback_result_needs_attention; then
			doctor_status FAIL "last rollback result: $(rollback_result_summary)" "Review the rollback detail, restore manually if needed, then rerun doctor."
		else
			doctor_status WARN "last rollback result: $(rollback_result_summary)" "Confirm the restored state, then remove ${ROLLBACK_RESULT_FILE} if no action is needed."
		fi
	else
		doctor_status OK "last rollback result: none"
	fi
	link_target="$(readlink "/opt/sbin/AdGuardHome" 2>/dev/null)"
	[ -L "/opt/sbin/AdGuardHome" ] && [ "${link_target}" = "${AGH_FILE}" ] && doctor_status OK "/opt/sbin/AdGuardHome -> ${link_target}" || doctor_status FAIL "/opt/sbin/AdGuardHome target is ${link_target:-missing}" "Run: sh installer doctor --fix"
	doctor_file_state "${ADDON_DIR}/AdGuardHome.sh" "-rwxr-xr-x" || true
	doctor_managed_script_state "/jffs/scripts/init-start" "init-start &" || true
	doctor_managed_script_state "/jffs/scripts/services-stop" "services-stop &" || true
	doctor_managed_script_state "/jffs/scripts/dnsmasq.postconf" "dnsmasq" || true
	doctor_managed_script_state "/jffs/scripts/firewall-start" 'firewall "$1"' || true
	doctor_file_state "/opt/etc/init.d/S99AdGuardHome" "-rwxr-xr-x" || true
	doctor_file_state "/opt/etc/init.d/rc.func.AdGuardHome" "-rw-r--r--" || true
	doctor_file_state "${YAML_FILE}" "-rw-------" || true
	doctor_file_state "${CONF_FILE}" "-rw-------" || true
	doctor_dns53_state || true
	for marker in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock /tmp/AdGuardHome.lock /tmp/AdGuardHome/pid; do
		[ -e "${marker}" ] && doctor_status WARN "dnsmasq handoff marker/lock exists: ${marker}" "Run: sh installer doctor --fix; if still present, check active installer/service processes." || doctor_status OK "dnsmasq handoff marker/lock absent: ${marker}"
	done
	count="$(agh_monitor_count)"
	[ "${count}" -eq 1 ] && doctor_status OK "monitor process count: ${count}" || doctor_status WARN "monitor process count: ${count}" "Restart the AdGuardHome service if the monitor count is unexpected."
	count="$(pidof AdGuardHome 2>/dev/null | wc -w)"
	[ "${count}" -eq 1 ] && doctor_status OK "AdGuardHome daemon process count: ${count}" || doctor_status WARN "AdGuardHome daemon process count: ${count}" "Start or restart AdGuardHome manually; --fix will not restart services."
	port="$(agh_web_port 2>/dev/null)"
	if [ -n "${port}" ]; then
		web_port_owned_by_agh "${port}" && doctor_status OK "WebUI port ${port} owned by AdGuardHome" || doctor_status WARN "WebUI port ${port} not owned by AdGuardHome" "Inspect the WebUI port owner and AdGuardHome config manually."
	else
		doctor_status WARN "WebUI port not configured" "Set ADGUARD_WEBUI_PORT or repair AdGuardHome.yaml manually."
	fi
	branch="$(conf_value INSTALLER_BRANCH 2>/dev/null)"
	version="$([ -x "${AGH_FILE}" ] && "${AGH_FILE}" --version 2>/dev/null | head -1)"
	doctor_status OK "installer version ${AI_VERSION}, branch ${branch:-unknown}"
	doctor_status OK "AdGuardHome version ${version:-unknown}"
	backup_file="${BASE_DIR}/backup_AdGuardHome.tar.gz"
	if [ -f "${backup_file}" ]; then
		adguard_archive_is_safe "${backup_file}" 1 && doctor_status OK "backup archive safety passed" || doctor_status FAIL "backup archive safety failed" "Remove or replace the unsafe backup archive manually."
	else
		doctor_status WARN "backup archive missing" "Create a fresh backup after confirming the installation is healthy."
	fi
	for ipset_file in "${TARG_DIR}/ipset.conf" "${TARG_DIR}/ipset.user" "$(adguardhome_yaml_ipset_file 2>/dev/null)"; do
		[ -n "${ipset_file}" ] || continue
		case "${ipset_file}" in /*) ;; *) ipset_file="${TARG_DIR}/${ipset_file}" ;; esac
		[ -f "${ipset_file}" ] && doctor_status OK "IPSET file present: ${ipset_file}" || doctor_status WARN "IPSET file missing: ${ipset_file}" "Recreate the IPSET file or update AdGuardHome.yaml manually."
	done
	doctor_show_nvram_dns
	[ "${DOCTOR_FAILED}" = "0" ]
}

status_line() {
	PTXT "$1: ${2:-unknown}"
}

status_dnsmasq_handoff_state() {
	local marker markers state
	state="inactive"
	markers=""
	for marker in /tmp/AdGuardHome.dnsmasq.handoff /tmp/AdGuardHome.dnsmasq.lock /tmp/AdGuardHome.dns-handoff/active /tmp/AdGuardHome.dns-handoff/lock; do
		if [ -e "${marker}" ]; then
			state="active/stale marker present"
			markers="${markers}${markers:+, }${marker}"
		fi
	done
	PTXT "${state}${markers:+ (${markers})}"
}

status_last_startup_result() {
	local result
	result=""
	if ai_have_cmd logread; then
		result="$(logread 2>/dev/null |
			awk '
				/AdGuardHome/ && /AdGuardHome startup completed/ { last = $0 }
				/AdGuardHome/ && /AdGuardHome startup failed/ { last = $0 }
				/AdGuardHome/ && /DNS\/WebUI readiness checks (passed|failed)/ { last = $0 }
				END { if (last != "") print last }
			')"
	fi
	[ -n "${result}" ] && PTXT "${result}" || PTXT "unknown (no startup marker/log entry found)"
}

status_port53_ownership() {
	local dns53
	dns53="$(netstat -nlp 2>/dev/null | awk '$0 ~ /:53[[:space:]]/ { print }')"
	if [ -z "${dns53}" ]; then
		PTXT "not listening"
		return 0
	fi
	PTXT "${dns53}" | awk '
		function owner(line, i, field) {
			for (i = NF; i >= 1; i--) {
				field = $i
				if (field ~ /^[0-9]+\/[^[:space:]]+$/) return field
			}
			return "unknown"
		}
		$0 ~ /^(tcp)6?[[:space:]]+/ { tcp = tcp ? tcp ", " owner($0) : owner($0) }
		$0 ~ /^(udp)6?[[:space:]]+/ { udp = udp ? udp ", " owner($0) : owner($0) }
		END {
			if (tcp != "" && udp != "") print "TCP " tcp "; UDP " udp
			else if (tcp != "") print "TCP " tcp
			else if (udp != "") print "UDP " udp
			else print "unknown"
		}
	'
}

status_webui_address() {
	local address host port
	address="$(awk '
		/^[[:space:]]*address:[[:space:]]*/ {
			sub(/^[[:space:]]*address:[[:space:]]*/, "")
			gsub(/^"|"$/, "")
			print
			exit
		}
	' "${YAML_FILE}" 2>/dev/null)"
	port="$(agh_web_port 2>/dev/null)"
	if [ -n "${address}" ]; then
		PTXT "${address}${port:+ (port ${port})}"
		return 0
	fi
	host="$(nvram get lan_ipaddr 2>/dev/null)"
	[ -n "${port}" ] && PTXT "${host:-router}:${port}" || PTXT "unknown"
}

status() {
	local branch count monitor_count monitor_state service_state version
	count="$(pidof AdGuardHome 2>/dev/null | wc -w)"
	monitor_count="$(agh_monitor_count)"
	if [ "${count}" -gt 0 ]; then
		if agh_startup_check; then
			service_state="running (ready)"
		else
			service_state="running (readiness check incomplete)"
		fi
	else
		service_state="stopped"
	fi
	case "${monitor_count}" in
		0) monitor_state="stopped" ;;
		1) monitor_state="running (1 process)" ;;
		*) monitor_state="running (${monitor_count} processes)" ;;
	esac
	branch="${1:-}"
	[ -n "${branch}" ] || branch="$(conf_value INSTALLER_BRANCH 2>/dev/null)"
	version="$([ -x "${AGH_FILE}" ] && "${AGH_FILE}" --version 2>/dev/null | head -1)"

	PTXT "AdGuardHome Installer Status"
	status_line "AdGuardHome service state" "${service_state}"
	status_line "Monitor process state" "${monitor_state}"
	status_line "AdGuardHome PID count" "${count}"
	status_line "Port 53 ownership" "$(status_port53_ownership)"
	status_line "AdGuardHome version" "${version:-unknown}"
	status_line "Installer version" "${AI_VERSION}"
	status_line "Selected branch" "${branch:-unknown}"
	status_line "WebUI address/port" "$(status_webui_address)"
	status_line "dnsmasq handoff state" "$(status_dnsmasq_handoff_state)"
	status_line "Last startup result" "$(status_last_startup_result)"
	status_line "Last rollback result" "$(rollback_result_summary 2>/dev/null || PTXT "none")"
}

agh_restart() {
	local old_pids
	if agh_is_running; then
		ptxt_step "Restarting AdGuardHome service."
		old_pids="$(pidof AdGuardHome 2>/dev/null)"
		if service restart_AdGuardHome >/dev/null 2>&1; then
			adguard_service_status_after_action restart "${old_pids}" || true
		elif /opt/etc/init.d/S99AdGuardHome restart; then
			adguard_service_status_after_action restart "${old_pids}" || true
		else
			ptxt_fail "AdGuardHome service restart command failed."
			return 1
		fi
	else
		agh_start || return 1
	fi
	sleep 1s
	if agh_startup_ready; then
		ptxt_ok "AdGuardHome service restarted."
		return 0
	fi
	ptxt_fail "AdGuardHome service did not become ready after restart."
	return 1
}

agh_start() {
	if ! agh_is_running; then
		ptxt_step "Starting AdGuardHome service."
		if service start_AdGuardHome >/dev/null 2>&1; then
			adguard_service_status_after_action start || true
		elif /opt/etc/init.d/S99AdGuardHome start; then
			adguard_service_status_after_action start || true
		else
			ptxt_fail "AdGuardHome service start command failed."
			return 1
		fi
	fi
	sleep 1s
	if ! agh_wait_started "${ADGUARDHOME_WAIT_TIMEOUT}"; then
		agh_log_start_failure || PTXT "${ERROR} AdGuardHome startup failed: process is not running."
		return 1
	fi
	sleep 1s
	if agh_startup_ready; then
		ptxt_ok "AdGuardHome service is running."
		return 0
	fi
	ptxt_fail "AdGuardHome service did not become ready after start."
	return 1
}

agh_start_error() {
	PTXT "${ERROR} Couldn't start AdGuardHome!" \
		"${ERROR} Please send WebUI System Log to dev."
}

agh_stop() {
	if [ "$(agh_process_count)" -ge "1" ]; then
		ptxt_step "Stopping AdGuardHome service."
		if service stop_AdGuardHome >/dev/null 2>&1; then
			adguard_service_status_after_action stop || true
		elif /opt/etc/init.d/S99AdGuardHome stop; then
			adguard_service_status_after_action stop || true
		elif /opt/etc/init.d/S99AdGuardHome kill; then
			adguard_service_status_after_action kill || true
		else
			if ai_have_cmd killall; then
				killall -q -9 AdGuardHome 2>/dev/null
			else
				PTXT "${ERROR} Unable to force-stop AdGuardHome because killall is unavailable."
				return 1
			fi
			rm -rf /opt/var/run/AdGuardHome.pid 2>/dev/null
			adguard_service_status_after_action kill || true
		fi
	fi
	sleep 1s
	if ! agh_wait_stopped "${ADGUARDHOME_WAIT_TIMEOUT}"; then
		ptxt_fail "Timed out waiting for AdGuardHome service to stop."
		return 1
	fi
	sleep 1s
	agh_check
	ptxt_ok "AdGuardHome service is stopped."
	return 0
}

agh_wait_ready() {
	local elapsed maxwait now remaining start
	maxwait="${1:-${ADGUARDHOME_READY_TIMEOUT}}"
	start="$(monotonic_seconds)" || return 1
	while :; do
		now="$(monotonic_seconds)" || return 1
		elapsed="$((now - start))"
		if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi
		remaining="$((maxwait - elapsed))"
		if check_connection "${remaining}" && agh_is_running; then return 0; fi
		now="$(monotonic_seconds)" || return 1
		elapsed="$((now - start))"
		if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi
		sleep 1s
	done
}

agh_wait_started() {
	local elapsed maxwait
	elapsed="0"
	maxwait="${1:-${ADGUARDHOME_WAIT_TIMEOUT}}"
	while ! agh_is_running; do
		if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi
		sleep 1s
		elapsed="$((elapsed + 1))"
	done
	return 0
}

agh_wait_stopped() {
	local elapsed maxwait
	elapsed="0"
	maxwait="${1:-${ADGUARDHOME_WAIT_TIMEOUT}}"
	while [ "$(agh_process_count)" -ge "1" ]; do
		if [ "${elapsed}" -ge "${maxwait}" ]; then return 1; fi
		sleep 1s
		elapsed="$((elapsed + 1))"
	done
	return 0
}

monotonic_seconds() {
	local uptime
	if read -r uptime _ </proc/uptime; then
		uptime="${uptime%%.*}"
		case "${uptime}" in
			"" | *[!0-9]*) return 1 ;;
		esac
		PTXT "${uptime}"
		return 0
	fi
	return 1
}

# Validation and discovery helpers

adguard_remote_archive() {
	case "$1" in
		release) PTXT "${REMOTE_RELEASE_ARCHIVE}" ;;
		beta) PTXT "${REMOTE_BETA_ARCHIVE}" ;;
		edge) PTXT "${REMOTE_EDGE_ARCHIVE}" ;;
		*) return 1 ;;
	esac
}

adguard_remote_md5() {
	case "$1" in
		release) PTXT "${REMOTE_RELEASE_MD5}" ;;
		beta) PTXT "${REMOTE_BETA_MD5}" ;;
		edge) PTXT "${REMOTE_EDGE_MD5}" ;;
		*) return 1 ;;
	esac
}

adguard_remote_sha256() {
	case "$1" in
		release) PTXT "${REMOTE_RELEASE_SHA256}" ;;
		beta) PTXT "${REMOTE_BETA_SHA256}" ;;
		edge) PTXT "${REMOTE_EDGE_SHA256}" ;;
		*) return 1 ;;
	esac
}

adguard_remote_version() {
	case "$1" in
		release) PTXT "${REMOTE_VER}" ;;
		beta) PTXT "${REMOTE_BETA}" ;;
		edge) PTXT "${REMOTE_EDGE}" ;;
		*) return 1 ;;
	esac
}

adguard_remote_url() {
	case "$1" in
		release) PTXT "${REMOTE_RELEASE_URL}" ;;
		beta) PTXT "${REMOTE_BETA_URL}" ;;
		edge) PTXT "${REMOTE_EDGE_URL}" ;;
		*) return 1 ;;
	esac
}

adguard_validate_metadata() {
	if { [ -n "${REMOTE_VER}" ] && [ -n "${REMOTE_BETA}" ] && [ -n "${REMOTE_EDGE}" ] &&
		[ -n "${REMOTE_RELEASE_ARCHIVE}" ] && [ -n "${REMOTE_BETA_ARCHIVE}" ] && [ -n "${REMOTE_EDGE_ARCHIVE}" ] &&
		[ -n "${REMOTE_RELEASE_URL}" ] && [ -n "${REMOTE_BETA_URL}" ] && [ -n "${REMOTE_EDGE_URL}" ] &&
		md5_is_valid "${REMOTE_RELEASE_MD5}" && md5_is_valid "${REMOTE_BETA_MD5}" && md5_is_valid "${REMOTE_EDGE_MD5}"; }; then
		return 0
	else
		return 1
	fi
}

init_adguard_metadata_defaults() {
	REMOTE_RELEASE_ARCHIVE="AdGuardHome_${ADGUARD_ARCH}.tar.gz"
	REMOTE_BETA_ARCHIVE="AdGuardHome_${ADGUARD_ARCH}.tar.gz"
	REMOTE_EDGE_ARCHIVE="AdGuardHome_${ADGUARD_ARCH}.tar.gz"
	REMOTE_RELEASE_MD5=""
	REMOTE_BETA_MD5=""
	REMOTE_EDGE_MD5=""
	REMOTE_RELEASE_SHA256=""
	REMOTE_BETA_SHA256=""
	REMOTE_EDGE_SHA256=""
	REMOTE_RELEASE_URL="https://static.adguard.com/adguardhome/release/${REMOTE_RELEASE_ARCHIVE}"
	REMOTE_BETA_URL="https://static.adguard.com/adguardhome/beta/${REMOTE_BETA_ARCHIVE}"
	REMOTE_EDGE_URL="https://static.adguard.com/adguardhome/edge/${REMOTE_EDGE_ARCHIVE}"
}

init_remote_adguard_metadata() {
	local COUNT CHANNEL_FILE
	REMOTE_VER=""
	REMOTE_BETA=""
	REMOTE_EDGE=""
	REMOTE_RELEASE_ARCHIVE=""
	REMOTE_BETA_ARCHIVE=""
	REMOTE_EDGE_ARCHIVE=""
	REMOTE_RELEASE_MD5=""
	REMOTE_BETA_MD5=""
	REMOTE_EDGE_MD5=""
	REMOTE_RELEASE_SHA256=""
	REMOTE_BETA_SHA256=""
	REMOTE_EDGE_SHA256=""
	REMOTE_RELEASE_URL=""
	REMOTE_BETA_URL=""
	REMOTE_EDGE_URL=""
	COUNT="0"
	CHANNEL_FILE="${URL_ARCH}/checksum.txt"
	ptxt_phase "Fetching AdGuardHome metadata from ${CHANNEL_FILE}."
	while [ "${COUNT}" -lt 3 ]; do
		rm -f "${ADGUARD_METADATA_FILE}"
		if http_get_file "${CHANNEL_FILE}" "${ADGUARD_METADATA_FILE}" "" insecure && [ -s "${ADGUARD_METADATA_FILE}" ]; then
			REMOTE_VER="$(awk '$1 !~ /^#/ && $2 == "stable" {VERSION = $3; sub(/^version=/, "", VERSION); print VERSION; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_BETA="$(awk '$1 !~ /^#/ && $2 == "beta" {VERSION = $3; sub(/^version=/, "", VERSION); print VERSION; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_EDGE="$(awk '$1 !~ /^#/ && $2 == "edge" {VERSION = $3; sub(/^version=/, "", VERSION); print VERSION; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_RELEASE_ARCHIVE="$(awk '$1 !~ /^#/ && $2 == "stable" {print $1; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_BETA_ARCHIVE="$(awk '$1 !~ /^#/ && $2 == "beta" {print $1; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_EDGE_ARCHIVE="$(awk '$1 !~ /^#/ && $2 == "edge" {print $1; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_RELEASE_MD5="$(awk '$1 !~ /^#/ && $2 == "stable" {print $4; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_BETA_MD5="$(awk '$1 !~ /^#/ && $2 == "beta" {print $4; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_EDGE_MD5="$(awk '$1 !~ /^#/ && $2 == "edge" {print $4; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_RELEASE_SHA256="$(awk '$1 !~ /^#/ && $2 == "stable" {print $5; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_BETA_SHA256="$(awk '$1 !~ /^#/ && $2 == "beta" {print $5; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_EDGE_SHA256="$(awk '$1 !~ /^#/ && $2 == "edge" {print $5; exit}' "${ADGUARD_METADATA_FILE}")"
			REMOTE_RELEASE_URL="${URL_ARCH}/${REMOTE_RELEASE_ARCHIVE}"
			REMOTE_BETA_URL="${URL_ARCH}/${REMOTE_BETA_ARCHIVE}"
			REMOTE_EDGE_URL="${URL_ARCH}/${REMOTE_EDGE_ARCHIVE}"
			if adguard_validate_metadata; then
				ptxt_ok "Detected AdGuardHome versions: release ${REMOTE_VER}, beta ${REMOTE_BETA}, edge ${REMOTE_EDGE}."
				export REMOTE_VER REMOTE_BETA REMOTE_EDGE REMOTE_RELEASE_ARCHIVE REMOTE_BETA_ARCHIVE REMOTE_EDGE_ARCHIVE REMOTE_RELEASE_MD5 REMOTE_BETA_MD5 REMOTE_EDGE_MD5 REMOTE_RELEASE_SHA256 REMOTE_BETA_SHA256 REMOTE_EDGE_SHA256 REMOTE_RELEASE_URL REMOTE_BETA_URL REMOTE_EDGE_URL
				cleanup_api_files
				return 0
			fi
		fi
		COUNT="$((COUNT + 1))"
		[ "${COUNT}" -lt 3 ] && ptxt_warn "Metadata fetch from ${CHANNEL_FILE} failed; retrying."
	done
	ptxt_warn "Metadata fetch from ${CHANNEL_FILE} failed; continuing with upstream static metadata."
	init_upstream_adguard_metadata "${CHANNEL_FILE}"
}

init_upstream_adguard_metadata() {
	local BETA_VERSION_FILE CHANNEL_FILE COUNT EDGE_VERSION_FILE RELEASE_VERSION_FILE
	CHANNEL_FILE="$1"
	init_adguard_metadata_defaults
	RELEASE_VERSION_FILE="${ADGUARD_METADATA_FILE}.release"
	BETA_VERSION_FILE="${ADGUARD_METADATA_FILE}.beta"
	EDGE_VERSION_FILE="${ADGUARD_METADATA_FILE}.edge"
	COUNT="0"
	ptxt_phase "Fetching upstream AdGuardHome version metadata."
	while [ "${COUNT}" -lt 3 ]; do
		rm -f "${RELEASE_VERSION_FILE}" "${BETA_VERSION_FILE}" "${EDGE_VERSION_FILE}"
		if http_get_file "https://static.adguard.com/adguardhome/release/version.txt" "${RELEASE_VERSION_FILE}" "" insecure && [ -s "${RELEASE_VERSION_FILE}" ] &&
			http_get_file "https://static.adguard.com/adguardhome/beta/version.txt" "${BETA_VERSION_FILE}" "" insecure && [ -s "${BETA_VERSION_FILE}" ] &&
			http_get_file "https://static.adguard.com/adguardhome/edge/version.txt" "${EDGE_VERSION_FILE}" "" insecure && [ -s "${EDGE_VERSION_FILE}" ]; then
			REMOTE_VER="$(awk 'NF {VERSION = $1; sub(/^version=/, "", VERSION); print VERSION; exit}' "${RELEASE_VERSION_FILE}")"
			REMOTE_BETA="$(awk 'NF {VERSION = $1; sub(/^version=/, "", VERSION); print VERSION; exit}' "${BETA_VERSION_FILE}")"
			REMOTE_EDGE="$(awk 'NF {VERSION = $1; sub(/^version=/, "", VERSION); print VERSION; exit}' "${EDGE_VERSION_FILE}")"
			if [ -n "${REMOTE_VER}" ] && [ -n "${REMOTE_BETA}" ] && [ -n "${REMOTE_EDGE}" ]; then
				ptxt_ok "Detected upstream AdGuardHome versions: release ${REMOTE_VER}, beta ${REMOTE_BETA}, edge ${REMOTE_EDGE}."
				export REMOTE_VER REMOTE_BETA REMOTE_EDGE REMOTE_RELEASE_ARCHIVE REMOTE_BETA_ARCHIVE REMOTE_EDGE_ARCHIVE REMOTE_RELEASE_MD5 REMOTE_BETA_MD5 REMOTE_EDGE_MD5 REMOTE_RELEASE_SHA256 REMOTE_BETA_SHA256 REMOTE_EDGE_SHA256 REMOTE_RELEASE_URL REMOTE_BETA_URL REMOTE_EDGE_URL
				rm -f "${RELEASE_VERSION_FILE}" "${BETA_VERSION_FILE}" "${EDGE_VERSION_FILE}"
				return 0
			fi
		fi
		COUNT="$((COUNT + 1))"
		[ "${COUNT}" -lt 3 ] && ptxt_warn "Upstream metadata fetch failed; retrying."
	done
	rm -f "${RELEASE_VERSION_FILE}" "${BETA_VERSION_FILE}" "${EDGE_VERSION_FILE}"
	PTXT "One or more critical variables could not be set in a timely manner." \
		"Unable to read complete AdGuardHome package metadata from ${CHANNEL_FILE} or upstream static metadata." \
		"Please be sure to check your internet connection, and please try again later." \
		"The installer script will now exit."
	sleep 3s
	exit 1
}

check_AdGuardHome_yaml() {
	local yaml_target
	yaml_target="${1:-${YAML_FILE}}"
	[ ! -f "${yaml_target}" ] && return
	chmod 600 "${yaml_target}"
	ptxt_step "Checking AdGuardHome configuration ${yaml_target}."
	if ! "${AGH_FILE}" --check-config -c "${yaml_target}" --no-check-update -l "/dev/null"; then
		if [ "${yaml_target}" = "${YAML_FILE}" ]; then
			PTXT "${INFO} Moving invalid configuration file to ${YAML_ERR}." \
				"${INFO} Operation will continue with clean config file."
			if mv "${yaml_target}" "${YAML_ERR}"; then
				rollback_result_write "yaml validation" "yaml-invalid-moved" "${YAML_ERR}"
			else
				rollback_result_write "yaml validation" "yaml-quarantine-failed" "${yaml_target}"
			fi
			rollback_result_notice
		else
			ptxt_warn "Moving invalid staged configuration file to ${yaml_target}.err; continuing."
			if mv "${yaml_target}" "${yaml_target}.err"; then
				rollback_result_write "yaml validation" "staged-yaml-invalid-moved" "${yaml_target}.err"
			else
				rollback_result_write "yaml validation" "staged-yaml-quarantine-failed" "${yaml_target}"
			fi
			rollback_result_notice
		fi
		return 1
	fi
	ptxt_ok "AdGuardHome configuration ${yaml_target} is valid."
}

# Blocklist analyzer helpers

blocklist_analyzer_ids() {
	awk '
		/^[[:space:]]*UNUSED BLOCKLISTS[[:space:]]*\(/ { in_unused = 1; next }
		in_unused && /^[-=][-=]*$/ { next }
		in_unused && /^$/ { exit }
		in_unused {
			line = $0
			sub(/^[[:space:]]*/, "", line)
			if (line ~ /^[0-9]+\.txt([[:space:]]|$)/) {
				sub(/\.txt.*/, "", line)
				print line
			}
		}
	' "$1"
}

ensure_blocklist_analyzer_dependencies() {
	ptxt_step "Checking for Entware python3."
	if [ ! -x /opt/bin/python3 ]; then
		ptxt_warn "python3 is required to run the analyzer, but it was not found."
		PTXT "${INFO} Entware python3 can be installed with: opkg install python3"
		if read_yesno "Do you want Entware to install python3 now?"; then
			ptxt_step "Installing Entware python3 package."
			if ! ensure_opkg_package python3 || [ ! -x /opt/bin/python3 ]; then
				ptxt_fail "Unable to install or find Entware python3."
				return 1
			fi
			ptxt_ok "Entware python3 is installed."
		else
			ptxt_fail "python3 is required to run the blocklist analyzer."
			return 1
		fi
	fi
	ptxt_ok "Entware python3 is available."

	ptxt_step "Checking for SHA-256 checksum support."
	ensure_sha256sum_tool || return 1
	ptxt_ok "SHA-256 checksum support is available."
}

install_blocklist_analyzer() {
	ptxt_phase "Preparing AdGuardHome blocklist analyzer."
	ptxt_step "Checking AdGuardHome target directory ${TARG_DIR}."
	if [ ! -d "${TARG_DIR}" ] || [ ! -d "${TARG_DIR}/data" ]; then
		ptxt_fail "${TARG_DIR} and ${TARG_DIR}/data are required before blocklists can be analyzed."
		return 1
	fi
	ptxt_ok "AdGuardHome data directory exists."

	ensure_blocklist_analyzer_dependencies || return 1

	if [ -z "${BLOCKLIST_ANALYZER_SHA256}" ]; then
		ptxt_fail "Blocklist analyzer SHA-256 is not pinned; refusing to download or run mutable remote code."
		return 1
	fi

	ptxt_step "Downloading blocklist analyzer to ${BLOCKLIST_ANALYZER_FILE}."
	rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp"
	if ! http_get_file "${BLOCKLIST_ANALYZER_URL}" "${BLOCKLIST_ANALYZER_FILE}.tmp"; then
		rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp"
		ptxt_fail "Unable to download blocklist analyzer."
		return 1
	fi
	ptxt_ok "Downloaded blocklist analyzer."

	ptxt_step "Verifying blocklist analyzer SHA-256 checksum."
	if ! ensure_sha256sum_tool; then
		rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp"
		return 1
	fi
	if [ "$(file_sha256 "${BLOCKLIST_ANALYZER_FILE}.tmp" 2>/dev/null)" != "${BLOCKLIST_ANALYZER_SHA256}" ]; then
		rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp"
		ptxt_fail "Blocklist analyzer checksum verification failed."
		return 1
	fi
	ptxt_ok "Blocklist analyzer checksum verified."

	ptxt_step "Installing blocklist analyzer with restrictive permissions."
	if ! mv "${BLOCKLIST_ANALYZER_FILE}.tmp" "${BLOCKLIST_ANALYZER_FILE}"; then
		rm -f "${BLOCKLIST_ANALYZER_FILE}.tmp"
		ptxt_fail "Unable to install blocklist analyzer."
		return 1
	fi
	chmod 700 "${BLOCKLIST_ANALYZER_FILE}"
	ptxt_ok "Blocklist analyzer is ready."
}

run_blocklist_analyzer() {
	local filter_file found_filter output_file ids_file python_cmd
	output_file="${TARG_DIR}/blocklist_analyzer.$$.out"
	ids_file="${TARG_DIR}/blocklist_analyzer.$$.ids"
	ptxt_phase "Running AdGuardHome blocklist analyzer."
	ptxt_step "Checking analyzer input files."
	if [ ! -d "${TARG_DIR}/data/filters" ]; then
		ptxt_fail "AdGuardHome filter files are missing; expected ${TARG_DIR}/data/filters."
		return 1
	fi
	found_filter=""
	for filter_file in "${TARG_DIR}/data/filters"/*.txt; do
		if [ -f "${filter_file}" ]; then
			found_filter="1"
			break
		fi
	done
	if [ -z "${found_filter}" ]; then
		ptxt_fail "AdGuardHome filter files are missing; ${TARG_DIR}/data/filters contains no filter files."
		return 1
	fi
	if [ ! -s "${TARG_DIR}/data/querylog.json" ]; then
		ptxt_fail "AdGuardHome query log is missing or empty; enable query logging and let AdGuardHome collect queries before analyzing blocklists."
		return 1
	fi
	ptxt_ok "Analyzer input files are available."

	python_cmd="/opt/bin/python3"
	if [ ! -x "${python_cmd}" ]; then
		ptxt_fail "Entware python3 is required to run the blocklist analyzer."
		return 1
	fi

	ptxt_step "Running analyzer from ${TARG_DIR} so ./data resolves correctly."
	if ! (cd "${TARG_DIR}" && "${python_cmd}" "${BLOCKLIST_ANALYZER_FILE}") >"${output_file}" 2>&1; then
		cat "${output_file}"
		rm -f "${output_file}" "${ids_file}"
		ptxt_fail "Blocklist analyzer failed."
		return 1
	fi
	ptxt_ok "Blocklist analyzer completed."
	PTXT "" "${INFO} Analyzer output:" "----------------------------------------------------"
	cat "${output_file}"
	PTXT "----------------------------------------------------"
	blocklist_analyzer_ids "${output_file}" >"${ids_file}"
	if [ ! -s "${ids_file}" ]; then
		rm -f "${output_file}" "${ids_file}"
		ptxt_ok "No unused blocklists were detected."
		return 2
	fi
	PTXT "${INFO} Unused blocklist IDs detected:"
	cat "${ids_file}"
	BLOCKLIST_ANALYZER_IDS_FILE="${ids_file}"
	BLOCKLIST_ANALYZER_OUTPUT_FILE="${output_file}"
	return 0
}

blocklist_yaml_candidates() {
	local ids_file yaml_target
	ids_file="$1"
	yaml_target="${2:-${YAML_FILE}}"
	awk -v idsfile="${ids_file}" '
		BEGIN { while ((getline wanted < idsfile) > 0) ids[wanted] = 1; close(idsfile); in_filters = 0; in_entry = 0 }
		function emit_entry() {
			if (in_entry && id != "" && ids[id]) print id "|" name "|" url
			id = ""; name = ""; url = ""; in_entry = 0
		}
		/^[^[:space:]][^:]*:/ {
			if (in_filters && $0 !~ /^filters:/) { emit_entry(); in_filters = 0 }
		}
		/^filters:[[:space:]]*$/ { emit_entry(); in_filters = 1; next }
		in_filters && /^[[:space:]]*-[[:space:]]*enabled:/ { emit_entry(); in_entry = 1; next }
		in_filters && in_entry {
			if ($0 ~ /^[[:space:]]*id:[[:space:]]*[0-9]+[[:space:]]*$/) { id = $0; sub(/.*id:[[:space:]]*/, "", id); sub(/[[:space:]]*$/, "", id) }
			if ($0 ~ /^[[:space:]]*name:[[:space:]]*/) { name = $0; sub(/^[[:space:]]*name:[[:space:]]*/, "", name) }
			if ($0 ~ /^[[:space:]]*url:[[:space:]]*/) { url = $0; sub(/^[[:space:]]*url:[[:space:]]*/, "", url) }
		}
		END { emit_entry() }
	' "${yaml_target}"
}

select_unused_blocklists_for_removal() {
	local ids_file candidates_file selected_file id name url list_label choice_status
	ids_file="$1"
	candidates_file="${TARG_DIR}/blocklist_analyzer.$$.candidates"
	selected_file="${TARG_DIR}/blocklist_analyzer.$$.selected"
	blocklist_yaml_candidates "${ids_file}" >"${candidates_file}"
	if [ ! -s "${candidates_file}" ]; then
		rm -f "${candidates_file}" "${selected_file}"
		ptxt_warn "No matching unused IDs were found under the AdGuardHome filters section."
		return 1
	fi
	PTXT "${INFO} Matching unused blocklists found in AdGuardHome.yaml:"
	while IFS='|' read -r id name url; do
		PTXT "  id=${id} name=${name} url=${url}"
	done <"${candidates_file}"
	PTXT "" \
		"${WARNING} Review these lists before removal." \
		"${WARNING} Unused means the list did not uniquely block anything in the analyzed query log period."
	if read_yesno "Do you want to remove all matching unused blocklists?"; then
		awk -F'|' '{ print $1 }' "${candidates_file}" >"${selected_file}"
		rm -f "${candidates_file}"
		BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="${selected_file}"
		return 0
	else
		choice_status="$?"
	fi
	if [ "${choice_status}" -ne 1 ]; then
		rm -f "${candidates_file}" "${selected_file}"
		return "${choice_status}"
	fi
	: >"${selected_file}" || {
		rm -f "${candidates_file}"
		return 1
	}
	while IFS='|' read -r id name url; do
		[ -n "${name}" ] && list_label="${name}" || list_label="id ${id}"
		PTXT "" "${INFO} Unused blocklist candidate:" "  id=${id}" "  name=${name}" "  url=${url}"
		read_yesno "Remove blocklist ${list_label} from AdGuardHome.yaml?"
		choice_status="$?"
		case "${choice_status}" in
			0) PTXT "${id}" >>"${selected_file}" ;;
			1) ptxt_ok "Keeping blocklist id ${id}." ;;
			*)
				rm -f "${candidates_file}" "${selected_file}"
				return "${choice_status}"
				;;
		esac
	done <"${candidates_file}"
	rm -f "${candidates_file}"
	if [ ! -s "${selected_file}" ]; then
		rm -f "${selected_file}"
		ptxt_warn "No unused blocklists were selected for removal."
		return 1
	fi
	BLOCKLIST_ANALYZER_SELECTED_IDS_FILE="${selected_file}"
	return 0
}

blocklist_yaml_replace_abort_on_signal() {
	local backup_file removed_file tmp_file
	backup_file="${BLOCKLIST_YAML_BACKUP_FILE:-}"
	removed_file="${BLOCKLIST_YAML_REMOVED_FILE:-}"
	tmp_file="${BLOCKLIST_YAML_TMP_FILE:-}"
	trap - HUP INT QUIT ABRT TERM
	if [ -n "${backup_file}" ] && [ -f "${backup_file}" ]; then
		if cp "${backup_file}" "${YAML_FILE}" || { rm -f "${YAML_FILE}" && mv "${backup_file}" "${YAML_FILE}"; }; then
			chmod 600 "${YAML_FILE}"
			rollback_result_write "blocklist yaml replacement" "restored" "interrupted; restored ${YAML_FILE}"
			rollback_result_notice
			ptxt_fail "Blocklist cleanup interrupted; restored ${YAML_FILE} from ${backup_file}."
		else
			rollback_result_write "blocklist yaml replacement" "restore-failed" "interrupted; backup ${backup_file}"
			rollback_result_notice
			ptxt_fail "Blocklist cleanup interrupted, and ${YAML_FILE} could not be restored from ${backup_file}."
		fi
	fi
	[ -z "${tmp_file}" ] || rm -f "${tmp_file}"
	[ -z "${removed_file}" ] || rm -f "${removed_file}"
	BLOCKLIST_YAML_BACKUP_FILE=""
	BLOCKLIST_YAML_REMOVED_FILE=""
	BLOCKLIST_YAML_TMP_FILE=""
	clear_screen
	exit 2
}

blocklist_yaml_replace_trap_disable() {
	trap 'clear_screen; end_op_message 2' HUP INT QUIT ABRT TERM
	BLOCKLIST_YAML_BACKUP_FILE=""
	BLOCKLIST_YAML_REMOVED_FILE=""
	BLOCKLIST_YAML_TMP_FILE=""
}

blocklist_yaml_replace_trap_enable() {
	BLOCKLIST_YAML_BACKUP_FILE="$1"
	BLOCKLIST_YAML_REMOVED_FILE="$2"
	BLOCKLIST_YAML_TMP_FILE="${3:-}"
	trap 'blocklist_yaml_replace_abort_on_signal' HUP INT QUIT ABRT TERM
}

remove_unused_blocklists_from_yaml() {
	local ids_file backup_file tmp_file removed_file
	ids_file="$1"
	backup_file="${YAML_FILE}.blocklists.$(date +%Y%m%d%H%M%S).$$.bak"
	tmp_file="${YAML_FILE}.blocklists.$$.tmp"
	removed_file="${YAML_FILE}.blocklists.$$.removed"
	ptxt_phase "Removing unused blocklists from AdGuardHome.yaml."
	if [ ! -f "${YAML_FILE}" ]; then
		ptxt_fail "${YAML_FILE} does not exist."
		return 1
	fi
	ptxt_step "Lists selected for removal from the filters section:"
	blocklist_yaml_candidates "${ids_file}" "${YAML_FILE}" | while IFS='|' read -r id name url; do
		PTXT "  id=${id} name=${name} url=${url}"
	done
	ptxt_step "Creating backup ${backup_file}."
	cp "${YAML_FILE}" "${backup_file}" || return 1
	ptxt_ok "Backup created."
	awk -v idsfile="${ids_file}" -v removedfile="${removed_file}" '
		BEGIN { while ((getline id < idsfile) > 0) ids[id] = 1; close(idsfile); in_filters = 0; in_entry = 0 }
		function flush_entry(   n) {
			if (!in_entry) return
			if (remove_entry) { print removed_id >> removedfile }
			else { printf "%s", entry }
			entry = ""; in_entry = 0; remove_entry = 0; removed_id = ""
		}
		/^[^[:space:]][^:]*:/ {
			if (in_filters && $0 !~ /^filters:/) { flush_entry(); in_filters = 0 }
		}
		/^filters:[[:space:]]*$/ { flush_entry(); in_filters = 1; print; next }
		in_filters && /^[[:space:]]*-[[:space:]]*enabled:/ { flush_entry(); entry = $0 ORS; in_entry = 1; next }
		in_filters && in_entry {
			entry = entry $0 ORS
			if ($0 ~ /^[[:space:]]*id:[[:space:]]*[0-9]+[[:space:]]*$/) { id = $0; sub(/.*id:[[:space:]]*/, "", id); sub(/[[:space:]]*$/, "", id); if (ids[id]) { remove_entry = 1; removed_id = id } }
			next
		}
		{ flush_entry(); print }
		END { flush_entry() }
	' "${YAML_FILE}" >"${tmp_file}" || {
		rm -f "${tmp_file}" "${removed_file}"
		rollback_result_write "blocklist-yaml" "failed: no change" "could not build edited YAML"
		rollback_result_notice
		return 1
	}
	if [ ! -s "${removed_file}" ]; then
		rm -f "${tmp_file}" "${removed_file}"
		ptxt_warn "No matching numeric filter IDs were found under filters:."
		return 1
	fi
	blocklist_yaml_replace_trap_enable "${backup_file}" "${removed_file}" "${tmp_file}"
	if ! mv "${tmp_file}" "${YAML_FILE}"; then
		rm -f "${tmp_file}" "${removed_file}"
		rollback_result_write "blocklist yaml replacement" "replace-failed" "${YAML_FILE}"
		rollback_result_notice
		blocklist_yaml_replace_trap_disable
		return 1
	fi
	chmod 600 "${YAML_FILE}"
	ptxt_step "Validating edited AdGuardHome configuration."
	if ! check_AdGuardHome_yaml "${YAML_FILE}"; then
		if cp "${backup_file}" "${YAML_FILE}" || { rm -f "${YAML_FILE}" && mv "${backup_file}" "${YAML_FILE}"; }; then
			chmod 600 "${YAML_FILE}"
			rm -f "${removed_file}"
			blocklist_yaml_replace_trap_disable
			rollback_result_write "blocklist yaml replacement" "restored" "validation failed; restored ${YAML_FILE}"
			rollback_result_notice
			ptxt_fail "Validation failed; restored ${YAML_FILE} from ${backup_file}."
		else
			rm -f "${removed_file}"
			blocklist_yaml_replace_trap_disable
			rollback_result_write "blocklist yaml replacement" "restore-failed" "validation failed; backup ${backup_file}"
			rollback_result_notice
			ptxt_fail "Validation failed, and ${YAML_FILE} could not be restored from ${backup_file}."
		fi
		return 1
	fi
	blocklist_yaml_replace_trap_disable
	ptxt_ok "Removed unused blocklist IDs:"
	cat "${removed_file}"
	rm -f "${removed_file}"
	ptxt_warn "Restarting AdGuardHome temporarily interrupts AdGuardHome DNS filtering."
	OLD_PIDS="$(pidof AdGuardHome 2>/dev/null)"
	if service restart_AdGuardHome >/dev/null 2>&1; then
		adguard_service_status_after_action restart "${OLD_PIDS}" || true
		ptxt_ok "AdGuardHome restarted after blocklist cleanup."
	else
		ptxt_warn "Configuration was saved, but AdGuardHome could not be restarted automatically."
	fi
}

cleanup_unused_blocklists() {
	install_blocklist_analyzer || {
		end_op_message 1
		return 1
	}
	run_blocklist_analyzer
	case "$?" in
		0) ;;
		2)
			end_op_message 0
			return 0
			;;
		*)
			end_op_message 1
			return 1
			;;
	esac
	if ! select_unused_blocklists_for_removal "${BLOCKLIST_ANALYZER_IDS_FILE}"; then
		rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}"
		ptxt_warn "Blocklist cleanup cancelled or nothing was selected."
		end_op_message 1
		return 1
	fi
	if remove_unused_blocklists_from_yaml "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE}"; then
		rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}"
		end_op_message 0
	else
		rm -f "${BLOCKLIST_ANALYZER_IDS_FILE:-}" "${BLOCKLIST_ANALYZER_OUTPUT_FILE:-}" "${BLOCKLIST_ANALYZER_SELECTED_IDS_FILE:-}"
		end_op_message 1
		return 1
	fi
}

########################Modified Version of @Adamm Check_Connection####################################################

check_connection() {
	local deadline i livecheck="0" maxwait now remaining sleep_for
	maxwait="${1:-}"
	if [ -n "${maxwait}" ]; then
		now="$(monotonic_seconds)"
		deadline="$((now + maxwait))"
	fi
	while [ "${livecheck}" != "4" ]; do
		for i in google.com github.com snbforums.com; do
			if [ -n "${deadline}" ]; then
				now="$(monotonic_seconds)"
				[ "${now}" -ge "${deadline}" ] && return 1
			fi
			if { ! nslookup "${i}" 127.0.0.1 >/dev/null 2>&1; } && { ping -q -w3 -c1 "${i}" >/dev/null 2>&1; }; then
				remaining=""
				if [ -n "${deadline}" ]; then
					now="$(monotonic_seconds)"
					remaining="$((deadline - now))"
					[ "${remaining}" -le 0 ] && return 1
				fi
				if ! http_probe "http://${i}" "${remaining}" >/dev/null 2>&1; then
					if [ -n "${deadline}" ]; then
						now="$(monotonic_seconds)"
						[ "${now}" -ge "${deadline}" ] && return 1
					fi
					sleep 1s
					continue
				fi
			fi
			return 0
		done
		livecheck="$((livecheck + 1))"
		if [ "${livecheck}" != "4" ]; then
			if [ -n "${deadline}" ]; then
				now="$(monotonic_seconds)"
				sleep_for="$((deadline - now))"
				[ "${sleep_for}" -le 0 ] && return 1
				[ "${sleep_for}" -gt 10 ] && sleep_for="10"
				sleep "${sleep_for}s"
			else
				sleep 10s
			fi
			continue
		fi
		return 1
	done
}

########################################################################################################################

check_dns_environment() {
	ptxt_phase "Preparing DNS environment."
	if [ -f "/opt/etc/init.d/S61stubby" ] || [ -f "/opt/sbin/stubby" ] || [ -f "/opt/bin/install_stubby" ] || [ -f "/jffs/scripts/install_stubby.sh" ] || [ -d "/jffs/dnscrypt" ] || [ -f "/opt/etc/init.d/S09dnscrypt-proxy" ]; then
		PTXT "${ERROR} Potential stubby or dnscrypt-proxy installation detected." \
			"${ERROR} Please remove before attempting to continue." \
			"${ERROR} Exiting..."
		exit 1
	fi
	local MODE NVCHECK
	dns_env_set_nvram() {
		local key expected cur changed
		key="$1"
		expected="$2"
		cur="$(nvram get "${key}" 2>/dev/null)"
		if [ "${cur}" = "${expected}" ]; then
			return 1
		fi
		nvram set "${key}=${expected}"
		changed="1"
		return 0
	}
	dns_env_apply_profile() {
		local changed
		changed="0"
		if dns_env_set_nvram "dnspriv_enable" "0"; then changed="$((changed + 1))"; fi
		if dns_env_set_nvram "dhcpd_dns_router" "1"; then changed="$((changed + 1))"; fi
		if dns_env_set_nvram "dhcp_dns1_x" ""; then changed="$((changed + 1))"; fi
		if dns_env_set_nvram "dhcp_dns2_x" ""; then changed="$((changed + 1))"; fi
		if [ "${changed}" != "0" ]; then return 0; else return 1; fi
	}
	dns_env_restore_profile() {
		local changed key cur old
		changed="0"
		for key in dnspriv_enable dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x; do
			cur="$(nvram get "${key}" 2>/dev/null)"
			case "${key}" in
				dnspriv_enable) old="${_OLD_dnspriv_enable}" ;;
				dhcpd_dns_router) old="${_OLD_dhcpd_dns_router}" ;;
				dhcp_dns1_x) old="${_OLD_dhcp_dns1_x}" ;;
				dhcp_dns2_x) old="${_OLD_dhcp_dns2_x}" ;;
			esac
			if [ "${cur}" != "${old}" ]; then
				nvram set "${key}=${old}"
				changed="$((changed + 1))"
			fi
		done
		if [ "${changed}" != "0" ]; then return 0; else return 1; fi
	}
	MODE="$1"
	NVCHECK="0"
	case "${MODE}" in
		0)
			# Save original values only once.
			if [ "${_DNS_NVRAM_SAVED:-0}" != "1" ]; then
				ptxt_step "Saving current DNS NVRAM settings."
				save_dns_nvram_environment
				ptxt_ok "Current DNS NVRAM settings saved."
			fi
			if [ "$(pidof stubby)" ]; then
				ptxt_step "Stopping conflicting stubby process."
				{ kill_processes stubby; }
				ptxt_ok "Conflicting stubby process stopped."
				NVCHECK="$((NVCHECK + 1))"
			fi
			ptxt_step "Applying installer DNS preparation profile."
			if dns_env_apply_profile; then
				ptxt_ok "Installer DNS preparation profile applied."
				NVCHECK="$((NVCHECK + 1))"
			else
				ptxt_ok "Installer DNS preparation profile was already active."
			fi
			;;
		1)
			# Do not restore if we never saved anything.
			if [ "${_DNS_NVRAM_SAVED:-0}" != "1" ]; then
				ptxt_ok "No saved DNS NVRAM settings to restore."
				return 0
			fi
			ptxt_step "Restoring saved DNS NVRAM settings."
			if dns_env_restore_profile; then
				ptxt_ok "Saved DNS NVRAM settings restored."
				NVCHECK="$((NVCHECK + 1))"
			else
				ptxt_ok "Saved DNS NVRAM settings were already restored."
			fi
			;;
		*)
			return 1
			;;
	esac
	if [ "${NVCHECK}" != "0" ]; then
		ptxt_step "Committing DNS NVRAM changes and restarting dnsmasq."
		{ nvram commit; }
		{ service restart_dnsmasq >/dev/null 2>&1; }
		(while { ! check_connection; }; do sleep 1s; done) &
		local PID="$!"
		wait "${PID}" 2>/dev/null
		ptxt_ok "DNS services refreshed after DNS environment changes."
	fi
	ptxt_ok "DNS Environment is Ready."
	return 0
}

check_dns_filter() {
	local NVCHECK USE_SOME YESNO_STATUS
	NVCHECK="0"
	if [ "$1" -eq 0 ]; then
		if [ "$(nvram get dnsfilter_enable_x)" != "0" ]; then
			{ nvram set dnsfilter_enable_x="0"; }
			NVCHECK="$((NVCHECK + 1))"
		fi
		PTXT "${INFO} DNS will not be forced through to AdGuardHome."
	fi
	if [ "$1" -eq 1 ]; then
		PTXT "${INFO} You can choose to keep any custom dnsfilter values by only redirect non-custom traffic or send all traffic through to AdGuardHome."
		read_yesno "Do you want to redirect only NON-CUSTOM DNS resolutions on your network through to AdGuardHome?"
		YESNO_STATUS="$?"
		case "${YESNO_STATUS}" in
			0) USE_SOME="0" ;;
			1) USE_SOME="1" ;;
			*) return "${YESNO_STATUS}" ;;
		esac
		if [ "$(nvram get dnsfilter_enable_x)" != "1" ]; then
			{ nvram set dnsfilter_enable_x="1"; }
			NVCHECK="$((NVCHECK + 1))"
		fi
		if [ "$USE_SOME" -eq 0 ]; then
			if [ "$(nvram get dnsfilter_mode)" != "11" ]; then
				{ nvram set dnsfilter_mode="11"; }
				NVCHECK="$((NVCHECK + 1))"
			fi
			PTXT "${INFO} DNSFilter is set to control DNS through to AdGuardHome, while leaving any Custom Rules and Values."
		fi
		if [ "$USE_SOME" -eq 1 ]; then
			for i in dnsfilter_mode dhcpd_dns_router dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 dnsfilter_rulelist dnsfilter_rulelist1 dnsfilter_rulelist2 dnsfilter_rulelist3 dnsfilter_rulelist4 dnsfilter_rulelist5 dhcp_dns1_x dhcp_dns2_x; do
				VAR="$(nvram get "${i}" 2>/dev/null)"
				if [ "${i}" = "dnsfilter_mode" ]; then
					if [ "$VAR" != "11" ]; then {
						nvram set "${i}"="11"
					}; else continue; fi
				elif [ "${i}" = "dhcpd_dns_router" ]; then
					if [ "$VAR" != "1" ]; then {
						nvram set "${i}"="1"
					}; else continue; fi
				elif [ "$VAR" ]; then {
					nvram set "${i}"=""
				}; else continue; fi
				NVCHECK="$((NVCHECK + 1))"
			done
			PTXT "${INFO} DNS is set to redirect All DNS resolutions through to AdGuardHome."
		fi
	fi
	if [ "${NVCHECK}" != "0" ]; then
		{ nvram commit; }
		{ service "restart_firewall;restart_dnsmasq" >/dev/null 2>&1; }
		if ! check_connection 300; then
			PTXT "${WARNING} Timed out waiting for DNS connectivity."
		fi
	fi
}

save_dns_filter_settings() {
	local DNS_FILTER_BACKUP_DIR="$1" DNS_FILTER_KEY
	mkdir -p "${DNS_FILTER_BACKUP_DIR}" || return 1
	for DNS_FILTER_KEY in dnsfilter_enable_x dnsfilter_mode dhcpd_dns_router dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 dnsfilter_rulelist dnsfilter_rulelist1 dnsfilter_rulelist2 dnsfilter_rulelist3 dnsfilter_rulelist4 dnsfilter_rulelist5 dhcp_dns1_x dhcp_dns2_x; do
		nvram get "${DNS_FILTER_KEY}" >"${DNS_FILTER_BACKUP_DIR}/${DNS_FILTER_KEY}" 2>/dev/null || return 1
	done
}

restore_dns_filter_settings() {
	local DNS_FILTER_BACKUP_DIR="$1" DNS_FILTER_KEY DNS_FILTER_VALUE
	[ -d "${DNS_FILTER_BACKUP_DIR}" ] || return 0
	for DNS_FILTER_KEY in dnsfilter_enable_x dnsfilter_mode dhcpd_dns_router dnsfilter_custom1 dnsfilter_custom2 dnsfilter_custom3 dnsfilter_rulelist dnsfilter_rulelist1 dnsfilter_rulelist2 dnsfilter_rulelist3 dnsfilter_rulelist4 dnsfilter_rulelist5 dhcp_dns1_x dhcp_dns2_x; do
		[ -f "${DNS_FILTER_BACKUP_DIR}/${DNS_FILTER_KEY}" ] || return 1
		DNS_FILTER_VALUE="$(cat "${DNS_FILTER_BACKUP_DIR}/${DNS_FILTER_KEY}")" || return 1
		nvram set "${DNS_FILTER_KEY}=${DNS_FILTER_VALUE}" || return 1
	done
	nvram commit || return 1
	service "restart_firewall;restart_dnsmasq" >/dev/null 2>&1 || return 1
	rm -rf "${DNS_FILTER_BACKUP_DIR}"
}

check_ipset() {
	local IPSET_ENABLED
	case "$1" in
		0)
			IPSET_ENABLED="NO"
			;;
		1)
			IPSET_ENABLED="YES"
			;;
	esac
	write_conf ADGUARD_IPSET "\"${IPSET_ENABLED}\""
}

check_dns_local() {
	local LOCAL_CACHE
	case "$1" in
		0)
			LOCAL_CACHE="NO"
			write_conf ADGUARD_LOCAL "\"${LOCAL_CACHE}\""
			;;
		1)
			LOCAL_CACHE="YES"
			write_conf ADGUARD_LOCAL "\"${LOCAL_CACHE}\""
			;;
	esac
}

check_jffs_enabled() {
	if [ "$(nvram get jffs2_format)" = "1" ]; then
		PTXT "${ERROR} JFFS partition is scheduled to be reformatted." \
			"${ERROR} Please reboot to format or disable that setting and try again." \
			"${ERROR} Exiting..."
		exit 1
	fi
	local JFFS2_SCRIPTS JFFS2_ENABLED jffs2_on
	JFFS2_SCRIPTS="$(nvram get jffs2_scripts)"
	[ -z "$(nvram get jffs2_enable)" ] && JFFS2_ENABLED="$(nvram get jffs2_on)" || JFFS2_ENABLED="$(nvram get jffs2_enable)"
	[ -z "$(nvram get jffs2_enable)" ] && jffs2_on="jffs2_on" || jffs2_on="jffs2_enable"
	if [ "${JFFS2_ENABLED}" != "1" ] || [ "${JFFS2_SCRIPTS}" != "1" ]; then
		PTXT "${INFO} JFFS custom scripts and configs are not enabled." \
			"${INFO} Enabling them now!"
		nvram set ${jffs2_on}="1"
		nvram set jffs2_scripts="1"
		nvram commit
	else
		PTXT "${INFO} JFFS custom scripts and configs are already enabled."
	fi
}

check_version() {
	local RMNSTALL LINSTALL MD5SUM_L MD5SUM_R MD5SUM_EXPECTED REMOTE_INSTALLER REMOTE_INSTALLER_MD5 ADGUARD_BRANCH ADGUARD_VER REMOTE_ADGUARD_VER varcnt
	if [ -f "${TARG_DIR}/installer" ] && [ -f "${AGH_FILE}" ] && [ -z "$2" ]; then
		if [ -n "$1" ] || { check_connection; }; then
			REMOTE_INSTALLER="${TARG_DIR}/.installer.remote.$$"
			REMOTE_INSTALLER_MD5="${REMOTE_INSTALLER}.md5sum"
			varcnt="0"
			until [ -n "${LINSTALL}" ] && [ -n "${RMNSTALL}" ] && [ -n "${MD5SUM_L}" ] && [ -n "${MD5SUM_R}" ]; do
				if [ -z "${LINSTALL}" ]; then LINSTALL="$(awk -F= '/^AI_VERSION=/ { gsub(/"/, "", $2); sub(/^v/, "", $2); if ($2 ~ /^[0-9]{1,2}(\.[0-9]{1,2}){2}$/) print $2; exit }' "${TARG_DIR}/installer")"; fi
				if [ -z "${MD5SUM_L}" ]; then MD5SUM_L="$(file_md5 "${TARG_DIR}/installer" 2>/dev/null)"; fi
				if [ -z "${RMNSTALL}" ] || [ -z "${MD5SUM_R}" ]; then
					rm -f "${REMOTE_INSTALLER}" "${REMOTE_INSTALLER_MD5}"
					if http_get_file "${RURL}/installer" "${REMOTE_INSTALLER}" && [ -s "${REMOTE_INSTALLER}" ]; then
						RMNSTALL="$(awk -F= '/^AI_VERSION=/ { gsub(/"/, "", $2); sub(/^v/, "", $2); if ($2 ~ /^[0-9]{1,2}(\.[0-9]{1,2}){2}$/) print $2; exit }' "${REMOTE_INSTALLER}")"
						MD5SUM_R="$(file_md5 "${REMOTE_INSTALLER}" 2>/dev/null)"
						MD5SUM_EXPECTED=""
						if http_get_file "${RURL}/installer.md5sum" "${REMOTE_INSTALLER_MD5}" && [ -s "${REMOTE_INSTALLER_MD5}" ]; then
							MD5SUM_EXPECTED="$(awk 'NR == 1 {print $1; exit}' "${REMOTE_INSTALLER_MD5}")"
						fi
						if md5_is_valid "${MD5SUM_EXPECTED}" && [ "${MD5SUM_R}" != "${MD5SUM_EXPECTED}" ]; then
							PTXT "${WARNING} Remote installer checksum mismatch; retrying version check."
							RMNSTALL=""
							MD5SUM_R=""
						fi
					fi
				fi
				[ -n "${LINSTALL}" ] && [ -n "${RMNSTALL}" ] && [ -n "${MD5SUM_L}" ] && [ -n "${MD5SUM_R}" ] && break
				varcnt="$((varcnt + 1))"
				if [ "${varcnt}" -ge 2 ]; then
					rm -f "${REMOTE_INSTALLER}" "${REMOTE_INSTALLER_MD5}"
					PTXT "One or more critical variables could not be set in a timely manner." "Please check your internet connection, or try again later." "The installer script will now exit."
					sleep 3s
					exit 1
				fi
			done
			rm -f "${REMOTE_INSTALLER}" "${REMOTE_INSTALLER_MD5}"
			ADGUARD_BRANCH="$(conf_value ADGUARD_BRANCH)"
			ADGUARD_VER="$("${AGH_FILE}" --version | cut -d" " -f4-)"
			[ -z "${LINSTALL}" ] && exit 1
			[ -z "${RMNSTALL}" ] && exit 1
			[ -z "${ADGUARD_VER}" ] && exit 1
			REMOTE_ADGUARD_VER="$(adguard_remote_version "${ADGUARD_BRANCH}")"
			[ -z "${REMOTE_ADGUARD_VER}" ] && exit 1
			if [ "${RMNSTALL}" != "${LINSTALL}" ]; then
				PTXT "${INFO} New AI_VERSION=v${RMNSTALL} Available!" \
					"${INFO} Run Option 1 of the Installer to upgrade Asuswrt-Merlin-AdGuardHome-Installer."
				AUTO_UPDATE="update"
			elif [ "${MD5SUM_R}" = "${MD5SUM_L}" ]; then
				PTXT "${INFO} AI_VERSION=v${LINSTALL}"
			else
				PTXT "${INFO} AI_VERSION=v${LINSTALL}, but a New Minor Update is Available!" \
					"${INFO} Run Option 1 of the Installer to upgrade Asuswrt-Merlin-AdGuardHome-Installer."
				AUTO_UPDATE="update"
			fi
			if [ "${REMOTE_ADGUARD_VER}" != "${ADGUARD_VER}" ] || [ "${ADGUARD_BRANCH}" = "edge" ]; then
				case "${ADGUARD_BRANCH}" in
					release)
						PTXT "${INFO} New RELEASE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!"
						;;
					beta)
						PTXT "${INFO} New BETA ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!"
						;;
					edge)
						if [ "${REMOTE_ADGUARD_VER}" = "${ADGUARD_VER}" ]; then
							PTXT "${INFO} ADGUARDHOME_BUILD=Edge" \
								"${INFO} Edge builds can refresh without a version change."
						else
							PTXT "${INFO} New EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!"
						fi
						;;
				esac
				AUTO_UPDATE="update"
			else
				case "${ADGUARD_BRANCH}" in
					release)
						PTXT "${INFO} ADGUARDHOME_BUILD=Release"
						;;
					beta)
						PTXT "${INFO} ADGUARDHOME_BUILD=Beta"
						;;
					edge)
						PTXT "${INFO} ADGUARDHOME_BUILD=Edge"
						;;
				esac
			fi
			PTXT "${INFO} ADGUARDHOME_VER=${ADGUARD_VER}" \
				"${INFO} Run Option 1 to if you want to change builds or upgrade when updates are available." \
				"${INFO} To visit AdGuardHome," \
				"${INFO} please go to http://$(nvram get lan_ipaddr):$(awk -F: '/.*address\:.*/{ print $3 }' "${YAML_FILE}")." \
				"${INFO} You can use the WebUI to change things to your liking!" \
				"${INFO} Stop by https://github.com/AdguardTeam/AdGuardHome/wiki " \
				"${INFO} for any configuration needs. "
		elif [ -z "${LINSTALL}" ] && [ -z "${RMNSTALL}" ] && [ -z "$1" ]; then
			if check_connection 300; then
				check_version x
			else
				PTXT "${ERROR} Timed out waiting for an Internet connection."
			fi
		else
			check_version x x
		fi
	fi
}

choose_branch() {
	if [ "$1" = "1" ]; then
		ADGUARD_BRANCH_OLD="$(conf_value ADGUARD_BRANCH)"
		export ADGUARD_BRANCH_OLD
		del_conf ADGUARD_BRANCH
	fi
	local BUILD
	if [ "${CLI_MODE:-0}" = "1" ] && [ -n "$(conf_value ADGUARD_BRANCH)" ]; then
		return 0
	fi
	if [ ! -f "${CONF_FILE}" ] || [ -z "$(conf_value ADGUARD_BRANCH)" ]; then
		PTXT "${INFO} Choose which build of AdGuardHome to install:" \
			"  1) Release" \
			"  2) Beta" \
			"  3) Edge"
		if ! read_input_num "Select your mode" 1 3; then
			PTXT "${ERROR} Unable to select an AdGuardHome build."
			if [ -n "${ADGUARD_BRANCH_OLD:-}" ]; then
				write_conf ADGUARD_BRANCH "\"${ADGUARD_BRANCH_OLD}\""
			fi
			return 1
		fi
		case "${CHOSEN}" in
			1)
				BUILD=release
				write_conf ADGUARD_BRANCH "\"${BUILD}\""
				;;
			2)
				BUILD=beta
				write_conf ADGUARD_BRANCH "\"${BUILD}\""
				;;
			3)
				BUILD=edge
				write_conf ADGUARD_BRANCH "\"${BUILD}\""
				;;
		esac
		if [ -n "${ADGUARD_BRANCH_OLD:-}" ] && [ "${ADGUARD_BRANCH_OLD}" != "${BUILD}" ]; then
			ADGUARD_BRANCH_CHANGED="1"
			export ADGUARD_BRANCH_CHANGED
		fi
		return 0
	else
		read_yesno "Do you want to switch AdGuardHome builds?"
		CONFIRM_STATUS=$?
		if [ "${CONFIRM_STATUS}" -eq 0 ]; then
			choose_branch 1
		elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
			PTXT "${INFO} continuing without changing builds."
			return 3
		else
			return "${CONFIRM_STATUS}"
		fi
	fi
}

save_dns_nvram_environment() {
	local VAR VALUE
	for VAR in dnspriv_enable dhcpd_dns_router dhcp_dns1_x dhcp_dns2_x; do
		VALUE="$(nvram get "${VAR}" 2>/dev/null)"
		case "${VAR}" in
			dnspriv_enable) _OLD_dnspriv_enable="${VALUE}" ;;
			dhcpd_dns_router) _OLD_dhcpd_dns_router="${VALUE}" ;;
			dhcp_dns1_x) _OLD_dhcp_dns1_x="${VALUE}" ;;
			dhcp_dns2_x) _OLD_dhcp_dns2_x="${VALUE}" ;;
		esac
	done
	export _OLD_dnspriv_enable _OLD_dhcpd_dns_router _OLD_dhcp_dns1_x _OLD_dhcp_dns2_x
	_DNS_NVRAM_SAVED="1"
	export _DNS_NVRAM_SAVED
}

# Installer file and setup helpers

adguard_archive_is_safe() {
	local ARCHIVE REQUIRE_STATE

	ARCHIVE="$1"
	REQUIRE_STATE="${2:-0}"

	case "${REQUIRE_STATE}" in
		1) REQUIRE_STATE="1" ;;
		*) REQUIRE_STATE="0" ;;
	esac
	[ -n "${ARCHIVE}" ] || return 1
	# First pass:
	# Validate tar can list verbosely, reject unsafe file types,
	# and reject unsafe symlink targets.
	if ! tar -tzvf "${ARCHIVE}" 2>/dev/null | awk -v require_state="${REQUIRE_STATE}" '
		function clean_path(path) {
			while (path ~ /^\.\//) {
				sub(/^\.\//, "", path)
			}
			sub(/\/$/, "", path)
			return path
		}
		function path_from_verbose(text,    p) {
			p = index(text, " ./AdGuardHome")
			if (p > 0) {
				return substr(text, p + 1)
			}
			p = index(text, " AdGuardHome")
			if (p > 0) {
				return substr(text, p + 1)
			}
			return ""
		}
		function symlink_target_is_safe(link_path, link_target,    base_path, depth, i, n, part, parts) {
			base_path = link_path
			sub(/\/[^\/]*$/, "", base_path)
			n = split(base_path "/" link_target, parts, "/")
			depth = 0
			for (i = 1; i <= n; i++) {
				part = parts[i]
				if (part == "" || part == ".") {
					continue
				}
				if (part == "..") {
					depth--
					if (depth < 1) {
						return 0
					}
					continue
				}
				depth++
				if (depth == 1 && part != "AdGuardHome") {
					return 0
				}
			}
			return depth >= 1
		}
		BEGIN {
			entries = 0
			data_dir = 0
		}
		{
			type = substr($0, 1, 1)
			if (type != "-" && type != "d" && type != "l") {
				exit 1
			}
			entries = 1
			if (type == "l") {
				line = $0
				arrow_count = 0
				while ((arrow_pos = index(line, " -> ")) > 0) {
					arrow_count++
					line = substr(line, arrow_pos + 4)
				}
				if (arrow_count != 1) {
					exit 1
				}
				arrow_pos = index($0, " -> ")
				link_path = substr($0, 1, arrow_pos - 1)
				link_target = substr($0, arrow_pos + 4)
				link_path = clean_path(path_from_verbose(link_path))
				if (link_path == "") {
					exit 1
				}
				if (link_path == "AdGuardHome/AdGuardHome") {
					exit 1
				}
				if (require_state && link_path == "AdGuardHome/AdGuardHome.yaml") {
					exit 1
				}
				if (require_state && link_path == "AdGuardHome/data") {
					exit 1
				}
				if (link_target == "" || link_target ~ /^\//) {
					exit 1
				}
				if (!symlink_target_is_safe(link_path, link_target)) {
					exit 1
				}
				next
			}
			entry_path = clean_path(path_from_verbose($0))
			if (entry_path == "") {
				exit 1
			}
			if (entry_path == "AdGuardHome/AdGuardHome" && type != "-") {
				exit 1
			}
			if (require_state && entry_path == "AdGuardHome/AdGuardHome.yaml" && type != "-") {
				exit 1
			}
			if (require_state && entry_path == "AdGuardHome/data" && type != "d") {
				exit 1
			}
			if (type == "d" && entry_path == "AdGuardHome/data") {
				data_dir = 1
			}
		}
		END {
			if (!entries) {
				exit 1
			}
			if (require_state && !data_dir) {
				exit 1
			}
		}
	'; then
		return 1
	fi
	# Second pass:
	# Validate archive paths and required AdGuardHome files.
	tar -tzf "${ARCHIVE}" 2>/dev/null | awk -v require_state="${REQUIRE_STATE}" '
		function clean_path(path) {
			while (path ~ /^\.\//) {
				sub(/^\.\//, "", path)
			}
			sub(/\/$/, "", path)
			return path
		}
		BEGIN {
			binary = 0
			yaml = 0
			data = 0
			unsafe = 0
		}
		{
			path = clean_path($0)
			if (path == "") {
				unsafe = 1
				exit
			}
			if (path ~ /^\//) {
				unsafe = 1
				exit
			}
			if (path ~ /(^|\/)\.\.(\/|$)/) {
				unsafe = 1
				exit
			}
			if (path !~ /^AdGuardHome(\/|$)/) {
				unsafe = 1
				exit
			}
			if (path == "AdGuardHome/AdGuardHome") {
				binary = 1
			}
			if (path == "AdGuardHome/AdGuardHome.yaml") {
				yaml = 1
			}
			if (path == "AdGuardHome/data") {
				data = 1
			}
		}
		END {
			if (unsafe) {
				exit 1
			}
			if (!binary) {
				exit 1
			}
			if (require_state && (!yaml || !data)) {
				exit 1
			}
			exit 0
		}
	'
}

adguard_restart_after_failed_replace() {
	[ "$1" = "1" ] || return 0
	if agh_start; then
		PTXT "${INFO} Restored the previously running AdGuardHome service."
		rollback_result_write "service-restart" "rollback complete" "previous AdGuardHome service restarted"
		return 0
	fi
	rollback_result_write "AdGuardHome service restart" "restart-failed" "previous binary restored"
	rollback_result_notice
	PTXT "${ERROR} The previous AdGuardHome binary was restored but could not be restarted."
	rollback_result_write "service-restart" "rollback partial" "previous AdGuardHome binary restored but service restart failed"
	return 1
}

adguard_restart_after_install_abort() {
	adguard_install_abort_trap_disable_preserve_defer
	[ "$1" = "1" ] || return 0
	if agh_is_running; then
		return 0
	fi
	if agh_start; then
		PTXT "${INFO} Restarted AdGuardHome after the installer aborted."
		rollback_result_write "install-abort" "rollback complete" "AdGuardHome restarted after abort"
		return 0
	fi
	rollback_result_write "installer abort" "restart-failed" "AdGuardHome could not be restarted"
	rollback_result_notice
	PTXT "${ERROR} AdGuardHome could not be restarted after the installer aborted."
	rollback_result_write "install-abort" "rollback partial" "AdGuardHome could not be restarted after abort"
	return 1
}

adguard_install_abort_trap_disable() {
	trap - HUP INT QUIT ABRT TERM
	ADGUARD_INSTALL_REPLACE_ACTIVE="0"
	ADGUARD_INSTALL_OLD_BINARY=""
	ADGUARD_INSTALL_STAGE_DIR=""
	ADGUARD_INSTALL_WAS_RUNNING="0"
	ADGUARD_RESTORE_ACTIVE="0"
	ADGUARD_RESTORE_ROLLBACK_DIR=""
	ADGUARD_RESTORE_STAGE_DIR=""
	ADGUARD_RESTORE_TARG_DIR=""
	ADGUARD_RESTORE_TARGET_INSTALLED="0"
}

adguard_install_abort_trap_disable_preserve_defer() {
	local DEFER_END_OP RESTORE_ACTIVE RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARGET_INSTALLED RESTORE_TARG_DIR WAS_RUNNING
	DEFER_END_OP="${ADGUARD_DEFER_END_OP:-0}"
	WAS_RUNNING="${ADGUARD_INSTALL_WAS_RUNNING:-0}"
	RESTORE_ACTIVE="${ADGUARD_RESTORE_ACTIVE:-0}"
	RESTORE_ROLLBACK_DIR="${ADGUARD_RESTORE_ROLLBACK_DIR:-}"
	RESTORE_STAGE_DIR="${ADGUARD_RESTORE_STAGE_DIR:-}"
	RESTORE_TARG_DIR="${ADGUARD_RESTORE_TARG_DIR:-}"
	RESTORE_TARGET_INSTALLED="${ADGUARD_RESTORE_TARGET_INSTALLED:-0}"
	adguard_install_abort_trap_disable
	ADGUARD_DEFER_END_OP="${DEFER_END_OP}"
	if [ "${DEFER_END_OP}" = "1" ] && [ "${RESTORE_ACTIVE}" = "1" ]; then
		ADGUARD_INSTALL_WAS_RUNNING="${WAS_RUNNING}"
		ADGUARD_RESTORE_ACTIVE="${RESTORE_ACTIVE}"
		ADGUARD_RESTORE_ROLLBACK_DIR="${RESTORE_ROLLBACK_DIR}"
		ADGUARD_RESTORE_STAGE_DIR="${RESTORE_STAGE_DIR}"
		ADGUARD_RESTORE_TARG_DIR="${RESTORE_TARG_DIR}"
		ADGUARD_RESTORE_TARGET_INSTALLED="${RESTORE_TARGET_INSTALLED}"
		trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM
	fi
}

adguard_install_abort_on_signal() {
	local OLD_BINARY REPLACE_ACTIVE RESTORE_ACTIVE RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARGET_INSTALLED RESTORE_TARG_DIR STAGE_DIR WAS_RUNNING
	OLD_BINARY="${ADGUARD_INSTALL_OLD_BINARY:-}"
	REPLACE_ACTIVE="${ADGUARD_INSTALL_REPLACE_ACTIVE:-0}"
	STAGE_DIR="${ADGUARD_INSTALL_STAGE_DIR:-}"
	WAS_RUNNING="${ADGUARD_INSTALL_WAS_RUNNING:-0}"
	RESTORE_ACTIVE="${ADGUARD_RESTORE_ACTIVE:-0}"
	RESTORE_ROLLBACK_DIR="${ADGUARD_RESTORE_ROLLBACK_DIR:-}"
	RESTORE_STAGE_DIR="${ADGUARD_RESTORE_STAGE_DIR:-}"
	RESTORE_TARG_DIR="${ADGUARD_RESTORE_TARG_DIR:-}"
	RESTORE_TARGET_INSTALLED="${ADGUARD_RESTORE_TARGET_INSTALLED:-0}"
	adguard_install_abort_trap_disable
	[ -z "${STAGE_DIR}" ] || rm -rf "${STAGE_DIR}"
	if [ "${RESTORE_ACTIVE}" = "1" ]; then
		adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${RESTORE_TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "${RESTORE_TARGET_INSTALLED}"
	elif [ "${REPLACE_ACTIVE}" = "1" ] && [ -n "${OLD_BINARY}" ]; then
		adguard_restore_after_failed_replace "${OLD_BINARY}" "${WAS_RUNNING}"
	else
		adguard_restart_after_install_abort "${WAS_RUNNING}"
	fi
	clear_screen
	ADGUARD_DEFER_END_OP="0"
	end_op_message 2
}

adguard_install_abort_trap_enable() {
	ADGUARD_INSTALL_REPLACE_ACTIVE="0"
	ADGUARD_INSTALL_OLD_BINARY=""
	ADGUARD_INSTALL_WAS_RUNNING="$1"
	ADGUARD_INSTALL_STAGE_DIR="${2:-}"
	ADGUARD_RESTORE_ACTIVE="0"
	trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM
}

adguard_restore_abort_trap_enable() {
	ADGUARD_INSTALL_REPLACE_ACTIVE="0"
	ADGUARD_INSTALL_OLD_BINARY=""
	ADGUARD_INSTALL_STAGE_DIR=""
	ADGUARD_INSTALL_WAS_RUNNING="${4:-0}"
	ADGUARD_RESTORE_ACTIVE="1"
	ADGUARD_RESTORE_ROLLBACK_DIR="$1"
	ADGUARD_RESTORE_TARG_DIR="$2"
	ADGUARD_RESTORE_STAGE_DIR="${3:-}"
	ADGUARD_RESTORE_TARGET_INSTALLED="0"
	trap 'adguard_install_abort_on_signal' HUP INT QUIT ABRT TERM
}

adguard_restore_after_failed_replace() {
	local OLD_BINARY WAS_RUNNING
	OLD_BINARY="$1"
	WAS_RUNNING="$2"
	if [ -f "${OLD_BINARY}" ]; then
		if ! mv "${OLD_BINARY}" "${AGH_FILE}"; then
			rollback_result_write "AdGuardHome binary rollback" "restore-failed" "${OLD_BINARY}"
			rollback_result_notice
			PTXT "${ERROR} Unable to restore the previous AdGuardHome binary; it remains at ${OLD_BINARY}."
			rollback_result_write "binary-replace" "rollback failed" "previous binary remains at ${OLD_BINARY}"
			return 1
		fi
		rollback_result_write "AdGuardHome binary rollback" "restored" "${AGH_FILE}"
	else
		if ! rm -f "${AGH_FILE}"; then
			rollback_result_write "AdGuardHome binary rollback" "cleanup-failed" "${AGH_FILE}"
			rollback_result_notice
			PTXT "${ERROR} Unable to remove the failed AdGuardHome binary."
			rollback_result_write "binary-replace" "rollback failed" "failed binary could not be removed"
			return 1
		fi
		if [ "${WAS_RUNNING}" = "1" ]; then
			rollback_result_write "AdGuardHome binary rollback" "restore-failed" "previous binary unavailable"
			rollback_result_notice
			PTXT "${ERROR} The previous AdGuardHome binary is unavailable for rollback."
			rollback_result_write "binary-replace" "rollback unavailable" "previous binary backup is missing"
			return 1
		fi
		rollback_result_write "AdGuardHome binary rollback" "removed-failed-install" "${AGH_FILE}"
	fi
	rollback_result_write "binary-replace" "rollback complete" "previous AdGuardHome binary restored"
	adguard_restart_after_failed_replace "${WAS_RUNNING}"
}

adguard_restore_after_failed_directory_restore() {
	local COMMAND_LINK ROLLBACK_DIR STAGE_DIR TARGET_DIR TARGET_INSTALLED WAS_RUNNING
	ROLLBACK_DIR="$1"
	TARGET_DIR="$2"
	STAGE_DIR="$3"
	WAS_RUNNING="${4:-0}"
	TARGET_INSTALLED="${5:-1}"
	COMMAND_LINK="${ADGUARD_COMMAND_LINK_PATH:-/opt/sbin/AdGuardHome}"
	[ -z "${STAGE_DIR}" ] || rm -rf "${STAGE_DIR}"
	if agh_is_running; then
		PTXT "${INFO} Stopping AdGuardHome before restoring the previous installation."
		if ! agh_stop; then
			rollback_result_write "AdGuardHome directory rollback" "restore-failed" "could not stop AdGuardHome"
			rollback_result_notice
			PTXT "${ERROR} Unable to safely stop AdGuardHome before restoring the previous installation."
			rollback_result_write "directory-restore" "rollback failed" "could not stop AdGuardHome before restore"
			return 1
		fi
	fi
	if [ -d "${ROLLBACK_DIR}" ] || [ "${TARGET_INSTALLED}" = "1" ]; then
		rm -rf "${TARGET_DIR}"
		if [ ! -d "${ROLLBACK_DIR}" ]; then
			ADGUARD_RESTORE_TARGET_INSTALLED="0"
		fi
	fi
	if [ -d "${ROLLBACK_DIR}" ]; then
		if ! mv "${ROLLBACK_DIR}" "${TARGET_DIR}"; then
			rollback_result_write "AdGuardHome directory rollback" "restore-failed" "${ROLLBACK_DIR}"
			rollback_result_notice
			PTXT "${ERROR} Unable to restore the previous AdGuardHome installation; it remains at ${ROLLBACK_DIR}."
			rollback_result_write "directory-restore" "rollback failed" "previous installation remains at ${ROLLBACK_DIR}"
			return 1
		fi
		rollback_result_write "AdGuardHome directory rollback" "restored" "${TARGET_DIR}"
		ADGUARD_RESTORE_ROLLBACK_DIR=""
		ADGUARD_RESTORE_TARGET_INSTALLED="0"
		ADGUARD_RESTORE_ACTIVE="0"
	elif [ -L "${COMMAND_LINK}" ] && [ "$(readlink "${COMMAND_LINK}")" = "${TARGET_DIR}/AdGuardHome" ]; then
		if ! rm -f "${COMMAND_LINK}"; then
			rollback_result_write "AdGuardHome directory rollback" "cleanup-failed" "${COMMAND_LINK}"
			rollback_result_notice
			PTXT "${ERROR} Unable to remove the failed AdGuardHome command symlink."
			rollback_result_write "directory-restore" "rollback failed" "could not remove failed symlink ${COMMAND_LINK}"
			return 1
		fi
		rollback_result_write "AdGuardHome directory rollback" "removed-failed-install" "${COMMAND_LINK}"
		ADGUARD_RESTORE_TARGET_INSTALLED="0"
		ADGUARD_RESTORE_ACTIVE="0"
	fi
	if [ ! -d "${ROLLBACK_DIR}" ] && [ "${ADGUARD_RESTORE_TARGET_INSTALLED:-0}" = "0" ]; then
		ADGUARD_RESTORE_ACTIVE="0"
	fi
	rm -rf "${STAGE_DIR}"
	adguard_restart_after_failed_replace "${WAS_RUNNING}"
	return 0
}

adguardhome_yaml_ipset_file() {
	[ -f "${YAML_FILE}" ] || return 0
	awk '
		function indentation(line,    text) { text = line; sub(/[^[:space:]].*$/, "", text); return length(text) }
		function block_marker(value) {
			gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
			return value ~ /^[>|][+-]?([[:space:]]+#.*)?$/
		}
		function block_scalar(parent_indent,    line, text) {
			while ((getline line) > 0) {
				if (line ~ /^[[:space:]]*($|#)/) continue
				if (indentation(line) <= parent_indent) exit
				text = line
				gsub(/^[[:space:]]+|[[:space:]]+$/, "", text)
				print text
				exit
			}
		}
		function scalar(value,    ch, decoded, i, next_ch, quote, rest) {
			gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
			quote = substr(value, 1, 1)
			if (quote != "\"" && quote != "\047") {
				sub(/[[:space:]]+#.*$/, "", value)
				gsub(/[[:space:]]+$/, "", value)
				if (value ~ /^(~|null|Null|NULL)$/) return ""
				return value
			}
			decoded = ""
			for (i = 2; i <= length(value); i++) {
				ch = substr(value, i, 1)
				next_ch = substr(value, i + 1, 1)
				if (quote == "\"" && ch == "\\") {
					if (next_ch == "\"" || next_ch == "\\" || next_ch == "/" || next_ch == " ") {
						decoded = decoded next_ch
						i++
						continue
					}
					exit 1
				}
				if (quote == "\047" && ch == quote && next_ch == quote) {
					decoded = decoded quote
					i++
					continue
				}
				if (ch == quote) {
					rest = substr(value, i + 1)
					if (rest !~ /^[[:space:]]*(#.*)?$/) exit 1
					return decoded
				}
				decoded = decoded ch
			}
			exit 1
		}
		/^(dns|\047dns\047|"dns"):[[:space:]]*(&[^][{},[:space:]]+[[:space:]]*)?(#.*)?$/ { in_dns = 1; next }
		/^(dns|\047dns\047|"dns"):/ { exit 1 }
		in_dns && /^[^[:space:]]/ { exit }
		in_dns && /^[[:space:]]*($|#)/ { next }
		in_dns && !child_indent { child_indent = indentation($0) }
		in_dns && indentation($0) == child_indent && substr($0, child_indent + 1) ~ /^(ipset_file|\047ipset_file\047|"ipset_file"):[[:space:]]*/ {
			value = substr($0, child_indent + 1)
			sub(/^(ipset_file|\047ipset_file\047|"ipset_file"):[[:space:]]*/, "", value)
			if (block_marker(value)) {
				block_scalar(child_indent)
				exit
			}
			print scalar(value)
			exit
		}
	' "${YAML_FILE}"
}

ipset_status_path() {
	case "$1" in
		"") return 1 ;;
		/*) printf '%s\n' "$1" ;;
		*) printf '%s\n' "${TARG_DIR}/$1" ;;
	esac
}

ipset_status_sets() {
	local file
	file="$1"
	[ -f "${file}" ] || return 0
	awk '
		/^[[:space:]]*(#|$)/ { next }
		{
			line = $0
			sub(/[[:space:]]+#.*$/, "", line)
			gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
			if (line == "") next
			slash = 0
			for (i = length(line); i > 0; i--) {
				if (substr(line, i, 1) == "/") {
					slash = i
					break
				}
			}
			if (!slash) next
			sets = substr(line, slash + 1)
			n = split(sets, set, ",")
			for (i = 1; i <= n; i++) {
				gsub(/^[[:space:]]+|[[:space:]]+$/, "", set[i])
				if (set[i] == "") continue
				print set[i]
			}
		}
	' "${file}"
}

ipset_status_set_family() {
	ipset list "$1" 2>/dev/null | awk '
		/^Header:/ {
			for (i = 1; i <= NF; i++) {
				if ($i == "family" && (i + 1) <= NF) {
					print $(i + 1)
					exit
				}
			}
		}
	'
}

ipset_status_report_set() {
	local family set
	set="$1"
	family="$(ipset_status_set_family "${set}")"
	if [ -z "${family}" ]; then
		PTXT "${WARNING} IPSET set missing: ${set}"
		return 0
	fi
	case "${family}" in
		inet)
			PTXT "${INFO} IPSET set exists: ${set} (family ${family})"
			PTXT "${INFO} IPv4 set-family check: ${set} OK (family ${family})"
			PTXT "${WARNING} IPv6 set-family mismatch likely: ${set} uses family ${family}"
			;;
		inet6)
			PTXT "${INFO} IPSET set exists: ${set} (family ${family})"
			PTXT "${WARNING} IPv4 set-family mismatch likely: ${set} uses family ${family}"
			PTXT "${INFO} IPv6 set-family check: ${set} OK (family ${family})"
			;;
		*)
			PTXT "${WARNING} IPSET set exists with unknown family: ${set} (family ${family})"
			PTXT "${WARNING} IPv4 set-family check unknown: ${set} uses family ${family}"
			PTXT "${WARNING} IPv6 set-family check unknown: ${set} uses family ${family}"
			;;
	esac
}

ipset_status() {
	local current_file enabled found generated_file ipset_file seen_sets set_entry yaml_file
	enabled="$(conf_value ADGUARD_IPSET)"
	case "${enabled}" in
		NO) PTXT "${INFO} IPSET integration enabled: NO" ;;
		YES) PTXT "${INFO} IPSET integration enabled: YES" ;;
		*) PTXT "${INFO} IPSET integration enabled: YES (default)" ;;
	esac
	yaml_file="$(adguardhome_yaml_ipset_file 2>/dev/null)"
	if [ -n "${yaml_file}" ]; then
		current_file="$(ipset_status_path "${yaml_file}")"
		if [ "${current_file}" = "${TARG_DIR}/ipset.conf" ]; then
			PTXT "${INFO} AdGuardHome.yaml managed dns.ipset_file: YES (${current_file})"
		else
			PTXT "${WARNING} AdGuardHome.yaml dns.ipset_file is not installer-managed: ${current_file}"
		fi
	else
		current_file=""
		PTXT "${WARNING} AdGuardHome.yaml managed dns.ipset_file: NO"
	fi
	for ipset_file in "${TARG_DIR}/ipset.user" "${TARG_DIR}/ipset.conf"; do
		[ -f "${ipset_file}" ] && PTXT "${INFO} IPSET file exists: ${ipset_file}" || PTXT "${WARNING} IPSET file missing: ${ipset_file}"
	done
	if ! ai_have_cmd ipset; then
		PTXT "${WARNING} router-stock ipset command unavailable; set existence and family checks skipped."
		return 0
	fi
	found="0"
	seen_sets="|"
	for ipset_file in "${TARG_DIR}/ipset.user" "${TARG_DIR}/ipset.conf"; do
		[ -f "${ipset_file}" ] || continue
		generated_file="$(ipset_status_sets "${ipset_file}")"
		[ -n "${generated_file}" ] || continue
		found="1"
		PTXT "${INFO} Referenced sets from ${ipset_file}:"
		while read -r set_entry; do
			[ -n "${set_entry}" ] || continue
			case "${seen_sets}" in
				*"|${set_entry}|"*) continue ;;
			esac
			seen_sets="${seen_sets}${set_entry}|"
			ipset_status_report_set "${set_entry}"
		done <<EOF_IPSET_STATUS_SETS
${generated_file}
EOF_IPSET_STATUS_SETS
	done
	[ "${found}" = "1" ] || PTXT "${WARNING} No IPSET set references found in installer-managed IPSET files."
}

chmod_adguardhome_data_files_600() {
	local DATA_DIR
	DATA_DIR="$1"
	[ -d "${DATA_DIR}" ] || return 0
	[ ! -L "${DATA_DIR}" ] || return 0
	find "${DATA_DIR}" -exec sh -c '
		[ ! -L "$1" ] || exit 0
		[ -f "$1" ] || exit 0
		chmod 600 "$1"
	' sh {} \;
}

ensure_adguardhome_directory_permissions() {
	local HTTP_USERNAME IPSET_PERMISSION_FILE PERMISSION_PATH
	if [ ! -d "${TARG_DIR}" ]; then
		PTXT "${ERROR} ${TARG_DIR} does not exist."
		return 1
	fi
	HTTP_USERNAME="$(nvram get http_username 2>/dev/null)"
	[ -n "${HTTP_USERNAME}" ] || HTTP_USERNAME="root"
	if ! chown "${HTTP_USERNAME}:root" "${TARG_DIR}"; then
		PTXT "${ERROR} Unable to set ownership on ${TARG_DIR}."
		return 1
	fi
	for PERMISSION_PATH in "${TARG_DIR}"/*; do
		[ ! -L "${PERMISSION_PATH}" ] || continue
		[ -f "${PERMISSION_PATH}" ] || [ -d "${PERMISSION_PATH}" ] || continue
		if ! chown "${HTTP_USERNAME}:root" "${PERMISSION_PATH}"; then
			PTXT "${ERROR} Unable to set ownership on ${PERMISSION_PATH}."
			return 1
		fi
	done
	if ! chmod 700 "${TARG_DIR}" || ! find "${TARG_DIR}" -exec sh -c '[ ! -L "$1" ] || exit 0; [ -d "$1" ] || exit 0; chmod 700 "$1"' sh {} \;; then
		PTXT "${ERROR} Unable to all set directory permissions for ${TARG_DIR}."
		return 1
	fi
	if [ -f "${YAML_FILE}" ] && ! chmod 600 "${YAML_FILE}"; then
		PTXT "${ERROR} Unable to set configuration permissions on ${YAML_FILE}."
		return 1
	fi
	if ! chmod_adguardhome_data_files_600 "${TARG_DIR}/data"; then
		PTXT "${ERROR} Unable to set data file permissions for ${TARG_DIR}/data."
		return 1
	fi
	if ! chmod_adguardhome_data_files_600 "${TARG_DIR}/data/filters"; then
		PTXT "${ERROR} Unable to set filter file permissions for ${TARG_DIR}/data/filters."
		return 1
	fi
	for IPSET_PERMISSION_FILE in "${TARG_DIR}/ipset.conf" "${TARG_DIR}/ipset.user" "$(adguardhome_yaml_ipset_file)"; do
		[ -n "${IPSET_PERMISSION_FILE}" ] || continue
		case "${IPSET_PERMISSION_FILE}" in
			../* | */../* | */.. | ..) continue ;;
		esac
		case "${IPSET_PERMISSION_FILE}" in
			/*)
				case "${IPSET_PERMISSION_FILE}" in
					"${TARG_DIR}"/*) ;;
					*) continue ;;
				esac
				;;
			*) IPSET_PERMISSION_FILE="${TARG_DIR}/${IPSET_PERMISSION_FILE}" ;;
		esac
		[ ! -L "${IPSET_PERMISSION_FILE}" ] || continue
		[ -f "${IPSET_PERMISSION_FILE}" ] || continue
		if ! chown "${HTTP_USERNAME}:root" "${IPSET_PERMISSION_FILE}"; then
			PTXT "${ERROR} Unable to set ownership on ${IPSET_PERMISSION_FILE}."
			return 1
		fi
		if ! chmod 644 "${IPSET_PERMISSION_FILE}"; then
			PTXT "${ERROR} Unable to set IPSET permissions on ${IPSET_PERMISSION_FILE}."
			return 1
		fi
	done
	if [ -f "${AGH_FILE}" ] && ! chmod 755 "${AGH_FILE}"; then
		PTXT "${ERROR} Unable to set executable permissions on ${AGH_FILE}."
		return 1
	fi
	return 0
}

create_backup_archive() {
	local BACKUP_FILE BACKUP_TMP
	BACKUP_FILE="${BASE_DIR}/backup_AdGuardHome.tar.gz"
	BACKUP_TMP="${BACKUP_FILE}.tmp.$$"
	rm -f "${BACKUP_TMP}"
	ptxt_step "Creating AdGuardHome backup archive."
	if ! tar -czf "${BACKUP_TMP}" -C "${BASE_DIR}" AdGuardHome >/dev/null 2>&1; then
		rm -f "${BACKUP_TMP}"
		PTXT "${ERROR} Unable to create the AdGuardHome backup archive."
		return 1
	fi
	ptxt_ok "Created AdGuardHome backup archive."
	ptxt_step "Verifying AdGuardHome backup archive."
	if ! adguard_archive_is_safe "${BACKUP_TMP}" 1; then
		PTXT "${ERROR} The new AdGuardHome backup archive failed safety validation."
		PTXT "${INFO} Backup archives must contain AdGuardHome/AdGuardHome, AdGuardHome/AdGuardHome.yaml, and AdGuardHome/data."
		PTXT "${INFO} Archive contents:"
		tar -tzvf "${BACKUP_TMP}" 2>/dev/null | sed -n '1,40p'
		rm -f "${BACKUP_TMP}"
		return 1
	fi
	ptxt_ok "Verified AdGuardHome backup archive."
	ptxt_step "Installing AdGuardHome backup archive."
	if ! chmod 600 "${BACKUP_TMP}" || ! mv -f "${BACKUP_TMP}" "${BACKUP_FILE}"; then
		rm -f "${BACKUP_TMP}"
		PTXT "${ERROR} Unable to install the new AdGuardHome backup archive."
		return 1
	fi
	ptxt_ok "Installed AdGuardHome backup archive."
	return 0
}

install_adguard_archive() {
	local ARCHIVE_FILE OLD_BINARY STAGE_BINARY STAGE_DIR WAS_RUNNING
	ARCHIVE_FILE="$1"
	STAGE_DIR="${BASE_DIR}/.AdGuardHome.extract.$$"
	STAGE_BINARY="${STAGE_DIR}/AdGuardHome/AdGuardHome"
	OLD_BINARY="${TARG_DIR}/.AdGuardHome.previous.$$"
	rm -rf "${STAGE_DIR}"
	rm -f "${OLD_BINARY}"
	adguard_install_abort_trap_enable 0 "${STAGE_DIR}"
	ptxt_step "Validating AdGuardHome archive layout."
	if ! adguard_archive_is_safe "${ARCHIVE_FILE}"; then
		adguard_install_abort_trap_disable
		rollback_result_write "package install" "rollback unavailable" "archive validation failed before changes"
		rollback_result_notice
		PTXT "${ERROR} The AdGuardHome archive contains an unsafe or incomplete file layout."
		return 1
	fi
	ptxt_ok "AdGuardHome archive layout is valid."
	ptxt_step "Extracting AdGuardHome archive into staging."
	if ! create_dir "${STAGE_DIR}" || ! tar xzf "${ARCHIVE_FILE}" -C "${STAGE_DIR}" >/dev/null 2>&1; then
		adguard_install_abort_trap_disable
		rm -rf "${STAGE_DIR}"
		rollback_result_write "package install" "rollback unavailable" "archive extraction failed before changes"
		rollback_result_notice
		PTXT "${ERROR} Unable to extract the AdGuardHome archive into staging."
		return 1
	fi
	ptxt_ok "Extracted AdGuardHome archive into staging."
	ptxt_step "Validating staged AdGuardHome binary."
	if [ ! -f "${STAGE_BINARY}" ] || [ -L "${STAGE_BINARY}" ] || ! chmod 755 "${STAGE_BINARY}" ||
		[ -z "$("${STAGE_BINARY}" --version 2>/dev/null | cut -d" " -f4-)" ]; then
		adguard_install_abort_trap_disable
		rm -rf "${STAGE_DIR}"
		rollback_result_write "package install" "rollback unavailable" "staged binary validation failed before changes"
		rollback_result_notice
		PTXT "${ERROR} The staged AdGuardHome binary failed validation."
		return 1
	fi
	ptxt_ok "Staged AdGuardHome binary is valid."
	if [ "$(agh_process_count)" -gt 0 ]; then WAS_RUNNING="1"; else WAS_RUNNING="0"; fi
	ADGUARD_INSTALL_WAS_RUNNING="${WAS_RUNNING}"
	if ! agh_prepare_binary_replace; then
		adguard_install_abort_trap_disable
		rm -rf "${STAGE_DIR}"
		return 1
	fi
	ptxt_step "Preserving installed AdGuardHome binary for rollback."
	if [ -f "${AGH_FILE}" ] && ! cp -p "${AGH_FILE}" "${OLD_BINARY}"; then
		adguard_install_abort_trap_disable
		rm -rf "${STAGE_DIR}"
		PTXT "${ERROR} Unable to preserve the installed AdGuardHome binary."
		adguard_restart_after_failed_replace "${WAS_RUNNING}"
		return 1
	fi
	ptxt_ok "Installed AdGuardHome binary preserved for rollback."
	ADGUARD_INSTALL_OLD_BINARY="${OLD_BINARY}"
	ADGUARD_INSTALL_REPLACE_ACTIVE="1"
	ptxt_step "Installing staged AdGuardHome binary."
	if ! mv "${STAGE_BINARY}" "${AGH_FILE}"; then
		adguard_install_abort_trap_disable
		rm -rf "${STAGE_DIR}"
		PTXT "${ERROR} Unable to install the staged AdGuardHome binary."
		adguard_restore_after_failed_replace "${OLD_BINARY}" "${WAS_RUNNING}"
		return 1
	fi
	ptxt_ok "Staged AdGuardHome binary installed."
	ptxt_step "Validating installed AdGuardHome binary."
	if ! ensure_adguardhome_directory_permissions ||
		[ -z "$("${AGH_FILE}" --version 2>/dev/null | cut -d" " -f4-)" ]; then
		adguard_install_abort_trap_disable
		rm -rf "${STAGE_DIR}"
		PTXT "${ERROR} Installed AdGuardHome binary validation failed."
		adguard_restore_after_failed_replace "${OLD_BINARY}" "${WAS_RUNNING}"
		return 1
	fi
	ptxt_ok "Installed AdGuardHome binary is valid."
	ADGUARD_INSTALL_REPLACE_ACTIVE="0"
	ADGUARD_INSTALL_OLD_BINARY=""
	rm -f "${OLD_BINARY}"
	rm -rf "${STAGE_DIR}"
	return 0
}

backup_restore() {
	local COMMAND_LINK COMMAND_LINK_DIR RESTORE_ROLLBACK_DIR RESTORE_STAGE_DIR RESTORE_TARG_DIR WAS_RUNNING
	COMMAND_LINK="${ADGUARD_COMMAND_LINK_PATH:-/opt/sbin/AdGuardHome}"
	COMMAND_LINK_DIR="${COMMAND_LINK%/*}"
	if [ "$1" = "BACKUP" ] && [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ]; then
		ptxt_phase "Backing up AdGuardHome."
		if [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then
			PTXT "${INFO} There is an old backup detected."
			local USE_OLD
			read_yesno "Do you want to continue?(this will remove the old backup)"
			CONFIRM_STATUS=$?
			if [ "${CONFIRM_STATUS}" -eq 0 ]; then
				USE_OLD="NO"
			elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
				USE_OLD="YES"
			else
				return "${CONFIRM_STATUS}"
			fi
			if [ "${USE_OLD}" = "YES" ]; then
				PTXT "${INFO} Leaving Old Backup."
				end_op_message 1
			elif [ "${USE_OLD}" = "NO" ]; then
				PTXT "${INFO} The old backup will be replaced only after the new backup is verified."
			fi
		fi
		PTXT "${INFO} This operation will backup AdGuardHome(<4MB)to ENTWARE /opt/etc." \
			"${INFO} Please wait a moment."
		if ! create_backup_archive; then
			[ -z "$2" ] && end_op_message 1
			return 1
		fi
		ptxt_ok "Backup complete at ${BASE_DIR}/backup_AdGuardHome.tar.gz."
		[ -z "$2" ] && end_op_message 0 || return 0
	elif [ "$1" = "BACKUP" ]; then
		PTXT "${ERROR} No ${AGH_FILE} to Backup!"
		end_op_message 1
		return 1
	fi
	if [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && [ "$1" = "RESTORE" ]; then
		ptxt_phase "Restoring AdGuardHome backup."
		ptxt_step "Restoring AdGuardHome from ${BASE_DIR}/backup_AdGuardHome.tar.gz; please wait."
		if ! adguard_archive_is_safe "${BASE_DIR}/backup_AdGuardHome.tar.gz" 1; then
			rollback_result_write "backup restore" "rollback unavailable" "unsafe backup archive refused before changes"
			rollback_result_notice
			ptxt_fail "Refusing to restore unsafe or incomplete backup ${BASE_DIR}/backup_AdGuardHome.tar.gz."
			return 1
		fi
		ptxt_ok "Backup archive safety check passed."
		RESTORE_STAGE_DIR="${BASE_DIR}/.AdGuardHome.restore.$$"
		RESTORE_TARG_DIR="${RESTORE_STAGE_DIR}/AdGuardHome"
		RESTORE_ROLLBACK_DIR="${BASE_DIR}/.AdGuardHome.rollback.$$"
		if [ "$(agh_process_count)" -gt 0 ]; then WAS_RUNNING="1"; else WAS_RUNNING="0"; fi
		rm -rf "${RESTORE_STAGE_DIR}" "${RESTORE_ROLLBACK_DIR}"
		adguard_restore_abort_trap_enable "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}"
		ptxt_step "Staging AdGuardHome backup archive."
		if ! create_dir "${RESTORE_STAGE_DIR}" || ! tar -xzvf "${BASE_DIR}/backup_AdGuardHome.tar.gz" -C "${RESTORE_STAGE_DIR}" >/dev/null 2>&1; then
			adguard_install_abort_trap_disable
			rm -rf "${RESTORE_STAGE_DIR}"
			rollback_result_write "backup restore" "rollback unavailable" "backup staging failed before changes"
			rollback_result_notice
			PTXT "${ERROR} Unable to stage ${BASE_DIR}/backup_AdGuardHome.tar.gz for restore."
			return 1
		fi
		ptxt_ok "Staged AdGuardHome backup archive."
		ptxt_step "Validating staged AdGuardHome backup."
		if [ ! -d "${RESTORE_TARG_DIR}" ] || [ -L "${RESTORE_TARG_DIR}" ] ||
			[ ! -f "${RESTORE_TARG_DIR}/AdGuardHome" ] ||
			[ ! -d "${RESTORE_TARG_DIR}/data" ] || [ -L "${RESTORE_TARG_DIR}/data" ]; then
			adguard_install_abort_trap_disable
			rm -rf "${RESTORE_STAGE_DIR}"
			rollback_result_write "backup restore" "rollback unavailable" "staged backup validation failed before changes"
			rollback_result_notice
			PTXT "${ERROR} The staged AdGuardHome backup is incomplete."
			return 1
		fi
		ptxt_ok "Validated staged AdGuardHome backup."
		ptxt_step "Preserving current AdGuardHome installation for rollback."
		if [ -e "${TARG_DIR}" ] && ! mv "${TARG_DIR}" "${RESTORE_ROLLBACK_DIR}"; then
			adguard_install_abort_trap_disable
			rm -rf "${RESTORE_STAGE_DIR}"
			rollback_result_write "backup restore" "rollback unavailable" "could not preserve current installation"
			rollback_result_notice
			PTXT "${ERROR} Unable to preserve the current AdGuardHome installation before restore."
			return 1
		fi
		ptxt_ok "Current AdGuardHome installation preserved for rollback."
		ptxt_step "Installing staged AdGuardHome backup."
		if ! mv "${RESTORE_TARG_DIR}" "${TARG_DIR}"; then
			adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1"
			adguard_install_abort_trap_disable
			PTXT "${ERROR} Unable to install the staged AdGuardHome backup."
			return 1
		fi
		ptxt_ok "Installed staged AdGuardHome backup."
		ADGUARD_RESTORE_TARGET_INSTALLED="1"
		rm -rf "${RESTORE_STAGE_DIR}"
		ptxt_step "Restoring AdGuardHome ownership and permissions."
		if ! ensure_adguardhome_directory_permissions; then
			adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1"
			adguard_install_abort_trap_disable
			PTXT "${ERROR} Unable to restore AdGuardHome ownership or permissions."
			return 1
		fi
		ptxt_ok "Restored AdGuardHome ownership and permissions."
		ptxt_step "Restoring AdGuardHome command symlink."
		if ! create_dir "${COMMAND_LINK_DIR}" || ! ln -sf "${AGH_FILE}" "${COMMAND_LINK}"; then
			adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1"
			adguard_install_abort_trap_disable
			PTXT "${ERROR} Unable to restore the AdGuardHome command symlink."
			return 1
		fi
		ptxt_ok "Restored AdGuardHome command symlink."
		ADGUARD_DEFER_END_OP="1"
		ptxt_step "Reinstalling AdGuardHome service scripts after restore."
		if ! inst_AdGuardHome "${1:-RESTORE}"; then
			ADGUARD_DEFER_END_OP="0"
			adguard_restore_after_failed_directory_restore "${RESTORE_ROLLBACK_DIR}" "${TARG_DIR}" "${RESTORE_STAGE_DIR}" "${WAS_RUNNING}" "1"
			adguard_install_abort_trap_disable
			rollback_result_notice
			ptxt_fail "Unable to complete AdGuardHome restore setup; restoring previous installation."
			return 1
		fi
		ptxt_ok "Reinstalled AdGuardHome service scripts after restore."
		ADGUARD_DEFER_END_OP="0"
		adguard_install_abort_trap_disable
		rm -rf "${RESTORE_ROLLBACK_DIR}"
		end_op_message 0 "${1:-RESTORE}"
	elif [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && [ "$1" = "RESTORE" ]; then
		rollback_result_write "backup restore" "rollback unavailable" "backup archive missing"
		rollback_result_notice
		PTXT "${ERROR} No Backup found!" \
			"${ERROR} Please make sure Backup Resides in ${BASE_DIR}"
		end_op_message 1
		return
	fi
}

cleanup() {
	mv "/opt/etc/init.d/S61AdGuardHome" "/opt/etc/init.d/S99AdGuardHome" >/dev/null 2>&1
	if ai_have_cmd cru && [ "$(cru l | grep -c "S99AdGuardHome")" -gt 0 ]; then cru -d "S99AdGuardHome"; fi
	if [ -f "${TARG_DIR}/localtime" ]; then mv "${TARG_DIR}/localtime" "${ADDON_DIR}/localtime"; fi
	remove_conflicting_apache
}

cleanup_legacy_firewall() {
	if [ -x "${ADDON_DIR}/AdGuardHome.sh" ] && grep -q 'Unload_IPTables' "${ADDON_DIR}/AdGuardHome.sh"; then
		"${ADDON_DIR}/AdGuardHome.sh" firewall unload 2>/dev/null
	fi
	while iptables -t raw -D OUTPUT -j ADGUARDHOME 2>/dev/null; do :; done
	iptables -t raw -F ADGUARDHOME 2>/dev/null
	iptables -t raw -X ADGUARDHOME 2>/dev/null
	del_jffs_script /jffs/scripts/firewall-start
}

create_dir() {
	if ! mkdir -p "${1}"; then
		PTXT "${ERROR} Unable to create ${1}!"
		return 1
	fi
}

del_between_magic() {
	local TARG MAGIC BOUNDS
	TARG="$1"
	MAGIC="$2"
	[ -f "${TARG}" ] || return
	BOUNDS="$(awk -v PATT="${MAGIC}" '($0 ~ PATT) {printf NR","}' "${TARG}")"
	if [ "${BOUNDS}" ]; then
		sed -i "${BOUNDS%,}d" "${TARG}"
	fi
}

del_conf() {
	[ ! -f "${CONF_FILE}" ] && return
	local KEY
	for KEY in "$@"; do
		sed -i "/^${KEY}=.*$/d" "${CONF_FILE}"
	done
}

del_jffs_script() {
	local TARG LINE_NUM LINE_ABOVE OP
	TARG="$1"
	[ -f "${TARG}" ] || return
	if [ "${2:-}" ]; then
		OP="$(printf '%.1s' "$2")"
		if [ "$OP" = "!" ]; then
			LINE_NUM="$(grep -n -F "[ -x ${ADDON_DIR}/" "${TARG}" | grep -v "$(_quote "$2")" | cut -d':' -f1)"
		else
			LINE_NUM="$(grep -n -F "[ -x ${ADDON_DIR}/" "${TARG}" | grep "$(_quote "$2")" | cut -d':' -f1)"
		fi
	else
		LINE_NUM="$(grep -n -F "[ -x ${ADDON_DIR}/" "${TARG}" | cut -d':' -f1)"
	fi
	[ -z "${LINE_NUM}" ] && return
	LINE_NUM="$(printf "%s\n" "${LINE_NUM}" | sort -rn)" || return 1
	for LINE_NUM in ${LINE_NUM}; do
		[ -z "${LINE_NUM}" ] && continue
		sed -i "${LINE_NUM}d" "${TARG}" || return 1
		if [ "${LINE_NUM}" -gt 1 ]; then
			LINE_NUM="$((LINE_NUM - 1))"
			LINE_ABOVE="$(sed "${LINE_NUM}q;d" "${TARG}")" || return 1
			if [ -z "${LINE_ABOVE}" ]; then
				sed -i "${LINE_NUM}d" "${TARG}" || return 1
			fi
		fi
	done
	LINE_ABOVE="$(awk '{ print }' "${TARG}")" || return 1
	if [ "${LINE_ABOVE}" = "#!/bin/sh" ]; then
		rm -f "${TARG}" || return 1
	fi
	return 0
}

download_file() {
	local TARG PERM URL RET FILENAME TMP_FILE TMP_MD5 TMP_SHA256 MD5SUM_OLD MD5SUM_CURR MD5SUM_EXPECTED SHA256SUM_CURR SHA256SUM_EXPECTED COUNT
	TARG="$1"
	shift
	PERM="$1"
	shift
	RET="0"
	ptxt_phase "Downloading and verifying files."
	for URL in "$@"; do
		FILENAME="${URL##*/}"
		TMP_FILE="${TARG}/.${FILENAME}.$$"
		TMP_MD5="${TMP_FILE}.md5sum"
		TMP_SHA256="${TMP_FILE}.sha256sum"
		MD5SUM_OLD=""
		MD5SUM_CURR=""
		MD5SUM_EXPECTED=""
		SHA256SUM_CURR=""
		SHA256SUM_EXPECTED=""
		COUNT="0"
		[ -f "${TARG}/${FILENAME}" ] && MD5SUM_OLD="$(file_md5 "${TARG}/${FILENAME}" 2>/dev/null)"
		while [ "${COUNT}" -lt 3 ]; do
			MD5SUM_CURR=""
			MD5SUM_EXPECTED=""
			SHA256SUM_CURR=""
			SHA256SUM_EXPECTED=""
			ptxt_step "Downloading ${FILENAME} to ${TARG}/${FILENAME}."
			rm -f "${TMP_FILE}" "${TMP_MD5}" "${TMP_SHA256}"
			if http_get_file "${URL}" "${TMP_FILE}" "" insecure && [ -s "${TMP_FILE}" ]; then
				ptxt_ok "Downloaded ${FILENAME}."
				ptxt_step "Checking remote checksum metadata for ${FILENAME}."
				if http_get_file "${URL}.sha256sum" "${TMP_SHA256}" "" insecure && [ -s "${TMP_SHA256}" ]; then
					SHA256SUM_EXPECTED="$(awk 'NR == 1 {print $1; exit}' "${TMP_SHA256}")"
				fi
				if http_get_file "${URL}.md5sum" "${TMP_MD5}" "" insecure && [ -s "${TMP_MD5}" ]; then
					MD5SUM_EXPECTED="$(awk 'NR == 1 {print $1; exit}' "${TMP_MD5}")"
				fi
				if sha256_is_valid "${SHA256SUM_EXPECTED}"; then
					ptxt_step "Verifying SHA-256 checksum for ${FILENAME}."
					if ! SHA256SUM_CURR="$(file_sha256 "${TMP_FILE}" 2>/dev/null)"; then
						SHA256SUM_CURR=""
					fi
					if [ "${SHA256SUM_CURR}" = "${SHA256SUM_EXPECTED}" ]; then
						MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)"
						ptxt_ok "SHA-256 checksum verified for ${FILENAME}."
						break
					fi
					if [ -n "${SHA256SUM_CURR}" ]; then
						ptxt_warn "SHA-256 checksum mismatch for ${FILENAME}; retrying download."
					else
						ptxt_warn "SHA-256 checksum could not be calculated for ${FILENAME}; falling back to MD5 if available."
					fi
				fi
				if ! sha256_is_valid "${SHA256SUM_EXPECTED}" || [ -z "${SHA256SUM_CURR}" ]; then
					if md5_is_valid "${MD5SUM_EXPECTED}"; then
						ptxt_step "Verifying MD5 checksum for ${FILENAME}."
						MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)"
						if [ "${MD5SUM_CURR}" = "${MD5SUM_EXPECTED}" ]; then
							ptxt_warn "Only MD5 checksum metadata was usable for ${FILENAME}; SHA-256 is preferred."
							break
						fi
						ptxt_warn "MD5 checksum mismatch for ${FILENAME}; retrying download."
					else
						ptxt_warn "No valid remote checksum metadata for ${FILENAME}; using download result."
						MD5SUM_CURR="$(file_md5 "${TMP_FILE}" 2>/dev/null)"
						break
					fi
				fi
			else
				ptxt_warn "Download attempt failed for ${FILENAME}; retrying if attempts remain."
			fi
			COUNT="$((COUNT + 1))"
		done
		rm -f "${TMP_MD5}" "${TMP_SHA256}"
		if [ "${COUNT}" -eq 3 ]; then
			rm -f "${TMP_FILE}"
			ptxt_fail "Unable to download ${BOLD}${URL}${NORM}."
			RET="$((RET + 1))"
			continue
		fi
		if md5_is_valid "${MD5SUM_CURR}" && [ "${MD5SUM_CURR}" = "${MD5SUM_OLD}" ]; then
			rm -f "${TMP_FILE}"
			ptxt_ok "${FILENAME} at ${TARG}/${FILENAME} is up to date; continuing."
			continue
		fi
		# Set permissions before replacing the working copy.  If chmod fails,
		# preserve the existing file instead of leaving a partially installed one.
		ptxt_step "Installing ${FILENAME} to ${TARG}/${FILENAME}."
		if chmod "${PERM}" "${TMP_FILE}" && mv "${TMP_FILE}" "${TARG}/${FILENAME}"; then
			ptxt_ok "Installed ${FILENAME} to ${TARG}/${FILENAME}."
			continue
		fi
		rm -f "${TMP_FILE}"
		ptxt_fail "Unable to install ${BOLD}${FILENAME}${NORM} to ${TARG}/${FILENAME}."
		RET="$((RET + 1))"
	done
	if [ "${RET}" -ne 0 ]; then
		ptxt_fail "One or more download failures occurred; rerun the installer or restore from backup."
	fi
	return "${RET}"
}

installer_update_reexec() {
	local ACTION NEW_MD5 OLD_MD5
	ACTION="$1"
	OLD_MD5="$2"
	[ "${ACTION}" = "update" ] || return 0
	[ "${AI_REEXECED_INSTALLER:-0}" = "1" ] && return 0
	[ -n "${OLD_MD5}" ] || return 0
	[ -f "${TARG_DIR}/installer" ] || return 0
	NEW_MD5="$(file_md5 "${TARG_DIR}/installer" 2>/dev/null)"
	[ -n "${NEW_MD5}" ] || return 0
	[ "${OLD_MD5}" != "${NEW_MD5}" ] || return 0
	ptxt_ok "Installer was updated at ${TARG_DIR}/installer; restarting the update with the new installer."
	chmod 755 "${TARG_DIR}/installer" >/dev/null 2>&1
	AI_REEXECED_INSTALLER="1"
	export AI_REEXECED_INSTALLER
	exec "${TARG_DIR}/installer" "${BRANCH}" "${ACTION}"
}

inst_AdGuardHome() {
	local ADGUARD_TAR ARCHIVE_MD5 ARCHIVE_SHA256 INSTALLER_MD5_OLD REMOTE_ADGUARD_ARCHIVE REMOTE_ADGUARD_MD5 REMOTE_ADGUARD_SHA256 REMOTE_ADGUARD_URL REMOTE_ADGUARD_VER RESTART_AFTER_ABORT SERVICE_REFRESH_ONLY
	ADGUARD_TAR="AdGuardHome_${ADGUARD_ARCH}.tar.gz"
	ptxt_phase "Running AdGuardHome ${1:-install} orchestration."
	ensure_sha256sum_tool || return 1
	INSTALLER_MD5_OLD=""
	RESTART_AFTER_ABORT="0"
	SERVICE_REFRESH_ONLY="${SERVICE_REFRESH_ONLY:-0}"
	[ -f "${SCRIPT_LOC}" ] && INSTALLER_MD5_OLD="$(file_md5 "${SCRIPT_LOC}" 2>/dev/null)"
	if [ -z "$2" ]; then
		if [ "${1:-RESTORE}" != "RESTORE" ]; then
			if [ ! -d "${TARG_DIR}" ] && [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then
				PTXT "${INFO} Backup is detected."
				local USE_OLD
				read_yesno "Do you want Restore instead?"
				CONFIRM_STATUS=$?
				if [ "${CONFIRM_STATUS}" -eq 0 ]; then
					USE_OLD="YES"
				elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
					USE_OLD="NO"
				else
					return "${CONFIRM_STATUS}"
				fi
				if [ "${USE_OLD}" = "YES" ]; then
					PTXT "${INFO} Installing from an old backup!"
					backup_restore RESTORE
				elif [ "${USE_OLD}" = "NO" ]; then
					PTXT "${INFO} Continuing without restoring from backup!"
				fi
			elif [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then
				read_yesno "Do you want create a backup before updating?"
				CONFIRM_STATUS=$?
				if [ "${CONFIRM_STATUS}" -eq 0 ]; then
					if ! backup_restore BACKUP 0; then
						PTXT "${ERROR} Unable to create the requested pre-update backup."
						end_op_message 1 "$1"
						return 1
					fi
				elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
					PTXT "${INFO} continuing without making a backup."
				else
					return "${CONFIRM_STATUS}"
				fi
			fi
		fi
		if [ -z "${REMOTE_VER}" ] || [ -z "${REMOTE_BETA}" ] || [ -z "${REMOTE_EDGE}" ] || { ! check_connection; }; then
			PTXT "${ERROR} Unable to detect the Internet!"
			end_op_message 1 "$1"
			return 1
		fi
		ptxt_ok "Internet and remote version metadata are available."
		ptxt_step "Preparing AdGuardHome target directory."
		if ! create_dir "${TARG_DIR}" || ! ensure_adguardhome_directory_permissions; then
			end_op_message 1 "$1"
			return 1
		fi
		ptxt_ok "AdGuardHome target directory is ready."
		ptxt_step "Downloading installer update."
		if ! download_file "${TARG_DIR}" 755 "${RURL}/installer" || ! awk '{ print }' "${TARG_DIR}/installer" | grep -m1 "^AI_VERSION=" | grep -qoE '[0-9]{1,2}([.][0-9]{1,2})([.][0-9]{1,2})'; then
			PTXT "${ERROR} Failed to download installer."
			end_op_message 1 "$1"
			return 1
		fi
		ptxt_ok "Installer update downloaded and validated."
		installer_update_reexec "${1:-install}" "${INSTALLER_MD5_OLD}"
		if [ "${1:-RESTORE}" != "RESTORE" ]; then
			if [ "${1:-install}" != "update" ]; then
				choose_branch
				CHOOSE_BRANCH_STATUS=$?
				if [ "${CHOOSE_BRANCH_STATUS}" -ne 0 ] && [ "${CHOOSE_BRANCH_STATUS}" -ne 3 ]; then
					PTXT "${ERROR} Unable to select an AdGuardHome build. Installation was aborted."
					end_op_message 1 "$1"
					return 1
				fi
			fi
			local ADGUARD_BRANCH ADGUARD_VER
			ADGUARD_BRANCH="$(conf_value ADGUARD_BRANCH)"
			if [ ! -f "${AGH_FILE}" ]; then
				inst_AdGuardHome "${1:-install}" "$ADGUARD_BRANCH"
			else
				ADGUARD_VER="$("${AGH_FILE}" --version | cut -d" " -f4-)"
				[ -z "${ADGUARD_VER}" ] && exit 1
				REMOTE_ADGUARD_VER="$(adguard_remote_version "${ADGUARD_BRANCH}")"
				[ -z "${REMOTE_ADGUARD_VER}" ] && exit 1
				if [ "${REMOTE_ADGUARD_VER}" != "${ADGUARD_VER}" ] || [ "${ADGUARD_BRANCH}" = "edge" ] || [ "${ADGUARD_BRANCH_CHANGED:-0}" = "1" ]; then
					if [ "${ADGUARD_BRANCH_CHANGED:-0}" = "1" ]; then
						case "${ADGUARD_BRANCH}" in
							release)
								PTXT "${INFO} Switching ADGUARDHOME_BUILD=Release" \
									"${INFO} Installing RELEASE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}."
								;;
							beta)
								PTXT "${INFO} Switching ADGUARDHOME_BUILD=Beta" \
									"${INFO} Installing BETA ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}."
								;;
							edge)
								PTXT "${INFO} Switching ADGUARDHOME_BUILD=Edge" \
									"${INFO} Installing EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}."
								;;
						esac
					else
						case "${ADGUARD_BRANCH}" in
							release)
								PTXT "${INFO} New RELEASE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" \
									"${INFO} Updating ADGUARDHOME_VER=${ADGUARD_VER} to ${REMOTE_ADGUARD_VER}."
								;;
							beta)
								PTXT "${INFO} New BETA ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" \
									"${INFO} Updating ADGUARDHOME_VER=${ADGUARD_VER} to ${REMOTE_ADGUARD_VER}."
								;;
							edge)
								if [ "${REMOTE_ADGUARD_VER}" = "${ADGUARD_VER}" ]; then
									PTXT "${INFO} Refreshing EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER}." \
										"${INFO} Edge builds can refresh without a version change."
								else
									PTXT "${INFO} New EDGE ADGUARDHOME_VER=${REMOTE_ADGUARD_VER} Available!" \
										"${INFO} Updating ADGUARDHOME_VER=${ADGUARD_VER} to ${REMOTE_ADGUARD_VER}."
								fi
								;;
						esac
					fi
					inst_AdGuardHome "${1:-update}" "${ADGUARD_BRANCH}"
				else
					if [ "${1:-update}" = "update" ]; then
						PTXT "${INFO} Refreshing installer-managed service integration."
						SERVICE_REFRESH_ONLY=1
						inst_AdGuardHome update "${ADGUARD_BRANCH}"
						return $?
					fi
					case "${ADGUARD_BRANCH}" in
						release)
							PTXT "${INFO} ADGUARDHOME_BUILD=Release" \
								"${INFO} No new release version available." \
								"${INFO} ADGUARDHOME_VER=${ADGUARD_VER}"
							;;
						beta)
							PTXT "${INFO} ADGUARDHOME_BUILD=Beta" \
								"${INFO} No new beta version available." \
								"${INFO} ADGUARDHOME_VER=${ADGUARD_VER}"
							;;
						edge)
							PTXT "${INFO} ADGUARDHOME_BUILD=Edge" \
								"${INFO} No new edge version available." \
								"${INFO} ADGUARDHOME_VER=${ADGUARD_VER}"
							;;
					esac
				fi
			fi
		fi
	else
		if [ "${SERVICE_REFRESH_ONLY}" = "1" ]; then
			ptxt_phase "Refreshing AdGuardHome service integration."
		else
			ptxt_phase "Installing AdGuardHome package."
			REMOTE_ADGUARD_ARCHIVE="$(adguard_remote_archive "$2")"
			REMOTE_ADGUARD_MD5="$(adguard_remote_md5 "$2")"
			REMOTE_ADGUARD_SHA256="$(adguard_remote_sha256 "$2")"
			REMOTE_ADGUARD_URL="$(adguard_remote_url "$2")"
			[ -z "${REMOTE_ADGUARD_ARCHIVE}" ] && REMOTE_ADGUARD_ARCHIVE="${ADGUARD_TAR}"
			[ -z "${REMOTE_ADGUARD_URL}" ] && REMOTE_ADGUARD_URL="${URL_ARCH}/${REMOTE_ADGUARD_ARCHIVE}"
			if ! download_file "${BASE_DIR}" 644 "${REMOTE_ADGUARD_URL}"; then
				PTXT "${ERROR} Unable to download AdGuardHome package for your router"
				end_op_message 1 "$1"
				return 1
			fi
			if sha256_is_valid "${REMOTE_ADGUARD_SHA256}"; then
				ptxt_step "Verifying SHA-256 checksum for ${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}."
				ARCHIVE_SHA256="$(file_sha256 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)" || {
					PTXT "${ERROR} Unable to calculate the downloaded AdGuardHome package SHA-256 checksum."
					rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}"
					end_op_message 1 "$1"
					return 1
				}
				if [ "${ARCHIVE_SHA256}" != "${REMOTE_ADGUARD_SHA256}" ]; then
					PTXT "${ERROR} SHA-256 checksum mismatch for downloaded AdGuardHome package metadata."
					rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}"
					end_op_message 1 "$1"
					return 1
				fi
				ptxt_ok "SHA-256 checksum verified for ${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}."
			elif md5_is_valid "${REMOTE_ADGUARD_MD5}"; then
				ptxt_step "Verifying MD5 checksum for ${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}."
				ARCHIVE_MD5="$(file_md5 "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}" 2>/dev/null)" || {
					PTXT "${ERROR} Unable to calculate the downloaded AdGuardHome package MD5 checksum."
					rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}"
					end_op_message 1 "$1"
					return 1
				}
				if [ "${ARCHIVE_MD5}" != "${REMOTE_ADGUARD_MD5}" ]; then
					PTXT "${ERROR} MD5 checksum mismatch for downloaded AdGuardHome package metadata."
					rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}"
					end_op_message 1 "$1"
					return 1
				fi
				ptxt_warn "Only MD5 package metadata was available; SHA-256 is preferred."
			fi
			if [ "$(agh_process_count)" -gt 0 ]; then RESTART_AFTER_ABORT="1"; fi
			ptxt_step "Replacing AdGuardHome package with downloaded archive."
			if ! install_adguard_archive "${BASE_DIR}/${REMOTE_ADGUARD_ARCHIVE}"; then
				rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}"
				rollback_result_notice
				end_op_message 1 "$1"
				return 1
			fi
			ptxt_ok "AdGuardHome package was replaced."
			rm -f "${BASE_DIR:?}/${REMOTE_ADGUARD_ARCHIVE}"
			[ -f "/opt/sbin/AdGuardHome" ] && rm -rf /opt/sbin/AdGuardHome
			ptxt_step "Creating AdGuardHome command symlink."
			if ! ln -sf "${AGH_FILE}" /opt/sbin/AdGuardHome || [ -z "$("${AGH_FILE}" --version | cut -d" " -f4-)" ]; then
				PTXT "${ERROR} Failed to download AdGuardHome package for your router"
				adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
				end_op_message 1 "$1"
				return 1
			fi
			ptxt_ok "AdGuardHome command symlink is ready."
		fi
	fi
	ptxt_phase "Installing service integration."
	ptxt_step "Installing AdGuardHome service scripts and addon hooks."
	if ! create_dir "${ADDON_DIR}"; then
		adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
		end_op_message 1 "$1"
		return 1
	fi
	ptxt_ok "AdGuardHome addon directory is ready."
	if [ "${1:-update}" = "update" ]; then
		configure_runtime_defaults upgrade || {
			adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
			end_op_message 1 "$1"
			return 1
		}
	fi
	cleanup_legacy_firewall
	if ! download_file "${ADDON_DIR}" 755 "${RURL}/AdGuardHome.sh" ||
		! download_file "/opt/etc/init.d" 755 "${RURL}/S99AdGuardHome" ||
		! download_file "/opt/etc/init.d" 644 "${RURL}/rc.func.AdGuardHome"; then
		PTXT "${ERROR} Required AdGuardHome service files could not be installed."
		adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
		end_op_message 1 "$1"
		return 1
	fi
	ptxt_ok "AdGuardHome service files installed."
	[ -f "/jffs/scripts/dnsmasq.postconf" ] && yaml_nvars_delete "#Asuswrt-Merlin AdGuardHome Installer" /jffs/scripts/dnsmasq.postconf
	for i in init-start services-stop; do { if { ! grep -q "${ADDON_DIR}/AdGuardHome.sh $i &" "/jffs/scripts/${i}" && grep -q "${ADDON_DIR}/AdGuardHome.sh $i" "/jffs/scripts/${i}"; }; then del_jffs_script "/jffs/scripts/${i}"; fi; }; done
	if ! write_manager_script /jffs/scripts/init-start "init-start &" ||
		! write_manager_script /jffs/scripts/services-stop "services-stop &" ||
		! write_manager_script /jffs/scripts/dnsmasq.postconf dnsmasq ||
		! write_manager_script /jffs/scripts/firewall-start 'firewall "$1"'; then
		PTXT "${ERROR} Required Asuswrt-Merlin event scripts could not be configured."
		adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
		end_op_message 1 "$1"
		return 1
	fi
	ptxt_ok "Asuswrt-Merlin event scripts configured."
	if nvram get rc_support | grep -q 'mtlancfg'; then del_jffs_script /jffs/scripts/dnsmasq-sdn.postconf; fi
	if nvram get rc_support | grep -q 'mtlancfg' && ! write_manager_script /jffs/scripts/dnsmasq-sdn.postconf 'dnsmasq-sdn $2'; then
		PTXT "${ERROR} Required SDN dnsmasq event script could not be configured."
		adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
		end_op_message 1 "$1"
		return 1
	fi
	del_between_magic /jffs/scripts/service-event-end '# Asuswrt-Merlin-AdGuardHome-Installer'
	if ! write_command_script /jffs/scripts/service-event-end 'if printf "%s" "$@" | /bin/grep -qE "^(((start|stop|restart|kill|reload)_?.*AdGuardHome)$)"; then { sh /jffs/addons/AdGuardHome.d/AdGuardHome.sh "$(printf "%s" "$@" | /bin/grep -oE "(start|stop|restart|kill|reload)")" x & }; fi # Asuswrt-Merlin-AdGuardHome-Installer'; then
		PTXT "${ERROR} Required service-event-end hook could not be configured."
		adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
		end_op_message 1 "$1"
		return 1
	fi
	if [ "${1:-update}" != "update" ]; then
		ptxt_phase "Configuring AdGuardHome first-run settings."
		if [ ! -f "${ADDON_DIR}/localtime" ]; then
			PTXT "${INFO} Before setting up AdGuardHome, please choose your right timezone!"
			if ! set_timezone; then
				adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
				end_op_message 1 "$1"
				return 1
			fi
		fi
		if ! setup_AdGuardHome "" "${1:-install}"; then
			adguard_restart_after_install_abort "${RESTART_AFTER_ABORT}"
			end_op_message 1
			return 1
		fi
		ptxt_ok "AdGuardHome first-run configuration completed."
	fi
	ptxt_phase "Final readiness summary."
	ptxt_step "Waiting for AdGuardHome post-install readiness checks."
	if ! agh_complete_startup; then
		adguard_install_abort_trap_disable_preserve_defer
		end_op_message 1 "$1"
		return 1
	fi
	adguard_install_abort_trap_disable_preserve_defer
	ptxt_ok "AdGuardHome ${1:-install} orchestration complete."
	ptxt_ok "Final readiness summary: service is installed, started, and ready."
	end_op_message 0 "$1"
}

set_timezone() {
	local TMP TZ_ARCH TZ_DATA INDEX TZ_FILE TZ_LIST
	TMP="/root"
	TZ_ARCH="$(uname -m)"
	case "${TZ_ARCH}" in
		"aarch64" | "arm64")
			TZ_ARCH="aarch64"
			;;
		"armv7l")
			TZ_ARCH="arm"
			;;
	esac
	TZ_DATA="tzdata-2021e-1-${TZ_ARCH}.pkg.tar.bz2"
	ensure_opkg_package column || return 1
	ai_have_cmd sort || {
		PTXT "${ERROR} Required timezone selection command is unavailable: sort"
		return 1
	}
	[ -x /opt/bin/column ] || {
		PTXT "${ERROR} Required Entware timezone selection command is unavailable: /opt/bin/column"
		return 1
	}
	download_file "${TMP}" 644 "${RURL}/${TZ_DATA}" || return 1
	TZ_LIST="$(tar tjf "${TMP}/${TZ_DATA}" 2>/dev/null)" || {
		PTXT "${ERROR} Unable to read timezone archive ${TZ_DATA}."
		return 1
	}
	INDEX="$(PTXT "${TZ_LIST}" | awk -F'/' '!/\/$/ && /\/posix\// {count++} END {print count + 0}')"
	[ "${INDEX}" -gt 0 ] || {
		PTXT "${ERROR} No timezone files were found in ${TZ_DATA}."
		return 1
	}
	PTXT "${INFO} Available timezones/locations:"
	PTXT "${TZ_LIST}" | awk -F'/' '!/\/$/ && /\/posix\//' | sort | cut -d'/' -f2- | awk -v INDEX=0 -F'/' '!/\/$/ {++INDEX;printf "  " INDEX") ";for (i=5; i<NF; i++)  printf $i "/"; print $NF}' | /opt/bin/column
	if ! read_input_num "Select your timezone/location" 1 "${INDEX}"; then
		PTXT "${ERROR} Unable to select a timezone."
		return 1
	fi
	TZ_FILE="$(PTXT "${TZ_LIST}" | awk -F'/' '!/\/$/ && /\/posix\//' | sort | awk -v INDEX="$CHOSEN" '{++i}i==INDEX{print $0}')"
	[ -n "${TZ_FILE}" ] || return 1
	PTXT "${INFO} ${TZ_FILE##*/} selected"
	tar xjf "${TMP}/${TZ_DATA}" -C "${TMP}" ./usr/share/zoneinfo/posix || return 1
	if ! mv "${TMP}/${TZ_FILE}" "${ADDON_DIR}/localtime"; then
		PTXT "${ERROR} Unable to set your timezone file"
		return 1
	fi
	rm -rf "${TMP:?}/${TZ_DATA}" "${TMP:?}/usr"
}

setup_AdGuardHome() {
	if [ ! -d "${TARG_DIR}" ] || [ ! -f "${AGH_FILE}" ]; then
		PTXT "${ERROR} AdGuardHome is not installed. Aborting...."
		return 1
	fi
	ptxt_phase "Configuring AdGuardHome using ${YAML_FILE}."
	setup_AdGuardHome_impl "$@"
	local RET="$?"
	if [ "$1" = "reconfig" ]; then
		if [ "$RET" -eq 0 ]; then
			ptxt_step "Restarting AdGuardHome service with ${YAML_FILE}."
			if ! agh_restart; then
				agh_start_error
				end_op_message 1
				return
			fi
			ptxt_ok "AdGuardHome setup is complete."
			end_op_message 0
		else
			end_op_message 1
		fi
	fi
	return "${RET}"
}

save_installer_config() {
	local CONFIG_BACKUP="$1"
	rm -f "${CONFIG_BACKUP}" "${CONFIG_BACKUP}.absent"
	if [ -f "${CONF_FILE}" ]; then
		cp -p "${CONF_FILE}" "${CONFIG_BACKUP}" || return 1
	else
		: >"${CONFIG_BACKUP}.absent" || return 1
	fi
}

restore_installer_config() {
	local CONFIG_BACKUP="$1"
	if [ -f "${CONFIG_BACKUP}" ]; then
		mv -f "${CONFIG_BACKUP}" "${CONF_FILE}" || return 1
	elif [ -f "${CONFIG_BACKUP}.absent" ]; then
		rm -f "${CONF_FILE}" || return 1
	fi
	rm -f "${CONFIG_BACKUP}.absent"
}

discard_installer_config_backup() {
	rm -f "$1" "$1.absent"
}

setup_restore_previous_yaml() {
	if [ "${YAML_BACKED_UP:-0}" -eq 1 ]; then
		if mv -f "${YAML_BAK}" "${YAML_FILE}"; then
			rollback_result_write "setup-yaml" "rollback complete" "previous ${YAML_FILE} restored"
			rollback_result_notice
			return 0
		fi
		rollback_result_write "setup-yaml" "rollback failed" "could not restore previous ${YAML_FILE}"
		rollback_result_notice
		PTXT "${ERROR} Unable to restore the previous AdGuardHome configuration."
		return 1
	fi
	return 0
}

setup_AdGuardHome_impl() {
	local CHOOSE_NEW=0 CONFIG_ROLLBACK="" DNS_FILTER_ROLLBACK_DIR="" DOMAIN_CONFIG_SELECTION="" DOMAIN_LAN_CHANGED=0 IPSET_SELECTION LAN_DOMAIN_ROLLBACK="" WEB_PORT_SELECTION YAML_ORI_NEW="" YAML_BACKED_UP=0 SCHEMA_VER="$("${AGH_FILE}" -v --version | awk '/Schema[[:space:]]version:/{ print $NF }' 2>/dev/null)"
	if [ -z "${SCHEMA_VER}" ]; then SCHEMA_VER="27"; fi
	if [ -z "$1" ] && [ -f "${YAML_FILE}" ]; then
		############################################################ Drop-in code to be removed/modified as needed. #############################################################################################################################################################
		if ! awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" 2>/dev/null | sed -e 's/^"//' -e 's/"$//' | grep -qoE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'; then
			PTXT "${INFO} The Web Port for AdGuardHome WebUI has not been manually configured." \
				"${INFO} Please set the preferred Web Port for AdGuardHome WebUI." \
				"${INFO} The chosen port must reside within the port range 3000-65535" \
				"${INFO} and not already in use by any other service."
			if ! read_input_port "Default is" 3000; then
				return 1
			fi
			if ! write_conf ADGUARD_WEBUI_PORT "\"${WEB_PORT}\""; then
				PTXT "${ERROR} Unable to save the AdGuardHome WebUI port."
				return 1
			fi
			yaml_nvars_replace "  address: 0.0.0.0:$(awk -F':' '/.*address\:.*/{ print $3 }' "${YAML_FILE}" | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" "  address: 0.0.0.0:${WEB_PORT}" "${YAML_FILE}"
		elif awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" 2>/dev/null | sed -e 's/^"//' -e 's/"$//' | grep -qoE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$' && [ "$(awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" | sed -e 's/^"//' -e 's/"$//' | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" != "$(awk -F':' '/.*address\:.*/{ print $3 }' "${YAML_FILE}" | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" ]; then
			yaml_nvars_replace "  address: 0.0.0.0:$(awk -F':' '/.*address\:.*/{ print $3 }' "${YAML_FILE}" | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" "  address: 0.0.0.0:$(awk -F'=' '/ADGUARD_WEBUI_PORT/ {print $2}' "${CONF_FILE}" | sed -e 's/^"//' -e 's/"$//' | grep -oE '^(3000|[3-9][0-9]{3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$')" "${YAML_FILE}"
		fi
		##########################################################################################################################################################################################################################################################################
		if [ -n "$(awk -F': ' '/.*schema\:.*/{ print $2 }' "${YAML_FILE}")" ] && [ "$(awk -F': ' '/.*schema\:.*/{ print $2 }' "${YAML_FILE}")" != "${SCHEMA_VER}" ]; then yaml_nvars_replace "schema_version: $(awk -F': ' '/.*schema\:.*/{ print $2 }' "${YAML_FILE}")" "schema_version: ${SCHEMA_VER}" "${YAML_FILE}"; fi
		if ! check_AdGuardHome_yaml; then
			setup_AdGuardHome_impl x
			return
		fi
		PTXT "${INFO} Found previous AdGuardHome config file"
		read_yesno "Do you want to use this file without reconfiguring?"
		CONFIRM_STATUS=$?
		if [ "${CONFIRM_STATUS}" -eq 0 ]; then
			PTXT "${INFO} Use previous settings file"
		elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
			setup_AdGuardHome_impl x
		else
			return "${CONFIRM_STATUS}"
		fi
	else
		if [ -f "${YAML_FILE}" ]; then
			if [ "$1" = "reconfig" ]; then
				if ! check_AdGuardHome_yaml; then
					setup_AdGuardHome_impl x
					return
				fi
				PTXT "${INFO} Found previous AdGuardHome config file"
			fi
			PTXT "${INFO} How do you want to reconfigure:" \
				"${INFO}   1) Use previous settings file" \
				"${INFO}   2) Restore original configuration selections" \
				"${INFO}   3) Choose new configuration selections"
			if ! read_input_num "Your selection" 1 3; then
				PTXT "${ERROR} Unable to select a reconfiguration mode. Setup was aborted."
				return 1
			fi
			case "${CHOSEN}" in
				1)
					PTXT "${INFO} Using previous settings file"
					;;
				2 | 3)
					PTXT "${INFO} Backing up previous settings file..."
					if ! mv "${YAML_FILE}" "${YAML_BAK}"; then
						PTXT "${ERROR} Unable to back up the previous AdGuardHome configuration. Setup was aborted."
						return 1
					fi
					YAML_BACKED_UP=1
					if [ "${CHOSEN}" = "3" ]; then
						CHOOSE_NEW=1
					else
						cp -f "${YAML_ORI}" "${YAML_FILE}"
					fi
					;;
			esac
		elif [ ! -f "${YAML_FILE}" ] && [ -f "${YAML_ORI}" ]; then
			cp -f "${YAML_ORI}" "${YAML_FILE}"
		fi
		case "${2:-reconfig}" in
			"install" | "reconfig")
				if [ ! -f "${YAML_FILE}" ] || [ "$2" = "reconfig" ]; then
					local DNS_FILTER_SELECTION LOCAL_CACHE_SELECTION YESNO_STATUS
					read_yesno "Do you want to redirect all DNS resolutions on your network through to AdGuardHome?"
					YESNO_STATUS="$?"
					case "${YESNO_STATUS}" in
						0) DNS_FILTER_SELECTION=1 ;;
						1) DNS_FILTER_SELECTION=0 ;;
						*)
							setup_restore_previous_yaml || true
							return "${YESNO_STATUS}"
							;;
					esac
					if [ "$(nvram get dns_local_cache)" != "1" ]; then
						read_yesno "Do you want to run AdGuardHome as a local caching DNS service which includes router traffic?"
						YESNO_STATUS="$?"
						case "${YESNO_STATUS}" in
							0) LOCAL_CACHE_SELECTION=1 ;;
							1) LOCAL_CACHE_SELECTION=0 ;;
							*)
								setup_restore_previous_yaml || true
								return "${YESNO_STATUS}"
								;;
						esac
					fi
					read_yesno "Do you want to enable AdGuardHome IPSET integration?"
					YESNO_STATUS="$?"
					case "${YESNO_STATUS}" in
						0) IPSET_SELECTION=1 ;;
						1) IPSET_SELECTION=0 ;;
						*)
							setup_restore_previous_yaml || true
							return "${YESNO_STATUS}"
							;;
					esac
					if [ "$2" != "reconfig" ] && ! check_ipset "${IPSET_SELECTION}"; then
						PTXT "${WARNING} Unable to save the optional AdGuardHome IPSET integration setting." \
							"${WARNING} Continuing setup with the previous or default IPSET preference."
					fi
				fi
				if { [ ! -f "${YAML_ORI}" ] && [ ! -f "${YAML_FILE}" ]; } || [ "${CHOOSE_NEW}" -eq 1 ]; then
					PTXT "${INFO} Requesting entries for AdGuardHome initial configuration..." \
						"${INFO} Please set the preferred Web Port for AdGuardHome WebUI." \
						"${INFO} The chosen port must reside within the port range 3000-65535" \
						"${INFO} and not already in use by any other service."
					if ! read_input_port "Default is" 3000; then
						setup_restore_previous_yaml || true
						return 1
					fi
					WEB_PORT_SELECTION="${WEB_PORT}"
					YAML_ORI_NEW="${YAML_ORI}.new.$$"
					SETUP_YAML_TMP_FILE="${YAML_ORI_NEW}"
					PTXT "http:" \
						"  address: 0.0.0.0:${WEB_PORT}" >"${YAML_ORI_NEW}"
					PTXT "${INFO} Set the Username and Password which will be encrypted to the yaml file."
					if ! AdGuardHome_authen 1 "${YAML_ORI_NEW}" 0; then
						rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						return 1
					fi
					PTXT "dns:" \
						"  bind_hosts:" \
						"    - 0.0.0.0" \
						"  port: 53" >>"${YAML_ORI_NEW}"
					PTXT "${INFO} Set the DNS server(s) for initializing AdGuardHome" \
						"${INFO} and router services (e.g. ntp) at boot"
					if ! read_input_dns "Default is" 9.9.9.9 || ! read_input_dns "2nd Default is" 8.8.8.8; then
						rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						return 1
					fi
					local DOMAIN="" NET_ADDR NET_ADDR6 LAN_IF
					LAN_IF="$(nvram get lan_ifname)"
					if [ -n "${LAN_IF}" ] && ai_have_cmd ip; then
						NET_ADDR="$(ip -o -4 addr list "${LAN_IF}" 2>/dev/null | awk 'NR==1{ split($4, ip_addr, "/"); print ip_addr[1] }')"
						NET_ADDR6="$(ip -o -6 addr list "${LAN_IF}" scope global 2>/dev/null | awk 'NR==1{ split($4, ip_addr, "/"); print ip_addr[1] }')"
					fi
					[ -n "${NET_ADDR:-}" ] || NET_ADDR="$(nvram get lan_ipaddr)"
					[ -n "${NET_ADDR6:-}" ] || NET_ADDR6="$(nvram get ipv6_rtr_addr)"
					LAN_DOMAIN_ROLLBACK="$(nvram get lan_domain)"
					if [ -n "${LAN_DOMAIN_ROLLBACK}" ]; then
						DOMAIN="${LAN_DOMAIN_ROLLBACK}"
						DOMAIN_CONFIG_SELECTION="no"
					else
						DOMAIN="lan"
						DOMAIN_CONFIG_SELECTION="yes"
					fi
					PTXT "  upstream_dns:" >>"${YAML_ORI_NEW}"
					[ -n "${NET_ADDR6}" ] && PTXT "  - '[/$(printf "%s\n" "${NET_ADDR6}" | sed 's/.$//' | awk -F: '{for(i=1;i<=NF;i++)x=x""sprintf (":%4s", $i);gsub(/ /,"0",x); printf x}' | cut -c 2- | cut -c 1-20 | sed 's/://g;s/^.*$/\n&\n/;tx;:x;s/\(\n.\)\(.*\)\(.\n\)/\3\2\1/;tx;s/\n//g;s/\(.\)/\1./g;s/$/ip6.arpa/')/][::]:553'" >>"${YAML_ORI_NEW}"
					PTXT "  - '[/router.asus.com/][::]:553'" \
						"  - '[/www.asusnetwork.net/][::]:553'" \
						"  - '[/www.asusrouter.com/][::]:553'" \
						"  - '[/use-application-dns.net/][::]:553'" \
						"  - '[/dns.resolver.arpa/][::]:553'" \
						"  - '[/${DOMAIN}/][::]:553'" \
						"  - '[//][::]:553'" \
						"  - ${BOOTSTRAP1}" \
						"  - ${BOOTSTRAP2}" \
						"  - tcp://${BOOTSTRAP1}" \
						"  - tcp://${BOOTSTRAP2}" \
						"  bootstrap_dns:" \
						"  - ${BOOTSTRAP1}" \
						"  - ${BOOTSTRAP2}" \
						"  fallback_dns:" \
						"  - ${BOOTSTRAP1}" \
						"  - ${BOOTSTRAP2}" \
						"  - tcp://${BOOTSTRAP1}" \
						"  - tcp://${BOOTSTRAP2}" \
						"  resolve_clients: true" \
						"  use_private_ptr_resolvers: true" \
						"  local_ptr_upstreams:" \
						"  - '[::]:553'" \
						"  - '[/10.in-addr.arpa/][::]:553'" \
						"  - '[/$(printf "%s\n" "${NET_ADDR}" | awk 'BEGIN{FS="."}{print $2"."$1".in-addr.arpa"}')/][::]:553'" \
						"schema_version: ${SCHEMA_VER}" >>"${YAML_ORI_NEW}"
					if ! check_AdGuardHome_yaml "${YAML_ORI_NEW}"; then
						rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						return 1
					fi
					PTXT "${INFO} Writing AdGuardHome configuration..."
					if ! cp -f "${YAML_ORI_NEW}" "${YAML_FILE}"; then
						rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						PTXT "${ERROR} Writing AdGuardHome configuration failed "
						return 1
					fi
				fi
				if ! check_AdGuardHome_yaml; then
					[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
					setup_restore_previous_yaml || true
					PTXT "${ERROR} Writing AdGuardHome configuration failed " \
						"${ERROR} Please send ${YAML_ERR} file and screen log of " \
						"${ERROR} all ssh terminal operations to the script developer."
					return 1
				fi
				if [ -n "${DNS_FILTER_SELECTION:-}" ]; then
					CONFIG_ROLLBACK="${TMPDIR:-/tmp}/AdGuardHome.config.$$"
					if ! save_installer_config "${CONFIG_ROLLBACK}"; then
						[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						PTXT "${ERROR} Unable to preserve the current installer preferences."
						return 1
					fi
					if [ -n "${DOMAIN_CONFIG_SELECTION}" ] && ! write_conf ADGUARD_DOMAIN "\"${DOMAIN_CONFIG_SELECTION}\""; then
						restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
						[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						PTXT "${ERROR} Unable to save the AdGuardHome domain setting."
						return 1
					fi
					if [ "${DOMAIN_CONFIG_SELECTION}" = "yes" ] && [ -z "${LAN_DOMAIN_ROLLBACK}" ]; then
						nvram set lan_domain="${DOMAIN}" && nvram commit
						DOMAIN_LAN_CHANGED=1
					fi
					DNS_FILTER_ROLLBACK_DIR="${TMPDIR:-/tmp}/AdGuardHome.dnsfilter.$$"
					rm -rf "${DNS_FILTER_ROLLBACK_DIR}"
					if ! save_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then
						rm -rf "${DNS_FILTER_ROLLBACK_DIR}"
						restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
						if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
							nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
							nvram commit
						fi
						[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						PTXT "${ERROR} Unable to preserve the current DNSFilter settings."
						return 1
					fi
					check_dns_filter "${DNS_FILTER_SELECTION}"
					YESNO_STATUS="$?"
					if [ "${YESNO_STATUS}" -ne 0 ]; then
						rm -rf "${DNS_FILTER_ROLLBACK_DIR}"
						restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
						if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
							nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
							nvram commit
						fi
						[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
						setup_restore_previous_yaml || true
						return "${YESNO_STATUS}"
					fi
					if [ "$(nvram get dns_local_cache)" != "1" ]; then
						if ! check_dns_local "${LOCAL_CACHE_SELECTION}"; then
							restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
							if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
								nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
								nvram commit
							fi
							if ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then
								PTXT "${ERROR} Unable to restore the previous DNSFilter settings."
							fi
							[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
							setup_restore_previous_yaml || true
							PTXT "${ERROR} Unable to save the AdGuardHome local cache setting."
							return 1
						fi
					else
						if ! check_dns_local 0; then
							restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
							if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
								nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
								nvram commit
							fi
							if ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then
								PTXT "${ERROR} Unable to restore the previous DNSFilter settings."
							fi
							[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
							setup_restore_previous_yaml || true
							PTXT "${ERROR} Unable to save the AdGuardHome local cache setting."
							return 1
						fi
					fi
				fi
				if [ "$2" = "reconfig" ] && [ -n "${IPSET_SELECTION:-}" ] && ! check_ipset "${IPSET_SELECTION}"; then
					PTXT "${WARNING} Unable to save the optional AdGuardHome IPSET integration setting." \
						"${WARNING} Continuing reconfiguration with the previous IPSET preference."
				fi
				if [ -n "${DNS_FILTER_SELECTION:-}" ] && ! configure_runtime_defaults new-install "${LOCAL_CACHE_SELECTION:-0}"; then
					[ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
					if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
						nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
						nvram commit
					fi
					if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then
						PTXT "${ERROR} Unable to restore the previous DNSFilter settings."
					fi
					[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
					setup_restore_previous_yaml || true
					PTXT "${ERROR} Unable to save the AdGuardHome runtime defaults."
					return 1
				fi
				if [ -n "${WEB_PORT_SELECTION:-}" ] && ! write_conf ADGUARD_WEBUI_PORT "\"${WEB_PORT_SELECTION}\""; then
					[ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
					if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
						nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
						nvram commit
					fi
					if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then
						PTXT "${ERROR} Unable to restore the previous DNSFilter settings."
					fi
					[ -z "${YAML_ORI_NEW:-}" ] || rm -f "${YAML_ORI_NEW}"
					setup_restore_previous_yaml || true
					PTXT "${ERROR} Unable to save the AdGuardHome WebUI port."
					return 1
				fi
				if [ -n "${YAML_ORI_NEW:-}" ] && ! mv -f "${YAML_ORI_NEW}" "${YAML_ORI}"; then
					[ -z "${CONFIG_ROLLBACK}" ] || restore_installer_config "${CONFIG_ROLLBACK}" || PTXT "${ERROR} Unable to restore the previous installer preferences."
					if [ "${DOMAIN_LAN_CHANGED}" -eq 1 ]; then
						nvram set lan_domain="${LAN_DOMAIN_ROLLBACK}"
						nvram commit
					fi
					if [ -n "${DNS_FILTER_ROLLBACK_DIR}" ] && ! restore_dns_filter_settings "${DNS_FILTER_ROLLBACK_DIR}"; then
						PTXT "${ERROR} Unable to restore the previous DNSFilter settings."
					fi
					setup_restore_previous_yaml || true
					PTXT "${ERROR} Unable to publish the original AdGuardHome configuration snapshot."
					return 1
				fi
				SETUP_YAML_TMP_FILE=""
				[ -z "${CONFIG_ROLLBACK}" ] || discard_installer_config_backup "${CONFIG_ROLLBACK}"
				[ -z "${DNS_FILTER_ROLLBACK_DIR}" ] || rm -rf "${DNS_FILTER_ROLLBACK_DIR}"
				;;
		esac
	fi
}

setup_amtmupdate() {
	local AMTM_UPDATE CURRENT_AMTM_UPDATE
	CURRENT_AMTM_UPDATE="$(conf_value AMTM_UPDATE)"
	if [ -z "${CURRENT_AMTM_UPDATE}" ] || [ "${CURRENT_AMTM_UPDATE}" = "DISABLED" ]; then
		AMTM_UPDATE="ENABLED"
	else
		AMTM_UPDATE="DISABLED"
	fi
	PTXT "${INFO} The installer will now set AMTM Update to ${AMTM_UPDATE}."
	write_conf AMTM_UPDATE "\"${AMTM_UPDATE}\""
	end_op_message 0
}

amtm_update_check() {
	local AMTM_UPDATE
	[ "${1:-}" = "amtmupdate" ] && [ "${2:-}" = "check" ] || return 1
	AMTM_UPDATE="$(conf_value AMTM_UPDATE)"
	if [ ! -f "${CONF_FILE}" ] || [ "${AMTM_UPDATE}" = "DISABLED" ]; then
		exit 1
	fi
	if [ "${AMTM_UPDATE}" = "ENABLED" ]; then
		exit 0
	fi
	exit 1
}

uninst_all() {
	if [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then
		PTXT "${INFO} Old Backup Detected!"
		read_yesno "Do you want to remove backup?(this will prevent restoring from backups later)"
		CONFIRM_STATUS=$?
		if [ "${CONFIRM_STATUS}" -eq 0 ]; then
			rm -rf "${BASE_DIR}/backup_AdGuardHome.tar.gz"
		elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
			PTXT "${INFO} Keeping backup instead."
		else
			return "${CONFIRM_STATUS}"
		fi
	fi
	agh_stop
	cleanup_legacy_firewall
	[ "$(conf_value ADGUARD_DOMAIN)" = "yes" ] && nvram set lan_domain="" && nvram commit
	mv "${TARG_DIR}/installer" "${HOME}/installer"
	rm -rf "${TARG_DIR}" "${ROLLBACK_RESULT_FILE}" "/opt/etc/init.d/S99AdGuardHome" "/opt/etc/init.d/rc.func.AdGuardHome" "/opt/sbin/AdGuardHome" "/opt/bin/bcrypt-tool" "${ADDON_DIR}" "/opt/var/log/AdGuardHome.log"
	yaml_nvars_delete "#Asuswrt-Merlin AdGuardHome Installer" /jffs/scripts/dnsmasq.postconf
	del_jffs_script /jffs/scripts/init-start
	del_jffs_script /jffs/scripts/dnsmasq.postconf
	del_jffs_script /jffs/scripts/firewall-start
	if nvram get rc_support | grep -q 'mtlancfg'; then del_jffs_script /jffs/scripts/dnsmasq-sdn.postconf; fi
	del_jffs_script /jffs/scripts/services-stop
	del_between_magic /jffs/scripts/service-event-end '# Asuswrt-Merlin-AdGuardHome-Installer'
	service restart_dnsmasq >/dev/null 2>&1
	end_op_message 0
}

write_command_script() {
	local TARG COMMAND FILENAME
	TARG="$1"
	COMMAND="$2"
	FILENAME="${TARG##*/}"
	if [ ! -f "${TARG}" ]; then
		PTXT "${INFO} Creating ${FILENAME} file"
		if ! PTXT "#!/bin/sh" >"${TARG}"; then
			PTXT "${ERROR} Unable to create ${TARG}"
			return 1
		fi
	fi
	if ! chmod 755 "${TARG}"; then
		PTXT "${ERROR} Unable to set executable permissions on ${TARG}"
		return 1
	fi
	if [ "$(grep -c -F "${COMMAND}" "${TARG}")" -gt 0 ]; then
		PTXT "${INFO} ${FILENAME} file already configured"
	else
		PTXT "${INFO} Configure ${FILENAME} file"
		if ! PTXT "${COMMAND}" >>"${TARG}"; then
			PTXT "${ERROR} Unable to update ${TARG}"
			return 1
		fi
	fi
	return 0
}

write_conf() {
	local VAR VALUE
	VAR="$1"
	VALUE="$2"
	if [ ! -f "${CONF_FILE}" ]; then
		if ! { touch "${CONF_FILE}" && chmod 644 "${CONF_FILE}"; }; then
			PTXT "${ERROR} Unable to create ${CONF_FILE}"
			return 1
		fi
	fi
	if grep -q "^${VAR}=" "${CONF_FILE}"; then
		VALUE=$(_quote "${VALUE}")
		if ! sed -i "/^${VAR}=/s/=.*/=${VALUE}/" "${CONF_FILE}"; then
			PTXT "${ERROR} Unable to update ${VAR} in ${CONF_FILE}"
			return 1
		fi
	else
		if ! PTXT "${VAR}=${VALUE}" >>"${CONF_FILE}"; then
			PTXT "${ERROR} Unable to append ${VAR} to ${CONF_FILE}"
			return 1
		fi
	fi
	return 0
}

write_manager_script() {
	local TARG OP FILENAME COMMAND
	TARG="$1"
	OP="$2"
	FILENAME="${TARG##*/}"
	COMMAND="${ADDON_DIR}/AdGuardHome.sh"
	if [ ! -f "${TARG}" ]; then
		PTXT "${INFO} Creating ${FILENAME} file"
		if ! PTXT "#!/bin/sh" >"${TARG}"; then
			PTXT "${ERROR} Unable to create ${TARG}"
			return 1
		fi
	fi
	if ! chmod 755 "${TARG}" "${COMMAND}"; then
		PTXT "${ERROR} Unable to set executable permissions for ${FILENAME}"
		return 1
	fi
	del_between_magic "${TARG}" Asuswrt-Merlin-AdGuardHome-Installer || return 1
	if [ "$(grep -c -F "[ -x ${COMMAND} ] && ${COMMAND} ${OP}" "${TARG}")" -gt 0 ]; then
		PTXT "${INFO} ${FILENAME} file already configured"
	else
		PTXT "${INFO} Configure ${FILENAME} file"
		if grep -q "^${COMMAND}" "${TARG}"; then
			if ! sed -i "s~^${COMMAND}~[ -x ${COMMAND} ] \&\& ${COMMAND} ${OP}~" "${TARG}"; then
				PTXT "${ERROR} Unable to update ${TARG}"
				return 1
			fi
		else
			del_jffs_script "${TARG}" !manager || return 1
			if [ "$(tail -1 "${TARG}" | grep -c '^$')" -eq 0 ] && ! PTXT "" >>"${TARG}"; then
				PTXT "${ERROR} Unable to update ${TARG}"
				return 1
			fi
			if ! PTXT "[ -x ${COMMAND} ] && ${COMMAND} ${OP}" >>"${TARG}"; then
				PTXT "${ERROR} Unable to update ${TARG}"
				return 1
			fi
		fi
	fi
	return 0
}

yaml_nvars_append() {
	echo "$1" >>"$2"
}

yaml_nvars_delete() {
	PATTERN="$(_quote "$1")"
	sed -i "/${PATTERN}/d" "$2"
}

yaml_nvars_insert() {
	PATTERN="$(_quote "$1")"
	CONTENT="$(_quote "$2")"
	sed -i "/${PATTERN}/a${CONTENT}" "$3"
}

yaml_nvars_replace() {
	PATTERN="$(_quote "$1")"
	CONTENT="$(_quote "$2")"
	sed -i "s/${PATTERN}/${CONTENT}/" "$3"
}

# Interactive menu helpers

end_op_header() {
	sed -n -e "1,$(($(grep -wn 'esac' "$0" | cut -d':' -f1 | tail -n1) + 1))p" "$0" >"${0}.tmp"
	chmod 755 "${0}.tmp"
	exec "${0}.tmp" "${BRANCH}" && rm -rf "${0}.tmp"
}

end_op_message() {
	case "${1:-0}" in
		1)
			if [ "${ROLLBACK_RESULT_UPDATED:-0}" != "1" ] && ! rollback_result_needs_attention; then
				rollback_result_write "operation" "no rollback attempted" "operation aborted before rollback was needed"
			fi
			;;
		2)
			if [ "${ROLLBACK_RESULT_UPDATED:-0}" != "1" ] && ! rollback_result_needs_attention; then
				rollback_result_write "operation" "interrupted: no rollback attempted" "operation interrupted before rollback was needed"
			fi
			;;
	esac
	if [ "${CLI_MODE:-0}" = "1" ]; then
		case "${1:-0}" in
			0) return 0 ;;
			1) return 1 ;;
			2) return 2 ;;
		esac
	fi
	if [ "${ADGUARD_DEFER_END_OP:-0}" = "1" ]; then
		case "${1:-0}" in
			0) return 0 ;;
			1) return 1 ;;
			2) return 2 ;;
		esac
	fi
	case "${1:-0}" in
		0)
			[ "$2" = "update" ] && exit 0
			PTXT "${INFO} Operation completed, returning to Main Menu. You can quit or continue."
			;;
		1)
			if [ "$2" = "update" ]; then
				[ -f "${ROLLBACK_RESULT_FILE}" ] && PTXT "${INFO} Last rollback result: $(rollback_result_summary)"
				exit 1
			fi
			PTXT "${INFO} Operation aborted, returning to Main Menu. You can quit or continue."
			[ -f "${ROLLBACK_RESULT_FILE}" ] && PTXT "${INFO} Last rollback result: $(rollback_result_summary)"
			;;
		2)
			PTXT "${INFO} Abnormal operations, returning to Main Menu. You can quit or continue."
			[ -f "${ROLLBACK_RESULT_FILE}" ] && PTXT "${INFO} Last rollback result: $(rollback_result_summary)"
			;;
	esac
	PTXT "====================================================="
	PTXT " "
	PTXT " "
	sleep 3s && clear_screen
	if [ -f "${TARG_DIR}/installer" ]; then
		chmod 755 "${TARG_DIR}/installer" >/dev/null 2>&1
		exec "${TARG_DIR}/installer" "${BRANCH}"
	elif [ ! -f "${TARG_DIR}/installer" ] && [ -f "${SCRIPT_LOC}" ]; then
		chmod 755 "${SCRIPT_LOC}" >/dev/null 2>&1
		exec "${SCRIPT_LOC}" "${BRANCH}"
	elif [ -f "${HOME}/installer" ]; then
		chmod 755 "${HOME}/installer"
		exec "${HOME}/installer" "${BRANCH}"
	else
		clear_screen && { end_op_header 2>/dev/null; }
	fi
	exit $?
}

menu() {
	local IPSET_SELECTION LOCAL_CACHE_SELECTION
	trap - HUP INT QUIT ABRT TERM
	case "$1" in
		"")
			PTXT "${INFO} Choose what you want to do:" \
				"  1) Install/Update AdGuardHome" \
				"  2) Uninstall"
			if { [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ]; }; then
				PTXT "  3) Change Username/Password" "  4) Reconfigure" "  5) Enable/Disable AMTM updates" "  6) Enable/Disable AdGuardHome Local Cache" "  7) Switch AdGuardHome Branch" "  8) Enable/Disable AdGuardHome IPSET Integration" "  m) Migrate Runtime Defaults"
				if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then
					PTXT "  9) Analyze and remove unused blocklists"
				fi
				PTXT "  b) Backup"
			fi
			if { [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; }; then { PTXT "  r) Restore"; }; fi
			PTXT "  q) Quit"
			if { [ ! -d "${TARG_DIR}" ] || [ ! -f "${AGH_FILE}" ]; } && [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then { read_input_num "Please enter the number that designates your selection:" 1 2 q; }; fi
			if { [ ! -d "${TARG_DIR}" ] || [ ! -f "${AGH_FILE}" ]; } && [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; then { read_input_num "Please enter the number that designates your selection:" 1 2 r q; }; fi
			if { [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ ! -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; }; then { if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then { read_input_num "Please enter the number that designates your selection:" 1 9 b m q; }; else { read_input_num "Please enter the number that designates your selection:" 1 8 b m q; }; fi; }; fi
			if { [ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ]; }; then { if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then { read_input_num "Please enter the number that designates your selection:" 1 9 b r m q; }; else { read_input_num "Please enter the number that designates your selection:" 1 8 b r m q; }; fi; }; fi
			;;
		*)
			CHOSEN="$1"
			;;
	esac
	[ -n "${CHOSEN}" ] && trap 'clear_screen; end_op_message 2' HUP INT QUIT ABRT TERM
	case "${CHOSEN}" in
		"1" | "install" | "update")
			ptxt_phase "Selecting install or update operation."
			if [ "${CHOSEN}" = "update" ]; then
				ptxt_step "Starting requested AdGuardHome update."
				AUTO_UPDATE="update"
				inst_AdGuardHome "${AUTO_UPDATE:-update}"
			else
				PTXT "${INFO} This operation will install AdGuardHome and related files (<6MB)" \
					"${INFO} to ENTWARE, no other data will be changed." \
					"${INFO} Also some start scripts will be installed/modified as required."
				if read_yesno "Do you want to install AdGuardHome?"; then
					ptxt_ok "Install operation confirmed."
					inst_AdGuardHome "${AUTO_UPDATE:-install}"
				else
					ptxt_warn "Install operation declined."
					end_op_message 1
				fi
			fi
			;;
		"2" | "uninstall")
			PTXT "${INFO} This operation will cleanup everything installed by this script."
			if read_yesno "Do you want to continue?"; then uninst_all; else end_op_message 1; fi
			;;
		"3" | "changepw")
			PTXT "${INFO} This operation will allow you to change your username and password."
			if read_yesno "Do you want to continue?"; then AdGuardHome_authen 0; else end_op_message 1; fi
			;;
		"4" | "reconfigure")
			PTXT "${INFO} This operation allows you to configure AdGuardHome"
			if read_yesno "Do you want to proceed?"; then setup_AdGuardHome reconfig; else end_op_message 1; fi
			;;
		"5" | "setamtmupdate")
			PTXT "${INFO} This operation allows you to enable or disable AMTM's ability to manage updates for this script and by extension, AdGuardHome." \
				"${INFO} If the option is already disabled and you choose to proceed, then you will be enabling." \
				"${INFO} The same is true if the opposite is true and you choose to proceed."
			if read_yesno "Do you want to proceed?"; then setup_amtmupdate; else end_op_message 1; fi
			;;
		"6" | "setlocalcache")
			if [ "$(nvram get dns_local_cache)" != "1" ]; then
				PTXT "${INFO} This operation allows you to switch to using AdGuardHome as a local caching dns service for the router's own requests." \
					"${INFO} A simple Yes or No will allow you to decide whether AdGuardhome handles DNS request for both the router and clients, or only clients."
				read_yesno "Do you want to run AdGuardHome as a local caching DNS service?"
				CONFIRM_STATUS=$?
				if [ "${CONFIRM_STATUS}" -eq 0 ]; then
					LOCAL_CACHE_SELECTION=1
				elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
					LOCAL_CACHE_SELECTION=0
				else
					end_op_message 1
					return "${CONFIRM_STATUS}"
				fi
				if ! check_dns_local "${LOCAL_CACHE_SELECTION}"; then
					PTXT "${ERROR} Unable to save the AdGuardHome local cache setting."
					end_op_message 1
					return 1
				fi
				PTXT "${INFO} For best results, please reboot the router after making these changes."
				end_op_message 0
			else
				PTXT "${INFO} The router's own nvram built-in dns local caching feature is enabled." \
					"${INFO} Hence why you cannot configure the installers method at this time."
				end_op_message 1
			fi
			;;
		"7" | "switchbranch")
			PTXT "${INFO} This operation allows you to switch between AdGuardHome branches installed." \
				"${INFO} Before you choose to proceed, it is recommended to perform a backup."
			if read_yesno "Do you want to proceed?"; then {
				if ! choose_branch x; then
					end_op_message 1
					return 1
				fi
				AUTO_UPDATE="update"
				inst_AdGuardHome "${AUTO_UPDATE:-update}"
			}; else end_op_message 1; fi
			;;
		"8" | "setipset")
			PTXT "${INFO} IPSET integration imports compatible domain-to-IPSET mappings for routing and firewall add-ons." \
				"${INFO} Disable it if this router does not use IPSET-based domain routing or filtering."
			read_yesno "Do you want to enable AdGuardHome IPSET integration?"
			CONFIRM_STATUS=$?
			if [ "${CONFIRM_STATUS}" -eq 0 ]; then
				IPSET_SELECTION=1
			elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
				IPSET_SELECTION=0
			else
				end_op_message 1
				return "${CONFIRM_STATUS}"
			fi
			if ! check_ipset "${IPSET_SELECTION}"; then
				PTXT "${ERROR} Unable to save the AdGuardHome IPSET integration setting. AdGuardHome was not restarted."
				end_op_message 1
				return 1
			fi
			OLD_PIDS="$(pidof AdGuardHome 2>/dev/null)"
			if ! service restart_AdGuardHome >/dev/null 2>&1; then
				PTXT "${WARNING} The setting was saved, but AdGuardHome could not be restarted automatically."
			else
				adguard_service_status_after_action restart "${OLD_PIDS}" || true
			fi
			end_op_message 0
			;;
		"m" | "M" | "migrate-runtime-defaults")
			PTXT "${INFO} This operation inspects legacy runtime defaults and can migrate them to safer v2.6.0 values."
			read_yesno "Do you want to migrate runtime defaults now?"
			CONFIRM_STATUS=$?
			if [ "${CONFIRM_STATUS}" -eq 0 ]; then
				cli_migrate_runtime_defaults --yes
				end_op_message "$?"
			elif [ "${CONFIRM_STATUS}" -eq 1 ]; then
				end_op_message 1
			else
				end_op_message 1
				return "${CONFIRM_STATUS}"
			fi
			;;
		"9" | "blocklists" | "unusedblocklists")
			if [ -z "${BLOCKLIST_ANALYZER_SHA256}" ]; then
				ptxt_fail "Blocklist cleanup is unavailable until the analyzer SHA-256 is pinned."
				end_op_message 1
			else
				cleanup_unused_blocklists
			fi
			;;
		"b" | "B" | "backup")
			ptxt_phase "Selecting backup operation."
			PTXT "${INFO} This operation will backup everything!"
			if read_yesno "Do you want to continue?"; then
				ptxt_ok "Backup operation confirmed."
				backup_restore BACKUP
			else
				ptxt_warn "Backup operation declined."
				end_op_message 1
			fi
			;;
		"r" | "R" | "restore")
			ptxt_phase "Selecting restore operation."
			PTXT "${INFO} This operation will restore everything!"
			if read_yesno "Do you want to continue?"; then
				ptxt_ok "Restore operation confirmed."
				backup_restore RESTORE
			else
				ptxt_warn "Restore operation declined."
				end_op_message 1
			fi
			;;
		"d" | "D" | "doctor")
			doctor
			;;
		"status")
			status "${BRANCH}"
			;;
		"q" | "Q")
			PTXT "${INFO} Operations have been applied if any has been made" \
				"${INFO} In case of anomaly, please reboot your router!"
			if [ -f "${HOME}/installer" ]; then rm -rf "${HOME}/installer"; fi
			sleep 3s
			clear_screen
			;;
	esac
}

read_input_dns() {
	local DNS_SERVER
	while :; do
		PTXT -n "${INPUT} ${1} ${BOLD}${2}: ${NORM}"
		if ! read -r DNS_SERVER; then
			PTXT "" "${ERROR} Unable to read DNS server input."
			return 1
		fi
		[ -z "${DNS_SERVER}" ] && DNS_SERVER="$2"
		if ! ipv4_is_valid "${DNS_SERVER}"; then
			PTXT "${ERROR} Invalid DNS server address entered"
			continue
		fi
		if [ "${DNS_SERVER1:-}" = "${DNS_SERVER}" ]; then
			PTXT "${ERROR} ${DNS_SERVER} DNS server address already entered, please try again!"
			continue
		fi
		case "$1" in
			"Default is")
				BOOTSTRAP1="${DNS_SERVER}"
				DNS_SERVER1="${DNS_SERVER}"
				;;
			"2nd Default is")
				BOOTSTRAP2="${DNS_SERVER}"
				;;
		esac
		return 0
	done
}

read_input_num() {
	local RANGE
	RANGE="[${2}-${3}"
	[ -n "${4:-}" ] && RANGE="${RANGE}/${4}"
	[ -n "${5:-}" ] && RANGE="${RANGE}/${5}"
	[ -n "${6:-}" ] && RANGE="${RANGE}/${6}"
	[ -n "${7:-}" ] && RANGE="${RANGE}/${7}"
	RANGE="${RANGE}]"
	while :; do
		PTXT -n "${INPUT} ${1}, ${BOLD}${RANGE}${NORM}: "
		if ! read -r CHOSEN; then
			PTXT "" "${ERROR} Unable to read menu selection."
			return 1
		fi
		if [ -z "${CHOSEN}" ]; then
			PTXT "${ERROR} Invalid character(s) entered! Retrying..."
			continue
		fi
		case "${CHOSEN}" in
			"${4:-}" | "${5:-}" | "${6:-}" | "${7:-}")
				return 1
				;;
			*)
				if ! PTXT "${CHOSEN}" | grep -qE '^[0-9]+$'; then
					PTXT "${ERROR} Invalid character(s) entered! Retrying..."
					continue
				fi
				if [ "${CHOSEN}" -lt "$2" ] || [ "${CHOSEN}" -gt "$3" ]; then
					PTXT "${ERROR} Chosen number is not in range! Retrying..."
					continue
				fi
				return 0
				;;
		esac
	done
}

read_input_port() {
	while :; do
		PTXT -n "${INPUT} ${1} ${BOLD}${2}: ${NORM}"
		if ! read -r WEB_PORT; then
			PTXT "" "${ERROR} Unable to read Web Port input."
			return 1
		fi
		[ -z "${WEB_PORT}" ] && WEB_PORT="$2"
		if ! port_is_valid "${WEB_PORT}"; then
			PTXT "${ERROR} Invalid Web Port entered, only Web Ports residing within the port range 3000-65535 are selectable."
			continue
		fi
		WEB_PORT="$(PTXT "${WEB_PORT}" | sed 's/^0*//')"
		if ! ai_have_cmd netstat; then
			PTXT "${ERROR} Unable to verify the Web Port because netstat is unavailable."
			return 1
		fi
		if web_port_in_use "${WEB_PORT}"; then
			PTXT "${ERROR} ${WEB_PORT} already in use, please choose a different port!"
			continue
		fi
		return 0
	done
}

read_yesno() {
	local YESNO
	if [ "${AI_ASSUME_YES:-0}" = "1" ]; then
		PTXT "${INPUT} ${1} ${BOLD}[y/n]${NORM}: y"
		return 0
	fi
	while :; do
		PTXT -n "${INPUT} ${1} ${BOLD}[y/n]${NORM}: "
		if ! read -r YESNO; then
			PTXT "" "${ERROR} Unable to read confirmation input."
			return 2
		fi
		case "${YESNO}" in
			y | Y)
				return 0
				;;
			n | N)
				return 1
				;;
			*)
				PTXT "${ERROR} Invalid input!"
				;;
		esac
	done
}

menu_action_allowed() {
	case "$1" in
		"1" | "install" | "update" | "2" | "uninstall")
			return 0
			;;
		[dD] | "doctor" | "status")
			return 0
			;;
		"3" | "changepw" | "4" | "reconfigure" | "5" | "setamtmupdate" | "6" | "setlocalcache" | "7" | "switchbranch" | "8" | "setipset" | [mM] | "migrate-runtime-defaults" | [bB] | "backup")
			[ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && return 0
			return 1
			;;
		"9" | "blocklists" | "unusedblocklists")
			[ -d "${TARG_DIR}" ] && [ -f "${AGH_FILE}" ] && [ -n "${BLOCKLIST_ANALYZER_SHA256}" ] && return 0
			return 1
			;;
		[rR] | "restore")
			[ -f "${BASE_DIR}/backup_AdGuardHome.tar.gz" ] && return 0
			return 1
			;;
		*)
			return 1
			;;
	esac
}

single_arg_menu_action() {
	case "$1" in
		"migrate-runtime-defaults")
			return 1
			;;
	esac
	menu_action_allowed "$1"
}

cli_usage() {
	PTXT "${INFO} Usage:" \
		"${INFO}   sh installer install [--installer-branch master] [--adguardhome-branch {release|beta|edge}] --yes [--allow-dns-nvram]" \
		"${INFO}   sh installer update [--installer-branch master] [--adguardhome-branch {release|beta|edge}] --yes [--dry-run]" \
		"${INFO}   sh installer backup --yes" \
		"${INFO}   sh installer restore --file /path/to/backup_AdGuardHome.tar.gz --yes [--dry-run]" \
		"${INFO}   sh installer preflight [install|reconfigure|update|restore|uninstall|status]" \
		"${INFO}   sh installer doctor [--fix]" \
		"${INFO}   sh installer ipset refresh [--yes] [--dry-run]" \
		"${INFO}   sh installer ipset status" \
		"${INFO}   sh installer ipset doctor" \
		"${INFO}   sh installer netcheck --mode {legacy|wan|lan} [--hosts host-list] [--dns server] [--require-http {YES|NO}] [--timeout seconds]" \
		"${INFO}   sh installer dns-port-policy --policy {legacy|refuse-unknown}" \
		"${INFO}   sh installer performance --profile {balanced|low-memory|fast}" \
		"${INFO}   sh installer migrate-runtime-defaults [--yes] [--dry-run]" \
		"${INFO}   sh installer uninstall --yes [--dry-run] [--allow-dns-nvram]"
}

cli_require_yes() {
	if [ "${CLI_YES:-0}" != "1" ]; then
		PTXT "${ERROR} Refusing destructive non-interactive action without --yes."
		return 1
	fi
	return 0
}

cli_enable_assume_yes() {
	AI_ASSUME_YES="1"
	export AI_ASSUME_YES
}

cli_dry_run() {
	PTXT "${INFO} Dry-run: would run $1."
}

cli_pre_remote_dry_run() {
	local cmd opt restore_file sub
	cmd="${1:-}"
	shift || true
	restore_file=""
	case "${cmd}" in
		install | update | restore | uninstall) ;;
		ipset)
			sub="${1:-}"
			[ "${sub}" = "refresh" ] || return 1
			shift || true
			;;
		*) return 1 ;;
	esac
	while [ "$#" -gt 0 ]; do
		opt="$1"
		case "${opt}" in
			--dry-run)
				case "${cmd}" in
					restore) cli_dry_run "restore ${restore_file:-${BASE_DIR}/backup_AdGuardHome.tar.gz}" ;;
					ipset) cli_dry_run "IPSET refresh" ;;
					*) cli_dry_run "${cmd}" ;;
				esac
				return 0
				;;
			--file)
				[ "$#" -gt 1 ] || return 1
				restore_file="$2"
				shift 2
				;;
			--branch | --adguardhome-branch | --installer-branch | --profile)
				[ "$#" -gt 1 ] || return 1
				shift 2
				;;
			*)
				shift
				;;
		esac
	done
	return 1
}

cli_pre_runtime_defaults_preview() {
	local apply preview_args
	[ "${1:-}" = "migrate-runtime-defaults" ] || return 3
	shift || true
	apply="0"
	preview_args=""
	while [ "$#" -gt 0 ]; do
		case "$1" in
			--yes)
				apply="1"
				;;
			--dry-run)
				preview_args="${preview_args} --dry-run"
				;;
			*)
				return 2
				;;
		esac
		shift
	done
	[ "${apply}" = "0" ] || return 3
	# Intentional unquoted expansion: preview_args is built from fixed option tokens only.
	cli_migrate_runtime_defaults ${preview_args}
	return "$?"
}

cli_backup_restore_file() {
	local restore_default restore_file restore_save restore_defer_saved cli_ret
	restore_file="$1"
	restore_default="${BASE_DIR}/backup_AdGuardHome.tar.gz"
	restore_defer_saved="${ADGUARD_DEFER_END_OP:-0}"
	if [ -z "${restore_file}" ]; then
		PTXT "${ERROR} restore requires --file /path/to/backup_AdGuardHome.tar.gz."
		return 1
	fi
	if [ ! -f "${restore_file}" ]; then
		PTXT "${ERROR} Restore file not found: ${restore_file}"
		return 1
	fi
	if ! adguard_archive_is_safe "${restore_file}" 1; then
		PTXT "${ERROR} Restore file failed backup safety validation: ${restore_file}"
		return 1
	fi
	if [ "${restore_file}" = "${restore_default}" ]; then
		ADGUARD_DEFER_END_OP="1"
		backup_restore RESTORE
		cli_ret="$?"
		ADGUARD_DEFER_END_OP="${restore_defer_saved}"
		return "${cli_ret}"
	fi
	restore_save="${restore_default}.cli.$$"
	[ -f "${restore_default}" ] && mv "${restore_default}" "${restore_save}"
	if ! cp "${restore_file}" "${restore_default}"; then
		[ -f "${restore_save}" ] && mv "${restore_save}" "${restore_default}"
		return 1
	fi
	if ! chmod 600 "${restore_default}"; then
		rm -f "${restore_default}"
		[ -f "${restore_save}" ] && mv "${restore_save}" "${restore_default}"
		return 1
	fi
	ADGUARD_DEFER_END_OP="1"
	backup_restore RESTORE
	cli_ret="$?"
	ADGUARD_DEFER_END_OP="${restore_defer_saved}"
	if [ -f "${restore_save}" ]; then
		mv "${restore_save}" "${BASE_DIR}/backup_AdGuardHome.tar.gz"
	fi
	return "${cli_ret}"
}

cli_bool_value() {
	case "$1" in
		YES | yes | Yes | ON | on | On | TRUE | true | True | 1) PTXT "YES" ;;
		NO | no | No | OFF | off | Off | FALSE | false | False | 0) PTXT "NO" ;;
		*) return 1 ;;
	esac
}

cli_adguard_branch_is_valid() {
	case "$1" in
		release | beta | edge) return 0 ;;
		*) return 1 ;;
	esac
}

cli_simple_value_is_safe() {
	case "$1" in
		"" | *[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._:-]*) return 1 ;;
		*) return 0 ;;
	esac
}

cli_host_list_is_safe() {
	[ -n "$1" ] || return 1
	[ -z "$(printf '%s' "$1" | tr -d 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._: -')" ]
}

cli_write_quoted_conf() {
	if ! write_conf "$1" "\"$2\""; then
		return 1
	fi
	return 0
}

cli_netcheck_config_values() {
	local dns host_list http_required mode timeout
	mode="$1"
	host_list="$2"
	dns="$3"
	http_required="$4"
	timeout="$5"
	case "${mode}" in
		legacy | wan | lan) ;;
		*)
			PTXT "${ERROR} netcheck requires --mode {legacy|wan|lan}."
			return 1
			;;
	esac
	[ -z "${host_list}" ] || cli_host_list_is_safe "${host_list}" || {
		PTXT "${ERROR} netcheck hosts contain unsupported characters."
		return 1
	}
	[ -z "${dns}" ] || cli_simple_value_is_safe "${dns}" || {
		PTXT "${ERROR} netcheck DNS server contains unsupported characters."
		return 1
	}
	case "${timeout}" in
		"") ;;
		*[!0123456789]*)
			PTXT "${ERROR} netcheck timeout must be a positive number of seconds."
			return 1
			;;
		*) [ "${timeout}" -gt 0 ] 2>/dev/null || {
			PTXT "${ERROR} netcheck timeout must be greater than zero."
			return 1
		} ;;
	esac
	cli_write_quoted_conf ADGUARD_NETCHECK_MODE "${mode}" || return 1
	[ -z "${host_list}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_HOSTS "${host_list}" || return 1
	[ -z "${dns}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_DNS "${dns}" || return 1
	[ -z "${http_required}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_REQUIRE_HTTP "${http_required}" || return 1
	[ -z "${timeout}" ] || cli_write_quoted_conf ADGUARD_NETCHECK_TIMEOUT "${timeout}" || return 1
	ptxt_ok "Netcheck mode set to ${mode}."
}

cli_dns_port_policy() {
	local policy
	policy=""
	while [ "$#" -gt 0 ]; do
		case "$1" in
			--policy)
				[ "$#" -gt 1 ] || return 2
				policy="$2"
				shift 2
				;;
			*) return 2 ;;
		esac
	done
	case "${policy}" in
		legacy)
			cli_write_quoted_conf ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "0" || return 1
			ptxt_ok "DNS port-owner cleanup policy set to legacy."
			;;
		refuse-unknown)
			cli_write_quoted_conf ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL "1" || return 1
			ptxt_ok "DNS port-owner cleanup policy set to refuse unknown owners."
			;;
		*)
			PTXT "${ERROR} dns-port-policy requires --policy {legacy|refuse-unknown}."
			return 1
			;;
	esac
}

cli_migrate_runtime_default() {
	local current key legacy safe status
	key="$1"
	legacy="$2"
	safe="$3"
	current="$(conf_value "${key}")"
	if [ "${current}" = "${legacy}" ]; then
		PTXT "${WARNING} Legacy runtime default: ${key}=\"${current}\"; v2.6.0 safer value is \"${safe}\"."
		status=1
	elif [ -z "${current}" ]; then
		PTXT "${INFO} Missing runtime default: ${key}; v2.6.0 safer value is \"${safe}\"."
		status=1
	else
		PTXT "${INFO} Runtime default already set: ${key}=\"${current}\"."
		status=0
	fi
	return "${status}"
}

cli_migrate_runtime_defaults() {
	local apply dry_run changes dns_port_change netcheck_change proc_optimize_change proc_profile_change proc_optimize_current
	apply="0"
	dry_run="0"
	changes="0"
	while [ "$#" -gt 0 ]; do
		case "$1" in
			--yes) apply="1" ;;
			--dry-run) dry_run="1" ;;
			*) return 2 ;;
		esac
		shift
	done
	if [ ! -f "${CONF_FILE}" ]; then
		PTXT "${ERROR} Runtime config not found: ${CONF_FILE}"
		return 1
	fi
	PTXT "${INFO} Inspecting runtime defaults in ${CONF_FILE}."
	dns_port_change="0"
	netcheck_change="0"
	proc_optimize_change="0"
	proc_profile_change="0"
	cli_migrate_runtime_default ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL 0 1 || {
		changes="1"
		dns_port_change="1"
	}
	cli_migrate_runtime_default ADGUARD_NETCHECK_MODE legacy wan || {
		changes="1"
		netcheck_change="1"
	}
	proc_optimize_current="$(conf_value ADGUARD_PROC_OPTIMIZE)"
	if [ -z "${proc_optimize_current}" ]; then
		PTXT "${INFO} Missing runtime default: ADGUARD_PROC_OPTIMIZE; v2.6.0 keeps optimization enabled only with the safer balanced profile."
		changes="1"
		proc_optimize_change="1"
	else
		PTXT "${INFO} Runtime default already set: ADGUARD_PROC_OPTIMIZE=\"${proc_optimize_current}\"."
	fi
	cli_migrate_runtime_default ADGUARD_PROC_PROFILE aggressive balanced || {
		changes="1"
		proc_profile_change="1"
	}
	if [ "${changes}" = "0" ]; then
		ptxt_ok "Runtime defaults already use v2.6.0 safer values or custom overrides."
		return 0
	fi
	if [ "${dry_run}" = "1" ]; then
		PTXT "${INFO} Dry-run: would write v2.6.0 safer runtime defaults."
		return 0
	fi
	if [ "${apply}" != "1" ]; then
		PTXT "${INFO} No changes made. Re-run with --yes to migrate runtime defaults."
		return 0
	fi
	if [ "${dns_port_change}" = "1" ]; then
		cli_write_quoted_conf ADGUARDHOME_REFUSE_UNKNOWN_DNS_PORT_KILL 1 || return 1
	fi
	if [ "${netcheck_change}" = "1" ]; then
		cli_write_quoted_conf ADGUARD_NETCHECK_MODE wan || return 1
	fi
	if [ "${proc_optimize_change}" = "1" ]; then
		cli_write_quoted_conf ADGUARD_PROC_OPTIMIZE YES || return 1
	fi
	if [ "${proc_profile_change}" = "1" ]; then
		cli_write_quoted_conf ADGUARD_PROC_PROFILE balanced || return 1
	fi
	ptxt_ok "Runtime defaults migrated to v2.6.0 safer values."
}

cli_installer_branch_from_args() {
	while [ "$#" -gt 0 ]; do
		case "$1" in
			--installer-branch)
				[ "$#" -gt 1 ] || return 1
				branch_is_safe "$2" || return 1
				PTXT "$2"
				return 0
				;;
			--branch | --adguardhome-branch | --file | --profile | --mode | --hosts | --dns | --require-http | --timeout | --policy)
				[ "$#" -gt 1 ] || return 1
				shift 2
				;;
			*)
				shift
				;;
		esac
	done
	return 1
}

cli_write_adguard_branch() {
	local old_branch new_branch
	new_branch="$1"
	old_branch="$(conf_value ADGUARD_BRANCH)"
	if ! write_conf ADGUARD_BRANCH "\"${new_branch}\""; then
		return 1
	fi
	if [ -n "${old_branch}" ] && [ "${old_branch}" != "${new_branch}" ]; then
		ADGUARD_BRANCH_CHANGED="1"
		export ADGUARD_BRANCH_CHANGED
	fi
	return 0
}

cli_run() {
	local cmd sub opt restore_file profile proc_profile adguard_branch installer_branch netcheck_mode netcheck_hosts netcheck_dns netcheck_http netcheck_timeout dns_policy migrate_args
	CLI_MODE="1"
	export CLI_MODE
	cmd="${1:-}"
	shift || true
	CLI_YES="0"
	CLI_DRY_RUN="0"
	CLI_ALLOW_DNS_NVRAM="0"
	restore_file=""
	profile=""
	adguard_branch=""
	installer_branch=""
	netcheck_mode=""
	netcheck_hosts=""
	netcheck_dns=""
	netcheck_http=""
	netcheck_timeout=""
	dns_policy=""
	migrate_args=""
	case "${cmd}" in
		install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) ;;
		*) return 2 ;;
	esac
	if [ "${cmd}" = "ipset" ]; then
		sub="${1:-}"
		[ -n "${sub}" ] && shift
	else
		sub=""
	fi
	while [ "$#" -gt 0 ]; do
		opt="$1"
		case "${opt}" in
			--yes)
				[ "${cmd}" != "migrate-runtime-defaults" ] || migrate_args="${migrate_args} --yes"
				CLI_YES="1"
				shift
				;;
			--dry-run)
				[ "${cmd}" != "migrate-runtime-defaults" ] || migrate_args="${migrate_args} --dry-run"
				CLI_DRY_RUN="1"
				shift
				;;
			--allow-dns-nvram)
				CLI_ALLOW_DNS_NVRAM="1"
				shift
				;;
			--fix)
				[ "${cmd}" = "doctor" ] || return 2
				sub="--fix"
				shift
				;;
			--branch | --adguardhome-branch)
				[ "$#" -gt 1 ] || return 2
				adguard_branch="$2"
				shift 2
				;;
			--installer-branch)
				[ "$#" -gt 1 ] || return 2
				installer_branch="$2"
				shift 2
				;;
			--file)
				[ "$#" -gt 1 ] || return 2
				restore_file="$2"
				shift 2
				;;
			--profile)
				[ "$#" -gt 1 ] || return 2
				profile="$2"
				shift 2
				;;
			--mode)
				[ "$#" -gt 1 ] || return 2
				[ "${cmd}" = "netcheck" ] || return 2
				netcheck_mode="$2"
				shift 2
				;;
			--hosts)
				[ "$#" -gt 1 ] || return 2
				[ "${cmd}" = "netcheck" ] || return 2
				netcheck_hosts="$2"
				shift 2
				;;
			--dns)
				[ "$#" -gt 1 ] || return 2
				[ "${cmd}" = "netcheck" ] || return 2
				netcheck_dns="$2"
				shift 2
				;;
			--require-http)
				[ "$#" -gt 1 ] || return 2
				[ "${cmd}" = "netcheck" ] || return 2
				netcheck_http="$(cli_bool_value "$2")" || return 2
				shift 2
				;;
			--timeout)
				[ "$#" -gt 1 ] || return 2
				[ "${cmd}" = "netcheck" ] || return 2
				netcheck_timeout="$2"
				shift 2
				;;
			--policy)
				[ "$#" -gt 1 ] || return 2
				[ "${cmd}" = "dns-port-policy" ] || return 2
				dns_policy="$2"
				shift 2
				;;
			*) return 2 ;;
		esac
	done
	if [ -n "${installer_branch}" ]; then
		branch_is_safe "${installer_branch}" || {
			PTXT "${ERROR} Unsupported installer branch: ${installer_branch}"
			return 1
		}
		write_conf INSTALLER_BRANCH "\"${installer_branch}\"" || return 1
	fi
	if [ -n "${adguard_branch}" ]; then
		cli_adguard_branch_is_valid "${adguard_branch}" || {
			PTXT "${ERROR} Unsupported AdGuardHome branch: ${adguard_branch}"
			return 1
		}
	fi
	case "${cmd}" in
		doctor)
			doctor "${sub}"
			return "$?"
			;;
		update)
			[ "${CLI_DRY_RUN}" = "1" ] && {
				cli_dry_run "update"
				return 0
			}
			cli_require_yes || return 1
			[ -z "${adguard_branch}" ] || cli_write_adguard_branch "${adguard_branch}" || return 1
			cli_enable_assume_yes
			AUTO_UPDATE="update"
			menu update
			;;
		backup)
			cli_require_yes || return 1
			cli_enable_assume_yes
			backup_restore BACKUP CLI
			return "$?"
			;;
		restore)
			[ "${CLI_DRY_RUN}" = "1" ] && {
				cli_dry_run "restore ${restore_file:-${BASE_DIR}/backup_AdGuardHome.tar.gz}"
				return 0
			}
			cli_require_yes || return 1
			cli_enable_assume_yes
			cli_backup_restore_file "${restore_file}"
			;;
		uninstall)
			[ "${CLI_DRY_RUN}" = "1" ] && {
				cli_dry_run "uninstall"
				return 0
			}
			cli_require_yes || return 1
			if [ "$(conf_value ADGUARD_DOMAIN)" = "yes" ] && [ "${CLI_ALLOW_DNS_NVRAM}" != "1" ]; then
				PTXT "${ERROR} Refusing uninstall DNS/NVRAM rewrite without --allow-dns-nvram."
				return 1
			fi
			cli_enable_assume_yes
			uninst_all
			;;
		ipset)
			case "${sub}" in
				status | doctor)
					ipset_status
					return "$?"
					;;
				refresh) ;;
				*) return 2 ;;
			esac
			[ "${CLI_DRY_RUN}" = "1" ] && {
				cli_dry_run "IPSET refresh"
				return 0
			}
			if [ "$(conf_value ADGUARD_IPSET)" = "NO" ]; then
				ptxt_ok "IPSET integration is disabled; no refresh needed."
				return 0
			fi
			if [ "${CLI_YES}" = "1" ]; then
				OLD_PIDS="$(pidof AdGuardHome 2>/dev/null)"
				if service restart_AdGuardHome >/dev/null 2>&1 && adguard_service_status_after_action restart "${OLD_PIDS}"; then
					return 0
				fi
				PTXT "${WARNING} IPSET refresh requested, but AdGuardHome could not be restarted."
				return 1
			else
				ptxt_ok "IPSET preference unchanged. Use --yes to restart AdGuardHome now."
			fi
			;;
		netcheck)
			cli_netcheck_config_values "${netcheck_mode}" "${netcheck_hosts}" "${netcheck_dns}" "${netcheck_http}" "${netcheck_timeout}"
			return "$?"
			;;
		dns-port-policy)
			cli_dns_port_policy --policy "${dns_policy}"
			return "$?"
			;;
		migrate-runtime-defaults)
			# Intentional unquoted expansion: migrate_args is built from fixed option tokens only.
			cli_migrate_runtime_defaults ${migrate_args}
			return "$?"
			;;
		performance)
			case "${profile}" in
				balanced) proc_profile="balanced" ;;
				low-memory) proc_profile="safe" ;;
				fast) proc_profile="aggressive" ;;
				*)
					PTXT "${ERROR} performance requires --profile {balanced|low-memory|fast}."
					return 1
					;;
			esac
			if ! write_conf ADGUARD_PROC_PROFILE "\"${proc_profile}\""; then
				return 1
			fi
			ptxt_ok "Performance profile set to ${profile}."
			;;
		install)
			[ "${CLI_DRY_RUN}" = "1" ] && {
				cli_dry_run "install"
				return 0
			}
			cli_require_yes || return 1
			if [ "${CLI_ALLOW_DNS_NVRAM}" != "1" ]; then
				PTXT "${ERROR} Refusing install DNS/NVRAM rewrite without --allow-dns-nvram."
				return 1
			fi
			if [ ! -f "${AGH_FILE}" ]; then
				PTXT "${ERROR} Non-interactive install cannot complete fresh first-run setup."
				PTXT "${ERROR} Run the interactive installer for fresh installs, or use restore --file with an existing backup."
				return 1
			fi
			cli_enable_assume_yes
			cleanup
			check_jffs_enabled
			check_dns_environment 0
			[ -n "${adguard_branch}" ] || adguard_branch="$(conf_value ADGUARD_BRANCH)"
			[ -n "${adguard_branch}" ] || adguard_branch="release"
			cli_adguard_branch_is_valid "${adguard_branch}" || {
				PTXT "${ERROR} Unsupported AdGuardHome branch: ${adguard_branch}"
				return 1
			}
			create_dir "${TARG_DIR}" || return 1
			[ -z "${installer_branch}" ] || write_conf INSTALLER_BRANCH "\"${installer_branch}\"" || return 1
			cli_write_adguard_branch "${adguard_branch}" || return 1
			inst_AdGuardHome install
			;;
	esac
}

if [ "${1:-}" = "preflight" ]; then
	preflight "${2:-install}"
	exit "$?"
fi
if [ "${1:-}" = "doctor" ]; then
	case "${2:-}" in
		"" | "--fix")
			doctor "${2:-}"
			exit "$?"
			;;
	esac
fi
if [ "${1:-}" = "status" ]; then
	status
	exit "$?"
fi
if [ "${2:-}" = "status" ]; then
	status "${1:-}"
	exit "$?"
fi

case "${1:-}" in
	install | update | restore | uninstall | ipset)
		if cli_pre_remote_dry_run "$@"; then
			exit 0
		fi
		;;
esac
cli_pre_runtime_defaults_preview "$@"
CLI_STATUS="$?"
case "${CLI_STATUS}" in
	0) exit 0 ;;
	2)
		cli_usage
		exit 2
		;;
esac

amtm_update_check "$@"

installer_dependencies_available || exit 1
trap 'on_installer_exit' EXIT
if [ -n "${BLOCKLIST_ANALYZER_SHA256}" ]; then
	BLOCKLIST_ANALYZER_SHA256="$(conf_value BLOCKLIST_ANALYZER_SHA256)"
else
	del_conf BLOCKLIST_ANALYZER_SHA256
fi
if ! conf_has_key BLOCKLIST_ANALYZER_SHA256; then
	write_conf BLOCKLIST_ANALYZER_SHA256 "\"${BLOCKLIST_ANALYZER_SHA256_DEFAULT}\""
	BLOCKLIST_ANALYZER_SHA256="${BLOCKLIST_ANALYZER_SHA256_DEFAULT}"
fi
if [ -z "${2:-}" ] && single_arg_menu_action "${1:-}"; then
	CHOSEN="${1}"
	BRANCH="$(conf_value INSTALLER_BRANCH)"
	if [ -n "${BRANCH}" ]; then
		BRANCH="$(sanitize_branch "${BRANCH}")"
	else
		BRANCH="master"
	fi
	set -- "${BRANCH}" "${CHOSEN}"
fi
case "${1}" in
	install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall)
		BRANCH="$(cli_installer_branch_from_args "$@" 2>/dev/null)"
		[ -n "${BRANCH}" ] || BRANCH="$(conf_value INSTALLER_BRANCH)"
		[ -n "${BRANCH}" ] && BRANCH="$(sanitize_branch "${BRANCH}")" || BRANCH="master"
		;;
	"amtmupdate")
		BRANCH="$(conf_value INSTALLER_BRANCH)"
		[ -n "${BRANCH}" ] && BRANCH="$(sanitize_branch "${BRANCH}")" || BRANCH="master"
		case "$2" in
			"")
				CHOSEN="${1#amtm}"
				set -- "$BRANCH" "$CHOSEN"
				;;
		esac
		;;
	"")
		BRANCH="master"
		;;
	*)
		BRANCH="${1}"
		;;
esac
BRANCH="$(sanitize_branch "${BRANCH}")"
write_conf INSTALLER_BRANCH "\"${BRANCH}\""
readonly BLOCKLIST_ANALYZER_SHA256
[ -z "$(nvram get odmpid)" ] && ROUTER_MODEL="$(nvram get productid)" || ROUTER_MODEL="$(nvram get odmpid)"
RURL="https://raw.githubusercontent.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer/${BRANCH}"
ROUTER_OS="$(/bin/uname)"
! PTXT "${ROUTER_MODEL}" | grep -vE "RT-AC(86|2900)" | grep -q "AC" && ROUTER_ARCH="$(/bin/uname -m)" || ROUTER_ARCH="armv7" # Exception for version ARMv5
WAN_IPADDR="$(nvram get wan_ipaddr 2>/dev/null)"
if ipv4_is_private "${WAN_IPADDR}"; then NAT_ENV="${WAN_IPADDR}"; else NAT_ENV=""; fi

[ -z "${ROUTER_MODEL}" ] && PTXT "${ERROR} This is an unsupported router, sorry." && sleep 3s && exit 1
[ "$(nvram get sw_mode)" != "1" ] && PTXT "${ERROR} You are not running in router mode, sorry." && sleep 3s && exit 1

if [ -n "${NAT_ENV}" ] && [ -z "$2" ]; then
	PTXT "${WARNING} This router is in a Double-Nat Environment. While for some users of AdGuardHome this may work fine," \
		"${WARNING} Consider this a warning that there is no guarentee that it will." \
		"${WARNING} The script developer cannot help in this scenario." \
		"${WARNING} However you are still welcome to use the script."
	sleep 3s
fi

if [ -z "$2" ]; then
	printf '\e[8;50;125t'
	printf '\033[?7l'
	clear_screen
	sed -n '2,21p' "$0"
	printf '\033[?7h'
fi

case "${ROUTER_MODEL}" in
	*)
		[ -z "$2" ] && PTXT "${INFO} Detected ${ROUTER_MODEL} router."
		;;
esac

case "${ROUTER_OS}" in
	"Linux")
		[ -z "$2" ] && PTXT "${INFO} Detected ${ROUTER_OS} platform."
		ROUTER_OS="linux"
		;;
	*)
		PTXT "${ERROR} This is an unsupported platform, sorry."
		exit 1
		;;
esac

case "${ROUTER_ARCH}" in
	"aarch64" | "arm64")
		ROUTER_ARCH="arm64"
		URL_ARCH="${RURL}/armv8"
		ADGUARD_ARCH="${ROUTER_OS}_${ROUTER_ARCH}"
		[ -z "$2" ] && PTXT "${INFO} Detected ARMv8 architecture."
		;;
	"armv7l")
		ROUTER_ARCH="armv7"
		URL_ARCH="${RURL}/${ROUTER_ARCH}"
		ADGUARD_ARCH="${ROUTER_OS}_${ROUTER_ARCH}"
		[ -z "$2" ] && PTXT "${INFO} Detected ARMv7 architecture."
		;;
	"armv7")
		ROUTER_ARCH="armv5"
		URL_ARCH="${RURL}/${ROUTER_ARCH}"
		ADGUARD_ARCH="${ROUTER_OS}_${ROUTER_ARCH}"
		[ -z "$2" ] && PTXT "${INFO} Detected ARMv7 architecture."
		;;
	*)
		PTXT "${ERROR} This is an unsupported architecture, sorry."
		exit 1
		;;
esac

case "${1:-}" in
	install | update)
		init_remote_adguard_metadata
		;;
	backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall) ;;
	*)
		init_remote_adguard_metadata
		;;
esac

case "${1:-}" in
	install | update | backup | restore | doctor | ipset | netcheck | dns-port-policy | performance | migrate-runtime-defaults | uninstall)
		cli_run "$@"
		CLI_STATUS="$?"
		if [ "${CLI_STATUS}" -ne 0 ]; then
			[ "${CLI_STATUS}" -eq 2 ] && cli_usage
			exit "${CLI_STATUS}"
		fi
		exit 0
		;;
	status)
		status "${BRANCH}"
		exit "$?"
		;;
esac

case "$2" in
	"")
		cleanup
		check_jffs_enabled
		check_dns_environment 0
		check_version
		menu
		;;
	*)
		if menu_action_allowed "$2"; then
			menu "$2"
		else
			PTXT "${INFO} Usage: sh installer {master|dev?|""} (1 - {install/update} | 2 - uninstall | 3 - changepw | 4 - reconfigure | 5 - setamtmupdate | 6 - setlocalcache | 7 - switchbranch | 8 - setipset | ${BLOCKLIST_ANALYZER_SHA256:+9 - unusedblocklists | }{b/B} - backup | {r/R} - restore | migrate-runtime-defaults | doctor | status)" \
				"${INFO} Doctor can also be run as: sh installer doctor [--fix]. Status can also be run as: sh installer status." \
				"${INFO} Branch or Tag must be represented by a value or empty quotes in place of string e.g. \"\" in the \$1 string position." \
				"${INFO} Action must be represented by a value (1 - {install/update} | 2 - uninstall | 3 - changepw | 4 - reconfigure | 5 - setamtmupdate | 6 - setlocalcache | 7 - switchbranch | 8 - setipset | ${BLOCKLIST_ANALYZER_SHA256:+9 - unusedblocklists | }{b/B} - backup | {r/R} - restore | migrate-runtime-defaults | doctor | status) in the \$2 string position." \
				"${INFO} An update example sh installer master update, or just use the regular menu by not specifying an Action."
		fi
		;;
esac
