#!/bin/bash
# OKTO Factory Server — one-command bootstrap.
#
# For a real customer the happy path is literally the single command
# OKTO gave you in the onboarding e-mail:
#
#     curl -sSL https://get.oktoterminal.com/server | sudo bash
#
# The hosted endpoint serves this script with the current bundle URL
# already injected, so there are no env vars for the operator to set
# and no YAML files to edit. The script pulls a self-contained
# release tarball (compose files + docker-save'd images), loads it,
# and prints the one-time setup-wizard URL.
#
# When running this file directly (not via the hosted endpoint — e.g.
# from a local checkout, an air-gapped mirror, or during development),
# you MUST supply OKTO_BUNDLE_URL yourself. The script refuses to guess.
#
# Optional environment variables (all safe to leave unset except as
# noted above):
#   OKTO_BUNDLE_URL        URL of the release tarball. Injected by the
#                          hosted `get.oktoterminal.com` endpoint; must
#                          be set manually when invoking this script
#                          directly.
#   OKTO_PUBLIC_HOSTNAME   Public FQDN or IP of this server (default:
#                          primary public/LAN IP, auto-detected).
#   OKTO_ADMIN_EMAIL       Contact for TLS issuance (default:
#                          admin@<hostname>).
#   OKTO_INSTALL_DIR       Target directory (default: /opt/okto-server).
#   OKTO_REPO_FALLBACK     Set to 1 to fall back to `git clone` when the
#                          bundle is unreachable. Meant for OKTO
#                          developers; public users should leave it off.
#   OKTO_REPO              Git URL used by the fallback path. Default
#                          points at the net0ai/okto-linux public repo.
#   OKTO_YES               If set to 1, run non-interactively and assume
#                          the operator wants default wizard values.
#   OKTO_NO_SUPPORT        Set to 1 to OPT OUT of Managed Support. The
#                          installer-gateway bakes a Tailscale auth key
#                          into every script served via
#                          https://get.oktoterminal.com/server; the
#                          installer uses it to join your Tailscale
#                          tailnet (tag:okto-cust-server) so OKTO
#                          engineers can help finish the hardware setup
#                          over SSH. Access is unlimited until you
#                          revoke it — either from the dashboard
#                          (Settings → Управляемая поддержка) or with
#                          `sudo tailscale logout`. Set OKTO_NO_SUPPORT=1
#                          BEFORE running the installer to skip the
#                          Tailscale step entirely.
#   OKTO_PROJECT           Opt-in project flag. Current known value:
#                              mars  — seed the 51-cabinet MARS rollout
#                                      plan on first boot so the dashboard
#                                      "Развёртывание" page is pre-filled
#                                      (LUZ/НОВ/МИР/РНД, DRY+WET). Any
#                                      other value (including unset) leaves
#                                      the plan empty.
#
# Expected bundle layout (tar -tzf):
#     okto-bundle/docker/docker-compose.server.yml
#     okto-bundle/docker/Caddyfile
#     okto-bundle/docker/postgresql.conf
#     okto-bundle/packaging/systemd/okto-server.service
#     okto-bundle/images.tar.gz          (docker save + gzip)
#     okto-bundle/VERSION                (release stamp, e.g. "2026.04.19-001")
#
# What this script does (in order, so a failure mid-way is recoverable):
#   1. Installs Docker + compose plugin (if absent).
#   2. Generates ALL secrets into /etc/okto/server.env (mode 640). Done
#      first so even if later steps fail, /etc/okto/server.env is never
#      stale.
#   3. Downloads the release bundle (default = upstream GitHub Release,
#      overridable via OKTO_BUNDLE_URL).
#   4. `docker load` of the image tarball shipped inside the bundle.
#   5. Installs the okto-server.service systemd unit and starts it.
#   6. Prints the one-time wizard URL + QR code.

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

log()   { echo -e "${CYAN}[okto]${NC} $*"; }
ok()    { echo -e "${GREEN}[ok]${NC}  $*"; }
warn()  { echo -e "${YELLOW}[warn]${NC} $*"; }
die()   { echo -e "${RED}[fail]${NC} $*" >&2; exit 1; }

# OSC 8 clickable hyperlink — all modern terminals (iTerm2, Terminal.app,
# VSCode, Warp, Alacritty, kitty, Windows Terminal, tmux 3.3+) render
# this as a Cmd+Click / Ctrl+Click link. Terminals that don't understand
# OSC 8 just ignore the escape sequence, so the URL still shows as plain
# text — no downside. Falls back to a plain URL if stdout isn't a TTY
# (e.g. piping the installer output to a file).
hyperlink() {
	local url="$1"; local text="${2:-$url}"
	if [ -t 1 ]; then
		printf '\033]8;;%s\033\\%s\033]8;;\033\\' "$url" "$text"
	else
		printf '%s' "$text"
	fi
}

