#!/bin/sh
# NanoPing installer.
#
#   curl -sS https://get.nanoping.com | sh
#
# Installs np into ~/.local/bin, where it can update itself. It offers to add that directory to the
# PATH, and to remove an np installed elsewhere, such as in /usr/local/bin, once the new one is in
# place.
#
#   NANOPING_INSTALL_DIR=<dir>     install somewhere else
#   NANOPING_MODIFY_PATH=yes|no    answer the PATH question without asking
#   NANOPING_REMOVE_OLD=yes|no     answer the removal question without asking
#
# Without a terminal to ask on, the answer to both is no.
set -eu

APP_NAME="NanoPing"
BIN_NAME="np"
VERSION="11.0.0"
BASE_URL="https://get.nanoping.com"
HOME_DIR="${HOME:-}"
INSTALL_DIR="${NANOPING_INSTALL_DIR:-${HOME_DIR:+$HOME_DIR/.local/bin}}"
# Where the binary keeps its state by default; np --app-data overrides it.
APP_DATA_DIR="${HOME:-~}/nanoping"
DOCS_URL="https://docs.nanoping.com"
PATH_MARKER="# added by the NanoPing installer"
NL='
'

fail() {
    echo "$APP_NAME installer: $1" >&2
    exit 1
}

step() {
    echo "✓ $1"
}

warn() {
    echo "! $1" >&2
}

on_path() {
    case ":$PATH:" in
        *":$1:"*) return 0 ;;
        *) return 1 ;;
    esac
}

