#!/bin/sh
# =============================================================================
#  CloudGram · CeP — protocol node installer  (POSIX sh; run as sh)
#
#  How it is invoked (see server/index.js):
#    curl -fsSL https://cloudgram.ru/cep/install | CG_NODE_DOMAIN=<domain> sh   (own protocol)
#    curl -fsSL https://cloudgram.ru/cep/install | CG_ENROLL=<code> sh          (CloudFather node)
#
#  Own-protocol mode (no CG_ENROLL, no manual keys): nothing is registered on cloudgram.ru.
#  The agent runs on THIS server on 127.0.0.1:8787 behind your own TLS web server; the owner then
#  connects it in @ceprotocol ("Connect your own protocol") and proves ownership of the domain.
#
#  What it does, step by step:
#    1) shows and records acceptance of the protocol license/terms;
#    2) registers the node on the server (POST /api/node/enroll with CG_ENROLL) and
#       gets an id, a secret, an upstream and a path — the secret is not written to a file;
#    3) downloads the agent (/api/node/agent) and the ws dependency;
#    4) starts a systemd service (or a one-off process if there is no systemd);
#    5) asks the server to verify the node (/api/node/verify) and it appears in
#       CloudFather as "awaiting confirmation".
#
#  Environment variables:
#    CG_ENROLL       enrollment code from CloudFather (bare-enroll mode);
#    CG_ORIGIN       server address (default https://cloudgram.ru);
#    CG_NODE_DOMAIN  this node's domain (otherwise the server records it by IP);
#    CG_NODE_PORT    node port (default 8787);
#    CG_ASSUME_YES   1 — do not ask for confirmations (for CI);
#    CG_VERIFY_TOKEN code from @ceprotocol, saved to <dir>/cep-verify.txt and served by the agent
#                    at /.well-known/cloudgram-cep.txt (ownership proof);
#    CG_NODE_HOST    listen address (own-protocol mode: 127.0.0.1, otherwise 0.0.0.0);
#    CG_NODE_PATH    listen path (default: random, kept across re-runs).
#  Manual mode without enroll (node keys known in advance): set
#    CG_NODE_ID + CG_NODE_SECRET + CG_UPSTREAM — enroll will be skipped.
#
#  NOTE: by installing and running a node, you accept the LICENSE and
#  PROTOCOL_TERMS. Details are below and in those files.
# =============================================================================

# POSIX-strict mode (no bashism pipefail): fail on the first error.
set -eu

# ---------------------------------------------------------------------------
#  Parameters
# ---------------------------------------------------------------------------
CG_ORIGIN="${CG_ORIGIN:-https://cloudgram.ru}"
CG_ENROLL="${CG_ENROLL:-}"
CG_NODE_DOMAIN="${CG_NODE_DOMAIN:-}"
CG_NODE_PORT="${CG_NODE_PORT:-8787}"
CG_ASSUME_YES="${CG_ASSUME_YES:-0}"
CG_NODE_ID="${CG_NODE_ID:-}"
CG_NODE_SECRET="${CG_NODE_SECRET:-}"
CG_UPSTREAM="${CG_UPSTREAM:-}"
CG_VERIFY_TOKEN="${CG_VERIFY_TOKEN:-}"
CG_NODE_HOST="${CG_NODE_HOST:-}"
CG_NODE_PATH="${CG_NODE_PATH:-}"
INSTALL_DIR="${CG_NODE_DIR:-/opt/cg-node}"

# ---------------------------------------------------------------------------
#  Colors. Turned off if stdout is not a terminal (pipe/log) — to avoid cluttering the output.
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
  C_RESET=$(printf '\033[0m');   C_DIM=$(printf '\033[2m');    C_BOLD=$(printf '\033[1m')
  C_CYAN=$(printf '\033[38;5;44m'); C_BLUE=$(printf '\033[38;5;39m'); C_PURP=$(printf '\033[38;5;135m')
  C_GREEN=$(printf '\033[38;5;42m'); C_RED=$(printf '\033[38;5;203m'); C_GREY=$(printf '\033[38;5;244m')
  HIDE_CUR=$(printf '\033[?25l'); SHOW_CUR=$(printf '\033[?25h')
else
  C_RESET=; C_DIM=; C_BOLD=; C_CYAN=; C_BLUE=; C_PURP=; C_GREEN=; C_RED=; C_GREY=; HIDE_CUR=; SHOW_CUR=
fi
# On any exit restore the cursor (after Ctrl-C the terminal does not go "blind").
cleanup() { printf '%s' "$SHOW_CUR"; }
trap cleanup EXIT INT TERM

