#!/bin/sh
# HeatherDB installer — macOS and Linux.
#
#   curl -fsSL https://heatherdb.com/install.sh | sh
#
# What it does, in order: work out your platform, resolve the latest release
# on GitHub, download the matching archive, verify its SHA-256 against the
# checksum manifest published with the release, extract it, and move one
# binary — `heather` — into place. Nothing else is written to your system.
#
# Environment overrides:
#   HEATHER_VERSION       release tag to install, e.g. v0.3.0  (default: latest)
#   HEATHER_INSTALL_DIR   where the binary goes  (default: /usr/local/bin,
#                         falling back to ~/.local/bin when that needs a
#                         password you may not have)
#   HEATHER_LIBC          gnu | musl — override libc detection on Linux
#   HEATHER_REPO          owner/name of the GitHub repo to pull from
#   HEATHER_NO_SUDO=1     never escalate; use ~/.local/bin instead
#
# Exit codes: 0 success, 1 anything else. The script does the whole download
# and verification in a temporary directory and only touches the install
# directory at the very last step, so a failure part-way leaves nothing behind.

set -eu

REPO="${HEATHER_REPO:-aphorion/heatherdb}"
BIN_NAME="heather"
TMPDIR_HEATHER=""

# ─── output helpers ─────────────────────────────────────────────────────────
# Colour only when stderr is a terminal; piped-into-sh installs are frequently
# captured to a log where escape codes are noise.
if [ -t 2 ]; then
  C_DIM='\033[2m'; C_BOLD='\033[1m'; C_RED='\033[31m'; C_OFF='\033[0m'
else
  C_DIM=''; C_BOLD=''; C_RED=''; C_OFF=''
fi

say()  { printf '%s\n' "$*" >&2; }
step() { printf "${C_BOLD}==>${C_OFF} %s\n" "$*" >&2; }
note() { printf "${C_DIM}    %s${C_OFF}\n" "$*" >&2; }
die()  { printf "${C_RED}error:${C_OFF} %s\n" "$*" >&2; exit 1; }

cleanup() {
  if [ -n "$TMPDIR_HEATHER" ] && [ -d "$TMPDIR_HEATHER" ]; then
    rm -rf "$TMPDIR_HEATHER"
  fi
}
trap cleanup EXIT INT TERM

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

# ─── fetch: one downloader, curl or wget ────────────────────────────────────
if have curl; then
  DOWNLOADER=curl
elif have wget; then
  DOWNLOADER=wget
else
  die "need either curl or wget on PATH to download anything"
fi

# fetch_to <url> <dest> — hard failure on any non-2xx.
fetch_to() {
  case "$DOWNLOADER" in
    curl) curl -fsSL --retry 3 --retry-delay 1 -o "$2" "$1" ;;
    wget) wget -q --tries=3 -O "$2" "$1" ;;
  esac
}

# fetch_stdout <url> — prints the body, non-fatal (caller checks the status).
fetch_stdout() {
  case "$DOWNLOADER" in
    curl) curl -fsSL --retry 2 "$1" ;;
    wget) wget -q -O - "$1" ;;
  esac
}

# ─── platform detection ─────────────────────────────────────────────────────
detect_os() {
  os="$(uname -s)"
  case "$os" in
    Darwin) echo macos ;;
    Linux)  echo linux ;;
    *)
      die "unsupported operating system: $os
    HeatherDB ships builds for macOS and Linux. On Windows use:
      irm https://heatherdb.com/install.ps1 | iex
    On anything else, build from source:
      https://github.com/${REPO}"
      ;;
  esac
}

detect_arch() {
  arch="$(uname -m)"
  case "$arch" in
    x86_64|amd64)        echo x86_64 ;;
    aarch64|arm64)       echo aarch64 ;;
    *)
      die "unsupported CPU architecture: $arch
    Builds exist for x86_64 and aarch64/arm64 only. Build from source:
      https://github.com/${REPO}"
      ;;
  esac
}

