#!/usr/bin/env sh
# Kepeink agent installer — one-line install for Linux and macOS.
#
#   curl -fsSL https://cdn.kepeink.hu/install.sh | sudo sh
#
# With token inline (skips the post-install prompt):
#
#   curl -fsSL https://cdn.kepeink.hu/install.sh | sudo sh -s -- \
#     --token=kpt_xxxxxxxx
#
# See --help (or usage() below) for modes and flags.

set -eu

# ── Defaults ──────────────────────────────────────────────────────

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

usage() {
	cat <<'EOF'
Kepeink agent installer — one-line install for Linux and macOS.

  curl -fsSL https://cdn.kepeink.hu/install.sh | sudo sh

With token inline (skips the post-install prompt):

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

Service installs use single-line machine logs by default. Use
--log-level=debug only while troubleshooting; simple foreground runs
default to pretty logs through run.sh instead.

Route configuration is fetched from the server on each session. A
local target remains accepted as a compatibility fallback:
  --target=localhost:8080         (most common)
  --target=192.168.1.5:8080       (LAN device)
  --target=10.0.0.1:5432          (private network database)

Modes:
  default        On Linux + macOS as root: install system-wide
                 (binary in /opt/kepeink, config in /etc/kepeink-agent,
                 service via systemd or launchd, auto-start at boot).
  --user         On Linux + macOS without root: install per-user
                 (binary in ~/.local/bin, config in ~/.config,
                 service via `systemctl --user` or LaunchAgent).
                 No sudo required. Auto-start at boot only when
                 the user is logged in (or `loginctl enable-linger
                 $USER` is set on Linux).
Other flags: --edge-url=URL, --version=vX.Y.Z (skips sha256 verify),
--log-level=LEVEL, --no-auto-update, --dry-run.

Re-running this script upgrades in place. Token / fallback target are
preserved across re-runs.
EOF
}

TOKEN=""
TARGET=""
EDGE_URL=""
FORCE_VERSION=""
USER_MODE=0
DRY_RUN=0
NO_AUTO_UPDATE=0
LOG_LEVEL=""

# ── Argument parsing ──────────────────────────────────────────────

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 ;;
		--log-level=*) LOG_LEVEL="${1#--log-level=}" ;;
		--log-level) LOG_LEVEL="$2"; shift ;;
		--user) USER_MODE=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

# Env-var fallbacks (lower precedence than CLI flags).
[ -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:-}"

# ── Platform detection ────────────────────────────────────────────

OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
case "$OS" in
	linux|darwin) ;;
	*)
		echo "Unsupported OS: $OS" >&2
		echo "Supported: linux, darwin (macOS). Windows uses install.ps1." >&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

# ── Helper functions (must be defined before use) ────────────────

update_env_file() {
	_path="$1"; _key="$2"; _val="$3"
	_tmp="${_path}.tmp.$$"
	_found=0
	if [ -f "$_path" ]; then
		while IFS= read -r _line || [ -n "$_line" ]; do
			case "$_line" in
				"$_key"=*) printf '%s=%s\n' "$_key" "$_val"; _found=1 ;;
				*) printf '%s\n' "$_line" ;;
			esac
		done <"$_path" >"$_tmp"
	else
		: >"$_tmp"
	fi
	if [ "$_found" -eq 0 ]; then
		printf '%s=%s\n' "$_key" "$_val" >>"$_tmp"
	fi
	mv -f "$_tmp" "$_path"
}

# restart_hint_for_mode prints the appropriate "manually restart"
# command for the current $MODE. Set after mode is chosen.
restart_hint_for_mode() {
	case "$MODE" in
		linux-system)  echo "sudo systemctl restart $SERVICE_NAME" ;;
		linux-user)    echo "systemctl --user restart $SERVICE_NAME" ;;
		darwin-system) echo "sudo launchctl kickstart -k system/hu.kepeink.agent" ;;
		darwin-user)   echo "launchctl kickstart -k gui/$(id -u)/hu.kepeink.agent" ;;
	esac
}

