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

# Copyright: 2022-2026 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
       ${0##*/} i"
}

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
	supervisedir=/home/$username/.runit/supervise
	systemdsvdir=/usr/lib/systemd/user/
else
	test -n "$runitsvdir" || runitsvdir=/etc/sv
	supervisedir=/run/runit/supervise
	systemdsvdir=/usr/lib/systemd/system
fi
test -n "$cpsvsrc" || cpsvsrc=/usr/share/runit/sv.current
#test -d "$runitsvdir" || fatal "${0##*/} $runitsvdir : not a directory"
test -d "$cpsvsrc" || fatal "${0##*/} $cpsvsrc : not a directory"

sysvdir=/etc/init.d
systemdetcdir=/etc/systemd/system

make_svlinks() {
	service="$1"; instance="$service"
	if [ -n "$2" ]; then
		instance="$2"
	fi
	if [ ! -d "$runitsvdir/$instance" ]; then #NOTE: p: must run 'cpsv s' with root privileges before
		[ ! -d "$cpsvsrc/$service" ] && fatal "$1: no template or service found in CPSV_SOURCE"
	fi
	if [ ! -x "$cpsvsrc/$service/run" ] && [ ! -x "$runitsvdir/$instance/run" ]; then
			warn "skipping $service: run file not found or not executable "
			return 0 #NOTE cpsv p service: called in runit-helper and the service may be in /etc/sv only
	fi
	if [ -n "$username" ]; then #user instance
		#strip  @user from $instance
		instance=${instance%@user}
	fi
	mkdir -p "$runitsvdir/$instance"
	for dir in log .meta ; do
		if [ -d "$cpsvsrc/$service/$dir" ]; then
			[ ! -d  "$runitsvdir/$instance/$dir" ] && mkdir "$runitsvdir/$instance/$dir"
		fi
	done
	#NOTE control, .meta and env are directories // maybe just .meta/*
	for file in run finish check down xchopts control .meta/bin .meta/onupgrade .meta/noreplace .meta/pkg .meta/enable env ; do
		if [ -e "$cpsvsrc/$service/$file" ]; then
			[ -e "$runitsvdir/$instance/$file" ] && continue
			ln -s "$cpsvsrc/$service/$file" "$runitsvdir/$instance/$file"
		fi
	done
	if [ -n "$username" ] && [ ! -e "$runitsvdir/$instance/env" ] ; then #user instance
		ln -s "/etc/sv/runsvdir@$username/env" "$runitsvdir/$instance/env"
	fi
	if [ ! -h "$runitsvdir/$instance/supervise" ] && [ ! -d "$runitsvdir/$instance/supervise" ]; then
			ln -s "$supervisedir/$instance" "$runitsvdir/$instance/supervise"
	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
				ln -s "$supervisedir/$instance".log "$runitsvdir/$instance/log/supervise"
		fi
		if [ ! -e "$runitsvdir/$instance/log/run" ]; then
			if [ -e "$cpsvsrc/$service/log/run" ] && [ ! -h "$cpsvsrc/$service/log" ]; then
				ln -s "$cpsvsrc/$service/log/run" "$runitsvdir/$instance/log/run"
			else
				ln -s /etc/sv/svlogd/run "$runitsvdir/$instance/log/run"
			fi
		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() {
	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 [ -d "$cpsvsrc/$service/log" ]; then
		#NOTE diff with empty dir(s): both empty=ok// one empty, other with a file=ok //
		#NOTE with /dev/null--> requires a file to compare with: /dev/null vs /b/* (empty)=ok
		#NOTE: assume that "$cpsvsrc/$service/log" either as file or symlink (created by 'cpsv p')
		if [ -d "$runitsvdir/$instance/log" ]; then
			if [ -e "$cpsvsrc/$service/log/run" ]; then
				if ! diff -Naur --color --exclude=supervise "$cpsvsrc/$service/log" "$runitsvdir/$instance/log"; then
					retdiff=1
				fi
			else
				exclude='--exclude=supervise --exclude=example-conf --exclude=example-log.config --exclude=README'
				if ! diff -Naur --color $exclude "/etc/sv/svlogd" "$runitsvdir/$instance/log"; then
					retdiff=1
				fi
			fi
		else
			retdiff=1 #regardless of diff exit code, log is absent in $runitsvdir/$instance/
			if [ -e "$cpsvsrc/$service/log/run" ]; then
				if ! diff -Naur --color --exclude=supervise "$cpsvsrc/$service/log/*" /dev/null ; then
					retdiff=1 #redundant
				fi
			else
				exclude='--exclude=supervise --exclude=example-conf --exclude=example-log.config --exclude=README'
				if ! diff -Naur --color $exclude "/etc/sv/svlogd/*" /dev/null ; then
					retdiff=1 #redundant
				fi
			fi
		fi
		return "$retdiff"
	elif [ -d "$runitsvdir/$instance/log" ]; then #no source, use /dev/null
		retdiff=1 #regardless of diff exit code, log is absent in $cpsvsrc/$service
		if  diff -Naur --color --exclude=supervise /dev/null "$runitsvdir/$instance/log/*"; then
			echo "Only in $runitsvdir/$instance: log"
		fi
		return "$retdiff"
	else
		return "$retdiff"
	fi
}

cp_sv() {
	service="$1"
	instance=
	#if [ -n "$username" ]; then #user instance # TODO separate instance 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 [ ! -x "$cpsvsrc/$service/run" ]; then
			warn "skipping $service: run file not found or not executable "
			return 0
	fi
	if [ -n "$username"  ] ; then
		instance=${service%@user}
		if [ ! -d "$runitsvdir/$instance" ]; then
			make_svlinks "$service"
		fi
	else
		if [ -d "$runitsvdir/$service" ]; then
			if [ -n "$force" ]; then
				cp -a "$cpsvsrc/$service" "$runitsvdir/"
				make_svlinks "$service" "$instance"
			else
				if ! sv_diff "$service" >/dev/null ; then
					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
	fi
}

loop_cpsv() {
	for dir in "$cpsvsrc"/* ; do
		stocksv=${dir##*/}
		instance=$stocksv
		#user-instance: map bar@user as bar, e.g. pipewire@user=pipewire
		instance=${instance%@user};
		if [ -n "$username" ] ; then # only loop over user services, discard system-wide services
			[ "$instance" = "$stocksv" ] && continue
		fi
		#multi-instance: map foo@default as foo, e.g. shh@default=ssh
		instance=${instance%@default};
		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
}

install_setup() {
	if [ -n "$username" ]; then #user instance
		if [ ! -d /home/"$username" ] ; then
			echo "/home/$username not found, nothing to do" && return 0
		fi
		for idir in sv supervise log runsvdir/default runsvdir/headless ; do
			mkdir -p /home/$username/.runit/$idir
		done
		if [ ! -e /home/$username/.service ] ; then
			ln -s /home/$username/.runit/runsvdir/current /home/$username/.service
		fi
		if [ ! -e /home/$username/.runit/runsvdir/current ] ; then
			ln -s /home/$username/.runit/runsvdir/default /home/$username/.runit/runsvdir/current
		fi
	else #root, #can't happen for now
		return 0
	fi
}

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
		;;
		i)
		[ -n "$opt" ] && warn "wrong syntax"  && usage
		[ -n "$2" ] && warn "wrong syntax"  && usage
		test "$(id -u)" != 0 || fatal "${0##*/} i must not be run by root." #for now
		opt=1
		install=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)
		[ -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 [ -z "$install" ]; then
	test -d "$runitsvdir" || fatal "${0##*/} $runitsvdir : not a directory"
fi

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 "$install" ]; then
	[ -n "$force" ] && warn "wrong syntax"  && usage
	install_setup
	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"
