#!/bin/sh
#
# APKG-BIN - Alice Binary Package Manager (C) 2023-2026 Emmett1
#
# Fetches pre-built .spm packages from a binary repo, resolves dependencies,
# and installs via spm. Alternative to source-based 'apkg'.

msg() {
	printf "%s\n" "[apkg-bin] $*" >&2
}

die() {
	[ "$1" ] && printf "%s\n" "error: $1" >&2
	exit 1
}

needroot() {
	[ "$(id -u)" = 0 ] || die "This operation requires root access"
}

prompt_user() {
	[ "$APKG_NOPROMPT" ] && return
	msg "Press ENTER to continue. Press Ctrl+C to abort."
	read -r _null
}

needbinsrc() {
	[ "$APKGBIN_REPO" ] || die "APKGBIN_REPO is not set"
}

needcachedb() {
	[ -f "$APKGBIN_CACHE_DIR/APKGBINDB" ] || die "No cached APKGBINDB found. Run 'apkg-bin -S' first."
	[ -r "$APKGBIN_CACHE_DIR/APKGBINDB" ] || die "No read permission on $APKGBIN_CACHE_DIR/APKGBINDB"
}

pkg_is_installed() {
	[ -s "$SPM_PKGDB/$1" ]
}

pkg_installed_ver() {
	head -n1 "$SPM_PKGDB/$1" 2>/dev/null
}

# get field from a APKGBINDB line: bin_fetch_field <line> <fieldnum>
# 1=namever, 2=size, 3=sha3sum, 4=deps, 5=preinstall, 6=postinstall, 7=desc
bin_fetch_field() {
	_line=$1
	_field=$2
	_rest=$_line
	_i=1
	while [ "$_i" -lt "$_field" ]; do
		_rest=${_rest#*|}
		_i=$((_i + 1))
	done
	case $_field in
		7) printf "%s\n" "${_rest#*|}" ;;
		*) printf "%s\n" "${_rest%%|*}" ;;
	esac
}

# find a package name in APKGBINDB, output the full line
bin_find_pkg() {
	_pkg=$1
	_line=$(grep "^$_pkg#" "$APKGBIN_CACHE_DIR/APKGBINDB" 2>/dev/null | head -n1)
	[ "$_line" ] && printf "%s\n" "$_line"
}

# get full name#version-release for a package name
bin_get_pkgid() {
	_line=$(bin_find_pkg "$1") || return 1
	bin_fetch_field "$_line" 1
}

# get deps for a package (space-separated names) from APKGBINDB only
bin_get_deps() {
	_line=$(bin_find_pkg "$1" 2>/dev/null)
	if [ "$_line" ]; then
		_deps=$(bin_fetch_field "$_line" 4)
		if [ "$_deps" ]; then
			printf "%s\n" "$_deps" | tr ',' ' '
		fi
	fi
}

# get description for a package
bin_get_desc() {
	_line=$(bin_find_pkg "$1") || return 0
	bin_fetch_field "$_line" 7
}

# get preinstall script (base64) for a package
bin_get_preinstall() {
	_line=$(bin_find_pkg "$1" 2>/dev/null) || return 0
	bin_fetch_field "$_line" 5
}

# get postinstall script (base64) for a package
bin_get_postinstall() {
	_line=$(bin_find_pkg "$1" 2>/dev/null) || return 0
	bin_fetch_field "$_line" 6
}

# run a pre/post install script from base64
bin_run_script() {
	_name=$1
	_b64=$2
	_tag=$3  # "pre" or "post"
	[ "$_b64" ] || return 0
	msg "Running ${_tag}install script for $_name ..."
	printf "%s\n" "$_b64" | base64 -d | sh -e || {
		msg "Warning: ${_tag}install script for $_name failed"
	}
}

solve_alias() {
	[ "$APKG_ALIAS" ] || {
		printf "%s\n" "$@"
		return
	}
	for _a in "$@"; do
		_d=$(printf "%s\n" $APKG_ALIAS | tr ' ' '\n' | grep "^$_a:" | head -n1 | awk -F : '{print $2}')
		printf "%s\n" "${_d:-$_a}"
	done
}

