#!/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.
# golangci-lint is installed via `go install` pinned to the same version
# the Dockerfile lint stage uses (never "latest"), and is reinstalled
# whenever the installed version differs from that pin. The install is
# then verified against the binary PATH actually resolves: if the pin is
# still not what would run, bootstrap fails instead of reporting
# success.
set -eu

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

# Pinned versions, 2026-08-07 (same version as the Dockerfile lint stage).
# This is the single source of truth for the linter version: the module
# ref below and the version comparison in main() are both derived from
# it, so a bump here cannot half-apply. Written without a leading "v",
# the way `golangci-lint --version` reports it.
GOLANGCI_LINT_VERSION="2.12.2"
GOLANGCI_LINT_MODULE="github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
GOLANGCI_LINT_REF="$GOLANGCI_LINT_MODULE@v$GOLANGCI_LINT_VERSION"

# Seconds to allow `golangci-lint --version` to run. Bootstrap now
# executes the binary rather than merely locating it, so a wedged one
# must not hang the script.
GOLANGCI_LINT_VERSION_TIMEOUT="30"

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
}

# Echo the installed golangci-lint version, or nothing when the tool is
# absent. The binary reports e.g.
#   golangci-lint has version X.Y.Z built with go1.26.5 from abc1234 ...
# so the version is the field after the literal word "version", and it
# carries no leading "v" (the module ref does). Some builds do print a
# leading "v", so strip one if present and compare bare versions.
#
# Only stdout is parsed; the binary's stderr is deliberately left
# connected to ours so that a present-but-broken linter (missing shared
# library, wrong architecture) says why instead of silently yielding the
# empty string. The call is bounded by timeout(1) where that exists —
# stock macOS has no timeout(1), and there the call runs unbounded, as
# it did before this check was version-aware.
golangci_lint_version() {
    command -v golangci-lint >/dev/null 2>&1 || return 0
    if command -v timeout >/dev/null 2>&1; then
        timeout "$GOLANGCI_LINT_VERSION_TIMEOUT" golangci-lint --version
    else
        golangci-lint --version
    fi | awk '
        {
            for (i = 1; i < NF; i++) {
                if ($i == "version") {
                    v = $(i + 1)
                    sub(/^v/, "", v)
                    print v
                    exit
                }
            }
        }
    '
}

# Confirm that the golangci-lint just installed is the one that will
# actually run. `go install` writes into "$(go env GOBIN)" (or
# "$(go env GOPATH)/bin"), but `make lint` runs whatever PATH resolves
# first. When a wrong-version binary sits ahead of that directory — a
# nix profile, apt, brew, apk, or a tarball in /usr/local/bin — the
# install lands behind the shadow and changes nothing the gate uses.
# Exiting 0 there would leave the local gate linting against a different
# ruleset than CI while claiming success, which is the failure this
# whole check exists to prevent. Diagnose and stop: naming both paths is
# what makes it fixable. Reordering PATH or deleting someone else's
# binary is not bootstrap's call.
verify_golangci_lint() {
    # Forget any remembered command locations first: the install may have
    # created a binary in a directory the shell already searched.
    hash -r 2>/dev/null || true

    goinstalldir="$(go env GOBIN)"
    if [ -z "$goinstalldir" ]; then
        goinstalldir="$(go env GOPATH)/bin"
    fi
    resolved="$(command -v golangci-lint 2>/dev/null || true)"
    effective="$(golangci_lint_version)"
    if [ "$effective" != "$GOLANGCI_LINT_VERSION" ]; then
        echo "bootstrap: installed golangci-lint $GOLANGCI_LINT_VERSION into" \
            "$goinstalldir, but the golangci-lint on PATH is" \
            "${resolved:-not resolvable} and reports" \
            "${effective:-no parseable version}" >&2
        echo "bootstrap: the install is shadowed or unreachable; put" \
            "$goinstalldir ahead of it on PATH (or remove the shadowing" \
            "binary) and re-run" >&2
        exit 1
    fi
}

main() {
    cd "$ROOT"

    # System tooling, deliberately unpinned: these come from the host
    # package manager and whatever version it ships is what the host
    # gets, so a presence check is the right check. The repo pins no
    # system toolchain versions — the Go language version is governed by
    # go.mod, and builds that must be reproducible run in the Docker
    # image, whose base images are pinned by digest.
    if missing git; then pkg_install git git git git; fi
    if missing make; then pkg_install gnumake make make make; fi
    if missing go; then pkg_install go golang go go; fi

    # Lint tooling, pinned via go install (installs into
    # "$(go env GOPATH)/bin"; ensure that is on your PATH). Unlike the
    # system tools above this is version-checked, not presence-checked:
    # the Dockerfile lint stage runs a digest-pinned linter, so a host
    # running any other version lints against different rules and
    # `make check` can go green on a commit CI then rejects. Any version
    # that is not the pin — older or newer — is reinstalled, and the
    # install is then verified to be the binary PATH resolves.
    installed="$(golangci_lint_version)"
    if [ "$installed" != "$GOLANGCI_LINT_VERSION" ]; then
        echo "bootstrap: golangci-lint ${installed:-absent or unparseable}," \
            "want $GOLANGCI_LINT_VERSION; installing"
        go install "$GOLANGCI_LINT_REF"
        verify_golangci_lint
    fi

    go mod download

    echo "bootstrap complete"
}

main "$@"
