#!/bin/sh
#
# Copyright 2025 AlmaForge Project.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

#
# One-off universal installer script intended to be piped to sh.
#
# Detects the host platform, installs the appropriate package or binary,
# and verifies the installation. Designed for first-time use only.
#
# NOTE: For portability, please avoid Bashisms and use POSIX syntax.
#

#
# User-overridable options.
#
: "${VERSION:=}" # '' or leave empty to install latest
readonly VERSION
: "${DOWNLOAD_URL:=https://get.almaforge.com}"
readonly DOWNLOAD_URL
: "${DEBUG:=}" # set to any non-empty value to trace every command
readonly DEBUG

#
# Product constants.
#
PRODUCT='AlmaForge'
PACKAGE='almaforge'
SERVER_BIN='almad'
DOWNLOAD_GPG_PUBLIC_KEY=$(
    cat <<'EOF'
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
=J7h+
-----END PGP PUBLIC KEY BLOCK-----

EOF
)
readonly PRODUCT PACKAGE SERVER_BIN DOWNLOAD_GPG_PUBLIC_KEY

###

log() {
    echo "==> $*"
}

err() {
    echo "ERROR: $*" >&2
}

set -eu

# The log() lines above are the progress channel a normal run prints.
# A shell trace is for debugging an install that went wrong, so it is
# opt-in rather than the first thing a newcomer sees.
if [ -n "${DEBUG}" ]; then
    set -x
fi

if [ -z "${DOWNLOAD_URL}" ]; then
    err 'DOWNLOAD_URL must be set'
    exit 1
fi

OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | amd64) ARCH=amd64 ;;
aarch64 | arm64) ARCH=arm64 ;;
*)
    err "This installer only supports x86_64 (amd64) and aarch64 (arm64)"
    exit 8
    ;;
esac
readonly OS ARCH

uid=$(id -u)
if [ "${uid}" -eq 0 ]; then
    # Already running as root: make sudo a no-op
    sudo() { "$@"; }
elif ! command -v sudo >/dev/null 2>&1; then
    err 'This script requires root privileges, but sudo is not installed'
    exit 2
fi

#
# Install from a YUM repository.
#
setup_yum_repo() {
    gpg_key="/etc/pki/rpm-gpg/${PACKAGE}"
    printf '%s' "${DOWNLOAD_GPG_PUBLIC_KEY}" | sudo tee "${gpg_key}" >/dev/null
    # RHEL 8 has ed25519 disabled in RPM.
    gpgcheck=$(rpm --version | awk '{print ($3 >= 4.16) ? 1 : 0}')
    sudo tee "/etc/yum.repos.d/${PACKAGE}.repo" <<EOF
[${PACKAGE}]
name=${PRODUCT}
baseurl=${DOWNLOAD_URL}
gpgkey=file://${gpg_key}
enabled=1
repo_gpgcheck=${gpgcheck}
gpgcheck=${gpgcheck}
EOF
}

install_via_dnf() {
    log "Installing ${PRODUCT} using DNF"
    sudo dnf install -y ca-certificates
    log "Adding YUM repository"
    setup_yum_repo
    sudo dnf makecache -y --disablerepo='*' --enablerepo="${PACKAGE}"
    log "Installing ${PACKAGE}"
    if [ -n "${VERSION}" ]; then
        log "Installing pinned version ${VERSION}"
        sudo dnf install -y --disablerepo='*' --enablerepo="${PACKAGE}" \
            --allowerasing "${PACKAGE}-${VERSION}"
    else
        log "Installing latest version"
        sudo dnf install -y --disablerepo='*' --enablerepo="${PACKAGE}" \
            --best "${PACKAGE}"
    fi
}

install_via_microdnf() {
    log "Installing ${PRODUCT} using microdnf"
    sudo microdnf install -y ca-certificates
    log "Adding YUM repository"
    setup_yum_repo
    sudo microdnf makecache -y --disablerepo='*' --enablerepo="${PACKAGE}"
    log "Installing ${PACKAGE}"
    if [ -n "${VERSION}" ]; then
        log "Installing pinned version ${VERSION}"
        sudo microdnf install -y --disablerepo='*' --enablerepo="${PACKAGE}" \
            "${PACKAGE}-${VERSION}"
    else
        log "Installing latest version"
        sudo microdnf install -y --disablerepo='*' --enablerepo="${PACKAGE}" \
            "${PACKAGE}"
    fi
}

#
# Install from a YUM repository on SUSE.
#
install_via_zypper() {
    log "Installing ${PRODUCT} using Zypper"
    log "Importing GPG key"
    gpg_key="$(mktemp)"
    printf '%s' "${DOWNLOAD_GPG_PUBLIC_KEY}" | sudo tee "${gpg_key}" >/dev/null
    sudo rpm --import "${gpg_key}"
    log "Adding YUM repository"
    sudo zypper ar -f "${DOWNLOAD_URL}" "${PACKAGE}"
    sudo zypper -n refresh "${PACKAGE}"
    log "Installing ${PACKAGE}"
    if [ -n "${VERSION}" ]; then
        log "Installing pinned version ${VERSION}"
        sudo zypper -n install "${PACKAGE}-${VERSION}"
    else
        log "Installing latest version"
        sudo zypper -n install "${PACKAGE}"
    fi
}

#
# Install from an APT repository.
#
install_via_apt() {
    log "Installing ${PRODUCT} using APT"
    export DEBIAN_FRONTEND=noninteractive
    sudo apt-get update -y
    if ! curl --version >/dev/null 2>&1 || ! gpg --version >/dev/null 2>&1; then
        log "Installing curl and gpg via APT"
        sudo apt-get install -y curl gpg
    fi
    log "Installing ca-certificates via APT"
    sudo apt-get install -y ca-certificates
    log "Adding APT repository"
    repo_file="/etc/apt/sources.list.d/${PACKAGE}.list"
    gpg_key="/etc/apt/keyrings/${PACKAGE}.gpg"
    printf '%s' "${DOWNLOAD_GPG_PUBLIC_KEY}" | gpg --dearmor | sudo tee "${gpg_key}" >/dev/null
    echo "deb [signed-by=\"${gpg_key}\"] ${DOWNLOAD_URL} generic main" |
        sudo tee "${repo_file}"
    sudo apt-get update -y
    log "Installing ${PACKAGE}"
    if [ -n "${VERSION}" ]; then
        log "Installing pinned version ${VERSION}"
        sudo apt-get install -y "${PACKAGE}=${VERSION}"
    else
        log "Installing latest version"
        sudo apt-get install -y "${PACKAGE}"
    fi
}

#
# A convenient helper to download and check binary artifacts.
# Inputs: os, arch, type.
# Output: artifact (global filename).
#
download_artifact() {
    if ! command -v curl >/dev/null 2>&1; then
        err 'curl is required'
        exit 3
    fi

    os="$1"
    arch="$2"
    type="$3"
    if [ -n "${VERSION}" ]; then
        log "Using pinned version ${VERSION}"
        version="${VERSION}"
    else
        log "Querying the latest version"
        version_url="${DOWNLOAD_URL}/VERSION"
        for url in "${version_url}" "${version_url}.asc"; do
            if ! curl -fSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 2 -O "${url}"; then
                err "Failed to download ${url}"
                exit 20
            fi
        done
        if command -v gpgv >/dev/null 2>&1 && command -v gpg >/dev/null 2>&1; then
            log "Validating the GPG signature of VERSION"
            gpg_key="${PWD}/${PACKAGE}.gpg"
            printf '%s' "${DOWNLOAD_GPG_PUBLIC_KEY}" | gpg --dearmor >"${gpg_key}"
            gpgv --quiet --keyring "${gpg_key}" VERSION.asc VERSION
        else
            err "Skipping GPG signature validation because gpgv or gpg is not installed"
        fi
        version="$(grep -m 1 -Eo '^[0-9]+\.[0-9]+\.[0-9]+' VERSION)"
        if [ -z "${version}" ]; then
            err "VERSION file at ${version_url} is empty or invalid"
            cat VERSION >&2
            exit 4
        fi
        log "Latest version is ${version}"
    fi

    artifact="${PACKAGE}-${version}-${os}-${arch}${type}"
    log "Downloading ${artifact}"
    url="${DOWNLOAD_URL}/${PACKAGE}/v${version}/${artifact}"

    for download_url in "${url}" "${url}.sha256" "${url}.sha256.asc"; do
        if ! curl -fSL --proto '=https' --tlsv1.2 --retry 3 --retry-delay 2 -O "${download_url}"; then
            err "Failed to download ${download_url}"
            exit 20
        fi
    done

    if command -v gpgv >/dev/null 2>&1 && command -v gpg >/dev/null 2>&1; then
        log "Validating GPG signature"
        gpg_key="${PWD}/${PACKAGE}.gpg"
        printf '%s' "${DOWNLOAD_GPG_PUBLIC_KEY}" | gpg --dearmor >"${gpg_key}"
        gpgv --quiet --keyring "${gpg_key}" "${artifact}.sha256.asc" "${artifact}.sha256"
    else
        err "Skipping GPG signature validation because gpgv or gpg is not installed"
    fi
    if command -v shasum >/dev/null 2>&1; then
        log "Validating SHA256 checksum"
        shasum -a 256 --check "${artifact}.sha256"
    elif command -v sha256sum >/dev/null 2>&1; then
        log "Validating SHA256 checksum"
        sha256sum -c "${artifact}.sha256"
    else
        err "Skipping SHA256 checksum validation because sha256sum is not installed"
    fi
}

