SHELL := /bin/bash
.SHELLFLAGS := -euo pipefail -c

# =============================================================================
# Configuration and Environment Variables
# =============================================================================

.DEFAULT_GOAL:=help
.ONESHELL:
.EXPORT_ALL_VARIABLES:
MAKEFLAGS += --no-print-directory
PYTHON_VERSION ?= 3.11
UV_SYNC_ARGS ?= --all-extras --dev

-include Makefile.local

# -----------------------------------------------------------------------------
# Display Formatting and Colors
# -----------------------------------------------------------------------------
BLUE := $(shell printf "\033[1;34m")
GREEN := $(shell printf "\033[1;32m")
RED := $(shell printf "\033[1;31m")
YELLOW := $(shell printf "\033[1;33m")
NC := $(shell printf "\033[0m")
INFO := $(shell printf "$(BLUE)ℹ$(NC)")
OK := $(shell printf "$(GREEN)✓$(NC)")
WARN := $(shell printf "$(YELLOW)⚠$(NC)")
ERROR := $(shell printf "$(RED)✖$(NC)")

# =============================================================================
# Help and Documentation
# =============================================================================

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

# =============================================================================
# Installation and Environment Setup
# =============================================================================

.PHONY: install-uv
install-uv:                                         ## Install latest version of uv
	@echo "${INFO} Installing uv... ⚡"
	@curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1
	@echo "${OK} UV installed successfully 🎉"

.PHONY: install
install: destroy clean                              ## Install all locked development dependencies
	@echo "${INFO} Starting fresh installation... ⚡"
	@uv python pin $(PYTHON_VERSION) >/dev/null 2>&1
	@uv venv >/dev/null 2>&1
	@uv sync $(UV_SYNC_ARGS)
	@echo "${OK} Installation complete! 🎉"

.PHONY: destroy
destroy:                                            ## Destroy virtual environment and clean caches
	@echo "${INFO} Destroying virtual environment... 🗑️"
	@uvx prek clean >/dev/null 2>&1 || true
	@rm -rf .venv
	@echo "${OK} Virtual environment destroyed 🗑️"

# =============================================================================
# Dependency Management
# =============================================================================

.PHONY: upgrade
upgrade:                                            ## Upgrade all dependencies to latest stable versions
	@echo "${INFO} Updating all dependencies... 🔄"
	@uv lock --upgrade
	@echo "${OK} Dependencies updated 🔄"
	@uvx prek autoupdate --cooldown-days 7
	@echo "${OK} Updated prek hooks (7-day cooldown) 🔄"
	@uv lock >/dev/null 2>&1

.PHONY: lock
lock:                                               ## Rebuild lockfiles from scratch
	@echo "${INFO} Rebuilding lockfiles... 🔄"
	@uv lock --upgrade >/dev/null 2>&1
	@echo "${OK} Lockfiles updated 🔄"

# =============================================================================
# Build and Release
# =============================================================================

.PHONY: build
build: js-build                                    ## Build the project
	@echo "${INFO} Building package... 📦"
	@uv build
	@uv run python tools/frontend/tests/distribution.py
	@echo "${OK} Package build complete 📦"

.PHONY: release
release:                                           ## Bump version and create release tag (bump=major|minor|patch)
	@if [ -z "$(bump)" ]; then \
		echo "${ERROR} Usage: make release bump=major|minor|patch"; \
		exit 1; \
	fi
	@echo "${INFO} Preparing for release... 📦"
	@make docs
	@make clean
	@make build
	@uv run bump-my-version bump $(bump)
	@uv lock --upgrade-package litestar-asyncapi >/dev/null 2>&1
	@echo "${OK} Release complete 🎉"

.PHONY: pre-release
pre-release:                                       ## Start a pre-release: make pre-release version=0.2.0-alpha.1
	@if [ -z "$(version)" ]; then \
		echo "${ERROR} Usage: make pre-release version=X.Y.Z-alpha.N"; \
		echo ""; \
		echo "Pre-release workflow:"; \
		echo "  1. Start alpha:     make pre-release version=0.2.0-alpha.1"; \
		echo "  2. Next alpha:      make pre-release version=0.2.0-alpha.2"; \
		echo "  3. Move to beta:    make pre-release version=0.2.0-beta.1"; \
		echo "  4. Move to rc:      make pre-release version=0.2.0-rc.1"; \
		echo "  5. Final release:   make release bump=pre (from rc) OR bump=patch/minor (from stable)"; \
		exit 1; \
	fi
	@echo "${INFO} Preparing pre-release $(version)... 🧪"
	@make clean
	@make build
	@uv run bump-my-version bump --new-version $(version) pre
	@uv lock --upgrade-package litestar-asyncapi >/dev/null 2>&1
	@echo "${OK} Pre-release $(version) complete 🧪"
	@echo ""
	@echo "${INFO} Next steps:"
	@echo "  1. Push: git push origin HEAD"
	@echo "  2. Create a GitHub pre-release: gh release create v$(version) --prerelease --title 'v$(version)'"
	@echo "  3. This will publish to PyPI with pre-release tags"

