.DEFAULT_GOAL := help

PYTHON      := python3
VENV        := .venv
BIN         := $(VENV)/bin
PIP         := $(BIN)/pip
PIP_COMPILE := $(BIN)/pip-compile
VENDOR_DIR  := vendor
# Stamp file: touched after a successful offline install.
# Make compares its mtime against requirements-dev.txt to know when to reinstall.
STAMP       := $(VENV)/.installed

# ── help ──────────────────────────────────────────────────────────────────────
.PHONY: help
help:  ## Show this help message
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
	  awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}'

# ── venv sentinels (real file targets — Make skips if already built) ──────────
#
# $(VENV)/bin/python    bare venv only; created offline, no extra packages
# $(VENV)/bin/pip-compile  venv + pip-tools; requires network
# $(STAMP)              venv + all dev deps from vendor/; requires vendor/

$(VENV)/bin/python:
	$(PYTHON) -m venv $(VENV)

$(VENV)/bin/pip-compile: $(VENV)/bin/python
	$(PIP) install --upgrade pip pip-tools

# Reinstall whenever requirements-dev.txt changes (mtime comparison).
$(STAMP): $(VENV)/bin/python requirements-dev.txt
	@test -d $(VENDOR_DIR) || \
	  (echo "ERROR: vendor/ not found. Run 'make vendor' on a networked machine first." && exit 1)
	@test -f requirements-dev.txt || \
	  (echo "ERROR: requirements-dev.txt missing. Run 'make update-locks' first." && exit 1)
	$(PIP) install \
		--no-index \
		--find-links=$(VENDOR_DIR) \
		--require-hashes \
		--no-build-isolation \
		-r requirements-dev.txt
	$(PIP) install \
		--no-index \
		--find-links=$(VENDOR_DIR) \
		--no-deps \
		--editable .
	@touch $(STAMP)

# ── venv: online bootstrap convenience alias ───────────────────────────────────
.PHONY: venv
venv: $(VENV)/bin/pip-compile  ## Create .venv and install pip-tools (needs network)

# ── lock file generation (REQUIRES NETWORK) ───────────────────────────────────
.PHONY: update-locks
update-locks: $(VENV)/bin/pip-compile  ## Regenerate requirements.txt + requirements-dev.txt (needs network)
	$(PIP_COMPILE) \
		--generate-hashes \
		--allow-unsafe \
		--resolver=backtracking \
		--output-file requirements.txt \
		requirements.in
	$(PIP_COMPILE) \
		--generate-hashes \
		--allow-unsafe \
		--resolver=backtracking \
		--output-file requirements-dev.txt \
		requirements-dev.in
	@echo "Lock files updated. Run 'make vendor' then commit all four files."

# ── vendor: download all wheels into vendor/ (REQUIRES NETWORK) ───────────────
# Hash lines are stripped before passing to pip download: hashes are for
# install-time verification only. pip download does not need them, and
# running in hash mode breaks transitive deps that have unpinned sub-deps
# (e.g. pip-api depends on pip without a pinned version).
# Python version used inside the runtime container image (python:3.12-slim).
# When the host Python differs, a second pip-download pass fetches binary wheels
# for this version so that 'make build-image' works without network access.
CONTAINER_PYTHON_VERSION := 3.12
CONTAINER_ABI            := cp312
CONTAINER_PLATFORM       := manylinux2014_x86_64

.PHONY: vendor
vendor: $(VENV)/bin/python requirements.txt requirements-dev.txt  ## Download all wheels to vendor/ (needs network)
	mkdir -p $(VENDOR_DIR)
	grep -hE '^[a-zA-Z0-9]' requirements.txt requirements-dev.txt \
		| sed 's/[[:space:]]*\\$$//' \
		| sort -u \
		> $(VENDOR_DIR)/.dl-reqs.txt
	# Host wheels (dev tools + whatever the host Python resolves)
	$(PIP) download \
		--no-cache-dir \
		--dest $(VENDOR_DIR) \
		-r $(VENDOR_DIR)/.dl-reqs.txt
	# Container wheels: binary-only, pinned to the runtime image's Python version.
	# Only runtime deps (requirements.txt) are installed inside the container.
	grep -hE '^[a-zA-Z0-9]' requirements.txt \
		| sed 's/[[:space:]]*\\$$//' \
		| sort -u \
		> $(VENDOR_DIR)/.dl-reqs-container.txt
	$(PIP) download \
		--no-cache-dir \
		--dest $(VENDOR_DIR) \
		--python-version $(CONTAINER_PYTHON_VERSION) \
		--abi $(CONTAINER_ABI) \
		--platform $(CONTAINER_PLATFORM) \
		--only-binary=:all: \
		-r $(VENDOR_DIR)/.dl-reqs-container.txt
	rm -f $(VENDOR_DIR)/.dl-reqs.txt $(VENDOR_DIR)/.dl-reqs-container.txt
	@echo "All wheels downloaded to $(VENDOR_DIR)/."

