#!/usr/bin/env sh
# Kepeink agent simple-run bootstrap for Linux, macOS, and Termux.
#
#   curl -fsSL https://cdn.kepeink.hu/run.sh | sh -s -- --token=kpt_xxxxxxxx
#
# Downloads the current agent into the directory where you run the
# command as ./kepeink, then starts it in the foreground. See --help
# for flags.

set -eu

CDN="${KEPEINK_AGENT_CDN:-https://cdn.kepeink.hu}"

usage() {
	cat <<'EOF'
Kepeink agent simple-run bootstrap for Linux, macOS, and Termux.

  curl -fsSL https://cdn.kepeink.hu/run.sh | sh -s -- --token=kpt_xxxxxxxx

This downloads the current agent into the directory where you run the
command as ./kepeink, then starts it in the foreground. Pretty logs are
enabled by default for humans. Robot users can pass --machine-logs for
single-line logs and --log-level=warn for quieter output.

Flags:
  --token=kpt_xxx     Tunnel token (or KEPEINK_AGENT_TOKEN env).
  --target=host:port  Optional local fallback target.
  --edge-url=URL      Override the edge endpoint.
  --version=vX.Y.Z    Pin a version (skips sha256 verification).
  --output=PATH       Where to place the binary (default ./kepeink).
  --log-level=LEVEL   debug | info | warn | error.
  --machine-logs      Single-line logs instead of pretty logs.
  --no-auto-update    Disable the agent self-updater.
  --dry-run           Print what would happen, change nothing.
EOF
}

TOKEN=""
TARGET=""
EDGE_URL=""
FORCE_VERSION=""
OUTPUT_PATH=""
LOG_LEVEL=""
MACHINE_LOGS=0
NO_AUTO_UPDATE=0
DRY_RUN=0

while [ $# -gt 0 ]; do
	case "$1" in
		--token=*) TOKEN="${1#--token=}" ;;
		--token) TOKEN="$2"; shift ;;
		--target=*) TARGET="${1#--target=}" ;;
		--target) TARGET="$2"; shift ;;
		--edge-url=*) EDGE_URL="${1#--edge-url=}" ;;
		--edge-url) EDGE_URL="$2"; shift ;;
		--version=*) FORCE_VERSION="${1#--version=}" ;;
		--version) FORCE_VERSION="$2"; shift ;;
		--output=*) OUTPUT_PATH="${1#--output=}" ;;
		--output) OUTPUT_PATH="$2"; shift ;;
		--log-level=*) LOG_LEVEL="${1#--log-level=}" ;;
		--log-level) LOG_LEVEL="$2"; shift ;;
		--machine-logs) MACHINE_LOGS=1 ;;
		--no-auto-update) NO_AUTO_UPDATE=1 ;;
		--dry-run) DRY_RUN=1 ;;
		--help|-h)
			usage
			exit 0
			;;
		*)
			echo "unknown argument: $1" >&2
			exit 64
			;;
	esac
	shift
done

[ -z "$TOKEN" ] && TOKEN="${KEPEINK_AGENT_TOKEN:-}"
[ -z "$TARGET" ] && TARGET="${KEPEINK_AGENT_TARGET:-}"
[ -z "$EDGE_URL" ] && EDGE_URL="${KEPEINK_AGENT_EDGE_URL:-}"
[ -z "$LOG_LEVEL" ] && LOG_LEVEL="${KEPEINK_AGENT_LOG_LEVEL:-}"

OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "$OS" in
	linux|darwin) ;;
	*)
		echo "Unsupported OS: $OS" >&2
		echo "Supported: linux, darwin (macOS), android through Termux." >&2
		exit 2
		;;
esac

ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
	x86_64|amd64) GOARCH=amd64 ;;
	aarch64|arm64) GOARCH=arm64 ;;
	armv8l|armv7l|armv7|armhf|armv6l) GOARCH=arm ;;
	*)
		echo "Unsupported architecture: $ARCH_RAW" >&2
		echo "Supported: x86_64, aarch64, armv7." >&2
		exit 2
		;;
esac

IS_TERMUX=0
if [ -n "${PREFIX:-}" ] && [ "${PREFIX#*com.termux}" != "$PREFIX" ]; then
	IS_TERMUX=1
