#!/bin/sh
# shellcheck disable=SC3043,SC2018,SC3020,SC1090,SC3045,SC2155,SC2015,SC3057,SC3044,SC2016,SC3003,SC3060

# adblock-lean - powerful and ultra efficient adblocking with dnsmasq
# Project homepage: https://github.com/lynxthecat/adblock-lean
ABL_VERSION=dev
ABL_UPD_CHANNEL=release

# Authors: @Lynx and @Wizballs (OpenWrt forum)
# Contributors: @antonk; @dave14305 (OpenWrt forum)
# Alpine conversion: Gemini CLI

# global exit codes:
# 0 - Success
# 1 - Error
# 254 - Failed to acquire lock

LC_ALL=C
_NL_='
'
DEFAULT_IFS="	 ${_NL_}"
IFS="${DEFAULT_IFS}"

CONFIG_FORMAT=v11

if [ -z "${MSGS_DEST}" ]
then
	if [ -t 0 ]
	then
		export MSGS_DEST=/dev/tty
	else
		export MSGS_DEST=/dev/null
	fi
fi

ABL_RUN_DIR=/var/run/adblock-lean
export ABL_TMP_DIR=${ABL_RUN_DIR}/tmp
ABL_UPD_DIR=${ABL_TMP_DIR}/update
ABL_PID_DIR=/run/adblock-lean

ABL_LIB_DIR=/usr/lib/adblock-lean
ABL_CONFIG_DIR=/etc/adblock-lean
ABL_CONF_STAGING_DIR="/tmp/abl-conf-staging"

ABL_SERVICE_PATH=/usr/bin/adblock-lean

ABL_FILE_TYPES="GEN LIB EXTRA"

ABL_GEN_FILES="${ABL_SERVICE_PATH}"
ABL_LIB_FILES="${ABL_LIB_DIR}/abl-lib.sh
	${ABL_LIB_DIR}/abl-process.sh"
ABL_EXTRA_FILES="" # may be useful in the future
ABL_EXEC_FILES="${ABL_SERVICE_PATH}"

ABL_CONFIG_FILE=${ABL_CONFIG_DIR}/config

PID_FILE="${ABL_PID_DIR}/adblock-lean.pid"
ACTION_FILE="${ABL_PID_DIR}/adblock-lean.action"

ABL_SESSION_LOG_FILE=/var/log/abl_session.log

ABL_CRON_CMD="/usr/bin/adblock-lean start"

SHARED_BLOCKLIST_PATH=${ABL_RUN_DIR}/abl-blocklist

SCHEDULER_PID=

export PATH=/usr/sbin:/usr/bin:/sbin:/bin

EXTRA_COMMANDS="setup version status pause resume gen_stats select_dnsmasq_instances gen_config upd_cron_job print_log calculate_limits"
EXTRA_HELP="
adblock-lean custom commands:
	version                   print adblock-lean version
	setup                     run automated setup for adblock-lean
	status                    check dnsmasq and entries count of existing blocklist
	pause                     pause adblock-lean
	resume                    resume adblock-lean
	gen_stats                 generate dnsmasq stats for system log
	select_dnsmasq_instances  analyze dnsmasq instances and set dnsmasq conf-dir
	gen_config                generate default config based on one of the pre-defined presets
	upd_cron_job              create cron job for adblock-lean with schedule set in the config option 'cron_schedule'.
	                          if config option set to 'disable', remove existing cron job if any
	print_log                 print most recent session log
	calculate_limits          calculate recommended values for max_ and min_ config options based on target entries count"


# silence shellcheck warnings
: "${action:=}" "${EXTRA_COMMANDS}" "${EXTRA_HELP}" "${boot_start_delay_s:=}"
: "${purple}" "${green}" "${red}" "${yellow}" "${TAB}" "${CR}" "${CR_LF}"
: "${AWK_CMD}" "${SORT_CMD}"
: "${ABL_GEN_FILES}" "${ABL_LIB_FILES}" "${ABL_EXTRA_FILES}" "${ABL_EXEC_FILES}" "${ABL_CONFIG_FILE}" "${CONFIG_FORMAT}"
: "${SHARED_BLOCKLIST_PATH}"


### UTILITY FUNCTIONS

# check if var names are safe to use with eval
are_var_names_safe() {
	local var_name
	for var_name in "${@}"
	do
		case "${var_name}" in [!a-zA-Z_]*|*[!a-zA-Z0-9_]*) reg_failure "Invalid var name '${var_name}'."; return 1; esac
	done
	:
}

check_func()
{
	check_util "${1}" && [ "$(type "${1}" 2>/dev/null | head -n1)" = "${1} is a function" ]
}

check_util()
{
	command -v "${1}" 1>/dev/null
}

# sets global variables for colors, tab delimiter and cr_lf
set_ansi()
{
	local IFS=" "
	# shellcheck disable=SC2046
	set -- $(printf '\033[0;31m \033[0;32m \033[1;34m \033[1;33m \033[0;35m \033[0m \35 \t \r')
	red="${1}" green="${2}" blue="${3}" yellow="${4}" purple="${5}" n_c="${6}" _DELIM_="${7}" TAB="${8}" CR="${9}" CR_LF="${9}${_NL_}"
}

# checks if string $1 is included in newline-separated list $2
# if $3 is specified, uses the value as list delimiter
# result via return status
is_included() {
	local delim="${3:-"${_NL_}"}"
	case "$2" in
		"$1"|"$1${delim}"*|*"${delim}$1"|*"${delim}$1${delim}"*)
			return 0 ;;
		*)
			return 1
	esac
}