bin_checkdep() {
	# skip packages not in APKGBINDB
	if ! bin_find_pkg "$1" >/dev/null 2>&1; then
		[ "$_process" ] && msg "Warning: '$1' not found in APKGBINDB, skipping"
		return
	fi
	_process="$_process $1"
	for _d in $(solve_alias $(bin_get_deps "$1")); do
		[ "$_d" = "$1" ] && continue
		if [ "$_skip_installed" ]; then
			pkg_is_installed "$_d" && continue
		fi
		# cycle detection
		printf "%s\n" $_process | tr ' ' '\n' | grep -Fxq "$_d" && continue
		# already resolved
		printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$_d" && continue
		bin_checkdep "$_d"
	done
	printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$1" || DEPS="$DEPS $1"
}

bin_deplist() {
	DEPS=""
	_process=""
	for _p in "$@"; do
		printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$_p" || bin_checkdep "$_p"
	done
	printf "%s\n" $DEPS | tr ' ' '\n' | grep -v ^$
}

bin_sync() {
	needbinsrc
	mkdir -p "$APKGBIN_CACHE_DIR" 2>/dev/null
	[ -w "$APKGBIN_CACHE_DIR" ] || die "No write permission on $APKGBIN_CACHE_DIR"
	case $APKGBIN_REPO in
		http://*|https://*|ftp://*)
			msg "Syncing $APKGBIN_REPO ..."
			curl -fL -o "$APKGBIN_CACHE_DIR/APKGBINDB.tmp" "$APKGBIN_REPO/APKGBINDB" || {
				rm -f "$APKGBIN_CACHE_DIR/APKGBINDB.tmp"
				die "Failed to fetch APKGBINDB from $APKGBIN_REPO"
			}
			mv "$APKGBIN_CACHE_DIR/APKGBINDB.tmp" "$APKGBIN_CACHE_DIR/APKGBINDB"
			;;
		*)
			[ -f "$APKGBIN_REPO/APKGBINDB" ] || die "No APKGBINDB found at $APKGBIN_REPO"
			msg "Syncing $APKGBIN_REPO (local) ..."
			cp "$APKGBIN_REPO/APKGBINDB" "$APKGBIN_CACHE_DIR/APKGBINDB"
			;;
	esac
	msg "Sync complete."
}

bin_generate() {
	[ "$APKG_REPO" ] || die "APKG_REPO is not set (needed to read abuild metadata)"
	[ -d "$APKG_PACKAGE_DIR" ] || die "APKG_PACKAGE_DIR ($APKG_PACKAGE_DIR) not found"
	[ -r "$APKG_PACKAGE_DIR" ] || die "No read permission on $APKG_PACKAGE_DIR"
	[ -w "$APKG_PACKAGE_DIR" ] || die "No write permission on $APKG_PACKAGE_DIR"
	mkdir -p "$APKGBIN_CACHE_DIR" 2>/dev/null
	[ -w "$APKGBIN_CACHE_DIR" ] || die "No write permission on $APKGBIN_CACHE_DIR"

	_output="${APKG_PACKAGE_DIR}/APKGBINDB"
	> "$_output"

	_count=0
	for _spm in "$APKG_PACKAGE_DIR"/*.spm; do
		[ -f "$_spm" ] || continue
		_filename=${_spm##*/}
		_namever=${_filename%.spm}
		_name=${_namever%%#*}
		_size=$(stat -c%s "$_spm" 2>/dev/null || wc -c < "$_spm")
		_sha3=$(sha3sum "$_spm" 2>/dev/null | awk '{print $1}')

		_deps=""
		for _r in $APKG_REPO; do
			if [ -f "$_r/$_name/depends" ]; then
				for _d in $(grep -Ev '^(#|$)' "$_r/$_name/depends" | awk '{print $1}'); do
					for _rr in $APKG_REPO; do
						[ -d "$_rr/$_d" ] && { _deps="$_deps$_d,"; break; }
					done
				done
				_deps=${_deps%,}
				break
			fi
		done

		_desc=""
		_pre=""
		_post=""
		for _r in $APKG_REPO; do
			[ -f "$_r/$_name/info" ] && [ ! "$_desc" ] && \
				_desc=$(grep '^description:' "$_r/$_name/info" | sed 's/^description:[[:space:]]*//')
			[ -f "$_r/$_name/preinstall" ] && [ ! "$_pre" ] && \
				_pre=$(base64 "$_r/$_name/preinstall" 2>/dev/null | tr -d '\n')
			[ -f "$_r/$_name/postinstall" ] && [ ! "$_post" ] && \
				_post=$(base64 "$_r/$_name/postinstall" 2>/dev/null | tr -d '\n')
		done

		printf "%s|%s|%s|%s|%s|%s|%s\n" "$_namever" "$_size" "$_sha3" "$_deps" "$_pre" "$_post" "$_desc" >> "$_output"
		_count=$((_count + 1))
	done

	# also cache locally so -l/-s/-i work immediately
	mkdir -p "$APKGBIN_CACHE_DIR"
	[ "$_output" = "$APKGBIN_CACHE_DIR/APKGBINDB" ] || cp "$_output" "$APKGBIN_CACHE_DIR/APKGBINDB"
	msg "Wrote APKGBINDB with $_count entries to $_output"
}

