#!/bin/sh
# Talky installer bootstrap — `curl -fsSL https://dl.talky.so/install.sh | sh`.
#
# This script is intentionally THIN and human-auditable (you are piping it to sh):
# it only detects the OS/arch and DELEGATES to the platform package manager —
# Homebrew on macOS, apt/dpkg on Debian/Ubuntu — which own versioning, signing, and
# init-system registration. It downloads nothing it does not first checksum-verify,
# and it FAILS LOUD (one actionable line) on anything unsupported (CLAUDE.md rule 1).
#
# macOS + Linux only for M1; Windows -> M3+ (T-DESKTOP-WINDOWS); snap/AUR/Fedora -> M2+.
#
# Overridable (all optional):
#   TALKY_VERSION          pin an exact version (default: the published `stable` channel)
#   TALKY_DIST_BASE        artifact base URL; `file://…` for a local mirror/offline (default: https://dl.talky.so)
#   TALKY_BREW_TAP         Homebrew tap (default: sriviswanath/talky)
#   TALKY_BOOTSTRAP_DRYRUN if set, detect + download + verify but DO NOT install (preview)
set -eu

TALKY_DIST_BASE="${TALKY_DIST_BASE:-https://dl.talky.so}"
TALKY_BREW_TAP="${TALKY_BREW_TAP:-sriviswanath/talky}"

info() { printf 'talky: %s\n' "$*"; }
err() { printf 'talky: %s\n' "$*" >&2; }
die() {
	err "$*"
	exit 1
}

# no_new_privs_set reports whether the kernel PR_SET_NO_NEW_PRIVS flag is set on THIS
# process. When it is (common inside containers and agent sandboxes), a setuid sudo
# CANNOT escalate to root, so we must not even try — we detect it and fail loud with one
# actionable line instead of letting sudo print its cryptic two-line error. Linux exposes
# the flag in /proc/self/status as "NoNewPrivs:\t1"; a missing file/line (non-Linux, old
# kernel) is treated as not set.
no_new_privs_set() {
	[ -r /proc/self/status ] || return 1
	grep -qE '^NoNewPrivs:[[:space:]]+1[[:space:]]*$' /proc/self/status 2>/dev/null
}

# fetch URL -> stdout. Remote is https-only (TLS 1.2+, fail-on-error). A file:// base
# (local mirror / offline / tests) is read directly — it never touches the network, so
# the https restriction does not apply to it.
fetch() {
	case "$1" in
	file://*)
		cat "${1#file://}"
		;;
	https://*)
		# Remote is https-only and must not downgrade to http. curl `--proto =https`
		# refuses an https->http redirect. GNU wget's `--https-only` only constrains
		# RECURSIVE link-following (not single-URL redirects), so the wget fallback also
		# uses `--max-redirect=0` to refuse ANY redirect (our dist layout is direct URLs)
		# — otherwise a 302 to http:// would silently fetch the artifact + checksum in
		# plaintext, defeating the sha256 check.
		if command -v curl >/dev/null 2>&1; then
			curl -fsSL --proto '=https' --tlsv1.2 "$1"
		elif command -v wget >/dev/null 2>&1; then
			wget -qO- --https-only --max-redirect=0 --secure-protocol=TLSv1_2 "$1"
		else
			die "need curl or wget to download the package; install one and retry."
		fi
		;;
	*)
		die "refusing a non-https URL: $1 (remote downloads must be https; use a file:// base for a local mirror)."
		;;
	esac
}

resolve_version() {
	version="${TALKY_VERSION:-}"
	if [ -z "$version" ]; then
		if ! stable="$(fetch "$TALKY_DIST_BASE/stable.txt")"; then
			die "could not reach $TALKY_DIST_BASE/stable.txt (set TALKY_VERSION to pin a version)."
		fi
		version="$(printf '%s' "$stable" | tr -d ' \t\r\n')"
	fi
	[ -n "$version" ] || die "empty version; set TALKY_VERSION to an exact version."
	version="${version#v}" # normalize a v-prefix to match build.sh's artifact names
	printf '%s\n' "$version"
}

