# openrc output backend
# shellcheck shell=bash

# add openrc shebang
#
# rely on in-built variables and functions available via the
# openrc-run interpreter
gen_openrc_script_shebang() {
	printf '%s\n' '#!/sbin/openrc-run'
}

# Return the complement of the supplied capabilities
cap_complement() {
    declare -A avail="( $(setpriv --list-caps | awk 'BEGIN{ORS=" ";} { print "CAP_" toupper($0), NR }') )"
    for cap do
	unset avail[${cap}]
    done
    echo "${!avail[@]}"
}
#cap_complement CAP_CHOWN CAP_LL CAP_ANOTHET

# generate variables from the values defined in service[key].
#
gen_openrc_script_variables() {
	local ssd_args
	# add service description
	printf '%s\n' "description=\"${unit[Description]:-None provided}.\""
	if [ "${unit[Documentation]:-}" ]; then
		printf '# %s:\n' 'Documentation'
		while read -r doc; do
			# shellcheck disable=SC2086
			printf '#  %s\n' ${doc} # Unquoted to split whitespace
		done <<<"${unit[Documentation]}"
	fi
	printf '\n'

	if [[ "${service[Type]}" != "forking" && "${service[Type]}" != oneshot ]]; then
		if [[ "${service[Restart]:-}" != no ]]; then
			# TODO: supervise-daemon doesn't distinguish Restart categories
			print_directive supervisor supervise-daemon
		fi
	fi

	gen_openrc_script_exec_event --start "${service[ExecStart]:-}"

	print_directive umask "${service[UMask]:-}"
	print_directive directory "${service[WorkingDirectory]:-}"

	if [[ -n "${service[Nice]:-}" ]]; then
		ssd_args="--nicelevel ${service[Nice]} "
	fi

	if [[ -n "${service[IOSchedulingClass]:-}" || -n "${service[IOSchedulingPriority]:-}" ]]; then
		ssd_args+="--iosched ${service[IOSchedulingClass]:-best-effort}:${service[IOSchedulingPriority]:-4} "
	fi

	if [[ -n "${service[NoNewPrivileges]:-}" ]] && is_true "${service[NoNewPrivileges]}"; then
		ssd_args+="--no-new-privs "
	fi

	if [[ -n "${service[SecureBits]:-}" ]]; then
		ssd_args+="--secbits ${service[SecureBits]} "
	fi

	if [[ -n "${service[OOMScoreAdjust]:-}" ]]; then
		ssh_args+="--oom-score-adj ${service[OOMScoreAdjust]} "
	fi

	if [[ "${service[CapabilityBoundingSet]+defined}" ]] ; then
		case ${service[CapabilityBoundingSet]} in
		    # openrc --capabilities use cap_iab_from_text(3) which doesn't
		    # support all/none. So build the complement manually.
		    '') caps=$(cap_complement)
			caps_args="!${caps//[$' \n\r\t']/,!}" ;;
		    ~*) caps=$(cap_complement ${service[CapabilityBoundingSet]#'~'})
			caps_args="!${caps//[$' \n\r\t']/,!}" ;;
		    *) caps_args="!${service[CapabilityBoundingSet]//[$' \n\r\t']/,!}" ;;
		esac
	fi
	if [[ "${service[AmbientCapabilities]+defined}" ]] ; then
		case ${service[AmbientCapabilities]} in
		    '') caps=$(cap_complement)
			caps_args="^${caps//[$' \n\r\t']/,^}" ;;
		    ~*) caps=$(cap_complement ${service[AmbientCapabilities]#'~'})
			caps_args+="^${caps//[$' \n\r\t']/,^}" ;;
		    *) caps_args+="^${service[AmbientCapabilities]//[$' \n\r\t']/,^}" ;;
		esac
	fi
	if [[ -n "${caps_args:-}" ]]; then
		ssd_args+="--capabilities $caps_args "
	fi

	print_directive start_stop_daemon_args "${ssd_args:-}"
}

# Replace insserv/LSB dependency with supplied expansion
replace_depend() {
	dep=$1
	key=$2
	shift 2

	# /etc/insserv.conf expansions are preceeded by +, i.e. optional, so move
	# Requires to Wants
	if [[ "$key" == Requires ]]; then
	    append_key=Wants
	else
	    append_key="$key"
	fi
	# Remove original
	remove_depend "$dep" "$key"
	# Add replacements
	add_depends "$append_key" "$@"
	# Remove circular dependencies
	remove_depend "${openrc_script}" "$key"
}

