#!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop
# this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present. Node is
# used directly if installed; otherwise it is installed at a pinned
# version via nvm (installing nvm itself first, from a hash-verified
# release archive, never curl | sh). Go, with its gofmt, is used
# directly if it is at least the version backend/go.mod asks for;
# otherwise the pinned Go release is installed from its hash-verified
# archive.
#
# What this script installs outside the system package manager lives
# under $HOME and is linked into ~/.local/bin, where make and the git
# hook find it once that directory is on PATH. Nothing in ~/.local/bin
# that this script did not create is ever replaced.
#
# golangci-lint is not installed: make lint runs it in Docker, which
# this script does not install either.
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"

# Pinned versions, 2026-07-07
NODE_VERSION="22.17.0"
NVM_VERSION="0.40.3"
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
YARN_VERSION="1.22.22"
# The Go inside the golang:1.25-alpine image Dockerfile builds the
# backend with, 2026-08-09. The archive hashes are in ensure_go.
GO_VERSION="1.25.7"

BIN_DIR="$HOME/.local/bin"
TOOLCHAIN="$HOME/.local/share/$("$ROOT/script/projectname")/toolchain"

PKGMGR=""
SUDO=""
APT_UPDATED=""

detect_pkgmgr() {
    [ -n "$PKGMGR" ] && return 0
    if command -v nix-env >/dev/null 2>&1; then
        PKGMGR="nix"
    elif command -v apt-get >/dev/null 2>&1; then
        PKGMGR="apt"
    elif command -v brew >/dev/null 2>&1; then
        PKGMGR="brew"
    elif command -v apk >/dev/null 2>&1; then
        PKGMGR="apk"
    else
        echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
        exit 1
    fi
    if [ "$PKGMGR" = "apt" ]; then
        export DEBIAN_FRONTEND=noninteractive
        if [ "$(id -u)" != "0" ]; then
            SUDO="sudo"
        fi
    fi
}

# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
pkg_install() {
    detect_pkgmgr
    case "$PKGMGR" in
        nix) nix-env -iA "nixpkgs.$1" ;;
        apt)
            if [ -z "$APT_UPDATED" ]; then
                $SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
                APT_UPDATED=1
            fi
            $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
            ;;
        brew) brew install "$3" ;;
        apk) apk add --no-cache "$4" ;;
    esac
}

missing() {
    ! command -v "$1" >/dev/null 2>&1
}

# verify_sha256 <file> <expected-hash>
verify_sha256() {
    if command -v sha256sum >/dev/null 2>&1; then
        actual="$(sha256sum "$1" | cut -d' ' -f1)"
    else
        actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
    fi
    if [ "$actual" != "$2" ]; then
        echo "bootstrap: sha256 mismatch for $1" >&2
        echo "  expected: $2" >&2
        echo "  actual:   $actual" >&2
        exit 1
    fi
}