# ── Mode selection ────────────────────────────────────────────────

# Termux auto-detection — highest priority. Termux's PREFIX is the
# unmistakable marker, plus the canonical filesystem root provides
# a reliable cross-check.
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

if [ "$IS_TERMUX" -eq 1 ]; then
	echo "Android/Termux is not currently supported." >&2
	echo "The attempted executable formats fail on real Android loaders; see docs/agent-build-platforms.md." >&2
	exit 2
elif [ "$USER_MODE" -eq 1 ]; then
	MODE="${OS}-user"
elif [ "$(id -u)" -eq 0 ]; then
	MODE="${OS}-system"
else
	# Non-root, no --user. Fail fast with a clear hint instead of
	# silently degrading to user-mode (which the operator may not
	# expect — they piped to `sudo sh` then sudo wasn't available).
	echo "Not running as root and --user not supplied." >&2
	echo "Either rerun with sudo (system-wide install) or pass --user (per-user install)." >&2
	exit 1
fi

# Download keys normally match uname. Termux is rejected above until a
# device-verified Android artifact exists.
ARTIFACT_OS="$OS"

echo "[install] platform=${ARTIFACT_OS}/${GOARCH} mode=${MODE}"

# ── Path layout per mode ──────────────────────────────────────────

case "$MODE" in
	linux-system|darwin-system)
		INSTALL_DIR="/opt/kepeink"
		BINARY_PATH="${INSTALL_DIR}/agent"
		SYMLINK_PATH="/usr/local/bin/kepeink-agent"
		CONFIG_DIR="/etc/kepeink-agent"
		;;
	linux-user|darwin-user)
		# `--user` mode: $HOME-rooted, no /usr or /etc writes.
		INSTALL_DIR="${HOME}/.local/share/kepeink"
		BINARY_PATH="${INSTALL_DIR}/agent"
		SYMLINK_PATH="${HOME}/.local/bin/kepeink-agent"
		CONFIG_DIR="${HOME}/.config/kepeink-agent"
		;;
esac
CONFIG_FILE="${CONFIG_DIR}/agent.env"

# ── Tool checks ───────────────────────────────────────────────────

need_tool() {
	if ! command -v "$1" >/dev/null 2>&1; then
		echo "Required tool '$1' not found. Install it and re-run." >&2
		exit 3
	fi
}

# 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 }'
}

manifest_platform_entry() {
	_manifest="$1"
	_platform="$2"
	awk -v platform="\"$_platform\"" '
	function object_body(s,    i,c,depth) {
		depth = 0
		for (i = 1; i <= length(s); i++) {
			c = substr(s, i, 1)
			if (c == "{") {
				depth++
			} else if (c == "}") {
				depth--
				if (depth == 0) {
					return substr(s, 2, i - 2)
				}
			}
		}
		return ""
	}
	{
		json = json $0
	}
	END {
		p = index(json, "\"platforms\"")
		if (!p) {
			exit 1
		}
		s = substr(json, p)
		p = index(s, ":")
		if (!p) {
			exit 1
		}
		s = substr(s, p + 1)
		p = index(s, "{")
		if (!p) {
			exit 1
		}
		platforms = object_body(substr(s, p))
		if (platforms == "") {
			exit 1
		}
		p = index(platforms, platform)
		if (!p) {
			exit 1
		}
		s = substr(platforms, p + length(platform))
		p = index(s, ":")
		if (!p) {
			exit 1
		}
		s = substr(s, p + 1)
		p = index(s, "{")
		if (!p) {
			exit 1
		}
		entry = object_body(substr(s, p))
		if (entry == "") {
			exit 1
		}
		print entry
	}
	' "$_manifest"
}

