CAVEMAN_URL  := https://raw.githubusercontent.com/juliusbrussee/caveman/main/skills/caveman/SKILL.md
SKILLS_DIR   := .claude/skills
SOURCES      := $(shell find src include tests benchmarks -type f \( -name '*.cpp' -o -name '*.hpp' -o -name '*.h' -o -name '*.cu' -o -name '*.cuh' \) 2>/dev/null)
# src/python is excluded: the nanobind NB_MODULE macro and capsule-deleter
# FFI patterns trip cppcoreguidelines checks that don't apply to boundary code.
LINT_SOURCES := $(shell find src -type f -name '*.cpp' -not -path 'src/python/*' 2>/dev/null)
TOY_SENTINEL := tests/data/.toy-fetched
AMAZON_SENTINEL := tests/data/.amazon-fetched

# Toolchain root: homebrew LLVM on macOS, apt.llvm.org layout on Linux.
# clang-tidy needs the macOS SDK sysroot spelled out; on Linux the driver
# finds its own sysroot. CI pins LLVM 21; prefer the matching versioned keg
# when installed — homebrew's mainline llvm drifted to 22, whose stricter
# pedantic set (-Wc2y-extensions on Catch2's __COUNTER__) and reformats
# break the repo. The llvm@21 bottle's libc++ also mislabels float
# from_chars availability on macOS, hence the define.
ifeq ($(shell uname -s),Darwin)
ifneq ($(wildcard /opt/homebrew/opt/llvm@21/bin),)
LLVM_BIN       ?= /opt/homebrew/opt/llvm@21/bin
export CXXFLAGS := $(CXXFLAGS) -D_LIBCPP_DISABLE_AVAILABILITY
else
LLVM_BIN       ?= /opt/homebrew/opt/llvm/bin
endif
SDK_PATH       := $(shell xcrun --show-sdk-path)
LINT_EXTRA_ARGS := -extra-arg=-isysroot -extra-arg=$(SDK_PATH)
else
LLVM_BIN       ?= /usr/lib/llvm-21/bin
LINT_EXTRA_ARGS :=
endif

all: build  ## Default target; same as build.

PYTHON ?= .venv/bin/python

build/build.ninja:
	@cmake -DCMAKE_BUILD_TYPE=Release \
	-DCMAKE_CXX_COMPILER=$(LLVM_BIN)/clang++ \
	-G Ninja -S . -B build

configure: build/build.ninja  ## Run CMake configure only, no compile (build/).

build: build/build.ninja  ## Configure and compile the CLI, library, and tests (build/).
	@cmake --build build -j

build-cuda/build.ninja:
	@cmake -DCMAKE_BUILD_TYPE=Release \
	-DCMAKE_CXX_COMPILER=$(LLVM_BIN)/clang++ \
	-DBONSAI_CUDA=ON \
	-G Ninja -S . -B build-cuda

build-cuda: build-cuda/build.ninja  ## Configure and compile with the CUDA backend (build-cuda/).
	@cmake --build build-cuda -j

test-cuda: build-cuda $(TOY_SENTINEL)  ## Build the CUDA variant and run ctest against it.
	@ctest --test-dir build-cuda

build-asan/build.ninja:
	@cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \
	-DCMAKE_CXX_COMPILER=$(LLVM_BIN)/clang++ \
	-DBONSAI_SANITIZE=ON \
	-G Ninja -S . -B build-asan

build-asan: build-asan/build.ninja  ## Build the ASan + UBSan variant (build-asan/).
	@cmake --build build-asan -j

test-asan: build-asan $(TOY_SENTINEL)  ## Build the ASan + UBSan variant and run ctest (CI-only on macOS).
	@ctest --test-dir build-asan

clean:  ## Remove build/, build-cuda/, and build-asan/.
	@rm -rf build build-cuda build-asan

rebuild: clean build  ## Clean, then build.

format:  ## clang-format in place over src/, include/, tests/, benchmarks/.
	@$(LLVM_BIN)/clang-format -i $(SOURCES)

format-check:  ## Check formatting with clang-format --dry-run --Werror (CI gate).
	@$(LLVM_BIN)/clang-format --dry-run --Werror $(SOURCES)

