#!/bin/sh
# Copyright (C) 2026 Aitor <aitor@genuen.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.

set -e

S6RC_SV="${S6RC_SV:-/etc/s6-rc/sv}"
S6RC_BUNDLES="${S6RC_BUNDLES:-/etc/s6-rc/bundles}"
S6RC_COMPILED="${S6RC_COMPILED:-/etc/s6-rc/compiled}"
S6RC_LIVE="${S6RC_LIVE:-/run/s6-rc}"
S6RC_RC="${S6RC_RC:-/etc/s6-rc/rc}"
S6RC_ADMINSV="${S6RC_ADMINSV:-/etc/s6-rc/adminsv}"
# Optional services go here (SysV rc?.d analogue), not in boot/default.
DEFAULT_ENABLE_BUNDLE="${DEFAULT_ENABLE_BUNDLE:-enabled-services}"

is_s6() {
	[ -d "${S6RC_LIVE}" ] && [ -d /run/service ]
}

have_cmd() {
	command -v "$1" >/dev/null 2>&1
}

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

policy_allows() {
	local action="$1"

	if [ ! -x /usr/sbin/policy-rc.d ]; then
		return 0
	fi
	set +e
	/usr/sbin/policy-rc.d "${SCRIPT}" "${action}"
	local rc=$?
	set -e
	if [ "$rc" = 101 ]; then
		echo "s6-helper: ${SCRIPT}: ${action} action denied by policy-rc.d" >&2
		return 1
	fi
	return 0
}

resolve_sv_path() {
	if [ -L "$S6RC_SV" ]; then
		readlink -f "$S6RC_SV"
	else
		printf '%s\n' "$S6RC_SV"
	fi
}

resolve_rc_dir() {
	if [ -L "$S6RC_RC" ]; then
		readlink -f "$S6RC_RC"
	elif [ -d "$S6RC_RC" ]; then
		printf '%s\n' "$S6RC_RC"
	else
		printf '%s\n' "$S6RC_RC"
	fi
}

remove_old_compiled_dirs() {
	local rc_dir="$1"
	local new="$2"
	local old

	(
		cd "$rc_dir" || exit 1
		for old in compiled-*; do
			case "$old" in
				"$new") continue ;;
				compiled-\*) continue ;;
			esac
			if [ -d "$old" ]; then
				rm -rf -- "$old"
			fi
		done
	)
}

link_compiled_relative() {
	local rc_dir="$1"
	local target="$2"

	(
		cd "$rc_dir" || exit 1
		rm -f compiled
		ln -s "$target" compiled
	)
}

ensure_etc_compiled_link() {
	# Boot uses -c /etc/s6-rc/compiled. Prefer layout:
	#   /etc/s6-rc/compiled -> rc/compiled -> compiled-@stamp
	# A leftover *directory* from older compile-in-place layouts shadows
	# the real DB and yields "default is not a recognized identifier".
	if [ -L /etc/s6-rc/compiled ]; then
		case "$(readlink /etc/s6-rc/compiled)" in
			rc/compiled|./rc/compiled) return 0 ;;
		esac
		ln -sfn rc/compiled /etc/s6-rc/compiled
		return 0
	fi
	if [ -d /etc/s6-rc/compiled ]; then
		echo "warning: s6-helper: replacing stale /etc/s6-rc/compiled dir with symlink to rc/compiled" >&2
		rm -rf /etc/s6-rc/compiled
	elif [ -e /etc/s6-rc/compiled ]; then
		echo "warning: s6-helper: replacing unexpected /etc/s6-rc/compiled with symlink to rc/compiled" >&2
		rm -f /etc/s6-rc/compiled
	fi
	ln -sfn rc/compiled /etc/s6-rc/compiled
}

ensure_sv_tree() {
	mkdir -p "$S6RC_SV" "$(resolve_rc_dir)"
}

