# --- Makefile (PyScaffold Clickstart; pyproject-only, Ruff-only) -------------
# Provides:
#   - Auto versioned builds (setuptools_scm)
#   - Linting/formatting via Ruff
#   - Incremental pytest testing via STAMPs
#   - Release helpers (Git tag based)
#   - venv-based bootstrap (.venv by default; override with VENV=...)
# -----------------------------------------------------------------------------

.SILENT:
.ONESHELL:
SHELL := $(shell which bash)
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help

# ---- Python / venv ----------------------------------------------------------
VENV ?= .venv

# ---- Environment install stamp ---------------------------------------------

CONF_FILES := pyproject.toml
ENV_STAMP := $(VENV)/.installed

$(ENV_STAMP): $(CONF_FILES) | venv
	@echo "Installing project dependencies (editable)..."
	"$(PY)" -m ensurepip --upgrade
	"$(PY)" -m pip install -U pip setuptools wheel
	"$(PY)" -m pip install -e ".[dev]"
	@echo "installed" > "$(ENV_STAMP)"

# Pick a system Python: use 'python' if present, else Windows launcher 'py -3'
PYTHON_SYS := $(shell command -v python >/dev/null 2>&1 && echo python || echo py -3)

# Linux friendly too
PY = $(if $(wildcard $(VENV)/Scripts/python.exe),$(VENV)/Scripts/python.exe,$(VENV)/bin/python)

# Main code package (templated)
DIST := profgen
PKG  := profgen

CODE_DIRS  := src/$(PKG)
STAMPS_DIR := .stamps
UNIT_STAMP  := $(STAMPS_DIR)/unit.ok
INTEG_STAMP := $(STAMPS_DIR)/integration.ok
NO_CACHE  ?= 0

# Default release kind for `make release` (patch|minor|major)
KIND ?= patch

# Pytest flags
PYTEST           := $(PY) -m pytest
PYTEST_Q         := -q
PYTEST_WARN      := --disable-warnings
PYTEST_COV_BASE  := --cov=$(PKG)
PYTEST_COV_UNIT  := $(PYTEST_COV_BASE) --cov-report= --cov-append
PYTEST_COV_INTEG := $(PYTEST_COV_BASE) --cov-report= --cov-append
PYTEST_XDIST    ?= -n auto
PYTEST_TIMEOUT  ?= --timeout=60

# Test directories
UNIT_DIR   := tests/unit
INTEG_DIR  := tests/integration
SYSTEM_DIR := tests/system  # live/system tests (opt-in, uncached)

.PHONY: help venv bootstrap precommit docs docs-pdf lint format \
        test test-all test-live clean-tests \
        build upload version fetch-tags changelog changelog-md \
        release-show release release-patch release-minor release-major \
        clean run-cli check-clean profile

help:
	@echo "Common targets:"
	@echo "  make venv                - create virtual environment"
	@echo "  make bootstrap           - create $(VENV) and install .[dev]"
	@echo "  make precommit           - install pre-commit hook (requires bootstrap)"
	@echo "  make docs                - build Sphinx/MyST docs to docs/_build/html"
	@echo "  make docs-pdf            - build a single PDF to docs/_build/latex/$(PKG).pdf (needs LaTeX)"
	@echo "  make lint                - run Ruff checks"
	@echo "  make format              - auto-fix via Ruff"
	@echo "  make test                - run cached unit+integration tests (not live)"
	@echo "  make test-all            - run all non-live tests (no stamps)"
	@echo "  make test-live           - run @live tests only (no cache)"
	@echo "  make build               - build wheel+sdist"
	@echo "  make upload              - upload to PyPI (via Twine)"
	@echo "  make version             - print setuptools_scm inferred version"
	@echo "  make changelog           - show changes since last Git tag"
	@echo "  make changelog-md        - write docs/CHANGELOG.md from Git history"
	@echo "  make release-show        - show scm ver, installed ver, last Git tag"
	@echo "  make release             - run tests, show changelog and tag (KIND=patch|minor|major)"
	@echo "  make clean               - remove build artifacts"
	@echo "  make run-cli             - run via python -m package (pass CLI_ARGS=...)"
	@echo "  make profile             - render a branded profile from CV using local/ template+style-map"
	@echo "                             (CV=cv.pdf [OUT=out.docx] [OFFLINE=1])"
	@echo ""
	@echo "Tip (Windows/OneDrive permission issues):"
	@echo '  make bootstrap VENV="$$HOME/.venvs/$(PKG)"'
	@echo '  make release   VENV="$$HOME/.venvs/$(PKG)" KIND=major'

$(VENV)/pyvenv.cfg:
	$(PYTHON_SYS) -m venv $(VENV)

venv: $(VENV)/pyvenv.cfg

bootstrap: $(ENV_STAMP)
	@echo "Environment ready."

precommit: bootstrap
	"$(PY)" -m pre_commit install

# -----------------------------------------------------------------------------#
# Docs
docs: $(ENV_STAMP)
	"$(PY)" -m sphinx -b html docs docs/_build/html

# Build a single PDF of the whole documentation set via the Sphinx LaTeX
# builder + latexmk. Requires a LaTeX toolchain (xelatex + latexmk) on PATH;
# install TeX Live or MiKTeX if `make docs-pdf` reports them missing.
# Output: docs/_build/latex/$(PKG).pdf
docs-pdf: $(ENV_STAMP)
	@command -v latexmk >/dev/null 2>&1 || { \
	  echo "latexmk not found. Install a LaTeX toolchain (TeX Live or MiKTeX) to build the PDF."; exit 1; }
	rm -rf docs/_build/latex 2>/dev/null || true   # best-effort clean; tolerate Windows/OneDrive locks
	"$(PY)" -m sphinx -b latex docs docs/_build/latex
	cd docs/_build/latex && latexmk -pdfxe -interaction=nonstopmode -halt-on-error $(PKG).tex
	@echo "PDF written to docs/_build/latex/$(PKG).pdf"

# -----------------------------------------------------------------------------#
# Linting / Formatting
lint: $(ENV_STAMP)
	"$(PY)" -m ruff check .
	"$(PY)" -m ruff format --check .

format: $(ENV_STAMP)
	"$(PY)" -m ruff check --fix .
	"$(PY)" -m ruff format .

# -----------------------------------------------------------------------------#
# Incremental Testing (cache via stamps)

$(STAMPS_DIR):
	mkdir -p $(STAMPS_DIR)

UNIT_SIG    := $(STAMPS_DIR)/unit.sig
INTEG_SIG   := $(STAMPS_DIR)/integration.sig

define compute_dir_sig
{ [ -d "$(1)" ] && find $(1) -type f -not -path "*/__pycache__/*" -print0 || true; } \
| LC_ALL=C sort -z | xargs -0r sha1sum | sha1sum | awk '{print $$1}'
endef

$(UNIT_STAMP): | $(STAMPS_DIR) $(ENV_STAMP)
	@tests_sig=$$( $(call compute_dir_sig,$(UNIT_DIR)) ); \
	code_sig=$$( $(call compute_dir_sig,$(CODE_DIRS)) ); \
	conf_sig=$$( sha1sum $(CONF_FILES) 2>/dev/null | awk '{print $$1}' | sha1sum | awk '{print $$1}' ); \
	new_sig=$$( printf "%s\n%s\n%s\n" "$$tests_sig" "$$code_sig" "$$conf_sig" | sha1sum | awk '{print $$1}' ); \
	old_sig=$$(cat $(UNIT_SIG) 2>/dev/null || echo -n); \
	if [ "$(NO_CACHE)" = "1" ] || [ "$$new_sig" != "$$old_sig" ] || [ ! -f $@ ]; then \
	  echo "=== Running unit tests ==="; \
	  rm -f .coverage; \
	  set +e; \
	  "$(PY)" -m pytest -q $(UNIT_DIR) -m "not live" $(PYTEST_WARN) $(PYTEST_XDIST) $(PYTEST_TIMEOUT) $(PYTEST_COV_UNIT); \
	  status=$$?; \
	  set -e; \
	  if [ "$$status" -eq 5 ]; then \
	    echo "No unit tests collected; treating as success."; \
	  elif [ "$$status" -ne 0 ]; then \
	    exit $$status; \
	  fi; \
	  echo "$$new_sig" > $(UNIT_SIG); \
	  touch $@; \
	else \
	  echo "No changes detected; skipping unit tests."; \
	fi

$(INTEG_STAMP): | $(STAMPS_DIR) $(ENV_STAMP)
	@tests_sig=$$( $(call compute_dir_sig,$(INTEG_DIR)) ); \
	code_sig=$$( $(call compute_dir_sig,$(CODE_DIRS)) ); \
	conf_sig=$$( sha1sum $(CONF_FILES) 2>/dev/null | awk '{print $$1}' | sha1sum | awk '{print $$1}' ); \
	new_sig=$$( printf "%s\n%s\n%s\n" "$$tests_sig" "$$code_sig" "$$conf_sig" | sha1sum | awk '{print $$1}' ); \
	old_sig=$$(cat $(INTEG_SIG) 2>/dev/null || echo -n); \
	if [ "$(NO_CACHE)" = "1" ] || [ "$$new_sig" != "$$old_sig" ] || [ ! -f $@ ]; then \
	  echo "=== Running integration tests ==="; \
	  set +e; \
	  "$(PY)" -m pytest -q $(INTEG_DIR) -m "not live" $(PYTEST_WARN) $(PYTEST_XDIST) $(PYTEST_TIMEOUT) $(PYTEST_COV_INTEG); \
	  status=$$?; \
	  set -e; \
	  if [ "$$status" -eq 5 ]; then \
	    echo "No integration tests collected; treating as success."; \
	  elif [ "$$status" -ne 0 ]; then \
	    exit $$status; \
	  fi; \
	  echo "$$new_sig" > $(INTEG_SIG); \
	  touch $@; \
	else \
	  echo "No changes detected; skipping integration tests."; \
	fi

