# ATCR Makefile
# Build targets for the ATProto Container Registry

.PHONY: all build build-appview build-hold build-credential-helper build-oauth-helper \
	build-trixie \
	generate test test-billing test-race test-verbose integration-test stripe-integration-test \
	lint lex-lint clean help install-credential-helper \
	develop develop-detached develop-down dev \
	docker docker-appview docker-hold docker-scanner

.DEFAULT_GOAL := help

help: ## Show this help message
	@echo "ATCR Build Targets:"
	@echo ""
	@awk 'BEGIN {FS = ":.*##"; printf ""} /^[a-zA-Z_-]+:.*?##/ { printf "  \033[36m%-28s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

all: generate build ## Generate assets and build all binaries (default)

# Generated asset files
GENERATED_ASSETS = \
	pkg/appview/public/js/htmx.min.js \
	pkg/appview/public/js/lucide.min.js \
	pkg/appview/licenses/spdx-licenses.json

generate: ## Run go generate ./... (always — regenerates cbor_gen, icon sprites, vendor assets)
	@echo "→ Running go generate ./..."
	go generate ./...

# File rule: lazily download missing vendor assets for fast incremental local builds.
# Production builds depend on the phony `generate` target instead so generated code
# (cbor_gen.go, icon sprites, etc.) is always up to date.
$(GENERATED_ASSETS):
	@echo "→ Generating vendor assets and code..."
	go generate ./...

##@ Build Targets

build: build-appview build-hold build-credential-helper ## Build all binaries

# Legal page "Last updated" dates come from the git commit date of the page
# templates. Empty values (e.g., Docker builds without .git) fall back to the
# hardcoded default in legal.go.
LEGAL_PKG := atcr.io/pkg/appview/handlers
PRIVACY_DATE := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/privacy.html 2>/dev/null)
TERMS_DATE   := $(shell git log -1 --format=%cs -- pkg/appview/templates/pages/terms.html 2>/dev/null)
APPVIEW_LDFLAGS := -X '$(LEGAL_PKG).privacyLastUpdated=$(PRIVACY_DATE)' -X '$(LEGAL_PKG).termsLastUpdated=$(TERMS_DATE)'

build-appview: $(GENERATED_ASSETS) ## Build appview binary only
	@echo "→ Building appview..."
	@mkdir -p bin
	go build -ldflags="$(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview

build-hold: $(GENERATED_ASSETS) ## Build hold binary only
	@echo "→ Building hold..."
	@mkdir -p bin
	go build -o bin/atcr-hold ./cmd/hold

build-credential-helper: ## Build credential helper only (atcr brand)
	@echo "→ Building credential helper..."
	@mkdir -p bin
	cd cmd/credential-helper/atcr && go build -ldflags="-X main.version=$(shell git describe --tags --always 2>/dev/null || echo dev) -X main.commit=$(shell git rev-parse HEAD 2>/dev/null || echo none)" -o ../../../bin/docker-credential-atcr .

build-oauth-helper: ## Build OAuth helper only
	@echo "→ Building OAuth helper..."
	@mkdir -p bin
	go build -o bin/oauth-helper ./cmd/oauth-helper

# Trixie cross-build (Debian 13, glibc 2.41) — produces binaries that run on
# any glibc ≥ 2.41 target, even when the host glibc is newer (e.g. Fedora's
# 2.43, which otherwise stamps sqrtf@GLIBC_2.43 onto cgo-linked output).
TRIXIE_BUILDER_IMAGE ?= golang:1-trixie

# Billing is OFF by default, and that default is load-bearing.
#
# pkg/billing sits behind the `billing` build tag, so the tag decides whether a
# deployment has Stripe compiled in at all. This target used to hardcode it,
# which meant the only supported way to build a non-billing appview was to
# bypass the Makefile and drive the container by hand — and every deploy that
# forgot to would silently switch billing on for a deployment whose operator
# had chosen to run without it.
#
# Opt in explicitly instead:
#
#   make build-trixie              # no billing (default)
#   make build-trixie BILLING=1    # billing compiled in
#
# Only the appview is affected; hold, scanner, labeler and the credential
# helper never reference pkg/billing. Verify what you built with:
#
#   strings -a bin/atcr-appview | grep -c stripe-go     # 0 when billing is off
BILLING ?=
ifeq ($(BILLING),)
APPVIEW_TAGS :=
else
APPVIEW_TAGS := -tags billing
endif