die() { printf '\n  %s✗ %s%s\n' "$C_RED" "$1" "$C_RESET" >&2; exit 1; }

# ---------------------------------------------------------------------------
#  ANIMATIONS (the mesmerizing part) — all in POSIX printf/sleep
# ---------------------------------------------------------------------------

# "Typewriter": text word by word (word-by-word display is safe for any
# locale — unlike cutting by characters, which breaks UTF-8 in the C locale).
type_out() {
  _first=1
  for _w in $1; do
    [ "$_first" = 1 ] && printf '  ' && _first=0 || printf ' '
    printf '%s' "$_w"; sleep 0.05
  done
  printf '\n'
}

# Wave banner: lines "come alive" with a light blue→blue→violet gradient.
banner() {
  printf '%s\n' "$HIDE_CUR"
  printf '  %s   ██████╗██╗      ██████╗ ██╗   ██╗██████╗  ██████╗ ██████╗  █████╗ ███╗   ███╗%s\n' "$C_CYAN" "$C_RESET"; sleep 0.05
  printf '  %s  ██╔════╝██║     ██╔═══██╗██║   ██║██╔══██╗██╔════╝ ██╔══██╗██╔══██╗████╗ ████║%s\n' "$C_CYAN" "$C_RESET"; sleep 0.05
  printf '  %s  ██║     ██║     ██║   ██║██║   ██║██║  ██║██║  ███╗██████╔╝███████║██╔████╔██║%s\n' "$C_BLUE" "$C_RESET"; sleep 0.05
  printf '  %s  ██║     ██║     ██║   ██║██║   ██║██║  ██║██║   ██║██╔══██╗██╔══██║██║╚██╔╝██║%s\n' "$C_BLUE" "$C_RESET"; sleep 0.05
  printf '  %s  ╚██████╗███████╗╚██████╔╝╚██████╔╝██████╔╝╚██████╔╝██║  ██║██║  ██║██║ ╚═╝ ██║%s\n' "$C_PURP" "$C_RESET"; sleep 0.05
  printf '  %s   ╚═════╝╚══════╝ ╚═════╝  ╚═════╝ ╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝%s\n' "$C_PURP" "$C_RESET"
  printf '           %sCeP · защищённый транспорт CloudGram%s\n\n' "$C_GREY" "$C_RESET"
  printf '%s' "$SHOW_CUR"
}

# Braille spinner: spins while the given command runs in the background.
#   spin "Label" command arguments...
spin() {
  _label="$1"; shift
  _frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'; _i=0
  printf '%s' "$HIDE_CUR"
  "$@" & _pid=$!
  while kill -0 "$_pid" 2>/dev/null; do
    _n=$(( _i % 10 )); _f=$(printf '%s' "$_frames" | cut -c$(( _n + 1 )))
    printf '\r  %s%s%s %s' "$C_CYAN" "$_f" "$C_RESET" "$_label"
    _i=$(( _i + 1 )); sleep 0.08
  done
  if wait "$_pid"; then
    printf '\r  %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$_label"
  else
    printf '\r  %s✗%s %s\n' "$C_RED" "$C_RESET" "$_label"; printf '%s' "$SHOW_CUR"; return 1
  fi
  printf '%s' "$SHOW_CUR"
}

# Smooth progress bar 0→100 (purely visual).
progress() {
  _label="$1"; _w=34; _p=0
  printf '%s' "$HIDE_CUR"
  while [ "$_p" -le 100 ]; do
    _fill=$(( _p * _w / 100 )); _empty=$(( _w - _fill ))
    _bar=$(printf "%${_fill}s" '' | tr ' ' '#'); _bar="$_bar$(printf "%${_empty}s" '' | tr ' ' '.')"
    printf '\r  %s%s%s [%s%s%s] %3d%%' "$C_BOLD" "$_label" "$C_RESET" "$C_BLUE" "$_bar" "$C_RESET" "$_p"
    _p=$(( _p + 5 )); sleep 0.02
  done
  printf '\r  %s%s%s [%s%s%s] %s100%%%s\n' "$C_BOLD" "$_label" "$C_RESET" \
         "$C_GREEN" "$(printf "%${_w}s" '' | tr ' ' '#')" "$C_RESET" "$C_GREEN" "$C_RESET"
  printf '%s' "$SHOW_CUR"
}

