#!/bin/sh
# Copyright (C) 2026 Aitor <aitor@genuen.org>
#
# Maintainer-script and admin helper for packages built with dh-66.
# Analogous to s6-helper / openrc-helper: does not Depend on 66 itself.

set -e

DEFAULT_TREE="${DEFAULT_TREE:-global}"
SERVICE_SYSTEM="${SERVICE_SYSTEM:-/usr/share/66/service}"
SERVICE_ADMIN="${SERVICE_ADMIN:-/etc/66/service}"
LIVEDIR="${LIVEDIR:-/run/66}"
SCANDIR="${SCANDIR:-${LIVEDIR}/scandir/0}"

have_66() {
	command -v 66 >/dev/null 2>&1
}

# True when 66's scandir is supervised (matches lib66 scandir_ok()).
# /run/66 alone is not enough: mkdir can exist without s6-svscan.
is_66_live() {
	local ctl="${SCANDIR}/.s6-svscan/control"

	[ -e "$ctl" ] || return 1
	# Open O_WRONLY without writing. Shell redirects are blocking on fifos, so
	# use a short timeout: ENXIO => not running; hang => treat as not live.
	# Avoid ": >fifo" which would inject data into svscan's control pipe.
	if command -v timeout >/dev/null 2>&1; then
		timeout 0.1 sh -c 'exec 3>"$1"' sh "$ctl" 2>/dev/null
	else
		# Best-effort without timeout(1): directory presence only.
		[ -d "${SCANDIR}/.s6-svscan" ]
	fi
}