# =============================================================================
# Documentation
# =============================================================================

DOCS_RUN = uv run --frozen

.PHONY: docs
docs:                                             ## Build, redirect and verify the documentation site
	@echo "${INFO} Building docs... 📚"
	@$(DOCS_RUN) python -m tools.docs.generate_cli_docs
	@$(DOCS_RUN) python -m tools.docs.export_example_documents
	@$(DOCS_RUN) zensical build --clean --strict
	@$(DOCS_RUN) python -m tools.docs.postbuild
	@$(DOCS_RUN) python -m tools.docs.verify_docs_artifacts
	@echo "${OK} Docs build complete 📚"

.PHONY: docs-serve
docs-serve:                                       ## Serve the documentation with live reload
	@$(DOCS_RUN) python -m tools.docs.generate_cli_docs
	@$(DOCS_RUN) python -m tools.docs.export_example_documents
	@$(DOCS_RUN) zensical serve

.PHONY: docs-linkcheck
docs-linkcheck:                                   ## Check external links in the built site with lychee
	@if ! command -v lychee >/dev/null 2>&1; then \
		echo "${WARN} lychee is not installed; skipping the external link check"; \
	elif [ ! -f site/index.html ]; then \
		echo "${ERROR} No built site; run make docs first"; \
		exit 1; \
	else \
		lychee --root-dir "$$(pwd)/site" 'site/**/*.html'; \
	fi

.PHONY: docs-clean
docs-clean:                                       ## Clean documentation artifacts
	@echo "${INFO} Cleaning docs artifacts... 📚"
	@rm -rf site docs/_generated docs/reference/cli.md >/dev/null 2>&1
	@echo "${OK} Docs artifacts cleaned 📚"

# =============================================================================
# Validation Targets
# =============================================================================

.PHONY: scan-docstrings
scan-docstrings:                                    ## Check inline docstrings on public dataclass fields
	@echo "${INFO} Scanning public docstrings... 🔍"
	@uv run python -m tools.docs.scan_public_docstrings
	@echo "${OK} Public docstrings complete ✨"

.PHONY: validate-pep723
validate-pep723:                                    ## Validate PEP 723 blocks in runnable examples
	@echo "${INFO} Validating PEP 723 script blocks... 🔍"
	@uv run python tools/ci/validate_pep723_blocks.py
	@echo "${OK} PEP 723 blocks valid ✨"

# =============================================================================
# Cleaning and Maintenance
# =============================================================================

.PHONY: clean
clean:                                              ## Cleanup temporary build artifacts
	@echo "${INFO} Cleaning working directory... 🧹"
	@rm -rf .pytest_cache .ruff_cache .hypothesis build/ dist/ .eggs/ .coverage coverage.xml coverage.json htmlcov/ .pytest_cache src/tests/.pytest_cache src/tests/**/.pytest_cache .mypy_cache test-results playwright-report .tmp >/dev/null 2>&1
	@find . -name '*.egg-info' -exec rm -rf {} + >/dev/null 2>&1
	@find . -type f -name '*.egg' -exec rm -f {} + >/dev/null 2>&1
	@find . -name '*.pyc' -exec rm -f {} + >/dev/null 2>&1
	@find . -name '*.pyo' -exec rm -f {} + >/dev/null 2>&1
	@find . -name '*~' -exec rm -f {} + >/dev/null 2>&1
	@find . -name '__pycache__' -exec rm -rf {} + >/dev/null 2>&1
	@find . -name '.ipynb_checkpoints' -exec rm -rf {} + >/dev/null 2>&1
	@echo "${OK} Working directory cleaned ✨"

# =============================================================================
# Testing and Quality Checks
# =============================================================================

.PHONY: test
test:                                              ## Run the tests
	@echo "${INFO} Running test cases... 🧪"
	@uv run pytest src/tests
	@echo "${OK} Tests complete 🧪"