bin_download_pkg() {
	_pkgid=$1
	_name=${_pkgid%%#*}
	_pkgfile="$_pkgid.spm"

	mkdir -p "$APKGBIN_CACHE_DIR" 2>/dev/null
	[ -w "$APKGBIN_CACHE_DIR" ] || die "No write permission on $APKGBIN_CACHE_DIR"

	case $APKGBIN_REPO in
		http://*|https://*|ftp://*)
			_urlfile=$(printf "%s\n" "$_pkgfile" | sed 's/#/%23/g')
			_target="$APKGBIN_CACHE_DIR/$_pkgfile"
			if [ "$force" ] || [ ! -f "$_target" ]; then
				msg "Downloading $APKGBIN_REPO/$_pkgfile"
				curl -fL -o "$_target.tmp" "$APKGBIN_REPO/$_urlfile" || {
					rm -f "$_target.tmp"
					die "Failed to download $_pkgfile from $APKGBIN_REPO"
				}
				mv "$_target.tmp" "$_target"
			fi
			;;
		*)
			_target="$APKGBIN_REPO/$_pkgfile"
			[ -f "$_target" ] || die "Package file not found: $_target"
			;;
	esac

	# verify sha3sum if available in APKGBINDB
	_line=$(bin_find_pkg "$_name")
	_expected=$(bin_fetch_field "$_line" 3)
	if [ "$_expected" ]; then
		_actual=$(sha3sum "$_target" | awk '{print $1}')
		[ "$_actual" = "$_expected" ] || die "sha3sum mismatch for $_pkgfile"
	fi

	printf "%s\n" "$_target"
}

bin_install() {
	_resolve=${1:-1}; shift  # 1=resolve deps, 0=no deps
	needroot
	needcachedb
	[ "$1" ] || die "No packages specified"

	_todo=""
	for _p in "$@"; do
		if pkg_is_installed "$_p"; then
			msg "Package '$_p' is already installed. Skipping."
		else
			_todo="$_todo $_p"
		fi
	done
	[ "$_todo" ] || { msg "Nothing to install."; exit 0; }

	if [ "$_resolve" = 1 ]; then
		msg "Solving dependencies..."
		_skip_installed=1
		_deps=$(bin_deplist $_todo) || true
		_skip_installed=""
	else
		_deps=$_todo
	fi
	[ "$_deps" ] || { msg "Nothing to install."; exit 0; }

	_total=$(printf "%s\n" "$_deps" | wc -l)
	msg "Installing $_total package(s): $(printf "%s\n" "$_deps" | tr '\n' ' ')"
	prompt_user

	for _d in $_deps; do
		_pkgid=$(bin_get_pkgid "$_d") || continue
		_pkgpath=$(bin_download_pkg "$_pkgid") || continue
		bin_run_script "$_d" "$(bin_get_preinstall "$_d")" pre
		msg "Installing $_pkgid ..."
		SPM_ROOT=${APKG_ROOT%/} spm -i "$_pkgpath" || die "Failed to install $_pkgid"
		bin_run_script "$_d" "$(bin_get_postinstall "$_d")" post
	done
}