check_executable_magic() {
	_path="$1"
	_magic="$(od -An -tx1 -N4 "$_path" | tr -d ' \n')"
	case "$_magic" in
		7f454c46|cafebabe|cafebabf|feedface|feedfacf|cefaedfe|cffaedfe)
			return 0
			;;
		1f8b08*)
			echo "downloaded artifact is gzip-compressed; expected a native agent executable" >&2
			return 1
			;;
		*)
			echo "downloaded artifact does not look like a native executable (magic=$_magic)" >&2
			return 1
			;;
	esac
}

# Hash tool. Linux has sha256sum; macOS has shasum -a 256.
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

# ── Manifest fetch + parse ────────────────────────────────────────

MANIFEST_TMP="$(mktemp)"
BIN_TMP=""
# `${BIN_TMP:-}` so cleanup survives `set -u` after the var is
# consumed (we clear it after the rename takes ownership). Signals
# are trapped explicitly because some shells skip the EXIT trap when
# killed — interrupted runs used to leave tmp files behind.
cleanup() {
	rm -f "$MANIFEST_TMP" "${BIN_TMP:-}"
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
trap 'cleanup; exit 129' HUP

echo "[install] fetching manifest from $CDN/manifest.json"
if ! fetch_url "$CDN/manifest.json" "$MANIFEST_TMP"; then
	echo "manifest fetch failed" >&2
	exit 4
fi

PLATFORM="${ARTIFACT_OS}/${GOARCH}"
PLATFORM_DIR="${ARTIFACT_OS}-${GOARCH}"

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="$(manifest_platform_entry "$MANIFEST_TMP" "$PLATFORM" || true)"
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" >&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 "[install] pinned version=$VERSION (sha256 verify SKIPPED)"
fi

echo "[install] target version: $VERSION"

if [ "$DRY_RUN" -eq 1 ]; then
	echo "[install] DRY-RUN: would download $BIN_URL"
	echo "[install] DRY-RUN: would install to $BINARY_PATH"
	echo "[install] DRY-RUN: would write $CONFIG_FILE"
	# Same omission hw-01 found on the Windows side 2026-08-28: the dry run
	# named every path it would touch and none of the permissions it would
	# set. The token file's mode is the security-relevant one and the thing
	# an operator deciding whether to run this most wants disclosed BEFORE
	# it happens.
	echo "[install] DRY-RUN: would chmod 0600 $CONFIG_FILE (token file, owner-only)"
	echo "[install] DRY-RUN: would chmod 0755 $CONFIG_DIR and $BINARY_PATH"
	if [ -n "$TOKEN" ]; then
		echo "[install] DRY-RUN: token present; service would be configured to start"
	else
		echo "[install] DRY-RUN: token missing; would install service but not start"
	fi
	if [ -n "$TARGET" ]; then
		echo "[install] DRY-RUN: target fallback present"
	else
		echo "[install] DRY-RUN: target server-managed"
	fi
	echo "[install] DRY-RUN: log_level=${LOG_LEVEL:-info}"
	echo "[install] DRY-RUN: mode=$MODE"
	exit 0
fi

# ── Download + verify ─────────────────────────────────────────────

BIN_TMP="$(mktemp)"
if [ -n "$SIZE_BYTES" ]; then
	echo "[install] downloading agent $VERSION ($(human_size "$SIZE_BYTES"))"
else
	echo "[install] downloading agent $VERSION"
fi
echo "[install] 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 "[install] sha256 verified"
fi

if ! check_executable_magic "$BIN_TMP"; then
	exit 6
fi

chmod 0755 "$BIN_TMP"
# Self-test: confirm the 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 install. (A
# dynamically linked GOOS=android build has its argv mangled by
# /system/bin/linker, so --version falls through to the supervisor — we
# no longer ship that build, but the self-test must fail fast rather
# than loop if one ever reaches a device.)
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
}
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 "[install] binary --version: $BIN_VERSION_REPORTED"

# ── Stop running service (if any), install, restart ──────────────

