#!/usr/bin/env bash
# Установщик узла Ostalari: касса на компьютере заведения.
#
# Что делает: скачивает образ, достаёт из него настройки узла, придумывает пароли, запускает четыре контейнера
# (приложение, база, вход сотрудников, веб-сервер) и заводит первого администратора.
# Повторный запуск — это обновление: пароли и данные сохраняются, образ и настройки берутся свежие.
#
# Нужно: Linux с Docker и плагином compose, запуск от root (sudo). Узел работает в локальной сети, наружу не выставляется.
#
#   sudo bash install.sh                      # адрес узла определится сам
#   sudo bash install.sh --host 192.168.0.50  # или задайте его явно
#   sudo bash install.sh --port 8080          # если 80-й порт занят
set -euo pipefail
export MSYS_NO_PATHCONV=1

IMAGE="${OSTALARI_IMAGE:-registry.gitlab.com/ronalda/ostalari:stable}"
# Реестр с копиями PostgreSQL, Keycloak и Caddy. Пусто — каждый образ скачивается из своего реестра.
MIRROR="${OSTALARI_MIRROR:-registry.gitlab.com/ronalda/ostalari/}"
DIR="${OSTALARI_DIR:-/opt/ostalari}"
HOST="${OSTALARI_HOST:-}"
PORT="${OSTALARI_PORT:-80}"
# Код из паспорта решения на сайте: с ним мастер первой настройки откроется уже с выбранными модулями.
SETUP="${OSTALARI_SETUP:-}"

say() { printf '%s\n' "$*"; }
fail() { printf 'Ошибка: %s\n' "$*" >&2; exit 1; }
usage() {
  say "Установщик узла Ostalari: касса на компьютере заведения."
  say "  --host <адрес>   адрес узла в локальной сети (по умолчанию определяется сам)"
  say "  --port <порт>    порт веб-сервера (по умолчанию 80)"
  say "  --dir <папка>    куда положить настройки и резервные копии (по умолчанию /opt/ostalari)"
  say "  --image <образ>  образ кассы (в скачанном с сайта установщике уже указан)"
  say "  --mirror <адрес> реестр с копиями PostgreSQL, Keycloak и Caddy (в скачанном с сайта установщике уже указан)"
  say "  --config <код>   код из паспорта решения на сайте: мастер настройки откроется с вашими модулями"
}

while [ $# -gt 0 ]; do
  case "$1" in
    --image) IMAGE="${2:-}"; shift 2 ;;
    --mirror) MIRROR="${2:-}"; shift 2 ;;
    --dir) DIR="${2:-}"; shift 2 ;;
    --host) HOST="${2:-}"; shift 2 ;;
    --port) PORT="${2:-}"; shift 2 ;;
    --config) SETUP="${2:-}"; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) fail "неизвестный параметр $1. Справка: --help" ;;
  esac
done

case "$IMAGE" in
  ""|__OSTALARI_IMAGE__) fail "не указан образ кассы. Скачайте установщик с сайта или запустите с --image <образ>." ;;
esac
[ "$MIRROR" != "__OSTALARI_MIRROR__" ] || MIRROR=""
case "$MIRROR" in *[!A-Za-z0-9._/:-]*) fail "адрес --mirror записан неверно: $MIRROR" ;; esac
case "$PORT" in ''|*[!0-9]*) fail "порт должен быть числом, получено: $PORT" ;; esac
case "$SETUP" in *[!A-Za-z0-9_-]*) fail "код --config повреждён: скопируйте команду с сайта целиком." ;; esac
[ "${#SETUP}" -le 2000 ] || fail "код --config слишком длинный: скопируйте команду с сайта целиком."

command -v docker >/dev/null 2>&1 || fail "Docker не найден. Поставьте его командой: curl -fsSL https://get.docker.com | sh"
docker compose version >/dev/null 2>&1 || fail "нет плагина docker compose. Обновите Docker: curl -fsSL https://get.docker.com | sh"
docker info >/dev/null 2>&1 || fail "Docker не отвечает. Запустите установщик через sudo и проверьте, что служба docker работает."

# Адрес узла в локальной сети: по нему кассу открывают планшеты, и он же зашивается в токены сотрудников.
if [ -z "$HOST" ]; then
  HOST="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}' || true)"
fi
if [ -z "$HOST" ]; then
  HOST="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
fi
[ -n "$HOST" ] || fail "не удалось определить адрес узла в локальной сети. Укажите его: --host 192.168.0.50"
ADDRESS="$HOST"
if [ "$PORT" != "80" ]; then
  case "$HOST" in *:*) ;; *) ADDRESS="$HOST:$PORT" ;; esac
fi

mkdir -p "$DIR/backups"
cd "$DIR"

say "1/5 Образ кассы: $IMAGE"
if ! docker pull "$IMAGE" >/dev/null 2>&1; then
  docker image inspect "$IMAGE" >/dev/null 2>&1 || fail "образ $IMAGE не скачался. Проверьте интернет и название образа."
  say "    скачать не удалось, беру образ, который уже есть на этом компьютере"
