# Go Bananas!!
#
# The Makefile is the primary interface to this repository. Every task a developer
# or a CI job needs is a target here, and each target is safe to run repeatedly.
#
# Conventions:
#   * `make help` lists everything.
#   * Targets are grouped by phase and the phases run in order for `make all`.
#   * Anything that changes the machine (installing a tool, pushing an image) is
#     marked ##@destructive so it is obvious in `make help`.
#   * Variables at the top are overridable: `make IMAGE_TAG=v2 build`.
#
# The `##@` markers are read by the self-documenting help target, which is why
# they are inline comments rather than a separate block.

SHELL := /usr/bin/env bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
.DELETE_ON_ERROR:
.SUFFIXES:

# --------------------------------------------------------------- variables ----

BINARY      := go-bananas
CMD         := ./cmd/go-bananas
BIN_DIR     := bin
DIST_DIR    := dist
WEB_DIR     := web
SDK_DIR     := sdk

VERSION     ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
COMMIT      ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BUILD_DATE  ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

REGISTRY    ?= nexus.urpi.be/docker-hosted
IMAGE_NAME  ?= $(REGISTRY)/$(BINARY)
IMAGE_TAG   ?= $(VERSION)
PLATFORM    ?= linux/amd64,linux/arm64

GO          ?= go
NPM         ?= npm

# Development tool versions, pinned so that every contributor and every CI run use
# the same linter. They are pinned here rather than in a separate tools module
# because `go install pkg@version` resolves the version itself and refuses a
# -modfile, which would make a tools.mod decorative rather than binding. `make
# tools-upgrade` bumps all three in one place.
AIR_VERSION ?= v1.67.4
GOLANGCI_VERSION ?= v2.14.0
SQLC_VERSION ?= v1.31.1
HELM        ?= helm
DOCKER      ?= docker

# Module cache and build cache live outside the tree so that a bind-mounted
# working directory does not slowly fill the developer's disk.
GO_CACHE    ?= $(CURDIR)/.cache/go-build
GOMODCACHE  ?= $(CURDIR)/.cache/go-mod

# The Go packages belonging to this module, excluding the JavaScript trees.
#
# Go does not ignore node_modules: any directory under the module root with Go
# files in it is part of `./...`, and a dependency that happens to ship a stray
# package would then be compiled and linted as if it were ours. `./internal/...`
# and `./cmd/...` are listed instead of filtering, so the omission is visible here
# rather than hidden in a pipe.
GO_PACKAGES = ./cmd/... ./internal/...

# The full set of ldflags. Kept in one variable so that `go build`, `go install`
# and goreleaser cannot drift apart and produce differently-identified binaries.
LDFLAGS := -s -w \
	-X github.com/urpi/go-bananas/internal/app.Version=$(VERSION) \
	-X github.com/urpi/go-bananas/internal/app.Commit=$(COMMIT) \
	-X github.com/urpi/go-bananas/internal/app.BuildDate=$(BUILD_DATE)

GOFLAGS_BUILD := -trimpath -ldflags "$(LDFLAGS)"

.DEFAULT_COLOR := \033[0m
.BOLD := \033[1m
.DIM := \033[2m
.GREEN := \033[32m
.YELLOW := \033[33m
.RED := \033[31m

##@ General

.PHONY: help
help: ## Show this help
	@printf "$(BOLD)go-bananas$(DEFAULT_COLOR) — runtime design-token service\n\n"
	@printf "$(DIM)Version$(DEFAULT_COLOR)  $(VERSION) ($(COMMIT))\n\n"
	@# The awk pass reads both the "##@" group markers and the "##" target help in
	@# one scan so that groups appear in file order rather than in two separate
	@# alphabetical lists. A trailing "##@destructive" is stripped from the
	@# description and rendered as a marker instead.
	@awk 'BEGIN {FS = ":.*##"} \
		/^##@/ { printf "\n$(BOLD)%s$(DEFAULT_COLOR)\n", substr($$0, 5); next } \
		/^[a-zA-Z0-9_-]+:.*?##/ { \
			help = $$0; sub(/^[^:]*:.*## /, "", help); \
			mark = ""; \
			if (help ~ /##@destructive/) { mark = "$(YELLOW) [modifies this machine]$(DEFAULT_COLOR)"; \
				sub(/ *##@destructive/, "", help) } \
			printf "  $(BOLD)%-20s$(DEFAULT_COLOR) %s%s\n", $$1, help, mark \
		}' $(MAKEFILE_LIST)
	@printf "\n$(DIM)Override any variable, e.g. make IMAGE_TAG=v2.1.0 image-push$(DEFAULT_COLOR)\n"

.PHONY: version
version: ## Print the version that would be built
	@echo "$(VERSION) ($(COMMIT)) built $(BUILD_DATE)"

##@ Development

.PHONY: install
install: ## Install Go and Node dependencies
	$(GO) mod download
	cd $(WEB_DIR) && $(NPM) install
	cd $(SDK_DIR) && $(NPM) install

.PHONY: dev
dev: ## Run the API with hot reload (Ctrl-C to stop)
	@command -v air >/dev/null 2>&1 || { \
		echo "air is not installed; run 'make tools' or see docs/development.md"; exit 1; }
	@BANANAS_ENVIRONMENT=development \
	 BANANAS_AUTH_MODE=dev \
	 BANANAS_HTTP_ADDR=:8080 \
	 BANANAS_DATABASE_PATH=./data/go-bananas.db \
	 BANANAS_STATIC_DIR=$(WEB_DIR)/dist \
	 BANANAS_SDK_DIR=$(SDK_DIR)/dist \
	 air -c .air.toml

.PHONY: dev-api
dev-api: ## Run the API (no SPA); Vite serves the UI, this serves the runtime SDK
	@BANANAS_ENVIRONMENT=development \
	 BANANAS_AUTH_MODE=dev \
	 BANANAS_HTTP_ADDR=:8080 \
	 BANANAS_DATABASE_PATH=./data/go-bananas.db \
	 BANANAS_STATIC_DIR= \
	 BANANAS_SDK_DIR=$(SDK_DIR)/dist \
	 $(GO) run -ldflags "$(LDFLAGS)" $(CMD)

.PHONY: dev-web
dev-web: ## Run the Vite dev server for the admin UI, proxying the API
	cd $(WEB_DIR) && $(NPM) run dev

.PHONY: dev-all
dev-all: build-sdk ## Run the API and the frontend dev server together
	@echo "API on http://localhost:8080, admin UI on http://localhost:5173"
	@trap 'kill 0' EXIT; \
	 $(MAKE) dev-api & \
	 $(MAKE) dev-web & \
	 wait

.PHONY: tools
tools: ## Install the pinned development tools (air, golangci-lint, sqlc) ##@destructive
	$(GO) install github.com/air-verse/air@$(AIR_VERSION)
	$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_VERSION)
	$(GO) install github.com/sqlc-dev/sqlc/cmd/sqlc@$(SQLC_VERSION)
	@echo "Tools installed into $$( $(GO) env GOBIN )"
	@echo "golangci-lint $(GOLANGCI_VERSION), air $(AIR_VERSION), sqlc $(SQLC_VERSION)"

.PHONY: tools-upgrade
tools-upgrade: ## Bump the pinned development tools to their latest releases ##@destructive
	@for tool in \
		github.com/air-verse/air:AIR_VERSION \
		github.com/golangci/golangci-lint/v2:GOLANGCI_VERSION \
		github.com/sqlc-dev/sqlc:SQLC_VERSION; do \
		pkg=$${tool%%:*}; var=$${tool##*:}; \
		latest=$$($(GO) list -m -versions $$pkg | tr ' ' '\n' | tail -1); \
		[ -n "$$latest" ] || { echo "could not resolve $$pkg"; exit 1; }; \
		sed -i.bak "s|^$$var .*|$$var ?= $$latest|" Makefile && rm -f Makefile.bak; \
		echo "$$var -> $$latest"; \
	done
	@echo "Run 'make tools' to install them and commit the Makefile change."

.PHONY: clean
clean: ## Remove build output and local data
	rm -rf $(BIN_DIR) $(DIST_DIR) data .cache
	cd $(WEB_DIR) && rm -rf dist
	cd $(SDK_DIR) && rm -rf dist
	@echo "Cleaned."

.PHONY: clean-cache
clean-cache: ## Remove every build cache, including the module cache ##@destructive
	$(GO) clean -cache -modcache
	rm -rf .cache
	cd $(WEB_DIR) && rm -rf node_modules/.vite
	cd $(SDK_DIR) && rm -rf node_modules/.vite

##@ Build

.PHONY: build
build: build-go build-web build-sdk ## Build every artefact

.PHONY: build-go
build-go: ## Build the server binary into bin/
	@mkdir -p $(BIN_DIR)
	CGO_ENABLED=0 $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(BINARY) $(CMD)
	@echo "Built $(BIN_DIR)/$(BINARY)"

.PHONY: build-web
build-web: ## Build the admin UI into web/dist
	cd $(WEB_DIR) && $(NPM) run build

.PHONY: build-sdk
build-sdk: ## Build the runtime SDK bundles into sdk/dist
	cd $(SDK_DIR) && $(NPM) run build

.PHONY: run
run: build-go ## Build and run the server with the current assets
	@mkdir -p data
	BANANAS_ENVIRONMENT=development \
	BANANAS_AUTH_MODE=dev \
	BANANAS_STATIC_DIR=$(WEB_DIR)/dist \
	BANANAS_SDK_DIR=$(SDK_DIR)/dist \
	./$(BIN_DIR)/$(BINARY)

.PHONY: release
release: ## Build release binaries for every supported platform ##@destructive
	@command -v goreleaser >/dev/null 2>&1 || { \
		echo "goreleaser is not installed; see docs/development.md"; exit 1; }
	goreleaser release --clean

.PHONY: release-snapshot
release-snapshot: ## Build release binaries without publishing ##@destructive
	goreleaser release --snapshot --clean --skip=publish

.PHONY: sbom
sbom: ## Generate a CycloneDX SBOM for the current version ##@destructive
	@command -v syft >/dev/null 2>&1 || { \
		echo "syft is not installed"; exit 1; }
	syft dir:. -o cyclonedx-json=sbom-$(VERSION).json

##@ Quality

.PHONY: check
check: fmt-check lint test ## Run every check

.PHONY: fmt
fmt: ## Format the Go code
	$(GO) fmt ./...

.PHONY: fmt-check
fmt-check: ## Fail if the Go code is not formatted
	@unformatted="$$(gofmt -l . | grep -v '^vendor/' || true)"; \
	if [ -n "$$unformatted" ]; then \
		echo "These files are not gofmt'd:"; echo "$$unformatted"; exit 1; \
	fi
	@echo "Go formatting is clean."

.PHONY: lint
lint: lint-go lint-web lint-sdk ## Run every linter

.PHONY: lint-go
lint-go: ## Run golangci-lint
	@command -v golangci-lint >/dev/null 2>&1 || { \
		echo "golangci-lint is not installed; run 'make tools'"; exit 1; }
	golangci-lint run $(GO_PACKAGES)

.PHONY: lint-web
lint-web: ## Lint and typecheck the admin UI
	cd $(WEB_DIR) && $(NPM) run lint
	cd $(WEB_DIR) && $(NPM) run typecheck

.PHONY: lint-sdk
lint-sdk: ## Lint and typecheck the runtime SDK
	cd $(SDK_DIR) && $(NPM) run typecheck

.PHONY: fmt-web
fmt-web: ## Format the frontend and SDK sources
	cd $(WEB_DIR) && $(NPM) run format
	cd $(SDK_DIR) && $(NPM) exec prettier --write .

.PHONY: tidy
tidy: ## Tidy the Go module
	$(GO) mod tidy

##@ Test

.PHONY: test
test: test-go test-web test-sdk ## Run every test suite

.PHONY: test-go
test-go: ## Run the Go tests
	$(GO) test -race -count=1 $(GO_PACKAGES)

.PHONY: test-cover
test-cover: ## Run the Go tests and write a coverage profile
	$(GO) test -race -count=1 -coverprofile=coverage.out -covermode=atomic $(GO_PACKAGES)
	$(GO) tool cover -func=coverage.out | tail -1
	@echo "Profile written to coverage.out"

.PHONY: test-web
test-web: ## Run the admin UI tests
	cd $(WEB_DIR) && $(NPM) test

.PHONY: test-sdk
test-sdk: ## Run the runtime SDK tests
	cd $(SDK_DIR) && $(NPM) test

.PHONY: bench
bench: ## Run the token engine benchmarks
	$(GO) test -bench=. -benchmem ./internal/tokenengine/

##@ Docker

.PHONY: image
image: ## Build the container image for the host architecture
	$(DOCKER) buildx build \
		--build-arg VERSION=$(VERSION) \
		--build-arg COMMIT=$(COMMIT) \
		--build-arg BUILD_DATE=$(BUILD_DATE) \
		--tag $(IMAGE_NAME):$(IMAGE_TAG) \
		--tag $(IMAGE_NAME):latest \
		--load \
		.

.PHONY: image-multi
image-multi: ## Build the multi-architecture image without pushing ##@destructive
	$(DOCKER) buildx build \
		--platform $(PLATFORM) \
		--build-arg VERSION=$(VERSION) \
		--build-arg COMMIT=$(COMMIT) \
		--build-arg BUILD_DATE=$(BUILD_DATE) \
		--tag $(IMAGE_NAME):$(IMAGE_TAG) \
		.

.PHONY: image-push
image-push: ## Build and push the multi-architecture image ##@destructive
	$(DOCKER) buildx build \
		--platform $(PLATFORM) \
		--build-arg VERSION=$(VERSION) \
		--build-arg COMMIT=$(COMMIT) \
		--build-arg BUILD_DATE=$(BUILD_DATE) \
		--push \
		--tag $(IMAGE_NAME):$(IMAGE_TAG) \
		.

.PHONY: image-run
image-run: ## Run the image locally on port 8080
	$(DOCKER) run --rm -it \
		-p 8080:8080 \
		-e BANANAS_ENVIRONMENT=development \
		-e BANANAS_AUTH_MODE=dev \
		-v go-bananas-data:/srv/data \
		$(IMAGE_NAME):$(IMAGE_TAG)

.PHONY: compose-up
compose-up: ## Start the local stack with docker compose
	$(DOCKER) compose up --build

.PHONY: compose-down
compose-down: ## Stop the local stack ##@destructive
	$(DOCKER) compose down

.PHONY: compose-logs
compose-logs: ## Follow the local stack logs
	$(DOCKER) compose logs -f

.PHONY: compose-shell
compose-shell: ## Open a shell in the running service ##@destructive
	$(DOCKER) compose exec go-bananas /bin/sh

##@ Helm

.PHONY: helm-lint
helm-lint: ## Lint the chart
	$(HELM) lint deploy/helm/go-bananas --strict

.PHONY: helm-matrix
helm-matrix: ## Render the chart across a matrix of valid and invalid configurations
	./hack/helm-matrix.sh

.PHONY: helm-template
helm-template: ## Render the chart with the default values
	$(HELM) template go-bananas deploy/helm/go-bananas

.PHONY: helm-template-auth
helm-template-auth: ## Render the chart configured for OIDC
	$(HELM) template go-bananas deploy/helm/go-bananas \
		--set identity.mode=oidc \
		--set identity.oidc.issuer=https://auth.lcl-urpi.be/realms/urpi \
		--set identity.oidc.clientId=go-bananas \
		--set identity.oidc.generateSecret=false \
		--set identity.oidc.existingSecret=go-bananas-oidc \
		--set config.publicBaseUrl=https://themes-dev.example.com

.PHONY: helm-install
helm-install: ## Install the chart into the current kube context ##@destructive
	$(HELM) upgrade --install go-bananas deploy/helm/go-bananas \
		--namespace go-bananas --create-namespace \
		--values deploy/helm/go-bananas/values.yaml \
		--wait --timeout 5m

.PHONY: helm-uninstall
helm-uninstall: ## Remove the release ##@destructive
	$(HELM) uninstall go-bananas --namespace go-bananas

##@ Local (lcl)

# The lcl environment is the developer's local k3s cluster with a registry at
# localhost:5000 and a TLS edge serving *.intern-urpi.be. go-* apps deploy into the
# shared `go-insane-lcl` namespace. These targets use values-lcl.yaml.
#
#   KUBECONFIG — the lcl kubeconfig. On this box the `local` kubecontext goes through a
#     Rancher proxy that can 502; the direct k3s config is /etc/rancher/k3s/k3s.yaml.
#     Set it to deploy without depending on the proxy: KUBECONFIG=/etc/rancher/k3s/k3s.yaml make deploy-lcl
#   LCL_TAG   — image tag. Defaults to a timestamp so a rebuild is always re-pulled;
#     `IfNotPresent` would otherwise serve a stale `0.0.1` from the node cache.

LCL_REGISTRY ?= localhost:5000
LCL_REPO     ?= go-bananas
LCL_TAG      ?= $(shell date -u +%Y%m%d%H%M%S)
LCL_NAMESPACE ?= go-insane-lcl
LCL_VALUES    = deploy/helm/go-bananas/values-lcl.yaml

.PHONY: image-lcl
image-lcl: ## Build and push the image to the local registry ##@destructive
	$(DOCKER) buildx build \
		--build-arg VERSION=$(VERSION) \
		--build-arg COMMIT=$(COMMIT) \
		--build-arg BUILD_DATE=$(BUILD_DATE) \
		--tag $(LCL_REGISTRY)/$(LCL_REPO):$(LCL_TAG) \
		--push \
		.

.PHONY: helm-template-lcl
helm-template-lcl: ## Render the lcl Helm manifest
	$(HELM) template go-bananas deploy/helm/go-bananas \
		--namespace $(LCL_NAMESPACE) \
		--values $(LCL_VALUES) \
		--set-string image.tag=$(LCL_TAG)

.PHONY: helm-install-lcl
helm-install-lcl: ## Deploy go-bananas to the local (lcl) environment ##@destructive
	$(HELM) upgrade --install go-bananas deploy/helm/go-bananas \
		--namespace $(LCL_NAMESPACE) --create-namespace \
		--values $(LCL_VALUES) \
		--set-string image.tag=$(LCL_TAG) \
		--wait --timeout 5m

.PHONY: helm-uninstall-lcl
helm-uninstall-lcl: ## Remove the lcl release ##@destructive
	$(HELM) uninstall go-bananas --namespace $(LCL_NAMESPACE)

.PHONY: deploy-lcl
deploy-lcl: image-lcl helm-install-lcl ## Build, push, deploy to lcl, then verify ##@destructive
	@echo "waiting for rollout ($(LCL_NAMESPACE)/go-bananas)..."
	@kubectl -n $(LCL_NAMESPACE) rollout status deploy/go-bananas --timeout=120s
	@echo "=== runtime manifest through the TLS edge ==="
	@curl -sk https://bananas-lcl.intern-urpi.be/rt/v1/manifest.json -o /dev/null -w 'manifest.json -> %{http_code}\n' || true
	@echo "deployed image: $(LCL_REGISTRY)/$(LCL_REPO):$(LCL_TAG)"

##@ Docs

.PHONY: docs-serve
docs-serve: ## Serve the documentation locally
	@command -v mkdocs >/dev/null 2>&1 || { echo "mkdocs is not installed"; exit 1; }
	mkdocs serve

.PHONY: docs-build
docs-build: ## Build the documentation ##@destructive
	mkdocs build --strict

.PHONY: api-spec
api-spec: ## Print the OpenAPI document summary
	@grep -E '^\s{2}/' openapi.yaml | sed 's/:$//' | tr -d ' ' | sed 's|^|  |'

##@ Contract

.PHONY: check-contract
check-contract: ## Drive the shared styling contract and the shipped controls in a real browser ##@destructive
	./hack/check-contract.sh

.PHONY: check-pilot
check-pilot: ## Prove both adopting applications follow the contract without editing their CSS ##@destructive
	./hack/check-pilot-go-loose.sh
	./hack/check-pilot-go-tell.sh

##@ CI helpers

.PHONY: ci
ci: check ## Everything CI runs, in one target

.PHONY: verify
verify: ci helm-lint helm-matrix check-contract check-pilot ## The full pre-merge gate

.PHONY: deps-outdated
deps-outdated: ## Report outdated Go and Node dependencies
	$(GO) list -u -m all
	@echo
	cd $(WEB_DIR) && $(NPM) outdated || true

.PHONY: deps-update
deps-update: ## Update Go and Node dependencies ##@destructive
	$(GO) get -u ./... && $(GO) mod tidy
	cd $(WEB_DIR) && $(NPM) update
	cd $(SDK_DIR) && $(NPM) update
