#!/bin/sh
# SecuBox Apps CLI
# Lists/installs/removes plugin manifests declared under plugins/*/manifest.json

set -eu

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
DEFAULT_PLUGINS_DIR="/usr/share/secubox/plugins"
if [ -n "${SECUBOX_PLUGINS_DIR:-}" ]; then
	PLUGINS_DIR="$SECUBOX_PLUGINS_DIR"
elif [ -d "$SCRIPT_DIR/../plugins" ]; then
	PLUGINS_DIR=$(cd "$SCRIPT_DIR/../plugins" && pwd)
else
	PLUGINS_DIR="$DEFAULT_PLUGINS_DIR"
fi
PKG_MGR=""
PKG_UPDATED=0

info() { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
err() { printf '[ERROR] %s\n' "$*" >&2; }

usage() {
	cat <<'USAGE'
SecuBox Apps CLI
Usage: secubox-app <command> [arguments]

Commands:
  list                     Show available app manifests
  show <app-id>            Display manifest details
  install <app-id>         Install required packages + run install action
  remove <app-id>          Remove packages listed in manifest
  status <app-id>          Show install state and run status action if defined
  update <app-id>          Run plugin update action or opkg upgrade
  validate                 Verify manifest schema/metadata

Environment:
  SECUBOX_PLUGINS_DIR   Override manifest directory (default: /usr/share/secubox/plugins)
USAGE
}

require_tool() {
	command -v "$1" >/dev/null 2>&1 || { err "Missing dependency: $1"; exit 1; }
}

require_jsonfilter() { require_tool jsonfilter; }

ensure_pkg_mgr() {
	if [ -n "$PKG_MGR" ]; then
		return
	fi
	if command -v opkg >/dev/null 2>&1; then
		PKG_MGR="opkg"
	elif command -v apk >/dev/null 2>&1; then
		PKG_MGR="apk"
	else
		err "Missing dependency: require opkg or apk"
		exit 1
	fi
}

pkg_update_once() {
	ensure_pkg_mgr
	[ "$PKG_UPDATED" -eq 1 ] && return
	case "$PKG_MGR" in
		opkg) opkg update >/dev/null ;;
		apk) apk update >/dev/null ;;
	esac
	PKG_UPDATED=1
}

pkg_is_installed() {
	ensure_pkg_mgr
	case "$PKG_MGR" in
		opkg) opkg status "$1" >/dev/null 2>&1 ;;
		apk) apk info -e "$1" >/dev/null 2>&1 ;;
	esac
}

pkg_install() {
	local pkg="$1"
	if pkg_is_installed "$pkg"; then
		info "$pkg already installed"
		return
	fi
	info "Installing $pkg"
	pkg_update_once
	case "$PKG_MGR" in
		opkg) opkg install "$pkg" ;;
		apk) apk add "$pkg" ;;
	esac
}

pkg_remove() {
	local pkg="$1"
	if ! pkg_is_installed "$pkg"; then
		info "$pkg not installed"
		return
	fi
	info "Removing $pkg"
	case "$PKG_MGR" in
		opkg) opkg remove "$pkg" ;;
		apk) apk del "$pkg" ;;
	esac
}

pkg_upgrade() {
	case "$PKG_MGR" in
		opkg) opkg upgrade "$1" ;;
		apk) apk upgrade "$1" ;;
	esac
}

manifest_files() {
	if [ -d "$PLUGINS_DIR/catalog" ]; then
		for file in "$PLUGINS_DIR"/catalog/*.json; do
			[ -f "$file" ] || continue
			echo "$file"
		done
	fi
	for file in "$PLUGINS_DIR"/*/manifest.json; do
		[ -f "$file" ] || continue
		echo "$file"
	done
}

manifest_path() {
	local id="$1"
	local catalog="$PLUGINS_DIR/catalog/$id.json"
	local legacy="$PLUGINS_DIR/$id/manifest.json"
	if [ -f "$catalog" ]; then
		printf '%s' "$catalog"
		return 0
	fi
	if [ -f "$legacy" ]; then
		printf '%s' "$legacy"
		return 0
	fi
	err "Manifest not found for '$id'"
	exit 1
}

manifest_field() {
	local file="$1"; shift
	cat "$file" | jsonfilter -e "$1" 2>/dev/null || true
}

manifest_packages() {
	local file="$1"
	cat "$file" | jsonfilter -e '@.packages[*]' 2>/dev/null || true
}

manifest_action() {
	local file="$1"; shift
	cat "$file" | jsonfilter -e "@.actions.$1" 2>/dev/null || true
}

manifest_runtime() {
	local file="$1"
	local value
	value=$(manifest_field "$file" '@.type')
	[ -n "$value" ] || value=$(manifest_field "$file" '@.runtime')
	printf '%s' "$value"
}