bin_upgrade() {
	needroot
	needcachedb
	[ "$1" ] || die "No packages specified"

	for _p in "$@"; do
		if ! pkg_is_installed "$_p"; then
			msg "Package '$_p' not installed. Skipping."
			continue
		fi
		_pkgid=$(bin_get_pkgid "$_p") || {
			msg "Package '$_p' not found in APKGBINDB. Skipping."
			continue
		}
		_pkgpath=$(bin_download_pkg "$_pkgid") || continue
		_installed=$(pkg_installed_ver "$_p")
		_available=${_pkgid#*#}
		[ "$_installed" = "$_available" ] && msg "Package '$_p' is up to date, reinstalling."
		bin_run_script "$_p" "$(bin_get_preinstall "$_p")" pre
		msg "Upgrading $_pkgid ..."
		SPM_ROOT=${APKG_ROOT%/} spm -u "$_pkgpath" || die "Failed to upgrade $_pkgid"
		bin_run_script "$_p" "$(bin_get_postinstall "$_p")" post
	done
}

bin_sysupgrade() {
	needroot
	needcachedb

	msg "Checking for outdated packages..."
	_outdated=$(_apply_mask=1 bin_list_outdated | awk '{print $1}')
	[ "$_outdated" ] || { msg "No outdated packages."; exit 0; }

	msg "Resolving dependencies..."
	_skip_installed=""
	DEPS=""
	for _name in $_outdated; do
		printf "%s\n" $DEPS | tr ' ' '\n' | grep -Fxq "$_name" || {
			_process=""
			bin_checkdep "$_name"
		}
	done
	_all=$(printf "%s\n" $DEPS | tr ' ' '\n' | grep -v ^$ | sort -u)

	_newpkgs=""
	_upgradepkgs=""
	_seen_new=""
	_seen_up=""
	for _name in $_all; do
		if pkg_is_installed "$_name"; then
			printf "%s\n" $_seen_up | tr ' ' '\n' | grep -Fxq "$_name" && continue
			_seen_up="$_seen_up $_name"
			_pkgid=$(bin_get_pkgid "$_name") || continue
			_installed=$(pkg_installed_ver "$_name")
			_available=${_pkgid#*#}
			[ "$_installed" = "$_available" ] && continue
			_upgradepkgs="$_upgradepkgs $_name"
		else
			printf "%s\n" $_seen_new | tr ' ' '\n' | grep -Fxq "$_name" && continue
			_seen_new="$_seen_new $_name"
			_newpkgs="$_newpkgs $_name"
		fi
	done

	[ "$_newpkgs" ] && msg "Installing new package(s): $_newpkgs"
	[ "$_upgradepkgs" ] || { msg "All packages up to date."; exit 0; }
	_ncount=$(printf "%s\n" $_upgradepkgs | wc -l)
	msg "Upgrading $_ncount package(s): $(printf "%s\n" $_upgradepkgs | tr '\n' ' ')"
	prompt_user

	if [ "$_newpkgs" ]; then
		for _name in $_newpkgs; do
			_pkgid=$(bin_get_pkgid "$_name") || continue
			_pkgpath=$(bin_download_pkg "$_pkgid") || continue
			bin_run_script "$_name" "$(bin_get_preinstall "$_name")" pre
			msg "Installing $_pkgid ..."
			SPM_ROOT=${APKG_ROOT%/} spm -i "$_pkgpath" || die "Failed to install $_pkgid"
			bin_run_script "$_name" "$(bin_get_postinstall "$_name")" post
		done
	fi
	for _name in $_upgradepkgs; do
		_pkgid=$(bin_get_pkgid "$_name") || continue
		_pkgpath=$(bin_download_pkg "$_pkgid") || continue
		bin_run_script "$_name" "$(bin_get_preinstall "$_name")" pre
		msg "Upgrading $_pkgid ..."
		SPM_ROOT=${APKG_ROOT%/} spm -u "$_pkgpath" || die "Failed to upgrade $_pkgid"
		bin_run_script "$_name" "$(bin_get_postinstall "$_name")" post
	done

	msg "System upgrade complete."
}

bin_list_outdated() {
	needcachedb
	[ -d "$SPM_PKGDB" ] || return 0
	for _db in "$SPM_PKGDB"/*; do
		[ -f "$_db" ] || continue
		_name=${_db##*/}
		if [ "$APKG_MASK" ] && [ "$_apply_mask" ]; then
			printf "%s\n" "$APKG_MASK" | tr ' ' '\n' | grep -Fxq "$_name" && continue
		fi
		_installed=$(head -n1 "$_db")
		_line=$(bin_find_pkg "$_name") || continue
		_namever=$(bin_fetch_field "$_line" 1)
		_available=${_namever#*#}
		[ "$_installed" = "$_available" ] || printf "%s\n" "$_name $_installed -> $_available"
	done
}

bin_search() {
	needcachedb
	_pattern=$1
	while IFS= read -r _line; do
		[ "$_line" ] || continue
		_namever=$(bin_fetch_field "$_line" 1)
		_name=${_namever%%#*}
		_display=$(printf "%s\n" "$_namever" | tr '#' ' ')
		if [ "$_pattern" ]; then
			printf "%s\n" "$_name" | grep -q "$_pattern" || continue
		fi
		if [ "$verbose" ]; then
			_desc=$(bin_fetch_field "$_line" 7)
			printf "%s  %s\n" "$_display" "$_desc"
		else
			printf "%s\n" "$_name"
		fi
	done < "$APKGBIN_CACHE_DIR/APKGBINDB"
}

bin_list_installed() {
	[ -d "$SPM_PKGDB" ] || return 0
	for _db in "$SPM_PKGDB"/*; do
		[ -f "$_db" ] || continue
		_name=${_db##*/}
		if [ "$verbose" ]; then
			_ver=$(head -n1 "$_db")
			printf "%s  %s\n" "$_name" "$_ver"
		else
			printf "%s\n" "$_name"
		fi
	done
}

bin_clean() {
	needcachedb
	_dldir="$APKGBIN_CACHE_DIR"
	[ -d "$_dldir" ] || { msg "No downloaded packages to clean."; exit 0; }
	_count=0
	for _spm in "$_dldir"/*.spm; do
		[ -f "$_spm" ] || continue
		_filename=${_spm##*/}
		_name=${_filename%%#*}
		_line=$(bin_find_pkg "$_name" 2>/dev/null)
		[ "$_line" ] || continue
		_expected=$(bin_fetch_field "$_line" 3)
		[ "$_expected" ] || continue
		_actual=$(sha3sum "$_spm" | awk '{print $1}')
		if [ "$_actual" != "$_expected" ]; then
			msg "Removing $_filename (sha3sum mismatch)"
			rm -f "$_spm"
			_count=$((_count + 1))
		fi
	done
	msg "Removed $_count package(s) with sha3sum mismatch."
}

bin_show_deps() {
	needcachedb
	[ "$1" ] || die "No package specified"
	_deps=$(bin_get_deps "$1")
	[ "$_deps" ] && printf "%s\n" $_deps
}

bin_show_deptree() {
	needcachedb
	[ "$1" ] || die "No package specified"
	[ "$(bin_find_pkg "$1")" ] || exit 0
	_deps=$(bin_deplist "$1")
	printf "%s\n" "$_deps" | grep -Fxq "$1" || _deps="$_deps
$1"
	printf "%s\n" "$_deps" | grep -v ^$
}

bin_help() {
	cat << 'EOF'
usage: apkg-bin <option> [arg(s)]

options:
  -g             generate APKGBINDB from APKG_PACKAGE_DIR and APKG_REPO
  -S             sync APKGBINDB from APKGBIN_REPO
  -s [pattern]   search available binary packages (optional filter)
  -i <pkg(s)>    install package(s) without dependency resolution
  -I <pkg(s)>    install package(s) with dependency resolution
  -u <pkg(s)>    upgrade package(s)
  -U             full system upgrade (all outdated packages)
  -o <pkg(s)>    download package(s) only (no install)
  -c             clean downloaded packages with sha3sum mismatch
  -d <pkg>       show direct dependencies for a package
  -D <pkg(s)>    show full dependency tree for package(s)
  -l             list outdated packages
  -a             list all installed packages
  -f             force re-download of cached packages
  -v             verbose output
  -h             print this help message

environment variables:
  APKGBIN_REPO         binary repo URL or path
  APKGBIN_CACHE_DIR    cache directory (required)
  APKG_REPO            source repo directories (needed for -g)
  APKG_PACKAGE_DIR     package directory (default: $PWD)
  APKG_ROOT            alternative install root
  APKG_MASK            packages to skip during -U (system upgrade)
  APKG_ALIAS           dependency substitution (real:alias pairs)
  APKG_NOPROMPT        skip confirmation prompts
EOF
}

_filter_flags() {
	_args=""
	for _a in "$@"; do
		case $_a in
			-f) force=1 ;;
			-v) verbose=1 ;;
			*) _args="$_args $_a" ;;
		esac
	done
}