# Create an empty s6-rc bundle if a package registers into it before
# the policy package has shipped the skeleton.
#
# Prefer /etc/s6-rc/bundles/<name> + sv/<name> -> ../bundles/<name> so we
# never leave a real directory under sv/ that blocks the packaging symlink.
ensure_bundle() {
	local bundle="$1" dest link

	[ -n "$bundle" ] || return 0
	ensure_sv_tree
	mkdir -p "$S6RC_BUNDLES"

	dest="${S6RC_BUNDLES}/${bundle}"
	link="${S6RC_SV}/${bundle}"

	# Real husk under sv/ (no type) blocks dh_link symlinks — replace it.
	if [ -e "$link" ] && [ ! -L "$link" ] && [ ! -f "${link}/type" ]; then
		echo "warning: s6-helper: replacing incomplete ${link} with bundles symlink" >&2
		rm -rf -- "$link"
	fi

	if [ -L "$link" ]; then
		# Symlink layout: materialize the bundle at the link target tree.
		dest="${S6RC_BUNDLES}/${bundle}"
		mkdir -p "$dest"
	elif [ -d "$link" ] && [ -f "${link}/type" ]; then
		# Legacy: bundle lives directly under sv/.
		dest="$link"
	else
		mkdir -p "$dest"
		if [ ! -e "$link" ]; then
			ln -s "../bundles/${bundle}" "$link"
		fi
	fi

	if [ ! -f "${dest}/type" ]; then
		printf 'bundle\n' > "${dest}/type"
	fi
	# Empty contents.d dirs are omitted from .debs; ship/create a packagable
	# empty "contents" file instead. Do not mkdir an empty contents.d on top
	# of an existing contents file — s6-rc-compile prefers contents.d.
	if [ ! -d "${dest}/contents.d" ] && [ ! -f "${dest}/contents" ]; then
		: > "${dest}/contents"
	fi
}

# Ensure every bundle has contents.d or contents before s6-rc-compile.
fix_bundle_defs() {
	local dir

	ensure_sv_tree
	for dir in "${S6RC_SV}"/*/; do
		[ -d "$dir" ] || continue
		[ -f "${dir}type" ] || continue
		grep -qx bundle "${dir}type" 2>/dev/null || continue
		if [ ! -d "${dir}contents.d" ] && [ ! -f "${dir}contents" ]; then
			: > "${dir}contents"
		fi
	done
}

scrub_bundle_membership() {
	local bundle_dir="${S6RC_SV}/${1}/contents.d"

	if [ ! -d "$bundle_dir" ]; then
		return 0
	fi
	rm -f "${bundle_dir}/${SCRIPT}" 2>/dev/null || true
}

scrub_all_bundles() {
	local bundle

	ensure_sv_tree
	for bundle in "${S6RC_SV}"/*/; do
		[ -d "$bundle" ] || continue
		scrub_bundle_membership "$(basename "$bundle")"
	done
}

configure_bundle() {
	if [ -z "${BUNDLE:-}" ]; then
		return 0
	fi

	ensure_sv_tree
	ensure_bundle "$BUNDLE"

	# Do not register a missing atomic/pipeline: s6-rc-compile would fail.
	if ! service_exists "$SCRIPT"; then
		echo "warning: s6-helper: service ${SCRIPT} not present under ${S6RC_SV}; skip ${BUNDLE} membership" >&2
		return 0
	fi

	mkdir -p "${S6RC_SV}/${BUNDLE}/contents.d"
	: > "${S6RC_SV}/${BUNDLE}/contents.d/${SCRIPT}"
	# contents.d now owns membership; drop empty legacy contents if present
	rm -f "${S6RC_SV}/${BUNDLE}/contents"
}