# Ruff, pinned the way LLVM is: linter drift breaks CI, not the code.
RUFF_VERSION := 0.15.21

lint-python:  ## Run ruff over python/ and scripts/ (pinned via uvx).
	@uvx ruff@$(RUFF_VERSION) check python scripts

# run-clang-tidy exits non-zero when findings exist; a non-zero exit with
# no findings means the tool itself failed and must not pass silently.
lint: build/build.ninja  ## Run clang-tidy over src/, header-filtered to bonsai.
	@log=$$($(LLVM_BIN)/run-clang-tidy -quiet -use-color=0 \
	    -clang-tidy-binary $(LLVM_BIN)/clang-tidy \
	    $(LINT_EXTRA_ARGS) \
	    -header-filter='include/bonsai/.*' -p build $(LINT_SOURCES) 2>&1); \
	status=$$?; \
	out=$$(echo "$$log" | grep -E '(warning|error):' \
	    | grep -v -E '(c\+\+/v1|system-headers|too many)'); \
	if [ -n "$$out" ]; then echo "$$out"; exit 1; fi; \
	if [ $$status -ne 0 ]; then echo "$$log" | tail -20; \
	    echo "lint: run-clang-tidy failed"; exit 1; fi; \
	echo "lint: no findings."

run: build  ## Build, then run ./build/src/bonsai with ARGS.
	@./build/src/bonsai $(ARGS)

# Re-extract the parameters reference input from the built CLI, then rerender
# docs/use/parameters.md. `bonsai params` dumps the default Config as TOML
# straight from the structs; the extract step needs Python 3.11+ (tomllib).
# CI runs only render_params.py --check against the committed JSON, so it
# never rebuilds the CLI to verify the page.
params-json: build  ## Re-extract docs/use/parameters.src.json from the built CLI and rerender the page.
	@./build/src/bonsai params | python3 scripts/render_params.py --extract
	@python3 scripts/render_params.py

test: build $(TOY_SENTINEL)  ## Build, fetch the pinned test datasets, run ctest.
	@ctest --test-dir build

perf-benchmark: build  ## Build and run the Catch2 perf microbenchmarks (ARGS forwarded).
	@./build/benchmarks/bonsai_bench $(ARGS)

# Python extension. Needs a python with nanobind + numpy installed
# (override with PYTHON=/path/to/python).
python: build/build.ninja  ## Build the _bonsai Python extension into build/python/.
	@cmake -B build -DBONSAI_PYTHON=ON -DBONSAI_OPENMP_STATIC=ON \
	    -DBONSAI_OPENMP_DYNAMIC_FALLBACK_OK=ON \
	    -DPython_EXECUTABLE=$(abspath $(PYTHON)) \
	    | grep -iE "openmp|error" || true
	@cmake --build build --target _bonsai -j
	@echo "module at build/python/bonsai — use PYTHONPATH=build/python"

python-test: python $(TOY_SENTINEL) $(AMAZON_SENTINEL)  ## Build the extension and run the Python test suites.
	@PYTHONPATH=build/python $(PYTHON) python/tests/test_bindings.py
	@PYTHONPATH=build/python $(PYTHON) python/tests/test_encoding.py
	@PYTHONPATH=build/python $(PYTHON) python/tests/test_doc_snippets.py
	@PYTHONPATH=build/python $(PYTHON) python/tests/test_bench.py
	@PYTHONPATH=build/python $(PYTHON) python/tests/test_xgb_compat.py

# CUDA-enabled extension in the CUDA tree; cuda_* growers can train.
python-cuda: build-cuda/build.ninja  ## Build the CUDA-enabled Python extension into build-cuda/python/.
	@cmake -B build-cuda -DBONSAI_PYTHON=ON -DBONSAI_OPENMP_STATIC=ON \
	    -DBONSAI_OPENMP_DYNAMIC_FALLBACK_OK=ON \
	    -DPython_EXECUTABLE=$(abspath $(PYTHON)) >/dev/null
	@cmake --build build-cuda --target _bonsai -j
	@echo "module at build-cuda/python/bonsai — use PYTHONPATH=build-cuda/python"

fit-benchmark: build $(TOY_SENTINEL)  ## Compare bonsai against reference libraries on California housing.
	@uv run scripts/compare.py --config configs/california_housing.toml $(ARGS)

