LOCUST_PORT ?= 10502
LOCUST_WEB_PORT ?= 10503
LOCUST_DB_DIR ?= /tmp/devplace_locust
LOCUST_DB ?= $(LOCUST_DB_DIR)/datastore.db
LOCUST_USERS ?= 20
LOCUST_SPAWN_RATE ?= 5
LOCUST_RUN_TIME ?= 120s
LOCUST_WEB_WORKERS ?= 4
WEB_WORKERS ?= $(shell nproc 2>/dev/null || echo 2)
DEVPLACE_RATE_LIMIT ?= 1000000

PYTHONDONTWRITEBYTECODE := 1
export PYTHONDONTWRITEBYTECODE

.PHONY: install dev clean tree tree-loc zip test test-headed test-unit test-api test-e2e test-fast test-failed test-first-failure test-slowest test-cache-clean coverage coverage-headed coverage-html locust locust-headless

install:
	pip install -e .
	python -m playwright install chromium

dev:
	uvicorn devplacepy.main:app --reload --reload-dir devplacepy --host 0.0.0.0 --port 10500 --backlog 4096

prod:
	DEVPLACE_STATIC_VERSION=$$(date +%s) DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(WEB_WORKERS) uvicorn devplacepy.main:app --host 0.0.0.0 --port 10500 --workers $(WEB_WORKERS) --backlog 8192 --proxy-headers --forwarded-allow-ips '*'

delete-pyc:
	find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true
	find . -name "*.pyc" -delete

tree:
	git ls-files | tree --fromfile --noreport

tree-loc:
	@git ls-files | while IFS= read -r f; do \
		loc=$$(wc -l < "$$f" 2>/dev/null || echo 0); \
		printf '%s [%s LOC]\n' "$$f" "$$loc"; \
	done | tree --fromfile --noreport

zip:
	@rm -f $(notdir $(CURDIR)).zip
	@git ls-files -z | xargs -0 zip -q $(notdir $(CURDIR)).zip
	@printf 'Wrote %s (%s files)\n' "$(notdir $(CURDIR)).zip" "$$(git ls-files | wc -l)"

test:
	PLAYWRIGHT_HEADLESS=1 python -m pytest tests/

test-headed:
	PLAYWRIGHT_HEADLESS=0 python -m pytest tests/

test-unit:
	python -m pytest tests/unit

test-api:
	python -m pytest tests/api

test-e2e:
	PLAYWRIGHT_HEADLESS=1 python -m pytest tests/e2e

test-fast:
	python -m pytest tests/unit tests/api

test-failed:
	PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ --last-failed --last-failed-no-failures none

test-first-failure:
	PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ -x

test-slowest:
	PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ --durations=40

coverage:
	rm -f .coverage .coverage.*
	COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=1 \
		python -m coverage run -m pytest tests/
	python -m coverage combine
	python -m coverage report

coverage-headed:
	rm -f .coverage .coverage.*
	COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=0 \
		python -m coverage run -m pytest tests/
	python -m coverage combine
	python -m coverage report

coverage-html: coverage
	python -m coverage html
	@echo "Report written to htmlcov/index.html"