build-trixie: generate ## Build all production binaries for linux/amd64 in a Debian 13 (glibc 2.41) container. Billing is off unless BILLING=1.
	@echo "→ Building in $(TRIXIE_BUILDER_IMAGE) for glibc 2.41 compatibility..."
	@mkdir -p bin
	docker run --rm \
		--user $$(id -u):$$(id -g) \
		-v "$(CURDIR)":/src \
		-w /src \
		-e HOME=/tmp \
		-e GOCACHE=/tmp/.gocache \
		-e GOMODCACHE=/tmp/.gomodcache \
		-e CGO_ENABLED=1 \
		-e GOOS=linux \
		-e GOARCH=amd64 \
		$(TRIXIE_BUILDER_IMAGE) \
		bash -c '\
			set -e && \
			go build -trimpath $(APPVIEW_TAGS) -ldflags="-s -w $(APPVIEW_LDFLAGS)" -o bin/atcr-appview ./cmd/appview && \
			go build -trimpath -ldflags="-s -w" -o bin/atcr-hold ./cmd/hold && \
			(cd cmd/credential-helper/atcr && go build -trimpath -ldflags="-s -w" -o ../../../bin/docker-credential-atcr .) && \
			go build -trimpath -ldflags="-s -w" -o bin/atcr-labeler ./cmd/labeler && \
			cd scanner && go build -trimpath -ldflags="-s -w" -o ../bin/atcr-scanner ./cmd/scanner'
	@echo "✓ Built to bin/ (glibc ≥ 2.41 compatible), appview billing: $(if $(BILLING),ENABLED,disabled)"

##@ Test Targets

# Every module in the workspace except the root. `go test ./...` only ever
# covers the module it is run from, so a workspace module needs its own
# invocation or its tests never run at all. That is how the scanner's suite sat
# dark: 17 test files, none of them reached by `make test`.
#
# Derived from go.work rather than listed by hand, so adding a module to the
# workspace is enough to get it tested. Falls back to the known set if the
# go tool cannot answer, so a broken workspace fails loudly in the test run
# rather than silently testing less.
GO_SUBMODULE_DIRS := $(shell go list -m -f '{{.Dir}}' 2>/dev/null | tail -n +2)
ifeq ($(strip $(GO_SUBMODULE_DIRS)),)
GO_SUBMODULE_DIRS := scanner cmd/credential-helper/atcr cmd/credential-helper/seamark deploy/upcloud
endif

# $(call test-submodules,<go test flags>)
define test-submodules
	@for dir in $(GO_SUBMODULE_DIRS); do \
		echo "→ Testing module $$(basename $$dir)..."; \
		(cd $$dir && go test $(1) ./...) || exit 1; \
	done
endef

# Tests run as a `testmode` build. The tag compiles pkg/atproto/indigo_local.go
# in place of indigo_prod.go, which is what lets a did:web on a loopback port
# (every test server, the integration harness, docker-compose) resolve over
# plain HTTP. Production binaries never carry the tag; there is no runtime
# switch. Tests that need it fail fast with a message naming the tag.
TEST_TAGS := testmode

test: test-billing ## Run all tests (every workspace module)
	@echo "→ Running tests..."
	go test -tags $(TEST_TAGS) -cover ./...
	$(call test-submodules,-tags $(TEST_TAGS) -cover)

# pkg/billing is behind the `billing` build tag, so `go test ./...` never
# compiles it, let alone runs it. Its tests covered the money path and had
# never executed in this target or in CI.
test-billing: ## Run the billing-tagged tests (skipped by plain `go test ./...`)
	@echo "→ Running billing-tagged tests..."
	go test -tags billing,$(TEST_TAGS) -cover ./pkg/billing/...

test-race: ## Run tests with race detector (every workspace module)
	@echo "→ Running tests with race detector..."
	go test -race -tags $(TEST_TAGS) ./...
	@echo "→ Running billing-tagged tests with race detector..."
	go test -race -tags billing,$(TEST_TAGS) ./pkg/billing/...
	$(call test-submodules,-race -tags $(TEST_TAGS))

test-verbose: ## Run tests with verbose output (every workspace module)
	@echo "→ Running tests with verbose output..."
	go test -v -tags $(TEST_TAGS) ./...
	$(call test-submodules,-v -tags $(TEST_TAGS))

integration-test: ## Run in-process smoke test (no docker, fake PDS + gofakes3 + hold + appview)
	@echo "→ Running integration smoke test..."
	go test -tags=integration,$(TEST_TAGS) -count=1 -race -timeout=120s ./test/integration/...

stripe-integration-test: ## Run Stripe sandbox-backed billing tests (needs STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_TEST_PRICE_MONTHLY, STRIPE_TEST_PRICE_YEARLY)
	@echo "→ Running Stripe sandbox integration tests..."
	@echo "  Required env: STRIPE_SECRET_KEY (sk_test_...), STRIPE_WEBHOOK_SECRET (whsec_...),"
	@echo "                STRIPE_TEST_PRICE_MONTHLY, STRIPE_TEST_PRICE_YEARLY"
	@echo "  Optional env: STRIPE_TEST_TIER_NAME (default 'Supporter'),"
	@echo "                STRIPE_TEST_EXISTING_CUSTOMER_DID (skips portal search-lag wait)"
	go test -tags="billing stripe_integration $(TEST_TAGS)" -count=1 -timeout=180s ./test/stripe-integration/...

