#!/usr/bin/env sh
# Shared helpers for the backup-tools installer and uninstaller.
#
# This file is inlined into the published install.sh / uninstall.sh by
# apps/unix/build.sh, so the online, offline, update and uninstall paths all use
# exactly the same implementation (FR-16.3, FR-16.13).

PROJECT_NAME="backup-tools"
COMMAND="backuptools"
PLATFORM="unix"
BASE_URL_DEFAULT="https://files.tymler.work"
VERSION_FALLBACK="v2.2.2"

INSECURE="${BACKUP_TOOLS_INSECURE:-0}"
PREFIX="${HOME}/.local"

log() { printf '%s\n' "$*" >&2; }
warn() { printf 'warning: %s\n' "$*" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
step() { printf '[%s/%s] %s\n' "$1" "$2" "$3" >&2; }

# platform_now maps uname -s to the release platform (Darwin -> macos, anything
# else -> unix).
platform_now() {
    case "$(uname -s)" in
        Darwin) printf 'macos' ;;
        *) printf 'unix' ;;
    esac
}

# binary_os maps a release platform to the GOOS used in bundled binary names.
binary_os() {
    case "$1" in
        unix) printf 'linux' ;;
        macos) printf 'darwin' ;;
        *) die "unknown platform: $1" ;;
    esac
}

arch_now() {
    case "$(uname -m)" in
        x86_64|amd64) printf 'amd64' ;;
        arm64|aarch64) printf 'arm64' ;;
        *) die "unsupported architecture: $(uname -m)" ;;
    esac
}

# resolve_base_url applies the documented priority:
# --base-url > BACKUP_TOOLS_BASE_URL > ~/.config/<project>/install.conf > injected.
resolve_base_url() {
    bt_base=$(resolve_base_url_optional)
    if [ -z "$bt_base" ]; then
        die "no base URL configured; pass --base-url URL, set BACKUP_TOOLS_BASE_URL, or add base_url= to $HOME/.config/$PROJECT_NAME/install.conf"
    fi
    printf '%s' "$bt_base"
}

resolve_base_url_optional() {
    bt_base=""
    if [ -n "${BASE_URL_CLI:-}" ]; then
        bt_base=$BASE_URL_CLI
    elif [ -n "${BACKUP_TOOLS_BASE_URL:-}" ]; then
        bt_base=$BACKUP_TOOLS_BASE_URL
    elif [ -n "${BACKUPTOOLS_BASE_URL:-}" ]; then
        bt_base=$BACKUPTOOLS_BASE_URL
    elif [ -f "$HOME/.config/$PROJECT_NAME/install.conf" ]; then
        bt_base=$(sed -n 's/^[[:space:]]*base_url[[:space:]]*=[[:space:]]*//p' \
            "$HOME/.config/$PROJECT_NAME/install.conf" | tail -n 1)
    elif [ -n "$BASE_URL_DEFAULT" ]; then
        bt_base=$BASE_URL_DEFAULT
    fi
    bt_base=$(printf '%s' "$bt_base" | tr -d '\r' | sed 's|/*$||')
    printf '%s' "$bt_base"
}

# download fetches url to dest, honouring --insecure (curl -k / wget
# --no-check-certificate). Progress goes to stderr.
download() {
    bt_dl_url=$1
    bt_dl_dest=$2
    mkdir -p "$(dirname "$bt_dl_dest")" || die "create download directory"
    if command -v curl >/dev/null 2>&1; then
        if [ "$INSECURE" = "1" ]; then
            curl -kfL --progress-bar "$bt_dl_url" -o "$bt_dl_dest" || die "download failed: $bt_dl_url"
        else
            curl -fL --progress-bar "$bt_dl_url" -o "$bt_dl_dest" || die "download failed: $bt_dl_url"
        fi
    elif command -v wget >/dev/null 2>&1; then
        if [ "$INSECURE" = "1" ]; then
            wget --no-check-certificate "$bt_dl_url" -O "$bt_dl_dest" || die "download failed: $bt_dl_url"
        else
            wget "$bt_dl_url" -O "$bt_dl_dest" || die "download failed: $bt_dl_url"
        fi
    else
        die "neither curl nor wget is available; use the offline archive instead"
    fi
}