verify_checksum() { # dir filename
	dir="$1"
	name="$2"
	checksums="$dir/checksums.txt"
	if ! grep "[ *]${name}\$" "$checksums" >"$dir/want.sha256" || [ ! -s "$dir/want.sha256" ]; then
		die "no checksum entry for $name in checksums.txt; refusing to install unverified."
	fi
	if command -v sha256sum >/dev/null 2>&1; then
		( cd "$dir" && sha256sum -c want.sha256 ) ||
			die "checksum verification FAILED for $name; refusing to install a tampered/corrupt package."
	elif command -v shasum >/dev/null 2>&1; then
		# shasum cannot consume GNU sha256sum's text-mode manifest directly.
		expected="$(awk '{print $1}' "$dir/want.sha256")"
		actual="$(shasum -a 256 "$dir/$name" | awk '{print $1}')"
		[ "$actual" = "$expected" ] ||
			die "checksum verification FAILED for $name; refusing to install a tampered/corrupt package."
	else
		die "need sha256sum or shasum to verify the package checksum."
	fi
}

ensure_homebrew_linked() { # formula
	formula="$1"
	if ! brew link "$formula"; then
		err "Homebrew installed talky but could not link the commands into $(brew --prefix)/bin."
		err "Inspect conflicts first: brew link --overwrite $formula --dry-run"
		die "If the listed files are old Talky files, run: brew link --overwrite $formula"
	fi
	brew_prefix="$(brew --prefix)"
	[ -x "$brew_prefix/bin/talky" ] ||
		die "Homebrew did not expose $brew_prefix/bin/talky after install; run: brew link $formula"
	[ -x "$brew_prefix/bin/talky-local-gateway" ] ||
		die "Homebrew did not expose $brew_prefix/bin/talky-local-gateway after install; run: brew link $formula"
	[ -x "$brew_prefix/bin/talky-status" ] ||
		die "Homebrew did not expose $brew_prefix/bin/talky-status after install; run: brew link $formula"
}

# Once Homebrew owns the macOS install, `brew services` is the canonical service
# manager. A leftover LaunchAgent from a pre-Homebrew install would race the brew
# service for the same daemon/socket — stop it and move it aside (disabled, not
# deleted, so a human can inspect/restore it).
maybe_stop_legacy_macos_launch_agent() {
	legacy_agent="$HOME/Library/LaunchAgents/so.talky.local-gateway.plist"
	[ -f "$legacy_agent" ] || return 0
	info "found a legacy launchd agent; the Homebrew service is canonical. Disabling it…"
	if command -v launchctl >/dev/null 2>&1; then
		# bootout fails when the agent is not loaded; that is fine — we only need it stopped.
		launchctl bootout "gui/$(id -u)/so.talky.local-gateway" 2>/dev/null || true
	fi
	mv "$legacy_agent" "${legacy_agent}.disabled" ||
		die "could not disable the legacy LaunchAgent at $legacy_agent; remove it manually, then rerun."
	info "legacy LaunchAgent disabled (moved to ${legacy_agent}.disabled)."
}

