.PHONY: bench-fleet bench-ops build run install install-user uninstall uninstall-user clean dev release-ready release-local dist-local test test-perf bench fmt lint ci css tools css-verify test-web test-web-unit test-web-e2e test-web-install goldens-update verify-docker

BINARY_NAME=agent-deck
BUILD_DIR=./build
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null | sed 's/^v//' || echo "dev")
LDFLAGS=-ldflags "-X main.Version=$(VERSION)"

# Tailwind v4 standalone CLI (PERF-01)
TAILWIND_VERSION=v4.2.2
# Keep in step with .github/workflows/golangci-lint.yml `version:`.
GOLANGCI_LINT_VERSION=v2.14.0
TAILWIND_BIN=$(HOME)/.local/bin/tailwindcss

# Keep local and CI builds on the version required by go.mod.
export GOTOOLCHAIN=go1.26.10

# Build the binary (requires compiled CSS via `make css`)
build: css
	go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/agent-deck

# Download the pinned Tailwind v4 standalone CLI binary if missing or wrong version
tools:
	@mkdir -p $(HOME)/.local/bin
	@OS=$$(uname -s | tr '[:upper:]' '[:lower:]'); \
	ARCH=$$(uname -m); \
	case "$$OS-$$ARCH" in \
		linux-x86_64)  ASSET=tailwindcss-linux-x64 ;; \
		linux-aarch64) ASSET=tailwindcss-linux-arm64 ;; \
		darwin-arm64)  ASSET=tailwindcss-macos-arm64 ;; \
		darwin-x86_64) ASSET=tailwindcss-macos-x64 ;; \
		*) echo "ERROR: unsupported host $$OS-$$ARCH for Tailwind binary" && exit 1 ;; \
	esac; \
	if [ -x "$(TAILWIND_BIN)" ] && "$(TAILWIND_BIN)" --help 2>&1 | grep -q "$(TAILWIND_VERSION)"; then \
		echo "tailwindcss $(TAILWIND_VERSION) already installed at $(TAILWIND_BIN)"; \
	else \
		echo "Downloading tailwindcss $(TAILWIND_VERSION) ($$ASSET) ..."; \
		curl -sSL -o "$(TAILWIND_BIN)" \
			"https://github.com/tailwindlabs/tailwindcss/releases/download/$(TAILWIND_VERSION)/$$ASSET"; \
		chmod +x "$(TAILWIND_BIN)"; \
		echo "Installed: $$( "$(TAILWIND_BIN)" --help 2>&1 | head -1 )"; \
	fi

# Compile Tailwind CSS (PERF-01 — replaces vendor/tailwind.js Play CDN runtime)
# Runs the brute-force diff gate (Pitfall #1 mitigation): generate twice and
# diff. Fails on any class present in brute-force output but missing from
# targeted globs unless explicitly allowlisted.
#
# Brute-force gate details:
#   - The brute src CSS must be semantically identical to styles.src.css
#     (same @theme, @variant dark, folded rules) so Tailwind can RECOGNISE
#     tn-* / sp-* / dark:* utilities. Without those, the diff is meaningless.
#   - Tailwind v4 resolves @source paths relative to the CSS source file's
#     directory, so the brute src MUST live inside internal/web/static/.
#     We copy styles.src.css to a hidden sibling and append a broader
#     @source glob. Any class the broader scan picks up that the targeted
#     scan missed will show up in the diff.
#   - The hidden copy is removed on success AND failure (trap).
css: tools
	@echo "==> Compiling Tailwind CSS (targeted globs)"
	$(TAILWIND_BIN) -i ./internal/web/static/styles.src.css \
		-o ./internal/web/static/styles.css \
		--minify
	@echo "==> Brute-force globbing diff (Pitfall #1 gate)"
	@trap 'rm -f ./internal/web/static/.brute-tw.src.css' EXIT INT TERM; \
	cp ./internal/web/static/styles.src.css ./internal/web/static/.brute-tw.src.css; \
	printf '\n/* --- brute-force additional @source (Pitfall #1 gate) --- */\n@source "./**/*.{js,mjs,html}";\n@source not "./vendor/**";\n@source not "./chart.umd.min.js";\n@source not "./sw.js";\n' \
		>> ./internal/web/static/.brute-tw.src.css; \
	$(TAILWIND_BIN) -i ./internal/web/static/.brute-tw.src.css \
		-o /tmp/agent-deck-tw-brute.css \
		--minify; \
	rm -f ./internal/web/static/.brute-tw.src.css; \
	if ! diff -q ./internal/web/static/styles.css /tmp/agent-deck-tw-brute.css >/dev/null 2>&1; then \
		if [ -f ./internal/web/static/.tailwind-allowlist.txt ]; then \
			echo "Brute-force diff non-empty but allowlist file present; review manually."; \
		else \
			echo "ERROR: brute-force @source diff non-empty (Pitfall #1). Add classes to internal/web/static/.tailwind-allowlist.txt or fix @source globs in styles.src.css." >&2; \
			diff ./internal/web/static/styles.css /tmp/agent-deck-tw-brute.css | head -40; \
			exit 1; \
		fi; \
	fi
	@SIZE=$$(gzip -c ./internal/web/static/styles.css | wc -c); \
	echo "==> Compiled styles.css gzipped size: $$SIZE bytes"; \
	if [ "$$SIZE" -gt 10240 ]; then \
		echo "ERROR: gzipped size $$SIZE exceeds 10240-byte sanity ceiling"; \
		exit 1; \
	fi