# True if name is an atomic/bundle (has type) or an s6-rc pipeline name.
service_exists() {
	local name="$1" f

	[ -f "${S6RC_SV}/${name}/type" ] && return 0
	for f in "${S6RC_SV}"/*/pipeline-name; do
		[ -f "$f" ] || continue
		grep -qx "$name" "$f" && return 0
	done
	return 1
}


# True if any sv/* entry is a relative symlink into bundles/
# (Genuen packaging layout via debian/links).
uses_bundle_sv_symlinks() {
	local p t

	for p in "${S6RC_SV}"/*; do
		[ -L "$p" ] || continue
		t="$(readlink "$p")"
		case "$t" in
			../bundles/*|../bundles|./../bundles/*)
				return 0
				;;
		esac
	done
	return 1
}

# If bundles/<name>/type exists, ensure sv/<name> is a relative symlink.
# Repairs husks left by postinsts that did mkdir -p sv/<bundle>/contents.d
# before the owning package's dh_link symlink was in place.
repair_bundle_sv_links() {
	local bdir name dest

	[ -d "$S6RC_BUNDLES" ] || return 0
	ensure_sv_tree
	for bdir in "${S6RC_BUNDLES}"/*/; do
		[ -d "$bdir" ] || continue
		[ -f "${bdir}type" ] || continue
		name="$(basename "$bdir")"
		dest="${S6RC_SV}/${name}"
		if [ -L "$dest" ]; then
			continue
		fi
		if [ -e "$dest" ]; then
			if [ -f "${dest}/type" ]; then
				# Legacy real bundle under sv/ — leave it.
				continue
			fi
			echo "warning: s6-helper: replacing incomplete ${dest} with symlink to bundles/${name}" >&2
			rm -rf -- "$dest"
		fi
		ln -s "../bundles/${name}" "$dest"
	done
}

# True if dpkg believes this path belongs to an installed package.
# Incomplete husks created by mkdir must still be scrubbed; package-
# owned trees must never be deleted (dpkg will not re-extract them).
path_owned_by_dpkg() {
	local path="$1"

	have_cmd dpkg-query || return 1
	dpkg-query -S "$path" >/dev/null 2>&1
}

# Remove one incomplete service path. Never rm -rf through a symlink
# (trailing slash would wipe the bundles/ target).
scrub_incomplete_one() {
	local path="$1"

	if [ -L "$path" ]; then
		if [ -f "${path}/type" ]; then
			return 0
		fi
		if path_owned_by_dpkg "$path"; then
			echo "warning: s6-helper: incomplete dpkg-owned symlink ${path}; not removing (reinstall owning package)" >&2
			return 0
		fi
		echo "warning: s6-helper: removing incomplete service symlink ${path}" >&2
		rm -f -- "$path"
		return 0
	fi
	[ -d "$path" ] || return 0
	[ -f "${path}/type" ] && return 0
	if path_owned_by_dpkg "$path"; then
		echo "warning: s6-helper: incomplete dpkg-owned dir ${path}; not removing (reinstall owning package)" >&2
		return 0
	fi
	echo "warning: s6-helper: removing incomplete service dir ${path}" >&2
	rm -rf -- "$path"
}

# Directories without a type file break s6-rc-compile
# ("unable to read .../type"). Scrub both sv/ and bundles/: an empty
# bundles/boot husk (e.g. after an old rm -rf through a symlink) is
# enough to fail compile when dual-tree mode is used.
scrub_incomplete_services() {
	local dir path

	ensure_sv_tree
	for dir in "${S6RC_SV}"/*/; do
		path="${dir%/}"
		[ -e "$path" ] || [ -L "$path" ] || continue
		scrub_incomplete_one "$path"
	done
	if [ -d "$S6RC_BUNDLES" ]; then
		for dir in "${S6RC_BUNDLES}"/*/; do
			path="${dir%/}"
			[ -e "$path" ] || [ -L "$path" ] || continue
			scrub_incomplete_one "$path"
		done
	fi
}

# True when the boot policy skeleton is on disk (unpacked is enough).
# Leaf packages configure before s6-bundle-boot; recompile then would
# only see a partial tree / leftover husks.
boot_policy_present() {
	[ -f "${S6RC_BUNDLES}/boot/type" ] || [ -f "${S6RC_SV}/boot/type" ]
}

# dpkg stages conffiles as *.dpkg-new when the on-disk conffile was
# deleted (or otherwise conflicted) and the unpack did not use
# --force-confmiss. Promote those leftovers when the real name is absent
# so a reinstall can recover without leaving half-empty service dirs.
promote_orphaned_dpkg_new() {
	local f base

	for f in \
		"${S6RC_SV}"/*/*.dpkg-new \
		"${S6RC_SV}"/*/*/*.dpkg-new \
		"${S6RC_BUNDLES}"/*/*.dpkg-new \
		"${S6RC_BUNDLES}"/*/*/*.dpkg-new
	do
		[ -f "$f" ] || continue
		base="${f%.dpkg-new}"
		[ "$base" != "$f" ] || continue
		if [ ! -e "$base" ]; then
			echo "warning: s6-helper: promoting ${f##*/} -> $(basename "$base") in $(dirname "$f")" >&2
			mv -f -- "$f" "$base"
		fi
	done
}

# True if any service/bundle dir is still missing type (compile would fail).
incomplete_services_remain() {
	local dir path

	for dir in "${S6RC_SV}"/*/ "${S6RC_BUNDLES}"/*/; do
		[ -d "$dir" ] || [ -L "${dir%/}" ] || continue
		path="${dir%/}"
		[ -e "$path" ] || [ -L "$path" ] || continue
		if [ ! -f "${path}/type" ]; then
			return 0
		fi
	done
	return 1
}

# Skip dpkg's temporary/conflict files in prune loops.
is_dpkg_temp_name() {
	case "$1" in
		*.dpkg-new|*.dpkg-old|*.dpkg-dist|*.dpkg-tmp|*.dpkg-bak)
			return 0
			;;
		*)
			return 1
			;;
	esac
}

# Drop contents.d entries that do not resolve to an existing service/bundle.
# Leftovers from older helpers or removed packages break s6-rc-compile.
prune_dangling_memberships() {
	local bundle_dir entry name

	ensure_sv_tree
	for bundle_dir in "${S6RC_SV}"/*/contents.d; do
		[ -d "$bundle_dir" ] || continue
		for entry in "$bundle_dir"/*; do
			[ -e "$entry" ] || continue
			name="$(basename "$entry")"
			case "$name" in
				.*|lost+found) continue ;;
			esac
			is_dpkg_temp_name "$name" && continue
			if ! service_exists "$name"; then
				echo "warning: s6-helper: pruning dangling ${bundle_dir}/${name}" >&2
				rm -f -- "$entry"
			fi
		done
	done
}


# Drop dependencies.d entries that do not resolve (e.g. hostname removed
# as an incomplete husk while checkroot.sh still declares the dependency).
prune_dangling_dependencies() {
	local dep_dir entry name

	ensure_sv_tree
	for dep_dir in "${S6RC_SV}"/*/dependencies.d; do
		[ -d "$dep_dir" ] || continue
		for entry in "$dep_dir"/*; do
			[ -e "$entry" ] || continue
			name="$(basename "$entry")"
			case "$name" in
				.*|lost+found) continue ;;
			esac
			is_dpkg_temp_name "$name" && continue
			if ! service_exists "$name"; then
				echo "warning: s6-helper: pruning dangling dependency ${dep_dir}/${name}" >&2
				rm -f -- "$entry"
			fi
		done
	done
}

# Layout (antiX / Genuen bootstrap):
#   /etc/s6-rc/compiled -> rc/compiled
#   /etc/s6-rc/rc/compiled -> compiled-@YYYYMMDD-HHMMSS
recompile_and_update() {
	local rc_dir sv_path stamp new live_db prev_boot_db

	ensure_sv_tree
	promote_orphaned_dpkg_new
	repair_bundle_sv_links
	scrub_incomplete_services
	if incomplete_services_remain; then
		echo "warning: s6-helper: incomplete service dirs remain; deferring recompile" >&2
		return 0
	fi
	prune_dangling_memberships
	prune_dangling_dependencies
	fix_bundle_defs

	if [ -x /etc/s6-rc/recompile ]; then
		run_or_error /etc/s6-rc/recompile
		return 0
	fi

	if have_cmd s6-db-reload; then
		run_or_error s6-db-reload -r
		rc_dir="$(resolve_rc_dir)"
		if [ -L "$rc_dir/compiled" ]; then
			target="$(readlink "$rc_dir/compiled")"
			case "$target" in
				/*) link_compiled_relative "$rc_dir" "${target##*/}" ;;
			esac
		fi
		ensure_etc_compiled_link
		return 0
	fi

	if ! have_cmd s6-rc-compile; then
		echo "warning: s6-helper: s6-rc-compile not available, skipping database rebuild" >&2
		return 0
	fi

	rc_dir="$(resolve_rc_dir)"
	sv_path="$(resolve_sv_path)"
	# Include $$ so parallel/rapid maintainer-script recompiles in the
	# same second do not collide on compiled-@YYYYMMDD-HHMMSS.
	stamp="@$(date '+%Y%m%d-%H%M%S')-$$"
	new="compiled-${stamp}"

	mkdir -p "$rc_dir"
	if [ -e "${rc_dir}/${new}" ]; then
		rm -rf -- "${rc_dir}/${new}"
	fi
	# Symlink layout (sv/NAME -> ../bundles/NAME): compile sv/ only.
	# Passing bundles/ as a second source duplicates every linked bundle
	# ("duplicate service definition: misc-essential...").
	# Detect ANY sv->bundles symlink, not only boot/default: those two
	# are often scrubbed as incomplete during partial apt configures.
	set -- "$sv_path"
	if [ -d "$S6RC_BUNDLES" ] && ! uses_bundle_sv_symlinks; then
		set -- "$@" "$S6RC_BUNDLES"
	fi
	if [ -d "$S6RC_ADMINSV" ]; then
		set -- "$@" "$S6RC_ADMINSV"
	fi
	run_or_error s6-rc-compile "${rc_dir}/${new}" "$@"

	if is_s6 && have_cmd s6-rc-update; then
		live_db=""
		if [ -e "${S6RC_LIVE}/compiled" ]; then
			live_db="$(readlink -f "${S6RC_LIVE}/compiled" 2>/dev/null || true)"
		fi
		run_or_error s6-rc-update -v2 -l "$S6RC_LIVE" "${rc_dir}/${new}"
		prev_boot_db=""
		if [ -e "$rc_dir/compiled" ]; then
			prev_boot_db="$(readlink -f "$rc_dir/compiled" 2>/dev/null || true)"
		fi
		link_compiled_relative "$rc_dir" "$new"
		ensure_etc_compiled_link
		if [ -n "$prev_boot_db" ] && [ "$prev_boot_db" != "${rc_dir}/${new}" ] && [ "$prev_boot_db" != "$live_db" ]; then
			rm -rf "$prev_boot_db"
		fi
		remove_old_compiled_dirs "$rc_dir" "$new"
	else
		link_compiled_relative "$rc_dir" "$new"
		ensure_etc_compiled_link
		remove_old_compiled_dirs "$rc_dir" "$new"
	fi
}

start_service() {
	if ! is_s6 || ! have_cmd s6-rc; then
		return 0
	fi
	if ! policy_allows start; then
		return 0
	fi
	run_or_error s6-rc -v2 -up change "$SCRIPT"
}

stop_service() {
	if ! is_s6 || ! have_cmd s6-rc; then
		return 0
	fi
	if ! policy_allows stop; then
		return 0
	fi
	run_or_error s6-rc -v2 -down change "$SCRIPT"
}

restart_service() {
	if ! is_s6 || ! have_cmd s6-rc; then
		return 0
	fi
	if ! policy_allows restart; then
		return 0
	fi
	run_or_error s6-rc -v2 -down change "$SCRIPT"
	run_or_error s6-rc -v2 -up change "$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}"

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

	scrub_all_bundles
	configure_bundle
	recompile_and_update
	start_service
}

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

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

	scrub_all_bundles
	configure_bundle
	recompile_and_update
	if [ -n "$previous" ]; then
		restart_service
	else
		start_service
	fi
}

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

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

	scrub_all_bundles
	configure_bundle
	recompile_and_update
}

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

	# Only drop this SCRIPT from bundles. Do not recompile: other services
	# from the same package are often already deleted, so s6-rc-compile
	# would fail and leave dpkg stuck (force-remove / half-removed).
	scrub_all_bundles
}