# HTTP helper: prints the response body. Needs curl or wget.
http_get()  { if command -v curl >/dev/null 2>&1; then curl -fsSL "$1"; else wget -qO- "$1"; fi; }
http_post() { # $1 url  $2 json
  if command -v curl >/dev/null 2>&1; then
    curl -fsSL -X POST -H 'Content-Type: application/json' -d "$2" "$1"
  else
    wget -qO- --header='Content-Type: application/json' --post-data="$2" "$1"
  fi
}
# Extract a string field from flat JSON without jq.
json_str() { sed -n 's/.*"'"$2"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<EOF
$1
EOF
}

# ---------------------------------------------------------------------------
#  STEP 0. Banner + license acceptance
# ---------------------------------------------------------------------------
banner
type_out "  Установщик узла CeP. Узел — «труба»: он пересылает зашифрованные"
type_out "  байты на сервер, но НЕ читает и НЕ может подменить трафик."
printf '\n'
printf '  %sЛицензия:%s используя протокол, вы принимаете LICENSE и PROTOCOL_TERMS.\n' "$C_BOLD" "$C_RESET"
printf '  %sЗакрытый протокол. Копировать код в чужой продукт нельзя. В код зашиты\n' "$C_GREY"
printf '  метки происхождения. Подробности — в файлах LICENSE и PROTOCOL_TERMS.%s\n\n' "$C_RESET"

# Consent. In a pipe (curl|sh) there is no tty — we treat the mere fact of running with
# the enrollment code CG_ENROLL as consent. In an interactive terminal we ask explicitly.
if [ "$CG_ASSUME_YES" != "1" ] && [ -t 0 ]; then
  printf '  Принять условия и продолжить установку? [y/N] '
  read reply || reply=""
  case "$reply" in
    y|Y|yes|YES|да|Да) : ;;
    *) die "Установка отменена — условия не приняты." ;;
  esac
fi
printf '  %s✓%s Условия приняты (%s)\n\n' "$C_GREEN" "$C_RESET" "$(date -u +%FT%TZ 2>/dev/null || date)"

# ---------------------------------------------------------------------------
#  STEP 1. Node registration (bare-enroll), if keys are not given manually
# ---------------------------------------------------------------------------
NODE_PATH=""
MODE="enroll"
if [ -n "$CG_NODE_ID" ] && [ -n "$CG_NODE_SECRET" ] && [ -n "$CG_UPSTREAM" ]; then
  MODE="manual"
  printf '  %s•%s Ручной режим: ключи узла заданы окружением, enroll пропущен.\n' "$C_GREY" "$C_RESET"
elif [ -z "$CG_ENROLL" ]; then
  # Own protocol on this server: no registration on cloudgram.ru. Keep the path stable across
  # re-runs, otherwise the address already connected in @ceprotocol would break.
  MODE="self"
  printf '  %s•%s Свой протокол: узел работает на этом сервере, на cloudgram.ru ничего не регистрируется.\n' "$C_GREY" "$C_RESET"
  if [ -n "$CG_NODE_PATH" ]; then
    NODE_PATH="/$(printf '%s' "$CG_NODE_PATH" | sed 's#^/*##' | tr -cd 'A-Za-z0-9._~-' | cut -c1-64)"
  elif [ -s "$INSTALL_DIR/node-path" ]; then
    NODE_PATH=$(head -1 "$INSTALL_DIR/node-path" | tr -cd 'A-Za-z0-9._~/-')
  fi
  [ "$NODE_PATH" = "/" ] && NODE_PATH=""
  [ -n "$NODE_PATH" ] || NODE_PATH="/$(od -An -N6 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || echo cep)"
  [ -n "$CG_NODE_HOST" ] || CG_NODE_HOST="127.0.0.1"
else
  # Our own random listen path — the server and the agent agree on the address.
  NODE_PATH="/$(od -An -N6 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || echo cep)"
  ENROLL_JSON="{\"token\":\"$CG_ENROLL\",\"host\":\"$CG_NODE_DOMAIN\",\"path\":\"$NODE_PATH\"}"
  RESP=""
  do_enroll() { RESP=$(http_post "$CG_ORIGIN/api/node/enroll" "$ENROLL_JSON"); }
  spin "Регистрирую узел на сервере" do_enroll || die "Enroll не прошёл (проверьте CG_ENROLL и доступность $CG_ORIGIN)."
  CG_NODE_ID=$(json_str "$RESP" id)
  CG_NODE_SECRET=$(json_str "$RESP" secret)
  CG_UPSTREAM=$(json_str "$RESP" upstream)
  RP=$(json_str "$RESP" path); [ -n "$RP" ] && NODE_PATH="$RP"
  [ -n "$CG_NODE_ID" ] && [ -n "$CG_NODE_SECRET" ] || die "Сервер не выдал ключи узла (ответ: $RESP)"
  printf '     %sid=%s  upstream=%s%s\n' "$C_DIM" "$CG_NODE_ID" "$CG_UPSTREAM" "$C_RESET"
