#!/usr/bin/env bash
# Инсталлятор VidConf.
#
# Опросник из 5 пресетов поставки:
#   1) MVP-ядро (лобби, конференции, календарь, закреплённые, гости)
#   2) 1 + чат
#   3) 2 + AI-уровень «мин» (CPU, faster-whisper small + Qwen3.5-4B)
#   4) 2 + AI-уровень «средний» (CPU, faster-whisper medium + Qwen3.5-9B)
#   5) 2 + AI-уровень «макс» (GPU ОБЯЗАТЕЛЕН, faster-whisper large-v3 + Qwen3.5-35B-A3B)
# Модели/железо — ADR-004 (docs/architecture/adr/004-ai-tier-matrix.md),
# единственный источник истины. Запись конференций сюда НЕ входит — она
# появится в одной из следующих версий.
#
# Идемпотентность: повторный запуск = апгрейд/даунгрейд пресета на месте.
# Секреты и пользовательские правки .env НЕ теряются — значения пишутся
# точечно (set_env_var ниже), а не перезаписью всего файла. Seed-скрипт
# backend (scripts/seed.py) и бутстрап instance_settings (backend, lifespan)
# идемпотентны сами по себе — существующие настройки не перетираются.
#
# Использование:
#   ./install.sh                 # интерактивный опросник
#   ./install.sh --preset 3      # неинтерактивно, пресет 3
#   ./install.sh --preset 3 --yes  # без подтверждений (скрипты/CI)
#   ./install.sh --preset 2 --monitoring --yes  # + профиль monitoring (Grafana/Prometheus)
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

COMPOSE_FILE="deploy/docker-compose.yml"
ENV_FILE="$SCRIPT_DIR/.env"
ENV_EXAMPLE="$SCRIPT_DIR/.env.example"
VERSION_FILE="$SCRIPT_DIR/VERSION"

PRESET=""
ASSUME_YES=0
# По умолчанию 0: `set_env_var COMPOSE_PROFILES` ниже (по каждому пресету)
# перезаписывает значение целиком, затирая `media,monitoring` из
# .env.example — без этого флага мониторинг остаётся не поднятым, пока
# оператор не допишет профиль вручную (см. .forcc/deploy/SESSION4-FINDINGS.md,
# ГРАБЛИ 2).
MONITORING=0

while [ $# -gt 0 ]; do
    case "$1" in
        --preset)
            PRESET="$2"
            shift 2
            ;;
        --yes|-y)
            ASSUME_YES=1
            shift
            ;;
        --monitoring)
            MONITORING=1
            shift
            ;;
        -h|--help)
            grep -E '^#( |$)' "$0" | sed 's/^#//'
            exit 0
            ;;
        *)
            echo "Неизвестный аргумент: $1" >&2
            exit 1
            ;;
    esac
done

# ---------------------------------------------------------------------------
# Утилиты работы с .env: точечное обновление ключа (не трогает остальные
# строки/комментарии/порядок) — идемпотентность и сохранность пользовательских
# правок при повторном запуске.
# ---------------------------------------------------------------------------

FRESH_ENV=0

# `.env.example` в git хранит ПЛЕЙСХОЛДЕРЫ секретов (`change-me...`,
# `POSTGRES_PASSWORD=vidconf` и т.п.) — не пустые строки. Если после
# `cp .env.example .env` оставить их как есть, `ensure_secret` ниже (генерация
# «только при отсутствии») их не тронет, и на проде осядут секреты из
# публичного репозитория. Поэтому при ПЕРВОМ создании .env явно очищаем
# управляемые секреты перед вызовом ensure_secret — на повторных запусках
# (файл уже существует) эта функция не трогает ничего, ensure_secret
# сохраняет и сгенерированные, и вручную заданные пользователем значения.
ensure_env_file() {
    if [ ! -f "$ENV_FILE" ]; then
        echo "[install] .env не найден — создаю из .env.example"
        cp "$ENV_EXAMPLE" "$ENV_FILE"
        FRESH_ENV=1
    fi
}

get_env_var() {
    key="$1"
    [ -f "$ENV_FILE" ] || { echo ""; return 0; }
    grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2- || true
}