recompile() {
	local when_ready=0
	while [ $# -gt 0 ]; do
		case "$1" in
			--when-ready) when_ready=1; shift ;;
			*) break ;;
		esac
	done
	if [ "$when_ready" -eq 1 ] && ! boot_policy_present; then
		echo "warning: s6-helper: boot policy not present yet; deferring recompile" >&2
		return 0
	fi
	recompile_and_update
}

# --- admin CLI: enable / disable / status / list-enabled ---

is_protected_bundle() {
	case "$1" in
		boot|default|mount|setup|misc|misc-essential|getty|logind|\
		devmanager|udev|recovery|shutdown)
			return 0
			;;
		*)
			return 1
			;;
	esac
}

bundle_contents_dir() {
	local bundle="$1"

	ensure_bundle "$bundle"
	mkdir -p "${S6RC_SV}/${bundle}/contents.d"
	rm -f "${S6RC_SV}/${bundle}/contents"
	printf '%s\n' "${S6RC_SV}/${bundle}/contents.d"
}

service_is_enabled_in() {
	local svc="$1" bundle="$2"

	[ -e "${S6RC_SV}/${bundle}/contents.d/${svc}" ]
}

service_is_up() {
	local svc="$1"

	is_s6 && have_cmd s6-rc || return 1
	s6-rc -a list 2>/dev/null | grep -qx "$svc"
}

check_bundle_writable() {
	local bundle="$1" force="$2"

	if is_protected_bundle "$bundle" && [ "$force" != 1 ]; then
		echo "s6-helper: refusing to modify protected bundle '${bundle}' (use --force)" >&2
		return 1
	fi
	return 0
}

enable_services() {
	local now=0 force=0 bundle svc cdir
	bundle="${BUNDLE:-$DEFAULT_ENABLE_BUNDLE}"

	while [ $# -gt 0 ]; do
		case "$1" in
			--now) now=1; shift ;;
			--force) force=1; shift ;;
			--bundle=*) bundle=${1#--bundle=}; shift ;;
			--bundle)
				[ $# -ge 2 ] || { echo "s6-helper: --bundle needs a name" >&2; return 1; }
				bundle=$2; shift 2
				;;
			-h|--help)
				usage
				return 0
				;;
			--) shift; break ;;
			-*)
				echo "s6-helper enable: unknown option: $1" >&2
				return 1
				;;
			*) break ;;
		esac
	done

	[ $# -gt 0 ] || { echo "s6-helper enable: need SERVICE..." >&2; usage >&2; return 1; }
	check_bundle_writable "$bundle" "$force" || return 1

	cdir="$(bundle_contents_dir "$bundle")"
	for svc in "$@"; do
		if ! service_exists "$svc"; then
			echo "s6-helper: service '${svc}' not found under ${S6RC_SV}" >&2
			return 1
		fi
		: > "${cdir}/${svc}"
		echo "s6-helper: enabled ${svc} in ${bundle}"
	done

	recompile_and_update

	if [ "$now" -eq 1 ]; then
		for svc in "$@"; do
			SCRIPT=$svc
			start_service
		done
	fi
}

disable_services() {
	local now=0 force=0 bundle svc cdir
	bundle="${BUNDLE:-$DEFAULT_ENABLE_BUNDLE}"

	while [ $# -gt 0 ]; do
		case "$1" in
			--now) now=1; shift ;;
			--force) force=1; shift ;;
			--bundle=*) bundle=${1#--bundle=}; shift ;;
			--bundle)
				[ $# -ge 2 ] || { echo "s6-helper: --bundle needs a name" >&2; return 1; }
				bundle=$2; shift 2
				;;
			-h|--help)
				usage
				return 0
				;;
			--) shift; break ;;
			-*)
				echo "s6-helper disable: unknown option: $1" >&2
				return 1
				;;
			*) break ;;
		esac
	done

	[ $# -gt 0 ] || { echo "s6-helper disable: need SERVICE..." >&2; usage >&2; return 1; }
	check_bundle_writable "$bundle" "$force" || return 1

	cdir="${S6RC_SV}/${bundle}/contents.d"
	for svc in "$@"; do
		if [ "$now" -eq 1 ]; then
			SCRIPT=$svc
			stop_service || true
		fi
		if [ -e "${cdir}/${svc}" ]; then
			rm -f "${cdir}/${svc}"
			echo "s6-helper: disabled ${svc} in ${bundle}"
		else
			echo "s6-helper: ${svc} was not enabled in ${bundle}"
		fi
	done

	recompile_and_update
}