# adds a string to a newline-separated list if it's not included yet
# 1 - name of var which contains the list
# 2 - new value
# 3 - (optional) list delimiter (instead of newline)
# returns 1 if bad var name, 0 otherwise
add2list() {
	case "${1}" in *[!A-Za-z0-9_]*)
		return 1
	esac

	local IFS="${DEFAULT_IFS}"
	local curr_list delim="${3:-"${_NL_}"}" fs=
	eval "curr_list=\"\${${1}}\""
	is_included "${2}" "${curr_list}" "${delim}" && return 0
	case "${curr_list}" in
		'') fs='' ;;
		*) fs="${delim}" ;;
	esac
	eval "${1}=\"\${${1}}${fs}${2}\""
	:
}

# reads first line from file into variables
# Args:
# -v <out_var_name>
# -f <file_path>
# Optional: '-a <attempts_num>'
# Optional: '-d' to delete the file after reading
# Optional: '-q' to quiet error message
# Optional: '-n X' to limit number of bytes read
# Optional: '-D <"[file_desc]">'
# Optional: '-V <"[default_val]">'
read_str_from_file()
{
	local me=read_str_from_file rs_del_file='' rs_quiet='' rs_num_bytes='' rs_outvars='' rs_file='' \
		rs_file_desc='' rs_def_val='' rs_read_failed='' rs_attempts=1 rs_attempt rs_var
	while getopts ":dqn:v:f:a:D:V:" opt
	do
		case "${opt}" in
			d) rs_del_file=1 ;;
			q) rs_quiet=1 ;;
			n) rs_num_bytes=-n${OPTARG} ;;
			v) rs_outvars=${OPTARG} ;;
			f) rs_file=${OPTARG} ;;
			a) rs_attempts=${OPTARG} ;;
			D) rs_file_desc=${OPTARG} ;;
			V) rs_def_val=${OPTARG} ;;
			*) reg_failure "${me}: unexpected option '${opt}'."; return 1;
		esac
	done

	[ -n "${rs_outvars}" ] && [ -n "${rs_file}" ] || { reg_failure "${me}: missing args"; return 1; }

	for rs_var in ${rs_outvars}
	do
		eval "${rs_var}="
	done

	rs_attempt=0
	while :
	do
		rs_attempt=$((rs_attempt+1))
		[ "${rs_attempt}" -le "${rs_attempts}" ] || { rs_read_failed=1; break; }
		[ "${rs_attempt}" = 1 ] || sleep 1

		read -r ${rs_num_bytes?} ${rs_outvars?} 2>/dev/null < "${rs_file}"
		for rs_var in ${rs_outvars}
		do
			case "${rs_var}" in _) continue; esac
			eval "[ -n \"\${${rs_var}}\" ]" || continue 2
		done
		break
	done

	[ -n "${rs_del_file}" ] && rm -f "${rs_file}"
	[ -z "${rs_read_failed}" ] && return 0

	for rs_var in ${rs_outvars}
	do
		eval "${rs_var}=\"${rs_def_val}\""
	done

	[ -n "${rs_quiet}" ] || reg_failure "Failed to read${rs_file_desc:+ }${rs_file_desc} file '${rs_file}'."

	return 1
}

# 0 - (optional) '-p'
# 1 - path
try_mkdir()
{
	local p=
	[ "${1}" = '-p' ] && { p='-p'; shift; }
	[ -d "${1}" ] && return 0
	mkdir ${p} "${1}" || { reg_failure "Failed to create directory '${1}'."; return 1; }
	:
}

# asks the user to pick an option
# 1 - input in the format 'a|b|c'
# output via $REPLY
pick_opt()
{
	update_action_file "Waiting for user input in console" || return 1
	while :
	do
		printf %s "$1: " > "${MSGS_DEST}"
		read -r REPLY
		case "$REPLY" in *[!A-Za-z0-9_]*) printf '\n%s\n\n' "Please enter $1" > "${MSGS_DEST}"; continue; esac
		eval "case \"$REPLY\" in 
				$1) return 0 ;;
				*) printf '\n%s\n\n' \"Please enter $1\" > \"${MSGS_DEST}\"
			esac"
	done
}


### HELPER FUNCTIONS

rc_disable()
{
	rc-update del adblock-lean default
}

rc_enable()
{
	rc-update add adblock-lean default
}

rc_enabled()
{
	rc-service adblock-lean status >/dev/null 2>&1
}