# Установить/заменить KEY=VALUE в .env; добавить строку, если ключа ещё нет.
# `sed -i.bak ... && rm -f *.bak` — переносимо между GNU sed (Linux) и BSD
# sed (macOS): оба принимают суффикс бэкапа сразу после -i, поведение без
# суффикса на BSD sed отличается (требует отдельного аргумента) — этим и
# отличаются друг от друга, суффикс убирает разницу.
set_env_var() {
    key="$1"
    value="$2"
    esc_value=$(printf '%s' "$value" | sed -e 's/[&|\\]/\\&/g')
    if grep -qE "^${key}=" "$ENV_FILE"; then
        sed -i.bak "s|^${key}=.*|${key}=${esc_value}|" "$ENV_FILE" && rm -f "$ENV_FILE.bak"
    else
        printf '%s=%s\n' "$key" "$value" >> "$ENV_FILE"
    fi
}

# Установить секрет ТОЛЬКО если он ещё не задан (не перезаписывать
# сгенерированные ранее/вручную заданные пользователем значения).
ensure_secret() {
    key="$1"
    bytes="${2:-32}"
    current="$(get_env_var "$key")"
    if [ -z "$current" ]; then
        generated="$(openssl rand -hex "$bytes" 2>/dev/null || head -c "$bytes" /dev/urandom | od -An -tx1 | tr -d ' \n')"
        set_env_var "$key" "$generated"
        echo "[install] ${key}: сгенерирован новый секрет"
    fi
}

# Установить дефолт для НЕ-секретного ключа только если он ещё не задан —
# аналог ensure_secret, но без генерации случайного значения. Не трогает
# значение, уже заданное вручную (реальный домен/IP боевого сервера).
ensure_default() {
    key="$1"
    default="$2"
    if [ -z "$(get_env_var "$key")" ]; then
        set_env_var "$key" "$default"
    fi
}