# ── vendor-pack: create a portable tarball of vendor/ ────────────────────────
.PHONY: vendor-pack
vendor-pack: vendor  ## Pack vendor/ into vendor.tar.gz for air-gap transfer
	tar -czf vendor.tar.gz $(VENDOR_DIR)/
	@echo "Created vendor.tar.gz — transfer this to the offline machine."

# ── install-offline: ensure venv exists and dev deps are installed ─────────────
.PHONY: install-offline
install-offline: $(STAMP)  ## Create .venv and install all dev deps from vendor/ — no network required

# ── pre-commit hooks (one-time per clone) ─────────────────────────────────────
.PHONY: pre-commit-install
pre-commit-install: $(STAMP)  ## Install git pre-commit hooks — no network required
	$(BIN)/pre-commit install
	@echo "pre-commit hooks installed. No network was used."

# ── formatting ────────────────────────────────────────────────────────────────
.PHONY: fmt
fmt: $(STAMP)  ## Format code with ruff
	$(BIN)/ruff format src/ tests/

# ── linting ───────────────────────────────────────────────────────────────────
.PHONY: lint
lint: $(STAMP)  ## Lint code with ruff
	$(BIN)/ruff check src/ tests/

# ── type checking ─────────────────────────────────────────────────────────────
.PHONY: typecheck
typecheck: $(STAMP)  ## Run mypy type checking
	$(BIN)/mypy

# ── security ──────────────────────────────────────────────────────────────────
.PHONY: security
security: $(STAMP)  ## Run bandit + pip-audit (pip-audit needs network for vuln DB)
	$(BIN)/bandit -c pyproject.toml -r src/
	$(BIN)/pip-audit --require-hashes -r requirements.txt

.PHONY: security-offline
security-offline: $(STAMP)  ## Run bandit only — fully offline security check
	$(BIN)/bandit -c pyproject.toml -r src/

# ── tests ─────────────────────────────────────────────────────────────────────
.PHONY: test
test: $(STAMP)  ## Run tests with coverage
	$(BIN)/pytest

# ── full check suite ──────────────────────────────────────────────────────────
.PHONY: check
check: fmt lint typecheck security-offline test  ## Run all checks (offline — CI equivalent)