detect_util()
{
	unexp_arg() { reg_failure "${me}: unexpected argument '${1}'."; }

	local du_out_var="${1}" gen_name="${2}" specific_name="${3}" specific_path="${4}" \
		res_cmd='' res_name='' _util _util_path='' busybox_variant_avail='' skip_links='' path_exists='' path_is_symlink='' \
		opt me=detect_util

	[ ${#} -ge 4 ] || { reg_failure "${me}: not enough arguments"; return 1; }
	shift 4

	while getopts :bs opt
	do
		case "${opt}" in
			b) busybox_variant_avail=1 ;;
			s) skip_links=1 ;;
			*) unexp_arg "${opt}"; return 1;
		esac
	done
	shift $((OPTIND-1))
	[ -z "${*}" ] || { unexp_arg "${*}"; return 1; }

	are_var_names_safe "${du_out_var}" || return 1

	if [ -n "${specific_path}" ] && [ -f "${specific_path}" ]
	then
		_util_path="${specific_path}" path_exists=1
	elif
		{ [ -n "${specific_name}" ] && check_util "${specific_name}" && res_name="${specific_name}"; } ||
		{ [ -n "${gen_name}" ] && check_util "${gen_name}" && res_name="${gen_name}"; }
	then
		_util_path="$(command -v "${res_name}")"
	fi

	if [ -n "${_util_path}" ] &&
		{
			[ -L "${_util_path}" ] && path_is_symlink=1
			if [ -n "${path_is_symlink}" ] && [ -z "${skip_links}" ]
			then
				# resolve symlinks
				_util_path="$(readlink "${_util_path}")" &&
				case "${_util_path}" in
					busybox|/bin/busybox) _util_path="/bin/busybox ${gen_name}" ;;
					''|/) false ;;
					/*) : ;;
					*) util_path="$(command -v "${util_path}")" &&
						case "${util_path}" in
							''|/) false ;;
							/*) : ;;
							*) false
						esac
				esac
			elif [ -z "${path_is_symlink}" ] && { [ -n "${path_exists}" ] || [ -f "${_util_path}" ]; }
			then
				# path is not symlink
				:
			else
				false
			fi
		}
	then
		res_cmd="${_util_path}"
	elif [ -n "${busybox_variant_avail}" ]
	then
		res_cmd="/bin/busybox ${gen_name}"
	else
		reg_failure "${me}: '${gen_name:-"${specific_name}"}' not found."
		return 1
	fi

	export "${du_out_var}=${res_cmd}"
}

# 1 (optional) : '-f' to force re-detection
# sets $AWK_CMD, $SED_CMD, $SORT_CMD
# shellcheck disable=SC2120
detect_main_utils()
{
	[ -n "${MAIN_UTILS_DETECTED}" ] && [ "${1}" != '-f' ] && return 0
	unset SED_CMD AWK_CMD SORT_CMD
	local failed_utils=''

	detect_util SED_CMD sed "" "" -b -s || add2list failed_utils sed ', '
	detect_util AWK_CMD awk gawk "" -b -s || add2list failed_utils awk ', '
	detect_util SORT_CMD sort "" "" -b -s || add2list failed_utils sort ', '

	[ -z "${failed_utils}" ] || { reg_failure "Can not find essential utilities: ${failed_utils}.";  return 1; }
	export MAIN_UTILS_DETECTED=1
	:
}

# 1: (optional) '-[color]'
# prints each argument into a separate line
print_msg()
{
	local m color=
	case "${1}" in -blue|-red|-green|-purple|-yellow) eval "color=\"\${${1#-}}\""; shift; esac
	for m in "${@}"
	do
		printf '%s\n' "${color}${m}${n_c}" > "$MSGS_DEST"
	done
}

# logs each message argument separately and prints to a separate line
# optional arguments: '-noprint', '-nolog', '-err', '-warn', '-[color]'
log_msg()
{
	local m msgs='' msgs_prefix='' _arg err_l=info color='' noprint='' nolog=''

	local IFS="${DEFAULT_IFS}"
	for _arg in "$@"
	do
		case "${_arg}" in
			"-noprint") noprint=1 ;;
			"-nolog") nolog=1 ;;
			"-err") err_l=err color="-red" msgs_prefix="Error: " ;;
			"-warn") err_l=warn color="-yellow" msgs_prefix="Warning: " ;;
			-blue|-red|-green|-purple|-yellow) color="${_arg}" ;;
			'') msgs="${msgs}dummy${_DELIM_}" ;;
			*) msgs="${msgs}${msgs_prefix}${_arg}${_DELIM_}"; [ -n "${msgs_prefix}" ] && msgs_prefix=
		esac
	done
	msgs="${msgs%"${_DELIM_}"}"
	IFS="${_DELIM_}"

	for m in ${msgs}
	do
		IFS="${DEFAULT_IFS}"
		case "${m}" in
			dummy) printf '\n' > "${MSGS_DEST}" ;;
			*)
				[ -z "${noprint}" ] && print_msg ${color} "${m}"
				[ -z "${nolog}" ] && logger -t adblock-lean -p user."${err_l}" "${m}"
				write_log_file "${m}" "${err_l}"
		esac
	done
	:
}

# Prints msg to console and to log file, doesn't send to system log unless ${ABL_DEBUG_LOG} is set
reg_msg()
{
	local nolog='-nolog'
	[ -n "${ABL_DEBUG_LOG}" ] && nolog=''
	log_msg ${nolog} "${@}"
}

# 1 - msg
# 2 - err level
write_log_file()
{
	[ -n "${LOG_FILE}" ] && date +"[%b %d %Y, %H:%M:%S] ${2:-info}: ${1}" >> "${LOG_FILE}" &
}

graceful_exit()
{
	exit "${1:-0}"
}

# exit with code ${1}
cleanup_and_exit()
{
	trap - INT TERM EXIT
	if [ -n "${CLEANUP_REQ}" ]
	then
		[ "${1}" != 0 ] && print_msg "" "Cleaning up..."
		[ -n "${SCHEDULER_PID}" ] && kill -s USR1 "${SCHEDULER_PID}" 2>/dev/null
		rm -rf "${ABL_TMP_DIR}"
	fi

	if [ -n "${FAIL_STOP_REQ}" ]
	then
		ABL_NOTRAPS=1 stop "${1}" -noexit
		rm -rf "${ABL_RUN_DIR}"
	fi

	rm -rf "${ABL_CONF_STAGING_DIR}"

	[ -n "${LOCK_REQ}" ] && rm_lock
	exit "${1}"
}

# shellcheck disable=SC2120
# 1 - (optional) '-nostop' to not call stop on failure
restart_dnsmasq()
{
	reg_action -nolog -blue "Restarting dnsmasq." || return 1

	rc-service dnsmasq restart > /dev/null 2>&1 ||
	{
		reg_failure "Failed to restart dnsmasq."
		[ "${ABL_CMD}" != stop ] && [ "${1}" != '-nostop' ] && stop 1 -noexit
		return 1
	}

	reg_action -nolog -blue "Waiting for dnsmasq initialization." || return 1
	local dnsmasq_ok=
	for i in $(seq 1 30)
	do
		if pidof dnsmasq > /dev/null 2>&1
		then
			# if process is up, try a lookup but don't fail immediately if it doesn't work yet
			nslookup google.com 127.0.0.1 "${DNSMASQ_PORT:-53}" > /dev/null 2>&1 && { dnsmasq_ok=1; break; }
		fi
		sleep 1;
	done

	[ -n "$dnsmasq_ok" ] ||
	{
		if pidof dnsmasq > /dev/null 2>&1
		then
			reg_msg -yellow "dnsmasq is running but local DNS resolution check failed. Proceeding anyway."
			dnsmasq_ok=1
		else
			reg_failure "dnsmasq initialization failed (process not found)."
			[ "${ABL_CMD}" != stop ] && [ "${1}" != '-nostop' ] && stop 1 -noexit
			return 1
		fi
	}

	reg_msg -green "Restart of dnsmasq completed."
	:
}

#
# return codes:
# 0 - success
# 1 - error
# 254 - lock file already exists
mk_lock()
{
	local me=mk_lock
	check_lock
	case ${?} in
		1) return 1 ;;
		2)
			report_curr_action -log
			log_msg -yellow "Refusing to open another instance."
			return 254
	esac

	[ -z "${PID_FILE}" ] && { reg_failure "${me}: \${PID_FILE} variable is unset."; return 1; }

	try_mkdir -p "${ABL_PID_DIR}" || return 1
	printf '%s\n' "${$}" > "${PID_FILE}" || { reg_failure "${me}: Failed to write to pid file '${PID_FILE}'."; return 1; }
	:
}

# assigns lock pid to $LOCK_PID
# return codes:
# 0 - no lock
# 1 - error
# 2 - lock file exists and belongs to another PID
# 3 - lock file belongs to current PID
check_lock()
{
	unset LOCK_PID
	[ -z "${PID_FILE}" ] && { reg_failure "\${PID_FILE} variable is unset."; return 1; }
	[ ! -f "${PID_FILE}" ] && return 0

	read_str_from_file -v "LOCK_PID _" -f "${PID_FILE}" -a 3 -D PID || return 1

	case "${LOCK_PID}" in
		"${$}") return 3 ;;
		*[!0-9]*) reg_failure "pid file '${PID_FILE}' contains unexpected string '${LOCK_PID}'."; unset LOCK_PID; return 1 ;;
		*) kill -0 "${LOCK_PID}" 2>/dev/null && return 2
	esac

	log_msg -warn "Detected stale pid file '${PID_FILE}' for PID ${LOCK_PID}. Removing."
	rm_lock || return 1
	:
}

rm_lock()
{
	rm -f "${PID_FILE}" || { reg_failure "Failed to delete the pid file '${PID_FILE}'."; return 1; }
	rm -rf "${ABL_PID_DIR}"
	LOCK_PID=
	:
}

# updates the pid file with a new action
# 1 - new action
update_action_file()
{
	local me="update_action_file"
	[ -z "${1%.}" ] && { reg_failure "${me}: action is unspecified."; return 1; }

	{
		try_mkdir -p "${ABL_PID_DIR}" &&
		printf '%s\n' "${1%.}" > "${ACTION_FILE}" || reg_failure "${me}: Failed to write to action file '${ACTION_FILE}'."
	} & # runs asynchronously
	:
}

# 1 (optional): '-log' to log current action as well
report_curr_action()
{
	[ "${MSGS_DEST}" = "/dev/null" ] && [ "${1}" != "-log" ] && return 0
	local reported_pid report_cmd=print_msg curr_action=''
	[ -n "${ACTION_FILE}" ] || { reg_failure "\$ACTION_FILE var is unset."; return 1; }
	[ -f "${ACTION_FILE}" ] || return 0
	if [ -z "${LOCK_PID}" ]
	then
		check_lock
		case ${?} in
			0) return 0 ;;
			1) return 1 ;;
			2|3) ;;
		esac
	fi
	reported_pid="${LOCK_PID:-unknown}"

	read_str_from_file -v curr_action -f "${ACTION_FILE}" -a 2 -D action -V "unknown action"

	[ "${1}" = -log ] && report_cmd=log_msg

	${report_cmd} "adblock-lean (PID: ${reported_pid}) is performing action '${curr_action}'."
	:
}

# (optional) -nolog
# (optional) -[color]
# other args - action
reg_action()
{
	local arg msg='' nolog='' color=''
	for arg in "$@"
	do
		case "${arg}" in
			-nolog) nolog="-nolog" ;;
			-blue|-red|-green|-purple|-yellow) color="${arg}" ;;
			*) msg="${msg}${arg} "
		esac
	done

	log_msg "" ${color} ${nolog} "${msg% }"
	if [ -n "${LOCK_REQ}" ]
	then
		update_action_file "${msg% }" || return 1
	fi
	:
}

reg_failure()
{
	log_msg -err "" "${@}"
	for arg in "${@}"; do
		failure_msg="${failure_msg:+"${failure_msg}${_NL_}"}${arg}"
	done
}

reg_success()
{
	log_msg -noprint "${1}"
	if [ -n "${custom_scr_sourced}" ] && check_func report_success
	then
		report_success "${1}"
	fi
}

# kills any running adblock-lean instances
kill_abl_pids()
{
	local abl_pids
	check_lock
	if [ ${?} = 2 ]
	then
		kill "${LOCK_PID}" 2>/dev/null
	else
		# if PID file doesn't exist, check for running abl processes just in case
		local pgrep_ptrn="(^((sh|/bin/sh)\s+){0,1}/usr/bin/)adblock-lean"
		abl_pids="$(pgrep -fa "${pgrep_ptrn}" | ${SED_CMD} -E "/\sstop$/d;s/\s+.*//;/^${$}$/d" | tr '\n' ' ')"
		[ -n "${abl_pids}" ] && kill_pids_recursive "${abl_pids}"
	fi

	# wait for adblock-lean instance to exit
	local i=0
	while [ -f "${PID_FILE}" ] && [ ${i} -lt 10 ]
	do
		check_lock
		[ ${?} != 2 ] && break
		sleep 1
		i=$((i+1))
	done

	:
}