# glibc or musl. `ldd --version` writes to stderr on glibc and exits non-zero
# on musl, so both streams are captured and the exit status ignored.
detect_libc() {
  if [ -n "${HEATHER_LIBC:-}" ]; then
    case "$HEATHER_LIBC" in
      gnu|musl) echo "$HEATHER_LIBC"; return ;;
      *) die "HEATHER_LIBC must be 'gnu' or 'musl', got: $HEATHER_LIBC" ;;
    esac
  fi

  ldd_out="$( (ldd --version) 2>&1 || true )"
  case "$ldd_out" in
    *musl*) echo musl; return ;;
    *GNU*|*GLIBC*|*glibc*)
      # GNU releases are runtime-tested on Ubuntu 24.04 (glibc 2.39).
      # Prefer the static build when that baseline is unavailable or unknown.
      glibc_version="$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}')"
      if [ -n "$glibc_version" ] && awk -v v="$glibc_version" 'BEGIN { split(v,p,"."); exit !(p[1] > 2 || (p[1] == 2 && p[2] >= 39)) }'; then
        echo gnu
      else
        echo musl
      fi
      return ;;

  esac

  # No usable ldd (busybox, stripped container). Look for the musl loader.
  for loader in /lib/ld-musl-*.so.1 /lib/ld-musl-*; do
    [ -e "$loader" ] && { echo musl; return; }
  done

  # Nothing conclusive. The static musl build runs everywhere glibc does,
  # so guessing musl is the safe direction to be wrong in.
  echo musl
}

# ─── resolve the release tag ────────────────────────────────────────────────
# Asset filenames embed the version, so "latest" has to become a real tag
# before any URL can be built. Two ways, cheapest first:
#   1. GitHub redirects /releases/latest to /releases/tag/<tag> — no API,
#      no rate limit, no token.
#   2. The REST API, as a fallback.
resolve_version() {
  if [ -n "${HEATHER_VERSION:-}" ]; then
    echo "$HEATHER_VERSION"
    return
  fi

  tag=""
  if [ "$DOWNLOADER" = curl ]; then
    url="$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
           "https://github.com/${REPO}/releases/latest" 2>/dev/null || true)"
    case "$url" in
      */releases/tag/*) tag="${url##*/releases/tag/}" ;;
    esac
  fi

  if [ -z "$tag" ]; then
    body="$(fetch_stdout "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null || true)"
    tag="$(printf '%s' "$body" \
           | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
           | head -n 1)"
  fi

  [ -n "$tag" ] || die "could not resolve the latest release of ${REPO}.
    The repository may be private, or GitHub may be unreachable from here.
    Pin a version explicitly and retry:
      HEATHER_VERSION=v0.3.0 sh -c \"\$(curl -fsSL https://heatherdb.com/install.sh)\""

  echo "$tag"
}

# ─── checksum ───────────────────────────────────────────────────────────────
sha256_of() {
  if have sha256sum; then
    sha256sum "$1" | cut -d' ' -f1
  elif have shasum; then
    shasum -a 256 "$1" | cut -d' ' -f1
  elif have openssl; then
    openssl dgst -sha256 "$1" | sed 's/.*= *//'
  else
    echo ""
  fi
}

# ─── main ───────────────────────────────────────────────────────────────────
OS="$(detect_os)"
ARCH="$(detect_arch)"

if [ "$OS" = linux ]; then
  LIBC="$(detect_libc)"
  LABEL="linux-${ARCH}-${LIBC}"
  PLATFORM_DESC="Linux ${ARCH} (${LIBC})"
else
  LIBC=""
  LABEL="macos-${ARCH}"
  case "$ARCH" in
    aarch64) PLATFORM_DESC="macOS (Apple silicon)" ;;
    *)       PLATFORM_DESC="macOS (Intel)" ;;
  esac
fi

step "Detecting platform"
note "$PLATFORM_DESC → build '${LABEL}'"

step "Resolving release"
VERSION="$(resolve_version)"
note "${REPO} ${VERSION}"