# ── sample-project end-to-end install test ────────────────────────────────────
# Scaffolds a fresh project with 'offliner init', simulates the full
# offline workflow (bundle export → pip-compile offline → vendor.tar.gz →
# bundle apply → make install-offline equivalent), verifies the project is
# importable, then exercises the image bundle workflow (bundle image export →
# synthetic images.tar.gz → bundle image apply).  Uses offliner's own vendor/
# as the wheel source.  The image apply step is skipped if no container runtime
# is found in PATH.
.PHONY: test-sample-project
test-sample-project: $(STAMP)  ## End-to-end: init → bundle → install → import → image export/apply (offline, no network)
	@PROJROOT="$(CURDIR)" && \
	TESTDIR="$$(mktemp -d -t offliner-sample.XXXXXX)" && \
	trap 'rm -rf "$$TESTDIR"' EXIT && \
	echo "[sample] Working in $$TESTDIR" && \
	\
	echo "[sample] 1/8 offliner init" && \
	cd "$$TESTDIR" && \
	"$$PROJROOT/$(BIN)/offliner" init sampleapp --author "Test User" --email "test@example.com" \
	    --images "docker.io/library/busybox:latest" && \
	cd sampleapp && \
	\
	echo "[sample] 2/8 bundle export" && \
	"$$PROJROOT/$(BIN)/offliner" bundle export && \
	sed -n "/requirements\.in.*<<'INEOF'/,/^INEOF/p" download.sh | grep -v "INEOF\|cat >" > requirements.in && \
	sed -n "/requirements-dev\.in.*<<'INEOF'/,/^INEOF/p" download.sh | grep -v "INEOF\|cat >" > requirements-dev.in && \
	\
	echo "[sample] 3/8 pip-compile (offline, using offliner vendor/)" && \
	"$$PROJROOT/$(BIN)/pip-compile" \
	    --no-index --find-links="$$PROJROOT/$(VENDOR_DIR)" \
	    --generate-hashes --allow-unsafe --no-emit-find-links \
	    --output-file requirements.txt requirements.in && \
	"$$PROJROOT/$(BIN)/pip-compile" \
	    --no-index --find-links="$$PROJROOT/$(VENDOR_DIR)" \
	    --generate-hashes --allow-unsafe --no-emit-find-links \
	    --output-file requirements-dev.txt requirements-dev.in && \
	\
	echo "[sample] 4/8 pip download (offline, filter to needed wheels)" && \
	mkdir -p vendor_staging && \
	"$$PROJROOT/$(BIN)/pip" download \
	    --no-index --find-links="$$PROJROOT/$(VENDOR_DIR)" \
	    --no-deps --dest vendor_staging \
	    -r requirements.txt -r requirements-dev.txt && \
	tar -czf vendor.tar.gz \
	    --transform 's|vendor_staging|vendor|' \
	    vendor_staging/ requirements.txt requirements-dev.txt && \
	\
	echo "[sample] 5/8 offliner bundle apply" && \
	"$$PROJROOT/$(BIN)/offliner" bundle apply vendor.tar.gz && \
	\
	echo "[sample] 6/8 make install-offline equivalent" && \
	python3 -m venv .venv && \
	.venv/bin/pip install \
	    --no-index --find-links=vendor \
	    --require-hashes --no-build-isolation \
	    -r requirements-dev.txt && \
	.venv/bin/pip install \
	    --no-index --find-links=vendor \
	    --no-build-isolation --no-deps \
	    --editable . && \
	.venv/bin/python -c "import sampleapp; print('[sample] import OK, version:', sampleapp.__version__)" && \
	\
	echo "[sample] 7/8 bundle image export" && \
	"$$PROJROOT/$(BIN)/offliner" bundle image export && \
	test -f pull-images.sh && \
	grep -q "docker.io/library/busybox:latest" pull-images.sh && \
	echo "[sample]     pull-images.sh verified" && \
	\
	echo "[sample] 8/8 bundle image apply" && \
	{ \
	  if command -v podman >/dev/null 2>&1 || command -v docker >/dev/null 2>&1; then \
	    SAFE_NAME="docker.io_library_busybox_latest" && \
	    IMG_REF="docker.io/library/busybox:latest" && \
	    mkdir -p img_work/staging/images && \
	    printf '{"architecture":"amd64","os":"linux","rootfs":{"type":"layers","diff_ids":[]},"config":{}}' \
	      > img_work/cfg.json && \
	    CONFIG_SHA="$$(sha256sum img_work/cfg.json | cut -d' ' -f1)" && \
	    cp img_work/cfg.json "img_work/$${CONFIG_SHA}.json" && \
	    printf '[{"Config":"%s.json","RepoTags":["%s"],"Layers":[]}]' \
	      "$$CONFIG_SHA" "$$IMG_REF" > img_work/manifest.json && \
	    tar -cf "img_work/staging/images/$${SAFE_NAME}.tar" \
	      -C img_work manifest.json "$${CONFIG_SHA}.json" && \
	    printf '%s  %s  unknown\n' "$${SAFE_NAME}.tar" "$$IMG_REF" \
	      > img_work/staging/images.manifest && \
	    (cd img_work/staging/images && sha256sum "$${SAFE_NAME}.tar") \
	      > img_work/staging/images.sha256 && \
	    sed -i 's|  \./|  |' img_work/staging/images.sha256 && \
	    tar -czf images.tar.gz \
	      -C img_work/staging images/ images.manifest images.sha256 && \
	    "$$PROJROOT/$(BIN)/offliner" bundle image apply images.tar.gz && \
	    echo "[sample]     bundle image apply PASS"; \
	  else \
	    echo "[sample]     SKIP bundle image apply — no container runtime in PATH"; \
	  fi; \
	} && \
	\
	echo "[sample] PASS — sample project installed successfully"

# ── build wheel + sdist ───────────────────────────────────────────────────────
.PHONY: build
build: $(STAMP)  ## Build wheel and sdist into dist/
	$(BIN)/python -m build --no-isolation
	@echo "Artifacts in dist/"

# ── runtime container image (offline — vendor/ must be present) ───────────────
.PHONY: build-image
build-image:  ## Build runtime container image offline (vendor/ required)
	podman build --network=none -t offliner:latest .
	@echo "Image built: offliner:latest"

# ── build-environment container image ─────────────────────────────────────────
# OS packages (make, git) require network; Python deps come from vendor/.
.PHONY: build-build-image
build-build-image:  ## Build the build-environment image (needs network for OS packages)
	podman build -f Containerfile.build -t offliner-build:latest .
	@echo "Image built: offliner-build:latest"

.PHONY: check-in-container
check-in-container: build-build-image  ## Run make check inside the build container
	podman run --rm offliner-build:latest

.PHONY: build-in-container
build-in-container: build-build-image  ## Build the wheel inside the container; artifacts written to ./dist
	mkdir -p dist
	podman run --rm \
		-v "$(CURDIR)/dist:/build/dist:Z" \
		offliner-build:latest \
		make build
	@echo "Artifacts written to dist/"

# ── clean ─────────────────────────────────────────────────────────────────────
.PHONY: clean
clean:  ## Remove build artifacts and caches
	rm -rf dist/ build/ .eggs/ .coverage htmlcov/ .pytest_cache/ .mypy_cache/ .ruff_cache/
	find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
	find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true

.PHONY: clean-venv
clean-venv:  ## Remove .venv (forces full reinstall on next make target)
	rm -rf $(VENV)

.PHONY: clean-vendor
clean-vendor:  ## Remove vendor/ (re-run 'make vendor' on a networked machine to restore)
	rm -rf $(VENDOR_DIR) vendor.tar.gz
	@echo "vendor/ removed. Run 'make vendor' on a networked machine to restore."