# kills specified pid's and their offspring
# 1 - whitespace-separated starting list of pid's
# 2 - pid's to exclude
kill_pids_recursive()
{
	# recursively add child pid's of pid $2 to whitespace-separated list stored in var $1
	# 1 - var name for output
	# 2 - pid
	add_child_pids()
	{
		local pid_scan_depth pid prev_pids child_pids
		: "${pid_scan_depth:=0}"
		pid_scan_depth=$((pid_scan_depth+1))
		[ "${pid_scan_depth}" -lt "${max_pid_scan_depth}" ] || return 0

		child_pids="$(
			pgrep -faP "${2}" |
			# exclude the dnsmasq processes and service calls from results
			${SED_CMD} -E \
				'/(^[0-9]+\s+((sh|\/bin\/sh)\s+){0,1}(\/sbin\/service\s+|\/etc\/rc.common\s+\/etc\/init.d\/){0,1}(\/usr\/sbin\/|\/sbin\/service\s){0,1}dnsmasq|\/sbin\/ujail)\s/d;
				s/\s.*//'
		)" || return 0

		eval "prev_pids=\"\${${1}}\""

		local IFS="${_NL_}"
		for pid in ${child_pids}
		do
			IFS="${DEFAULT_IFS}"
			is_included "${pid}" "${prev_pids}" " " || eval "${1}=\"\${${1}}\${pid} \""
			add_child_pids "${1}" "${pid}"
		done
	}

	local initial_pids='' exclude_pids="${2}" pid max_pid_scan_depth=10 max_k_attempts=10

	# compile a list of initial pids and recursively child pids
	for pid in ${1}
	do
		case "${pid}" in *[!0-9]*) continue; esac
		is_included "${pid}" "${exclude_pids}" " " && continue

		initial_pids="${initial_pids}${pid} "
		add_child_pids initial_pids "${pid}"
	done
	[ -n "${initial_pids}" ] || return 0

	kill "${initial_pids}" 2>/dev/null
	local running_pids="${initial_pids}" k_attempt=0
	while :
	do
		k_attempt=$((k_attempt+1))
		[ ${k_attempt} -le ${max_k_attempts} ] || break

		for pid in ${running_pids}
		do
			add_child_pids running_pids "${pid}"
		done

		kill "${running_pids}" 2>/dev/null

		local alive_pids=''
		for pid in ${running_pids}
		do
			[ -d "/proc/${pid}" ] && alive_pids="${alive_pids}${pid} "
		done
		running_pids="${alive_pids}"
		[ -n "${running_pids}" ] || return 0

		sleep 1
	done
	kill -9 "${running_pids}" 2>/dev/null
	:
}