status_services() {
	local bundle svc enabled up installed
	bundle="${BUNDLE:-$DEFAULT_ENABLE_BUNDLE}"

	while [ $# -gt 0 ]; do
		case "$1" in
			--bundle=*) bundle=${1#--bundle=}; shift ;;
			--bundle)
				[ $# -ge 2 ] || { echo "s6-helper: --bundle needs a name" >&2; return 1; }
				bundle=$2; shift 2
				;;
			-h|--help)
				usage
				return 0
				;;
			--) shift; break ;;
			-*)
				echo "s6-helper status: unknown option: $1" >&2
				return 1
				;;
			*) break ;;
		esac
	done

	[ $# -gt 0 ] || { echo "s6-helper status: need SERVICE..." >&2; return 1; }

	for svc in "$@"; do
		installed=no
		enabled=no
		up=no
		service_exists "$svc" && installed=yes
		service_is_enabled_in "$svc" "$bundle" && enabled=yes
		service_is_up "$svc" && up=yes
		printf '%s: installed=%s enabled_in_%s=%s up=%s\n' \
			"$svc" "$installed" "$bundle" "$enabled" "$up"
	done
}

list_enabled() {
	local bundle="${1:-$DEFAULT_ENABLE_BUNDLE}"
	local cdir entry

	cdir="${S6RC_SV}/${bundle}/contents.d"
	if [ ! -d "$cdir" ]; then
		echo "s6-helper: bundle '${bundle}' has no contents.d" >&2
		return 1
	fi
	for entry in "$cdir"/*; do
		[ -e "$entry" ] || continue
		basename "$entry"
	done | sort
}

usage() {
	cat <<EOF
Usage: s6-helper COMMAND [OPTIONS] [ARGS]

Admin commands:
  enable  [--now] [--force] [--bundle=NAME] SERVICE...
  disable [--now] [--force] [--bundle=NAME] SERVICE...
  status  [--bundle=NAME] SERVICE...
  list-enabled [BUNDLE]
  recompile [--when-ready]

  Default enable/disable bundle: ${DEFAULT_ENABLE_BUNDLE}
  Protected bundles (need --force): boot default mount setup
    misc misc-essential getty logind devmanager udev recovery shutdown

Maintainer-script commands (SCRIPT / BUNDLE from environment):
  postinst | postinst_restart | postinst_nostart
  prerm | prerm_norestart | postrm
  preinst_chmod | preinst_stop
EOF
}

main() {
	local cmd

	if [ $# -lt 1 ]; then
		usage >&2
		exit 1
	fi

	cmd=$1
	shift

	case "$cmd" in
		enable)
			enable_services "$@"
			;;
		disable)
			disable_services "$@"
			;;
		status)
			status_services "$@"
			;;
		list-enabled|list_enabled)
			list_enabled "$@"
			;;
		help|-h|--help)
			usage
			;;
		# Maintainer / legacy entry points (function names).
		postinst|postinst_restart|postinst_nostart|prerm|prerm_norestart|postrm|\
		preinst_chmod|preinst_stop|recompile|recompile_and_update|\
		configure_bundle|scrub_all_bundles|start_service|stop_service|restart_service)
			"$cmd" "$@"
			;;
		*)
			echo "s6-helper: unknown command: $cmd" >&2
			usage >&2
			exit 1
			;;
	esac
}

main "$@"