# Install a freedesktop .desktop launcher so the operator sees a
# clickable icon for the dashboard both in their application menu AND on
# every logged-in user's Desktop. No-op on headless servers where no
# /home/*/Desktop directory exists.
#
# Args: 1=display name, 2=URL, 3=Icon (freedesktop name or absolute path
# to a PNG), 4=basename of the .desktop file.
install_desktop_shortcut() {
	local name="$1"; local url="$2"; local icon="$3"; local fname="$4"
	local sys_dir="/usr/share/applications"
	mkdir -p "$sys_dir"
	cat > "$sys_dir/$fname" <<EOF
[Desktop Entry]
Type=Application
Version=1.0
Name=$name
Comment=OKTO · центральная консоль управления парком шкафов
Exec=xdg-open "$url"
Icon=$icon
Terminal=false
Categories=Network;Settings;
StartupNotify=true
EOF
	chmod 644 "$sys_dir/$fname"
	# Refresh the app menu immediately so the operator doesn't have
	# to log out / log in.
	command -v update-desktop-database >/dev/null 2>&1 \
		&& update-desktop-database "$sys_dir" >/dev/null 2>&1 || true
	# Per-user copy on each human user's Desktop. We iterate over /home
	# so the shortcut appears for the actual logged-in operator even
	# when the installer runs via sudo.
	local user_home u
	for user_home in /home/*; do
		[ -d "$user_home" ] || continue
		u="$(basename "$user_home")"
		# Skip system/service users.
		id "$u" >/dev/null 2>&1 || continue
		# Only install on users that already have a Desktop directory
		# (XDG_DESKTOP_DIR), i.e. ones who've logged into the GUI at
		# least once.
		[ -d "$user_home/Desktop" ] || continue
		cp "$sys_dir/$fname" "$user_home/Desktop/$fname"
		chmod +x "$user_home/Desktop/$fname"
		chown "$u:$u" "$user_home/Desktop/$fname" 2>/dev/null || true
		# GNOME 43+ refuses to launch unsigned .desktop files unless
		# we mark them trusted via gio metadata.
		sudo -u "$u" -H gio set "$user_home/Desktop/$fname" "metadata::trusted" true 2>/dev/null || true
	done
}

# Best-effort "open this URL in the operator's default browser" — works
# automatically on three distinct scenarios:
#
#   1. Running on a Linux box with a logged-in desktop session (GNOME,
#      KDE, etc.). We find the active seat's user via `loginctl` and
#      launch `xdg-open` in that user's session. This is rare for
#      production factory servers (usually headless) but common on dev
#      desktops.
#   2. Running under SSH + X forwarding. `$DISPLAY` is set; we try
#      xdg-open as-is.
#   3. Running on macOS dev machines. The `open` binary does the right
#      thing.
#
# Never fatal — if we can't open anything, we just return silently so
# the printed URL is still there for the operator to click/paste.
try_open_browser() {
	local url="$1"
	# macOS
	if command -v open >/dev/null 2>&1 && [ "$(uname -s)" = "Darwin" ]; then
		open "$url" >/dev/null 2>&1 && return 0
	fi
	# Linux with a display
	if command -v xdg-open >/dev/null 2>&1; then
		if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
			xdg-open "$url" >/dev/null 2>&1 && return 0
		fi
		# Find an active graphical session on this seat and launch the
		# browser inside its user environment. Common case: Ubuntu
		# Desktop, operator already logged in, sshing in as root.
		if command -v loginctl >/dev/null 2>&1; then
			local seat_user seat_display
			seat_user="$(loginctl list-sessions --no-legend 2>/dev/null \
				| awk '$3 != "gdm" && $3 != "greeter" {print $3; exit}')"
			if [ -n "${seat_user:-}" ]; then
				seat_display="$(sudo -u "$seat_user" -H sh -c 'echo "${DISPLAY:-:0}"' 2>/dev/null)"
				sudo -u "$seat_user" -H env DISPLAY="$seat_display" xdg-open "$url" >/dev/null 2>&1 && return 0
			fi
		fi
	fi
	return 1
}

# Join this server to the OKTO support tailnet via Tailscale SSH.
#
# Called from the main flow (between "service started" and "print final
# banner") and written so that ANY failure is soft — installing the
# factory-server itself is the mission; Managed Support is a nice-to-
# have on top. If Tailscale's servers are unreachable, if the auth key
# expired, if tailscaled fails to install — we warn, set
# MANAGED_SUPPORT_ENABLED=0, and keep going.
#
# On success the banner at the end of install prints a short block
# telling the customer "support is enabled; disable with `sudo tailscale
# logout` or from the dashboard." Customers who don't want any OKTO
# SSH access set OKTO_NO_SUPPORT=1 before running the installer.
install_managed_support() {
	# Opt-out.
	if [ "${OKTO_NO_SUPPORT:-0}" = "1" ]; then
		log "Управляемая поддержка отключена (OKTO_NO_SUPPORT=1)"
		return 0
	fi
	# Script was served WITHOUT gateway injection (raw S3 / local checkout).
	if [ "${__OKTO_SUPPORT_AUTHKEY__}" = "PLACEHOLDER" ] || [ -z "${__OKTO_SUPPORT_AUTHKEY__}" ]; then
		log "Управляемая поддержка пропущена (скрипт запущен не через get.oktoterminal.com)"
		return 0
	fi
	# Egress sanity check — Tailscale control plane reachable?
	if ! curl -fsS --max-time 5 -o /dev/null https://login.tailscale.com/ 2>/dev/null; then
		warn "Нет доступа к login.tailscale.com — Управляемая поддержка пропущена (сервер без исходящего интернета?)"
		return 0
	fi
	# Install tailscaled if needed. The official one-liner handles every
	# distro we support (Ubuntu 22/24, Debian 12, RHEL/Rocky/Alma 9).
	if ! command -v tailscale >/dev/null 2>&1; then
		log "устанавливаем Tailscale..."
		if ! curl -fsSL https://tailscale.com/install.sh | sh >/dev/null 2>&1; then
			warn "установка Tailscale не удалась — Управляемая поддержка пропущена"
			return 0
		fi
	fi
	# Make sure the daemon is running + on boot.
	systemctl enable --now tailscaled >/dev/null 2>&1 || {
		warn "tailscaled не запустился — Управляемая поддержка пропущена"
		return 0
	}

	# If this is a RE-run and the node is already up, don't churn the
	# connection — every tailscale up rewrites the node's identity on
	# the control plane, which would produce duplicate nodes in the
	# OKTO tailnet over multiple reinstalls.
	if tailscale status --json 2>/dev/null | grep -q '"BackendState":"Running"'; then
		ok "Tailscale уже подключён — Управляемая поддержка осталась включённой с прошлой установки"
		MANAGED_SUPPORT_ENABLED=1
		return 0
	fi

	# Stable hostname. Tailscale defaults to `hostname -s` which gets a
	# numeric suffix on collision; prepending `okto-srv-` makes the OKTO
	# support engineer's `tailscale status` immediately readable.
	local short_host
	short_host="$(hostname -s 2>/dev/null | tr -cd 'a-zA-Z0-9-' | head -c 28)"
	[ -z "$short_host" ] && short_host="srv"
	local ts_hostname="okto-srv-${short_host}"

	log "подключаемся к защищённому каналу поддержки (Tailscale: ${__OKTO_SUPPORT_TAG__})..."
	# --accept-dns=false: don't let Tailscale take over the resolver —
	#   the factory server's Caddy + Postgres + edge DNS should stay on
	#   whatever the operator configured.
	# --ssh: enables Tailscale SSH so the OKTO support engineer can
	#   `tailscale ssh root@host` without exchanging SSH keys.
	# --accept-routes=false: we only need our own node reachable; we
	#   don't want to receive subnet routes advertised by any other
	#   tailnet member.
	if tailscale up \
		--authkey="${__OKTO_SUPPORT_AUTHKEY__}" \
		--ssh \
		--hostname="$ts_hostname" \
		--advertise-tags="${__OKTO_SUPPORT_TAG__}" \
		--accept-dns=false \
		--accept-routes=false \
		--reset >/dev/null 2>&1; then
		local tsip
		tsip="$(tailscale ip -4 2>/dev/null | head -1)"
		ok "Управляемая поддержка включена (Tailscale IP: ${tsip:-?})"
		MANAGED_SUPPORT_ENABLED=1
	else
		warn "tailscale up завершился с ошибкой — Управляемая поддержка выключена"
		warn "проверьте: journalctl -u tailscaled -n 40"
	fi
}
MANAGED_SUPPORT_ENABLED=0

[ "${EUID:-$(id -u)}" -eq 0 ] || die "run as root (sudo bash)"

# === Managed Support — per-customer Tailscale auth key ==================
#
# The two lines below are LITERAL PLACEHOLDERS in the repo and the
# release bundle. They are rewritten at request time by the
# installer-gateway (see factory-server/src/main/kotlin/ru/okto/factory/
# gateway/InstallerRenderer.kt) when a customer runs
# `curl -sSL https://get.oktoterminal.com/server | sudo bash`. The
# rewrite substitutes:
#   __OKTO_SUPPORT_AUTHKEY__  ← a fresh `tskey-auth-...` (single-use,
#                                preauthorized, 24h-to-claim, tagged
#                                `tag:okto-cust-server`).
#   __OKTO_SUPPORT_TAG__      ← `tag:okto-cust-server`.
#
# An installer that bypasses the gateway (local checkout / air-gapped
# mirror / someone curl'ing the raw S3 object) will still have the
# literal "PLACEHOLDER" value here; the Managed Support step below
# detects that and skips Tailscale enrolment. No error, no noise —
# Managed Support simply isn't enabled for that install.
#
# Set OKTO_NO_SUPPORT=1 before running the installer to opt out even
# when the gateway did inject a key. `sudo tailscale logout` revokes
# after-the-fact.
__OKTO_SUPPORT_AUTHKEY__="PLACEHOLDER"
__OKTO_SUPPORT_TAG__="PLACEHOLDER"
# =========================================================================

INSTALL_DIR="${OKTO_INSTALL_DIR:-/opt/okto-server}"

# Canonical public bundle URL baked in at build time. Points at the
# OKTO release bucket (eu-west-1) and always resolves to the current
# okto-bundle.tar.gz the OKTO team publishes. An operator can override
# for air-gapped / mirrored setups by exporting OKTO_BUNDLE_URL.
DEFAULT_BUNDLE_URL="https://releases.oktoterminal.com/okto-bundle.tar.gz"
# CloudFront origin fallback when the custom domain is unreachable (e.g.
# registrar clientHold). Same bucket, no DNS on oktoterminal.com required.
CF_BUNDLE_URL="https://djm55bfyv7u6a.cloudfront.net/okto-bundle.tar.gz"
BUNDLE_URL="${OKTO_BUNDLE_URL:-$DEFAULT_BUNDLE_URL}"

REPO_URL="${OKTO_REPO:-https://github.com/net0ai/okto-linux.git}"
BRANCH="${OKTO_REPO_BRANCH:-main}"

# Prefer the server's public IP over its LAN IP — a cloud VM's
# `hostname -I` returns the private RFC1918 address, which is useless for
# the wizard URL. Fall back to LAN, then loopback.
detect_public_ip() {
	for url in \
		https://checkip.amazonaws.com \
		https://api.ipify.org \
		https://ifconfig.me; do
		ip="$(curl -4 -sf --max-time 5 "$url" 2>/dev/null | tr -d '\n\r[:space:]' || true)"
		[[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] && { echo "$ip"; return; }
	done
	hostname -I 2>/dev/null | awk '{print $1}'
}
PRIMARY_IP="$(detect_public_ip)"
PRIMARY_IP="${PRIMARY_IP:-127.0.0.1}"
PUBLIC_HOSTNAME="${OKTO_PUBLIC_HOSTNAME:-$PRIMARY_IP}"
ADMIN_EMAIL="${OKTO_ADMIN_EMAIL:-admin@$(hostname -f 2>/dev/null || hostname)}"

cat <<'BANNER'
   ____  _  _______ ___
  / __ \| |/ /_   _/ _ \    F A C T O R Y    S E R V E R
 / /_/ /|   / | |/ // /        one-command bootstrap
 \____/_|\_\ /_/\___/
BANNER
echo
log "каталог      : $INSTALL_DIR"
log "адрес сервера: $PUBLIC_HOSTNAME"
log "e-mail админа: $ADMIN_EMAIL"
if [ "${OKTO_REPO_FALLBACK:-0}" = "1" ]; then
	log "источник    : git ($REPO_URL)"
else
	log "архив релиза: $BUNDLE_URL"
fi
echo

# ---------- 1. Docker ----------
if ! command -v docker >/dev/null 2>&1; then
	log "устанавливаем Docker..."
	if [ -f /etc/os-release ]; then . /etc/os-release; else die "cannot detect OS"; fi
	case "${ID:-}" in
		ubuntu|debian)
			apt-get update -qq
			# Core required packages — fail hard if any of these can't install.
			apt-get install -y -qq ca-certificates curl gnupg git openssl
			# Optional: qrencode lives in Ubuntu's `universe` repo which is
			# NOT enabled on barebones cloud AMIs. We try it once, and if
			# it fails, we enable universe and retry; if even that fails
			# (air-gapped apt mirror without universe) we just skip it —
			# the wizard URL is still printed as clickable text.
			apt-get install -y -qq qrencode 2>/dev/null \
				|| { apt-get install -y -qq software-properties-common 2>/dev/null && add-apt-repository -y universe >/dev/null 2>&1 && apt-get update -qq && apt-get install -y -qq qrencode 2>/dev/null; } \
				|| warn "qrencode not available in apt — skipping the QR code in the final banner (URL will still be printed)"
			install -m 0755 -d /etc/apt/keyrings
			curl -fsSL "https://download.docker.com/linux/${ID}/gpg" \
				| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
			chmod a+r /etc/apt/keyrings/docker.gpg
			echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${ID} $(lsb_release -cs) stable" \
				> /etc/apt/sources.list.d/docker.list
			apt-get update -qq
			apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
			;;
		fedora|rhel|centos|rocky|almalinux)
			dnf install -y -q dnf-plugins-core git openssl
			dnf install -y -q qrencode 2>/dev/null \
				|| warn "qrencode not available in dnf — skipping the QR code"
			dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo
			dnf install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin
			;;
		*) die "unsupported distro: ${ID:-unknown}" ;;
	esac
	systemctl enable --now docker
	ok "Docker установлен"
else
	ok "Docker уже установлен ($(docker --version))"
fi

for pkg in git openssl qrencode curl; do
	command -v "$pkg" >/dev/null 2>&1 && continue
	if command -v apt-get >/dev/null; then apt-get install -y -qq "$pkg" || true
	elif command -v dnf >/dev/null; then dnf install -y -q "$pkg" || true
	fi
done

docker compose version >/dev/null 2>&1 || die "docker compose plugin missing"

# ---------- 2. Secrets — upgrade-safe (fresh install OR in-place upgrade)
#
# This block runs on BOTH fresh installs and re-runs. The pattern:
#   * Read every secret from the existing /etc/okto/server.env if present.
#   * If any is still empty, generate a fresh one.
# So a re-run keeps DATABASE_PASSWORD / JWT_SECRET / OKTO_ENROLLMENT_KEY
# stable — which is critical: rotating JWT_SECRET would invalidate every
# user session, and rotating DATABASE_PASSWORD would lock the server out
# of its own Postgres volume until a manual rescue.
#
# OKTO_SETUP_OTP: on UPGRADE we PRESERVE the existing value. Previously
# the installer re-rolled it on every run — technically safe (the wizard
# seals itself on first run, so a rotated OTP is never valid anyway) but
# it broke two reasonable expectations:
#   1. the Quickstart claim that "all secrets are preserved on upgrade"
#      (sha256sum of server.env stayed stable → a nice integrity signal
#      for operators);
#   2. any external automation that happens to read the OTP out of
#      server.env (e.g. a CI script driving an unattended first-run).
# On a FRESH install (no env file), we mint a new OTP as before.
# ------------------------------------------------------------------------
mkdir -p /etc/okto
chmod 750 /etc/okto
ENV_FILE="/etc/okto/server.env"

UPGRADE_MODE=0
if [ -f "$ENV_FILE" ]; then
	UPGRADE_MODE=1
	log "найдена предыдущая установка ($ENV_FILE) — обновляем на месте (секреты и база данных сохраняются)"
fi

prev_get() { grep -E "^${1}=" "$ENV_FILE" 2>/dev/null | tail -1 | cut -d= -f2- || true; }

DB_PASSWORD="$(prev_get DATABASE_PASSWORD)"
JWT_SECRET="$(prev_get JWT_SECRET)"
ENROLLMENT_KEY="$(prev_get OKTO_ENROLLMENT_KEY)"
CLOUD_AUTH_TOKEN="$(prev_get OKTO_CLOUD_AUTH_TOKEN)"
SETUP_OTP="$(prev_get OKTO_SETUP_OTP)"

# Project flag. Honoured by RolloutService in factory-server:
#   OKTO_PROJECT=mars → seed the 51-cabinet MARS plan on first boot.
# Precedence: explicit env from the caller wins; otherwise keep whatever
# was written on a previous install. Any other value (including unset)
# leaves the dashboard's Rollout page in a neutral empty state.
OKTO_PROJECT_VALUE="${OKTO_PROJECT:-$(prev_get OKTO_PROJECT)}"

gen() { openssl rand -base64 "${1:-32}" | tr -d '\n/+=' | cut -c1-40; }
[ -n "$DB_PASSWORD" ]    || { DB_PASSWORD="$(gen 24)";    log "сгенерирован пароль БД"; }
[ -n "$JWT_SECRET" ]     || { JWT_SECRET="$(gen 48)";     log "сгенерирован JWT-секрет"; }
[ -n "$ENROLLMENT_KEY" ] || { ENROLLMENT_KEY="$(gen 24)"; log "сгенерирован ключ регистрации шкафов"; }
if [ -n "$SETUP_OTP" ]; then
	log "одноразовый код мастера сохранён из $ENV_FILE"
else
	SETUP_OTP="$(gen 18)"
	log "сгенерирован одноразовый код мастера"
fi

cat > "$ENV_FILE" <<ENV
# OKTO Factory Server — server-side environment.
OKTO_PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME
OKTO_ADMIN_EMAIL=$ADMIN_EMAIL
OKTO_TLS_MODE=internal
OKTO_SITE_NAME=OKTO Factory
OKTO_DEFAULT_CONNECTION_MODE=VIA_LOCAL_SERVER
OKTO_CLOUD_SYNC_ENABLED=true
OKTO_CLOUD_HOST=app.okto.ru
OKTO_ALLOW_AUTO_ENROLLMENT=false
OKTO_PROJECT=$OKTO_PROJECT_VALUE

# Secrets (generated once; keep this file mode 640).
DATABASE_PASSWORD=$DB_PASSWORD
JWT_SECRET=$JWT_SECRET
OKTO_ENROLLMENT_KEY=$ENROLLMENT_KEY
OKTO_SETUP_OTP=$SETUP_OTP
OKTO_CLOUD_AUTH_TOKEN=$CLOUD_AUTH_TOKEN
ENV
chmod 640 "$ENV_FILE"
ok "сохранено: $ENV_FILE"

# ---------- 3. Fetch assets ----------
mkdir -p "$INSTALL_DIR"

# Install modes, in order of preference:
#   1. `OKTO_BUNDLE_URL` set  → download a self-contained release
#      tarball (compose files + docker-save'd images). Zero calls to
#      Docker Hub or git. This is what the hosted `get.oktoterminal.com`
#      endpoint injects into the script for real users.
#   2. `OKTO_REPO_FALLBACK=1` → git clone + local build. Intended for
#      OKTO developers working against the (currently private) repo.
#   3. Neither set            → hard fail with a short, actionable
#      message. We refuse to guess at a default because the source of
#      truth is OKTO's onboarding, not a URL hard-coded in the script.
use_git_fallback=0
if [ "${OKTO_REPO_FALLBACK:-0}" = "1" ]; then
	use_git_fallback=1
fi

if [ "$use_git_fallback" = "0" ]; then
	log "скачиваем архив релиза OKTO..."
	log "  ссылка: $BUNDLE_URL"
	tmp="$(mktemp -d)"
	trap 'rm -rf "$tmp"' EXIT
	curl_code=0
	curl -fsSL --retry 3 --retry-delay 5 -o "$tmp/bundle.tar.gz" "$BUNDLE_URL" || curl_code=$?
	if [ $curl_code -eq 6 ] && [ "$BUNDLE_URL" != "$CF_BUNDLE_URL" ]; then
		warn "не удалось разрешить $BUNDLE_URL — пробуем CloudFront напрямую ..."
		BUNDLE_URL="$CF_BUNDLE_URL"
		curl -fsSL --retry 3 --retry-delay 5 -o "$tmp/bundle.tar.gz" "$BUNDLE_URL" || curl_code=$?
	fi
	if [ $curl_code -ne 0 ]; then
		# We've already done 3 retries inside curl, so this is a real
		# failure. Give the user an actionable one-liner instead of
		# a debugging essay — the default bundle URL is managed by
		# OKTO and should always be reachable.
		case $curl_code in
			22) die "Bundle URL returned HTTP 4xx/5xx ($BUNDLE_URL). Ask your OKTO contact for a current URL — the one you have may have expired." ;;
			6)  die "Could not resolve the bundle hostname ($BUNDLE_URL). Check DNS / internet access on this server." ;;
			7)  die "Could not connect to the bundle host ($BUNDLE_URL). Check firewall rules for outbound HTTPS." ;;
			28) die "Bundle download timed out ($BUNDLE_URL). Retry, or ask OKTO for a closer mirror." ;;
			*)  die "Bundle download failed (curl exit $curl_code) from $BUNDLE_URL." ;;
		esac
	fi
	tar -xzf "$tmp/bundle.tar.gz" -C "$tmp" \
		|| die "bundle is not a valid gzipped tar archive. The URL may point at an HTML redirect page instead of the raw asset."
	# Flatten the outer `okto-bundle/` dir if present.
	src="$tmp"
	[ -d "$tmp/okto-bundle" ] && src="$tmp/okto-bundle"
	[ -f "$src/docker/docker-compose.server.yml" ] \
		|| die "bundle is missing docker/docker-compose.server.yml — the URL probably points at an unrelated tarball. Ask OKTO support@oktoterminal.com to re-check."
	cp -a "$src/." "$INSTALL_DIR/"
	if [ -f "$INSTALL_DIR/images.tar.gz" ]; then
		log "загружаем Docker-образы из архива..."
		gunzip -c "$INSTALL_DIR/images.tar.gz" | docker load
		rm -f "$INSTALL_DIR/images.tar.gz"
	fi
	ok "архив распакован в $INSTALL_DIR"
else
	# Git fallback — opt-in (OKTO_REPO_FALLBACK=1). Only used by OKTO
	# developers behind the corporate VPN. No public user should ever
	# hit this branch.
	if [ ! -d "$INSTALL_DIR/.git" ]; then
		log "клонируем репозиторий $REPO_URL (режим OKTO_REPO_FALLBACK=1) ..."
		git clone --depth=1 --branch="$BRANCH" "$REPO_URL" "$INSTALL_DIR" \
			|| die "git clone failed — that host is likely private. Use OKTO_BUNDLE_URL for public installs."
	else
		log "обновляем уже склонированный репозиторий..."
		git -C "$INSTALL_DIR" fetch --depth=1 origin "$BRANCH" >/dev/null
		git -C "$INSTALL_DIR" reset --hard "origin/$BRANCH" >/dev/null
	fi
fi
ok "файлы сервера в $INSTALL_DIR"

# ---------- 3.5 Cache terminal artifacts for LAN installs ----------
# Shop-floor terminals in VIA_LOCAL_SERVER mode must NOT depend on Docker
# Hub or releases.oktoterminal.com. The factory server downloads the
# terminal image tarball + manifest once (it has outbound internet during
# install) and serves them locally at /okto-terminal.tar.gz and
# /latest.json. install.sh on each terminal then loads from this host.
if [ -x "$INSTALL_DIR/scripts/cache-terminal-artifacts.sh" ]; then
	log "кэшируем образ терминала для установки по LAN ..."
	_local_origin="http://${PUBLIC_HOSTNAME}"
	bash "$INSTALL_DIR/scripts/cache-terminal-artifacts.sh" "$_local_origin" \
		|| warn "не удалось закэшировать образ терминала — шкафы на LAN не смогут установиться без интернета"
elif [ -f "$INSTALL_DIR/scripts/cache-terminal-artifacts.sh" ]; then
	chmod +x "$INSTALL_DIR/scripts/cache-terminal-artifacts.sh"
	bash "$INSTALL_DIR/scripts/cache-terminal-artifacts.sh" "http://${PUBLIC_HOSTNAME}" \
		|| warn "не удалось закэшировать образ терминала — шкафы на LAN не смогут установиться без интернета"
else
	warn "scripts/cache-terminal-artifacts.sh отсутствует в архиве — пропускаем кэш образа терминала"
fi

# ---------- 4. systemd units ----------
install -m 0644 "$INSTALL_DIR/packaging/systemd/okto-server.service" \
	/etc/systemd/system/okto-server.service
install -m 0644 "$INSTALL_DIR/packaging/systemd/okto-terminal-cache.service" \
	/etc/systemd/system/okto-terminal-cache.service 2>/dev/null \
	|| warn "okto-terminal-cache.service not in bundle — LAN terminal installs need manual cache"
install -m 0644 "$INSTALL_DIR/packaging/systemd/okto-terminal-cache.timer" \
	/etc/systemd/system/okto-terminal-cache.timer 2>/dev/null || true
systemctl daemon-reload
systemctl enable --now okto-terminal-cache.timer >/dev/null 2>&1 \
	|| warn "could not enable okto-terminal-cache.timer"
systemctl start okto-terminal-cache.service >/dev/null 2>&1 \
	|| warn "initial terminal cache failed — floor installs may fail until cache succeeds"

# ---------- 5. Start stack ----------
# Default path — the bundle already did `docker load` in step 3 so every
# referenced image exists locally; compose up will not talk to any
# registry. Git-fallback path (OKTO developers only) has to build from
# source since the public registry copies are OKTO-internal.
if [ "$use_git_fallback" = "1" ]; then
	log "собираем Docker-образы локально из исходников (~5–10 минут)..."
	( cd "$INSTALL_DIR" && docker compose -f docker/docker-compose.server.yml build ) \
		|| die "local build failed. Fix the build errors and re-run the installer."
fi

if [ "$UPGRADE_MODE" = "1" ]; then
	# In-place upgrade path.
	#
	# The bug we're closing here: `systemctl enable --now` is a no-op
	# when the service is already active (ExecStart doesn't re-run),
	# so previously an upgrade would `docker load` fresh images onto
	# disk but never recreate the running containers against the new
	# digests. Users saw a green "обновлено" banner while the old
	# binaries were still serving traffic.
	#
	# Fix: explicitly `docker compose up -d --force-recreate` through
	# the compose file. Compose tears down the old containers (their
	# volumes and networks stay put), then creates new ones on the
	# freshly-loaded `:latest` digests. Adding `--force-recreate` is
	# the belt: even if compose's image-digest comparison has a blind
	# spot, the flag guarantees the swap. systemctl is then told the
	# service is still considered "active" — it was never stopped.
	log "применяем новые образы к запущенным контейнерам..."
	(
		cd "$INSTALL_DIR" &&
		docker compose -f docker/docker-compose.server.yml \
			$( [ -f docker/docker-compose.override.yml ] && echo '-f docker/docker-compose.override.yml' ) \
			up -d --force-recreate --remove-orphans
	) || {
		warn "compose up --force-recreate failed — fallback to systemctl restart"
		systemctl restart okto-server.service || die "systemctl restart failed; inspect 'systemctl status okto-server' and 'docker compose logs'."
	}
	# Make sure systemd considers the unit enabled so it comes up on
	# reboot (no-op if already enabled).
	systemctl enable okto-server.service >/dev/null 2>&1 || true
else
	log "запускаем службу okto-server.service..."
	systemctl enable --now okto-server.service
fi

log "ждём готовности factory-server..."
for i in $(seq 1 60); do
	if docker exec okto-factory-server curl -sf http://localhost:8081/health >/dev/null 2>&1; then
		ok "factory-server готов к работе"
		break
	fi
	sleep 2
done

# ---------- 5.5 Managed Support (soft-fail) ----------
# See install_managed_support() above for the full contract. Runs AFTER
# the factory-server is healthy so that if Tailscale join takes a while,
# the customer doesn't stare at a blank terminal — they've already seen
# "factory-server готов к работе" first.
install_managed_support

# Install the `okto` CLI wrapper (`/usr/local/bin/okto`). Runs whether
# or not Managed Support is actually enabled — the CLI is also useful
# as `okto support status` for diagnostics, and costs ~4 KB on disk.
install_okto_cli() {
	local src=""
	# Try the bundle's copy first (same release semantics as the
	# rest of the compose assets). Falls back to the upstream
	# releases bucket if the bundle didn't ship it yet.
	if [ -f "$INSTALL_DIR/scripts/okto" ]; then
		src="$INSTALL_DIR/scripts/okto"
	else
		tmp_cli="$(mktemp)"
		if curl -fsSL --max-time 10 \
			-o "$tmp_cli" \
			"https://releases.oktoterminal.com/okto-cli"; then
			src="$tmp_cli"
		fi
	fi
	if [ -z "$src" ] || [ ! -s "$src" ]; then
		warn "CLI 'okto' не установлен — используйте 'sudo tailscale logout' для отключения поддержки"
		return 0
	fi
	install -m 0755 "$src" /usr/local/bin/okto
	ok "CLI 'okto' установлен — попробуйте: sudo okto support status"
	# Clean up temp copy if we downloaded one.
	[ -n "${tmp_cli:-}" ] && rm -f "$tmp_cli"
}
install_okto_cli

# ---------- 6. Finish ----------
# URL precedence:
#   - If the operator set OKTO_TLS_MODE=letsencrypt or gave a proper DNS
#     hostname (dot in the middle, not an IP), we print HTTPS first — the
#     cert will be valid and the browser won't complain.
#   - Otherwise we print HTTP first. Internal mode ships with a
#     self-signed cert that modern Chrome flags as "Not Secure" by
#     default; leading with HTTP means the QR code + first-time
#     non-technical user experience doesn't involve a scary red warning
#     page.
WIZARD_URL_HTTPS="https://${PUBLIC_HOSTNAME}/setup?otp=${SETUP_OTP}"
WIZARD_URL_HTTP="http://${PUBLIC_HOSTNAME}/setup?otp=${SETUP_OTP}"

looks_like_ip=0
[[ "$PUBLIC_HOSTNAME" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] && looks_like_ip=1

# Read the VERSION that just got loaded in step 3. Bundle puts it at the
# root of the tarball, so it's now at $INSTALL_DIR/VERSION (possibly
# missing on dev builds without a release.sh invocation — fall back).
INSTALLED_VERSION="$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo 'dev')"

# Install a desktop launcher so the operator can reopen the dashboard
# from the Activities / application grid later without remembering the
# IP. Icon name `preferences-system-network` is in every standard
# desktop theme (Adwaita, Papirus, Yaru…), so we don't ship a PNG.
if [ "$looks_like_ip" = 1 ]; then
	DASHBOARD_URL="http://${PUBLIC_HOSTNAME}/"
else
	DASHBOARD_URL="https://${PUBLIC_HOSTNAME}/"
fi
install_desktop_shortcut "OKTO — Центральная консоль" "$DASHBOARD_URL" "preferences-system-network" "okto-dashboard.desktop"

echo
echo -e "${GREEN}========================================================${NC}"
if [ "$UPGRADE_MODE" = "1" ]; then
	echo -e "${GREEN}  OKTO Factory Server обновлён до версии $INSTALLED_VERSION${NC}"
else
	echo -e "${GREEN}  OKTO Factory Server $INSTALLED_VERSION запущен${NC}"
fi
echo -e "${GREEN}========================================================${NC}"
echo
if [ "$UPGRADE_MODE" = "1" ]; then
	echo "  Все настройки, пользователи и зарегистрированные шкафы"
	echo "  сохранены. Дашборд доступен по прежнему адресу:"
	echo
	if [ "$looks_like_ip" = 1 ]; then
		DASH_URL="http://${PUBLIC_HOSTNAME}/"
	else
		DASH_URL="https://${PUBLIC_HOSTNAME}/"
	fi
	printf '    '; hyperlink "$DASH_URL"; echo
	echo
	echo "  Что нового в этом релизе:"
	printf '    '; hyperlink "https://github.com/net0ai/okto-linux/releases"; echo
else
	# Prefer HTTP for IP-based hostnames (no self-signed TLS warning in
	# the browser) and HTTPS for DNS hostnames (real cert expected).
	if [ "$looks_like_ip" = 1 ]; then
		WIZARD_URL_PRIMARY="$WIZARD_URL_HTTP"
		WIZARD_URL_ALT="$WIZARD_URL_HTTPS"
		WIZARD_ALT_LABEL="или по HTTPS с самоподписанным сертификатом"
	else
		WIZARD_URL_PRIMARY="$WIZARD_URL_HTTPS"
		WIZARD_URL_ALT="$WIZARD_URL_HTTP"
		WIZARD_ALT_LABEL="или по HTTP"
	fi

	echo "  Откройте мастер первого запуска в браузере:"
	echo
	printf '    '; hyperlink "$WIZARD_URL_PRIMARY"; echo
	echo
	echo "  ($WIZARD_ALT_LABEL:"
	printf '   '; hyperlink "$WIZARD_URL_ALT"; echo
	echo "  )"
	echo

	# Best-effort: if the operator ran the installer on a desktop box
	# (or via SSH X-forwarding), punch the URL straight into their
	# browser. Silent no-op on headless servers.
	if try_open_browser "$WIZARD_URL_PRIMARY"; then
		ok "открыл мастер в вашем браузере автоматически — можно больше ничего не копировать"
		echo
	fi

	if command -v qrencode >/dev/null 2>&1; then
		echo "  Или наведите камеру телефона на QR-код — он ведёт на ту же ссылку:"
		echo
		qrencode -t ANSIUTF8 -m 1 "$WIZARD_URL_PRIMARY"
	fi
	echo
	echo "  Одноразовый код мастера действителен только до его запечатывания."
fi
echo
echo "  Управление службой:"
echo "    systemctl status okto-server"
echo "    docker compose -f ${INSTALL_DIR}/docker/docker-compose.server.yml logs -f"
echo

# ---------- 7. Managed Support banner ----------
# Only shown when install_managed_support() actually succeeded — a
# failed or opt-out'd install prints nothing here so the operator isn't
# confused about a feature that isn't live.
if [ "$MANAGED_SUPPORT_ENABLED" = "1" ]; then
	cat <<BANNER

${GREEN}========================================================${NC}
  Управляемая поддержка: ВКЛЮЧЕНА
${GREEN}========================================================${NC}

  Инженеры OKTO могут подключиться к этому серверу по
  защищённому каналу Tailscale SSH, чтобы помочь с настройкой
  оборудования (принтеры, сканеры, MODBUS, ИБП, СКУД и т. п.).

  Что это значит:
    * Канал активен всегда — мы не отключаем его по таймеру.
    * SSH-доступ только у авторизованных инженеров OKTO
      (двухфакторная аутентификация + аудит-лог).
    * Клиентские серверы изолированы друг от друга — с вашего
      сервера нельзя попасть на сервер другого клиента.

  Отключить в любой момент:
    * Быстро:        sudo okto support stop
    * Посмотреть:    sudo okto support status
    * В дашборде:    Настройки → Управляемая поддержка → Отключить
    * Навсегда:      переустановите сервер с OKTO_NO_SUPPORT=1

  Контакт поддержки: support@okto.ru

${GREEN}========================================================${NC}

BANNER
fi
