# HomeLab Ansible — единая точка входа для РУЧНОГО управления инфраструктурой.
#
# Запускать из каталога ansible/ (или через `make -C ansible <цель>`):
#
#     make                     список целей
#     make check               базовая проверка связности
#     make deploy-gitea        playbooks/pve-gitea.yml с загруженным .env
#     make dry-gitea           то же самое в режиме --check --diff
#     make update-gitea        playbooks/gitea-update.yml
#     make docs                markdown-таблица хостов в stdout
#
# Дополнительные флаги ansible-playbook передаются через EXTRA:
#
#     make check EXTRA="--limit pve_nodes -vv"
#
# Интерпретатор определяется автоматически: если рядом есть ./.venv — берётся он,
# иначе бинарь ищется в PATH (nix devshell, системный ansible). Любой путь можно
# переопределить переменной окружения, например:
#
#     ANSIBLE_PLAYBOOK=$(which ansible-playbook) make check

SHELL := /bin/sh
.DEFAULT_GOAL := help

MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
ANSIBLE_DIR   := $(patsubst %/,%,$(dir $(MAKEFILE_PATH)))
REPO_ROOT     := $(abspath $(ANSIBLE_DIR)/..)
VENV_BIN      := $(ANSIBLE_DIR)/.venv/bin

# Разрешение бинарей: сначала рабочий локальный venv, иначе PATH (nix devshell,
# системный ansible). VENV_OK проверяет, что интерпретатор venv реально запускается,
# — иначе сломанный .venv (например, после смены системного python) молча ломал бы
# все цели. Переменные окружения имеют приоритет благодаря `?=`.
VENV_OK := $(shell [ -x '$(VENV_BIN)/python3' ] && '$(VENV_BIN)/python3' -c '' >/dev/null 2>&1 && echo yes)
venv_bin = $(if $(VENV_OK),$(if $(wildcard $(VENV_BIN)/$(1)),$(VENV_BIN)/$(1),$(1)),$(1))

ANSIBLE_PLAYBOOK      ?= $(call venv_bin,ansible-playbook)
ANSIBLE_INVENTORY_BIN ?= $(call venv_bin,ansible-inventory)
ANSIBLE_GALAXY        ?= $(call venv_bin,ansible-galaxy)
ANSIBLE_LINT          ?= $(call venv_bin,ansible-lint)
YAMLLINT              ?= $(call venv_bin,yamllint)
# scripts/gen-inventory-docs.py обходится стандартной библиотекой, поэтому берём
# python3 из PATH, а не из .venv.
PYTHON                ?= python3

# Секреты живут в КОРНЕ репозитория, а не в ansible/: их потребляет не только
# Ansible, но и OpenTofu (tofu/), а держать два файла или ходить в подкаталог
# из соседнего инструмента неудобно. Путь абсолютный, поэтому цели работают
# из любого cwd.
ENV_FILE ?= $(REPO_ROOT)/.env

# Дополнительные аргументы ansible-playbook для любой цели.
EXTRA ?=
# Спрашивать sudo-пароль там, где нужен become. `make openvpn ASK_BECOME=` отключает.
ASK_BECOME ?= -K
# Спрашивать пароль Ansible Vault. `make gyro ASK_VAULT=` отключает.
ASK_VAULT ?= --ask-vault-pass

# Строгая загрузка корневого .env: обязательна для Proxmox API и секретов.
# Каждая строка рецепта make — отдельный шелл, поэтому source и запуск идут одной строкой.
REQUIRE_ENV = if [ ! -f '$(ENV_FILE)' ]; then \
		printf 'ОШИБКА: не найден %s\n' '$(ENV_FILE)' >&2; \
		printf 'Создай его и заполни реальными значениями:\n' >&2; \
		printf '    cp %s/.env.example %s\n' '$(REPO_ROOT)' '$(ENV_FILE)' >&2; \
		printf 'Нужны PROXMOX_* (и MONITORING_* / EMERGENCY_* для профильных целей).\n' >&2; \
		exit 1; \
	fi; \
	set -a; . '$(ENV_FILE)'; set +a