ASSET="${BIN_NAME}-${VERSION}-${LABEL}.tar.gz"
BASE="https://github.com/${REPO}/releases/download/${VERSION}"

# ─── pick an install directory ──────────────────────────────────────────────
SUDO=""
if [ -n "${HEATHER_INSTALL_DIR:-}" ]; then
  INSTALL_DIR="$HEATHER_INSTALL_DIR"
  # An explicit directory is honoured as given; escalate only if we must and
  # can, otherwise fail rather than silently install somewhere else.
  if [ ! -d "$INSTALL_DIR" ]; then
    mkdir -p "$INSTALL_DIR" 2>/dev/null || {
      if [ "${HEATHER_NO_SUDO:-0}" != 1 ] && have sudo; then
        SUDO="sudo"
      else
        die "cannot create HEATHER_INSTALL_DIR=$INSTALL_DIR"
      fi
    }
  fi
  if [ -z "$SUDO" ] && [ -d "$INSTALL_DIR" ] && [ ! -w "$INSTALL_DIR" ]; then
    if [ "${HEATHER_NO_SUDO:-0}" != 1 ] && have sudo; then
      SUDO="sudo"
    else
      die "HEATHER_INSTALL_DIR=$INSTALL_DIR is not writable and sudo is unavailable"
    fi
  fi
elif [ -w /usr/local/bin ] 2>/dev/null; then
  INSTALL_DIR=/usr/local/bin
elif [ "$(id -u)" = 0 ]; then
  INSTALL_DIR=/usr/local/bin
elif [ "${HEATHER_NO_SUDO:-0}" != 1 ] && have sudo; then
  INSTALL_DIR=/usr/local/bin
  SUDO="sudo"
else
  INSTALL_DIR="${HOME}/.local/bin"
fi

step "Plan"
note "download  ${BASE}/${ASSET}"
note "verify    SHA-256 against SHA256SUMS.txt from the same release"
note "install   ${INSTALL_DIR}/${BIN_NAME}"
if [ -n "$SUDO" ]; then
  note "sudo      yes — the final move needs it (${INSTALL_DIR} is root-owned)"
fi

TMPDIR_HEATHER="$(mktemp -d 2>/dev/null || mktemp -d -t heather)"
[ -d "$TMPDIR_HEATHER" ] || die "could not create a temporary directory"

step "Downloading ${ASSET}"
fetch_to "${BASE}/${ASSET}" "${TMPDIR_HEATHER}/${ASSET}" || die "download failed: ${BASE}/${ASSET}
    That build may not exist for release ${VERSION}. Every published archive is
    listed at https://heatherdb.com/download"

step "Verifying checksum"
ACTUAL="$(sha256_of "${TMPDIR_HEATHER}/${ASSET}")"
[ -n "$ACTUAL" ] || die "no sha256sum, shasum or openssl on PATH — cannot verify the download.
    Install one of them, or set HEATHER_SKIP_CHECKSUM=1 to proceed unverified."

EXPECTED=""
# The release publishes one combined manifest; older releases published a
# per-asset sidecar instead. Try both before giving up.
if MANIFEST="$(fetch_stdout "${BASE}/SHA256SUMS.txt" 2>/dev/null)" && [ -n "$MANIFEST" ]; then
  EXPECTED="$(printf '%s\n' "$MANIFEST" \
              | grep -F " ${ASSET}" \
              | head -n 1 | cut -d' ' -f1)"
fi
if [ -z "$EXPECTED" ]; then
  if SIDECAR="$(fetch_stdout "${BASE}/${ASSET}.sha256" 2>/dev/null)" && [ -n "$SIDECAR" ]; then
    EXPECTED="$(printf '%s\n' "$SIDECAR" | head -n 1 | cut -d' ' -f1)"
  fi
fi