# return codes:
# 0 - running
# 1 - error
# 2 - (reserved)
# 3 - paused
# 4 - stopped
get_abl_run_state()
{
	[ -n "${DNSMASQ_CONF_DIRS}" ] || { reg_failure "\$DNSMASQ_CONF_DIRS is not set. Failed to check adblock-lean run state."; return 1; }
	local f dir file_exists=0 file_missing=0

	for dir in "${ABL_RUN_DIR}" ${DNSMASQ_CONF_DIRS}
	do
		for f in "${dir}/abl-blocklist"* "${dir}/.abl-blocklist"*
		do
			case "${f}" in ''|*"*") continue; esac
			file_exists=1
			continue 2
		done
		file_missing=1
	done

	case "${file_exists}${file_missing}" in
		11) return 0 ;;
		10) reg_failure "Invalid run state."; return 1
	esac

	for f in "${ABL_RUN_DIR}/prev_blocklist"*
	do
		case "${f}" in ''|*"*") continue; esac
		return 3
	done

	return 4
}

source_libs()
{
	[ -n "${LIBS_SOURCED}" ] && return 0

	local file libs_missing='' libs_source_failed=''

	# source libs
	for file in ${ABL_LIB_FILES}
	do
		file="${file##*/}"
		[ -f "${ABL_LIB_DIR}/${file}" ] || { libs_source_failed=1 libs_missing=1; break; }
		# shellcheck source=/dev/null
		. "${ABL_LIB_DIR}/${file}" || { libs_source_failed=1; break; }
	done

	[ -n "${libs_source_failed}" ] &&
	{
		[ -n "${libs_missing}" ] && reg_failure "Missing library scripts."
		reg_failure "Failed to source library scripts."
		return 1
	}
	LIBS_SOURCED=1
	:
}

# args: 1 - (optional) schedule
# return codes:
# 0 - cron job with same schedule exists
# 1 - error
# 2 - cron job with a different schedule exists
# 3 - cron job doesn't exist
get_curr_crontab()
{
	local curr_cron
	curr_cron="$(crontab -u root -l 2>/dev/null)" || { reg_failure "get_curr_crontab: Failed to read crontab."; return 1; }
	printf '%s\n' "${curr_cron}"

	# check if adblock-lean cron job with same schedule exists
	case "${curr_cron}" in
		*"${cron_schedule}"*"${ABL_CRON_CMD}"*) return 0 ;;
		*"${ABL_CRON_CMD}"*) return 2 ;;
		*) return 3 ;;
	esac
}