test: $(UNIT_STAMP) $(INTEG_STAMP)
	@echo "=== Aggregated coverage report ==="
	"$(PY)" -m coverage report
	"$(PY)" -m coverage xml
	@echo "✅ Unit + Integration tests up-to-date (not live)"

test-all: $(ENV_STAMP)
	"$(PY)" -m pytest -v -m "not live" $(PYTEST_WARN) $(PYTEST_XDIST) $(PYTEST_TIMEOUT) --cov=$(PKG) --cov-report=term-missing --cov-report=xml --cov-fail-under=40

test-live: $(ENV_STAMP)
	SF_LIVE_TESTS=true "$(PY)" -m pytest -v -m live $(PYTEST_WARN) --timeout=180 --cov=$(PKG) --cov-report=xml

clean-tests:
	rm -rf $(STAMPS_DIR)

# -----------------------------------------------------------------------------#
# Build & Publish
build: $(ENV_STAMP)
	"$(PY)" -m pip install -U build
	"$(PY)" -m build

upload: build
	"$(PY)" -m pip install -U twine
	"$(PY)" -m twine check dist/*
	"$(PY)" -m twine upload dist/*

# -----------------------------------------------------------------------------#
# Version & Release helpers (setuptools_scm + Git tags)
# -----------------------------------------------------------------------------#
# Version & Release helpers (setuptools_scm + Git tags)

fetch-tags:
	@git fetch --tags --force --prune 2>/dev/null || true

LAST_TAG := $(strip $(shell git tag --list "v[0-9]*.[0-9]*.[0-9]*" --sort=-version:refname | head -n 1))
ifeq ($(LAST_TAG),)
LAST_TAG := v0.0.0
endif

# Robust parsing using sed/cut for cross-platform compatibility
MAJOR := $(shell echo "$(LAST_TAG)" | sed 's/^v//' | cut -d. -f1)
MINOR := $(shell echo "$(LAST_TAG)" | sed 's/^v//' | cut -d. -f2)
PATCH := $(shell echo "$(LAST_TAG)" | sed 's/^v//' | cut -d. -f3)

ifeq ($(strip $(MAJOR)),)
MAJOR := 0
endif
ifeq ($(strip $(MINOR)),)
MINOR := 0
endif
ifeq ($(strip $(PATCH)),)
PATCH := 0
endif

version: $(ENV_STAMP)
	@"$(PY)" -m setuptools_scm || true

define CHANGELOG
$(shell \
  if git rev-parse "$(LAST_TAG)" >/dev/null 2>&1; then \
    git log "$(LAST_TAG)..HEAD" --pretty=format:"- %s (%h)" --no-merges; \
  else \
    git log HEAD --pretty=format:"- %s (%h)" --no-merges; \
  fi \
)
endef

changelog: fetch-tags
	@echo "Changes since $(LAST_TAG):"
	@echo "$(CHANGELOG)"

changelog-md: fetch-tags
	@mkdir -p docs
	@echo "Writing docs/CHANGELOG.md ..."
	@printf "# Changelog\n\n## Since %s\n\n%s\n" "$(LAST_TAG)" "$(CHANGELOG)" > docs/CHANGELOG.md
	@echo "✅ docs/CHANGELOG.md updated"

release-show: fetch-tags $(ENV_STAMP)
	@echo "python exe:"; "$(PY)" -c "import sys; print(sys.executable)"
	@echo "setuptools_scm version:"; "$(PY)" -m setuptools_scm || echo "(unavailable)"
	@echo "installed dist version:"; "$(PY)" -c "import importlib.metadata as m; print(m.version('$(PKG)'))" || echo "(package not installed)"
	@echo "Last Git tag: $(LAST_TAG)"
	@echo "Parsed: MAJOR=$(MAJOR) MINOR=$(MINOR) PATCH=$(PATCH)"

check-clean:
	@if ! git diff --quiet || ! git diff --cached --quiet; then \
	  echo "❌ Working directory not clean. Commit or stash changes before releasing."; \
	  git status -s; \
	  exit 1; \
	fi
	@if [ "$$(git rev-parse @ 2>/dev/null)" != "$$(git rev-parse @{u} 2>/dev/null)" ]; then \
	  echo "❌ Local branch not in sync with upstream (push/pull first)."; \
	  exit 1; \
	fi

NL := $(shell printf "\n")

release-patch: fetch-tags check-clean
	@NEW="v$(MAJOR).$(MINOR).$$(($(PATCH) + 1))"; \
	echo "Tagging $$NEW (from LAST_TAG=$(LAST_TAG))"; \
	TMP="$$(mktemp -t dof-tagmsg.XXXXXX)"; \
	printf 'release: %s\n\n%s\n' "$$NEW" "$(CHANGELOG)" > "$$TMP"; \
	git tag -a "$$NEW" -F "$$TMP"; \
	rm -f "$$TMP"; \
	git push origin "$$NEW"; \
	echo "Tagged $$NEW"

release-minor: fetch-tags check-clean
	@NEW="v$(MAJOR).$$(($(MINOR) + 1)).0"; \
	echo "Tagging $$NEW (from LAST_TAG=$(LAST_TAG))"; \
	TMP="$$(mktemp -t dof-tagmsg.XXXXXX)"; \
	printf 'release: %s\n\n%s\n' "$$NEW" "$(CHANGELOG)" > "$$TMP"; \
	git tag -a "$$NEW" -F "$$TMP"; \
	rm -f "$$TMP"; \
	git push origin "$$NEW"; \
	echo "Tagged $$NEW"

release-major: fetch-tags check-clean
	@NEW="v$$(($(MAJOR) + 1)).0.0"; \
	echo "Tagging $$NEW (from LAST_TAG=$(LAST_TAG))"; \
	TMP="$$(mktemp -t dof-tagmsg.XXXXXX)"; \
	printf 'release: %s\n\n%s\n' "$$NEW" "$(CHANGELOG)" > "$$TMP"; \
	git tag -a "$$NEW" -F "$$TMP"; \
	rm -f "$$TMP"; \
	git push origin "$$NEW"; \
	echo "Tagged $$NEW"

release: fetch-tags $(ENV_STAMP)
	@echo "=== Running full test suite before release ==="
	$(MAKE) test-all
	@echo "=== Changelog (from $(LAST_TAG) to HEAD) ==="
	$(MAKE) changelog
	@echo "=== Performing $(KIND) release ==="
	@if [ "$(KIND)" = "patch" ]; then \
	  $(MAKE) release-patch; \
	elif [ "$(KIND)" = "minor" ]; then \
	  $(MAKE) release-minor; \
	elif [ "$(KIND)" = "major" ]; then \
	  $(MAKE) release-major; \
	else \
	  echo "Unknown KIND=$(KIND). Use: patch | minor | major"; \
	  exit 1; \
	fi


# -----------------------------------------------------------------------------#
# CLI convenience
CLI_ARGS ?=
run-cli: $(ENV_STAMP)
	"$(PY)" -m $(PKG) $(CLI_ARGS)

# -----------------------------------------------------------------------------#
# Branded profile from your own (gitignored) template + style map.
# Put your private assets under $(LOCAL_DIR)/ (gitignored):
#   $(LOCAL_TEMPLATE)   your branded style-donor .docx
#   $(LOCAL_STYLEMAP)   TOML mapping logical roles -> your template's style names
#                       (copy examples/style-map.example.toml and edit)
# Usage: make profile CV=path/to/cv.(pdf|docx|txt) [OUT=out.docx] [OFFLINE=1]
# Defaults to the Claude path (needs ANTHROPIC_API_KEY); OFFLINE=1 forces the
# deterministic, network-free client.
LOCAL_DIR      ?= local
LOCAL_TEMPLATE ?= $(LOCAL_DIR)/template.docx
LOCAL_STYLEMAP ?= $(LOCAL_DIR)/style-map.toml
CV  ?=
OUT ?=

profile: $(ENV_STAMP)
	[ -n "$(CV)" ] || { echo "Usage: make profile CV=path/to/cv.(pdf|docx|txt) [OUT=out.docx] [OFFLINE=1]"; exit 2; }
	test -f "$(LOCAL_TEMPLATE)" || { echo "Missing $(LOCAL_TEMPLATE) - add your branded .docx under $(LOCAL_DIR)/"; exit 2; }
	test -f "$(LOCAL_STYLEMAP)" || { echo "Missing $(LOCAL_STYLEMAP) - copy examples/style-map.example.toml to $(LOCAL_STYLEMAP) and edit"; exit 2; }
	"$(PY)" -m $(PKG) convert "$(CV)" --template "$(LOCAL_TEMPLATE)" --style-map "$(LOCAL_STYLEMAP)" $(if $(OUT),--output "$(OUT)",) $(if $(OFFLINE),--offline,)

# -----------------------------------------------------------------------------#
clean:
	rm -rf build dist .eggs *.egg-info .coverage htmlcov .pytest_cache coverage.xml
	find . -type d -name "__pycache__" -prune -exec rm -rf {} +
	rm -rf $(STAMPS_DIR)
