#!/usr/bin/env bash
# Purpose: check an Apple silicon Mac against everything the rest of the course assumes:
#          a native arm64 shell, the macOS version, the chip and its GPU, the wired-memory
#          limit and what MLX reports about it, the Xcode command-line tools and Homebrew,
#          the toolchain, the Part 1 environment and notebook, the hf CLI and the model
#          storage. It changes nothing.
# Platform: mac (Apple silicon; macOS 26 is the course target, 15 is the floor for the
#          wired-limit steps)
# Minimum memory: 8 GB
# Assumes: ~/llm-course from Part 1 with MLX in its .venv (override with COURSE=) and
#          ~/models from Part 4 (override with MODELS=)
#
# Usage: bash prepare-mac.sh [--help]
# Output: "ok" (in place), "MISSING" (a later part fails without it; the text names the
#         task that fixes it) or "NOTE" (read it, not blocking), then
#         "summary: N MISSING, M NOTE". Exit status 1 when anything is MISSING.
set -euo pipefail

MODELS="${MODELS:-$HOME/models}"
COURSE="${COURSE:-$HOME/llm-course}"
UV_PIN="0.12.11"
HF_MIN="1.30.0"
INSTALL_HINT="brew install"

for arg in "$@"; do
  case "$arg" in
    -h|--help) sed -n '2,16p' "$0"; exit 0 ;;
    *) echo "prepare-mac: unknown argument '$arg' (the script only reports; try --help)" >&2; exit 2 ;;
  esac
done

MISSING=0
NOTES=0
have()    { command -v "$1" >/dev/null 2>&1; }
section() { printf '\n== %s\n' "$1"; }
ok()      { printf '   ok       %-20s %s\n' "$1" "$2"; }
missing() { MISSING=$((MISSING + 1)); printf '   MISSING  %-20s %s\n' "$1" "$2"; }
note()    { NOTES=$((NOTES + 1)); printf '   NOTE     %-20s %s\n' "$1" "$2"; }
# version_ge A B: true when version A >= version B
version_ge() {
  awk -v a="$1" -v b="$2" 'BEGIN {
    na = split(a, x, /[.-]/); nb = split(b, y, /[.-]/); n = (na > nb) ? na : nb
    for (i = 1; i <= n; i++) { xi = (i <= na) ? x[i] + 0 : 0; yi = (i <= nb) ? y[i] + 0 : 0
      if (xi > yi) exit 0; if (xi < yi) exit 1 }
    exit 0 }'
}
first_match() { awk -v pat="$1" '$0 ~ pat && !done { print; done = 1 }'; }

printf 'prepare-mac.sh on %s, %s\n' "$(hostname)" "$(date '+%Y-%m-%d %H:%M')"

section "1. Machine and operating system"
arch="$(uname -m)"
if [ "$arch" = "arm64" ]; then
  ok "architecture" "$arch"
else
  missing "architecture" "$arch: this shell runs under Rosetta; open a native terminal"
fi
macos="$(sw_vers -productVersion)"
if version_ge "$macos" "26"; then
  ok "macOS" "$macos (build $(sw_vers -buildVersion))"
elif version_ge "$macos" "15"; then
  note "macOS" "$macos: works, but the course is written against macOS 26 (task 2)"
else
  missing "macOS" "$macos: MLX's wired-limit function needs 15.0 or later (task 2)"
fi
ok "chip" "$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo unknown)"
mem_bytes="$(sysctl -n hw.memsize)"
ok "memory (hw.memsize)" "$(awk -v b="$mem_bytes" 'BEGIN {printf "%d bytes = %.0f GiB", b, b / 1073741824}')"

section "2. GPU and the wired-memory limit"
display="$(system_profiler SPDisplaysDataType 2>/dev/null || true)"
ok "GPU" "$(printf '%s\n' "$display" | awk -F': ' '/Chipset Model/ && !done {print $2; done = 1}'), $(printf '%s\n' "$display" | awk -F': ' '/Total Number of Cores/ && !done {print $2 " cores"; done = 1}')"
wired="$(sysctl -n iogpu.wired_limit_mb 2>/dev/null || echo unavailable)"
if [ "$wired" = "unavailable" ]; then
  missing "iogpu.wired_limit_mb" "the sysctl does not exist on this macOS: task 2"
elif [ "$wired" = "0" ]; then
  note "iogpu.wired_limit_mb" "0, the system default: task 4 sets it"
else
  ok "iogpu.wired_limit_mb" "$(awk -v m="$wired" -v b="$mem_bytes" 'BEGIN {printf "%d MB = %.1f GiB = %.0f%% of memory", m, m / 1024, 100 * m * 1048576 / b}')"