if [ -z "$EXPECTED" ]; then
  if [ "${HEATHER_SKIP_CHECKSUM:-0}" = 1 ]; then
    note "no published checksum found — continuing because HEATHER_SKIP_CHECKSUM=1"
  else
    die "no published checksum found for ${ASSET} in release ${VERSION}.
    Refusing to install an unverified binary. Set HEATHER_SKIP_CHECKSUM=1 to
    override, or download by hand from https://heatherdb.com/download"
  fi
elif [ "$EXPECTED" != "$ACTUAL" ]; then
  die "checksum mismatch for ${ASSET}
      expected  ${EXPECTED}
      actual    ${ACTUAL}
    Nothing was installed. This is worth reporting:
      https://github.com/${REPO}/issues"
else
  note "sha256 ${ACTUAL}"
fi

step "Extracting"
tar -xzf "${TMPDIR_HEATHER}/${ASSET}" -C "$TMPDIR_HEATHER" \
  || die "could not extract ${ASSET} — the download may be truncated"

# The archive holds one directory, `heather-<version>-<label>/`, but the
# binary is located rather than assumed so a layout change does not silently
# install nothing.
SRC=""
for candidate in \
  "${TMPDIR_HEATHER}/${BIN_NAME}-${VERSION}-${LABEL}/${BIN_NAME}" \
  "${TMPDIR_HEATHER}/${BIN_NAME}"
do
  [ -f "$candidate" ] && { SRC="$candidate"; break; }
done
if [ -z "$SRC" ]; then
  SRC="$(find "$TMPDIR_HEATHER" -type f -name "$BIN_NAME" -print 2>/dev/null | head -n 1)"
fi
[ -n "$SRC" ] && [ -f "$SRC" ] || die "no '${BIN_NAME}' binary inside ${ASSET}"

chmod 755 "$SRC"

step "Installing to ${INSTALL_DIR}"
if [ -n "$SUDO" ]; then
  $SUDO mkdir -p "$INSTALL_DIR" || die "could not create ${INSTALL_DIR}"
  # install(1) writes atomically; a failed write leaves the old binary intact.
  $SUDO install -m 755 "$SRC" "${INSTALL_DIR}/${BIN_NAME}" \
    || die "could not install to ${INSTALL_DIR} (even with sudo)"
else
  mkdir -p "$INSTALL_DIR" || die "could not create ${INSTALL_DIR}"
  install -m 755 "$SRC" "${INSTALL_DIR}/${BIN_NAME}" 2>/dev/null \
    || cp "$SRC" "${INSTALL_DIR}/${BIN_NAME}" \
    || die "could not install to ${INSTALL_DIR}"
  chmod 755 "${INSTALL_DIR}/${BIN_NAME}"
fi

# macOS quarantines anything downloaded by a browser or curl; clearing it here
# is the difference between `heather --version` working and Gatekeeper killing
# the process. The builds are not codesigned or notarized.
if [ "$OS" = macos ] && have xattr; then
  ${SUDO:+$SUDO} xattr -d com.apple.quarantine "${INSTALL_DIR}/${BIN_NAME}" 2>/dev/null || true
fi

step "Done"
note "$("${INSTALL_DIR}/${BIN_NAME}" --version 2>/dev/null || echo "${BIN_NAME} installed")"

# Is it actually reachable?
case ":${PATH}:" in
  *":${INSTALL_DIR}:"*) ;;
  *)
    say ""
    say "${INSTALL_DIR} is not on your PATH. Add it:"
    say ""
    say "    export PATH=\"${INSTALL_DIR}:\$PATH\""
    say ""
    say "…and put that line in ~/.bashrc, ~/.zshrc, or your shell's equivalent."
    ;;
esac

say ""
say "Next: start it."
say ""
say "    HEATHER_DATA_DIR=/tmp/heather-quickstart \\"
say "    HEATHER_DIMENSION=8 \\"
say "    HEATHER_ADMIN_USER=admin \\"
say "    HEATHER_ADMIN_PASSWORD='quickstart-password' \\"
say "      ${BIN_NAME}"
say ""
say "Docs: https://heatherdb.com/docs/getting-started/"