# link_bin <target> <name>: make an installed tool reachable as
# $BIN_DIR/<name>. Only a symlink this script made, one pointing into
# $TOOLCHAIN or ~/.nvm, is ever replaced; if anything else is already
# there, bootstrap stops.
link_bin() {
    link="$BIN_DIR/$2"
    if [ -L "$link" ] || [ -e "$link" ]; then
        case "$(readlink "$link" || true)" in
            "$TOOLCHAIN"/* | "$HOME"/.nvm/*) ;;
            *)
                echo "bootstrap: $link was not created by this script;" >&2
                echo "  remove or rename it, then re-run bootstrap" >&2
                exit 1
                ;;
        esac
    fi
    mkdir -p "$BIN_DIR"
    ln -sf "$1" "$link"
}

# nvm is a bash script; run a command in a bash with nvm loaded
nvm_sh() {
    bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
}

ensure_nvm() {
    [ -s "$HOME/.nvm/nvm.sh" ] && return 0
    # nvm prerequisites; nvm itself requires bash
    if missing bash; then pkg_install bash bash bash bash; fi
    if missing curl; then pkg_install curl curl curl curl; fi
    if missing git; then pkg_install git git git git; fi
    tmp="$(mktemp -d)"
    curl -fsSL -o "$tmp/nvm.tar.gz" \
        "https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
    verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
    mkdir -p "$HOME/.nvm"
    tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
    rm -rf "$tmp"
}

ensure_node() {
    if ! missing node; then return 0; fi
    ensure_nvm
    nvm_sh "nvm install $NODE_VERSION"
    link_bin "$HOME/.nvm/versions/node/v$NODE_VERSION/bin/node" node
}

# ensure_yarn: corepack writes its shims (pnpm and yarnpkg as well as
# yarn) into $TOOLCHAIN rather than next to itself, and the npm fallback
# installs there too; only yarn is linked.
ensure_yarn() {
    if ! missing yarn; then return 0; fi
    shims="$TOOLCHAIN/corepack-shims"
    mkdir -p "$shims"
    if ! missing corepack; then
        corepack enable --install-directory "$shims"
        corepack prepare "yarn@$YARN_VERSION" --activate
    elif [ -s "$HOME/.nvm/nvm.sh" ]; then
        nvm_sh "nvm use $NODE_VERSION >/dev/null && \
            corepack enable --install-directory \"$shims\" && \
            corepack prepare yarn@$YARN_VERSION --activate"
    else
        npm install -g --prefix "$TOOLCHAIN/npm-global" "yarn@$YARN_VERSION"
        shims="$TOOLCHAIN/npm-global/bin"
    fi
    link_bin "$shims/yarn" yarn
}

# go_ok: the go on PATH has its gofmt beside it (a Go release ships the
# two together) and is at least the version backend/go.mod asks for.
# GOTOOLCHAIN=local makes an older go fail here instead of fetching a
# newer toolchain for itself.
go_ok() {
    if missing go; then return 1; fi
    [ -x "$(dirname "$(command -v go)")/gofmt" ] || return 1
    (cd "$ROOT/backend" && GOTOOLCHAIN=local go list -m >/dev/null 2>&1)
}

# ensure_go: unless go_ok, install GO_VERSION and link its go and gofmt.
# They are linked on every run that needs them, so a deleted link is put
# back, and the archive is unpacked again if either binary is missing.
ensure_go() {
    if go_ok; then return 0; fi
    go_dir="$TOOLCHAIN/go-$GO_VERSION"
    if [ ! -x "$go_dir/bin/go" ] || [ ! -x "$go_dir/bin/gofmt" ]; then
        # sha256 of each archive, from https://go.dev/dl/?mode=json
        case "$(uname -s)-$(uname -m)" in
            Linux-x86_64)
                plat="linux-amd64"
                sha="12e6d6a191091ae27dc31f6efc630e3a3b8ba409baf3573d955b196fdf086005"
                ;;
            Linux-aarch64)
                plat="linux-arm64"
                sha="ba611a53534135a81067240eff9508cd7e256c560edd5d8c2fef54f083c07129"
                ;;
            Darwin-x86_64)
                plat="darwin-amd64"
                sha="bf5050a2152f4053837b886e8d9640c829dbacbc3370f913351eb0904cb706f5"
                ;;
            Darwin-arm64)
                plat="darwin-arm64"
                sha="ff18369ffad05c57d5bed888b660b31385f3c913670a83ef557cdfd98ea9ae1b"
                ;;
            *)
                echo "bootstrap: no pinned Go release for this platform" >&2
                exit 1
                ;;
        esac
        if missing curl; then pkg_install curl curl curl curl; fi
        mkdir -p "$TOOLCHAIN"
        curl -fsSL -o "$go_dir.tar.gz" \
            "https://go.dev/dl/go$GO_VERSION.$plat.tar.gz"
        verify_sha256 "$go_dir.tar.gz" "$sha"
        # Unpacked beside its final place and then moved there, so an
        # interrupted run never leaves a partial Go that looks complete.
        rm -rf "$go_dir.partial"
        mkdir "$go_dir.partial"
        tar -xzf "$go_dir.tar.gz" -C "$go_dir.partial" --strip-components=1
        rm -rf "$go_dir" "$go_dir.tar.gz"
        mv "$go_dir.partial" "$go_dir"
    fi
    link_bin "$go_dir/bin/go" go
    link_bin "$go_dir/bin/gofmt" gofmt
}

main() {
    cd "$ROOT"

    # Tools linked on an earlier run count as installed, and tools linked
    # on this run are found by the steps after it.
    path_hint=""
    case ":$PATH:" in
        *":$BIN_DIR:"*) ;;
        *) path_hint=yes ;;
    esac
    PATH="$BIN_DIR:$PATH"

    if missing make; then pkg_install gnumake make make make; fi
    if missing git; then pkg_install git git git git; fi

    ensure_node
    ensure_yarn
    yarn install --frozen-lockfile

    ensure_go
    (cd "$ROOT/backend" && go mod download)

    if missing docker; then
        echo "bootstrap: docker not found; make lint, and so make check" >&2
        echo "  and the pre-commit hook, need it to run the Go linter" >&2
    fi
    if [ -n "$path_hint" ] && [ -d "$BIN_DIR" ]; then
        echo "bootstrap: add $BIN_DIR to the front of your PATH, e.g." >&2
        echo "  export PATH=\"\$HOME/.local/bin:\$PATH\"" >&2
    fi

    echo "bootstrap complete"
}

main "$@"