# When genuen-init-select has chosen s6-rc, skip runtime start/stop.
# Tree enable still runs so a later switch to 66 has membership ready.
is_66_rc_selected() {
	local target

	[ -L /etc/s6-linux-init/manager ] || return 0
	target=$(readlink -f /etc/s6-linux-init/manager 2>/dev/null || readlink /etc/s6-linux-init/manager 2>/dev/null || true)
	case "$target" in
		*/66/*) return 0 ;;
		*/s6-rc/*) return 1 ;;
		*) return 0 ;;
	esac
}

run_or_error() {
	if ! "$@"; then
		eval "${ERROR_HANDLER:-exit 1}"
	fi
}

# Soft variant for enable/start during dpkg: never break configure.
run_or_warn() {
	local what="$1"
	shift
	if ! "$@"; then
		echo "warning: 66-helper: ${what} failed for ${SCRIPT} (non-fatal)" >&2
		return 0
	fi
}

policy_allows() {
	local action="$1"

	[ -x /usr/sbin/policy-rc.d ] || return 0
	set +e
	/usr/sbin/policy-rc.d "${SCRIPT}" "${action}"
	local rc=$?
	set -e
	case $rc in
		0) return 0 ;;
		101)
			echo "66-helper: ${SCRIPT}: ${action} action denied by policy-rc.d" >&2
			return 1
			;;
		*)
			echo "66-helper: ${SCRIPT}: ${action} action denied by policy-rc.d" >&2
			return 1
			;;
	esac
}

service_frontend_exists() {
	local name="$1"

	[ -e "${SERVICE_SYSTEM}/${name}" ] || [ -e "${SERVICE_ADMIN}/${name}" ]
}

# True when 66 already has a resolve for SCRIPT (frontend was parsed before).
service_is_parsed() {
	local v

	have_66 || return 1
	v=$(66 state -n -f isparsed "$SCRIPT" 2>/dev/null) || return 1
	[ "$v" = "1" ]
}

ensure_tree() {
	local tree="${1:-$DEFAULT_TREE}"

	have_66 || return 0
	# resolve succeeds if the tree already exists
	if 66 tree resolve "$tree" >/dev/null 2>&1; then
		return 0
	fi
	echo "66-helper: creating tree ${tree}" >&2
	66 tree create "$tree" >/dev/null 2>&1 || true
}

# Trees owned by 66-policy / shared infrastructure — never delete on package remove.
# Package-private trees (fake-hwclock, cryptsetup, …) are removed so Master does
# not keep dangling names that hang boot ("unable to read the group of tree").
is_shared_tree() {
	case "$1" in
		Master|boot|default|devmanager|enabled-services|getty|global|live|live-shutdown|logind|misc|misc-essential|mount|recovery|setup|shutdown)
			return 0
			;;
		*)
			return 1
			;;
	esac
}

# Drop a package-private tree from Master after its services are gone.
remove_tree() {
	local tree="${1:-}"

	[ -n "$tree" ] || return 0
	have_66 || return 0
	is_shared_tree "$tree" && return 0

	if ! 66 tree resolve "$tree" >/dev/null 2>&1; then
		return 0
	fi
	echo "66-helper: removing tree ${tree}" >&2
	66 tree remove "$tree" >/dev/null 2>&1 || true
}

# On package upgrade, refresh resolve from the new frontend. 66 reconfigure
# builds the graph from the *old* resolve first, so a Depends change that
# drops a missing service can still fail — fall back to remove -P + enable.
reconfigure_or_refresh() {
	local previous="${1:-}"

	[ -n "$previous" ] || return 0
	service_is_parsed || return 0

	if 66 reconfigure -P "$SCRIPT" >/dev/null 2>&1; then
		return 0
	fi
	echo "warning: 66-helper: reconfigure ${SCRIPT} failed; removing resolve to re-enable from frontend" >&2
	# Plain remove so reverse RequiredBy edges are scrubbed (see remove_service).
	66 remove "$SCRIPT" >/dev/null 2>&1 || true
}

# Enable SCRIPT in TREE (TREE empty => skip enable, e.g. disable= in .66).
# Optional $1 = previous package version (dpkg postinst $2): triggers
# reconfigure/refresh on upgrade before enable.
configure_tree() {
	local previous="${1:-}"

	if [ -z "${TREE:-}" ]; then
		return 0
	fi
	if ! have_66; then
		echo "warning: 66-helper: 66 not available, skip enable for ${SCRIPT}" >&2
		return 0
	fi
	if ! service_frontend_exists "$SCRIPT"; then
		echo "warning: 66-helper: frontend for ${SCRIPT} not under ${SERVICE_SYSTEM} or ${SERVICE_ADMIN}; skip enable" >&2
		return 0
	fi
	reconfigure_or_refresh "$previous"
	ensure_tree "$TREE"
	# Soft-fail: dependency frontends may arrive later in the same apt run
	# (66-policy / udev / … not configured yet), or parse stubs may be missing.
	run_or_warn "enable ${SCRIPT} in tree ${TREE}" 66 -t "$TREE" enable "$SCRIPT"
}

disable_service() {
	if ! have_66; then
		return 0
	fi
	66 disable "$SCRIPT" >/dev/null 2>&1 || true
}

# Drop resolve/svc for SCRIPT.  Use plain 66 remove (not -P): remove -P skips
# clean_depends, so reverse RequiredBy edges (e.g. mount-procfs → this service)
# stay behind and later sanitize_graph fatals with "unable to find service
# frontend file of: <SCRIPT>".  Plain remove also drops hard-Depends consumers;
# packages that only OptsDepends on SCRIPT keep their resolve.
remove_service() {
	if ! have_66; then
		return 0
	fi
	66 remove "$SCRIPT" >/dev/null 2>&1 || true
}

start_service() {
	if ! have_66 || ! is_66_live || ! is_66_rc_selected; then
		return 0
	fi
	policy_allows start || return 0
	# Soft-fail: scandir may race or service may not be startable yet.
	run_or_warn "start ${SCRIPT}" 66 start "$SCRIPT"
}

stop_service() {
	if ! have_66 || ! is_66_live || ! is_66_rc_selected; then
		return 0
	fi
	policy_allows stop || return 0
	# Soft-fail: service may never have been started in a chroot image.
	66 stop "$SCRIPT" >/dev/null 2>&1 || true
}

restart_service() {
	if ! have_66 || ! is_66_live || ! is_66_rc_selected; then
		return 0
	fi
	policy_allows restart || return 0
	66 stop "$SCRIPT" >/dev/null 2>&1 || true
	run_or_warn "start ${SCRIPT}" 66 start "$SCRIPT"
}

preinst_chmod() {
	local action="${1}"

	if [ "$action" = install ] && [ -n "${2:-}" ] && [ -e "/etc/init.d/${SCRIPT}" ]; then
		chmod +x "/etc/init.d/${SCRIPT}" >/dev/null || true
	fi
}

preinst_stop() {
	local action="${1}"

	if [ "$action" = upgrade ]; then
		stop_service
	fi
}

postinst() {
	local action="${1}"
	local previous="${2:-}"

	case "$action" in
		configure|abort-upgrade|abort-deconfigure|abort-remove)
			;;
		*)
			return 0
			;;
	esac

	configure_tree "$previous"
	start_service
}

postinst_restart() {
	local action="${1}"
	local previous="${2:-}"

	case "$action" in
		configure|abort-upgrade|abort-deconfigure|abort-remove)
			;;
		*)
			return 0
			;;
	esac

	configure_tree "$previous"
	if [ -n "$previous" ]; then
		restart_service
	else
		start_service
	fi
}

postinst_nostart() {
	local action="${1}"
	local previous="${2:-}"

	case "$action" in
		configure|abort-upgrade|abort-deconfigure|abort-remove)
			;;
		*)
			return 0
			;;
	esac

	configure_tree "$previous"
}

prerm_norestart() {
	local action="${1}"

	if [ "$action" = remove ]; then
		stop_service
	fi
}

prerm() {
	stop_service
}

postrm() {
	local action="${1}"

	if [ "$action" = remove ] && [ -x "/etc/init.d/${SCRIPT}" ]; then
		chmod -x "/etc/init.d/${SCRIPT}" >/dev/null || true
	fi

	if [ "$action" != remove ] && [ "$action" != purge ]; then
		return 0
	fi

	disable_service
	stop_service
	# Clear resolve/svc so dependents are not left pointing at a missing
	# frontend after the package ships its services away.
	remove_service
	# Drop package-private trees from Master (TREE from debian/*.66).
	remove_tree "${TREE:-}"
}

# --- admin CLI ---

admin_enable() {
	SCRIPT="${1:?usage: 66-helper enable <service> [tree]}"
	TREE="${2:-$DEFAULT_TREE}"
	configure_tree
}

admin_disable() {
	SCRIPT="${1:?usage: 66-helper disable <service>}"
	disable_service
	stop_service
}

admin_start() {
	SCRIPT="${1:?usage: 66-helper start <service>}"
	start_service
}

admin_stop() {
	SCRIPT="${1:?usage: 66-helper stop <service>}"
	stop_service
}

admin_remove() {
	SCRIPT="${1:?usage: 66-helper remove <service>}"
	disable_service
	stop_service
	remove_service
}

# Re-run enable for every SCRIPT/TREE pair embedded in dpkg postinsts.
# Use after fixing frontends/stubs when earlier configures soft-failed.
admin_dpkg_sync() {
	local info="${1:-/var/lib/dpkg/info}"
	local tmp pair script tree

	have_66 || {
		echo "66-helper: 66 not available" >&2
		return 1
	}

	tmp=$(mktemp) || return 1
	# Lines look like: SCRIPT='vdevd' TREE='devmanager' ERROR_HANDLER=...
	grep -h -E "SCRIPT='[^']+'[[:space:]]+TREE='[^']+'" \
		"$info"/*.postinst 2>/dev/null \
		| sed -n "s/.*SCRIPT='\\([^']*\\)'[[:space:]]*TREE='\\([^']*\\)'.*/\\1 \\2/p" \
		| sort -u >"$tmp"

	if [ ! -s "$tmp" ]; then
		echo "66-helper: no SCRIPT/TREE pairs found under $info" >&2
		rm -f "$tmp"
		return 1
	fi

	echo "66-helper: enabling $(wc -l <"$tmp") service(s) from dpkg postinsts"
	while read -r script tree; do
		[ -n "$script" ] && [ -n "$tree" ] || continue
		if ! service_frontend_exists "$script"; then
			echo "warning: 66-helper: skip $script (no frontend)" >&2
			continue
		fi
		SCRIPT=$script TREE=$tree
		echo "66-helper: enable $SCRIPT -> tree $TREE"
		configure_tree
	done <"$tmp"
	rm -f "$tmp"
}

