#!/usr/bin/env bash
# scripts/install.sh — per-platform bootstrap for the Yaarbal CLI
# (quick-260901-1mg, cross-platform CLI epic item 4).
#
# Creates (or reuses) a venv, verifies Python >= 3.11, upgrades pip, and
# installs yaarbal[tracker,seeder] with the llama-cpp-python prebuilt wheel
# index chosen for this platform (metal / cu124 / cpu, or a plain PyPI
# source-build fallback), then prints the `yaarbal` next steps.
#
# Two install sources (quick-261007-dst, DST-02 -- the repository is private,
# so most users have no checkout):
#   wheel     YAARBAL_WHEEL / --wheel names a yaarbal wheel by local path or
#             https:// URL. This script then needs nothing else from the
#             repository and can be run from anywhere, alone. There is NO
#             built-in download location: with no wheel named and no checkout
#             around the script, it stops and says what to set.
#   checkout  no wheel named and the script sits in scripts/ of a source
#             checkout (team members): editable install of that checkout,
#             exactly as before.
#
# `pip install -e ".[seeder]"` against plain PyPI has no llama-cpp-python
# wheels and forces a CMake source build on every platform; pip cannot encode
# an index URL in pyproject.toml, so the index selection has to live here.
#
# Usage:
#   install.sh [--wheel PATH|URL] [--dry-run] [--no-seeder] [--flavor metal|cu124|cpu|source] [--extras a,b] [-h|--help]
#
# Env overrides (all optional):
#   YAARBAL_WHEEL            yaarbal wheel to install: local path or https:// URL (same as --wheel)
#   YAARBAL_WHEEL_SHA256     expected SHA-256 (hex) of that wheel; the install stops on a mismatch
#   RMG_WHEEL, RMG_WHEEL_SHA256
#                            older names for the two variables above; still read when the
#                            YAARBAL_* one is unset (quick-261007-ybl, product rename)
#   RENTMYGPU_LLAMA_FLAVOR   forces the llama-cpp-python wheel flavor (same as --flavor)
#   RMG_VENV_DIR             venv location (default: <repo>/.venv from a checkout,
#                            ~/.rentmygpu-venv when installing a wheel)
#   RMG_PYTHON                interpreter to bootstrap with (default: python3)
#   RMG_WSL_NVIDIA_SMI        path checked for a WSL-passthrough nvidia-smi (default: /usr/lib/wsl/lib/nvidia-smi)
#   RMG_PROC_VERSION          path checked for the "microsoft" WSL marker (default: /proc/version)
set -euo pipefail

# The directory above this script: the repo root when run from a checkout.
# Empty when the script has no file of its own (piped into `bash -s`), and
# meaningless when the script was handed out alone -- is_checkout decides.
REPO_ROOT=""
if [ -n "${BASH_SOURCE[0]:-}" ]; then
  REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd || true)"
fi

# --- Logging -----------------------------------------------------------------
if [ -t 1 ]; then _c_grn=$'\033[32m'; _c_ylw=$'\033[33m'; _c_red=$'\033[31m'; _c_dim=$'\033[2m'; _c_rst=$'\033[0m'
else _c_grn=; _c_ylw=; _c_red=; _c_dim=; _c_rst=; fi
info() { printf '%s==>%s %s\n' "$_c_grn" "$_c_rst" "$*"; }
step() { printf '%s--- %s%s\n' "$_c_dim" "$*" "$_c_rst"; }
warn() { printf '%swarn:%s %s\n' "$_c_ylw" "$_c_rst" "$*" >&2; }
die()  { printf '%serror:%s %s\n' "$_c_red" "$_c_rst" "$*" >&2; exit 1; }

# --- Platform / flavor detection ---------------------------------------------

# detect_nvidia: 0 if `nvidia-smi` is on PATH, or the WSL-passthrough path is
# executable. The env default lets tests point it at a non-existent path.
detect_nvidia() {
  command -v nvidia-smi >/dev/null 2>&1 && return 0
  local wsl_nvidia_smi="${RMG_WSL_NVIDIA_SMI:-/usr/lib/wsl/lib/nvidia-smi}"
  [ -x "$wsl_nvidia_smi" ] && return 0
  return 1
}

# is_wsl: 0 if /proc/version (or RMG_PROC_VERSION override) exists and
# mentions "microsoft" (case-insensitive).
is_wsl() {
  local proc_version="${RMG_PROC_VERSION:-/proc/version}"
  [ -r "$proc_version" ] || return 1
  grep -qi microsoft "$proc_version" 2>/dev/null
}