fi
[ -n "$CG_UPSTREAM" ] || CG_UPSTREAM="wss://cloudgram.ru/cep"
[ -n "$CG_NODE_HOST" ] || CG_NODE_HOST="0.0.0.0"

# Ownership code for @ceprotocol: from the environment or, in an interactive terminal, asked here.
if [ "$MODE" = "self" ] && [ -z "$CG_VERIFY_TOKEN" ] && [ "$CG_ASSUME_YES" != "1" ] && [ -t 0 ]; then
  printf '  Код подтверждения из @ceprotocol (Enter — позже): '
  read CG_VERIFY_TOKEN || CG_VERIFY_TOKEN=""
fi
CG_VERIFY_TOKEN=$(printf '%s' "$CG_VERIFY_TOKEN" | tr -cd 'A-Za-z0-9_-' | cut -c1-128)

# ---------------------------------------------------------------------------
#  STEP 2. Node.js
# ---------------------------------------------------------------------------
ensure_node() {
  command -v node >/dev/null 2>&1 && return 0
  if   command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq nodejs npm >/dev/null 2>&1
  elif command -v dnf     >/dev/null 2>&1; then dnf install -y -q nodejs npm >/dev/null 2>&1
  elif command -v yum     >/dev/null 2>&1; then yum install -y -q nodejs npm >/dev/null 2>&1
  elif command -v apk     >/dev/null 2>&1; then apk add --no-cache nodejs npm >/dev/null 2>&1
  else return 1; fi
}
spin "Проверяю Node.js" ensure_node || die "Не удалось установить Node.js — поставьте вручную и повторите."
printf '     %snode %s%s\n' "$C_DIM" "$(node -v 2>/dev/null || echo '?')" "$C_RESET"

# ---------------------------------------------------------------------------
#  STEP 3. Directory, agent, ws dependency
# ---------------------------------------------------------------------------
mkdir -p "$INSTALL_DIR"
fetch_agent() { http_get "$CG_ORIGIN/api/node/agent" > "$INSTALL_DIR/cg-node-agent.js"; }
spin "Скачиваю агент" fetch_agent || die "Не удалось скачать агент с $CG_ORIGIN/api/node/agent"
# Sanity check: the agent is JS with a node shebang, not HTML (SPA fallback). If HTML
# came back, the address is wrong; we do not run garbage as code.
head -1 "$INSTALL_DIR/cg-node-agent.js" | grep -q 'usr/bin/env node' \
  || die "С $CG_ORIGIN/api/node/agent пришёл не агент (похоже на HTML). Проверьте CG_ORIGIN."

if [ "$MODE" = "self" ]; then
  printf '%s\n' "$NODE_PATH" > "$INSTALL_DIR/node-path"
  if [ -n "$CG_VERIFY_TOKEN" ]; then
    printf '%s\n' "$CG_VERIFY_TOKEN" > "$INSTALL_DIR/cep-verify.txt"
    chmod 644 "$INSTALL_DIR/cep-verify.txt" 2>/dev/null || true
  fi
fi

install_ws() { ( cd "$INSTALL_DIR" && { [ -f package.json ] || npm init -y >/dev/null 2>&1; } && npm install ws >/dev/null 2>&1 ); }
spin "Ставлю зависимость ws" install_ws || printf '     %s(агент доустановит ws сам при старте)%s\n' "$C_DIM" "$C_RESET"

progress "Готовлю окружение"

# ---------------------------------------------------------------------------
#  STEP 4. Launch: systemd if present; otherwise a one-off background process
# ---------------------------------------------------------------------------
if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then
  {
    echo "[Unit]"
    echo "Description=CloudGram CeP node"
    echo "After=network-online.target"
    echo "Wants=network-online.target"
    echo ""
    echo "[Service]"
    echo "Type=simple"
    echo "WorkingDirectory=$INSTALL_DIR"
    echo "ExecStart=$(command -v node) $INSTALL_DIR/cg-node-agent.js"
    printf 'Environment=CG_UPSTREAM=%s CG_NODE_ID=%s CG_NODE_SECRET=%s CG_NODE_PORT=%s CG_NODE_PATH=%s CG_NODE_HOST=%s\n' \
           "$CG_UPSTREAM" "$CG_NODE_ID" "$CG_NODE_SECRET" "$CG_NODE_PORT" "$NODE_PATH" "$CG_NODE_HOST"
    echo "Restart=always"
    echo "RestartSec=3"
    echo ""
    echo "[Install]"
    echo "WantedBy=multi-user.target"
  } > /etc/systemd/system/cloudgram-node.service
  start_svc() { systemctl daemon-reload && systemctl enable cloudgram-node.service >/dev/null 2>&1 && systemctl restart cloudgram-node.service >/dev/null 2>&1; }
  spin "Регистрирую systemd-сервис" start_svc || die "Не удалось запустить сервис."
  RUN_HINT="systemctl status cloudgram-node · journalctl -u cloudgram-node -f"