fi

say "2/5 Настройки узла в $DIR"
extract() {
  docker run --rm --entrypoint cat "$IMAGE" "/app/deploy/selfhost/$1" </dev/null > "$1.tmp" || fail "в образе нет файла $1: это не образ Ostalari или он слишком старый."
  mv "$1.tmp" "$1"
}
extract compose.yaml
extract Caddyfile
extract realm.json
extract backup.sh
chmod +x backup.sh

# Пароли придумываются один раз. При обновлении берём прежние: с новыми база и вход не открылись бы.
secret() { head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'; }
keep() { if [ -f .env ]; then sed -n "s/^$1=//p" .env | head -n 1; fi; }
POSTGRES_PASSWORD="$(keep POSTGRES_PASSWORD)"; [ -n "$POSTGRES_PASSWORD" ] || POSTGRES_PASSWORD="$(secret)"
KEYCLOAK_ADMIN_PASSWORD="$(keep KEYCLOAK_ADMIN_PASSWORD)"; [ -n "$KEYCLOAK_ADMIN_PASSWORD" ] || KEYCLOAK_ADMIN_PASSWORD="$(secret)"
SESSION_SECRET="$(keep SESSION_SECRET)"; [ -n "$SESSION_SECRET" ] || SESSION_SECRET="$(secret)"
[ -n "$SETUP" ] || SETUP="$(keep OSTALARI_SETUP)"
# Копии чужих образов: адрес с косой чертой на конце подставляется в compose.yaml перед именем образа.
[ -n "$MIRROR" ] || MIRROR="$(keep OSTALARI_MIRROR)"
[ -z "$MIRROR" ] || MIRROR="${MIRROR%/}/"

umask 077
cat > .env <<EOF
# Узел Ostalari. Файл создан установщиком; пароли не меняйте — на них держатся база и вход сотрудников.
OSTALARI_IMAGE=$IMAGE
OSTALARI_MIRROR=$MIRROR
OSTALARI_HOST=$ADDRESS
OSTALARI_PORT=$PORT
POSTGRES_PASSWORD=$POSTGRES_PASSWORD
KEYCLOAK_ADMIN_PASSWORD=$KEYCLOAK_ADMIN_PASSWORD
SESSION_SECRET=$SESSION_SECRET
OSTALARI_SETUP=$SETUP
EOF

say "3/5 Запуск"
docker compose up -d --remove-orphans </dev/null

wait_healthy() {
  status=""
  for _ in $(seq 1 120); do
    id="$(docker compose ps -q app 2>/dev/null || true)"
    if [ -n "$id" ]; then status="$(docker inspect --format '{{.State.Health.Status}}' "$id" 2>/dev/null || true)"; fi
    [ "$status" = "healthy" ] && return 0
    sleep 2
  done
  fail "касса не поднялась. Посмотрите журнал: cd $DIR && docker compose logs app"
}

say "4/5 Ждём, пока касса поднимется (первый раз до пары минут)"
wait_healthy

say "5/5 Первый администратор"
# </dev/null обязателен: установщик запускают как «curl … | bash», и без него exec съел бы остаток скрипта из потока.
if ! INIT="$(docker compose exec -T app node tools/selfhost-init.js </dev/null 2>&1)"; then
  say "$INIT"
  fail "не удалось завести администратора. Запустите установщик ещё раз: данные не пострадают."
fi
# Набор модулей приложение читает при старте и держит в памяти: после первой настройки перезапускаем его,
# иначе облачные модули оставались бы включёнными до следующей перезагрузки.
if printf '%s\n' "$INIT" | grep -q '^OSTALARI_INIT=created'; then
  docker compose restart app >/dev/null 2>&1
  wait_healthy
fi

say ""
if printf '%s\n' "$INIT" | grep -q '^OSTALARI_INIT=created'; then
  LOGIN="$(printf '%s\n' "$INIT" | sed -n 's/^OSTALARI_ADMIN_LOGIN=//p')"
  PASSWORD="$(printf '%s\n' "$INIT" | sed -n 's/^OSTALARI_ADMIN_PASSWORD=//p')"
  say "Готово. Касса работает."
  say ""
  say "  Адрес:   http://$ADDRESS"
  say "  Логин:   $LOGIN"
  say "  Пароль:  $PASSWORD"
  say ""
  say "Пароль одноразовый: касса попросит сменить его при первом входе. Больше он нигде не показывается."
  say "После входа откроется первая настройка: название заведения, юрлицо, точка и модули."
else
  say "Готово. Узел обновлён, данные и пароли сохранены."
  say ""
  say "  Адрес:   http://$ADDRESS"
fi
say ""
say "Открывайте адрес с любого планшета или компьютера в этой сети."
say "Резервная копия базы: $DIR/backup.sh (добавьте в cron, подсказка внутри файла)."
say "Обновление: запустите установщик ещё раз."