# Мягкая загрузка: .env подхватывается если есть, иначе просто предупреждение.
LOAD_ENV = if [ -f '$(ENV_FILE)' ]; then set -a; . '$(ENV_FILE)'; set +a; \
	else printf 'ВНИМАНИЕ: %s не найден, продолжаю без него.\n' '$(ENV_FILE)' >&2; fi

# Защита от случайного запуска опасных плейбуков.
REQUIRE_CONFIRM = if [ "$(CONFIRM)" != "1" ]; then \
		printf 'ОПАСНАЯ ЦЕЛЬ. Повтори явно: make $@ CONFIRM=1\n' >&2; \
		exit 1; \
	fi

# host_vars/gyro/vault.yml зашифрован Vault. Для чтения инвентаря он не нужен,
# поэтому vars-плагины отключаются, чтобы make не спрашивал пароль Vault.
INVENTORY_ENV = ANSIBLE_VARS_ENABLED= ANSIBLE_NOCOLOR=1

##@ Справка

.PHONY: help
help: ## Показать этот список целей
	@awk 'BEGIN { FS = ":.*##"; \
		print ""; \
		print "HomeLab Ansible — ручное управление инфраструктурой"; \
		print ""; \
		print "  Использование: make <цель> [EXTRA=\"--limit host -vv\"]"; \
		print "  Секреты берутся из .env в КОРНЕ репозитория"; \
	} \
	/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5); next } \
	/^[a-zA-Z0-9_%.-]+:.*##/ { printf "  \033[36m%-24s\033[0m %s\n", $$1, $$2 } \
	END { print "" }' $(MAKEFILE_PATH)

##@ Setup

.PHONY: setup
setup: ## Создать .venv, поставить requirements.txt и galaxy-коллекции (не нужно в nix)
	$(PYTHON) -m venv $(ANSIBLE_DIR)/.venv
	$(VENV_BIN)/pip install --upgrade pip
	$(VENV_BIN)/pip install -r requirements.txt
	$(VENV_BIN)/ansible-galaxy collection install -r requirements.yml -p collections

.PHONY: collections
collections: ## Доустановить только galaxy-коллекции из requirements.yml
	$(ANSIBLE_GALAXY) collection install -r requirements.yml -p collections

.PHONY: env-check
env-check: ## Проверить наличие .env и заполненность ключевых переменных
	@$(REQUIRE_ENV); \
	rc=0; \
	for v in PROXMOX_HOST PROXMOX_USER PROXMOX_TOKEN_ID PROXMOX_TOKEN_SECRET; do \
		eval "val=\$$$$v"; \
		if [ -z "$$val" ] || [ "$$val" = 'replace-me' ]; then \
			printf 'не задано: %s\n' "$$v"; rc=1; \
		else printf 'ok:       %s\n' "$$v"; fi; \
	done; \
	exit $$rc

##@ Проверки и диагностика

.PHONY: check
check: ## Проверка связности и ожидаемых IP (playbooks/check.yml)
	$(ANSIBLE_PLAYBOOK) playbooks/check.yml $(EXTRA)

.PHONY: status
status: ## Сводный статус инфраструктуры (playbooks/status.yml)
	ANSIBLE_CALLBACK_RESULT_FORMAT=yaml $(ANSIBLE_PLAYBOOK) playbooks/status.yml $(EXTRA)

