VENV ?= $(HOME)/.py_envs/robotex
PYTHON := $(VENV)/bin/python
PIP := $(PYTHON) -m pip
WHEELS_DIR := wheels
REQUIREMENTS := requirements.txt
ver ?= latest

MAKECMDGOALS_TARGETS := install freeze wheels sync sync-offline clean help build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push dev dev-deps dev-key
PKG := $(filter-out $(MAKECMDGOALS_TARGETS),$(MAKECMDGOALS))

.PHONY: install freeze wheels sync sync-offline clean help venv build release sync-dev test test-browser up down logs smoke key-create key-list key-revoke stats push dev dev-deps dev-key

# образы сервисов: api — без Chrome, worker — Chrome + Xvfb; rabbitmq и redis — сторонние, кладутся в релиз
REGISTRY := docker.sokolab.xyz
API_IMAGE := robotex-api
WORKER_IMAGE := robotex-worker
DEPS_IMAGES := rabbitmq:4.1-alpine redis:8.2-alpine postgres:17-alpine
API_URL ?= http://localhost:8000
API_KEY ?=

# make dev (#90): rabbitmq, redis, postgres — в Docker с портами на 127.0.0.1 (docker-compose.dev.yml), api и worker —
# на хосте, Chrome открывается на рабочем столе. Порты нестандартные: 5672/5432/8000 часто заняты другими проектами.
DEV_COMPOSE := docker compose -f docker-compose.yml -f docker-compose.dev.yml
DEV_API_PORT ?= 8010
DEV_RABBITMQ_PORT ?= 5673
DEV_REDIS_PORT ?= 6380
DEV_POSTGRES_PORT ?= 5433
# окон Chrome на рабочем столе; потоков dramatiq — вдвое больше, как в Dockerfile
DEV_SLOTS ?= 2
RABBITMQ_PASSWORD ?= robotex
POSTGRES_PASSWORD ?= robotex
export DEV_RABBITMQ_PORT DEV_REDIS_PORT DEV_POSTGRES_PORT RABBITMQ_PASSWORD POSTGRES_PASSWORD
DEV_ENV := RABBITMQ_URL=amqp://robotex:$(RABBITMQ_PASSWORD)@localhost:$(DEV_RABBITMQ_PORT)/ \
	REDIS_URL=redis://localhost:$(DEV_REDIS_PORT)/0 \
	DATABASE_URL=postgresql+asyncpg://robotex:$(POSTGRES_PASSWORD)@localhost:$(DEV_POSTGRES_PORT)/robotex \
	BROWSER_SLOTS=$(DEV_SLOTS) \
	LOG_LEVEL=DEBUG

help:
	@echo "make install <pkg> [<pkg> ...]  - установить пакет(ы) в $(VENV), обновить $(REQUIREMENTS), собрать wheel в $(WHEELS_DIR)/"
	@echo "make sync                       - установить все пакеты из $(REQUIREMENTS) (из индекса)"
	@echo "make sync-offline               - установить все пакеты из $(REQUIREMENTS) только из $(WHEELS_DIR)/, без обращения к индексу"
	@echo "make freeze                     - перезаписать $(REQUIREMENTS) текущим состоянием окружения"
	@echo "make wheels                     - собрать wheel для всех пакетов из $(REQUIREMENTS)"
	@echo "make clean                      - удалить $(WHEELS_DIR)/"
	@echo "make build [ver=latest]         - собрать образы $(API_IMAGE):<ver> и $(WORKER_IMAGE):<ver>"
	@echo "make push ver=<ver>             - собрать образы и отправить в $(REGISTRY)"
	@echo "make release ver=<ver> [out=registry|file] - собрать образы и пакет для деплоя в build/<ver>/ (compose, .env);"
	@echo "                                  образы: registry — push в $(REGISTRY) (по умолчанию), file — архив .tar.gz"
	@echo "make up                         - собрать и запустить локально: api, worker, rabbitmq, redis"
	@echo "make down                       - остановить локальный стенд"
	@echo "make logs                       - логи api и worker"
	@echo "make dev                        - api и worker на хосте с видимым Chrome, rabbitmq/redis/postgres в Docker; API на :$(DEV_API_PORT)"
	@echo "make dev-key name=<клиент>      - выпустить API-ключ клиенту при make dev"
	@echo "make smoke API_KEY=... [API_URL=...] - проверить запущенный стенд: ключ, /v1/solve, /v2/tasks, удержание окна"
	@echo "make key-create name=<клиент>   - выпустить API-ключ клиенту (в запущенном compose)"
	@echo "make key-list                   - клиенты и их ключи (без самих ключей)"
	@echo "make key-revoke id=<id>         - отозвать ключ клиента"
	@echo "make stats [days=7]             - статистика: запросы, их время, время работы браузера по клиентам"
	@echo "make sync-dev                   - установить зависимости для тестов (requirements-dev.txt)"
	@echo "make test                       - юнит-тесты без браузера"
	@echo "make client-build               - собрать wheel клиента robotex в client/dist/"
	@echo "make test-browser [ver=latest]  - собрать образ и прогнать сквозные тесты с Chrome в Docker (нужна сеть)"