##@ Quality Targets

.PHONY: check-golangci-lint
check-golangci-lint:
	@LINT_PKG=github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest; \
	CUR_GO=$$(go version | grep -oE 'go[0-9]+\.[0-9]+' | head -1 | sed 's/^go//'); \
	if ! command -v golangci-lint > /dev/null 2>&1; then \
		echo "→ Installing golangci-lint..."; \
		go install $$LINT_PKG; \
	else \
		LINT_GO=$$(golangci-lint --version 2>&1 | grep -oE 'built with go[0-9]+\.[0-9]+' | head -1 | sed 's/^built with go//'); \
		if [ -n "$$LINT_GO" ] && [ "$$LINT_GO" != "$$CUR_GO" ] && \
		   [ "$$(printf '%s\n%s\n' $$LINT_GO $$CUR_GO | sort -V | head -1)" = "$$LINT_GO" ]; then \
			echo "→ golangci-lint built with go$$LINT_GO but project targets go$$CUR_GO — reinstalling..."; \
			go install $$LINT_PKG; \
		fi; \
	fi

lint: check-golangci-lint ## Run golangci-lint
	@echo "→ Running golangci-lint..."
	golangci-lint run ./...
	@echo "→ Running golangci-lint (billing tag)..."
	golangci-lint run --build-tags=billing ./pkg/billing/...
	@echo "→ Running golangci-lint (testmode tag)..."
	golangci-lint run --build-tags=testmode ./...
	@echo "→ Running golangci-lint (integration tags)..."
	golangci-lint run --build-tags=integration,testmode ./test/integration/...
	golangci-lint run --build-tags=billing,stripe_integration,testmode ./test/stripe-integration/...

lex-lint: ## Lint ATProto lexicon schemas
	goat lex lint ./lexicons/

##@ Install Targets

install-credential-helper: build-credential-helper ## Install credential helper to /usr/local/sbin
	@echo "→ Installing credential helper to /usr/local/sbin..."
	install -m 755 bin/docker-credential-atcr /usr/local/sbin/docker-credential-atcr
	@echo "✓ Installed docker-credential-atcr to /usr/local/sbin/"

##@ Development Targets

# Air's build command appends $GO_TAGS to its -tags list; a local run needs
# `testmode` for the same reason the tests do (see TEST_TAGS above).
dev: export GO_TAGS ?= testmode
dev: $(GENERATED_ASSETS) ## Run AppView locally with Air hot reload (testmode build)
	@which air > /dev/null || (echo "→ Installing Air..." && go install github.com/air-verse/air@latest)
	air -c .air.toml

##@ Docker Targets

docker: docker-appview docker-hold docker-scanner ## Build all Docker images

docker-appview: ## Build appview Docker image
	@echo "→ Building appview Docker image..."
	docker build -f Dockerfile.appview \
		--build-arg PRIVACY_DATE=$(PRIVACY_DATE) \
		--build-arg TERMS_DATE=$(TERMS_DATE) \
		-t atcr.io/atcr.io/appview:latest .

docker-hold: ## Build hold Docker image
	@echo "→ Building hold Docker image..."
	docker build -f Dockerfile.hold -t atcr.io/atcr.io/hold:latest .

docker-scanner: ## Build scanner Docker image
	@echo "→ Building scanner Docker image..."
	docker build -f Dockerfile.scanner -t atcr.io/atcr.io/scanner:latest .

develop: ## Build and start docker-compose with Air hot reload
	@echo "→ Building Docker images..."
	docker-compose build
	@echo "→ Starting docker-compose with hot reload..."
	docker-compose up

develop-detached: ## Build and start docker-compose with hot reload (detached)
	@echo "→ Building Docker images..."
	docker-compose build
	@echo "→ Starting docker-compose with hot reload (detached)..."
	docker-compose up -d
	@echo "✓ Services started in background with hot reload"
	@echo "  AppView: http://localhost:5000"
	@echo "  Hold:    http://localhost:8080"

develop-down: ## Stop docker-compose services
	@echo "→ Stopping docker-compose..."
	docker-compose down

##@ Utility Targets

clean: ## Remove built binaries and generated assets
	@echo "→ Cleaning build artifacts..."
	rm -rf bin/
	rm -f pkg/appview/licenses/spdx-licenses.json
	@echo "✓ Clean complete"