# Drift gate: regenerate CSS and fail if committed file differs from generated
css-verify: css
	@git diff --exit-code -- ./internal/web/static/styles.css || \
		(echo "ERROR: internal/web/static/styles.css drifted from generated output. Run 'make css' and commit." && exit 1)

# Run in development
run:
	go run ./cmd/agent-deck

# Install to /usr/local/bin (requires sudo)
install: build
	sudo rm -f /usr/local/bin/$(BINARY_NAME)
	sudo cp $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/$(BINARY_NAME)
	@# macOS (Apple Silicon) SIGKILLs a binary whose ad-hoc signature was
	@# invalidated by an in-place overwrite. rm+cp gives a fresh inode and the
	@# re-sign guarantees a valid signature. No-op on other platforms.
	@if [ "$$(uname)" = "Darwin" ]; then sudo codesign --force --sign - /usr/local/bin/$(BINARY_NAME); fi
	@echo "✅ Installed to /usr/local/bin/$(BINARY_NAME)"
	@echo "Run 'agent-deck' to start"

# Install to user's local bin (no sudo required)
install-user: build
	mkdir -p $(HOME)/.local/bin
	rm -f $(HOME)/.local/bin/$(BINARY_NAME)
	cp $(BUILD_DIR)/$(BINARY_NAME) $(HOME)/.local/bin/$(BINARY_NAME)
	@# macOS (Apple Silicon) SIGKILLs a binary whose ad-hoc signature was
	@# invalidated by an in-place overwrite. rm+cp gives a fresh inode and the
	@# re-sign guarantees a valid signature. No-op on other platforms.
	@if [ "$$(uname)" = "Darwin" ]; then codesign --force --sign - $(HOME)/.local/bin/$(BINARY_NAME); fi
	@echo "✅ Installed to $(HOME)/.local/bin/$(BINARY_NAME)"
	@echo "Make sure $(HOME)/.local/bin is in your PATH"
	@echo "Run 'agent-deck' to start"

# Uninstall from /usr/local/bin
uninstall:
	sudo rm -f /usr/local/bin/$(BINARY_NAME)
	@echo "✅ Uninstalled $(BINARY_NAME)"

# Uninstall from user's local bin
uninstall-user:
	rm -f $(HOME)/.local/bin/$(BINARY_NAME)
	@echo "✅ Uninstalled $(BINARY_NAME)"

# Clean build artifacts
clean:
	rm -rf $(BUILD_DIR)
	go clean

# Development with auto-reload
dev:
	@which air > /dev/null || go install github.com/air-verse/air@latest
	$(shell go env GOPATH)/bin/air

# Run tests (with race detector)
test:
	go test -race -v ./...

# Regenerate the CLI/storage-bytes behaviour-freeze goldens under
# cmd/agent-deck/testdata/goldens/ after a reviewed, intended behaviour
# change. Never run this to make a red suite green
# without reading the diff first — see testdata/goldens/README.md.
goldens-update:
	AGENTDECK_UPDATE_GOLDENS=1 go test ./cmd/agent-deck/ -run 'TestCLIGoldens$$|TestStorageBytesGoldens$$' -v

# Run hard-gated walltime regression tests (Track B). Honors PERF_BUDGET_MULTIPLIER
# (default 1.0 locally; CI sets 2.0). See docs/perf-budget-suite.md.
#
# ./... (not just ./cmd/agent-deck/...) so Tier 1 TestPerf_* added in any
# package — internal/statedb, internal/session — are exercised locally, matching
# the perf-smoke.yml CI gate which already runs the whole module.
test-perf:
	PERF_BUDGET_MULTIPLIER=$${PERF_BUDGET_MULTIPLIER:-1.0} \
		go test -run '^TestPerf_' -race -v -count=1 -timeout 120s \
		./...

# Run advisory benchmarks (Track A). No -race — race overhead distorts ns/op.
# Output is for trending; not a CI gate.
bench:
	go test -run '^$$' -bench '^Benchmark' -benchmem -benchtime=1x -count=3 -timeout 5m \
		./cmd/agent-deck/... ./internal/tmux/...

# Operation benchmarks (BenchmarkOp_*) on a ref, recorded in bench/ops/results
# and rendered into docs/performance.md. BENCH_REF=. means this checkout.
#   make bench-ops BENCH_LABEL=1.16.31 BENCH_REF=release/1.16.31
BENCH_LABEL ?= local
BENCH_REF ?= .
bench-ops:
	./bench/ops/run.sh $(BENCH_LABEL) $(BENCH_REF) $(BENCH_OPS_ARGS)

# Isolated synthetic fleet suite, including a private tmux server (Docker only).
bench-fleet:
	./bench/run-docker.sh $(BENCH_ARGS)

# Format code
fmt:
	go fmt ./...

# Lint
lint:
	go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) run

# Reproduce the CI Go checks in Docker: gofmt, build, vet, golangci-lint and
# the gotestsum race suite, with versions read from go.mod, this Makefile and
# go-test.yml. Pass VERIFY_ARGS, e.g.
#   make verify-docker VERIFY_ARGS="--stages test --pkgs ./internal/session/..."
verify-docker:
	./scripts/verify-docker.sh $(VERIFY_ARGS)

# Run local CI checks (same as pre-push hook: lint + test + build in parallel)
ci:
	@which lefthook > /dev/null || (echo "ERROR: lefthook not found. Run: brew install lefthook" && exit 1)
	lefthook run pre-push --force --no-auto-install

# Run before pushing a release tag: release gap, Version bump, soak of
# update/deploy changes, and the update path end to end. Pass flags with
# READY_ARGS, e.g. make release-ready READY_ARGS="--fixes v1.16.30".
release-ready:
	./scripts/release-readiness.sh $(READY_ARGS)

# Local release using GoReleaser
# Prerequisites: brew install goreleaser
# Required env: GITHUB_TOKEN
# HOMEBREW_TAP_GITHUB_TOKEN is NOT needed: with brews.skip_upload GoReleaser only
# generates dist/homebrew/Formula/agent-deck.rb and never touches the tap. CI's
# post-publish step owns the push (#1763), so a local release does not update the
# tap at all.
release-local:
	@echo "=== Pre-flight checks ==="
	@which goreleaser > /dev/null || (echo "ERROR: goreleaser not found. Run: brew install goreleaser" && exit 1)
	@test -n "$$GITHUB_TOKEN" || (echo "ERROR: GITHUB_TOKEN not set" && exit 1)
	@TAG=$$(git describe --tags --exact-match 2>/dev/null) || (echo "ERROR: HEAD is not tagged. Run: git tag vX.Y.Z" && exit 1); \
	CODE_VERSION=$$(grep 'var Version' cmd/agent-deck/main.go | sed 's/.*"\(.*\)".*/\1/'); \
	TAG_VERSION=$${TAG#v}; \
	if [ "$$TAG_VERSION" != "$$CODE_VERSION" ]; then \
		echo "ERROR: Tag $$TAG ($$TAG_VERSION) != code Version $$CODE_VERSION"; \
		exit 1; \
	fi; \
	echo "Version: $$CODE_VERSION"
	@echo "=== Running tests ==="
	go test -race ./...
	@echo "=== Running GoReleaser ==="
	goreleaser release --clean
	@echo "=== Draft release built ==="
	@echo "Assets are attached to a DRAFT (.goreleaser.yml release.draft, #1759)."
	@echo "Verify the assets, then publish it yourself — CI's publish step does not run for a local release:"
	@echo "  gh release view $$(git describe --tags --exact-match) --repo asheshgoplani/agent-deck"
	@echo "  gh release edit $$(git describe --tags --exact-match) --repo asheshgoplani/agent-deck --draft=false --latest"
	@echo "Note: no SLSA provenance and no Homebrew tap update happen for a local release —"
	@echo "both live in .github/workflows/release.yml. Prefer pushing the tag and letting CI do it."

# Web UI test targets
# Vitest (unit) + Playwright (e2e + screenshot regression). Both run against
# the in-memory web fixture binary at tests/web/fixtures/cmd/web-fixture/.
# See documentation/webui-overhaul-plan.md for the parity strategy.

# One-shot install for fresh clones / CI. Installs npm deps + chromium browser.
test-web-install:
	cd tests/web && npm install --no-audit --no-fund
	cd tests/web && npx playwright install --with-deps chromium

# Unit tests (Vitest, jsdom). Fast (<5s on warm cache).
test-web-unit:
	cd tests/web && npm run test:unit

# End-to-end tests (Playwright). Builds the fixture binary, boots it,
# runs every spec including screenshot regression.
test-web-e2e:
	cd tests/web && npm run test:e2e

# Full suite (default): unit + e2e.
test-web: test-web-unit test-web-e2e

# Build unpublished archives using the committed release assets.
# Override with: make dist-local DIST_VERSION=1.16.10+local.custom DIST_OUTPUT=/tmp/dist
DIST_VERSION ?=
DIST_OUTPUT ?= dist-local
dist-local:
	go run ./tools/dist-local -version "$(DIST_VERSION)" -output "$(DIST_OUTPUT)"

# Functional checks run in Docker locally. Native execution is CI-only.
FUNCCHECK_IMAGE ?= agentdeck-funccheck:go1.26.10
FUNCCHECK_BINARY ?=
FUNCCHECK_MODE ?= docker
.PHONY: check-functional funccheck-image
funccheck-image:
	@printf '%s\n' 'FROM golang:1.26.10-bookworm' \
		'RUN apt-get update -qq && apt-get install -y --no-install-recommends tmux git' \
		'WORKDIR /deps' 'COPY go.mod go.sum ./' \
		'RUN go mod download && chmod -R a+rX /go/pkg/mod' \
		'RUN mkdir -p /funccheck-cache && chown 1000:1000 /funccheck-cache' \
		'ENV GOTOOLCHAIN=local' | docker build -t "$(FUNCCHECK_IMAGE)" -f - .

ifeq ($(FUNCCHECK_MODE),native)
check-functional:
	@test "$$CI" = true || { echo 'Native functional checks are CI-only. Use the default Docker mode locally.' >&2; exit 1; }
	@mkdir -p build
	@if [ -z "$(FUNCCHECK_BINARY)" ]; then go build $(LDFLAGS) -o build/agent-deck-funccheck ./cmd/agent-deck; fi
	GOMODCACHE="$$(go env GOMODCACHE)" GOCACHE="$$(go env GOCACHE)" FUNCCHECK_SOURCE_CHECKS=1 go run ./tools/funccheck "$(or $(FUNCCHECK_BINARY),build/agent-deck-funccheck)"
else
check-functional: funccheck-image
	docker run --rm --init -u 1000:1000 --network none --cap-drop ALL \
		-v "$(CURDIR):/src" -w /src -e HOME=/tmp/funccheck-home \
		-v agentdeck-funccheck-gocache:/funccheck-cache \
		-e GOCACHE=/funccheck-cache -e GOMODCACHE=/go/pkg/mod -e GOTOOLCHAIN=local \
		-e AGENTDECK_SKIP_UPDATE_CHECK=1 -e FUNCCHECK_SOURCE_CHECKS=1 -e GOFLAGS=-buildvcs=false \
		-e FUNCCHECK_BINARY="$(FUNCCHECK_BINARY)" "$(FUNCCHECK_IMAGE)" \
		sh -ec 'mkdir -p "$$HOME"; if [ -z "$$FUNCCHECK_BINARY" ]; then go build $(LDFLAGS) -o /tmp/agent-deck-funccheck ./cmd/agent-deck; FUNCCHECK_BINARY=/tmp/agent-deck-funccheck; fi; go run ./tools/funccheck "$$FUNCCHECK_BINARY"'
endif

# Visual check: drives a real agent-deck
# binary through every TUI screen in a private tmux server, diffs captured
# frames against tools/visualcheck/testdata/golden, writes contact-sheet.html.
# Needs Linux + a real tmux (see tools/visualcheck/README.md); run on the
# g14 test box, not natively on macOS.
VISUALCHECK_BINARY ?=
.PHONY: visual-check visual-check-golden
visual-check:
	@if [ -z "$(VISUALCHECK_BINARY)" ]; then go build $(LDFLAGS) -o build/agent-deck ./cmd/agent-deck; fi
	go run ./tools/visualcheck "$(or $(VISUALCHECK_BINARY),build/agent-deck)"

visual-check-golden:
	@if [ -z "$(VISUALCHECK_BINARY)" ]; then go build $(LDFLAGS) -o build/agent-deck ./cmd/agent-deck; fi
	UPDATE_GOLDEN=1 go run ./tools/visualcheck "$(or $(VISUALCHECK_BINARY),build/agent-deck)"
