.DEFAULT_GOAL := help
SOURCE_DIR = ./src
PY_VERSIONS = 3.10 3.11 3.12 3.13 3.14 3.14t 3.15 3.15t
export UV_MANAGED_PYTHON ?= 1

##@ Build
.PHONY: build
build: ## Build
	uv build

.PHONY: docs
docs: ## build docs
	rm -rf .doctrees site
	uv run --group docs sphinx-build -d .doctrees -b html docs site --fail-on-warning

.PHONY: docs-serve
docs-serve: ## Open built docs in browser
	uv run python -m webbrowser site/index.html

.PHONY: wheels
wheels: ## build wheels (musllinux skipped; unset CIBW_SKIP or run cibuildwheel directly to include it)
	CIBW_SKIP='*-musllinux_*' uvx --from cibuildwheel==4.2.1 cibuildwheel

##@ Quality
.PHONY: test-cov
test-cov: ## Run tests with coverage
	uv run --reinstall-package structtype pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=structtype

.PHONY: test-cov-c
test-cov-c: ## Run tests with Python + C coverage (lcov report in htmlcov-c/)
	rm -rf build coverage-c.info coverage-c.info.* htmlcov-c
	STRUCTTYPE_COVERAGE=1 uv run --with setuptools python setup.py build_ext --inplace --force
	./.venv/bin/python -m pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=structtype \
	  --deselect tests/test_json.py::TestEncoderMisc::test_encode_infinite_recursive_object_errors
	lcov --capture --directory build --output-file coverage-c.info --rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1
	lcov --extract coverage-c.info "*/src/structtype/*" --output-file coverage-c.info --rc branch_coverage=1
	lcov --summary coverage-c.info --rc branch_coverage=1
	genhtml coverage-c.info --output-directory htmlcov-c --branch-coverage >/dev/null

# C coverage is version-dependent: `_core.c` has many `#if PY30x_PLUS` and
# `#ifdef Py_GIL_DISABLED` regions, so a single build only covers part of the
# code. This target builds an instrumented extension for every supported
# Python, runs the suite under each, and merges the lcov data.
.PHONY: test-cov-c-all
test-cov-c-all: ## Run C coverage across all supported Pythons, merged (htmlcov-c/)
	rm -rf build coverage-c.info coverage-c.info.* htmlcov-c
	@set -e; \
	for py in $(PY_VERSIONS); do \
	  tag=$$(echo "$$py" | tr -d '.'); \
	  venv=".venv-cov-$$tag"; \
	  echo "=== C coverage: $$py ($$venv) ==="; \
	  rm -rf build; \
	  env -u VIRTUAL_ENV UV_PROJECT_ENVIRONMENT="$$venv" STRUCTTYPE_COVERAGE=1 \
	    uv run -p "$$py" --with setuptools python setup.py build_ext --inplace --force; \
	  "$$venv/bin/python" -m pytest -q \
	    --deselect tests/test_json.py::TestEncoderMisc::test_encode_infinite_recursive_object_errors; \
	  lcov --capture --directory build --output-file "coverage-c.info.$$tag" \
	    --rc branch_coverage=1 --rc geninfo_unexecuted_blocks=1; \
	  lcov --extract "coverage-c.info.$$tag" "*/src/structtype/*" \
	    --output-file "coverage-c.info.$$tag" --rc branch_coverage=1; \
	done; \
	lcov $$(for f in coverage-c.info.*; do printf ' -a %s' "$$f"; done) \
	  --output-file coverage-c.info --rc branch_coverage=1; \
	lcov --summary coverage-c.info --rc branch_coverage=1; \
	genhtml coverage-c.info --output-directory htmlcov-c --branch-coverage >/dev/null; \
	env -u VIRTUAL_ENV uv run --reinstall-package structtype python -c "import structtype" >/dev/null; \
	echo "Merged C coverage written to htmlcov-c/. Re-run 'make test-all' to restore the other optimized builds."

.PHONY: test
test: ## Run tests in current Python
	uv run --reinstall pytest

.PHONY: test-doc
test-doc: ## Run doctests
	uv run pytest --doctest-modules --pyargs structtype

TEST_ALL_TARGETS = $(PY_VERSIONS:%=test-all-%)

.PHONY: test-all $(TEST_ALL_TARGETS)
test-all: $(TEST_ALL_TARGETS) ## Run tests in all supported Python versions (parallel: make -j$(nproc) test-all)

define test-all-rule
.PHONY: test-all-$(1)
test-all-$(1): ## Run tests for Python $(1)
	uv run --isolated --reinstall-package structtype -p $(1) pytest
endef
$(foreach py_v,$(PY_VERSIONS),$(eval $(call test-all-rule,$(py_v))))

UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
ASAN_RUNTIME := $(shell clang --print-file-name=libclang_rt.asan_osx_dynamic.dylib)
SANITIZE_PRELOAD := DYLD_INSERT_LIBRARIES=$(ASAN_RUNTIME)
else
ASAN_RUNTIME := $(shell gcc --print-file-name=libasan.so)
SANITIZE_PRELOAD := LD_PRELOAD=$(ASAN_RUNTIME)
endif

DEBUG_PY ?= 3.14+debug
DEBUG_VENV = .venv-debug

.PHONY: test-debug
test-debug: ## Build core with Py_DEBUG + ASan/UBSan + debug allocator and run all tests
	uv venv --clear --python $(DEBUG_PY) $(DEBUG_VENV)
	STRUCTTYPE_SANITIZE=1 uv pip install --python $(DEBUG_VENV) --reinstall --no-cache --group dev .
	$(SANITIZE_PRELOAD) STRUCTTYPE_ASAN_RUNTIME=$(ASAN_RUNTIME) ASAN_OPTIONS=detect_leaks=0 \
		PYTHONMALLOC=debug PYTHONFAULTHANDLER=1 PYTHONDEVMODE=1 \
		$(DEBUG_VENV)/bin/python -m pytest

.PHONY: check
check: ## Run all checks
	-uvx ty check ${SOURCE_DIR}
	uvx ruff check ${SOURCE_DIR}

.PHONY: ruff-check
ruff-check: ## Lint using ruff
	uvx ruff check ${SOURCE_DIR}

.PHONY: type-check
type-check: ## Type check with
	-uvx ty check ${SOURCE_DIR}
	uvx pyrefly check ${SOURCE_DIR}

.PHONY: typecheck-tests
typecheck-tests: ## Type check tests/typecheck fixtures (skipped when tools unavailable)
	@if uvx ty --version >/dev/null 2>&1; then uvx ty check tests/typecheck; else echo "skip: ty unavailable"; fi
	@if uvx pyrefly --version >/dev/null 2>&1; then uvx pyrefly check tests/typecheck; else echo "skip: pyrefly unavailable"; fi
	@if uvx mypy --version >/dev/null 2>&1; then uvx mypy --python-executable .venv/bin/python tests/typecheck; else echo "skip: mypy unavailable"; fi

.PHONY: format
format: ## Format files using ruff format
	uvx ruff format ${SOURCE_DIR}

##@ Benchmark
.PHONY: bench
bench: ## run benchmarks
	uv run -p 3.15 benchmarks/bench_libs.py

.PHONY: bench-validators
bench-validators: ## run Serializer/Validator benchmarks
	uv run -p 3.15 benchmarks/bench_validators.py

.PHONY: bench-codecs
bench-codecs: ## run Constraint + Serializer heavy benchmarks
	uv run -p 3.15 benchmarks/bench_codecs.py

.PHONY: bench-csv-1m
bench-csv-1m: ## run 1M-row on-disk CSV benchmark
	uv run -p 3.15 benchmarks/bench_csv_1m.py

##@ Utility
.PHONY: clean
clean: ## Delete all temporary files
	rm -rf .pytest_cache
	rm -rf **/.pytest_cache
	rm -rf .mypy_cache
	rm -rf .ruff_cache
	rm -rf __pycache__
	rm -rf **/__pycache__
	rm -rf build
	rm -rf dist
	rm -rf htmlcov-c
	rm -f .coverage
	rm -f coverage-c.info coverage-c.info.*

.PHONY: install
install: install-uv ## Install virtual environment
	uv sync --frozen

.PHONY: install-uv
install-uv: ## Install uv
	@command -v uv >/dev/null 2>&1 || curl -LsSf https://astral.sh/uv/install.sh | sh

.PHONY: update-uv
update-uv: ## Update uv
	uv self update

.PHONY: update-lock
update-lock: ## Update lockfile
	uv lock --upgrade

.PHONY: update-python
update-python: ## Reinstall managed Python versions to latest release
	for py_v in $(PY_VERSIONS); do \
		uv python install --reinstall $$py_v ; \
	done


.PHONY: help
help:  ## Display this help
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make <target>\033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