# ---------------------------------------------------------------------------
# Fail-fast валидация прод-значений .env перед подъёмом стека.
#
# `ensure_secret`/`ensure_default` пишут значение только если ключ ПУСТ, а
# `.env.example` хранит непустые dev-плейсхолдеры (`NGINX_SERVER_NAMES=
# example.com`, `LIVEKIT_PUBLIC_URL=ws://localhost:7880`, `LIVEKIT_API_KEY=
# devkey` и т.д.) — при `cp .env.example .env` без ручной правки install.sh
# доходил до конца и рапортовал об успехе, а на выходе получался прод с
# mixed-content (браузер режет `ws://` на HTTPS-странице) и nginx, слушающим
# несуществующий домен `example.com` (см. .forcc/deploy/SESSION4-FINDINGS.md,
# ГРАБЛИ 1 — реальный инцидент этого проекта).
#
# Признак «это прод, не dev»: `NGINX_SERVER_NAMES` — реальный домен, не
# `localhost`/`127.0.0.1` (дефолт `ensure_default` для локальной разработки).
# Плейсхолдер `example.com` из .env.example — ошибка ВСЕГДА (ни для прода,
# ни как осознанный dev-дефолт: тот — `localhost`), проверяется отдельно и
# раньше остальных.
validate_prod_env() {
    nginx_names="$(get_env_var NGINX_SERVER_NAMES)"

    case "$nginx_names" in
        *example.com*)
            echo "[install] ОШИБКА: NGINX_SERVER_NAMES='${nginx_names}' — непочатый плейсхолдер из .env.example." >&2
            echo "[install] Перед запуском install.sh задайте в .env реальный домен(ы) (боевой сервер)" >&2
            echo "[install] или 'localhost' (локальная разработка) — см. docs/deploy/DEPLOYMENT.md." >&2
            exit 1
            ;;
    esac

    case "$nginx_names" in
        localhost|127.0.0.1) return 0 ;;
    esac

    # Дальше — прод-ветка: NGINX_SERVER_NAMES указывает на реальный домен,
    # остальные прод-значения обязаны быть с ним согласованы.
    fail=0
    fail_check() {
        echo "[install] ОШИБКА: $1" >&2
        fail=1
    }

    livekit_public_url="$(get_env_var LIVEKIT_PUBLIC_URL)"
    case "$livekit_public_url" in
        wss://*) ;;
        *)
            fail_check "LIVEKIT_PUBLIC_URL='${livekit_public_url}' — должен быть wss://<домен>/livekit/ (иначе mixed-content на HTTPS, конференции не стартуют — см. .forcc/deploy/SESSION2-FINDINGS.md)."
            ;;
    esac

    frontend_url="$(get_env_var FRONTEND_URL)"
    case "$frontend_url" in
        https://*) ;;
        *)
            fail_check "FRONTEND_URL='${frontend_url}' — должен быть https://<домен> (иначе битые ссылки подтверждения email)."
            ;;
    esac

    if [ "$(get_env_var AUTH_COOKIE_SECURE)" != "true" ]; then
        fail_check "AUTH_COOKIE_SECURE должен быть 'true' на HTTPS-проде (сейчас: '$(get_env_var AUTH_COOKIE_SECURE)')."
    fi

    if [ "$(get_env_var LIVEKIT_API_KEY)" = "devkey" ]; then
        fail_check "LIVEKIT_API_KEY='devkey' — публично известный ключ из .env.example, замените на реальный."
    fi

    node_ip="$(get_env_var LIVEKIT_NODE_IP)"
    if [ "$node_ip" = "127.0.0.1" ] || [ -z "$node_ip" ]; then
        fail_check "LIVEKIT_NODE_IP='${node_ip}' — задайте внешний IP/домен сервера (иначе ICE-кандидаты недостижимы извне)."
    fi

    cert_name="$(get_env_var NGINX_CERT_NAME)"
    case "$cert_name" in
        *example.com*)
            fail_check "NGINX_CERT_NAME='${cert_name}' — непочатый плейсхолдер из .env.example."
            ;;
    esac
    # НЕ хард-фейл: отсутствие реального сертификата — не тихий провал вроде
    # mixed-content (тот ломает WS в консоли браузера, легко пропустить).
    # docker-entrypoint-certs.sh сгенерирует самоподписанный и nginx стартует
    # штатно — HTTPS с явным предупреждением браузера, что сразу заметно. Это
    # ОСОЗНАННЫЙ путь бутстрапа: сначала поднять стек (self-signed), потом
    # certbot --webroot (challenge идёт через уже работающий nginx), см.
    # docs/deploy/DEPLOYMENT.md, раздел «Выпуск SSL».
    if [ ! -d "/etc/letsencrypt/live/${cert_name}" ]; then
        echo "[install] ПРЕДУПРЕЖДЕНИЕ: /etc/letsencrypt/live/${cert_name} не найден — nginx поднимется с самоподписанным сертификатом." >&2
        echo "[install] Это ожидаемо при первом бутстрапе (см. docs/deploy/DEPLOYMENT.md, «Выпуск SSL» — certbot --webroot ПОСЛЕ первого запуска install.sh)." >&2
    fi

    # TURN понижен до опционального (Сессия 5: реальные сети в тестировании
    # прошли без раздачи TURN клиентам) — только предупреждение, не блокер.
    turn_ip="$(get_env_var TURN_EXTERNAL_IP)"
    if [ "$turn_ip" = "127.0.0.1" ]; then
        echo "[install] ПРЕДУПРЕЖДЕНИЕ: TURN_EXTERNAL_IP=127.0.0.1 — coturn-релей не будет работать для внешних клиентов." >&2
        echo "[install] Не блокер (TURN опционален, см. docs/deploy/DEPLOYMENT.md, раздел «TURN для экстремального NAT»), но проверьте, что это осознанный выбор." >&2
    fi

    if [ "$fail" = "1" ]; then
        echo "[install] Остановлено: .env содержит dev-плейсхолдеры на боевом домене (NGINX_SERVER_NAMES='${nginx_names}')." >&2
        echo "[install] Полный разбор переменных — docs/deploy/DEPLOYMENT.md." >&2
        exit 1
    fi
}

# ---------------------------------------------------------------------------
# Автодетект железа (ADR-004, таблица требований).
# ---------------------------------------------------------------------------

detect_cpus() {
    if command -v nproc >/dev/null 2>&1; then
        nproc
    elif command -v sysctl >/dev/null 2>&1; then
        sysctl -n hw.ncpu 2>/dev/null || echo 0
    else
        echo 0
    fi
}

