#!/bin/bash
# Celestia APT repository installer, version 1.0.0.
# This connects one repository. It does not install/upgrade its packages.
set -Eeuo pipefail
umask 077

readonly REPO_ORIGIN='https://packages.maid.moe'
readonly KEY_FINGERPRINT='6419E6F1783BAA760272A9180FAD370362D09982'
readonly APT_ETC='/etc/apt'
mode=install
replace=0
work=''
backup=''
committing=0
changed=()
targets=()
payloads=()

usage() {
    printf '%s\n' \
      'Celestia APT installer 1.0.0 — Debian 13 trixie, amd64/arm64' \
      'Usage: bash install.sh [--check | --install] [--replace]' \
      '  --check    Verify OS, HTTPS, pinned OpenPGP key, signature and APT in a temporary directory.' \
      '             No changes to system APT configuration; does not require root.' \
      '  --install  Add the verified source and priority 100; default mode, requires root.' \
      '  --replace  Explicitly allow replacing differing Celestia files, with backups.' \
      '  -h, --help Show this help.' \
      'Missing prerequisites are reported, not installed automatically.' \
      'No packages are installed, upgraded or removed. Never disables APT authentication.'
}
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
say() { printf '%s\n' "$*"; }

safe_root_dir() {
    local directory=$1 perms
    [[ -d "$directory" && ! -L "$directory" ]] || die "Not a real directory: $directory"
    [[ $(stat -c %u "$directory") == 0 ]] || die "Directory not owned by root: $directory"
    perms=$(stat -c %a "$directory")
    (( (8#$perms & 0022) == 0 )) || die "Directory writable by non-root users: $directory"
}

cleanup() {
    local result=$? index target temporary
    trap - EXIT INT TERM
    set +e
    if (( committing )); then
        printf 'Installation failed; restoring only files changed by this run.\n' >&2
        for ((index=${#changed[@]}-1; index>=0; index--)); do
            target=${targets[${changed[$index]}]}
            # Do not overwrite another administrator's subsequent edit.
            if [[ -L "$target" ]] || ! cmp -s "$target" "${payloads[${changed[$index]}]}"; then
                printf 'Manual recovery required for %s; backup: %s\n' "$target" "$backup" >&2
                continue
            fi
            if [[ -f "$backup/${changed[$index]}.before" ]]; then
                temporary=$(mktemp "$(dirname "$target")/.celestia-restore.XXXXXXXX")
                if cp -p -- "$backup/${changed[$index]}.before" "$temporary"; then
                    mv -T -- "$temporary" "$target"
                fi
            else
                # This exact target did not exist before this run.
                rm -f -- "$target"
            fi
        done
        printf 'Backup retained: %s\n' "$backup" >&2
    fi
    case "$work" in
        /tmp/celestia-apt.????????) [[ ! -L "$work" ]] && rm -rf -- "$work" ;;
    esac
    exit "$result"
}

for argument in "$@"; do
    case "$argument" in
        --check) mode=check ;;
        --install) mode=install ;;
        --replace) replace=1 ;;
        -h|--help) usage; exit 0 ;;
        *) die "Unknown argument: $argument (use --help)" ;;
    esac
done
[[ "$mode" != check || $replace == 0 ]] || die '--replace is only valid with --install'
[[ $(uname -s) == Linux && -r /etc/os-release ]] || die 'Only Debian Linux is supported'
# os-release is local administrator-controlled data; parse it without executing it.
os_id=$(awk -F= '$1=="ID" {gsub(/"/,"",$2); print $2}' /etc/os-release)
os_version=$(awk -F= '$1=="VERSION_ID" {gsub(/"/,"",$2); print $2}' /etc/os-release)
os_codename=$(awk -F= '$1=="VERSION_CODENAME" {gsub(/"/,"",$2); print $2}' /etc/os-release)
[[ "$os_id" == debian && "$os_version" == 13 && "$os_codename" == trixie ]] ||
    die 'This repository is currently for Debian 13 trixie only; no files changed'