usage() {
	cat >&2 <<EOF
Usage: 66-helper <action> [args]

Maintainer-script actions (env: SCRIPT, TREE, ERROR_HANDLER):
  preinst_chmod | preinst_stop
  postinst | postinst_restart | postinst_nostart
  prerm | prerm_norestart | postrm

Admin actions:
  enable <service> [tree]   (default tree: ${DEFAULT_TREE})
  disable <service>
  start <service>
  stop <service>
  remove <service>          66 remove (drop resolve; scrub reverse RequiredBy)
  dpkg-sync [info-dir]      enable all SCRIPT/TREE from dpkg postinsts

On package upgrade, postinst reconfigures a previously parsed service
(or remove + enable if reconfigure fails). On remove/purge, postrm
runs 66 remove after disable/stop (not -P: -P leaves orphan RequiredBy)
and 66 tree remove for package-private TREE names.
EOF
	exit 2
}

action="${1:-}"
[ -n "$action" ] || usage
shift

case "$action" in
	preinst_chmod|preinst_stop|postinst|postinst_restart|postinst_nostart|prerm|prerm_norestart|postrm)
		"$action" "$@"
		;;
	enable)
		admin_enable "$@"
		;;
	disable)
		admin_disable "$@"
		;;
	start)
		admin_start "$@"
		;;
	stop)
		admin_stop "$@"
		;;
	remove)
		admin_remove "$@"
		;;
	dpkg-sync)
		admin_dpkg_sync "$@"
		;;
	*)
		usage
		;;
esac