# select_flavor: prints exactly one of metal|cu124|cpu|source on stdout.
select_flavor() {
  local uname_s="${RMG_UNAME_S:-$(uname -s)}"
  local uname_m="${RMG_UNAME_M:-$(uname -m)}"

  if [ -n "${RENTMYGPU_LLAMA_FLAVOR:-}" ]; then
    case "$RENTMYGPU_LLAMA_FLAVOR" in
      metal|cu124|cpu|source) printf '%s\n' "$RENTMYGPU_LLAMA_FLAVOR"; return 0 ;;
      *) die "RENTMYGPU_LLAMA_FLAVOR must be one of: metal cu124 cpu source (got: $RENTMYGPU_LLAMA_FLAVOR)" ;;
    esac
  fi

  if [ "$uname_s" = "Darwin" ]; then
    case "$uname_m" in
      arm64|aarch64) printf 'metal\n'; return 0 ;;
      *)
        warn "no prebuilt Intel-Mac llama-cpp-python wheel exists on any index — pip will build it from source (needs Xcode Command Line Tools + cmake)."
        printf 'source\n'
        return 0
        ;;
    esac
  fi

  if detect_nvidia; then
    case "$uname_m" in
      x86_64|amd64) printf 'cu124\n'; return 0 ;;
      *)
        warn "NVIDIA GPU detected but the cu124 wheel index ships no $uname_m build — falling back to cpu wheels."
        printf 'cpu\n'
        return 0
        ;;
    esac
  fi

  printf 'cpu\n'
}

# flavor_index_url <flavor>: prints the matching wheel index URL, or nothing
# for "source".
flavor_index_url() {
  local flavor="$1"
  case "$flavor" in
    metal) printf 'https://abetlen.github.io/llama-cpp-python/whl/metal/\n' ;;
    cu124) printf 'https://abetlen.github.io/llama-cpp-python/whl/cu124/\n' ;;
    cpu)   printf 'https://abetlen.github.io/llama-cpp-python/whl/cpu/\n' ;;
    source) : ;;
  esac
}

# is_checkout: 0 if this script sits in scripts/ of a rentmygpu source tree.
is_checkout() {
  [ -n "$REPO_ROOT" ] && [ -f "$REPO_ROOT/pyproject.toml" ] && [ -d "$REPO_ROOT/rentmygpu" ]
}

# wheel_is_url <wheel>: 0 for a URL (anything with a scheme), 1 for a path.
wheel_is_url() {
  case "$1" in
    *://*) return 0 ;;
    *) return 1 ;;
  esac
}

# wheel_requirement <wheel> <extras> [sha256]: prints the pip requirement for
# a wheel given by path or URL. A URL becomes a PEP 508 direct reference; an
# expected hash rides along as a `#sha256=` fragment, which pip verifies after
# the download. Only https:// (and file://) URLs are accepted -- the wheel is
# code that will run with the user's identity key in reach.
wheel_requirement() {
  local wheel="$1" extras="$2" sha="${3:-}"
  if wheel_is_url "$wheel"; then
    case "$wheel" in
      https://*|file://*) : ;;
      *) die "YAARBAL_WHEEL (RMG_WHEEL) must be a local path or an https:// URL (got: $wheel)" ;;
    esac
    if [ -n "$sha" ]; then
      case "$wheel" in
        *'#sha256='*) : ;;
        *) wheel="$wheel#sha256=$sha" ;;
      esac
    fi
    printf 'yaarbal[%s] @ %s\n' "$extras" "$wheel"
  else
    [ -f "$wheel" ] || die "YAARBAL_WHEEL (RMG_WHEEL) does not name an existing file: $wheel"
    case "$wheel" in
      *.whl) : ;;
      *) die "YAARBAL_WHEEL (RMG_WHEEL) must be a .whl file (got: $wheel)" ;;
    esac
    printf '%s[%s]\n' "$wheel" "$extras"
  fi
}

# verify_wheel_sha256 <python> <wheel-path> <expected-hex>: dies on mismatch.
verify_wheel_sha256() {
  local python_bin="$1" wheel="$2" expected="$3" actual
  actual="$("$python_bin" -c 'import hashlib,sys
h=hashlib.sha256()
with open(sys.argv[1],"rb") as f:
    for b in iter(lambda: f.read(1<<20), b""):
        h.update(b)
print(h.hexdigest())' "$wheel")"
  expected="$(printf '%s' "$expected" | tr 'A-F' 'a-f')"
  [ "$actual" = "$expected" ] \
    || die "wheel SHA-256 mismatch for $wheel: expected $expected, got $actual -- do not install this file; download the wheel again from https://docs.yaarbal.app/dl/ and compare against its SHA256SUMS"
}