sha256_of() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$1" | awk '{print $1}'
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 256 "$1" | awk '{print $1}'
    elif command -v openssl >/dev/null 2>&1; then
        openssl dgst -sha256 "$1" | awk '{print $NF}'
    else
        die "no sha256 tool available (need sha256sum, shasum or openssl)"
    fi
}

shell_kind() {
    case "${1##*/}" in
        bash) printf 'bash' ;;
        zsh) printf 'zsh' ;;
    esac
}

# detect_shell prefers $SHELL and falls back to the parent process (piped runs
# often have no SHELL).
detect_shell() {
    bt_shell=""
    if [ -n "${SHELL:-}" ]; then
        bt_shell=$(shell_kind "$SHELL")
    fi
    if [ -z "$bt_shell" ]; then
        bt_parent=$(ps -o ppid= -p $$ 2>/dev/null | tr -d ' ')
        if [ -n "$bt_parent" ]; then
            bt_comm=$(ps -o comm= -p "$bt_parent" 2>/dev/null | tr -d ' ')
            bt_shell=$(shell_kind "$bt_comm")
        fi
    fi
    [ -n "$bt_shell" ] || return 1
    printf '%s' "$bt_shell"
}

rc_file() {
    case "$1" in
        zsh) printf '%s/.zshrc' "${ZDOTDIR:-$HOME}" ;;
        bash) printf '%s/.bashrc' "$HOME" ;;
        *) die "unsupported shell: $1" ;;
    esac
}

# remove_marked_block deletes every block delimited by the given markers,
# keeping the rest of the rc file and its permissions intact.
remove_marked_block() {
    bt_rc=$1
    bt_begin=$2
    bt_end=$3
    [ -f "$bt_rc" ] || return 0
    grep -qF "$bt_begin" "$bt_rc" || return 0
    bt_tmp="$bt_rc.tmp.$$"
    cp -p "$bt_rc" "$bt_tmp" || die "copy $bt_rc"
    awk -v b="$bt_begin" -v e="$bt_end" '
        $0 == b { skip = 1; next }
        $0 == e { skip = 0; next }
        !skip { print }
    ' "$bt_rc" > "$bt_tmp" || die "rewrite $bt_rc"
    mv -f "$bt_tmp" "$bt_rc" || die "replace $bt_rc"
}

# remove_block deletes the current project-managed block.
remove_block() {
    remove_marked_block "$1" "# >>> $PROJECT_NAME >>>" "# <<< $PROJECT_NAME <<<"
}

# append_block appends one managed block through an atomic rename.
append_block() {
    bt_rc=$1
    bt_block=$2
    bt_dir=$(dirname "$bt_rc")
    [ -d "$bt_dir" ] || mkdir -p "$bt_dir"
    bt_tmp="$bt_rc.tmp.$$"
    if [ -f "$bt_rc" ]; then
        cp -p "$bt_rc" "$bt_tmp" || die "copy $bt_rc"
        { cat "$bt_tmp"; printf '%s\n' "$bt_block"; } > "$bt_tmp.new" || die "write $bt_rc"
        mv -f "$bt_tmp.new" "$bt_tmp" || die "rewrite $bt_rc"
    else
        printf '%s\n' "$bt_block" > "$bt_tmp" || die "write $bt_rc"
    fi
    mv -f "$bt_tmp" "$bt_rc" || die "replace $bt_rc"
}

record_dir() { printf '%s/share/%s' "$PREFIX" "$PROJECT_NAME"; }
record_path() { printf '%s/install-record' "$(record_dir)"; }

record_version() {
    bt_rec=$(record_path)
    [ -f "$bt_rec" ] || return 1
    sed -n '1s/^# version: //p' "$bt_rec"
}

# write_record reads installed paths from stdin (one per line) and writes the
# record atomically. $1 is the installed version.
write_record() {
    bt_rec=$(record_path)
    bt_dir=$(dirname "$bt_rec")
    mkdir -p "$bt_dir" || die "create $bt_dir"
    bt_tmp="$bt_rec.tmp.$$"
    {
        printf '# version: %s\n' "$1"
        cat
    } > "$bt_tmp" || die "write $bt_rec"
    chmod 0600 "$bt_tmp" 2>/dev/null || true
    mv -f "$bt_tmp" "$bt_rec" || die "replace $bt_rec"
}