stop_service() {
	case "$MODE" in
		linux-system) systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null \
			&& { echo "[install] stopping $SERVICE_NAME"; systemctl stop "$SERVICE_NAME"; } || true ;;
		linux-user) systemctl --user is-active --quiet "$SERVICE_NAME" 2>/dev/null \
			&& { echo "[install] stopping $SERVICE_NAME (user)"; systemctl --user stop "$SERVICE_NAME"; } || true ;;
		darwin-system) launchctl print system/hu.kepeink.agent >/dev/null 2>&1 \
			&& { echo "[install] unloading hu.kepeink.agent (system)"; launchctl bootout system /Library/LaunchDaemons/hu.kepeink.agent.plist 2>/dev/null || true; } || true ;;
		darwin-user)
			uid="$(id -u)"
			launchctl print "gui/$uid/hu.kepeink.agent" >/dev/null 2>&1 \
			&& { echo "[install] unloading hu.kepeink.agent (user)"; launchctl bootout "gui/$uid" "$HOME/Library/LaunchAgents/hu.kepeink.agent.plist" 2>/dev/null || true; } || true ;;
	esac
}
stop_service

mkdir -p "$INSTALL_DIR"
chmod 0755 "$INSTALL_DIR"
[ -f "$BINARY_PATH" ] && mv -f "$BINARY_PATH" "${BINARY_PATH}.old" || true
mv -f "$BIN_TMP" "$BINARY_PATH"
chmod 0755 "$BINARY_PATH"
BIN_TMP=""  # rename consumed it; clear so the trap rm -f is a no-op

mkdir -p "$(dirname "$SYMLINK_PATH")"
ln -sfn "$BINARY_PATH" "$SYMLINK_PATH"

# ── Config ─────────────────────────────────────────────────────────

mkdir -p "$CONFIG_DIR"
chmod 0755 "$CONFIG_DIR"

if [ ! -f "$CONFIG_FILE" ]; then
	cat >"$CONFIG_FILE" <<EOF
# Kepeink agent — environment file.
# Edit and restart the service:
#   $(restart_hint_for_mode)
#
# REQUIRED:
KEPEINK_AGENT_TOKEN=${TOKEN:-REPLACE_ME}

# OPTIONAL fallback only. The server sends the tunnel target on each
# session; set this only for older servers or local dev.
KEPEINK_AGENT_TARGET=${TARGET:-}