.PHONY: coverage
coverage:                                          ## Run tests with coverage report
	@echo "${INFO} Running tests with coverage... 🧪"
	@uv run pytest src/tests --cov -n auto
	@uv run coverage html >/dev/null 2>&1
	@uv run coverage xml >/dev/null 2>&1
	@echo "${OK} Coverage report generated 🧪"

# -----------------------------------------------------------------------------
# Type Checking
# -----------------------------------------------------------------------------

.PHONY: mypy
mypy:                                              ## Run mypy
	@echo "${INFO} Running mypy... 🔍"
	@uv run mypy
	@echo "${OK} Mypy checks passed ✨"

.PHONY: pyright
pyright:                                           ## Run pyright
	@echo "${INFO} Running pyright... 🔍"
	@uv run pyright
	@echo "${OK} Pyright checks passed ✨"

.PHONY: type-check
type-check: mypy pyright                           ## Run all type checking

# -----------------------------------------------------------------------------
# Linting and Formatting
# -----------------------------------------------------------------------------

WORKING_TREE_FILES = $$(git ls-files --cached --others --exclude-standard 2>/dev/null)

.PHONY: prek
prek:                                               ## Run prek hooks
	@echo "${INFO} Running prek checks... 🔍"
	@files="${WORKING_TREE_FILES}"; \
	if [ -n "$$files" ]; then \
		uvx prek run --show-diff-on-failure --color=always --files $$files; \
	else \
		uvx prek run --show-diff-on-failure --color=always --all-files; \
	fi
	@echo "${OK} prek checks passed ✨"

.PHONY: zizmor
zizmor:                                             ## Run zizmor workflow security scanner
	@echo "${INFO} Running zizmor workflow security checks... 🛡️"
	@if [ -d ".github/workflows" ]; then \
		uvx zizmor .github/workflows; \
	else \
		echo "${WARN} No .github/workflows directory found"; \
	fi
	@echo "${OK} zizmor workflow checks passed ✨"

.PHONY: slotscheck
slotscheck:                                        ## Run slotscheck
	@echo "${INFO} Running slots check... 🔍"
	@uv run slotscheck src/litestar_asyncapi/
	@echo "${OK} Slots check passed ✨"

.PHONY: fix
fix:                                               ## Fix linting issues
	@echo "${INFO} Fixing linting issues... 🔍"
	@uv run ruff check --fix --unsafe-fixes .
	@uv run ruff format .
	@echo "${OK} Linting issues fixed ✨"

.PHONY: lint
lint: prek type-check slotscheck zizmor validate-pep723 scan-docstrings ## Run all linting checks

.PHONY: check-all
check-all: lint test                               ## Run lint, then the test suite once with coverage

.PHONY: validate-asyncapi
validate-asyncapi: export-asyncapi-fixture export-example-documents ## Validate offline AsyncAPI contracts, docs examples and Draft07 payloads
	@npm run validate:asyncapi
	@node tools/validate_asyncapi.mjs .tmp/asyncapi-fixture.json docs/_generated

.PHONY: export-example-documents
export-example-documents:                          ## Export every docs example document to docs/_generated
	@uv run python -m tools.docs.export_example_documents

.PHONY: export-asyncapi-fixture
export-asyncapi-fixture:                           ## Export application contracts for the official gate
	@mkdir -p .tmp
	@uv run litestar --app-dir src --app tests.fixtures.apps.cli:app asyncapi export --output .tmp/asyncapi-fixture.json --overwrite
	@uv run litestar --app-dir src --app tests.fixtures.apps.bindings:app_3_0 asyncapi export --output src/tests/fixtures/asyncapi/bindings-3.0.0.json --overwrite
	@uv run litestar --app-dir src --app tests.fixtures.apps.bindings:app_3_1 asyncapi export --output src/tests/fixtures/asyncapi/bindings-3.1.0.json --overwrite
	@uv run litestar --app-dir src --app tests.fixtures.apps.realtime:app_3_0 asyncapi export --output src/tests/fixtures/asyncapi/realtime-3.0.0.json --overwrite
	@uv run litestar --app-dir src --app tests.fixtures.apps.realtime:app_3_1 asyncapi export --output src/tests/fixtures/asyncapi/realtime-3.1.0.json --overwrite
	@uv run litestar --app-dir src --app tests.fixtures.apps.openapi:app_3_0 asyncapi export --output src/tests/fixtures/asyncapi/openapi-3.0.0.json --overwrite
	@uv run litestar --app-dir src --app tests.fixtures.apps.openapi:app_3_1 asyncapi export --output src/tests/fixtures/asyncapi/openapi-3.1.0.json --overwrite