fi
if [ -x "$COURSE/.venv/bin/python" ]; then
  mlx_line="$("$COURSE/.venv/bin/python" -c '
import mlx.core as mx
info = mx.device_info()
ws = info.get("max_recommended_working_set_size")
total = info.get("memory_size")
print(f"max_recommended_working_set_size {ws} bytes ({ws / 2**30:.1f} GiB) of memory_size {total} bytes" if ws and total else "")
' 2>/dev/null || true)"
  if [ -n "$mlx_line" ]; then ok "MLX device_info" "$mlx_line"; else note "MLX device_info" "mlx not importable from $COURSE/.venv: Part 1's lab installs it"; fi
fi

section "3. Command-line tools and Homebrew"
if clt="$(xcode-select -p 2>/dev/null)"; then
  ok "developer tools" "$clt"
else
  missing "developer tools" "not installed: task 3 (xcode-select --install)"
fi
if have brew; then
  ok "homebrew" "$(brew --version | first_match Homebrew) at $(brew --prefix)"
else
  missing "homebrew" "not found; Part 6 installs cmake with it: task 3"
fi

section "4. Containers"
ok "containers" "not used on this track: the course runs engines natively on macOS (task 6)"

section "5. Toolchain and the Part 1 course directory"
for tool in git python3 curl; do
  if have "$tool"; then
    ok "$tool" "$("$tool" --version 2>&1 | awk 'NR == 1 {print $1, $2, $3}')"
  else
    missing "$tool" "install it with: $INSTALL_HINT $tool"
  fi
done
if have uv; then
  uv_version="$(uv --version | awk '{print $2}')"
  if [ "$uv_version" = "$UV_PIN" ]; then ok "uv" "$uv_version (the course pin)"; else note "uv" "$uv_version; the course is pinned to $UV_PIN (task 5)"; fi
else
  missing "uv" "not found on PATH: task 5"
fi
if [ -x "$COURSE/.venv/bin/python" ]; then ok "course environment" "$COURSE/.venv"; else missing "course environment" "$COURSE/.venv not found: Part 1's lab, tasks 2 and 3"; fi
if [ -f "$COURSE/labbook.md" ]; then ok "lab notebook" "$COURSE/labbook.md"; else missing "lab notebook" "$COURSE/labbook.md not found: Part 1's lab, task 1"; fi

section "6. Hugging Face CLI, login and model storage"
hf_bin=""
if have hf; then hf_bin="$(command -v hf)"; elif [ -x "$COURSE/.venv/bin/hf" ]; then hf_bin="$COURSE/.venv/bin/hf"; fi
if [ -n "$hf_bin" ]; then
  hf_version="$("$hf_bin" version --format json 2>/dev/null | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p')"
  if [ -n "$hf_version" ] && version_ge "$hf_version" "$HF_MIN"; then
    ok "hf" "$hf_version ($hf_bin)"
  else
    missing "hf" "version '${hf_version:-unknown}' is older than $HF_MIN: task 7"
  fi
else
  missing "hf" "not on PATH and not in $COURSE/.venv: task 7"
fi
if [ -d "$MODELS" ]; then ok "model library" "$MODELS"; else missing "model library" "$MODELS not found: Part 4's lab, task 7, or task 7 here"; fi
if [ -f "$MODELS/README.md" ]; then ok "library README" "$MODELS/README.md"; else note "library README" "missing: Part 4's lab, task 7, writes it"; fi
if [ -d "$MODELS/hf" ]; then ok "HF_HOME directory" "$MODELS/hf"; else missing "HF_HOME directory" "$MODELS/hf not found: task 7"; fi
if [ "${HF_HOME:-}" = "$MODELS/hf" ]; then
  ok "HF_HOME" "$HF_HOME"
else
  missing "HF_HOME" "is '${HF_HOME:-unset}' in this shell, expected $MODELS/hf: task 7"
fi
if [ -f "$MODELS/hf/token" ]; then
  ok "stored login" "$MODELS/hf/token"
elif [ -f "$HOME/.cache/huggingface/token" ]; then
  note "stored login" "only under ~/.cache/huggingface, which hf stops reading once HF_HOME moves: task 7"
else
  note "stored login" "none; needed only for gated models (task 7)"
fi
df_target="$MODELS"; [ -d "$df_target" ] || df_target="$HOME"
ok "free disk" "$(df -Pk "$df_target" | awk 'NR == 2 {printf "%d GiB", $4 / 1048576}') on the filesystem holding $df_target"

printf '\nsummary: %d MISSING, %d NOTE\n' "$MISSING" "$NOTES"
[ "$MISSING" -eq 0 ]