locust:
	export DEVPLACE_DATABASE_URL="sqlite:///$(LOCUST_DB)"; \
	export DEVPLACE_RATE_LIMIT=$(DEVPLACE_RATE_LIMIT); \
	fuser -k $(LOCUST_PORT)/tcp 2>/dev/null || true; \
	sleep 1; \
	mkdir -p $(LOCUST_DB_DIR); \
	rm -f $(LOCUST_DB); \
	DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(LOCUST_WEB_WORKERS) uvicorn devplacepy.main:app --host 127.0.0.1 --port $(LOCUST_PORT) --workers $(LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>&1 & \
	PID=$$!; \
	while kill -0 $$PID 2>/dev/null && ! curl -s http://127.0.0.1:$(LOCUST_PORT)/ > /dev/null 2>&1; do sleep 0.5; done; \
	if ! kill -0 $$PID 2>/dev/null; then echo "Server failed to start (port $(LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log"; exit 1; fi; \
	locust --host http://127.0.0.1:$(LOCUST_PORT) --web-port $(LOCUST_WEB_PORT); \
	kill $$PID 2>/dev/null || true; \
	rm -rf $(LOCUST_DB_DIR)

locust-headless:
	export DEVPLACE_DATABASE_URL="sqlite:///$(LOCUST_DB)"; \
	export DEVPLACE_RATE_LIMIT=$(DEVPLACE_RATE_LIMIT); \
	fuser -k $(LOCUST_PORT)/tcp 2>/dev/null || true; \
	sleep 1; \
	mkdir -p $(LOCUST_DB_DIR); \
	rm -f $(LOCUST_DB); \
	DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(LOCUST_WEB_WORKERS) uvicorn devplacepy.main:app --host 127.0.0.1 --port $(LOCUST_PORT) --workers $(LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>&1 & \
	PID=$$!; \
	while kill -0 $$PID 2>/dev/null && ! curl -s http://127.0.0.1:$(LOCUST_PORT)/ > /dev/null 2>&1; do sleep 0.5; done; \
	if ! kill -0 $$PID 2>/dev/null; then echo "Server failed to start (port $(LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log"; exit 1; fi; \
	locust --host http://127.0.0.1:$(LOCUST_PORT) --headless -u $(LOCUST_USERS) -r $(LOCUST_SPAWN_RATE) --run-time $(LOCUST_RUN_TIME) --html=$(LOCUST_DB_DIR)/report.html; \
	kill $$PID 2>/dev/null || true; \
	rm -rf $(LOCUST_DB_DIR)

clean:
	find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
	find . -type f -name '*.pyc' -delete
	rm -rf devplacepy.egg-info
	rm -rf .pytest_cache
	rm -rf .venv

test-cache-clean:
	rm -rf .pytest_cache

# Container Manager works out of the box: the overlay installs the docker CLI in
# the image and mounts the host socket. DOCKER_GID is read straight from the
# socket so the UID-1000 app can use it; the data dir is the project's own data/
# at its real host path, so the DooD bind-mount (host == container path) holds
# with no /srv dir and no sudo.
COMPOSE := docker compose -f docker-compose.yml -f docker-compose.containers.yml
DEVPLACE_DATA_DIR ?= $(CURDIR)/data
DOCKER_GID ?= $(shell stat -c '%g' /var/run/docker.sock 2>/dev/null)
DEVPLACE_CONTAINER_NETWORK ?= bridge
export DEVPLACE_DATA_DIR
export DOCKER_GID

.PHONY: docker-build docker-up docker-attach docker-reload docker-down docker-logs docker-clean docker-prep ppy

# Build the single shared container image every instance runs. Build once;
# rebuild only when ppy.Dockerfile, the sudo shim, or pagent change.
ppy:
	docker build --network=host -f ppy.Dockerfile -t ppy:latest devplacepy/services/containers/files

docker-prep:
	mkdir -p $(DEVPLACE_DATA_DIR)

docker-build: docker-prep
	$(COMPOSE) build

docker-up: docker-prep
	$(COMPOSE) up -d
	$(MAKE) docker-attach

# Workspace tunnels reach a container port that was never published on the host,
# so the app must sit on the same docker network as the instances it runs. The
# default bridge rejects the network-scoped aliases compose always sends, so
# this cannot live in docker-compose.containers.yml and is wired here instead.
docker-attach:
	@app=$$($(COMPOSE) ps -q app); \
	test -n "$$app" || { echo "app container is not running"; exit 1; }; \
	docker network connect $(DEVPLACE_CONTAINER_NETWORK) $$app 2>/dev/null \
		&& echo "attached app to the $(DEVPLACE_CONTAINER_NETWORK) network" \
		|| echo "app is already on the $(DEVPLACE_CONTAINER_NETWORK) network"

docker-reload:
	$(COMPOSE) restart app
	$(COMPOSE) up -d --wait
	$(MAKE) docker-attach

docker-down:
	$(COMPOSE) down

docker-logs:
	$(COMPOSE) logs -f

docker-clean:
	$(COMPOSE) down -v

docker-bup: docker-build docker-up

deploy:
	git checkout production
	git merge master
	git push origin production