detect_ram_mb() {
    if command -v free >/dev/null 2>&1; then
        free -m | awk '/^Mem:/{print $2}'
    elif command -v sysctl >/dev/null 2>&1; then
        bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0)
        echo $((bytes / 1024 / 1024))
    else
        echo 0
    fi
}

# Печатает "имя_gpu|vram_mb" через `|` (имя может содержать пробелы) или
# ничего, если nvidia-smi недоступен/GPU не найден.
detect_gpu() {
    if command -v nvidia-smi >/dev/null 2>&1; then
        nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits 2>/dev/null \
            | head -1 \
            | awk -F', *' '{print $1"|"$2}'
    fi
}

HW_CPUS="$(detect_cpus)"
HW_RAM_MB="$(detect_ram_mb)"
HW_GPU_LINE="$(detect_gpu || true)"
HW_GPU_NAME="${HW_GPU_LINE%%|*}"
HW_VRAM_MB="${HW_GPU_LINE##*|}"
if [ "$HW_GPU_LINE" = "" ] || [ "$HW_GPU_NAME" = "$HW_VRAM_MB" ]; then
    HW_GPU_NAME=""
    HW_VRAM_MB=""
fi

echo "[install] Обнаружено железо: CPU=${HW_CPUS} vCPU, RAM=${HW_RAM_MB} МБ, GPU=${HW_GPU_NAME:-нет}${HW_VRAM_MB:+, VRAM=${HW_VRAM_MB} МБ}"

# ---------------------------------------------------------------------------
# Рекомендация пресета по обнаруженному железу (таблица ADR-004).
# ---------------------------------------------------------------------------

recommend_preset() {
    if [ -n "$HW_VRAM_MB" ] && [ "$HW_VRAM_MB" -ge 16000 ] 2>/dev/null \
        && [ "$HW_RAM_MB" -ge 65536 ] 2>/dev/null && [ "$HW_CPUS" -ge 16 ] 2>/dev/null; then
        echo 5
    elif [ "$HW_RAM_MB" -ge 32768 ] 2>/dev/null && [ "$HW_CPUS" -ge 12 ] 2>/dev/null; then
        echo 4
    elif [ "$HW_RAM_MB" -ge 16384 ] 2>/dev/null && [ "$HW_CPUS" -ge 8 ] 2>/dev/null; then
        echo 3
    else
        echo 2
    fi
}

RECOMMENDED_PRESET="$(recommend_preset)"

# ---------------------------------------------------------------------------
# Опросник (интерактивный, если --preset не передан).
# ---------------------------------------------------------------------------

print_menu() {
    cat <<EOF

Выберите пресет поставки VidConf:
  1) MVP-ядро          — лобби, конференции, календарь, закреплённые, гости
  2) 1 + чат            — то же + групповой чат конференции
  3) 2 + AI «мин»       — CPU: faster-whisper small + Qwen3.5-4B   (8 vCPU / 16 ГБ RAM / 100 ГБ)
  4) 2 + AI «средний»   — CPU: faster-whisper medium + Qwen3.5-9B  (12-16 vCPU / 32 ГБ RAM / 150 ГБ)
  5) 2 + AI «макс»      — GPU ОБЯЗАТЕЛЕН: faster-whisper large-v3 + Qwen3.5-35B-A3B (16+ vCPU / 64 ГБ RAM / GPU NVIDIA ≥16 ГБ VRAM / 250 ГБ)

Рекомендация по обнаруженному железу: пресет ${RECOMMENDED_PRESET}.
Подробности требований — docs/architecture/adr/004-ai-tier-matrix.md.
EOF
}

if [ -z "$PRESET" ]; then
    print_menu
    printf "Номер пресета [%s]: " "$RECOMMENDED_PRESET"
    read -r answer
    PRESET="${answer:-$RECOMMENDED_PRESET}"
fi

case "$PRESET" in
    1|2|3|4|5) ;;
    *)
        echo "Пресет должен быть числом от 1 до 5 (получено: '${PRESET}')" >&2
        exit 1
        ;;
esac

# Пресет 5 (max) требует GPU ОБЯЗАТЕЛЬНО (ADR-004) — если детект его не
# нашёл, явно предупреждаем и просим подтверждение (можно ставить заранее,
# до подключения GPU-хоста, но AI-контейнеры не поднимутся здоровыми).
if [ "$PRESET" = "5" ] && { [ -z "$HW_VRAM_MB" ] || [ "$HW_VRAM_MB" -lt 16000 ]; }; then
    echo "[install] ВНИМАНИЕ: пресет 5 требует NVIDIA GPU ≥16 ГБ VRAM — не обнаружен (или недостаточно VRAM)." >&2
    if [ "$ASSUME_YES" != "1" ]; then
        printf "Продолжить всё равно? [y/N]: "
        read -r confirm
        case "$confirm" in
            y|Y|yes|Yes) ;;
            *) echo "Отменено."; exit 1 ;;
        esac
    fi
fi

# ---------------------------------------------------------------------------
# Заполнение .env: секреты (только при отсутствии), железо, версия, модели,
# профили compose — точечно, см. set_env_var.
# ---------------------------------------------------------------------------

ensure_env_file

if [ "$FRESH_ENV" = "1" ]; then
    for key in JWT_SECRET POSTGRES_PASSWORD REDIS_PASSWORD TURN_STATIC_AUTH_SECRET \
        LIVEKIT_API_SECRET GRAFANA_ADMIN_PASSWORD SEED_ADMIN_PASSWORD; do
        set_env_var "$key" ""
    done
fi

ensure_secret JWT_SECRET 32
ensure_secret POSTGRES_PASSWORD 16
# DATABASE_URL хранит пароль ВНУТРИ строки подключения — пересобираем её из
# POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB на каждом запуске (не только
# при первом), иначе после регенерации POSTGRES_PASSWORD backend/worker
# продолжили бы стучаться со старым паролем из .env.example и не
# подключились бы к БД. Хост — `postgres` (имя сервиса в docker-сети
# compose), не `localhost` (тот годится только для запуска backend на хосте
# вне контейнера, см. docs/deploy/dev-setup.md).
set_env_var DATABASE_URL "postgresql+asyncpg://$(get_env_var POSTGRES_USER):$(get_env_var POSTGRES_PASSWORD)@postgres:5432/$(get_env_var POSTGRES_DB)"
ensure_secret REDIS_PASSWORD 24
# REDIS_URL здесь — только для запуска backend/worker НА ХОСТЕ вне
# docker-сети (см. docs/deploy/dev-setup.md): внутри контейнеров
# docker-compose.yml всегда подставляет redis://:${REDIS_PASSWORD}@redis:6379/0
# сам, независимо от этого значения. Пересобираем на каждом запуске по
# тому же принципу, что и DATABASE_URL выше — иначе после регенерации
# REDIS_PASSWORD это значение осталось бы со старым паролем.
set_env_var REDIS_URL "redis://:$(get_env_var REDIS_PASSWORD)@localhost:6379/0"
ensure_secret TURN_STATIC_AUTH_SECRET 24
ensure_secret LIVEKIT_API_SECRET 32
ensure_secret GRAFANA_ADMIN_PASSWORD 16
# SEED_ADMIN_PASSWORD — единственный секрет с человекочитаемым дефолтом
# (сообщается пользователю в конце) вместо случайной генерации: это учётные
# данные, которыми оператор будет логиниться вручную сразу после установки.
if [ -z "$(get_env_var SEED_ADMIN_PASSWORD)" ]; then
    generated_admin_pw="$(openssl rand -hex 8 2>/dev/null || head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')"
    set_env_var SEED_ADMIN_PASSWORD "$generated_admin_pw"
fi

set_env_var VIDCONF_VERSION "$(cat "$VERSION_FILE")"
set_env_var HW_CPUS "$HW_CPUS"
set_env_var HW_RAM_MB "$HW_RAM_MB"
set_env_var HW_GPU_NAME "$HW_GPU_NAME"
set_env_var HW_VRAM_MB "$HW_VRAM_MB"

# nginx/coturn/livekit — дефолты для локальной разработки (localhost/127.0.0.1),
# только если ещё не заданы. На боевом сервере ДО первого запуска install.sh
# задайте в .env реальные значения (домены, внешний IP) — ensure_default их
# не перезапишет.
ensure_default NGINX_SERVER_NAMES "localhost"
ensure_default NGINX_CERT_NAME "localhost"
ensure_default LIVEKIT_USE_EXTERNAL_IP "false"
ensure_default LIVEKIT_NODE_IP "127.0.0.1"
ensure_default TURN_EXTERNAL_IP "127.0.0.1"
# Пусто = TURN over TLS выключен (см. docs/deploy/DEPLOYMENT.md §8) — не
# генерируем и не требуем здесь, только гарантируем, что ключ явно есть в
# .env (для discoverability), а не отсутствует молча.
ensure_default TURN_TLS_HOST ""

# Профили compose и модели по пресету. GPU-профили — ТОЛЬКО для пресета 5
# (max): в текущей матрице уровней (backend/services/ai_tiers.py, ADR-004)
# уровень `medium` фиксированно CPU-only (GPU для него — ручная настройка
# администратора вне детекта), поэтому отдельного GPU-варианта для пресета 4
# нет — не путать с прозой ADR-004 про «опциональный GPU для medium».
#
# BOOTSTRAP_CHAT_ENABLED/BOOTSTRAP_TRANSCRIPTION_ENABLED/BOOTSTRAP_AI_LEVEL:
# без них
# бутстрап настроек инстанса (backend, lifespan) брал ВСЁ enabled: true из
# config/plugins.yaml независимо от пресета, и на пресетах 1/2 (без AI)
# сеанс конференции навсегда зависал в очереди transcribe, которую некому
# обслуживать. Значения читает backend при первом бутстрапе И скрипт
# scripts.apply_preset_settings (принудительное применение, см. ниже).
case "$PRESET" in
    1)
        set_env_var COMPOSE_PROFILES "media"
        set_env_var BOOTSTRAP_CHAT_ENABLED "false"
        set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "false"
        set_env_var BOOTSTRAP_AI_LEVEL "min"
        ;;
    2)
        set_env_var COMPOSE_PROFILES "media"
        set_env_var BOOTSTRAP_CHAT_ENABLED "true"
        set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "false"
        set_env_var BOOTSTRAP_AI_LEVEL "min"
        ;;
    3)
        set_env_var COMPOSE_PROFILES "media,transcribe,llm"
        set_env_var WHISPER_MODEL "small"
        set_env_var LLM_MODEL_FILE "qwen3.5-4b-instruct-q4_k_m.gguf"
        set_env_var LLM_MODEL_URL "https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf"
        set_env_var LLM_MODEL_MIN_SIZE "2000000000"
        set_env_var LLM_TOKENIZER_FILE "qwen3.5-4b-instruct.tokenizer.json"
        set_env_var LLM_TOKENIZER_URL "https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json"
        set_env_var BOOTSTRAP_CHAT_ENABLED "true"
        set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "true"
        set_env_var BOOTSTRAP_AI_LEVEL "min"
        ;;
    4)
        set_env_var COMPOSE_PROFILES "media,transcribe,llm"
        set_env_var WHISPER_MODEL "medium"
        set_env_var LLM_MODEL_FILE "qwen3.5-9b-instruct-q4_k_m.gguf"
        set_env_var LLM_MODEL_URL "https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf"
        set_env_var LLM_MODEL_MIN_SIZE "5000000000"
        set_env_var LLM_TOKENIZER_FILE "qwen3.5-9b-instruct.tokenizer.json"
        set_env_var LLM_TOKENIZER_URL "https://huggingface.co/Qwen/Qwen3.5-9B/resolve/main/tokenizer.json"
        set_env_var BOOTSTRAP_CHAT_ENABLED "true"
        set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "true"
        set_env_var BOOTSTRAP_AI_LEVEL "medium"
        ;;
    5)
        set_env_var COMPOSE_PROFILES "media,transcribe-gpu,llm-gpu"
        set_env_var WHISPER_MODEL "large-v3"
        set_env_var LLM_MODEL_FILE "qwen3.5-35b-a3b-instruct-q4_k_m.gguf"
        set_env_var LLM_MODEL_URL "https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-Q4_K_M.gguf"
        set_env_var LLM_MODEL_MIN_SIZE "20000000000"
        set_env_var LLM_TOKENIZER_FILE "qwen3.5-35b-a3b-instruct.tokenizer.json"
        set_env_var LLM_TOKENIZER_URL "https://huggingface.co/Qwen/Qwen3.5-35B-A3B/resolve/main/tokenizer.json"
        set_env_var BOOTSTRAP_CHAT_ENABLED "true"
        set_env_var BOOTSTRAP_TRANSCRIPTION_ENABLED "true"
        set_env_var BOOTSTRAP_AI_LEVEL "max"
        ;;
esac

# --monitoring: дописать профиль monitoring к тому, что выставил пресет
# выше (set_env_var перезаписывает значение целиком, поэтому мониторинг
# нельзя было получить только сменой пресета — см. комментарий у MONITORING).
if [ "$MONITORING" = "1" ]; then
    current_profiles="$(get_env_var COMPOSE_PROFILES)"
    case ",${current_profiles}," in
        *,monitoring,*) ;;
        *) set_env_var COMPOSE_PROFILES "${current_profiles},monitoring" ;;
    esac
fi

echo "[install] .env обновлён (пресет ${PRESET}, COMPOSE_PROFILES=$(get_env_var COMPOSE_PROFILES))"

echo "[install] Проверяю .env на dev-плейсхолдеры перед подъёмом стека"
validate_prod_env

# ---------------------------------------------------------------------------
# Подъём стека и миграции/seed.
# ---------------------------------------------------------------------------

# `COMPOSE_PROFILES` в .env НЕ подхватывается автоматически командой
# `docker compose ... up` в этой версии compose (проверено на v5.3.1) —
# нужен явный повторяемый флаг `--profile`, иначе профильные сервисы
# (livekit/coturn для media, транскрибация/LLM для остальных пресетов)
# молча не поднимаются, хотя .env выглядит корректно заполненным.
PROFILE_ARGS=()
IFS=',' read -r -a _profiles <<< "$(get_env_var COMPOSE_PROFILES)"
for _p in "${_profiles[@]}"; do
    [ -n "$_p" ] && PROFILE_ARGS+=(--profile "$_p")
done

echo "[install] Рендерю coturn/livekit/egress из шаблонов (deploy/render-templates.sh)"
"$SCRIPT_DIR/deploy/render-templates.sh"

echo "[install] docker compose pull (лучше сразу, но необязательный шаг — образы, которых нет в реестре под текущим тегом, пропускаются)"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "${PROFILE_ARGS[@]}" pull --ignore-buildable 2>/dev/null || true

echo "[install] docker compose build"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "${PROFILE_ARGS[@]}" build

echo "[install] Поднимаю postgres/redis (--wait) — нужны для миграций до старта backend"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --wait postgres redis

# backend.lifespan бутстрапит instance_settings при КАЖДОМ старте приложения,
# поэтому миграции обязаны применяться ДО первого запуска backend/worker —
# иначе на чистой БД backend падает на "relation instance_settings does not
# exist" и healthcheck (--wait) никогда не проходит. `compose run` запускает
# одноразовый контейнер с нужной командой, не поднимая uvicorn/lifespan.
echo "[install] Применяю миграции Alembic и seed (админ/справочники) — до старта backend"
# `--no-sync`: окружение собрано в образе, повторная синхронизация в рантайме
# только тянула бы dev-группу (см. комментарий в backend/Dockerfile).
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm backend uv run --no-sync alembic upgrade head
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm backend uv run --no-sync python -m scripts.seed

echo "[install] docker compose up -d --wait (backend/worker/nginx и остальные сервисы профиля)"
# На слабой/загруженной машине healthcheck может не успеть пройти за отведённые
# retries, и `--wait` вернёт "container is
# unhealthy", хотя сервис через несколько секунд становится healthy. Команда
# идемпотентна, поэтому повторяем её несколько раз: повтор лишь дожидается
# уже стартующих контейнеров, ничего не пересоздавая.
UP_ATTEMPTS=3
for attempt in $(seq 1 "$UP_ATTEMPTS"); do
    if docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "${PROFILE_ARGS[@]}" up -d --wait; then
        break
    fi
    if [ "$attempt" -eq "$UP_ATTEMPTS" ]; then
        echo "[install] ОШИБКА: сервисы не стали healthy за $UP_ATTEMPTS попыток." >&2
        echo "[install] Проверьте логи: docker compose -f $COMPOSE_FILE --env-file $ENV_FILE logs backend worker" >&2
        exit 1
    fi
    echo "[install] Не все сервисы healthy (попытка $attempt/$UP_ATTEMPTS) — жду 10с и повторяю..."
    sleep 10
done

# ---------------------------------------------------------------------------
# Настройки модулей инстанса (чат/AI-уровень) под пресет — только на живой
# установке (fix/installer-preset-modules): при СВЕЖЕМ .env (FRESH_ENV=1)
# они уже применятся сами при бутстрапе backend (lifespan читает
# BOOTSTRAP_* из .env), повторно накатывать не нужно. При ПОВТОРНОМ запуске
# .env уже существовал ДО этого запуска — настройки уже в БД и могли быть
# вручную изменены администратором, поэтому перезаписываем их только с
# явным подтверждением (или --yes для неинтерактивного режима).
# ---------------------------------------------------------------------------
if [ "$FRESH_ENV" != "1" ]; then
    APPLY_PRESET_SETTINGS=0
    if [ "$ASSUME_YES" = "1" ]; then
        APPLY_PRESET_SETTINGS=1
    else
        printf "Обновить настройки модулей инстанса (чат, уровень AI) под пресет %s? [Y/n]: " "$PRESET"
        read -r update_settings_answer
        case "$update_settings_answer" in
            n|N|no|No) APPLY_PRESET_SETTINGS=0 ;;
            *) APPLY_PRESET_SETTINGS=1 ;;
        esac
    fi
    if [ "$APPLY_PRESET_SETTINGS" = "1" ]; then
        echo "[install] Применяю настройки модулей инстанса под пресет ${PRESET}"
        docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T backend uv run --no-sync python -m scripts.apply_preset_settings --force
    else
        echo "[install] Настройки модулей инстанса НЕ изменены — сохранены ручные правки администратора"
    fi
fi

VERSION_VALUE="$(cat "$VERSION_FILE")"
ADMIN_EMAIL="$(get_env_var SEED_ADMIN_EMAIL)"
ADMIN_PASSWORD="$(get_env_var SEED_ADMIN_PASSWORD)"
GRAFANA_USER="$(get_env_var GRAFANA_ADMIN_USER)"

if [ "$MONITORING" = "1" ]; then
    MONITORING_BLOCK="$(cat <<EOF2
 Мониторинг (Grafana/Prometheus) поднят вместе со стеком (--monitoring):
   Prometheus: http://127.0.0.1:9090 (только localhost, см. docs/deploy/DEPLOYMENT.md)
   Grafana:    http://127.0.0.1:3001 (${GRAFANA_USER:-admin} / см. .env GRAFANA_ADMIN_PASSWORD)
EOF2
)"
else
    MONITORING_BLOCK="$(cat <<EOF2
 Мониторинг (Grafana/Prometheus) — отдельный профиль compose, не входит
 в базовые пресеты. Поднять сейчас: docker compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} --profile monitoring up -d
 (или перезапустите install.sh с флагом --monitoring).
   Grafana: http://localhost:3001 (${GRAFANA_USER:-admin} / см. .env GRAFANA_ADMIN_PASSWORD)
EOF2
)"
fi

cat <<EOF

============================================================
 VidConf ${VERSION_VALUE} установлен (пресет ${PRESET}).
============================================================
 Backend:  http://localhost/api/health
 Frontend: http://localhost/
 Админ:    ${ADMIN_EMAIL:-см. .env SEED_ADMIN_EMAIL} / ${ADMIN_PASSWORD:-см. .env SEED_ADMIN_PASSWORD}
           (смените пароль сразу после первого входа)

${MONITORING_BLOCK}

 Настройки модулей инстанса (чат, уровень AI) синхронизированы под
 пресет ${PRESET} (админка → Настройки, при желании переключите вручную).

 Повторный запуск ./install.sh с другим --preset — апгрейд/даунгрейд
 на месте (модели докачиваются, .env обновляется точечно, секреты и
 пользовательские правки сохраняются; настройки модулей обновляются
 только с вашего подтверждения или флагом --yes).
============================================================
EOF