# OPTIONAL:
KEPEINK_AGENT_EDGE_URL=${EDGE_URL:-https://agents.kepeink.hu}
# KEPEINK_AGENT_MIN_SESSIONS=2
# KEPEINK_AGENT_MAX_SESSIONS=4
# Service installs default to single-line machine logs. Use debug only
# while troubleshooting; foreground run.sh enables pretty logs instead.
KEPEINK_AGENT_LOG_LEVEL=${LOG_LEVEL:-info}

# Self-update: ON by default. Disable with --no-auto-update,
# KEPEINK_AGENT_AUTO_UPDATE=0, or kepeink-agent --no-auto-update.
# KEPEINK_AGENT_UPDATE_INTERVAL=1h
#
# Capability pins (breach containment): the agent takes all config
# from the server, but these local pins can only REDUCE what it will
# do — never grant. One comma-separated list clamps backend modes and
# the updater off regardless of server config:
#   KEPEINK_AGENT_DISABLED_FEATURES=ssh,static,proxy,auto-update
EOF
	if [ "$NO_AUTO_UPDATE" -eq 1 ]; then
		update_env_file "$CONFIG_FILE" KEPEINK_AGENT_AUTO_UPDATE "0"
	fi
	chmod 0600 "$CONFIG_FILE"
else
	# In-place update of provided fields, preserve everything else.
	[ -n "$TOKEN" ] && update_env_file "$CONFIG_FILE" KEPEINK_AGENT_TOKEN "$TOKEN"
	[ -n "$TARGET" ] && update_env_file "$CONFIG_FILE" KEPEINK_AGENT_TARGET "$TARGET"
	[ -n "$EDGE_URL" ] && update_env_file "$CONFIG_FILE" KEPEINK_AGENT_EDGE_URL "$EDGE_URL"
	[ -n "$LOG_LEVEL" ] && update_env_file "$CONFIG_FILE" KEPEINK_AGENT_LOG_LEVEL "$LOG_LEVEL"
	[ "$NO_AUTO_UPDATE" -eq 1 ] && update_env_file "$CONFIG_FILE" KEPEINK_AGENT_AUTO_UPDATE "0"
fi

# ── Per-mode service installation ─────────────────────────────────

install_service_linux_system() {
	cat >"/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=Kepeink agent — public reverse-tunnel client
Documentation=https://cdn.kepeink.hu/
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=$BINARY_PATH
EnvironmentFile=$CONFIG_FILE
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
TimeoutStopSec=45

NoNewPrivileges=true
ProtectHome=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$INSTALL_DIR

[Install]
WantedBy=multi-user.target
EOF
	chmod 0644 "/etc/systemd/system/${SERVICE_NAME}.service"
	systemctl daemon-reload
	systemctl enable "$SERVICE_NAME" >/dev/null 2>&1 || true
}

install_service_linux_user() {
	mkdir -p "${HOME}/.config/systemd/user"
	cat >"${HOME}/.config/systemd/user/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=Kepeink agent — public reverse-tunnel client (user)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=$BINARY_PATH
EnvironmentFile=$CONFIG_FILE
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=default.target
EOF
	systemctl --user daemon-reload
	systemctl --user enable "$SERVICE_NAME" >/dev/null 2>&1 || true
}

install_service_darwin_system() {
	# launchd plist can't EnvironmentFile-source a 0600 secret file
	# directly; use a small wrapper script that sources the env then
	# execs the agent. The plist points at the wrapper.
	WRAPPER="$INSTALL_DIR/launch.sh"
	cat >"$WRAPPER" <<EOF
#!/bin/sh
set -e
set -a
[ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE"
set +a
exec "$BINARY_PATH"
EOF
	chmod 0755 "$WRAPPER"

	cat >"/Library/LaunchDaemons/hu.kepeink.agent.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>hu.kepeink.agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>$WRAPPER</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/var/log/kepeink-agent.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/kepeink-agent.log</string>
</dict>
</plist>
EOF
	chmod 0644 "/Library/LaunchDaemons/hu.kepeink.agent.plist"
	launchctl bootstrap system "/Library/LaunchDaemons/hu.kepeink.agent.plist" 2>/dev/null \
		|| launchctl load -w "/Library/LaunchDaemons/hu.kepeink.agent.plist"
}

install_service_darwin_user() {
	WRAPPER="$INSTALL_DIR/launch.sh"
	cat >"$WRAPPER" <<EOF
#!/bin/sh
set -e
set -a
[ -f "$CONFIG_FILE" ] && . "$CONFIG_FILE"
set +a
exec "$BINARY_PATH"
EOF
	chmod 0755 "$WRAPPER"

	mkdir -p "$HOME/Library/LaunchAgents"
	cat >"$HOME/Library/LaunchAgents/hu.kepeink.agent.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>hu.kepeink.agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>$WRAPPER</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>
EOF
	uid="$(id -u)"
	launchctl bootstrap "gui/$uid" "$HOME/Library/LaunchAgents/hu.kepeink.agent.plist" 2>/dev/null \
		|| launchctl load -w "$HOME/Library/LaunchAgents/hu.kepeink.agent.plist"
}

start_service() {
	case "$MODE" in
		linux-system)  systemctl start "$SERVICE_NAME" ;;
		linux-user)    systemctl --user start "$SERVICE_NAME" ;;
		darwin-system) launchctl kickstart -k system/hu.kepeink.agent || true ;;
		darwin-user)   uid="$(id -u)"; launchctl kickstart -k "gui/$uid/hu.kepeink.agent" || true ;;
	esac
}