# GPU perf loop (benchmarks/README.md): MSD ladder vs xgboost-GPU,
# appends benchmarks/results/gpu_msd.jsonl. Needs the MSD dataset
# (scripts/fetch_year_msd.py) and a CUDA-capable host.
bench-gpu: build-cuda  ## Run the MSD GPU ladder vs xgboost-GPU with profile breakdowns.
	@BONSAI_CUDA_PROFILE=1 BONSAI_GROW_PROFILE=1 uv run scripts/bench_gpu.py $(ARGS)

# Scaling suite (benchmarks/README.md): synthetic rows/cols/bins/threads
# sweep vs xgboost/lightgbm/catboost, appends benchmarks/results/scaling.jsonl.
# Uses the CUDA module tree when present, else the CPU one.
bench-scaling:  ## Run the synthetic rows/cols/bins/threads scaling sweep.
	@PYTHONPATH=$(if $(wildcard build-cuda/python),build-cuda/python,build/python) \
	    uv run scripts/bench_scaling.py $(ARGS)

$(TOY_SENTINEL):
	@uv run scripts/fetch_toy.py
	@touch $@

# test_encoding.py's amazon quality pin needs the stage-1 CSVs (gitignored).
$(AMAZON_SENTINEL):
	@uv run scripts/fetch_amazon.py
	@touch $@

help:  ## List the common make targets.
	@echo "Targets:"
	@echo "  make build              Configure + compile."
	@echo "  make rebuild            Clean + build."
	@echo "  make test               Build + run ctest."
	@echo "  make build-cuda         Configure + compile with the CUDA backend (build-cuda/)."
	@echo "  make test-cuda          Build CUDA variant + run ctest against it."
	@echo "  make test-asan          Build ASan+UBSan variant (build-asan/) + run ctest."
	@echo "  make run ARGS=...       Build + run ./build/src/bonsai with ARGS."
	@echo "  make perf-benchmark     Build + run Catch2 perf microbenchmarks (ARGS forwarded)."
	@echo "  make fit-benchmark      Build + compare bonsai vs lightgbm/catboost on cal housing."
	@echo "  make bench-gpu          MSD ladder vs xgboost-GPU with profile breakdowns (GPU perf loop)."
	@echo "  make bench-scaling      Synthetic rows/cols/bins/threads scaling sweep vs reference libraries."
	@echo "  make python             Build the Python extension (PYTHON=... to pick the interpreter)."
	@echo "  make python-cuda        Build the CUDA-enabled extension into build-cuda/python."
	@echo "  make python-test        Build + run python/tests/test_bindings.py."
	@echo "  make format             clang-format src/ + include/ + tests/ + benchmarks/ in place."
	@echo "  make format-check       clang-format --dry-run --Werror (CI gate)."
	@echo "  make lint               clang-tidy on src/ (header-filtered to bonsai)."
	@echo "  make lint-python        ruff on python/ + scripts/ (pinned via uvx)."
	@echo "  make params-json        Re-extract the parameters reference from the built CLI."
	@echo "  make skills             Install project-local Claude Code skills (currently: caveman)."
	@echo "  make skills-clean       Remove installed project-local skills."

install-hooks:  ## Point core.hooksPath at the versioned hooks (commit-msg format gate).
	@chmod +x scripts/git-hooks/*
	@git config core.hooksPath scripts/git-hooks
	@echo "hooks installed: core.hooksPath = scripts/git-hooks"

skills: $(SKILLS_DIR)/caveman/SKILL.md  ## Install project-local Claude Code skills (currently caveman).

$(SKILLS_DIR)/caveman/SKILL.md:
	@mkdir -p $(@D)
	@echo "Fetching caveman skill -> $@"
	@curl -fsSL $(CAVEMAN_URL) -o $@
	@echo "Done. Restart Claude Code (or trigger a skill rediscovery) to pick it up."

skills-clean:  ## Remove installed project-local skills.
	rm -rf $(SKILLS_DIR)

.PHONY: configure build build-cuda build-asan clean rebuild format format-check lint lint-python all run params-json test test-cuda test-asan perf-benchmark fit-benchmark bench-gpu bench-scaling python python-cuda python-test install-hooks skills skills-clean help
