#!/bin/sh
# Execute with: sh install-portable-tools.sh
# Do not source this installer. csh/tcsh can launch it using sh too.
set -eu
umask 077

fail() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
note() { printf '[INFO] %s\n' "$*"; }
have() { command -v "$1" >/dev/null 2>&1; }

case ${1-} in
    -h|--help)
        cat <<'HELP'
Usage: sh install-portable-tools.sh
       env PREFIX=/absolute/path sh install-portable-tools.sh

Default PREFIX: /usr/local (normally requires root).
Installs bak_cp / bak_cpd in PREFIX/bin only if absent.
With Bash installed, also installs apt_auto, apt_init, dnf_auto, dnf_init.
Package commands retain the upstream Bash definitions and dependencies.
Does not source downloaded code or change any shell startup file.
HELP
        exit 0 ;;
    '') ;;
    *) fail 'Unexpected argument. Use --help.' ;;
esac

PREFIX=${PREFIX:-/usr/local}
case $PREFIX in
    /*) ;;
    *) fail 'PREFIX must be an absolute path.' ;;
esac
# Resolve the prefix before embedding paths in the command launcher.
(umask 022; mkdir -p "$PREFIX") || fail "Cannot create $PREFIX; run as root or set PREFIX."
PREFIX=$(CDPATH= cd -P "$PREFIX" && pwd -P)
BIN_DIR=$PREFIX/bin
LIB_DIR=$PREFIX/lib/apt-dnf-auto
BASH_BIN=$(command -v bash 2>/dev/null || :)
case $BASH_BIN in /*) ;; *) BASH_BIN='' ;; esac

for cmd in mktemp date sed cp mv chmod rm grep; do
    have "$cmd" || fail "Missing command: $cmd"
done
if have curl; then
    DOWNLOADER=curl
elif have wget; then
    DOWNLOADER=wget
else
    fail 'Install curl or wget with HTTPS support first.'
fi

TMP_BASE=${TMPDIR:-/tmp}
case $TMP_BASE in /*) ;; *) fail 'TMPDIR must be an absolute path.' ;; esac
WORK_DIR=$(mktemp -d "$TMP_BASE/portable-tools.XXXXXXXXXX") || fail 'mktemp failed.'
PENDING=''
cleanup() {
    if [ -n "$PENDING" ]; then rm -f "$PENDING" || :; fi
    rm -rf "$WORK_DIR" || :
}
trap cleanup 0
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

ROOT_URI=https://gist.githubusercontent.com/Cellularhacker/d8e87f0e2a068802971cf4b22205e468/raw
LIB_URI=$ROOT_URI/linux-tools-apt_dnf_auto.bashrc
STAMP=$(date '+%Y%m%d-%H%M%S')

fetch() {
    note "Downloading $1"
    case $DOWNLOADER in
        curl) curl -fLsS --connect-timeout 15 --max-time 120 -o "$2" "$1" ;;
        wget) wget -O "$2" "$1" ;;
    esac || fail "Download failed: $1"
    [ -s "$2" ] || fail "Empty download: $1"
    # Do not accidentally install a successfully served HTML error page.
    if grep -i -E '<!doctype[[:space:]]+html|<html([[:space:]>])' "$2" >/dev/null 2>&1; then
        fail "Received HTML instead of a shell script: $1"
    fi
}

# Quote one argument for a generated POSIX shell script, including apostrophes.
shell_quote() {
    printf "'"
    printf '%s' "$1" | sed "s/'/'\\\\''/g"
    printf "'"
}

# Download and syntax-check everything needed before installing any file.
for tool in bak_cp bak_cpd; do
    if [ -e "$BIN_DIR/$tool" ] || [ -L "$BIN_DIR/$tool" ]; then
        note "Keeping existing $BIN_DIR/$tool"
        continue
    fi
    fetch "$ROOT_URI/$tool" "$WORK_DIR/$tool"
    IFS= read -r first_line < "$WORK_DIR/$tool" || fail "Cannot read $tool"
    [ "$first_line" = '#!/bin/sh' ] || fail "$tool no longer declares /bin/sh; inspect upstream first."
    /bin/sh -n "$WORK_DIR/$tool" || fail "Shell syntax check failed: $tool"
done

if [ -n "$BASH_BIN" ]; then
    fetch "$LIB_URI" "$WORK_DIR/library.download"
    # The real library, not linux-tools-install_apt_dnf_auto_rc.bash.
    "$BASH_BIN" -n "$WORK_DIR/library.download" || fail 'Bash library syntax check failed.'
    sed -e "s|REPLACE_DL_FROM|$LIB_URI|g" \
        -e "s|APPENDED_AT|$STAMP|g" \
        "$WORK_DIR/library.download" > "$WORK_DIR/library.bash"
    "$BASH_BIN" -n "$WORK_DIR/library.bash" || fail 'Rendered Bash library syntax check failed.'

    cat > "$WORK_DIR/runner.bash" <<'RUNNER'
#!/usr/bin/env bash
# This file is invoked by an explicit Bash executable, not the login shell.
lib=$1
shift
operation=$1
shift
case $operation in
    apt_auto|apt_init) command -v apt >/dev/null 2>&1 || { printf '%s\n' 'This command requires apt.' >&2; exit 127; } ;;
    dnf_auto|dnf_init) command -v dnf >/dev/null 2>&1 || { printf '%s\n' 'This command requires dnf.' >&2; exit 127; } ;;
    *) printf '%s\n' 'Unknown operation.' >&2; exit 2 ;;
esac
# Aliases in the upstream file must expand when its functions are parsed.
shopt -s expand_aliases
source "$lib" || exit $?
# operation is restricted above; user arguments remain quoted, not evaluated as code.
eval "$operation \"\$@\""
RUNNER
    "$BASH_BIN" -n "$WORK_DIR/runner.bash"
    for tool in apt_auto apt_init dnf_auto dnf_init; do
        {
            printf '#!/bin/sh\nexec '
            shell_quote "$BASH_BIN"
            printf ' --noprofile --norc '
            shell_quote "$LIB_DIR/runner.bash"
            printf ' '
            shell_quote "$LIB_DIR/library.bash"
            printf ' %s "$@"\n' "$tool"
        } > "$WORK_DIR/$tool"
        /bin/sh -n "$WORK_DIR/$tool"
    done
else
    note 'Bash not found: installing backup tools only; apt/dnf commands are skipped.'
fi

# Atomic replacement of each individual file. Old versions are kept beside it.
install_file() {
    source_file=$1
    destination=$2
    mode=$3
    [ ! -d "$destination" ] || fail "Destination is a directory: $destination"
    if [ -e "$destination" ] || [ -L "$destination" ]; then
        [ ! -L "$destination" ] || fail "Refusing to replace a symlink: $destination"
        cp -p "$destination" "$destination.previous.$STAMP.$$"
    fi
    PENDING=$(mktemp "$destination.tmp.XXXXXXXXXX")
    cp "$source_file" "$PENDING"
    chmod "$mode" "$PENDING"
    mv -f "$PENDING" "$destination"
    PENDING=''
    note "Installed $destination"
}

(umask 022; mkdir -p "$BIN_DIR")
if [ -n "$BASH_BIN" ]; then
    (umask 022; mkdir -p "$LIB_DIR")
    install_file "$WORK_DIR/library.bash" "$LIB_DIR/library.bash" 644
    install_file "$WORK_DIR/runner.bash" "$LIB_DIR/runner.bash" 644
fi
for tool in bak_cp bak_cpd apt_auto apt_init dnf_auto dnf_init; do
    [ -f "$WORK_DIR/$tool" ] || continue
    install_file "$WORK_DIR/$tool" "$BIN_DIR/$tool" 755
done

note "Finished. Commands are in $BIN_DIR"
note 'Add that directory to PATH if it is not already present.'
if [ -r /etc/profile ] && grep -F '/etc/apt_dnf_auto.bashrc' /etc/profile >/dev/null 2>&1; then
    printf '%s\n' '[WARN] /etc/profile still references the old /etc/apt_dnf_auto.bashrc.' \
        'Remove that old source/. line when migrating; this installer does not edit /etc/profile.' >&2
fi
note 'Tiny Core: persist these files using your existing backup/extension setup before reboot.'