for tool in curl gpg gpgv apt-get dpkg mktemp install stat cmp cp mv awk grep flock getent sha256sum; do
    command -v "$tool" >/dev/null || die "Missing $tool. Install prerequisites: apt-get install ca-certificates curl gnupg util-linux"
done
architecture=$(dpkg --print-architecture)
[[ "$architecture" == amd64 || "$architecture" == arm64 ]] || die "Unsupported architecture: $architecture"
if [[ "$mode" == install && $EUID != 0 ]]; then
    die 'Install mode needs root. Use sudo bash install.sh, or bash install.sh --check'
fi

trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
work=$(mktemp -d /tmp/celestia-apt.XXXXXXXX)
# All files in this work directory are public metadata; allow the _apt sandbox to traverse it.
chmod 0755 "$work"
mkdir -m 0700 "$work/gnupg"
mkdir -m 0755 "$work/lists" "$work/cache"
mkdir -m 0700 "$work/lists/partial"
apt_user=$(id -un)
if (( EUID == 0 )); then
    getent passwd _apt >/dev/null || die 'APT sandbox account _apt is missing'
    apt_user=_apt
    chown _apt "$work/lists/partial"
fi

download() {
    curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
        --connect-timeout 10 --max-time 60 --retry 2 --max-filesize 8388608 \
        "$REPO_ORIGIN/$1" -o "$2"
}

say "Checking Debian 13 / $architecture, HTTPS and repository signing key..."
download keys/celestia-archive.asc "$work/key.asc"
gpg --homedir "$work/gnupg" --batch --with-colons --show-keys "$work/key.asc" > "$work/key.info"
[[ $(awk -F: '$1=="pub" {n++} END {print n+0}' "$work/key.info") == 1 ]] || die 'Expected exactly one primary key'
actual_fingerprint=$(awk -F: '$1=="fpr" {print $10; exit}' "$work/key.info")
[[ "$actual_fingerprint" == "$KEY_FINGERPRINT" ]] || die "Signing key fingerprint mismatch: $actual_fingerprint"
gpg --homedir "$work/gnupg" --batch --dearmor --output "$work/key.gpg" "$work/key.asc"
download debian/dists/trixie/InRelease "$work/InRelease"
gpgv --homedir "$work/gnupg" --keyring "$work/key.gpg" --output "$work/Release" "$work/InRelease"
grep -qx 'Origin: Celestia' "$work/Release" || die 'Unexpected repository Origin'
grep -qx 'Codename: trixie' "$work/Release" || die 'Unexpected repository Codename'
grep -qx 'Components: main' "$work/Release" || die 'Unexpected repository Components'
say "Verified OpenPGP fingerprint: $KEY_FINGERPRINT"

write_source() {
    printf '%s\n' 'Types: deb' "URIs: $REPO_ORIGIN/debian/" 'Suites: trixie' \
        'Components: main' 'Architectures: amd64 arm64' "Signed-By: $1" > "$2"
}
write_source "$work/key.asc" "$work/test.sources"
write_source "$APT_ETC/keyrings/celestia-archive.asc" "$work/celestia.sources"
printf '%s\n' 'Package: *' 'Pin: origin packages.maid.moe' 'Pin-Priority: 100' > "$work/celestia.pref"
chmod 0644 "$work/key.asc" "$work/test.sources" "$work/celestia.sources" "$work/celestia.pref"

# Ignore unrelated system sources/config hooks. APT validates hashes and Valid-Until.
apt_options=(-o Dir::Etc::parts=- -o Dir::Etc::main=- -o Dir::Etc::sourceparts=-
             -o Acquire::Languages=none -o APT::Update::Error-Mode=any)
apt-get "${apt_options[@]}" -o "Dir::Etc::sourcelist=$work/test.sources" \
    -o "Dir::State::lists=$work/lists" -o "Dir::Cache=$work/cache" \
    -o "APT::Sandbox::User=$apt_user" update
