export CGO_ENABLED  = 0
export GOTOOLCHAIN ?= local+auto

MODULE   = $(shell $(GO) list -m)
VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \
			cat .version 2> /dev/null || echo v0)
PKGS     = $(or $(PKG),$(shell $(GO) list ./...))

GO      = go
PNPM    = $(or $(shell command -v pnpm 2>/dev/null),\
			$(shell command -v corepack 2>/dev/null | sed 's/$$/ pnpm/'),\
			$(error No pnpm command found))
CLANG   = clang
TIMEOUT = 45s
LSFILES = git ls-files -cmo --exclude-standard --
V = 0
Q = $(if $(filter 1,$V),,@)
M = $(shell if [ "$$(tput colors 2> /dev/null || echo 0)" -ge 8 ]; then printf "\033[34;1m▶\033[0m"; else printf "▶"; fi)

GENERATED_JS = \
	console/frontend/node_modules
GENERATED_GO = \
	common/pb/rawflow.pb.go \
	common/pb/rawflow_vtproto.pb.go \
	common/schema/definition_gen.go \
	inlet/flow/input/udp/reuseport_bpfeb.o \
	inlet/flow/input/udp/reuseport_bpfel.o \
	orchestrator/clickhouse/data/asns.csv \
	orchestrator/clickhouse/data/protocols.csv \
	orchestrator/clickhouse/data/tcp.csv \
	orchestrator/clickhouse/data/udp.csv \
	console/filter/parser.go \
	inlet/kafka/loadbalancealgorithm_enumer.go \
	outlet/core/asnprovider_enumer.go \
	outlet/core/netprovider_enumer.go \
	outlet/metadata/provider/snmp/authprotocol_enumer.go \
	outlet/metadata/provider/snmp/privprotocol_enumer.go \
	outlet/metadata/provider/gnmi/ifspeedpathunit_enumer.go \
	console/homepagetopwidget_enumer.go \
	common/kafka/saslmechanism_enumer.go
GENERATED_TEST_GO = \
	common/clickhousedb/mocks/mock_driver.go
GENERATED = \
	$(GENERATED_GO) \
	$(GENERATED_JS) \
	console/data/frontend \
	common/embed/data/embed.zip

.PHONY: all all-indep
BUILD_ARGS =
# For architecture variants, see:
# - https://go.dev/wiki/MinimumRequirements#amd64
# - https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels
# - https://clang.llvm.org/docs/UsersManual.html#x86
# - https://go.dev/wiki/MinimumRequirements#arm64
# - https://github.com/golang/go/issues/60905
# - https://github.com/llvm/llvm-project/blob/main/llvm/unittests/TargetParser/TargetParserTest.cpp#L1077
# - https://en.wikipedia.org/wiki/Comparison_of_ARM_processors
# - https://docs.docker.com/build/building/variables/#pre-defined-build-arguments
# - https://github.com/containerd/platforms/pull/8
all: fmt lint all-indep ; $(info $(M) building executable…) @ ## Build program binary
	$Q env GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) \
         $(if $(filter amd64,$(TARGETARCH)),GOAMD64=$(TARGETVARIANT),\
         $(if $(filter arm64,$(TARGETARCH)),GOARM64=$(if $(findstring .,$(TARGETVARIANT)),$(TARGETVARIANT),$(TARGETVARIANT:%=%.0)),\
         $(if $(filter arm,$(TARGETARCH)),GOARM=$(TARGETVARIANT:v%=%)))) \
	   $(GO) build \
		-tags release,grpcnotrace \
		-ldflags '-X $(MODULE)/common/helpers.AkvoradoVersion=$(VERSION)' \
		$(BUILD_ARGS) \
		-o bin/$(basename $(MODULE)) .
all-indep: $(GENERATED)

# Tools

BUF         = env GO=$(GO) ./bin/external-tool buf
ENUMER      = go tool enumer
GOIMPORTS   = go tool goimports
GOTESTSUM   = go tool gotestsum
MOCKGEN     = go tool mockgen
PIGEON      = go tool pigeon
REVIVE      = go tool revive
STATICCHECK = go tool staticcheck
WWHRD       = go tool wwhrd

# Generated files

.SUFFIXES:
.DELETE_ON_ERROR:

$(filter %.go, $(GENERATED_GO) $(GENERATED_TEST_GO)): go.mod

common/pb/rawflow.pb.go common/pb/rawflow_vtproto.pb.go &: .buf.gen.yaml common/pb/rawflow.proto ; $(info $(M) compiling protocol buffers $@…)
	$Q $(BUF) generate --template $(PWD)/.buf.gen.yaml --path $(@:.pb.go=.proto)

common/clickhousedb/mocks/mock_driver.go: ; $(info $(M) generate mocks for ClickHouse driver…)
	$Q $(MOCKGEN) -package mocks -build_constraint "!release" -destination $@ \
		github.com/ClickHouse/clickhouse-go/v2/lib/driver Conn,Row,Rows,ColumnType
	$Q touch $@

inlet/flow/input/udp/reuseport_%.o: inlet/flow/input/udp/reuseport_kern.c inlet/flow/input/udp/vmlinux.h ; $(info $(M) generate eBPF program for input/udp ($*)…)
	$Q ! $(CLANG) -print-targets 2> /dev/null | grep -qF $* || \
		 $(CLANG) -O2 -g -Wall -target $* -c $< -o $@

inlet/kafka/loadbalancealgorithm_enumer.go: inlet/kafka/config.go ; $(info $(M) generate enums for LoadBalanceAlgorithm…)
	$Q $(ENUMER) -type=LoadBalanceAlgorithm -text -transform=kebab -trimprefix=LoadBalance inlet/kafka/config.go
outlet/core/asnprovider_enumer.go: outlet/core/config.go ; $(info $(M) generate enums for ASNProvider…)
	$Q $(ENUMER) -type=ASNProvider -text -transform=kebab -trimprefix=ASNProvider outlet/core/config.go
outlet/core/netprovider_enumer.go: outlet/core/config.go ; $(info $(M) generate enums for NetProvider…)
	$Q $(ENUMER) -type=NetProvider -text -transform=kebab -trimprefix=NetProvider outlet/core/config.go
outlet/metadata/provider/snmp/authprotocol_enumer.go: outlet/metadata/provider/snmp/config.go ; $(info $(M) generate enums for AuthProtocol…)
	$Q $(ENUMER) -type=AuthProtocol -text -transform=kebab -trimprefix=AuthProtocol outlet/metadata/provider/snmp/config.go
outlet/metadata/provider/snmp/privprotocol_enumer.go: outlet/metadata/provider/snmp/config.go ; $(info $(M) generate enums for PrivProtocol…)
	$Q $(ENUMER) -type=PrivProtocol -text -transform=kebab -trimprefix=PrivProtocol outlet/metadata/provider/snmp/config.go
outlet/metadata/provider/gnmi/ifspeedpathunit_enumer.go: outlet/metadata/provider/gnmi/config.go ; $(info $(M) generate enums for IfSpeedPathUnit…)
	$Q $(ENUMER) -type=IfSpeedPathUnit -text -transform=kebab -trimprefix=Speed outlet/metadata/provider/gnmi/config.go
console/homepagetopwidget_enumer.go: console/config.go ; $(info $(M) generate enums for HomepageTopWidget…)
	$Q $(ENUMER) -type=HomepageTopWidget -text -json -transform=kebab -trimprefix=HomepageTopWidget console/config.go
common/kafka/saslmechanism_enumer.go: common/kafka/config.go ; $(info $(M) generate enums for SASLMechanism…)
	$Q $(ENUMER) -type=SASLMechanism -text -transform=kebab -trimprefix=SASL common/kafka/config.go

common/schema/definition_gen.go: common/schema/definition.go common/schema/definition_gen.sh ; $(info $(M) generate column definitions…)
	$Q ./common/schema/definition_gen.sh > $@
	$Q $(GOIMPORTS) -w $@

console/filter/parser.go: console/filter/parser.peg ; $(info $(M) generate PEG parser for filters…)
	$Q $(PIGEON) -optimize-basic-latin $< > $@

console/frontend/node_modules: console/frontend/package.json console/frontend/pnpm-lock.yaml
console/frontend/node_modules: ; $(info $(M) fetching node modules…)
	$Q (cd console/frontend ; $(PNPM) install --loglevel=error --frozen-lockfile) && touch $@
console/data/frontend: $(GENERATED_JS)
console/data/frontend: $(shell $(LSFILES) console/frontend 2> /dev/null)
console/data/frontend: ; $(info $(M) building console frontend…)
	$Q cd console/frontend && $(PNPM) run --silent build

ASNS_URL = https://vincentbernat.github.io/asn2org/asns.csv
PROTOCOLS_URL = http://www.iana.org/assignments/protocol-numbers/protocol-numbers-1.csv
SERVICES_URL = https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.csv
define caturl
$(if $(filter http://% https://%, $(1)),curl --retry 3 --no-progress-meter --location --fail $(1),cat $(1))
endef

orchestrator/clickhouse/data/asns.csv: ; $(info $(M) generate ASN map…)
	$Q $(call caturl,$(ASNS_URL)) | sed 's|,[^,]*$$||' > $@
	$Q test -s $@
orchestrator/clickhouse/data/protocols.csv: ; $(info $(M) generate protocol map…)
	$Q $(call caturl,$(PROTOCOLS_URL)) \
		| sed -nE -e "1 s/.*/proto,name,description/p" -e "2,$$ s/^([0-9]+,[^ ,]+,[^\",]+),.*/\1/p" \
		> $@
	$Q test -s $@
orchestrator/clickhouse/data/udp.csv orchestrator/clickhouse/data/tcp.csv: orchestrator/clickhouse/data/%.csv: ; $(info $(M) generate $* port numbers…)
	$Q $(call caturl,$(SERVICES_URL)) \
		| sed -nE -e "1 s/.*/port,name/p" -e "2,$$ s/^([^,]+),([0-9]+),$*,.*/\2,\1/p" \
		| awk -F',' '!seen[$$1]++' \
		> $@
	$Q test -s $@

changelog.md: docs/99-changelog.md # To be used by GitHub actions only.
	$Q >  $@ < docs/99-changelog.md \
		sed -n '/^## '$${GITHUB_REF##*/v}' -/,/^## /{//!p}'
	$Q >> $@ echo "**Docker image**: \`docker pull ghcr.io/$${GITHUB_REPOSITORY}:$${GITHUB_REF##*/v}\`"
	$Q >> $@ echo "**Full changelog**: https://github.com/$${GITHUB_REPOSITORY}/compare/v$$(< docs/99-changelog.md sed -n '/^## '$${GITHUB_REF##*/v}' -/,/^## /{s/^## \([0-9.a-z-]*\) -.*/\1/p}' | tail -1)...v$${GITHUB_REF##*/v}"

# Update default.pgo with the locally running "docker compose" instance.
# Use: "make -j default.pgo".
default.pgo: default-inlet.pgo default-outlet.pgo default-console.pgo
	$Q go tool pprof -proto $^ > $@
	$Q rm $^
default-%.pgo:
	$Q container=akvorado-akvorado-$*-1 ; \
	   ip=$$(docker container inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $$container) ; \
	   [ -n $$ip ] ; \
	   curl -so $@ "http://$$ip:8080/debug/pprof/profile?seconds=30"

common/embed/data/embed.zip: console/data/frontend console/authentication/data/avatars console/data/docs
common/embed/data/embed.zip: orchestrator/clickhouse/data/protocols.csv orchestrator/clickhouse/data/icmp.csv orchestrator/clickhouse/data/asns.csv orchestrator/clickhouse/data/tcp.csv orchestrator/clickhouse/data/udp.csv
common/embed/data/embed.zip: ; $(info $(M) generate embed.zip…)
	$Q mkdir -p common/embed/data
	$Q TMPDIR=$$(mktemp -d) \
	&& trap 'rm -rf "$$TMPDIR"' EXIT \
	&& tar -cf - $^ | tar -C "$$TMPDIR" -xf - \
	&& find "$$TMPDIR"/console/data/docs -name "*.svg" -type f -print0 | xargs -0 sed -i.bak 's/ content="[^"]*"//g' \
	&& ( cd "$$TMPDIR" && zip --quiet --recurse-paths --exclude "*.svg.bak" - .) > $@

# Tests

.PHONY: check test tests test-race test-short test-bench test-coverage
.PHONY: test-go test-go-units test-go-staticcheck test-js test-coverage-go test-coverage-js
check test tests: test-go test-js ## Run tests
test-coverage: test-coverage-go test-coverage-js ## Run coverage tests

test-go-units test-go-checks test-bench test-race test-coverage-go: .fmt-go~ .lint-go~ $(GENERATED) $(GENERATED_TEST_GO)
test-go: test-go-units test-go-checks ## Run Go tests
test-go-checks: ; $(info $(M) running Go static checks…)
	$Q $(STATICCHECK) -f stylish -checks inherit,-SA1012 $(PKGS)
test-go-units: ; $(info $(M) running Go tests$(GOTEST_MORE)…)
	$Q mkdir -p test/go
	$Q env PATH=$(dir $(abspath $(shell command -v $(GO)))):$(PATH) \
	  $(GOTESTSUM) --junitfile test/go/tests.xml -- \
		-timeout $(TIMEOUT) \
	    -fullpath \
		$(GOTEST_ARGS) $(PKGS)
test-race: CGO_ENABLED=1
test-race: GOTEST_ARGS=-race
test-race: GOTEST_MORE=, with race detector
test-race: test-go-units  ## Run Go tests with race detector
test-short: GOTEST_ARGS=-short
test-short: GOTEST_MORE=, only short tests
test-short: test-go  ## Run only short Go tests
test-bench: ; $(info $(M) running benchmarks…) @ ## Run Go benchmarks
	$Q $(GO) test \
		-fullpath -run=__absolutelynothing__ -bench=. \
		$(PKGS) # -benchmem -memprofile test/go/memprofile.out -cpuprofile test/go/cpuprofile.out
test-coverage-go: ; $(info $(M) running Go coverage tests…) @ ## Run Go coverage tests
	$Q mkdir -p test/go
	$Q env PATH=$(dir $(abspath $(shell command -v $(GO)))):$(PATH) \
	  $(GOTESTSUM) -- \
	    -fullpath \
		-coverpkg=$(shell echo $(PKGS) | tr ' ' ',') \
		-covermode=atomic \
		-coverprofile=test/go/profile.out.tmp $(PKGS)
	$Q GENERATED=$$(awk -F: '(NR > 1) {print $$1}' test/go/profile.out.tmp \
			| sort | uniq | sed "s+^$(MODULE)/++" \
			| xargs grep -l "^//.*DO NOT EDIT\.$$" \
			| sed "s+\(.*\)+^$(MODULE)/\1:+" | paste -s -d '|' -) ; \
	   if [ -n "$$GENERATED" ]; then grep -Ev "$$GENERATED" test/go/profile.out.tmp > test/go/profile.out ; \
	   else cp test/go/profile.out.tmp test/go/profile.out ; \
	   fi
	$Q $(GO) tool cover -html=test/go/profile.out -o test/go/coverage.html
	@printf "Code coverage: "; \
		go tool cover -func test/go/profile.out | awk '($$1 == "total:") { print $$NF}'

test-js: .fmt-js~ .lint-js~ $(GENERATED_JS)
test-js: ; $(info $(M) running JS tests…) @ ## Run JS tests
	$Q cd console/frontend && $(PNPM) run --silent type-check && $(PNPM) run --silent test
test-coverage-js: ; $(info $(M) running JS coverage tests…) @ ## Run JS coverage tests
	$Q cd console/frontend && $(PNPM) run --silent type-check && $(PNPM) run --silent test -- --coverage

.PHONY: lint
lint: .lint-go~ .lint-js~ ## Run linting
.lint-go~: .revive.toml $(shell $(LSFILES) '*.go' 2> /dev/null) ; $(info $(M) running golint…)
	$Q $(REVIVE) -config $(PWD)/.revive.toml -formatter stylish -set_exit_status ./...
	$Q touch $@
.lint-js~: $(shell $(LSFILES) '*.js' '*.ts' '*.vue' '*.html' 2> /dev/null)
.lint-js~: $(GENERATED_JS) ; $(info $(M) running jslint…)
	$Q cd console/frontend && $(PNPM) run --silent lint
	$Q touch $@

.PHONY: fmt
fmt: .fmt-go~ .fmt-js~ ## Format all source files
.fmt-go~: $(shell $(LSFILES) '*.go' 2> /dev/null) ; $(info $(M) formatting Go code…)
	$Q $(GOIMPORTS) -local $(MODULE) -w $? < /dev/null
	$Q touch $@
.fmt-js~: $(shell $(LSFILES) '*.js' '*.ts' '*.vue' '*.html' 2> /dev/null)
.fmt-js~: $(GENERATED_JS) ; $(info $(M) formatting JS code…)
	$Q cd console/frontend && $(PNPM) run --silent format
	$Q touch $@

# Misc

.PHONY: licensecheck
licensecheck: console/frontend/node_modules ; $(info $(M) check dependency licenses…) @ ## Check licenses
	$Q ! git grep -L SPDX-License-Identifier: "*.go" "*.ts" "*.js" || \
		(>&2 echo "*** Missing license identifiers!"; false)
	$Q err=0 ; $(GO) mod vendor && $(WWHRD) --quiet check || err=$$? ; rm -rf vendor/ ; exit $$err
	$Q cd console/frontend ; \
		allowed="$$(sed -n '/^allowlist:/,/^[a-z]/p' ../../.wwhrd.yml | sed -n 's/^  - //p' | paste -sd "|")" ; \
		! $(PNPM) licenses ls --json | jq -r --exit-status --arg allowed "$$allowed" \
			'to_entries[] | select(.key | test("^('"$$allowed"')$$") | not) | "Unallowed license: " + .key + " (" + (.value | map(.name) | join(", ")) + ")"'

.PHONY: clean
clean: ; $(info $(M) cleaning…)	@ ## Cleanup everything
	@rm -rf test $(GENERATED) $(GENERATED_TEST_GO) *~ bin/akvorado

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

.PHONY: version
version:
	@echo $(VERSION)

.PHONY: docker docker-dev
DOCKER_BUILD_ARGS =
docker: ; $(info $(M) build Docker image…) @ ## Build Docker image
	$Q git ls-files | tar -T- -cf- | docker build --pull -f docker/Dockerfile $(DOCKER_BUILD_ARGS) \
		--build-arg VERSION=$(VERSION) -t ghcr.io/akvorado/akvorado:main -
docker-dev: TARGETOS=linux
docker-dev: all ; $(info $(M) build development Docker image…) @ ## Build development Docker image
	$Q docker build -f docker/Dockerfile.dev $(DOCKER_BUILD_ARGS) \
		--build-arg VERSION=$(VERSION) -t ghcr.io/akvorado/akvorado:main .

.PHONY: all-coverage docker-dev-coverage
all-coverage: BUILD_ARGS=-cover -covermode=atomic
all-coverage: all
docker-dev-coverage: BUILD_ARGS=-cover -covermode=atomic
docker-dev-coverage: docker-dev

# This requires "skopeo". I fetch it from nix.
.PHONY: docker-upgrade-versions
docker-upgrade-versions: ; $(info $(M) check for Docker image updates…) @ ## Check for Docker image updates
	$Q sed -En 's/^\s*image:\s+(.+):(.+)\s+#\s+(.+)$$/\1 \2 \3/p' docker/versions.yml \
		| while read -r image version regex; do \
			latest=$$(nix run nixpkgs\#skopeo -- list-tags docker://"$$image" \
				| sed -En 's/\s+"(.*)",?/\1/p' \
				| grep -xP "$$regex" \
				| sort -Vr | head -1); \
			[ "$$version" = "$$latest" ] || { \
				>&2 echo "$$image $$version→$$latest"; \
				sed -i "s,$$image:$$version,$$image:$$latest," docker/versions.yml; }; \
		done
