#!/bin/sh
# install.sh -- install the Libre Computer app (Debian package libre-computer-app).
#
#   curl -fsSL https://app.libre.computer/install.sh | sh
#   curl -fsSL https://app.libre.computer/install.sh | sh -s -- --dry-run
#
# Installs libre-computer-app: the desktop app, the lc command-line tool and the udev rule that
# gives the logged-in user access to the boards' USB devices.
#
# It NEVER subscribes the machine to the Libre Computer apt repository. Two cases:
#   - A machine that already carries our keyring package (a Libre Computer board, or one already
#     set up for our repo) gets the app from the repository it is already on, and updates with it.
#   - Any other machine -- a plain PC someone runs this on -- gets ONLY the app: the single .deb
#     for its distribution and architecture, with the GTK/WebKit/libusb dependencies resolved from
#     its OWN distribution. No apt source is written, no keyring package is installed, nothing is
#     left watching for updates.
#
# The .deb to fetch is found by reading the repository's own apt index (the Packages file's
# Filename field for this suite and architecture), so it is always the current published version;
# there is no separate "latest" link to keep in sync.
#
# sudo is used only for the install step. With --dry-run nothing is written and nothing runs as
# root: the script prints what it would do. POSIX sh: it runs under dash, the shell `| sh` gets on
# Debian.
#
# Options: --dry-run   print the steps, change nothing
#          -h, --help  this text

# Everything is inside main, called on the last line, so a truncated download runs nothing.

# --- Configuration -----------------------------------------------------------
# The one place the repository's values live. All are published values of the Libre Computer apt
# repository; nothing here is a placeholder.
LC_REPO_URL="https://packages.libre.computer/deb"
LC_COMPONENTS="main non-free"
LC_PACKAGE="libre-computer-app"
# The keyring package's names (Debian/Ubuntu, then the older name). Its PRESENCE marks a machine
# already on our repo; this script installs it on nobody.
LC_KEYRING_PACKAGES="libre-computer-archive-keyring libretech-keyring"
# <os-release ID>:<VERSION_CODENAME>, one suite each in the repository.
LC_SUITES="debian:trixie debian:bookworm ubuntu:resolute ubuntu:noble"
# dpkg architectures the repository serves.
LC_ARCHES="amd64 arm64 armhf"

# Test hook: a directory standing in for / when READING os-release, plus a PATH of fake tools
# (dpkg-query, curl) standing in for the system's. A real install refuses it.
LC_ROOT="${LCAPP_ROOT:-}"

# --- Helpers -------------------------------------------------------------------
say() { printf 'install.sh: %s\n' "$*" >&2; }
die() { say "ERROR: $*"; exit 1; }

usage() {
	cat << EOF
Install the Libre Computer app ($LC_PACKAGE).

  curl -fsSL https://app.libre.computer/install.sh | sh
  curl -fsSL https://app.libre.computer/install.sh | sh -s -- --dry-run

A machine already set up for the Libre Computer repository (its keyring is installed) gets the app
from that repository. Any other machine gets only the app's single .deb, with dependencies from its
own distribution; it is not subscribed to the repository.

Options:
  --dry-run   print the steps, change nothing (no root, no writes)
  -h, --help  this text

Supported: Debian 13 and 12, Ubuntu 26.04 and 24.04 LTS; architectures $LC_ARCHES.
sudo is used only for the install step.
EOF
}

have() { command -v "$1" > /dev/null 2>&1; }

# fetch URL OUT: download URL to OUT with curl or wget.
fetch() {
	if have curl; then
		curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1"
	elif have wget; then
		wget -q --https-only -O "$2" "$1"
	else
		die "neither curl nor wget is installed"
	fi
}

# run CMD...: run a privileged command, or print it under --dry-run.
run() {
	if [ "$DRY_RUN" = 1 ]; then
		printf 'would run: %s\n' "$SUDO$*"
	else
		say "running: $SUDO$*"
		if [ -n "$SUDO" ]; then
			sudo "$@"
		else
			"$@"
		fi
	fi
}

# --- Detection -------------------------------------------------------------------

# detect_distro sets OS_ID and OS_CODENAME from os-release and refuses an unsupported pair.
detect_distro() {
	osr="$LC_ROOT/etc/os-release"
	[ -r "$osr" ] || osr="$LC_ROOT/usr/lib/os-release"
	[ -r "$osr" ] || die "cannot read /etc/os-release: this does not look like Debian or Ubuntu"
	OS_ID=$(sed -n 's/^ID=//p' "$osr" | tr -d '"' | head -n 1)
	OS_CODENAME=$(sed -n 's/^VERSION_CODENAME=//p' "$osr" | tr -d '"' | head -n 1)
	OS_NAME=$(sed -n 's/^PRETTY_NAME=//p' "$osr" | tr -d '"' | head -n 1)
	for s in $LC_SUITES; do
		if [ "$s" = "$OS_ID:$OS_CODENAME" ]; then
			return 0
		fi
	done
	die "unsupported system: ${OS_NAME:-${OS_ID:-unknown}} (ID=${OS_ID:-?}, VERSION_CODENAME=${OS_CODENAME:-?}). Supported: Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 26.04 LTS (resolute), Ubuntu 24.04 LTS (noble)"
}

# detect_arch sets ARCH (dpkg's name for this userland) and refuses one the repository lacks.
detect_arch() {
	if have dpkg; then
		ARCH=$(dpkg --print-architecture)
	else
		m=$(uname -m)
		case $m in
		x86_64) ARCH=amd64 ;;
		aarch64 | arm64) ARCH=arm64 ;;
		armv7l | armv8l) ARCH=armhf ;;
		*) ARCH=$m ;;
		esac
	fi
	for a in $LC_ARCHES; do
		if [ "$a" = "$ARCH" ]; then
			return 0
		fi
	done
	die "unsupported architecture: $ARCH. Supported: $LC_ARCHES"
}

# dpkg_status prints a package's dpkg status string, or empty. The fake PATH supplies dpkg-query
# under the test hook.
dpkg_status() {
	have dpkg-query || { echo ""; return 0; }
	dpkg-query -W -f='${Status}' "$1" 2> /dev/null || echo ""
}

# keyring_present: the machine already carries our keyring package, so it is already on our repo.
keyring_present() {
	for pkg in $LC_KEYRING_PACKAGES; do
		case "$(dpkg_status "$pkg")" in
		*"ok installed"*) return 0 ;;
		esac
	done
	return 1
}

# check_published: find the app in this suite's index for this architecture, and set
# LC_DEB_FILENAME to its pool path (the apt index IS the pointer to the current version). The index
# is public and read without root.
check_published() {
	LC_DEB_FILENAME=""
	for comp in $LC_COMPONENTS; do
		idx="$TMP/Packages-$comp.gz"
		url="$LC_REPO_URL/dists/$OS_CODENAME/$comp/binary-$ARCH/Packages.gz"
		fetch "$url" "$idx" 2> /dev/null || continue
		# The Filename of the libre-computer-app record, if this component has one.
		LC_DEB_FILENAME=$(gzip -dc "$idx" 2> /dev/null | awk -v p="$LC_PACKAGE" '
			/^Package:/ { cur = $2 }
			/^Filename:/ { if (cur == p) { print $2; exit } }')
		if [ -n "$LC_DEB_FILENAME" ]; then
			LC_COMPONENT="$comp"
			break
		fi
	done
	if [ -n "$LC_DEB_FILENAME" ]; then
		say "$LC_PACKAGE is published for $OS_CODENAME/$ARCH ($LC_COMPONENT): $LC_DEB_FILENAME"
		return 0
	fi
	msg="$LC_PACKAGE is not yet published in $LC_REPO_URL for $OS_CODENAME/$ARCH"
	if [ "$DRY_RUN" = 1 ]; then
		say "NOTE: $msg; a real run would stop here"
		return 0
	fi
	die "$msg. Nothing was changed"
}

# --- Install ---------------------------------------------------------------------

install_app() {
	if keyring_present; then
		say "this machine is already set up for the Libre Computer repository (its keyring is installed); installing the app from it"
		run apt-get update
		run env DEBIAN_FRONTEND=noninteractive apt-get install --yes "$LC_PACKAGE"
		return 0
	fi

	# No keyring: not a machine set up for our repo. Install ONLY the app, from its single .deb,
	# with dependencies from this machine's own distribution. Add no source, install no keyring.
	say "installing only the app; this machine is not subscribed to the Libre Computer repository"
	if [ -z "$LC_DEB_FILENAME" ]; then
		[ "$DRY_RUN" = 1 ] && { say "NOTE: nothing published to download yet; a real run would stop at check_published"; return 0; }
		die "no published .deb to download"
	fi
	url="$LC_REPO_URL/$LC_DEB_FILENAME"
	deb="$TMP/$LC_PACKAGE.deb"
	if [ "$DRY_RUN" = 1 ]; then
		printf 'would download: %s (as you, no root)\n' "$url"
	else
		fetch "$url" "$deb" || die "could not download $url"
	fi
	# apt-get install of a local .deb pulls its dependencies from the machine's own apt sources.
	run env DEBIAN_FRONTEND=noninteractive apt-get install --yes "$deb"
}

# --- Main --------------------------------------------------------------------------

main() {
	DRY_RUN=0
	for arg in "$@"; do
		case $arg in
		--dry-run) DRY_RUN=1 ;;
		-h | --help) usage; exit 0 ;;
		*) die "unknown option: $arg (--help)" ;;
		esac
	done
	if [ -n "$LC_ROOT" ] && [ "$DRY_RUN" != 1 ]; then
		die "LCAPP_ROOT is a test hook; it works only with --dry-run"
	fi

	detect_distro
	detect_arch
	say "system: ${OS_NAME:-$OS_ID $OS_CODENAME}, $ARCH"
	have apt-get || [ "$DRY_RUN" = 1 ] || die "apt-get is not installed"

	if [ "$(id -u)" = 0 ]; then
		SUDO=""
	else
		SUDO="sudo "
		have sudo || [ "$DRY_RUN" = 1 ] || die "sudo is not installed: run this as root, or install sudo"
	fi

	TMP=$(mktemp -d) || die "mktemp failed"
	trap 'rm -rf "$TMP"' EXIT
	trap 'exit 130' INT TERM

	check_published
	install_app
	if [ "$DRY_RUN" = 1 ]; then
		say "dry run: nothing was changed"
	else
		say "done: start the Libre Computer app from your applications menu"
	fi
}

main "$@"