case "$MODE" in
	linux-system)  install_service_linux_system ;;
	linux-user)    install_service_linux_user ;;
	darwin-system) install_service_darwin_system ;;
	darwin-user)   install_service_darwin_user ;;
esac

# ── Start / decide ─────────────────────────────────────────────────

# Tokens: start the service once the token is present. Route target
# configuration is delivered by the server; KEPEINK_AGENT_TARGET is a
# compatibility fallback only.
TOKEN_SET=0
TARGET_SET=0
if grep -q "^KEPEINK_AGENT_TOKEN=" "$CONFIG_FILE" \
	&& ! grep -q "^KEPEINK_AGENT_TOKEN=REPLACE_ME$" "$CONFIG_FILE" \
	&& ! grep -q "^KEPEINK_AGENT_TOKEN=$" "$CONFIG_FILE"; then
	TOKEN_SET=1
fi
if grep -q "^KEPEINK_AGENT_TARGET=" "$CONFIG_FILE" \
	&& ! grep -q "^KEPEINK_AGENT_TARGET=$" "$CONFIG_FILE"; then
	TARGET_SET=1
fi

if [ "$TOKEN_SET" -eq 1 ]; then
	echo "[install] starting service"
	start_service
fi

# ── Post-install message ──────────────────────────────────────────

cat <<EOF

  ╭────────────────────────────────────────────────────────╮
  │   kepeink-agent $VERSION installed (mode=$MODE)
  ╰────────────────────────────────────────────────────────╯

  Binary:   $BINARY_PATH
  On PATH:  $SYMLINK_PATH
  Config:   $CONFIG_FILE
EOF

case "$MODE" in
	linux-system)
		echo "  Service:  systemd unit kepeink-agent.service"
		echo
		echo "  Watch logs:    sudo journalctl -u kepeink-agent -f"
		echo "  Status:        sudo systemctl status kepeink-agent"
		;;
	linux-user)
		echo "  Service:  systemd --user unit kepeink-agent.service"
		echo
		echo "  Watch logs:    journalctl --user -u kepeink-agent -f"
		echo "  Status:        systemctl --user status kepeink-agent"
		echo
		echo "  For auto-start without an active login session, run ONCE as root:"
		echo "      sudo loginctl enable-linger $(id -un)"
		;;
	darwin-system)
		echo "  Service:  launchd LaunchDaemon hu.kepeink.agent"
		echo
		echo "  Watch logs:    sudo tail -f /var/log/kepeink-agent.log"
		echo "  Restart:       sudo launchctl kickstart -k system/hu.kepeink.agent"
		;;
	darwin-user)
		echo "  Service:  launchd LaunchAgent hu.kepeink.agent (loads on login)"
		echo
		echo "  Watch logs:    log stream --predicate 'process == \"agent\"'"
		echo "  Restart:       launchctl kickstart -k gui/$(id -u)/hu.kepeink.agent"
		;;
esac

if [ "$TOKEN_SET" -ne 1 ]; then
	echo
	echo "  Next: set token. Either edit $CONFIG_FILE manually,"
	echo "        or use:   kepeink-agent provision --token=kpt_xxx"
	echo "        then:    $(restart_hint_for_mode)"
elif [ "$TARGET_SET" -ne 1 ]; then
	echo
	echo "  Target: server-managed. Optional local fallback:"
	echo "          kepeink-agent provision --target=host:port"
fi

if [ "$USER_MODE" -eq 1 ]; then
	UNINSTALL_SUFFIX=" -s -- --user"
else
	UNINSTALL_SUFFIX=""
fi
echo
echo "  Uninstall:  curl -fsSL $CDN/uninstall.sh | sh${UNINSTALL_SUFFIX}"
echo