# A path under the home directory, written as $HOME/... (or ~/... with "~").
home_relative() {
    case "$1" in
        "$HOME_DIR"/*) echo "${2:-\$HOME}/${1#"$HOME_DIR"/}" ;;
        *) echo "$1" ;;
    esac
}

# Answers a yes/no question with its preset, or by asking on the terminal.
ask() {
    case "${2:-}" in
        [Yy]*|1|[Tt]rue) return 0 ;;
        [Nn]*|0|[Ff]alse) return 1 ;;
    esac
    (: </dev/tty) 2>/dev/null || return 1
    printf '%s [Y/n] ' "$1" >/dev/tty
    read -r answer </dev/tty || answer=n
    case "$answer" in
        ""|[Yy]*) return 0 ;;
        *) return 1 ;;
    esac
}

# Appends a line to a shell startup file, once.
append_line() {
    file="$1"
    line="$2"
    if [ -f "$file" ] && grep -qxF "$line" "$file"; then
        return 0
    fi
    mkdir -p "$(dirname "$file")"
    if [ -s "$file" ] && [ -n "$(tail -c 1 "$file")" ]; then
        echo "" >>"$file"
    fi
    echo "$line" >>"$file"
}

# Notes an np of NanoPing's that is not the one being installed.
note_old_install() {
    [ -f "$1" ] || return 0
    [ "$1" -ef "$target" ] && return 0
    grep -q "nanoping/core" "$1" 2>/dev/null || return 0
    for old in $old_installs; do
        [ "$old" -ef "$1" ] && return 0
    done
    old_installs="${old_installs:+$old_installs$NL}$1"
}

remove_file() {
    rm -f "$1" 2>/dev/null && return 0
    command -v sudo >/dev/null 2>&1 || return 1
    echo "Removing $1 needs sudo:"
    sudo rm -f "$1"
}

[ -n "$INSTALL_DIR" ] || fail "HOME is not set: set NANOPING_INSTALL_DIR to the directory to install into"
command -v curl >/dev/null 2>&1 || fail "curl is required"

os=$(uname -s 2>/dev/null || echo unknown)
arch=$(uname -m 2>/dev/null || echo unknown)

case "$os" in
    Linux) os=linux ;;
    Darwin) os=macos ;;
    MINGW*|MSYS*|CYGWIN*|Windows_NT) os=windows ;;
    *) fail "unsupported operating system: $os" ;;
esac

case "$arch" in
    x86_64|amd64) arch=x86_64 ;;
    arm64|aarch64|armv7l|armv8l) arch=arm ;;
    *) fail "unsupported architecture: $arch" ;;
esac

target="$INSTALL_DIR/$BIN_NAME"
old_installs=
saved_ifs=$IFS
set -f
IFS=:
set -- $PATH
IFS=$NL
for dir in "$@"; do
    [ -n "$dir" ] && [ "$dir" != . ] || continue
    dir=$(cd "$dir" 2>/dev/null && pwd -P) || continue
    note_old_install "$dir/$BIN_NAME"
done

remove_old=
if [ -n "$old_installs" ]; then
    echo "Found an older $BIN_NAME:"
    for old in $old_installs; do
        echo "    $old"
    done
    case "$old_installs" in
        *"$NL"*) question="Remove them once the new version is installed?" ;;
        *) question="Remove it once the new version is installed?" ;;
    esac
    if ask "$question" "${NANOPING_REMOVE_OLD:-}"; then
        remove_old=1
    fi
fi
IFS=$saved_ifs
set +f

echo "Installing latest version of $BIN_NAME ($APP_NAME $VERSION) …"

tmp=$(mktemp -d 2>/dev/null || mktemp -d -t nanoping)
trap 'rm -rf "$tmp"' EXIT
trap 'exit 1' INT TERM

(cd "$tmp" && curl -fsSL -O -J "$BASE_URL/latest/$os/$arch") ||
    fail "no $VERSION download available for $os/$arch"

asset=$(ls "$tmp" | head -n 1)
[ -n "$asset" ] || fail "download failed"
step "finished downloading"

unpack_dir="$tmp/unpacked"
mkdir -p "$unpack_dir"
case "$asset" in
    *.tar.gz|*.tgz)
        command -v tar >/dev/null 2>&1 || fail "tar is required to unpack $asset"
        tar -xzf "$tmp/$asset" -C "$unpack_dir"
        ;;
    *.zip)
        command -v unzip >/dev/null 2>&1 || fail "unzip is required to unpack $asset"
        unzip -q "$tmp/$asset" -d "$unpack_dir"
        ;;
    *)
        mv "$tmp/$asset" "$unpack_dir/$BIN_NAME"
        chmod +x "$unpack_dir/$BIN_NAME"
        ;;
esac

# The command sits in the archive root under its own name, which is the name it is documented and
# supported under, so keep it. An extensionless file wins over a sibling script.
binary=$(find "$unpack_dir" -maxdepth 1 -type f -name "$BIN_NAME" | head -n 1)
if [ -z "$binary" ]; then
    binary=$(find "$unpack_dir" -maxdepth 1 -type f -perm -u+x ! -name '*.*' | sort | head -n 1)
fi
if [ -z "$binary" ]; then
    binary=$(find "$unpack_dir" -maxdepth 1 -type f -perm -u+x | sort | head -n 1)
fi
if [ -z "$binary" ]; then
    # Archives that wrap everything in a single top-level directory.
    binary=$(find "$unpack_dir" -maxdepth 2 -type f -perm -u+x | sort | head -n 1)
fi
[ -n "$binary" ] || fail "no executable found in $asset"

command_name=$(basename "$binary")
chmod 755 "$binary"
target="$INSTALL_DIR/$command_name"

if mkdir -p "$INSTALL_DIR" 2>/dev/null && [ -w "$INSTALL_DIR" ]; then
    as_owner=
elif command -v sudo >/dev/null 2>&1; then
    as_owner=sudo
    sudo mkdir -p "$INSTALL_DIR"
else
    fail "$INSTALL_DIR is not writable and sudo is unavailable: set NANOPING_INSTALL_DIR to a writable directory"
fi

# Renamed into place from the same directory, so a running node keeps its executable.
$as_owner cp "$binary" "$INSTALL_DIR/.$command_name.new"
$as_owner chmod 755 "$INSTALL_DIR/.$command_name.new"
$as_owner mv -f "$INSTALL_DIR/.$command_name.new" "$target"
step "installed $command_name to $target"

set -f
IFS=$NL
for old in $old_installs; do
    if [ -z "$remove_old" ]; then
        warn "kept the older $old"
    elif remove_file "$old"; then
        step "removed the older $old"
    else
        warn "could not remove the older $old: remove it with sudo rm $old"
    fi
done
IFS=$saved_ifs
set +f

path_added=
if ! on_path "$INSTALL_DIR"; then
    dir=$(home_relative "$INSTALL_DIR")
    shell_name=$(basename "${SHELL:-sh}")
    zsh_rc="${ZDOTDIR:-$HOME_DIR}/.zshrc"
    fish_conf="${XDG_CONFIG_HOME:-$HOME_DIR/.config}/fish"

    startup_files=
    for file in "$HOME_DIR/.profile" "$HOME_DIR/.bash_profile" "$HOME_DIR/.bash_login" "$HOME_DIR/.bashrc" "$zsh_rc"; do
        edit=
        [ -f "$file" ] && edit=1
        case "$shell_name:$file" in
            bash:"$HOME_DIR/.bashrc") edit=1 ;;
            zsh:"$zsh_rc") edit=1 ;;
            bash:"$HOME_DIR/.profile")
                # Login shells, such as macOS Terminal's, read this instead of .bashrc.
                [ -f "$HOME_DIR/.bash_profile" ] || [ -f "$HOME_DIR/.bash_login" ] || edit=1
                ;;
            bash:*|zsh:*|fish:*) ;;
            *:"$HOME_DIR/.profile") edit=1 ;;
        esac
        [ -n "$edit" ] && startup_files="${startup_files:+$startup_files$NL}$file"
    done
    fish_file=
    if [ "$shell_name" = fish ] || [ -d "$fish_conf" ]; then
        fish_file="$fish_conf/conf.d/nanoping.fish"
    fi

    shown=
    set -f
    IFS=$NL
    for file in $startup_files $fish_file; do
        shown="${shown:+$shown, }$(home_relative "$file" "~")"
    done
    IFS=$saved_ifs
    set +f

    if ask "Add $(home_relative "$INSTALL_DIR" "~") to your PATH in $shown?" "${NANOPING_MODIFY_PATH:-}"; then
        sh_line="case \":\$PATH:\" in *\":$dir:\"*) ;; *) export PATH=\"$dir:\$PATH\" ;; esac $PATH_MARKER"
        set -f
        IFS=$NL
        for file in $startup_files; do
            append_line "$file" "$sh_line"
        done
        IFS=$saved_ifs
        set +f
        if [ -n "$fish_file" ]; then
            mkdir -p "$(dirname "$fish_file")"
            echo "contains -- \"$dir\" \$PATH; or set -gx PATH \"$dir\" \$PATH $PATH_MARKER" >"$fish_file"
        fi
        path_added=1
        step "added $INSTALL_DIR to the PATH in $shown"
    fi
fi

step "configuration storage defaults to $APP_DATA_DIR (override with --app-data)"
echo ""

if ! on_path "$INSTALL_DIR"; then
    if [ -n "$path_added" ]; then
        echo "$command_name is on the PATH in new terminals. To use it in this one, run:"
    else
        echo "$INSTALL_DIR is not on the PATH. To add it in this terminal, run:"
    fi
    echo ""
    if [ "$(basename "${SHELL:-sh}")" = fish ]; then
        echo "    set -gx PATH \"$INSTALL_DIR\" \$PATH"
    else
        echo "    export PATH=\"$INSTALL_DIR:\$PATH\""
    fi
    echo ""
fi

echo "Go to $DOCS_URL for documentation."
echo ""
# sudo searches a fixed PATH without home directories, so it is given the full path.
echo "Now run \"sudo $(home_relative "$target" "~") up\" to start $APP_NAME as a daemon"