else
  ( cd "$INSTALL_DIR" && CG_UPSTREAM="$CG_UPSTREAM" CG_NODE_ID="$CG_NODE_ID" \
      CG_NODE_SECRET="$CG_NODE_SECRET" CG_NODE_PORT="$CG_NODE_PORT" CG_NODE_PATH="$NODE_PATH" \
      CG_NODE_HOST="$CG_NODE_HOST" nohup node cg-node-agent.js >"$INSTALL_DIR/node.log" 2>&1 & )
  RUN_HINT="лог: $INSTALL_DIR/node.log"
fi

# ---------------------------------------------------------------------------
#  STEP 5. Ask the server to verify the node (it will appear in CloudFather)
# ---------------------------------------------------------------------------
if [ -n "$CG_NODE_ID" ] && [ -n "$CG_NODE_SECRET" ]; then
  verify() { http_post "$CG_ORIGIN/api/node/verify" "{\"id\":\"$CG_NODE_ID\",\"secret\":\"$CG_NODE_SECRET\"}" >/dev/null 2>&1 || true; }
  sleep 2  # дать службе подняться
  spin "Проверяю узел на сервере" verify || true
fi

# ---------------------------------------------------------------------------
#  Done
# ---------------------------------------------------------------------------
printf '\n  %s%s УЗЕЛ ПОДНЯТ%s\n' "$C_GREEN" "$C_BOLD" "$C_RESET"
printf '  %s├─%s upstream : %s\n' "$C_GREY" "$C_RESET" "$CG_UPSTREAM"
if [ "$MODE" = "self" ]; then
  SHOW_DOMAIN="${CG_NODE_DOMAIN:-ваш-домен}"
  printf '  %s├─%s слушает  : %s:%s%s\n' "$C_GREY" "$C_RESET" "$CG_NODE_HOST" "$CG_NODE_PORT" "$NODE_PATH"
  printf '  %s├─%s адрес    : %swss://%s%s%s\n' "$C_GREY" "$C_RESET" "$C_BOLD" "$SHOW_DOMAIN" "$NODE_PATH" "$C_RESET"
  printf '  %s└─%s %s\n\n' "$C_GREY" "$C_RESET" "$RUN_HINT"
  printf '  Дальше:\n'
  printf '   1) TLS на 443 перед агентом — установщик его НЕ настраивает. Пример /etc/caddy/Caddyfile\n'
  printf '      (Caddy сам получит сертификат Let%ss Encrypt):\n' "'"
  printf '        %s {\n          reverse_proxy %s:%s\n        }\n' "$SHOW_DOMAIN" "$CG_NODE_HOST" "$CG_NODE_PORT"
  printf '   2) В CloudGram: @ceprotocol → «Подключить свой протокол» → пришлите адрес выше.\n'
  printf '   3) Код подтверждения от бота: echo <код> | sudo tee %s/cep-verify.txt\n' "$INSTALL_DIR"
  if [ -n "$CG_VERIFY_TOKEN" ]; then
    printf '      %s(код уже записан в %s/cep-verify.txt)%s\n' "$C_DIM" "$INSTALL_DIR" "$C_RESET"
  fi
  printf '\n  %sПротокол работает только на вашем сервере — CloudGram его не размещает.%s\n\n' "$C_DIM" "$C_RESET"
else
  printf '  %s├─%s узел id  : %s\n' "$C_GREY" "$C_RESET" "$CG_NODE_ID"
  printf '  %s├─%s порт     : %s\n' "$C_GREY" "$C_RESET" "$CG_NODE_PORT"
  printf '  %s└─%s %s\n' "$C_GREY" "$C_RESET" "$RUN_HINT"
  printf '  %sУзел появится в CloudFather → «Серверы» как «ожидает подтверждения».%s\n\n' "$C_DIM" "$C_RESET"
fi