parseopts() {
	while [ "$1" ]; do
		case $1 in
			-g) bin_generate; exit 0;;
			-S) bin_sync; exit 0;;
			-s) _do_search=1; if [ "$2" ] && [ "${2#-}" = "$2" ]; then search_pattern="$2"; shift; fi;;
			-i) shift; _filter_flags "$@"; bin_install 0 $_args; exit 0;;
			-I) shift; _filter_flags "$@"; bin_install 1 $_args; exit 0;;
			-u) shift; _filter_flags "$@"; bin_upgrade $_args; exit 0;;
			-U) bin_sysupgrade; exit 0;;
			-o) shift; _filter_flags "$@"; needcachedb; for _p in $_args; do _pkgid=$(bin_get_pkgid "$_p") || die "Package '$_p' not found in APKGBINDB"; bin_download_pkg "$_pkgid"; done; exit 0;;
			-c) bin_clean; exit 0;;
			-d) bin_show_deps "$2"; exit 0;;
			-D) shift; _filter_flags "$@"; needcachedb; _valid=""; for _p in $_args; do [ "$(bin_find_pkg "$_p")" ] && _valid="$_valid $_p"; done; [ "$_valid" ] || exit 0; _out=$(bin_deplist $_valid); for _p in $_valid; do printf "%s\n" "$_out" | grep -Fxq "$_p" || _out="$_out
$_p"; done; printf "%s\n" "$_out" | grep -v ^$; exit 0;;
			-l) bin_list_outdated; exit 0;;
			-a) bin_list_installed; exit 0;;
			-f) force=1;;
			-v) verbose=1;;
			-h) bin_help; exit 0;;
			-*) die "invalid option '$1'";;
			 *) pkg="$pkg $1";;
		esac
		shift
	done
}

main() {
	parseopts "$@"

	if [ "$_do_search" ]; then
		bin_search "$search_pattern"
		exit 0
	fi

	bin_help
	exit 0
}

umask 022

SPM_PKGDB="${APKG_ROOT%/}/var/lib/spm/db"
APKG_PACKAGE_DIR="${APKG_PACKAGE_DIR:-$PWD}"
[ "$APKGBIN_CACHE_DIR" ] || die "APKGBIN_CACHE_DIR is not set"

main "$@"

exit 0