compgen -G "$work/lists/*InRelease" >/dev/null || die 'APT did not save authenticated metadata'
say 'PASS: repository signature, expiry, hashes and isolated apt-get update'
[[ "$mode" == install ]] || { say 'Check complete. System APT configuration was not changed.'; exit 0; }

safe_root_dir /var/lib
state_dir=/var/lib/celestia-apt-installer
if [[ -e "$state_dir" || -L "$state_dir" ]]; then safe_root_dir "$state_dir"; else mkdir -m 0700 "$state_dir"; fi
[[ ! -L "$state_dir/lock" ]] || die 'Refusing a symlink lock'
exec {lock_fd}> "$state_dir/lock"
flock --exclusive "$lock_fd"
safe_root_dir /etc
safe_root_dir "$APT_ETC"
for directory in "$APT_ETC/keyrings" "$APT_ETC/sources.list.d" "$APT_ETC/preferences.d"; do
    if [[ -e "$directory" || -L "$directory" ]]; then safe_root_dir "$directory"; else mkdir -m 0755 "$directory"; fi
done
targets=("$APT_ETC/keyrings/celestia-archive.asc" "$APT_ETC/sources.list.d/celestia.sources" "$APT_ETC/preferences.d/celestia.pref")
payloads=("$work/key.asc" "$work/celestia.sources" "$work/celestia.pref")
shopt -s nullglob
for source_file in "$APT_ETC/sources.list" "$APT_ETC/sources.list.d/"*.list "$APT_ETC/sources.list.d/"*.sources; do
    [[ -f "$source_file" && "$source_file" != "${targets[1]}" ]] || continue
    if grep -Ev '^[[:space:]]*(#|$)' "$source_file" | grep -Fq packages.maid.moe; then
        die "Another source already mentions this host: $source_file. Resolve the duplicate manually."
    fi
done
pending=()
for index in "${!targets[@]}"; do
    target=${targets[$index]}
    [[ ! -L "$target" ]] || die "Refusing a symlink target: $target"
    if [[ -e "$target" ]]; then
        [[ -f "$target" ]] || die "Not a regular file: $target"
        [[ $(stat -c %u "$target") == 0 ]] || die "Existing file not owned by root: $target"
        if cmp -s "$target" "${payloads[$index]}" && [[ $(stat -c %a "$target") == 644 ]]; then continue; fi
        (( replace )) || die "Existing file differs (or has unsuitable permissions): $target. Review it; --replace creates a backup."
    fi
    pending+=("$index")
done
if (( ${#pending[@]} )); then
    safe_root_dir /var/backups
    backup_parent=/var/backups/celestia-apt
    if [[ -e "$backup_parent" || -L "$backup_parent" ]]; then safe_root_dir "$backup_parent"; else mkdir -m 0700 "$backup_parent"; fi
    backup=$(mktemp -d "$backup_parent/$(date -u +%Y%m%dT%H%M%SZ)-XXXXXXXX")
    for index in "${pending[@]}"; do
        target=${targets[$index]}
        printf '%s\n' "$target" > "$backup/$index.path"
        if [[ -f "$target" ]]; then cp -p -- "$target" "$backup/$index.before"; fi
    done
    committing=1
    for index in "${pending[@]}"; do
        target=${targets[$index]}
        temporary=$(mktemp "$(dirname "$target")/.celestia-install.XXXXXXXX")
        install -m 0644 -o root -g root "${payloads[$index]}" "$temporary"
        mv -T -- "$temporary" "$target"
        changed+=("$index")
    done
    say "Configuration backup: $backup"
else
    say 'The repository is already configured identically; no configuration files replaced.'
fi
apt-get "${apt_options[@]}" -o "Dir::Etc::sourcelist=${targets[1]}" update
committing=0
say 'Done: Celestia trixie/main is connected and verified. No packages were installed or upgraded.'
say 'Inspect candidates with: apt-cache policy PACKAGE'