# Post-install step. Registration is login-driven (ADR 0010): `talky login` signs
# you in, registers this machine as a box, writes ~/.talky/local-gateway.json, and
# starts the daemon itself. An already-registered machine needs a service restart to
# pick up the new binaries — RUN it (the install may have just disabled a legacy
# LaunchAgent, so only printing the command would leave the daemon stopped); if the
# restart fails, print the command loudly for the user to run themselves.
#
# A restart command exits 0 the moment the start job is ACCEPTED — it does NOT wait to
# see the daemon stay up. So after a successful restart, VERIFY the daemon is actually
# running (health_check_command) before reporting success: a daemon that immediately
# crash-loops (e.g. it keeps rejecting a stale/expired box link) is only briefly "up"
# each ~RestartSec cycle, so a single check can land in that sliver and false-pass —
# sample several times across a window wider than one restart cycle and require it to
# hold every time. Install already succeeded, so a daemon that won't stay up is a loud,
# actionable warning (re-run `talky login`), NOT an install failure — hence the message
# + return 0, but never a silent "daemon restarted" (CLAUDE.md rule 1, fail loud).
print_next_step() { # restart_command manual_start_command health_check_command
	if [ ! -f "$HOME/.talky/local-gateway.json" ]; then
		info "installed. Next: run \`talky login\`. It signs you in AND registers this machine as a box;"
		info "the daemon starts automatically."
		info "(troubleshooting: if the daemon is not running after login, start it with: $2)"
		return 0
	fi
	info "installed. This machine is already registered as a box; restarting the daemon to pick up the new version…"
	if ! sh -c "$1"; then
		err "automatic daemon restart failed; run it manually:"
		err "  $1"
		return 0
	fi
	if [ -n "${3:-}" ]; then
		daemon_up=1
		# 5 samples 1s apart span 4s > one RestartSec=2 crash cycle, so a crash-looping
		# unit (down most of each cycle) trips at least one sample; a healthy daemon
		# stays active throughout.
		for _ in 1 2 3 4 5; do
			sh -c "$3" || { daemon_up=0; break; }
			sleep 1
		done
		if [ "$daemon_up" -ne 1 ]; then
			err "the daemon was restarted but is not staying up; it may be crash-looping."
			err "a common cause is an expired box link; re-register this machine, then re-check:"
			err "  talky login   # re-mints the box credential and restarts the daemon"
			err "  talky status"
			return 0
		fi
	fi
	info "daemon restarted."
}

install_or_upgrade_homebrew_formula() { # formula
	formula="$1"
	if brew list --formula "$formula" >/dev/null 2>&1; then
		info "talky is already installed via Homebrew; upgrading if needed…"
		if ! brew upgrade "$formula"; then
			err "Homebrew upgrade failed."
			err "If Homebrew reported link conflicts, inspect first: brew link --overwrite $formula --dry-run"
			die "Otherwise fix the Homebrew error above, then rerun this installer."
		fi
	else
		info "installing talky via Homebrew ($TALKY_BREW_TAP)…"
		if ! brew install "$formula"; then
			err "Homebrew install failed."
			err "If Homebrew reported link conflicts, inspect first: brew link --overwrite $formula --dry-run"
			die "Otherwise fix the Homebrew error above, then rerun this installer."
		fi
	fi
}

# --- detect OS + arch -------------------------------------------------------
os="$(uname -s)"
arch="$(uname -m)"
case "$arch" in
x86_64 | amd64) arch="amd64" ;;
arm64 | aarch64) arch="arm64" ;;
*) die "unsupported CPU architecture '$arch' (talky ships amd64 + arm64 only)." ;;
esac

case "$os" in
Darwin)
	# macOS: delegate to Homebrew. The tap's formula is the version source of truth
	# (its `service` block REGISTERS the launchd agent; brew does not auto-start it).
	if ! command -v brew >/dev/null 2>&1; then
		die "Homebrew is required on macOS. Install it from https://brew.sh then re-run this installer."
	fi
	if [ -n "${TALKY_VERSION:-}" ]; then
		info "note: Homebrew installs the tap's current formula; TALKY_VERSION pinning is honored on the Linux/.deb path."
	fi
	if [ -n "${TALKY_BOOTSTRAP_DRYRUN:-}" ]; then
		info "DRY-RUN: would install talky via Homebrew ($TALKY_BREW_TAP/talky). Skipping install."
		exit 0
	fi
	brew_formula="$TALKY_BREW_TAP/talky"
	install_or_upgrade_homebrew_formula "$brew_formula"
	ensure_homebrew_linked "$brew_formula"
	maybe_stop_legacy_macos_launch_agent
	print_next_step "brew services restart $brew_formula" \
		"brew services start $brew_formula" \
		"brew services info $brew_formula --json | grep -q '\"running\": *true'"
	;;
Linux)
	# Linux: Debian/Ubuntu (apt/dpkg) for M1. Other distros -> M2+.
	if ! command -v dpkg >/dev/null 2>&1 || ! command -v apt-get >/dev/null 2>&1; then
		die "unsupported Linux distribution (M1 supports Debian/Ubuntu via apt; others -> M2+)."
	fi
	# Resolve the version to install: an explicit pin, else the published stable
	# channel. Capture the fetch SEPARATELY so a network failure fails loud here — a
	# `fetch | tr` pipeline would mask curl's exit status in POSIX sh (no pipefail).
	version="$(resolve_version)"
	deb="talky_${version}_${arch}.deb"
	tmp="$(mktemp -d)"
	trap 'rm -rf "$tmp"' EXIT
	info "downloading $deb…"
	fetch "$TALKY_DIST_BASE/v${version}/${deb}" >"$tmp/$deb" || die "download failed: $TALKY_DIST_BASE/v${version}/${deb}"
	fetch "$TALKY_DIST_BASE/v${version}/checksums.txt" >"$tmp/checksums.txt" || die "could not fetch checksums.txt to verify the download."
	# Verify the .deb against the published sha256 BEFORE touching the system. Require
	# an explicit checksum entry — fail loud if the manifest lacks our file (defense in
	# depth; `sha256sum -c` on empty input already exits non-zero).
	# Accept both sha256sum output markers: text mode ("<hash>  name") and binary mode
	# ("<hash> *name"). Our build.sh emits text mode, but a manifest built elsewhere may
	# use binary mode — match either so verification never spuriously "finds no entry".
	verify_checksum "$tmp" "$deb"
	if [ -n "${TALKY_BOOTSTRAP_DRYRUN:-}" ]; then
		info "DRY-RUN: verified $deb (checksum OK); would install via apt-get. Skipping install."
		exit 0
	fi
	# apt >= 1.1 sandboxes package acquisition as the unprivileged `_apt` user — even a
	# local .deb path is "acquired". mktemp -d is 0700, so `_apt` cannot read the file and
	# apt falls back to copying it as root, printing "N: Download is performed unsandboxed
	# as root … (13: Permission denied)" — a scary notice that makes a SUCCESSFUL install
	# read as failed. Open the dir/file so the sandboxed acquire works and the notice does
	# not print under the default world-traversable TMPDIR (/tmp, 1777). `_apt` also needs
	# +x on every TMPDIR ancestor, so a non-traversable custom TMPDIR (e.g. libpam-tmpdir's
	# 0700 /tmp/user/<uid>) still gets apt's harmless unsandboxed-copy fallback; we honor
	# TMPDIR rather than force /tmp or chmod ancestor dirs we do not own.
	# Safe post-checksum: the .deb is a public artifact, and only the owner can write
	# (dir 0755), so nothing can be swapped between verification and install.
	chmod 755 "$tmp"
	chmod 644 "$tmp/$deb"
	# Installing a system .deb needs root. If we are root, install directly. Otherwise
	# escalate with sudo — but ONLY when escalation can actually work: the no_new_privs
	# flag neuters setuid (so sudo can never become root), and a host without sudo cannot
	# escalate at all. In either case fail loud with the one move that works (a root shell
	# needs no escalation) instead of a cryptic sudo error or a bare "command not found".
	if [ "$(id -u)" -eq 0 ]; then
		info "checksum OK; installing (apt-get install)…"
		apt-get install -y "$tmp/$deb"
	elif no_new_privs_set; then
		die "cannot install as a non-root user: this process has the kernel no_new_privs flag set (common inside containers and agent sandboxes), so sudo cannot escalate to root. Re-run from a root shell, where no privilege escalation is needed (a root login, or 'docker exec -u 0 <container>'), then retry."
	elif ! command -v sudo >/dev/null 2>&1; then
		die "cannot install as a non-root user: you are not root and sudo is not installed, so the package cannot be installed. Re-run from a root shell (root needs no escalation), or install sudo, then retry."
	else
		info "checksum OK; installing (sudo apt-get install)…"
		sudo apt-get install -y "$tmp/$deb"
	fi
	print_next_step \
		"systemctl --user daemon-reload && systemctl --user restart talky-local-gateway.service" \
		"systemctl --user enable --now talky-local-gateway.service" \
		"systemctl --user is-active --quiet talky-local-gateway.service"
	;;
*)
	die "unsupported operating system '$os' (talky supports macOS + Linux; Windows -> M3+)."
	;;
esac