validate_manifest_file() {
	require_jsonfilter
	local file="$1"
	local id name runtime packages category
	id=$(manifest_field "$file" '@.id')
	name=$(manifest_field "$file" '@.name')
	category=$(manifest_field "$file" '@.category')
	runtime=$(manifest_runtime "$file")
	packages=$(manifest_packages "$file")
	if [ -z "$id" ] || [ -z "$name" ] || [ -z "$runtime" ]; then
		warn "Skipping manifest $file (missing id/name/runtime)"
		return 1
	fi
	if [ -z "$packages" ]; then
		warn "Manifest $id has no packages defined"
		return 1
	fi
	if [ -z "$category" ]; then
		warn "Manifest $id missing category"
		return 1
	fi
	return 0
}

plugin_state() {
	local file="$1"
	local pkgs pkg missing=0 installed=0 total=0
	pkgs=$(manifest_packages "$file")
	for pkg in $pkgs; do
		total=$((total+1))
		if pkg_is_installed "$pkg"; then
			installed=$((installed+1))
		else
			missing=$((missing+1))
		fi
	 done
	if [ "$total" -eq 0 ]; then
		echo "n/a"
	elif [ "$missing" -eq 0 ]; then
		echo "installed"
	elif [ "$installed" -eq 0 ]; then
		echo "missing"
	else
		echo "partial"
	fi
}

list_plugins() {
	require_jsonfilter
	ensure_pkg_mgr
	local seen_ids=""
	printf '%-18s %-24s %-10s %-10s %-10s\n' "ID" "Name" "Runtime" "Category" "State"
	printf '%-18s %-24s %-10s %-10s %-10s\n' "--" "----" "-------" "--------" "-----"
	while IFS= read -r file; do
		[ -f "$file" ] || continue
		if ! validate_manifest_file "$file"; then
			continue
		fi
		local id name runtime state category
		id=$(manifest_field "$file" '@.id')
		name=$(manifest_field "$file" '@.name')
		runtime=$(manifest_runtime "$file")
		category=$(manifest_field "$file" '@.category')
		case " $seen_ids " in
			*" $id "*) continue ;;
		esac
		seen_ids="$seen_ids $id"
		state=$(plugin_state "$file")
		printf '%-18s %-24s %-10s %-10s %-10s\n' "$id" "${name:-Unknown}" "${runtime:-unknown}" "${category:-?}" "$state"
	done <<EOF
$(manifest_files | sort)
EOF
}

show_manifest() {
	require_jsonfilter
	local file=$(manifest_path "$1")
	cat "$file"
}

install_plugin() {
	require_jsonfilter
	ensure_pkg_mgr
	local file=$(manifest_path "$1")
	local pkgs pkg
	pkgs=$(manifest_packages "$file")
	if [ -z "$pkgs" ]; then
		warn "Manifest has no packages; nothing to install."
	else
		for pkg in $pkgs; do
			pkg_install "$pkg"
		done
	fi
	local install_cmd
	install_cmd=$(manifest_action "$file" install)
	if [ -n "$install_cmd" ]; then
		info "Running install action: $install_cmd"
		sh -c "$install_cmd"
	fi
}

remove_plugin() {
	require_jsonfilter
	ensure_pkg_mgr
	local file=$(manifest_path "$1")
	local pkgs pkg
	pkgs=$(manifest_packages "$file")
	for pkg in $pkgs; do
		pkg_remove "$pkg"
	done
}

plugin_status_cmd() {
	require_jsonfilter
	local file=$(manifest_path "$1")
	local status_cmd
	status_cmd=$(manifest_action "$file" status)
	local state
	state=$(plugin_state "$file")
	printf 'App: %s\nState: %s\n' "$1" "$state"
	if [ -n "$status_cmd" ]; then
		printf 'Status command output:\n'
		sh -c "$status_cmd" || true
	fi
}

update_plugin() {
	require_jsonfilter
	ensure_pkg_mgr
	local file=$(manifest_path "$1")
	local update_cmd pkgs pkg
	update_cmd=$(manifest_action "$file" update)
	if [ -n "$update_cmd" ]; then
		info "Running update action: $update_cmd"
		sh -c "$update_cmd"
	else
		pkgs=$(manifest_packages "$file")
		for pkg in $pkgs; do
			info "Upgrading $pkg"
			pkg_update_once
			pkg_upgrade "$pkg" || true
		done
	fi
}

validate_manifests_cmd() {
	require_jsonfilter
	local failed=0 passed=0 file
	while IFS= read -r file; do
		[ -f "$file" ] || continue
		if validate_manifest_file "$file"; then
			passed=$((passed + 1))
		else
			failed=$((failed + 1))
		fi
	done <<EOF
$(manifest_files)
EOF
	info "Validated $passed manifest(s)"
	if [ "$failed" -gt 0 ]; then
		err "$failed manifest(s) failed validation"
		return 1
	fi
	return 0
}

case "${1:-}" in
	list) shift; list_plugins ;;
	show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;;
	install) shift; [ $# -ge 1 ] || { err "install requires an app id"; exit 1; }; install_plugin "$1" ;;
	remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;;
	status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;;
	update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;;
	validate) shift; if ! validate_manifests_cmd; then exit 1; fi ;;
	help|--help|-h|'') usage ;;
	*) err "Unknown command: $1"; usage; exit 1 ;;
esac