rm_cron_job()
{
	local curr_cron
	crontab -u root -l 1>/dev/null 2>/dev/null || return 0
	curr_cron="$(get_curr_crontab)"
	case ${?} in
		1) return 1 ;;
		3) return 0
	esac

	log_msg -purple "" "Removing cron job for adblock-lean."
	printf '%s\n' "${curr_cron}" | ${SED_CMD} '/adblock-lean start/d;/^$/d' | crontab -u root - ||
		{ reg_failure "Failed to update crontab."; return 1; }
	:
}



### Commands init
init_command()
{
	ABL_CMD="${1}"
	local config_req='' work_dir_req='' init_action_msg='' process_vars_req=''

	[ -z "${DO_DIALOGS}" ] && [ -z "${APPROVE_UPD_CHANGES}" ] && [ "${MSGS_DEST}" = "/dev/tty" ] && \
		DO_DIALOGS=1

	if [ -z "${ABL_NOTRAPS}" ]
	then
		if [ -n "${UPD_SOURCED}" ]
		then
			trap 'exit 1' INT TERM
		else
			trap 'cleanup_and_exit 1' INT TERM
			trap 'cleanup_and_exit ${?}' EXIT
		fi
	fi

	# set requirements
	case ${ABL_CMD} in
		help|enabled|enable|disable|print_log|'') ;;
		gen_stats) ;;
		status) libs_req=1 work_dir_req=1 config_req=1 process_vars_req=1 ;;
		upd_cron_job) libs_req=1 config_req=1 ;;
		setup|gen_config) libs_req=1 LOCK_REQ=1 ;;
		boot) libs_req=1 config_req=1 LOCK_REQ=1 ;;
		pause) libs_req=1 work_dir_req=1 config_req=1 process_vars_req=1 LOCK_REQ=1 ;;
		start|resume) libs_req=1 work_dir_req=1 config_req=1 process_vars_req=1 CLEANUP_REQ=1 LOCK_REQ=1 ;;
		stop)
			init_action_msg="Stopping adblock-lean."
			reg_action -purple "${init_action_msg}" || exit 1

			source_libs || exit 1
			kill_abl_pids
			check_lock
			case ${?} in
				1) exit 1 ;;
				2) reg_failure "Failed to kill running adblock-lean processes."; exit 1
			esac
			load_config
			CLEANUP_REQ=1 LOCK_REQ=1 ;;
		select_dnsmasq_instances)
			libs_req=1 LOCK_REQ=1
			[ "${action}" = select_dnsmasq_instances ] && config_req=1 # require config when called directly
			;;
		reload|restart) reg_action -purple "Restarting adblock-lean." || exit 1 ;;
		update) work_dir_req=1 CLEANUP_REQ=1 LOCK_REQ=1 ;;
		*) reg_failure "Invalid action '${ABL_CMD}'."; exit 1
	esac

	[ -n "${process_vars_req}" ] && libs_req=1 # ensure that libs are loaded

	# source library scripts
	if [ -n "${libs_req}" ]
	then
		source_libs || exit 1
	fi

	# report installed utils
	if [ "${ABL_CMD}" = start ]
	then
		detect_pkg_manager
		report_utils
	fi

	# register lock status at init
	check_lock
	local init_lock_status=${?}

	# make lock if needed
	if [ -n "${LOCK_REQ}" ]
	then
		mk_lock || { local rv=${?}; unset LOCK_REQ CLEANUP_REQ; exit ${rv}; }
		update_action_file "${ABL_CMD}"
	fi

	# enable writing session log if we have the lock
	LOG_FILE=
	case ${init_lock_status} in 0|3)
		LOG_FILE="${ABL_SESSION_LOG_FILE}"
		[ "${ABL_CMD}" = update ] && LOG_FILE="${ABL_UPDATE_LOG_FILE}"
	esac

	# if creating new session, rotate the old session log file
	[ "${init_lock_status}" = 0 ] && [ -n "${LOG_FILE}" ] && [ -f "${LOG_FILE}" ] && mv "${LOG_FILE}" "${LOG_FILE}.0"

	[ -n "${init_action_msg}" ] && write_log_file "${init_action_msg}" "info"

	[ -n "${work_dir_req}" ] && { try_mkdir -p "${ABL_TMP_DIR}" || exit 1; }

	if [ -n "${config_req}" ] && [ -z "${CONFIG_LOADED}" ]
	then
		load_config || { reg_failure "Failed to load config."; exit 1; }
		CONFIG_LOADED=1
		export DNSMASQ_PORT
	fi

	# detect compression/extraction utils
	[ -n "${process_vars_req}" ] && { detect_processing_utils && set_processing_vars || exit 1; }

	# check dnsmasq, source custom script
	case ${ABL_CMD} in
		start|pause|resume)
			check_dnsmasq_instances || exit 1
			if [ -n "${custom_script}" ]
			then
				custom_scr_sourced=
				# shellcheck source=/dev/null
				[ -f "${custom_script}" ] && . "${custom_script}" && custom_scr_sourced=1 ||
					reg_failure "Custom script '${custom_script}' doesn't exist or it returned an error."
			fi
	esac

	:
}

# shellcheck disable=SC2120
# get config format from config or main script file contents
# input via STDIN or ${1}
get_config_format()
{
	local conf_form_sed_expr='/^[ \t]*(CONFIG_FORMAT|#[ \t]*config_format)=v/{s/.*=v//;p;:1 n;b1;}'
	if [ -n "${1}" ]
	then
		$SED_CMD -En "${conf_form_sed_expr}" "${1}"
	else
		$SED_CMD -En "${conf_form_sed_expr}"
	fi
}

# 1 - <-s|-h> to output as [s]econds since epoch or [h]uman-readable
get_blocklist_timestamp()
{
	local dir file date_fmt_str me=get_blocklist_timestamp
	case "${1}" in
		-s) date_fmt_str='+%s' ;;
		-h) date_fmt_str='+%b %d %Y, %H:%M:%S' ;;
		*) reg_failure "${me}: unexpected format '${1}'."; return 1
	esac

	for dir in "${ABL_RUN_DIR}" ${DNSMASQ_CONF_DIRS}
	do
		for file in "${dir}/abl-blocklist"*
		do
			case "${file}" in ''|*"*") continue; esac
			date -r "${file}" "${date_fmt_str}" && return 0
			reg_failure "${me}: Failed to get the timestamp of file '${file}'."
			return 1
		done
	done

	reg_failure "${me}: no blocklist file found."
	return 1
}


### MAIN COMMAND FUNCTIONS

version()
{
	print_msg "adblock-lean version: '${ABL_VERSION}', update channel: '${ABL_UPD_CHANNEL}'."
}

gen_config()
{
	init_command gen_config &&
	do_gen_config
}

setup()
{
	init_command setup &&
	do_setup
}

print_log()
{
	[ ! -s "${ABL_SESSION_LOG_FILE}" ] && { log_msg -err "Session log file '${ABL_SESSION_LOG_FILE}' doesn't exist or is empty."; exit 1; }
	echo "Most recent session log:"
	cat "${ABL_SESSION_LOG_FILE}"
	:
}

upd_cron_job()
{
	local me="upd_cron_job" curr_cron cron_line

	log_msg -purple "" "Updating cron job for adblock-lean."

	init_command upd_cron_job || return 1

	case "${cron_schedule}" in
		'') reg_failure "${me}: the \$cron_schedule variable is unset."; return 1 ;;
		disable)
			reg_msg -yellow "cron_schedule is set to 'disable' in config."
			rm_cron_job
			return 0
	esac

	enable_cron_service || return 1
	curr_cron="$(get_curr_crontab)"
	case ${?} in
		0) print_msg -green "Cron job for adblock-lean with schedule '${cron_schedule}' aldready exists."; return 0 ;;
		1) return 1 ;;
		2) curr_cron="$(printf %s "${curr_cron}" | $SED_CMD "s~^.*${ABL_CRON_CMD}.*\$~~")" ;; # remove cron job with a different schedule
		3) ;; # no adblock-lean cron job exists
	esac

	cron_line="${cron_schedule} RANDOM_DELAY=1 ${ABL_CRON_CMD} 1>/dev/null"

	#### Create new cron job
	print_msg -blue "Creating cron job with schedule '${blue}${cron_schedule}${n_c}'."
	printf '%s\n' "${curr_cron}${_NL_}${cron_line}" | $SED_CMD '/^$/d' | crontab -u root - ||
		{ reg_failure "Failed to update crontab."; return 1; }
	:
}

select_dnsmasq_instances() {
	init_command select_dnsmasq_instances &&
	do_select_dnsmasq_instances "${@}"
}

# 1 - (optional) '-noexit' to return to the calling function
gen_stats()
{
	source_libs &&
	reg_action -nolog -blue "Generating dnsmasq stats." || exit 1
	local dnsmasq_pid
	dnsmasq_pid="$(pidof /usr/sbin/dnsmasq)" || { reg_failure "Failed to detect dnsmasq PID or dnsmasq is not running."; exit 1; }
	kill -USR1 "${dnsmasq_pid}"
	print_msg "dnsmasq stats available for reading using 'logread'."
	[ "${1}" != '-noexit' ] && exit 0
	:
}

boot()
{
	init_command boot || exit 1
	reg_action -purple "Sleeping for ${boot_start_delay_s} seconds."
	sleep "${boot_start_delay_s}"
	start "$@"
}

start()
{
	reg_action -purple "Starting adblock-lean, version ${ABL_VERSION}."
	init_command start || exit 1

	if [ "${RANDOM_DELAY}" = "1" ]
	then
		random_delay_mins=$(($(hexdump -n 1 -e '"%u"' </dev/urandom)%60))
		reg_action -purple "Delaying adblock-lean by: ${random_delay_mins} minutes (thundering herd prevention)." || exit 1
		sleep "${random_delay_mins}m"
	fi

	local rv=1

	export_blocklist
	if [ ${?} != 1 ]
	then
		if FAIL_STOP_REQ=1 gen_and_process_blocklist
		then
			rv=0
		else
			reg_failure "Failed to generate new blocklist."
			local restore_failed=1
			if restore_saved_blocklist
			then
				if check_active_blocklist
				then
					log_msg -green "Previous blocklist restored and dnsmasq check passed."
					restore_failed=
				else
					reg_failure "Active blocklist check failed with previous blocklist file."
				fi
			fi
			[ -n "${restore_failed}" ] && stop 1 -noexit
		fi
	fi

	exit ${rv}
}

# 1 - (optional) exit code
# 1/2 - (optional) '-noexit' to return to the calling function
stop()
{
	local stop_rc=0 noexit=
	for _arg in "$@"
	do
		case "${_arg}" in
			"-noexit") noexit=1 ;;
			*[!0-9]*|'') ;;
			*) stop_rc="${_arg}"
		esac
	done
	msg="${msg% }"

	init_command stop || { FAIL_STOP_REQ=''; exit 1; }

	reg_msg "" "Removing any adblock-lean blocklist files."
	rm -f "${ABL_RUN_DIR}/prev_blocklist"*
	clean_dnsmasq_dir || stop_rc=1
	restart_dnsmasq -nostop &&
	log_msg -purple "" "Stopped adblock-lean." || stop_rc=1
	[ -n "$noexit" ] && return "${stop_rc}"
	FAIL_STOP_REQ=
	exit "${stop_rc}"
}

restart()
{
	init_command restart || exit 1
	stop -noexit || exit 1
	start
}

reload()
{
	restart
}

# return codes:
# 0 - adblock-lean blocklist is loaded
# 1 - error
# 2 - adblock-lean is performing an action
# 3 - adblock-lean is paused
# 4 - adblock-lean is stopped
status()
{
	local run_state active_entries_cnt=0 timestamp active_entries_cnt_human dnsmasq_status=''
	init_command status || exit 1
	check_lock
	case ${?} in
		1) exit 1 ;;
		2)
			report_curr_action
			exit 2
	esac

	print_msg -purple "" "adblock-lean (version ${ABL_VERSION}) status:"
	get_abl_run_state
	run_state=${?}

	check_dnsmasq_instances -q || run_state=1
	case ${run_state} in
		0|1) ;;
		3) print_msg -yellow "" "adblock-lean is paused." ;;
		4) print_msg -yellow "" "adblock-lean is stopped."
	esac

	rc_enabled
	case ${?} in
		0) print_msg -green "" "adblock-lean service is enabled." ;;
		1) print_msg -yellow "" "adblock-lean service is disabled."
	esac

	if [ "${run_state}" = 0 ]
	then
		check_active_blocklist
		dnsmasq_status=${?}
		if [ ${dnsmasq_status} = 0 ]
		then
			active_entries_cnt="$(get_active_entries_cnt)" && [ "${active_entries_cnt}" != 0 ] ||
				{ reg_failure "No entries found in the blocklist file."; run_state=1; }
			int2human active_entries_cnt_human "${active_entries_cnt}"
			print_msg -green "" \
				"The dnsmasq check passed and the presently installed blocklist has entries count: ${blue}${active_entries_cnt_human}" \
				"adblock-lean is active."
			timestamp="$(get_blocklist_timestamp -h)" &&
				print_msg "" "Blocklist was updated on: ${blue}${timestamp}${n_c}"
			gen_stats -noexit
		else
			reg_failure "The dnsmasq check failed with existing blocklist file."
			run_state=1
			print_msg "Consider running 'adblock-lean restart' in order to restore adblocking."
		fi
	fi

	exit ${run_state}
}

pause()
{
	init_command pause || exit 1
	get_abl_run_state
	case ${?} in
		0) ;;
		1) exit 1 ;;
		3) reg_msg -err "adblock-lean is already paused."; exit 1 ;;
		4) reg_msg -err "adblock-lean is currently stopped."; exit 1;
	esac
	FAIL_STOP_REQ=1
	reg_action -purple "Pausing adblock-lean." &&
	export_blocklist &&
	restart_dnsmasq || exit 1
	FAIL_STOP_REQ=
	log_msg -purple "adblock-lean is now paused."
	exit 0
}

resume()
{
	init_command resume || exit 1
	get_abl_run_state
	case ${?} in
		0) reg_msg -err "adblock-lean is already running."; exit 1 ;;
		1) exit 1 ;;
		3) ;;
		4) reg_msg -err "adblock-lean is currently stopped, not paused. Can not resume."; exit 1;
	esac

	reg_action -purple "Resuming adblock-lean." || exit 1
	FAIL_STOP_REQ=1
	restore_saved_blocklist || stop 1
	FAIL_STOP_REQ=
	log_msg -purple "adblock-lean is now resumed."
	exit 0
}

# optional: '-s <path>' to simulate update (intended for testing: service adblock-lean update -s <path_to_new_ver> -v <version>)
enable()
{
	if rc_enabled
	then
		reg_msg -green "The adblock-lean service is already enabled."
		return 0
	fi
	log_msg -purple "" "Enabling the adblock-lean service."
	rc_enable && rc_enabled ||
		{ reg_failure "Failed to enable the adblock-lean service"; return 4; }
	source_libs && load_config || return 3
	[ -n "${cron_schedule}" ] && { upd_cron_job || return 6; }
	:
}

disable()
{
	local rv=0
	if rc_enabled
	then
		log_msg -purple "Disabling adblock-lean."
		rm_cron_job
		rc_disable && ! rc_enabled ||
			{ reg_failure "Failed to disable the adblock-lean service"; local rv=1; }
		stop -noexit
	else
		reg_msg "The adblock-lean service is already disabled."
	fi
	return "$rv"
}

calculate_limits()
{
	source_libs || exit 1
	do_calculate_limits "${@}"
	exit ${?}
}

set_ansi

# test named pipe support (essential for the refactored bash-free logic)
if ! mkfifo /tmp/abl-test-fifo 2>/dev/null
then
	reg_failure "Failed to create named pipes. Please ensure the system supports FIFOs."
	graceful_exit 1
fi
rm -f /tmp/abl-test-fifo


detect_main_utils || graceful_exit 1

# register 1st arg as $ABL_CMD when called by '/usr/bin/adblock-lean'
if [ "${0}" = "${ABL_SERVICE_PATH}" ] && [ -n "${1}" ]
then
	ABL_CMD="${1}"
	shift
fi

LIBS_SOURCED=

case "${ABL_CMD}" in
	enable|disable|setup) ${ABL_CMD} "${@}"; exit ${?} ;;
esac

:
