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

# Non-secret deployment settings. Runtime secrets remain in separate shell-compatible
# files below remote/setup instead of being parsed by Make.
-include config.mk

define load_envrc
set -a; [[ ! -f ./.envrc ]] || source ./.envrc; set +a;
endef

# ==================================================================================== #
# HELPERS
# ==================================================================================== #

## help: print this help message
.PHONY: help
help:
	@echo 'Usage:'
	@sed -n 's/^##//p' $(firstword $(MAKEFILE_LIST)) | column -t -s ':' | sed -e 's/^/ /'

.PHONY: confirm
confirm:
	@read -r -p "Are you sure? [y/N] " answer && [[ "$${answer:-N}" == y ]]

## config/init: create missing local configuration files from their examples
.PHONY: config/init
config/init:
	@if [[ -e ./.env ]]; then echo 'keep   .env'; else install -m 0600 ./.env.example ./.env; echo 'create .env'; fi
	@if [[ -e ./.envrc ]]; then echo 'keep   .envrc'; else install -m 0600 ./.envrc.example ./.envrc; echo 'create .envrc'; fi
	@if [[ -e ./config.mk ]]; then echo 'keep   config.mk'; else install -m 0600 ./config.mk.example ./config.mk; echo 'create config.mk'; fi
	@if [[ -e ./remote/setup/.env ]]; then echo 'keep   remote/setup/.env'; else install -m 0600 ./remote/setup/.env.example ./remote/setup/.env; echo 'create remote/setup/.env'; fi
	@if [[ -e ./remote/setup/.env.testserver ]]; then echo 'keep   remote/setup/.env.testserver'; else install -m 0600 ./remote/setup/.env.example ./remote/setup/.env.testserver; echo 'create remote/setup/.env.testserver'; fi
	@if [[ -e ./remote/setup/.env.demo ]]; then echo 'keep   remote/setup/.env.demo'; else install -m 0600 ./remote/setup/.env.example ./remote/setup/.env.demo; echo 'create remote/setup/.env.demo'; fi

# ==================================================================================== #
# DEVELOPMENT
# ==================================================================================== #

## run/api: run the cmd/api application
.PHONY: run/api
run/api:
	@${load_envrc} go run ./cmd/api

## run/web: run the cmd/web application
.PHONY: run/web
run/web:
	@${load_envrc} go run ./cmd/web

## run/cli: run the cmd/cli administration tool (use ARGS='...')
.PHONY: run/cli
run/cli:
	@${load_envrc} go run ./cmd/cli ${ARGS}

## db/psql: connect to the database using psql
.PHONY: db/psql
db/psql:
	@${load_envrc} test -n "$${GARDOMATIC_DB_DSN:-}" || { echo 'GARDOMATIC_DB_DSN is required' >&2; exit 1; }; psql "$$GARDOMATIC_DB_DSN"

## db/migrations/new name=$1: create a new database migration
.PHONY: db/migrations/new
db/migrations/new:
	@[[ "${name}" =~ ^[a-z0-9_]+$$ ]] || { echo 'name must contain lowercase letters, digits, or underscores' >&2; exit 1; }
	migrate create -seq -ext=.sql -dir=./internal/storage/postgres/migrations "${name}"

## db/migrations/up: apply all up database migrations
.PHONY: db/migrations/up
db/migrations/up:
	@${load_envrc} test -n "$${GARDOMATIC_DB_DSN:-}" || { echo 'GARDOMATIC_DB_DSN is required' >&2; exit 1; }; migrate -path ./internal/storage/postgres/migrations -database "$$GARDOMATIC_DB_DSN" up

## db/migrations/down: apply all down database migrations
.PHONY: db/migrations/down
db/migrations/down: confirm
	@${load_envrc} test -n "$${GARDOMATIC_DB_DSN:-}" || { echo 'GARDOMATIC_DB_DSN is required' >&2; exit 1; }; migrate -path ./internal/storage/postgres/migrations -database "$$GARDOMATIC_DB_DSN" down

# ==================================================================================== #
# QUALITY CONTROL
# ==================================================================================== #

## tidy: tidy module dependencies and format all Go files
.PHONY: tidy
tidy:
	go mod tidy
	go mod verify
	go fmt ./...

## audit: run quality control checks
.PHONY: audit
audit:
	go mod tidy -diff
	go mod verify
	go vet ./...
	go tool staticcheck ./...
	go test -race -vet=off ./...

## test/integration: migrate the configured PostgreSQL database and run integration tests
.PHONY: test/integration
test/integration:
	@${load_envrc} test -n "$${GARDOMATIC_TEST_DB_DSN:-}" || { echo 'GARDOMATIC_TEST_DB_DSN is required and must point to a disposable test database' >&2; exit 1; }; migrate -path ./internal/storage/postgres/migrations -database "$$GARDOMATIC_TEST_DB_DSN" up; go test -count=1 ./internal/storage/postgres

# ==================================================================================== #
# BUILD
# ==================================================================================== #

.PHONY: build/dirs
build/dirs:
	mkdir -p ./bin

## build/api: build the cmd/api application
.PHONY: build/api
build/api: build/dirs
	go build -ldflags='-s' -o=./bin/api ./cmd/api

## build/web: build the cmd/web application
.PHONY: build/web
build/web: build/dirs
	go build -ldflags='-s' -o=./bin/web ./cmd/web

## build/cli: build the cmd/cli administration tool
.PHONY: build/cli
build/cli: build/dirs
	go build -ldflags='-s' -o=./bin/cli ./cmd/cli

DEPLOYMENT_GOOS ?= linux
DEPLOYMENT_GOARCH ?= amd64
DEPLOYMENT_BUILD_DIR = ./bin/${DEPLOYMENT_GOOS}_${DEPLOYMENT_GOARCH}

.PHONY: build/deployment
build/deployment: build/dirs
	mkdir -p ${DEPLOYMENT_BUILD_DIR}
	GOOS=${DEPLOYMENT_GOOS} GOARCH=${DEPLOYMENT_GOARCH} go build -trimpath -ldflags='-s -w' -o=${DEPLOYMENT_BUILD_DIR}/api ./cmd/api
	GOOS=${DEPLOYMENT_GOOS} GOARCH=${DEPLOYMENT_GOARCH} go build -trimpath -ldflags='-s -w' -o=${DEPLOYMENT_BUILD_DIR}/web ./cmd/web
	GOOS=${DEPLOYMENT_GOOS} GOARCH=${DEPLOYMENT_GOARCH} go build -trimpath -ldflags='-s -w' -o=${DEPLOYMENT_BUILD_DIR}/cli ./cmd/cli

## build/production: cross-compile all production binaries (compatibility alias)
.PHONY: build/production
build/production:
	@$(MAKE) --no-print-directory build/deployment DEPLOYMENT_GOOS=${PRODUCTION_GOOS} DEPLOYMENT_GOARCH=${PRODUCTION_GOARCH}

## code/stats: print source lines and their estimated number of book pages
.PHONY: code/stats
code/stats:
	@lines=$$(git ls-files -z -- '*.go' '*.tmpl' '*.html' '*.css' '*.js' '*.sql' '*.sh' \
		':(exclude)vendor/**' ':(exclude)**/*.min.css' ':(exclude)**/*.min.js' \
		| xargs -0 awk 'END { print NR }'); \
	pages=$$((($$lines + 49) / 50)); \
	printf 'Codeumfang: %s Zeilen\nBuchumfang: ca. %s Seiten (bei 50 Codezeilen pro Seite)\n' "$$lines" "$$pages"

## build/all: build API, web, and administration CLI, then print code statistics
.PHONY: build/all
build/all: build/api build/web build/cli
	@$(MAKE) --no-print-directory code/stats

# ==================================================================================== #
# REMOTE DEPLOYMENTS
# ==================================================================================== #

PRODUCTION_HOST ?=
PRODUCTION_SSH_USER ?= root
PRODUCTION_SSH_PORT ?= 22
PRODUCTION_SSH_IDENTITY_FILE ?=
PRODUCTION_GOOS ?= linux
PRODUCTION_GOARCH ?= amd64

TESTSERVER_HOST ?=
TESTSERVER_SSH_USER ?= root
TESTSERVER_SSH_PORT ?= 22
TESTSERVER_SSH_IDENTITY_FILE ?=
TESTSERVER_GOOS ?= linux
TESTSERVER_GOARCH ?= amd64

DEMO_HOST ?=
DEMO_SSH_USER ?= root
DEMO_SSH_PORT ?= 22
DEMO_SSH_IDENTITY_FILE ?=
DEMO_GOOS ?= linux
DEMO_GOARCH ?= amd64

deployments := production testserver demo
deployment_prefix.production := PRODUCTION
deployment_prefix.testserver := TESTSERVER
deployment_prefix.demo := DEMO
deployment_env.production := ./remote/setup/.env
deployment_env.testserver := ./remote/setup/.env.testserver
deployment_env.demo := ./remote/setup/.env.demo

deployment_setting = $($(deployment_prefix.$1)_$2)
deployment_target = $(call deployment_setting,$1,SSH_USER)@$(call deployment_setting,$1,HOST)
deployment_identity_option = $(if $(strip $(call deployment_setting,$1,SSH_IDENTITY_FILE)),-i $(call deployment_setting,$1,SSH_IDENTITY_FILE))
deployment_ssh_options = -p $(call deployment_setting,$1,SSH_PORT) $(call deployment_identity_option,$1)
deployment_scp_options = -P $(call deployment_setting,$1,SSH_PORT) $(call deployment_identity_option,$1)
deployment_build_dir = ./bin/$(call deployment_setting,$1,GOOS)_$(call deployment_setting,$1,GOARCH)

deployment_check_targets := $(addsuffix /check-config,$(deployments))
deployment_connect_targets := $(addsuffix /connect,$(deployments))
deployment_provision_targets := $(addsuffix /provision,$(deployments))
deployment_deploy_targets := $(addsuffix /deploy,$(deployments))
deployment_admin_targets := $(addsuffix /create-admin,$(deployments))

.PHONY: $(deployment_check_targets) $(deployment_connect_targets) $(deployment_provision_targets) $(deployment_deploy_targets) $(deployment_admin_targets)

$(deployment_check_targets): %/check-config:
	@test -n "$(call deployment_setting,$*,HOST)" || { echo '$(deployment_prefix.$*)_HOST is required; configure it in config.mk' >&2; exit 1; }
	@test -n "$(call deployment_setting,$*,SSH_USER)" || { echo '$(deployment_prefix.$*)_SSH_USER is required' >&2; exit 1; }
	@[[ "$(call deployment_setting,$*,SSH_PORT)" =~ ^[0-9]+$$ ]] && (( $(call deployment_setting,$*,SSH_PORT) >= 1 && $(call deployment_setting,$*,SSH_PORT) <= 65535 )) || { echo '$(deployment_prefix.$*)_SSH_PORT must be between 1 and 65535' >&2; exit 1; }
	@[[ "$(call deployment_setting,$*,GOOS)" == linux ]] || { echo '$(deployment_prefix.$*)_GOOS must be linux' >&2; exit 1; }
	@[[ "$(call deployment_setting,$*,GOARCH)" == amd64 || "$(call deployment_setting,$*,GOARCH)" == arm64 ]] || { echo '$(deployment_prefix.$*)_GOARCH must be amd64 or arm64' >&2; exit 1; }
	@if [[ -n "$(call deployment_setting,$*,SSH_IDENTITY_FILE)" && ! -f "$(call deployment_setting,$*,SSH_IDENTITY_FILE)" ]]; then echo '$(deployment_prefix.$*)_SSH_IDENTITY_FILE does not exist' >&2; exit 1; fi

$(deployment_connect_targets): %/connect: %/check-config
	ssh $(call deployment_ssh_options,$*) $(call deployment_target,$*)

$(deployment_provision_targets): %/provision: %/check-config
	@test -f $(deployment_env.$*) || { echo 'Copy remote/setup/.env.example to $(deployment_env.$*) and configure it first' >&2; exit 1; }
	@permissions=$$(stat -c '%a' $(deployment_env.$*)); (( (8#$$permissions & 077) == 0 )) || { echo '$(deployment_env.$*) must have mode 0600' >&2; exit 1; }
	scp $(call deployment_scp_options,$*) ./remote/setup/provision-server.sh $(call deployment_target,$*):/tmp/gardomatic-provision-server.sh
	ssh $(call deployment_ssh_options,$*) $(call deployment_target,$*) 'status=0; chmod 700 /tmp/gardomatic-provision-server.sh && /tmp/gardomatic-provision-server.sh --env-file - || status=$$?; rm -f /tmp/gardomatic-provision-server.sh; exit $$status' < $(deployment_env.$*)

$(deployment_deploy_targets): %/deploy: %/check-config
	@$(MAKE) --no-print-directory build/deployment DEPLOYMENT_GOOS=$(call deployment_setting,$*,GOOS) DEPLOYMENT_GOARCH=$(call deployment_setting,$*,GOARCH)
	ssh $(call deployment_ssh_options,$*) $(call deployment_target,$*) 'mkdir -p "$$HOME/gardomatic-deploy/migrations"'
	rsync -P -e "ssh $(call deployment_ssh_options,$*)" $(call deployment_build_dir,$*)/api $(call deployment_build_dir,$*)/web $(call deployment_build_dir,$*)/cli $(call deployment_target,$*):gardomatic-deploy/
	rsync -rP --delete -e "ssh $(call deployment_ssh_options,$*)" ./internal/storage/postgres/migrations/ $(call deployment_target,$*):gardomatic-deploy/migrations/
	rsync -P -e "ssh $(call deployment_ssh_options,$*)" ./remote/production/api.service ./remote/production/web.service ./remote/production/demo-reset.service ./remote/production/demo-reset.timer ./remote/production/deploy-server.sh ./remote/setup/create-admin.sh $(call deployment_target,$*):gardomatic-deploy/
	ssh -t $(call deployment_ssh_options,$*) $(call deployment_target,$*) 'chmod 700 "$$HOME/gardomatic-deploy/deploy-server.sh" && "$$HOME/gardomatic-deploy/deploy-server.sh"'

$(deployment_admin_targets): %/create-admin: %/check-config
	ssh -t $(call deployment_ssh_options,$*) $(call deployment_target,$*) /usr/local/sbin/gardomatic-create-admin

## production/connect|provision|deploy|create-admin: manage the production server
## testserver/connect|provision|deploy|create-admin: manage the test server
## demo/connect|provision|deploy|create-admin: manage the public demo server