venv:
	@test -d $(VENV) || python3 -m venv $(VENV)

install: venv
ifeq ($(strip $(PKG)),)
	$(error Укажите пакет: make install <package>, например make install redis)
endif
	$(PIP) install --upgrade $(PKG)
	$(PIP) freeze > $(REQUIREMENTS)
	@mkdir -p $(WHEELS_DIR)
	$(PIP) wheel $(PKG) -w $(WHEELS_DIR)
	@echo "Готово: $(PKG) установлен, $(REQUIREMENTS) обновлён, wheel-пакеты в $(WHEELS_DIR)/"

sync: venv
	$(PIP) install -r $(REQUIREMENTS)

sync-offline: venv
	$(PIP) install --no-index --find-links=$(WHEELS_DIR) -r $(REQUIREMENTS)

freeze: venv
	$(PIP) freeze > $(REQUIREMENTS)

wheels: venv
	@mkdir -p $(WHEELS_DIR)
	$(PIP) wheel -r $(REQUIREMENTS) -w $(WHEELS_DIR)

clean:
	rm -rf $(WHEELS_DIR)

build:
	docker build -f Dockerfile.api -t $(API_IMAGE):$(ver) .
	docker build -t $(WORKER_IMAGE):$(ver) .

push: build
	for img in $(API_IMAGE) $(WORKER_IMAGE); do \
		docker tag $$img:$(ver) $(REGISTRY)/$$img:$(ver) && docker push $(REGISTRY)/$$img:$(ver) || exit 1; \
	done

up: build
	docker compose up -d

down:
	docker compose down

logs:
	docker compose logs -f api worker

# контейнерные api и worker останавливаются: иначе worker в Docker тоже берёт задачи из общей очереди и выполняет их невидимо
dev-deps:
	-docker compose stop api worker
	$(DEV_COMPOSE) up -d --wait rabbitmq redis postgres

# Ctrl+C останавливает оба процесса; инфраструктура остаётся запущенной (make down — остановить)
# api и worker — в фоне: фоновые процессы sh не получают SIGINT от Ctrl+C, поэтому при выходе (Ctrl+C, закрытие
# терминала, kill) trap останавливает оба и ждёт их — иначе осиротевший worker остаётся работать (#110)
dev: dev-deps
	cd src && export $(DEV_ENV) && \
		{ $(PYTHON) -m uvicorn main:app --reload --port $(DEV_API_PORT) & api=$$!; \
		$(PYTHON) -m dramatiq worker.actors --processes 1 --threads $$(( $(DEV_SLOTS) * 2 )) & worker=$$!; \
		trap 'trap - EXIT INT TERM HUP; kill $$api $$worker 2>/dev/null; wait' EXIT INT TERM HUP; \
		wait $$worker; }