.PHONY: js-build js-test browser-test
js-build:                                          ## Build packaged browser assets
	@npm run build

js-test:                                           ## Test rendering-view transformations
	@npm test

browser-test:                                      ## Test packaged documentation in Chromium
	@npm run test:browser

WHEEL ?= $(wildcard dist/*.whl)
LITESTAR_BOUNDARY ?= minimum

.PHONY: js-install installed-test browser-test-installed
js-install:                                        ## Install the exact frontend lockfile
	@npm ci

installed-test:                                    ## Test a built wheel outside the checkout on PYTHON_VERSION
	@set -eu; \
	wheel="$$(realpath "$(WHEEL)")"; \
	workdir="$$(mktemp -d)"; \
	trap 'rm -rf "$$workdir"' EXIT; \
	cp -R src/tests "$$workdir/tests"; \
	cp pyproject.toml "$$workdir/pyproject.toml"; \
	uv export --frozen --only-group tests --no-emit-project --no-hashes --output-file "$$workdir/requirements.txt" >/dev/null; \
	uv venv --python "$(PYTHON_VERSION)" "$$workdir/.venv"; \
	framework='litestar==2.24.0'; \
	if [ "$(LITESTAR_BOUNDARY)" = latest ]; then framework='litestar>=2.24,<3'; fi; \
	uv pip install --python "$$workdir/.venv/bin/python" "$$wheel" "$$framework"; \
	(cd "$$workdir" && uv run --no-project --no-sync --python .venv/bin/python python -c 'import sys; from pathlib import Path; from importlib.metadata import version; import litestar_asyncapi; from litestar import Litestar; from litestar_asyncapi import AsyncAPIPlugin; assert Path(litestar_asyncapi.__file__).is_relative_to(sys.prefix); plugin = AsyncAPIPlugin(); assert plugin.get_asyncapi_schema(Litestar(plugins=[plugin]))["asyncapi"] == "3.1.0"; print("Minimal installed wheel:", sys.version, "Litestar", version("litestar"), litestar_asyncapi.__file__)'); \
	uv pip install --python "$$workdir/.venv/bin/python" -r "$$workdir/requirements.txt" "$$wheel"; \
	if [ "$(LITESTAR_BOUNDARY)" = latest ]; then uv pip install --python "$$workdir/.venv/bin/python" --upgrade 'litestar[standard,attrs,pydantic]>=2.24'; else uv pip install --python "$$workdir/.venv/bin/python" 'litestar[standard,attrs,pydantic]==2.24.0'; fi; \
	"$$workdir/.venv/bin/python" -c 'import sys; from importlib.metadata import version; resolved = version("litestar"); assert sys.argv[1] != "minimum" or resolved == "2.24.0"; print("Full test dependency boundary:", sys.argv[1], resolved)' "$(LITESTAR_BOUNDARY)"; \
	(cd "$$workdir" && uv run --no-project --no-sync --python .venv/bin/python python -m pytest tests --no-cov -q)

browser-test-installed:                            ## Open the built wheel in a real browser outside the checkout
	@set -eu; \
	wheel="$$(realpath "$(WHEEL)")"; \
	workdir="$$(mktemp -d)"; \
	trap 'rm -rf "$$workdir"' EXIT; \
	cp tools/frontend/tests/server.py "$$workdir/server.py"; \
	uv venv --python "$(PYTHON_VERSION)" "$$workdir/.venv"; \
	uv pip install --python "$$workdir/.venv/bin/python" "$$wheel" uvicorn websockets; \
	manifest="$$(realpath src/litestar_asyncapi/assets/ui/manifest.json)"; \
	(cd "$$workdir" && .venv/bin/python -c 'import sys; from pathlib import Path; from hashlib import sha256; import litestar_asyncapi; package = Path(litestar_asyncapi.__file__).parent; assert package.is_relative_to(sys.prefix); assert sha256((package / "assets/ui/manifest.json").read_bytes()).digest() == sha256(Path(sys.argv[1]).read_bytes()).digest(); print("Installed browser package and exact manifest:", package)' "$$manifest"); \
	ASYNCAPI_BROWSER_SERVER="$$workdir/.venv/bin/python $$workdir/server.py" npm run test:browser