# depends[] is insserv/LSB style. Recursively expand virtual references.
expand_lsb_depends() {
	while [[ "${depends[*]}" =~ '$' ]]; do
		for key in Requires Wants After Before; do
			for dep in ${depends[$key]:-}; do
				case "$dep" in
				    # TODO: taken from /etc/insserv.conf. Consider grep rather than harcoding.
				    \$local_fs) replace_depend "$dep" "$key" mountall mountall-bootclean mountoverflowtmp umountfs ;;
				    \$network) replace_depend "$dep" "$key" networking ifupdown ;;
				    \$named) replace_depend "$dep" "$key" named dnsmasq lwresd bind9 unbound pdns-recursor \$network ;;
				    \$remote_fs) replace_depend "$dep" "$key" \$local_fs mountnfs mountnfs-bootclean umountnfs sendsigs ;;
				    \$syslog) replace_depend "$dep" "$key" rsyslog sysklogd syslog-ng dsyslog inetutils-syslogd ;;
				    \$time) replace_depend "$dep" "$key" hwclock ;;
				    #  and /etc/insserv.conf.d
				    \$portmap) replace_depend "$dep" "$key" rpcbind ;;
				    \$*) echo "WARNING: ignoring unknown virtual dependency $dep" >&2
				    replace_depend "$dep" "$key" '' ;;
				esac
			done
		done
	done
}

# dependency resolution
#
# pending: special cases
gen_openrc_script_functions() {
	# if dependencies exist then check which dependency exists. according to the
	# check, use either one of `need`, `use`, `before` and `after`.
	if [[ -n "${depends[*]}"||
		-n "${install[Alias]:-}" ]]; then

		expand_lsb_depends

		printf '%s\n' 'depend() {'

		if [[ -n "${depends[Requires]:-}" ]]; then
			printf "%s\n" "    need ${depends[Requires]}"
		fi

		if [[ -n "${depends[Wants]:-}" ]]; then
			printf "%s\n" "    use ${depends[Wants]}"
		fi

		if [[ -n "${depends[Before]:-}" ]]; then
			printf "%s\n" "    before ${depends[Before]}"
		fi

		if [[ -n "${depends[After]:-}" ]]; then
			printf "%s\n" "    after ${depends[After]}"
		fi

		if [[ -n "${install[Alias]:-}" ]]; then
			alias=${install[Alias]//.service}
			printf "%s\n" "    provide ${alias#$}" # Could be lsb/insserv style; remove any leading $
		fi
		printf '%s\n' '}'
	fi

	for t in Stop Start-Pre Start-Post Stop-Pre Stop-Post Reload ; do
		gen_openrc_script_exec_event "--${t@L}" "${service[Exec${t/-/}]:-}"
	done
}

# generate all environment variables required by the service
#
# - if the service uses environment variables then print them out
#
# - if the service refers to a file which contains those variables, print the
#   file
#
# - to account for the case where the service uses both of them, the `if` blocks
#   do not depend on each other
gen_openrc_script_env() {
	gen_environment

	if [[ "${service[ExecStop]:-}${service[ExecReload]:-}" =~ '$MAINPID' ]]; then
		print_directive MAINPID '$([ ! -f "/run/supervise-${RC_SVCNAME}.pid" ] || cat "/run/supervise-${RC_SVCNAME}.pid")'
	fi
}

# a generic function that handles all events of type: start, stop, reload,
# start_pre, stop_pre, start_post, stop_post.
#
# this function accepts two arguments: first one is the type of execution
# (start, stop, etc) and the second is the actual command performing said
# execution.
gen_openrc_script_exec_event() {
	local exec_name exec_args exec_type exec_event

	# type is {start,stop,restart,reload} and event is the actual $type command
	exec_type="${1}"
	exec_event=$(handle_exec_prefixes "${2}")

	# start generating the script
	case "${exec_type}" in
		"--start")
			# Handle oneshot with a custom start(). Multiple ExecStart possible
			if [[ "${service[Type]}" == oneshot ]]; then
				print_sh_function start "${exec_event}"
			else
				# Everything before the first space
				exec_name=${exec_event%% *}
				# Everything not in $exec_name
				exec_args=${exec_event#"${exec_name}"}
				printf '%s\n' "command=\"${exec_name}\""
				printf '%s\n' "command_args=\"${exec_args# }\"" # trim initial whitespace
			fi

			# run as $user and $group if the systemd service provides one
			if [[ "${service[User]:-}" ]]; then
				local user
				user="${service[User]}"

				if [[ "${service[Group]:-}" ]]; then
					local group
					group="${service[Group]}"

					printf '%s\n' "command_user=\"${user}:${group}\""
				else
					printf '%s\n' "command_user=\"${user}\""
				fi
			fi

			# define PID if the systemd service provides one
			if [[ "${service[PIDFile]:-}" ]]; then
				local pid_file
				pid_file="${service[PIDFile]}"

				printf '%s\n' "pidfile=\"${pid_file}\""
			fi
			;;
		"--stop")
			print_sh_function stop "${exec_event}"
			;;
		"--start-pre")
		    chks=$(cat <<EOF
$(gen_pre_checks)
$(gen_instantiated_check)
$exec_event
EOF
			)
		    print_sh_function start_pre "${chks}"
			;;
		"--start-post")
			print_sh_function start_post "${exec_event}"
			;;
		"--stop-pre")
			print_sh_function stop_pre "${exec_event}"
			;;
		"--stop-post")
			print_sh_function stop_post "${exec_event}"
			;;
		"--reload")
		    	if [[ "${exec_event}" ]]; then
				printf '%s\n' 'extra_started_commands="reload"'
				print_sh_function reload 'ebegin "Reloading ${RC_SVCNAME}"'$'\n'"${exec_event}"
			fi
			;;
	esac
}

# Translate systemd.unit(5) specifiers.
# These are embedded in the openrc script, so POSIX only and be careful with quoting.
replace_specifiers() {
	sed --sandbox "s,%a,\$(arch),g;
	s,%A,\$(awk -F= '/^IMAGE_VERSION=/ {print \$2}' /etc/os-release),g;
	s,%b,\(cat /proc/sys/kernel/random/boot_id,g;
	s,%B,\$(awk -F= '/^BUILD_ID=/ {print \$2}' /etc/os-release),g;
	s,%C,\${XDG_CACHE_HOME:-/var/cache},g;
	s,%d,\${CREDENTIALS_DIRECTORY:-},g;
	s,%D,\${XDG_DATA_HOME:-/usr/share},g;
	s,%E,\${XDG_CONFIG_HOME:-/etc},g;
	s,%f,/\$(echo \${RC_SVCNAME#*.}|tr - /),g;
	s,%g,\$(getent group \$(id -g)|cut -d: -f1),g;
	s,%G,\$(id -g),ig;
	s,%h,\$(getent passwd \$USER|cut -d: -f6),g;
	s,%H,\$(hostname -f),g;
	s,%i,\${RC_SVCNAME#*.},g;
	s,%I,\$(echo \${RC_SVCNAME#*.}|tr - /),g;
	s,%j,\$(echo \${RC_SVCNAME%.*}|sed s/^.*-//),g;
	s,%J,\$(echo \${RC_SVCNAME%.*}|sed s/^.*-//| tr - /),g;
	s,%[lq],\$(hostname),g;
	s,%L,\${XDG_STATE_HOME:-/var}/log,g;
	s,%m,\$(cat /etc/machine-id),g;
	s,%M,\$(awk -F= '/^IMAGE_VERSION=/ {print \$2}' /etc/os-release),g;
	s,%n,\${RC_SVCNAME},g;
	s,%N,\${RC_SVCNAME%.*},g;
	s,%o,\$(awk -F= '/^IMAGE_VERSION=/ {print \$2}' /etc/os-release),g;
	s,%p,\${RC_SVCNAME%.*},g;
	s,%P,\$(echo \${RC_SVCNAME%.*}|tr - /),g;
	s,%s,\$(getent passwd \$USER|cut -d: -f7),g;
	s,%S,\${XDG_STATE_HOME:-/var/lib},g;
	s,%t,\${XDG_RUNTIME_DIR:-/run},g;
	s,%T,\${TMPDIR:-/tmp},g;
	s,%u,\$(whoami),g;
	s,%v,\$(uname -r),g;
	s,%V,\${TMPDIR:-/var/tmp},g;
	s,%w,\$(awk -F= '/^VERSION_ID=/ {print \$2}' /etc/os-release),g;
	s,%W,\$(awk -F= '/^VARIANT_ID=/ {print \$2}' /etc/os-release),g;
	s,%y,,gi; # Fragments unsupported
	s,%%,%,g;
	"
}

# read a boolean value and returns true or false
# usage: is_true val
# val		boolean value
is_true() { case "$1" in 1 | [Oo][Nn] | [Tt]* | [Yy]*) true ;; *) false ;; esac }

gen_instantiated_check() {
    	if [[ "${instantiated}" -eq 1 ]]; then
	    # shellcheck disable=SC2016
	    {
		printf '%s\n' 'if [ -z "${RC_SVCNAME#*.}" ]; then'
		printf '%s\n' '  eerror "${RC_SVCNAME} cannot be started directly."'
		printf '%s\n' '  eerror "You must make symbolic links to the instances you want to start."'
		printf '%s\n' '  return 1'
		printf '%s\n' 'fi'
	    }
	fi
}

gen_pre_checks() {
	for constraint in Assert Condition; do
		for test in ACPower Architecture Capability ControlGroupController CPUFeature CPUs DirectoryNotEmpty Environment FileIsExecutable FileNotEmpty Firmware FirstBoot Group Host KernelCommandLine KernelVersion Memory NeedsUpdate OSRelease PathExists PathExistsGlob PathIsDirectory PathIsEncrypted PathIsMountPoint PathIsReadWrite PathIsSymbolicLink Security User Virtualization; do
		    while
			read -r trigger
			read -r pre
			read -r p; do
			t=$(gen_test_case "$test" "$pre" "$p")
			if [ "${t}" ]; then
					if [ "${trigger}" ] ; then
					    triggers+=$'\n'"( $t ) ||"
					else
					    [ $constraint = 'Assert' ] && fail="echo \"Prohibited by ${constraint}${test} ${pre}${p}\"; exit 1" ||
						    fail="start() { einfo \"Skipped due to ${constraint}${test} ${pre}${p}\"; }"
					    echo "$t || $fail"
					fi
				else
					echo "WARNING: unsupported test: $constraint $test ${pre}${p}" >&2
					echo "# WARNING: skipped unsupported ${constraint}${test} ${pre}${p}"
				fi
				unset t	trigger pre
			done < <(split_constraint "${unit[${constraint}${test}]:-}")
		done
	done
	if [[ "${triggers:-}" ]] ; then
	    printf '( # Triggering conditions\n'
	    # Remove the final trailing '||'
	    print_lines "${triggers%||}" ' '
	    printf ') || start() { einfo "Skipped due to no Triggering Conditions" ; }'
	fi
}

export_service() {
	systemd_unit=$1
	base_dir=$2
	openrc_init_dir="${base_dir}/init.d"
	mkdir -p "${openrc_init_dir}"

	openrc_script=$(basename "${systemd_unit%.*}")

	# indicate instantiated units
	if [[ -z "${openrc_script#*@}" ]]; then
	    instantiated=1
	    openrc_script="${openrc_script/\@/\.}"
	else
	    instantiated=0
	fi

	# Setup notify-reload
	if [[ "${service[Type]}" == notify-reload ]]; then
	    service[ExecReload]='supervise-daemon "${RC_SVCNAME}" -s HUP'
	fi

	# generate the "#!/sbin/openrc-run" shebang which is required to run
	# openrc scripts
	gen_openrc_script_shebang >"${openrc_init_dir}/${openrc_script}"

	# embed source and sha256
	gen_origin >>"${openrc_init_dir}/${openrc_script}"

	# generate and store necessary openrc variables in the appropriate file
	gen_openrc_script_variables | replace_specifiers >>"${openrc_init_dir}/${openrc_script}"

	# generate and store necessary openrc functions in the appropriate file
	gen_openrc_script_functions | replace_specifiers >>"${openrc_init_dir}/${openrc_script}"

	# make resulting script executable by the owner only
	chmod u+x "${openrc_init_dir}/${openrc_script}"

	# Handle directives for conf.d file
	conf_d_contents=$(gen_openrc_script_env)
	conf_d_contents+=$(print_directive rc_ulimit "$(gen_ulimit_args)")
	if [[ -n "${conf_d_contents:-}" ]]; then
		openrc_conf_dir="${base_dir}/conf.d"
		mkdir -p "${openrc_conf_dir}"

		# generate and store the environment variables in the appropriate file
		gen_origin >"${openrc_conf_dir}/${openrc_script}"
		printf '%s\n' "$conf_d_contents" >>"${openrc_conf_dir}/${openrc_script}"
	fi
}