# prune_dirs removes empty parent directories, never leaving the prefix.
prune_dirs() {
    bt_p=$1
    bt_root=$2
    while [ -n "$bt_p" ] && [ "$bt_p" != "/" ] && [ "$bt_p" != "$bt_root" ]; do
        case "$bt_p" in
            "$bt_root"/*) ;;
            *) break ;;
        esac
        rmdir "$bt_p" 2>/dev/null || break
        bt_p=$(dirname "$bt_p")
    done
}

# cleanup_legacy removes completion files and rc blocks written by v1
# installers. The shared zsh rc block is only reported, because other tools may
# use it. It runs before the new files are installed and skips paths that are
# current install destinations, so the default prefix (where the v1 bash path
# equals the v2 one) never removes the new completion.
cleanup_legacy() {
    bt_new_bash="$PREFIX/share/bash-completion/completions/$COMMAND"
    bt_new_zsh="$PREFIX/share/zsh/site-functions/_$COMMAND"
    for bt_old in \
        "$HOME/.zfunc/_$COMMAND" \
        "${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions/$COMMAND"; do
        # Never treat a current install destination as a legacy path.
        [ "$bt_old" = "$bt_new_bash" ] && continue
        [ "$bt_old" = "$bt_new_zsh" ] && continue
        if [ -e "$bt_old" ]; then
            rm -f "$bt_old" || die "remove legacy file $bt_old"
            log "removed legacy completion: $bt_old"
        fi
    done
    bt_zshrc="${ZDOTDIR:-$HOME}/.zshrc"
    if [ -f "$bt_zshrc" ] && grep -qF '# >>> backuptools shell completion >>>' "$bt_zshrc"; then
        remove_marked_block "$bt_zshrc" \
            '# >>> backuptools shell completion >>>' \
            '# <<< backuptools shell completion <<<'
        log "removed legacy rc block '# >>> backuptools shell completion >>>' from $bt_zshrc"
    fi
    if [ -f "$bt_zshrc" ] && grep -qF '# >>> zsh user completions >>>' "$bt_zshrc"; then
        warn "your zsh rc still has the legacy shared 'zsh user completions' block; remove it manually if no other tool uses it"
    fi
}

version_gt() {
    awk -v a="$1" -v b="$2" '
        function n(s) { return (s == "" ? 0 : s + 0) }
        BEGIN {
            sub(/^v/, "", a); sub(/^v/, "", b)
            split(a, x, "."); split(b, y, ".")
            for (i = 1; i <= 3; i++) {
                if (n(x[i]) > n(y[i])) exit 0
                if (n(x[i]) < n(y[i])) exit 1
            }
            exit 1
        }'
}

version_lt() {
    version_gt "$2" "$1"
}
# backup-tools installer.
#
# One file, two modes:
#   - bootstrap: piped from <base>/backup-tools/<platform>/install.sh; downloads
#     the release archive, verifies sha256 and forwards to the bundled installer.
#   - local: run from an extracted archive (or by the updater); installs the
#     bundled binary without any network access.
set -eu

PREFIX_OPT=""
SHELL_OPT=""
BASE_URL_CLI=""
REINSTALL=0
FORCE=0

usage() {
    cat <<EOF
Usage: install.sh [options]

Install $COMMAND for this machine ($PROJECT_NAME, platform $PLATFORM).

Options:
  --base-url URL   Release root (default: \$BACKUP_TOOLS_BASE_URL,
                   ~/.config/$PROJECT_NAME/install.conf, or the published default)
  --prefix DIR     Install prefix (default: \$HOME/.local)
  --shell bash|zsh Target shell for the completion block (auto-detected)
  --reinstall      Reinstall even when the same version is installed
  --force          Overwrite an unmanaged installation or downgrade
  --insecure       Skip TLS certificate verification (self-signed/private CA)
  -h, --help       Show this help
EOF
}

parse_args() {
    while [ $# -gt 0 ]; do
        case "$1" in
            --base-url)
                [ $# -ge 2 ] || { usage >&2; exit 2; }
                BASE_URL_CLI=$2
                shift 2
                ;;
            --prefix)
                [ $# -ge 2 ] || { usage >&2; exit 2; }
                PREFIX_OPT=$2
                shift 2
                ;;
            --shell)
                [ $# -ge 2 ] || { usage >&2; exit 2; }
                SHELL_OPT=$2
                shift 2
                ;;
            --reinstall)
                REINSTALL=1
                shift
                ;;
            --force)
                FORCE=1
                shift
                ;;
            --insecure)
                INSECURE=1
                shift
                ;;
            -h|--help)
                usage
                exit 0
                ;;
            -*)
                printf 'error: unknown option: %s\n' "$1" >&2
                usage >&2
                exit 2
                ;;
            *)
                printf 'error: unexpected argument: %s\n' "$1" >&2
                usage >&2
                exit 2
                ;;
        esac
    done
    if [ -n "$PREFIX_OPT" ]; then
        PREFIX=$PREFIX_OPT
    fi
    if [ -n "$SHELL_OPT" ]; then
        case "$SHELL_OPT" in
            bash|zsh) ;;
            *)
                printf 'error: --shell must be bash or zsh\n' >&2
                exit 2
                ;;
        esac
    fi
}

write_rc_block() {
    bt_shell=$1
    bt_rc=$(rc_file "$bt_shell")
    remove_block "$bt_rc"
    if [ "$bt_shell" = "zsh" ]; then
        # compinit must run after fpath is extended: the rc file may already
        # call compinit earlier (oh-my-zsh, other installers), and functions in
        # directories added afterwards are never registered.
        bt_block="# >>> $PROJECT_NAME >>>
export PATH=\"$PREFIX/bin:\$PATH\"
fpath=(\"$PREFIX/share/zsh/site-functions\" \$fpath)
autoload -Uz compinit && compinit
# <<< $PROJECT_NAME <<<"
    else
        bt_block="# >>> $PROJECT_NAME >>>
export PATH=\"$PREFIX/bin:\$PATH\"
[ -f \"$PREFIX/share/bash-completion/completions/$COMMAND\" ] && . \"$PREFIX/share/bash-completion/completions/$COMMAND\"
# <<< $PROJECT_NAME <<<"
    fi
    append_block "$bt_rc" "$bt_block"
    printf '%s\n' "$bt_rc"
}

install_local() {
    local_platform=$(platform_now)
    if [ "$PLATFORM" != "$local_platform" ]; then
        die "this archive targets $PLATFORM but this machine is $local_platform"
    fi
    bt_shell=$(detect_shell) || bt_shell=$SHELL_OPT
    [ -n "$bt_shell" ] || die "cannot determine the current shell; re-run with --shell bash|zsh"

    bt_version=$(tr -d '[:space:]' < "$script_dir/version")
    [ -n "$bt_version" ] || die "archive is missing its version file"

    bt_rec=$(record_path)
    bt_current=""
    if [ -f "$bt_rec" ]; then
        bt_current=$(record_version 2>/dev/null || true)
        if [ "$bt_current" = "$bt_version" ] && [ "$REINSTALL" = 0 ] && [ "$FORCE" = 0 ]; then
            log "$COMMAND $bt_version is already installed (use --reinstall to reinstall)"
            return 0
        fi
        if [ -n "$bt_current" ] && version_lt "$bt_version" "$bt_current" && [ "$FORCE" = 0 ]; then
            die "refusing to downgrade from $bt_current to $bt_version (use --force)"
        fi
    elif [ -e "$PREFIX/bin/$COMMAND" ] && [ "$FORCE" = 0 ]; then
        die "$PREFIX/bin/$COMMAND already exists but is not managed by this installer; use --force to overwrite"
    fi

    bt_bin="$script_dir/bin/$COMMAND-$(binary_os "$PLATFORM")-$(arch_now)"
    [ -f "$bt_bin" ] || die "bundled binary not found: $bt_bin"
    [ -f "$script_dir/share/bash-completion/completions/$COMMAND" ] || die "archive is missing the bash completion"
    [ -f "$script_dir/share/zsh/site-functions/_$COMMAND" ] || die "archive is missing the zsh completion"

    bt_old_list=""
    if [ -f "$bt_rec" ]; then
        bt_old_list="$PREFIX/.$PROJECT_NAME.old.$$"
        sed -n '2,$p' "$bt_rec" > "$bt_old_list"
    fi

    # Remove legacy files and rc blocks before installing: the legacy bash
    # completion path is the same as the default v2 destination, so cleaning
    # after the install would delete the freshly installed file.
    cleanup_legacy

    bt_stage="$PREFIX/.$PROJECT_NAME.tmp.$$"
    rm -rf "$bt_stage"
    trap 'rm -rf "$bt_stage"; if [ -n "$bt_old_list" ]; then rm -f "$bt_old_list"; fi' EXIT HUP INT TERM
    mkdir -p "$bt_stage/bin" \
        "$bt_stage/share/bash-completion/completions" \
        "$bt_stage/share/zsh/site-functions" \
        "$bt_stage/share/$PROJECT_NAME/config-templates"

    install -m 0755 "$bt_bin" "$bt_stage/bin/$COMMAND"
    install -m 0644 "$script_dir/share/bash-completion/completions/$COMMAND" \
        "$bt_stage/share/bash-completion/completions/$COMMAND"
    install -m 0644 "$script_dir/share/zsh/site-functions/_$COMMAND" \
        "$bt_stage/share/zsh/site-functions/_$COMMAND"
    cp -R "$script_dir/share/$PROJECT_NAME/config-templates/." "$bt_stage/share/$PROJECT_NAME/config-templates/" \
        || die "copy config templates"

    mkdir -p "$PREFIX/bin" \
        "$PREFIX/share/bash-completion/completions" \
        "$PREFIX/share/zsh/site-functions" \
        "$PREFIX/share/$PROJECT_NAME/config-templates" || die "create install directories"

    mv -f "$bt_stage/bin/$COMMAND" "$PREFIX/bin/$COMMAND" || die "install $COMMAND"
    mv -f "$bt_stage/share/bash-completion/completions/$COMMAND" \
        "$PREFIX/share/bash-completion/completions/$COMMAND" || die "install bash completion"
    mv -f "$bt_stage/share/zsh/site-functions/_$COMMAND" \
        "$PREFIX/share/zsh/site-functions/_$COMMAND" || die "install zsh completion"
    for bt_tpl in "$bt_stage/share/$PROJECT_NAME/config-templates"/*; do
        [ -f "$bt_tpl" ] || continue
        mv -f "$bt_tpl" "$PREFIX/share/$PROJECT_NAME/config-templates/" || die "install template $(basename "$bt_tpl")"
    done
    rm -rf "$bt_stage"

    {
        printf '%s\n' "$PREFIX/bin/$COMMAND"
        printf '%s\n' "$PREFIX/share/bash-completion/completions/$COMMAND"
        printf '%s\n' "$PREFIX/share/zsh/site-functions/_$COMMAND"
        printf '%s\n' "$PREFIX/share/$PROJECT_NAME/config-templates/auth.example"
        printf '%s\n' "$PREFIX/share/$PROJECT_NAME/config-templates/config.example.json"
    } | write_record "$bt_version"

    # Remove files installed by the previous version that the new version no
    # longer provides. Only paths recorded by this installer and inside the
    # prefix are touched (NFR-4, FR-15.4).
    if [ -n "$bt_old_list" ] && [ -f "$bt_old_list" ]; then
        bt_new_record=$(record_path)
        while IFS= read -r bt_old_path; do
            [ -n "$bt_old_path" ] || continue
            case "$bt_old_path" in
                "$PREFIX"/*) ;;
                *) continue ;;
            esac
            if [ -e "$bt_old_path" ] && ! grep -Fqx "$bt_old_path" "$bt_new_record"; then
                rm -f "$bt_old_path" || die "remove obsolete file $bt_old_path"
                log "removed obsolete file: $bt_old_path"
                prune_dirs "$(dirname "$bt_old_path")" "$PREFIX"
            fi
        done < "$bt_old_list"
        rm -f "$bt_old_list"
        bt_old_list=""
    fi

    bt_rc=$(write_rc_block "$bt_shell")

    log "installed $COMMAND $bt_version to $PREFIX/bin/$COMMAND"
    log "enabled $bt_shell completion via $bt_rc"
    log "restart the shell or run: source $bt_rc"
    if ! printf '%s' ":$PATH:" | grep -q ":$PREFIX/bin:"; then
        log "note: $PREFIX/bin is not in the current PATH; open a new terminal"
    fi
    log "configure Gitee access at $HOME/.config/$PROJECT_NAME/auth (copy $PREFIX/share/$PROJECT_NAME/config-templates/auth.example)"
}

install_remote() {
    local_platform=$(platform_now)
    if [ "$PLATFORM" != "$local_platform" ]; then
        bt_base=$(resolve_base_url_optional)
        die "this installer targets $PLATFORM but this machine is $local_platform; use ${bt_base:-<base>}/$PROJECT_NAME/$local_platform/install.sh"
    fi
    bt_shell=$(detect_shell) || bt_shell=$SHELL_OPT
    [ -n "$bt_shell" ] || die "cannot determine the current shell; re-run with --shell bash|zsh"
    bt_base=$(resolve_base_url)
    bt_root_url="$bt_base/$PROJECT_NAME/$PLATFORM"

    bt_tmp=$(mktemp -d "${TMPDIR:-/tmp}/$PROJECT_NAME.XXXXXX")
    trap 'rm -rf "$bt_tmp"' EXIT HUP INT TERM

    step 4 8 "download sha256sums"
    download "$bt_root_url/sha256sums" "$bt_tmp/sha256sums"
    bt_matches=$(awk -v prefix="$PROJECT_NAME-$PLATFORM-" 'index($2, prefix) == 1 {print}' "$bt_tmp/sha256sums")
    bt_count=$(printf '%s\n' "$bt_matches" | grep -c . || true)
    [ "$bt_count" -eq 1 ] || die "sha256sums must contain exactly one $PROJECT_NAME-$PLATFORM-<version>.tar.gz entry (found $bt_count)"
    bt_want=$(printf '%s\n' "$bt_matches" | awk '{print $1}')
    bt_name=$(printf '%s\n' "$bt_matches" | awk '{print $2}')

    step 5 8 "download $bt_name"
    download "$bt_root_url/$bt_name" "$bt_tmp/$bt_name"

    step 6 8 "verify sha256"
    bt_got=$(sha256_of "$bt_tmp/$bt_name")
    [ "$bt_got" = "$bt_want" ] || die "sha256 mismatch for $bt_name: got $bt_got, want $bt_want"

    step 7 8 "extract $bt_name"
    mkdir -p "$bt_tmp/extract"
    tar -xzf "$bt_tmp/$bt_name" -C "$bt_tmp/extract" || die "extract $bt_name"
    bt_inner="$bt_tmp/extract/$PROJECT_NAME/install.sh"
    [ -f "$bt_inner" ] || die "archive has no $PROJECT_NAME/install.sh"

    if [ "$INSECURE" = "1" ]; then
        export BACKUP_TOOLS_INSECURE=1
    fi
    set -- "$bt_inner" --prefix "$PREFIX" --shell "$bt_shell"
    [ "$REINSTALL" = 1 ] && set -- "$@" --reinstall
    [ "$FORCE" = 1 ] && set -- "$@" --force
    if [ "$FORCE" = 0 ] && [ ! -f "$(record_path)" ] && [ -e "$PREFIX/bin/$COMMAND" ]; then
        set -- "$@" --force
    fi
    sh "$@"

    step 8 8 "installed"
}

main() {
    parse_args "$@"
    script_dir=""
    case "$0" in
        */*)
            if [ -f "$0" ]; then
                script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
            fi
            ;;
    esac
    if [ -n "$script_dir" ] && [ -f "$script_dir/version" ] && [ -d "$script_dir/bin" ]; then
        install_local
    else
        install_remote
    fi
}

main "$@"