# Оба линтера ищут конфиг (.ansible-lint / .yamllint) в текущем каталоге, а не
# у родителей. Запуск из ansible/ конфиг не находил, из-за чего в проверку
# затягивался вендоренный collections/ и цель выдавала около тысячи чужих
# нарушений. Запускаем из корня — ровно как .gitea/workflows/lint.yml.
.PHONY: lint
lint: ## Прогнать ansible-lint, yamllint и syntax-check — то же, что делает CI
	cd '$(REPO_ROOT)' && $(ANSIBLE_LINT)
	cd '$(REPO_ROOT)' && $(YAMLLINT) .
	@rc=0; for f in playbooks/*.yml; do \
		if $(ANSIBLE_PLAYBOOK) --syntax-check "$$f" >/dev/null 2>&1; then \
			printf 'ok   %s\n' "$$f"; \
		else \
			rc=1; printf 'FAIL %s\n' "$$f"; $(ANSIBLE_PLAYBOOK) --syntax-check "$$f" 2>&1 | sed 's/^/       /'; \
		fi; \
	done; exit $$rc

.PHONY: docs
docs: ## Напечатать markdown-таблицу хостов и групп в stdout
	@ANSIBLE_INVENTORY_BIN='$(ANSIBLE_INVENTORY_BIN)' $(PYTHON) scripts/gen-inventory-docs.py

.PHONY: inventory
inventory: ## Показать дерево инвентаря (ansible-inventory --graph)
	@$(INVENTORY_ENV) $(ANSIBLE_INVENTORY_BIN) -i inventory/hosts.yml --graph

.PHONY: validate
validate: ## Сверить реестр homelab_services с фактическим состоянием Proxmox (gate)
	$(ANSIBLE_PLAYBOOK) playbooks/validate.yml $(EXTRA)

.PHONY: openvpn-check
openvpn-check: ## Проверить OpenVPN-транспорт ru-vps <-> ovpn-mini
	$(ANSIBLE_PLAYBOOK) playbooks/openvpn-check.yml $(EXTRA)

.PHONY: backup-audit
backup-audit: ## Аудит свежести бэкапов PBS и offsite restic
	$(ANSIBLE_PLAYBOOK) playbooks/backup-audit.yml $(EXTRA)

dry-%: ## Прогон playbooks/pve-<имя>.yml в режиме --check --diff (пример: make dry-gitea)
	@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-$*.yml --check --diff $(EXTRA)

##@ Деплой LXC/VM через Proxmox API (требует .env)

deploy-%: ## Применить playbooks/pve-<имя>.yml (пример: make deploy-gitea, deploy-adguard, deploy-monitoring)
	@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-$*.yml $(EXTRA)

.PHONY: backup-jobs
backup-jobs: ## Настроить PBS backup jobs (playbooks/pve-backup-jobs.yml)
	@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-backup-jobs.yml $(EXTRA)

.PHONY: pbs-storage
pbs-storage: ## Убрать storage-level prune policy с PBS storage (playbooks/pve-storage-pbs.yml)
	$(ANSIBLE_PLAYBOOK) playbooks/pve-storage-pbs.yml $(EXTRA)

.PHONY: bootstrap-pve-token
bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с ноды (нужен sudo)
	$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-pve-api-token.yml $(ASK_BECOME) $(EXTRA)

.PHONY: bootstrap-monitoring-token
bootstrap-monitoring-token: ## Выпустить read-only PVE-токен для мониторинга
	$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-monitoring-pve-token.yml $(EXTRA)

.PHONY: bootstrap-dashboard-token
bootstrap-dashboard-token: ## Выпустить read-only PVE-токен homepage@pve!dashboard для виджета Proxmox
	$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-dashboard-pve-token.yml $(EXTRA)

.PHONY: bootstrap-ansible-user
bootstrap-ansible-user: ## Завести сервисный аккаунт ansible на shell-хостах (нужен sudo)
	$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-ansible-user.yml $(ASK_BECOME) $(EXTRA)

##@ Настройка сервисов

.PHONY: ru-vps-base
ru-vps-base: ## Базовое состояние ru-vps и стек Caddy (закрепляет образ, ставит homelab-caddy.service)
	$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-base.yml $(EXTRA)

.PHONY: dry-ru-vps-base
dry-ru-vps-base: ## Предпросмотр ru-vps-base.yml (--check --diff)
	$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-base.yml --check --diff $(EXTRA)

.PHONY: reverse-proxy
reverse-proxy: ## Единый reverse-proxy для gitea/vaultwarden/grimmory
	@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/reverse-proxy.yml $(EXTRA)

.PHONY: openvpn
openvpn: ## Поднять OpenVPN-транспорт ru-vps <-> ovpn-mini (нужен sudo)
	$(ANSIBLE_PLAYBOOK) playbooks/openvpn-vps-mini.yml $(ASK_BECOME) $(EXTRA)

.PHONY: openvpn-laptop
openvpn-laptop: ## Настроить OpenVPN-профиль ноутбука
	$(ANSIBLE_PLAYBOOK) playbooks/openvpn-laptop.yml $(EXTRA)

.PHONY: bash-config
bash-config: ## Раскатать единый bash-конфиг на shell_hosts
	$(ANSIBLE_PLAYBOOK) playbooks/bash-config.yml $(EXTRA)

.PHONY: user-ssh-key
user-ssh-key: ## Разложить публичный SSH-ключ пользователя на все хосты
	$(ANSIBLE_PLAYBOOK) playbooks/user-ssh-key.yml $(EXTRA)

.PHONY: emergency-access
emergency-access: ## Настроить emergency reverse-SSH и Telegram-бота (нужны EMERGENCY_* в .env)
	@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/emergency-access.yml $(EXTRA)

.PHONY: gyro
gyro: ## Настроить gyro-аллокатор в production CT 156 (Tofu-managed); CT 150 — остановленный rollback (спрашивает пароль Vault)
	@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/gyro.yml $(ASK_VAULT) $(EXTRA)

.PHONY: uptime-kuma
uptime-kuma: ## Развернуть/обновить Uptime Kuma на monitoring LXC
	$(ANSIBLE_PLAYBOOK) playbooks/uptime-kuma.yml $(EXTRA)

.PHONY: dashboard
dashboard: ## Дашборд-обзор инфраструктуры (Homepage) на monitoring LXC, конфиг из реестра
	@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/dashboard.yml $(EXTRA)

.PHONY: dry-dashboard
dry-dashboard: ## Предпросмотр dashboard.yml (--check --diff)
	@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/dashboard.yml --check --diff $(EXTRA)

.PHONY: offsite-restic
offsite-restic: ## Настроить offsite restic-бэкапы на Yandex.Disk
	@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/offsite-restic-yadisk.yml $(EXTRA)

play-%: ## Запустить произвольный playbooks/<имя>.yml с загруженным .env (пример: make play-check)
	@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/$*.yml $(EXTRA)

##@ Обновления сервисов (сначала свежий бэкап, потом апдейт, потом проверка)

update-%: ## Обновить сервис через playbooks/<имя>-update.yml (gitea, vaultwarden, adguard, mihomo, grimmory)
	@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/$*-update.yml $(EXTRA)

.PHONY: update-all
update-all: ## Последовательно обновить gitea, vaultwarden, adguard, mihomo, grimmory (требует CONFIRM=1)
	@$(REQUIRE_CONFIRM)
	$(MAKE) update-vaultwarden
	$(MAKE) update-gitea
	$(MAKE) update-adguard
	$(MAKE) update-mihomo
	$(MAKE) update-grimmory

##@ OpenTofu (пилот provisioning LXC)

TOFU_DIR := $(REPO_ROOT)/tofu
TOFU     ?= tofu

# Tofu, в отличие от Ansible, ходит в Proxmox API напрямую по HTTPS и НЕ умеет
# ProxyJump из ssh_config. Вне локальной сети узлы недоступны (проверено:
# https://192.168.1.10:8006 отдаёт connection failure), поэтому на время команды
# поднимаем SSH-туннель через ru-vps и направляем провайдер в localhost.
# Изнутри LAN это тоже работает — просто лишний хоп, зато поведение одинаково.
TOFU_TUNNEL_PORT ?= 18006
TOFU_SSH_SOCKET  := $(ANSIBLE_DIR)/.ansible/tofu-tunnel.sock

# insecure=true здесь не настройка вкуса: сертификат Proxmox выписан на узел,
# а обращаемся мы к 127.0.0.1, так что проверка имени не пройдёт в любом случае.
#
# Способ аутентификации выбирается автоматически: если в корневом .env задан
# PROXMOX_ROOT_PASSWORD — идём как root@pam (только так Proxmox разрешает
# features кроме nesting, device passthrough и bind mount каталога хоста),
# иначе токеном ansible@pve. Выбранный режим печатается в stderr, чтобы из
# вывода было видно, какими правами шёл apply.
TOFU_ENV = $(REQUIRE_ENV); \
	mkdir -p '$(dir $(TOFU_SSH_SOCKET))'; \
	ssh -F '$(ANSIBLE_DIR)/ssh_config' -M -S '$(TOFU_SSH_SOCKET)' -fN \
	    -L $(TOFU_TUNNEL_PORT):$$PROXMOX_HOST:$${PROXMOX_PORT:-8006} ru-vps; \
	trap "ssh -S '$(TOFU_SSH_SOCKET)' -O exit ru-vps 2>/dev/null || true" EXIT; \
	export TF_VAR_pve_endpoint="https://127.0.0.1:$(TOFU_TUNNEL_PORT)"; \
	export TF_VAR_pve_insecure=true; \
	if [ -n "$${PROXMOX_ROOT_PASSWORD:-}" ] && [ "$${PROXMOX_ROOT_PASSWORD}" != 'replace-me' ]; then \
		export TF_VAR_pve_username="$${PROXMOX_ROOT_USER:-root@pam}"; \
		export TF_VAR_pve_password="$$PROXMOX_ROOT_PASSWORD"; \
		export TF_VAR_pve_api_token=""; \
		printf 'Proxmox: аутентификация %s (привилегированный режим)\n' "$${PROXMOX_ROOT_USER:-root@pam}" >&2; \
	else \
		export TF_VAR_pve_api_token="$$PROXMOX_USER!$$PROXMOX_TOKEN_ID=$$PROXMOX_TOKEN_SECRET"; \
		export TF_VAR_pve_username=""; \
		export TF_VAR_pve_password=""; \
		printf 'Proxmox: аутентификация токеном %s (features кроме nesting и dev-passthrough недоступны)\n' "$$PROXMOX_USER" >&2; \
	fi

.PHONY: tofu-init
tofu-init: ## Скачать провайдер и инициализировать рабочий каталог tofu/
	@cd '$(TOFU_DIR)' && $(TOFU) init -input=false

.PHONY: tofu-plan
tofu-plan: ## Показать план (read-only, ничего не меняет)
	@$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) plan -input=false

.PHONY: tofu-apply
tofu-apply: ## Применить план — СОЗДАЁТ гостей в Proxmox (требует CONFIRM=1)
	@$(REQUIRE_CONFIRM)
	@$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) apply -input=false -auto-approve

.PHONY: tofu-destroy
tofu-destroy: ## УНИЧТОЖИТЬ всё, что создано в tofu/ (требует CONFIRM=1)
	@$(REQUIRE_CONFIRM)
	@printf 'Удаляет гостей Proxmox из состояния tofu вместе с их дисками.\n' >&2
	@$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) destroy -input=false -auto-approve

##@ Опасное (только осознанно, требует CONFIRM=1)

.PHONY: mihomo-harden
mihomo-harden: ## РОТИРУЕТ живые SOCKS-креды Mihomo на ru-vps и убирает публичный доступ
	@$(REQUIRE_CONFIRM)
	@printf 'Ротация кредов необратима для клиентов без отката бэкапа конфига.\n' >&2
	$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true $(EXTRA)

.PHONY: zerotier-decommission
zerotier-decommission: ## Вывести ZeroTier с ru-vps и почистить его правила UFW (требует CONFIRM=1)
	@$(REQUIRE_CONFIRM)
	@printf 'Останавливает контейнер zerotier и удаляет правила UFW. Данные и файлы остаются.\n' >&2
	$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-zerotier-decommission.yml $(EXTRA)

.PHONY: dry-zerotier-decommission
dry-zerotier-decommission: ## Предпросмотр вывода ZeroTier (--check --diff)
	$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-zerotier-decommission.yml --check --diff $(EXTRA)

.PHONY: monitoring
monitoring: ## ЗАМОРОЖЕН: стек Prometheus. Запускать только при восстановлении мониторинга
	@$(REQUIRE_CONFIRM)
	@printf 'monitoring.yml заморожен, пока используется Uptime Kuma.\n' >&2
	@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/monitoring.yml $(EXTRA)
