# 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]:+"${service[WorkingDirectory]#-}"}" # Ignore '-' prefix
	if [[ -n "${service[RootDirectory]:-}" ]]; then
		ssd_args="--chroot ${service[RootDirectory]} "
	fi

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

	if [[ "$(uname -s)" == 'Linux' ]] ; then
		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
		    local secbits=0
		    for sbit in ${service[SecureBits]}; do
			case $sbit in
			    # See linux/securebits.h
			    noroot) bit=0;;
			    noroot-locked) bit=1;;
			    no-setuid-fixup) bit=2;;
			    no-setuid-fixup-locked) bit=3;;
			    keep-caps) bit=4;;
			    keep-caps-locked) bit=5;;
			    *) bit=;;
			esac
			[ "$bit" ] &&
			    ((secbits+=2**bit))
		    done
		    ssd_args+="--secbits ${secbits} "
		fi

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

		if [[ "${service[CapabilityBoundingSet]+defined}" ]] ; then
			# openrc --capabilities use cap_iab_from_text(3) which doesn't
			# support all/none. So build the complement manually.
			caps=$(cap_complement ${service[CapabilityBoundingSet]#'~'})
			case ${service[CapabilityBoundingSet]} in
			    '') caps_args="!${caps//[$' \n\r\t']/,!}" ;;
			    ~*) caps_args="${caps//[$' \n\r\t']/,}"
				caps_args+="${service[CapabilityBoundingSet]//[$' ~\n\r\t']/,!}" ;;
			    *) 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 ${service[AmbientCapabilities]#'~'})
				caps_args+="${caps_args:+,}^${caps//[$' \n\r\t']/,^}" ;;
			    *) caps_args+="${caps_args:+,}^${service[AmbientCapabilities]//[$' \n\r\t']/,^}" ;;
			esac
		fi
		if [[ -n "${caps_args:-}" ]]; then
			ssd_args+="--capabilities $caps_args "
		fi
	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 configuration required by the service
gen_openrc_script_conf() {
	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
	if [[ "${service[KillMode]:-control-group}" = 'control-group' ]]; then
		print_directive rc_cgroup_cleanup YES
		print_directive rc_send_sighup "${service[SendSIGHUP]:-no}"
	fi
	while read -r tmp ; do
		ulimit+="$tmp "
	done < <(gen_ulimit_args)
	print_directive rc_ulimit "${ulimit:-}"
}

# 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

	# Handle service Type specifics
	case "${service[Type]}" in
	    oneshot)
		if ! is_true "${service[RemainAfterExit]:-no}" ; then
		    service[ExecStartPost]+=$'\nmark_service_stopped'
		fi ;;
	    notify-reload)
		service[ExecReload]='supervise-daemon "${RC_SVCNAME}" -s HUP' ;;
	esac

	# 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_conf)
	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
}