#
# Install from a binary tarball.
#
install_via_tarball() {
    log "Installing ${PRODUCT} using a binary tarball"
    (
        workdir="$(mktemp -d)"
        trap 'rm -rf "${workdir}"' EXIT
        cd "${workdir}" || exit 5
        download_artifact "${OS}" "${ARCH}" ".tar.xz"
        log "Unpacking tarball"
        prefix=/usr/local
        sudo mkdir -p "${prefix}/bin"
        sudo tar -xavf "${artifact}" -C "${prefix}/bin" --strip-components=1 --exclude='VERSION'
        PATH=${prefix}/bin:${PATH}
        export PATH
        hash -r
    )
}

#
# Install from a macOS PKG.
#
install_via_macpkg() {
    log "Installing ${PRODUCT} using a macOS installer package"
    (
        workdir="$(mktemp -d)"
        trap 'rm -rf "${workdir}"' EXIT
        cd "${workdir}" || exit 6
        download_artifact "darwin" "${ARCH}" ".pkg"
        log "Installing macOS package"
        sudo installer -pkg "${artifact}" -target /
    )
}

# Detect the best way to install the package.
if [ "${OS}" = 'darwin' ]; then
    install_via_macpkg
elif [ "${OS}" = 'linux' ]; then
    if command -v apt-get >/dev/null 2>&1; then
        install_via_apt
    elif command -v dnf >/dev/null 2>&1; then
        install_via_dnf
    elif command -v microdnf >/dev/null 2>&1; then
        install_via_microdnf
    elif command -v zypper >/dev/null 2>&1; then
        install_via_zypper
    else
        # No package manager - install dependencies and use tarball
        # The dynamic loader is named after the architecture, and its
        # absence is how a musl system announces itself.
        case "${ARCH}" in
        amd64) loader='ld-linux-x86-64.so.2' ;;
        arm64) loader='ld-linux-aarch64.so.1' ;;
        # Unreachable: ARCH is normalized to one of the two above and
        # anything else has already exited. Named anyway, because a
        # third architecture reaching here would leave the loader unset
        # and die on "unbound variable" rather than say what is wrong.
        *)
            err "This installer only supports x86_64 (amd64) and aarch64 (arm64)"
            exit 8
            ;;
        esac
        if [ ! -e "/lib64/${loader}" ] && [ ! -e "/lib/${loader}" ]; then
            err "This installer only supports glibc-based systems"
            err "Please kindly contact support"
            exit 8
        fi
        log "Checking and installing required dependencies"
        if command -v pacman >/dev/null 2>&1; then
            sudo pacman -Sy --noconfirm curl tar xz ca-certificates gnupg
        elif command -v xbps-install >/dev/null 2>&1; then
            sudo xbps-install -Sy curl tar xz ca-certificates gnupg2
        else
            err "Unable to detect package manager, hoping dependencies are already installed"
            err "Please kindly contact support if it fails"
        fi
        install_via_tarball
    fi
else
    err "Unsupported OS: ${OS}"
    exit 9
fi

log "Checking ${SERVER_BIN} version"
if ! "${SERVER_BIN}" version | grep -F "${PRODUCT}"; then
    err "${SERVER_BIN} not found in PATH"
    exit 10
fi

# Create the config and data directories so a package manager that ships
# neither, or a tarball install, still leaves a working layout behind.
# The data directory holds host identity and keys, so it must be 0700 to
# match what almad enforces when it creates the directory itself.
log "Creating ${PRODUCT} directories"
sudo mkdir -p '/etc/almaforge/almad.d'
sudo mkdir -p '/var/lib/almaforge'
sudo chmod 700 '/var/lib/almaforge'

log "${PRODUCT} installed successfully"

exit 0