# no_source_message: what to do when there is neither a wheel nor a checkout.
no_source_message() {
  cat <<'EOF'
no yaarbal wheel was named and this script is not inside a source checkout.

Set YAARBAL_WHEEL (or pass --wheel) to the yaarbal wheel you were given, as a
local path or an https:// URL, for example:

  YAARBAL_WHEEL=./yaarbal-0.1.0-py3-none-any.whl bash install.sh

(RMG_WHEEL, the variable's older name, still works.)

There is no built-in download location. The current wheel, this installer and
their SHA256SUMS are published at https://docs.yaarbal.app/dl/ .
EOF
}

# run <cmd...>: executes the command, or under DRY_RUN=1 prints it
# [dry-run]-prefixed and executes nothing. Every mutating command in main()
# goes through this.
run() {
  if [ "${DRY_RUN:-0}" = 1 ]; then
    printf '[dry-run] '
    printf '%q ' "$@"
    printf '\n'
    return 0
  fi
  "$@"
}

usage() {
  cat <<EOF
Usage: install.sh [options]

Options:
  --wheel <path|https-url>            install this yaarbal wheel (no source checkout needed)
  --flavor <metal|cu124|cpu|source>   force the llama-cpp-python wheel flavor
  --no-seeder                         skip the [seeder] extra (tracker only)
  --extras "a,b"                      additional pip extras to install
  --dry-run                           print resolved commands, execute nothing
  -h, --help                          show this help and exit

Env overrides: YAARBAL_WHEEL, YAARBAL_WHEEL_SHA256 (older names RMG_WHEEL,
RMG_WHEEL_SHA256 still work), RENTMYGPU_LLAMA_FLAVOR, RMG_VENV_DIR, RMG_PYTHON

With no wheel named, the script installs the source checkout it sits in
(team members); outside a checkout it stops and asks for YAARBAL_WHEEL.
EOF
}

main() {
  local no_seeder=0
  local extra_extras=""
  # quick-261007-ybl: YAARBAL_WHEEL is the documented name; RMG_WHEEL (the
  # name before the product rename) is still read when it is unset.
  local wheel="${YAARBAL_WHEEL:-${RMG_WHEEL:-}}"
  local wheel_sha="${YAARBAL_WHEEL_SHA256:-${RMG_WHEEL_SHA256:-}}"
  DRY_RUN=0

  while [ $# -gt 0 ]; do
    case "$1" in
      --flavor)
        [ $# -ge 2 ] || die "--flavor requires an argument"
        RENTMYGPU_LLAMA_FLAVOR="$2"
        shift 2
        ;;
      --wheel)
        [ $# -ge 2 ] || die "--wheel requires an argument"
        wheel="$2"
        shift 2
        ;;
      --no-seeder)
        no_seeder=1
        shift
        ;;
      --dry-run)
        DRY_RUN=1
        shift
        ;;
      --extras)
        [ $# -ge 2 ] || die "--extras requires an argument"
        extra_extras="$2"
        shift 2
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "unknown argument: $1 (see --help)"
        ;;
    esac
  done

  # 0. Install source: a named wheel wins; else the checkout around this
  # script; else stop before touching anything.
  local source_mode
  if [ -n "$wheel" ]; then
    source_mode=wheel
  elif is_checkout; then
    source_mode=checkout
  else
    die "$(no_source_message)"
  fi

  # 1. Locate + verify the bootstrap Python.
  local python_bin="${RMG_PYTHON:-python3}"
  command -v "$python_bin" >/dev/null 2>&1 || die "python interpreter not found on PATH: $python_bin"
  local found_version
  found_version="$("$python_bin" -c 'import sys; print(sys.version.split()[0])')"
  "$python_bin" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' \
    || die "Yaarbal requires Python >= 3.11; found $found_version at $python_bin"
  info "using $python_bin ($found_version)"

  # 2. Venv bootstrap.
  local default_venv="$REPO_ROOT/.venv"
  [ "$source_mode" = wheel ] && default_venv="$HOME/.rentmygpu-venv"
  local venv_dir="${RMG_VENV_DIR:-$default_venv}"
  local venv_py="$venv_dir/bin/python"
  if [ -x "$venv_py" ]; then
    info "reusing existing venv at $venv_dir"
  else
    step "creating venv at $venv_dir"
    # Always --without-pip: on a Debian/Ubuntu python3-venv without ensurepip,
    # a bare `-m venv` fails and leaves a half-built directory.
    run "$python_bin" -m venv --without-pip "$venv_dir"
  fi

  # 3. Ensure pip is present in the venv. Guarded with `|| true` since the
  # venv may not exist yet under --dry-run.
  local have_pip=0
  "$venv_py" -m pip --version >/dev/null 2>&1 && have_pip=1 || true
  if [ "$have_pip" != 1 ]; then
    step "bootstrapping pip"
    if ! run "$venv_py" -m ensurepip --upgrade; then
      run curl -sS -o "$venv_dir/get-pip.py" https://bootstrap.pypa.io/get-pip.py
      run "$venv_py" "$venv_dir/get-pip.py"
      run rm -f "$venv_dir/get-pip.py"
    fi
  fi
  run "$venv_py" -m pip install --upgrade pip

  # 4. Build the extras list + resolve the llama-cpp-python flavor.
  local extras="tracker"
  local include_seeder=1
  [ "$no_seeder" = 1 ] && include_seeder=0
  [ "$include_seeder" = 1 ] && extras="$extras,seeder"
  if [ -n "$extra_extras" ]; then
    extras="$extras,$extra_extras"
  fi

  local flavor="" index_url=""
  if [ "$include_seeder" = 1 ]; then
    flavor="$(select_flavor)"
    info "llama-cpp-python flavor: $flavor"
    index_url="$(flavor_index_url "$flavor")"
    if [ "$flavor" = "source" ]; then
      warn "installing llama-cpp-python from source — this requires a C/C++ toolchain (cmake) and may take many minutes."
    fi
  fi

  local -a pip_install_argv
  if [ "$source_mode" = wheel ]; then
    step "installing yaarbal[$extras] from wheel $wheel"
    local requirement
    requirement="$(wheel_requirement "$wheel" "$extras" "$wheel_sha")"
    if [ -n "$wheel_sha" ] && ! wheel_is_url "$wheel"; then
      verify_wheel_sha256 "$python_bin" "$wheel" "$wheel_sha"
    fi
    pip_install_argv=("$venv_py" -m pip install "$requirement")
  else
    step "installing yaarbal[$extras] from the checkout at $REPO_ROOT"
    pip_install_argv=("$venv_py" -m pip install -e "$REPO_ROOT[$extras]")
  fi
  if [ -n "$index_url" ]; then
    pip_install_argv+=(--extra-index-url "$index_url")
  fi
  run "${pip_install_argv[@]}"

  # 5. WSL cu124 runtime libraries.
  if [ "$include_seeder" = 1 ] && [ "$flavor" = "cu124" ] && is_wsl; then
    step "installing WSL cu124 runtime libraries"
    run "$venv_py" -m pip install nvidia-cuda-runtime-cu12 nvidia-cublas-cu12
    warn "WSL passes through only the NVIDIA driver — the cu124 wheel links libcudart.so.12/libcublas.so.12 from the packages just installed. Those libraries must also be PRELOADED at runtime; see the CLI quickstart docs for the preload command (out of scope here)."
  fi

  # 5b. CPU-backend ISA probe (debug cu124-wheel-sigill-cpu-attn, spike 063).
  # Prebuilt llama-cpp-python CUDA wheels are compiled with -march=native of
  # whichever GitHub runner built them (upstream never passes GGML_NATIVE=off
  # on that workflow), so a release can ship a CPU backend needing AVX512-FP16
  # / AMX -- 0.3.35 cu124 does. Full GPU offload still works; any CPU-side
  # compute SIGILLs. The probe runs the exact kernels in a throwaway child
  # (no model, no GPU, no CUDA preload needed). Advisory: never fails install.
  if [ "$include_seeder" = 1 ]; then
    step "probing the llama-cpp-python CPU backend against this CPU"
    local probe_rc=0
    run "$venv_py" -m rentmygpu.seeder.cpu_backend_guard || probe_rc=$?
    if [ "$probe_rc" = 1 ]; then
      warn "this llama-cpp-python build's CPU backend uses instructions this CPU lacks."
      warn "GPU-offloaded inference works; CPU-only or partial-offload loads are refused by the seeder (exit 4) instead of crashing with SIGILL."
      if [ "$flavor" = "cu124" ]; then
        warn "to get a CPU backend built for THIS machine: CMAKE_ARGS='-DGGML_CUDA=on' scripts/install.sh --flavor source  (needs cmake + a C/C++ toolchain + the CUDA toolkit)"
      else
        warn "to get a CPU backend built for THIS machine: scripts/install.sh --flavor source  (needs cmake + a C/C++ toolchain)"
      fi
      warn "or install a different llama-cpp-python release from the same index, then re-check: $venv_py -m rentmygpu.seeder.cpu_backend_guard"
    elif [ "$probe_rc" != 0 ]; then
      warn "could not probe the CPU backend (exit $probe_rc); check later with: $venv_py -m rentmygpu.seeder.cpu_backend_guard"
    fi
  fi

  # 6. Next steps.
  info "done."
  echo
  echo "Next steps:"
  echo "  source $venv_dir/bin/activate"
  echo "  yaarbal --help"
  echo "  yaarbal models list"
  echo "  yaarbal models pull <id>"
  echo "  yaarbal seeder --model <id> --tracker-url https://rmg-tracker-oracle.yaarbal.app"
  if [ "$include_seeder" = 0 ]; then
    echo
    echo "  (ran with --no-seeder — re-run without that flag to add the [seeder] extra)"
  fi
}

if [[ -z "${BASH_SOURCE[0]:-}" || "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
