#!/bin/sh
#cpsv - utility to install and manage runscripts

# Copyright: 2022-2025 Lorenzo Puliti <plorenzo@disroot.org>
# License: BSD-3-clause
set -e

err() { >&2 printf '%s\n\n' "$*"; exit 1; }
fatal() { err "${0##*/}: fatal: $*"; }
warn() { >&2 printf '%s\n' "${0##*/}: warning: $*"; }
usage() {
  err "Usage: ${0##*/} [ -f ] a  <service-directory> [<service-directory2> ... ]
       ${0##*/} p  <service-directory> [<service-name>]
       ${0##*/} d  <service-directory> [<service-name>]
       ${0##*/} [ -f ] s
       ${0##*/} l"
}

if [ $# -eq 0 ]; then
	warn "wrong syntax"  && usage
fi

rcode=0
runitsvdir=$CPSV_DEST
cpsvsrc=$CPSV_SOURCE

if [ "$(id -u)" != 0 ] ; then #user instances
	[ "$(id -u)" -lt '1000' ] && fatal "${0##*/} cpsv : invalid uid"
	username="$(id -u -n)"
	test -n "$runitsvdir" || runitsvdir=/home/$username/.runit/sv
	test -n "$cpsvsrc" || cpsvsrc=/usr/share/runit/sv.current #NOTE: requires 'cpsv s' with root privileges
else
	test -n "$runitsvdir" || runitsvdir=/etc/sv
	test -n "$cpsvsrc" || cpsvsrc=/usr/share/runit/sv.src
fi
test -d "$runitsvdir" || fatal "${0##*/} $runitsvdir : not a directory"
test -d "$cpsvsrc" || fatal "${0##*/} $cpsvsrc : not a directory"

sysvdir=/etc/init.d
systemdsvdir=/usr/lib/systemd/system
systemdetcdir=/etc/systemd/system

make_svlinks() {
	service="$1"; instance="$service"
	if [ -n "$2" ]; then
		instance="$2"
	fi
	if [ -n "$username" ]; then #user instance
		[ ! -d "/usr/share/runit/sv.current/$service" ] && fatal "no template found in sv.current for $1"
		#replace  @user with @$username in $instance
		instance=${instance%@user}; instance=$instance@$username
	fi
	if [ ! -d "$runitsvdir/$instance" ]; then
		if [ ! -d "/usr/share/runit/sv.current/$service" ]; then
			[ ! -d "$cpsvsrc/$service" ] && fatal "no service found for $1"
			cp -a "$cpsvsrc/$service" "/usr/share/runit/sv.current"
		fi
		mkdir "$runitsvdir/$instance"
	fi
	if [ -d "$cpsvsrc/$service/log" ]; then
		[ ! -d  "$runitsvdir/$instance/log" ] && mkdir "$runitsvdir/$instance/log"
	fi
	for file in run finish check down xchopts control .meta env ; do #NOTE control, .meta and env are directories
		if [ -e "/usr/share/runit/sv.current/$service/$file" ]; then
			[ -e "$runitsvdir/$instance/$file" ] && continue
			ln -s "/usr/share/runit/sv.current/$service/$file" "$runitsvdir/$instance/$file"
		fi
	done
	if [ ! -h "$runitsvdir/$instance/supervise" ] && [ ! -d "$runitsvdir/$instance/supervise" ]; then
		if [ -n "$username" ]; then #user instance
			mkdir "$runitsvdir/$instance/supervise"
		else
			ln -s /run/runit/supervise/"$instance" "$runitsvdir/$instance/supervise"
		fi
	fi
	if [ -e "$runitsvdir/$instance/.meta/finish" ] && [ ! -e "$runitsvdir/$instance/finish" ]; then
		ln -s /lib/runit/finish-exec "$runitsvdir/$instance/finish"
	fi
	if [ -d "$runitsvdir/$instance/log" ]; then
		if [ ! -h "$runitsvdir/$instance/log/supervise" ] && [ ! -d "$runitsvdir/$instance/log/supervise" ]; then
			if [ -n "$username" ]; then #user instance
				mkdir "$runitsvdir/$instance"/log/supervise
			else
				ln -s /run/runit/supervise/"$instance".log "$runitsvdir/$instance"/log/supervise
			fi
		fi
		if [ ! -e "$runitsvdir/$instance/log/run" ]; then
			ln -s /etc/sv/svlogd/run "$runitsvdir/$instance/log/run"
		elif [ -h "$runitsvdir/$instance/log/run" ]; then
			if [ -n "$force" ] && [ ! -e "$cpsvsrc/$service/log/run" ]; then
				rm "$runitsvdir/$instance/log/run"
				ln -s /etc/sv/svlogd/run "$runitsvdir/$instance/log/run"
			fi
		fi
	fi
}

sv_diff() { #TODO needs $2/ instance as in make svlinks?
	retdiff=0
	service="$1"; instance="$service"
	if [ -n "$2" ]; then
		instance="$2"
	fi
	if [ -n "$username" ]; then #user instance
		[ ! -d "/usr/share/runit/sv.now/$service" ] && fatal "no template found in sv.now for $1"
		#replace  @user with @$username in $instance
		instance=${instance%@user}; instance=$instance@$username
	fi
	exclude='--exclude=supervise --exclude=conf --exclude=wtime --exclude=log'
	if ! diff -Naur --color $exclude "$cpsvsrc/$service" "$runitsvdir/$instance"; then
		retdiff=1
	fi
	if [ -e "$cpsvsrc/$service/log/run" ] || [ -d "$runitsvdir/$instance/log" ]; then
		if [ ! -d "$cpsvsrc/$service/log" ]; then
			if ! diff -Naur --color --exclude=supervise "$cpsvsrc/$service/log" "$runitsvdir/$instance/log"; then
				retdiff=1
			fi
		fi
	else
		if [ -d "$cpsvsrc/$service/log" ]; then
			if ! diff -Naur --color "/etc/sv/svlogd/run" "$runitsvdir/$instance/log/run"; then
				retdiff=1
			fi
			if ! diff -Naur --color --exclude=supervise --exclude=run "$cpsvsrc/$service/log" "$runitsvdir/$instance/log"; then
				retdiff=1
			fi
		fi
	fi
	return "$retdiff"
}

cp_sv() {
	service="$1" 
	#if [ -n "$username" ]; then #user instance # TODO separate intance and service TODO fix diff !
	#	[ ! -d "/usr/share/runit/sv.current/$service" ] && fatal "no template found in sv.current for $1"
	#	#replace  @user with @$username in $instance
	#	service=${service%@user}; service=$service@$username
	#fi
	if [ -d "$runitsvdir/$service" ]; then
		if ! sv_diff "$service" >/dev/null ; then
			if [ -n "$force" ]; then
				cp -a "$cpsvsrc/$service" "$runitsvdir/"
				make_svlinks "$service"
			else
				warn "skipping $service, local version exists, use -f to overwrite" \
				&& rcode=$((rcode+1))
			fi
		fi
	else
		cp -a "$cpsvsrc/$service" "$runitsvdir/"
		make_svlinks "$service"
	fi
}

loop_cpsv() {
	for dir in "$cpsvsrc"/* ; do
		stocksv=${dir##*/}
		instance=$stocksv
		#multi-instance: map foo@default as foo, e.g. shh@default=ssh
		instance=${instance%@default};
		#user-instance: map bar@user as bar, e.g. pipewire@user=pipewire
		instance=${instance%@user};
		if [ -e "$systemdsvdir/$instance.service" ] || [ -x "$sysvdir/$instance" ]; then
			cp_sv "$stocksv"
		else
			if [ -r "$cpsvsrc/$stocksv/.meta/bin" ]; then
				binpath="$(cat $cpsvsrc/$stocksv/.meta/bin)"
				test -n "$binpath" || continue
				if [ -e "$binpath" ]; then
					cp_sv "$stocksv"
				fi
			fi
		fi
	done
}

loop_listsv() {
	for dir in "$cpsvsrc"/* ; do
		service=${dir##*/}
		status=
		if [ -e "$systemdsvdir/$service.service" ] || [ -x "$sysvdir/$service" ]; then
			if [ ! -d "$runitsvdir/$service" ]; then
				status='[a]' #available, not installed in /etc/sv/
			elif ! sv_diff "$service" >/dev/null ; then
				status='[l]' #installed but is local version
			else
				status='[i]' # installed, package version
			fi
		elif [ -r "$cpsvsrc/$service/.meta/bin" ]; then
			binpath="$(cat $cpsvsrc/$service/.meta/bin)"
			if [ -n "$binpath" ] && [ -e "$binpath" ]; then
				if [ ! -d "$runitsvdir/$service" ]; then
					status='[a]' #available, not installed in /etc/sv/
				elif ! sv_diff "$service" >/dev/null ; then
					status='[l]' #installed but is local version
				else
					status='[i]' # installed, package version
				fi
			else
				if [ -d "$runitsvdir/$service" ]; then
					status='[p]' #purged
				else
					continue
				fi
			fi
		else
			if [ -d "$runitsvdir/$service" ] && [ ! -e "$systemdetcdir/$service.service" ]; then
				#purged: inaccurate for sysv only services
				status='[p]'
			else
				continue
			fi
		fi
		echo "$status: $service"
	done
}

while [ $# -gt 0 ]; do
	case $1 in
		a)
		test "$(id -u)" = 0 || fatal "${0##*/} a must be run by root."
		[ -n "$opt" ] && warn "wrong syntax"  && usage
		[ -z "$2" ] && warn "wrong syntax"  && usage
		add=1
		opt=1
		shift
		;;
		-f)
		force=1
		shift
		;;
#		-r)
#		#TODO replace/swap instead of overwrite (like in -f) the service dir
#		replace=1
#		shift
#		;;
		p)
		[ -n "$opt" ] && warn "wrong syntax"  && usage
		[ -z "$2" ] && warn "wrong syntax"  && usage
		# $3 is the optional instance name
		[ -n "$4" ] && warn "wrong syntax"  && usage
		symlink=1
		opt=1
		shift
		;;
		d)
		[ -n "$opt" ] && warn "wrong syntax"  && usage
		[ -z "$2" ] && warn "wrong syntax"  && usage
		# $3 is the optional instance name
		[ -n "$4" ] && warn "wrong syntax"  && usage
		diff=1
		opt=1
		shift
		;;
		l|--list)
		[ -n "$opt" ] && warn "wrong syntax"  && usage
		[ -n "$2" ] && warn "wrong syntax"  && usage
		list=1
		opt=1
		shift
		;;
		s|--sync)
		test "$(id -u)" = 0 || fatal "${0##*/} s must be run by root."
		[ -n "$opt" ] && warn "wrong syntax"  && usage
		[ -n "$2" ] && warn "wrong syntax"  && usage
		sync=1
		opt=1
		shift
		;;
		-*|--*)
		warn "unknown option $1" && usage
		;;
		*)
		break
		;;
	esac
done

#extra syntax check
[ -z "$opt" ] && warn "wrong syntax" && usage

if [ -n "$diff" ]; then #TODO: needs to get $2/instance?
	[ -n "$force" ] && warn "wrong syntax"  && usage
	[ ! -d "$cpsvsrc/$1" ] && fatal "no stock service found for $1"
	sv_diff "$1" "$2"
	exit
fi
if [ -n "$symlink" ]; then
	[ -n "$force" ] && warn "wrong syntax"  && usage
	#[ ! -d "$runitsvdir/$1" ] && fatal "no service found for $1"# multi-instance
	make_svlinks "$1" "$2"
	exit 0
fi
if [ -n "$list" ]; then
	[ -n "$force" ] && warn "wrong syntax"  && usage
	loop_listsv
	exit 0
fi
if [ -n "$sync" ]; then
	loop_cpsv
	exit "$rcode"
fi
#add
for arg in "$@"; do
	if [ ! -d "$cpsvsrc/$arg" ]; then
		warn "no stock service found for $arg"
		rcode=$((rcode+1))
	else
		cp_sv "$arg"
	fi
	shift
done

exit "$rcode"