elif [ -d "/data/data/com.termux/files/usr/bin" ]; then
	IS_TERMUX=1
fi

# Termux reports `uname -s` = Linux and runs native ELF binaries, so
# it gets the ordinary static linux artifact. We deliberately do NOT
# ship a GOOS=android build: that binary is dynamically linked against
# /system/bin/linker64, and the linker interposes on argv/exe-path — so
# --version never parses (the flag scanner stops at the mangled argv[0])
# and the binary drops into the supervisor respawn loop instead of
# printing its version. A CGO-free GOOS=linux binary is fully static,
# has no linker interposition, and runs on the Android kernel via raw
# syscalls. IS_TERMUX still gates the supervisor choice below
# (phantom-process killer).
PLATFORM="${OS}/${GOARCH}"
PLATFORM_DIR="${OS}-${GOARCH}"

if [ -z "$OUTPUT_PATH" ]; then
	OUTPUT_PATH="./kepeink"
fi
case "$OUTPUT_PATH" in
	*/*) RUN_PATH="$OUTPUT_PATH" ;;
	*) RUN_PATH="./$OUTPUT_PATH" ;;
esac
OUTPUT_DIR="${RUN_PATH%/*}"
TOKEN_FILE="${OUTPUT_DIR}/kepeink.token"

# Progress bars only help a human watching the terminal.
if [ -t 2 ]; then
	SHOW_PROGRESS=1
else
	SHOW_PROGRESS=0
fi

# fetch_url <url> <out> — quiet fetch for small files (the manifest).
fetch_url() {
	_url="$1"; _out="$2"
	if command -v curl >/dev/null 2>&1; then
		curl --fail --silent --show-error --location \
			--connect-timeout 10 --retry 2 "$_url" -o "$_out"
	elif command -v wget >/dev/null 2>&1; then
		wget -q -T 30 -t 2 -O "$_out" "$_url"
	elif command -v busybox >/dev/null 2>&1 && busybox wget --help >/dev/null 2>&1; then
		busybox wget -q -T 30 -O "$_out" "$_url"
	elif command -v toybox >/dev/null 2>&1 && toybox wget --help >/dev/null 2>&1; then
		toybox wget -O "$_out" "$_url"
	else
		echo "No downloader found (need curl, wget, busybox wget, or toybox wget)" >&2
		return 1
	fi
}

# fetch_bin <url> <out> — big-artifact fetch: progress bar on a tty,
# connect timeout, stall abort (under 1 KiB/s for 30 s), retries.
fetch_bin() {
	_url="$1"; _out="$2"
	if command -v curl >/dev/null 2>&1; then
		if [ "$SHOW_PROGRESS" -eq 1 ]; then
			curl --fail --location --progress-bar \
				--connect-timeout 10 --retry 2 \
				--speed-limit 1024 --speed-time 30 \
				"$_url" -o "$_out"
		else
			curl --fail --location --silent --show-error \
				--connect-timeout 10 --retry 2 \
				--speed-limit 1024 --speed-time 30 \
				"$_url" -o "$_out"
		fi
	elif command -v wget >/dev/null 2>&1; then
		if [ "$SHOW_PROGRESS" -eq 1 ]; then
			wget -T 30 -t 2 -O "$_out" "$_url"
		else
			wget -q -T 30 -t 2 -O "$_out" "$_url"
		fi
	elif command -v busybox >/dev/null 2>&1 && busybox wget --help >/dev/null 2>&1; then
		if [ "$SHOW_PROGRESS" -eq 1 ]; then
			busybox wget -T 30 -O "$_out" "$_url"
		else
			busybox wget -q -T 30 -O "$_out" "$_url"
		fi
	elif command -v toybox >/dev/null 2>&1 && toybox wget --help >/dev/null 2>&1; then
		toybox wget -O "$_out" "$_url"
	else
		echo "No downloader found (need curl, wget, busybox wget, or toybox wget)" >&2
		return 1
	fi
}

human_size() {
	awk -v b="$1" 'BEGIN { printf "%.1f MB", b / 1048576 }'
}

if command -v sha256sum >/dev/null 2>&1; then
	SHA256() { sha256sum "$1" | awk '{print $1}'; }
elif command -v shasum >/dev/null 2>&1; then
	SHA256() { shasum -a 256 "$1" | awk '{print $1}'; }
else
	echo "No sha256 tool found (need sha256sum or shasum)" >&2
	exit 3
fi

# Self-test: confirm a binary runs and reports its version. Force
# worker-only mode and a hard timeout so a mis-parsed argv can never
# spin up the supervisor's respawn loop or hang the run.
selftest_version() {
	if command -v timeout >/dev/null 2>&1; then
		KEPEINK_AGENT_NO_SUPERVISOR=1 timeout 10 "$1" --version 2>&1
	else
		KEPEINK_AGENT_NO_SUPERVISOR=1 "$1" --version 2>&1
	fi
}

MANIFEST_TMP="$(mktemp)"
BIN_TMP=""
cleanup() {
	rm -f "$MANIFEST_TMP" "${BIN_TMP:-}"
}
# Some shells skip the EXIT trap when killed by a signal. Trap the
# signals explicitly so interrupted runs do not leave tmp binaries behind.
trap cleanup EXIT
trap 'cleanup; trap - INT; exit 130' INT
trap 'cleanup; exit 143' TERM
trap 'cleanup; exit 129' HUP

echo "[run] fetching manifest from $CDN/manifest.json"
if ! fetch_url "$CDN/manifest.json" "$MANIFEST_TMP"; then
	echo "manifest fetch failed — check your network and $CDN" >&2
	exit 4
fi

MANIFEST_ONE_LINE="$(tr -d '\n' <"$MANIFEST_TMP")"
VERSION="$(printf '%s\n' "$MANIFEST_ONE_LINE" | sed -n 's|.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*|\1|p')"
PLATFORM_ENTRY="$(printf '%s\n' "$MANIFEST_ONE_LINE" | sed -n "s|.*\"$PLATFORM\"[[:space:]]*:[[:space:]]*{\([^}]*\)}.*|\1|p")"
BIN_URL="$(printf '%s\n' "$PLATFORM_ENTRY" | sed -n 's|.*"url"[[:space:]]*:[[:space:]]*"\([^"]*\)".*|\1|p')"
EXPECTED_SHA="$(printf '%s\n' "$PLATFORM_ENTRY" | sed -n 's|.*"sha256"[[:space:]]*:[[:space:]]*"\([^"]*\)".*|\1|p')"
SIZE_BYTES="$(printf '%s\n' "$PLATFORM_ENTRY" | sed -n 's|.*"size"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*|\1|p')"
if [ -z "$VERSION" ] || [ -z "$BIN_URL" ]; then
	echo "manifest parse failed for platform $PLATFORM" >&2
	exit 4
fi

if [ -n "$FORCE_VERSION" ] && [ "$FORCE_VERSION" != "$VERSION" ]; then
	VERSION="$FORCE_VERSION"
	BIN_URL="$CDN/$VERSION/$PLATFORM_DIR/agent"
	EXPECTED_SHA=""
	SIZE_BYTES=""
	echo "[run] pinned version=$VERSION (sha256 verify SKIPPED)"
fi

LOCAL_VERSION=""
if [ -x "$RUN_PATH" ] && LOCAL_VERSION_OUT="$(selftest_version "$RUN_PATH")"; then
	LOCAL_VERSION="$(echo "$LOCAL_VERSION_OUT" | head -n1 | tr -d '[:space:]')"
fi
LOCAL_CURRENT=0
if [ "$LOCAL_VERSION" = "$VERSION" ]; then
	LOCAL_CURRENT=1
fi

if [ "$DRY_RUN" -eq 1 ]; then
	echo "[run] DRY-RUN: platform=$PLATFORM"
	if [ "$LOCAL_CURRENT" -eq 1 ]; then
		echo "[run] DRY-RUN: local $OUTPUT_PATH is already $VERSION; would skip download"
	else
		echo "[run] DRY-RUN: would download $BIN_URL"
	fi
	echo "[run] DRY-RUN: would write $OUTPUT_PATH"
	echo "[run] DRY-RUN: state_dir=$OUTPUT_DIR"
	echo "[run] DRY-RUN: pretty_logs=$([ "$MACHINE_LOGS" -eq 1 ] && echo disabled || echo enabled)"
	echo "[run] DRY-RUN: log_level=${LOG_LEVEL:-info}"
	exit 0
fi

NEED_TOKEN=0
if [ ! -f "$TOKEN_FILE" ]; then
	NEED_TOKEN=1
	if [ -z "$TOKEN" ]; then
		echo "KEPEINK_AGENT_TOKEN or --token is required for first run" >&2
		exit 64
	fi
fi

# Sweep partial downloads left behind by previously interrupted runs.
rm -f "${OUTPUT_PATH}".tmp.* 2>/dev/null || true

if [ "$LOCAL_CURRENT" -eq 1 ]; then
	echo "[run] existing $OUTPUT_PATH is already $VERSION; skipping download"
else
	BIN_TMP="${OUTPUT_PATH}.tmp.$$"
	if [ -n "$SIZE_BYTES" ]; then
		echo "[run] downloading agent $VERSION for $PLATFORM ($(human_size "$SIZE_BYTES"))"
	else
		echo "[run] downloading agent $VERSION for $PLATFORM"
	fi
	echo "[run] from $BIN_URL"
	if ! fetch_bin "$BIN_URL" "$BIN_TMP"; then
		echo "binary download failed — check your network and retry" >&2
		exit 5
	fi

	ACTUAL_SHA="$(SHA256 "$BIN_TMP")"
	if [ -n "$EXPECTED_SHA" ] && [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
		echo "sha256 mismatch:" >&2
		echo "  expected: $EXPECTED_SHA" >&2
		echo "  got:      $ACTUAL_SHA" >&2
		exit 6
	fi
	if [ -n "$EXPECTED_SHA" ]; then
		echo "[run] sha256 verified"
	fi

	chmod 0755 "$BIN_TMP"
	if ! BIN_VERSION_OUT="$(selftest_version "$BIN_TMP")"; then
		echo "downloaded binary failed --version self-test:" >&2
		echo "$BIN_VERSION_OUT" >&2
		exit 6
	fi
	BIN_VERSION_REPORTED="$(echo "$BIN_VERSION_OUT" | head -n1 | tr -d '[:space:]')"
	echo "[run] binary --version: $BIN_VERSION_REPORTED"

	mv -f "$BIN_TMP" "$OUTPUT_PATH"
	BIN_TMP=""
	chmod 0755 "$OUTPUT_PATH"
fi

export KEPEINK_AGENT_DIR="$OUTPUT_DIR"
unset KEPEINK_AGENT_TOKEN
[ -n "$TARGET" ] && export KEPEINK_AGENT_TARGET="$TARGET"
[ -n "$EDGE_URL" ] && export KEPEINK_AGENT_EDGE_URL="$EDGE_URL"
[ -n "$LOG_LEVEL" ] && export KEPEINK_AGENT_LOG_LEVEL="$LOG_LEVEL"
if [ "$NO_AUTO_UPDATE" -eq 1 ]; then
	export KEPEINK_AGENT_AUTO_UPDATE=0
fi
if [ "$IS_TERMUX" -eq 1 ]; then
	# Android 12+ kills app child processes in foreign process groups
	# (the "phantom process killer"), which silently reaps the
	# supervisor's spawned worker. Run worker-only on Termux: no child
	# processes, self-update falls back to in-place exec.
	export KEPEINK_AGENT_NO_SUPERVISOR=1
fi

set --
if [ "$NEED_TOKEN" -eq 1 ]; then
	set -- "$@" "--token" "$TOKEN"
fi
if [ "$MACHINE_LOGS" -eq 1 ]; then
	export KEPEINK_AGENT_PRETTY_LOGS=0
	set -- "$@" "--machine-logs"
else
	set -- "$@" "--pretty-logs"
fi
if [ "$NO_AUTO_UPDATE" -eq 1 ]; then
	set -- "$@" "--no-auto-update"
fi

if [ "$NEED_TOKEN" -eq 1 ]; then
	echo "[run] starting $OUTPUT_PATH and saving token to $TOKEN_FILE (Ctrl-C stops the agent)"
else
	echo "[run] starting $OUTPUT_PATH using $TOKEN_FILE (Ctrl-C stops the agent)"
fi
exec "$RUN_PATH" "$@"