dev-key: dev-deps
	cd src && $(DEV_ENV) $(PYTHON) -m db.cli create "$(name)"

smoke:
	$(PYTHON) tests/smoke.py $(API_URL) $(API_KEY)

key-create:
	docker compose exec api python -m db.cli create "$(name)"

key-list:
	docker compose exec api python -m db.cli list

key-revoke:
	docker compose exec api python -m db.cli revoke $(id)

days ?= 7
stats:
	docker compose exec api python -m db.cli stats --days $(days)

sync-dev: venv
	$(PIP) install -r requirements-dev.txt

test:
	$(PYTHON) -m pytest

client-build: venv
	rm -rf client/dist client/build
	$(PIP) wheel ./client -w client/dist --no-deps
	@echo "Готово: client/dist/ — pip install client/dist/robotex-*.whl"

test-browser: build
	docker run --rm --shm-size=2g \
		-v $(CURDIR)/tests:/app/tests:ro -v $(CURDIR)/pytest.ini:/app/pytest.ini:ro \
		-v $(CURDIR)/requirements-dev.txt:/app/requirements-dev.txt:ro \
		-w /app $(WORKER_IMAGE):$(ver) sh -c 'pip install -q --user -r requirements-dev.txt && \
		xvfb-run -a --server-args="-screen 0 1920x1080x24" python -m pytest -m browser -p no:cacheprovider -s'

RELEASE_DIR := build/$(ver)
# куда деваются образы при make release: registry — push в $(REGISTRY), file — архив .tar.gz в $(RELEASE_DIR)/
out ?= registry

ifneq ($(filter release push,$(MAKECMDGOALS)),)
ifeq ($(ver),latest)
$(error Укажите версию: make release ver=v0.0.2)
endif
ifeq ($(filter $(out),registry file),)
$(error out=registry или out=file, а не "$(out)")
endif
endif

ifeq ($(out),file)
RELEASE_RUN := docker load -i robotex-$(ver).tar.gz
else
RELEASE_RUN := docker compose pull
endif

release: $(if $(filter file,$(out)),build,push)
	@echo "make release $(ver) [out=$(out)] -> $(RELEASE_DIR)/"
	@mkdir -p $(RELEASE_DIR)
	sed -e 's|image: .*$(API_IMAGE):.*|image: $(REGISTRY)/$(API_IMAGE):$(ver)|' \
		-e 's|image: .*$(WORKER_IMAGE):.*|image: $(REGISTRY)/$(WORKER_IMAGE):$(ver)|' \
		docker-compose.prod.yml > $(RELEASE_DIR)/docker-compose.yml
	cp dot_env $(RELEASE_DIR)/.env
ifeq ($(out),file)
# сторонние образы (rabbitmq, redis, postgres) кладутся в архив; при out=registry сервер берёт их с Docker Hub
	for img in $(DEPS_IMAGES); do docker image inspect $$img >/dev/null 2>&1 || docker pull $$img; done
	docker tag $(API_IMAGE):$(ver) $(REGISTRY)/$(API_IMAGE):$(ver)
	docker tag $(WORKER_IMAGE):$(ver) $(REGISTRY)/$(WORKER_IMAGE):$(ver)
	docker save $(REGISTRY)/$(API_IMAGE):$(ver) $(REGISTRY)/$(WORKER_IMAGE):$(ver) $(DEPS_IMAGES) | gzip > $(RELEASE_DIR)/robotex-$(ver).tar.gz
endif
	@printf '%s\n' \
		'# Robotex $(ver)' \
		'' \
		'```bash' \
		'$(RELEASE_RUN)' \
		'docker compose up -d' \
		'```' \
		'' \
		'Настройки (порт, слоты, CPU и память воркера, пароли RabbitMQ и Postgres) — в .env.' \
		'Ключ клиенту: docker compose exec api python -m db.cli create <имя>' > $(RELEASE_DIR)/README.md
	@echo "Готово: $(RELEASE_DIR)/"

# позволяет передавать имя пакета как аргумент: make install redis
%:
	@:
