#!/usr/bin/env bash
# Установка и обновление WiseTeam.app одной командой.
#
#   curl -fsSL https://install.team.wiseorg.io | bash
#
# ОДИН СКРИПТ НА ДВА СПОСОБА. Рядом лежит WiseTeam.tar.gz (его кладёт сборка и
# отдаёт релиз на GitHub) - ставим из него. Не лежит - забираем последнюю версию
# из CDN по манифесту. Две копии этого скрипта неизбежно разъехались бы, а
# разъехавшийся установщик ломает установку молча.
#
# ПОЧЕМУ tar.gz, А НЕ dmg. Карантин вешают браузер и Finder, а не сам файл.
# Смонтированный dmg его получает - и человек упирается в диалог Gatekeeper,
# потому что подпись у нас ad-hoc, без Apple Developer ID. Через curl карантин
# не ставится вовсе, поэтому этот путь не показывает диалога ни разу.
# Измерения - в desktop-probe/RESULT.md.
#
# НИКАКИХ ВОПРОСОВ ЧЕЛОВЕКУ. Под `curl | bash` stdin - это сам скрипт, поэтому
# `read` съел бы собственный текст, а `sudo` повис бы на приглашении к паролю
# без единой буквы на экране. Не хватает прав - честно говорим и выходим.
set -euo pipefail

MANIFEST_URL="${WT_MANIFEST_URL:-https://cdn-pub.team.wiseorg.io/desktop/latest.json}"
APPS="${WT_APPS_DIR:-/Applications}"
APP="$APPS/WiseTeam.app"

# Цвет только в терминале: под `| tee` или в лог уехали бы управляющие символы.
if [ -t 1 ]; then
  O='\033[38;2;255;90;31m'   # фирменный оранжевый #ff5a1f
  R='\033[31m'; B='\033[1m'; N='\033[0m'; D='\033[2m'
else
  O=''; R=''; B=''; N=''; D=''
fi

# Марка: «W» с приподнятым правым штрихом, переходящим в хвост - та же фигура,
# что на иконке приложения (apps/web/public/brand/wiseteam-clay-icon.svg).
banner() {
  printf '\n'
  printf "  ${O}█▙   ▟█▙   ▟█${N}\n"
  printf "  ${O} █▙ ▟█ █▙ ▟█${N}\n"
  printf "  ${O}  ▜█▛   ▜█▛ ▀▄${N}\n"
  printf "\n  ${B}WiseTeam${N} ${D}для macOS${N}\n\n"
}

say()  { printf "${O}==>${N} %s\n" "$*"; }
fail() { printf "${R}не вышло:${N} %s\n" "$*" >&2; exit 1; }


# Размер по-человечески: «94 МБ» вместо 98544097.
human() {
  local b="${1:-0}"
  if [ "$b" -ge 1048576 ]; then printf '%s МБ' "$(( b / 1048576 ))"
  elif [ "$b" -ge 1024 ]; then printf '%s КБ' "$(( b / 1024 ))"
  else printf '%s Б' "$b"; fi
}

# Скачивание с индикатором, который ДОПИСЫВАЕТСЯ, а не перерисовывается.
#
# ПОЧЕМУ НЕ `curl --progress-bar`. Он рисует полосу возвратом каретки, и
# терминалы, которые её не перерисовывают (Warp и прочие «блочные»), печатают
# КАЖДОЕ обновление новой строкой - вместо полосы получается простыня на сотню
# экранов. Точки дописываются слева направо и остаются одной строкой в любом
# терминале, потому что управляющих символов в них нет вовсе.
#
# ПОЧЕМУ ПРОЦЕНТЫ ВПЛЕТЕНЫ В ТОЧКИ. Одни точки не отвечают на вопрос «сколько
# осталось»: 84 МБ идут около двух минут, и человек видит растущий ряд, но не
# знает, четверть это или три четверти. Число печатается через каждые 25% ПРЯМО
# в строку - это по-прежнему дописывание, поэтому в блочных терминалах ничего не
# разъезжается.
#
# Сколько скачано, знаем без curl: размер лежит в манифесте, а растущий файл
# видно через `wc`.
# Один шаг индикатора: точка либо число процентов на каждой четверти.
#
# Отдельной функцией, потому что дорисовать хвост в конце надо ТЕМ ЖЕ способом:
# два места с одной логикой однажды разошлись бы, и в конце строки появилось бы
# «100%» дважды или ни разу.
step() {
  local n="$1"
  case "$n" in
    10|20|30|40) printf "%b" "${D}$(( n * 100 / 40 ))%${N}" ;;
    *) printf '%b' "${O}·${N}" ;;
  esac
}

download() {
  local url="$1" out="$2" total="${3:-0}" pid shown=0 cur want
  curl -fL --retry 3 --connect-timeout 15 --no-progress-meter "$url" -o "$out" &
  pid=$!
  printf '    '
  while kill -0 "$pid" 2>/dev/null; do
    if [ "$total" -gt 0 ]; then
      # Проверяем НАЛИЧИЕ файла: перенаправление `< файл` падает в самой
      # оболочке, до запуска wc, и `2>/dev/null` на wc от этого не спасает -
      # первые доли секунды сыпались бы «No such file or directory».
      if [ -f "$out" ]; then cur="$(wc -c < "$out")"; else cur=0; fi
      want=$(( cur * 40 / total ))
      while [ "$shown" -lt "$want" ] && [ "$shown" -lt 40 ]; do
        shown=$(( shown + 1 )); step "$shown"
      done
    fi
    sleep 0.4
  done
  if ! wait "$pid"; then printf '\n'; return 1; fi
  while [ "$shown" -lt 40 ]; do shown=$(( shown + 1 )); step "$shown"; done
  printf '  готово\n'
}

# Долгая работа без собственного вывода: тикаем точками, пока она идёт.
#
# ЗАЧЕМ. Распаковка 195 МБ и ожидание закрытия приложения молчат по десять с
# лишним секунд каждая. Человек в этот момент видит последнюю строку и решает,
# что установщик повис - именно на это и пожаловались. Точка раз в полсекунды
# стоит ничего и отвечает на единственный вопрос: живо оно ещё или нет.
tick_while() {
  local pid="$1"
  printf '    '
  while kill -0 "$pid" 2>/dev/null; do
    printf '%b' "${O}·${N}"
    sleep 0.5
  done
  printf '\n'
  wait "$pid"
}

# --- проверки до единого изменения на диске --------------------------------

banner

[ "$(uname -s)" = "Darwin" ] || fail "WiseTeam есть только под macOS"

# Собираем только под Apple Silicon (dist/mac-arm64). На Intel распакованный
# бандл просто не запустится, и молчаливая установка сломанного приложения
# хуже честного отказа.
[ "$(uname -m)" = "arm64" ] || fail "нужен Mac на Apple Silicon; сборки под Intel у нас нет"

for tool in curl tar shasum; do
  command -v "$tool" >/dev/null 2>&1 || fail "нет $tool"
done

[ -w "$APPS" ] || fail "$APPS закрыт на запись; войдите под учётной записью администратора"

# --- откуда берём приложение -----------------------------------------------

# Под `curl | bash` источника на диске нет вовсе, поэтому спрашиваем про соседний
# архив только когда скрипт запущен файлом.
SELF="${BASH_SOURCE[0]:-}"
LOCAL=""
if [ -n "$SELF" ] && [ -f "$SELF" ]; then
  HERE="$(cd "$(dirname "$SELF")" && pwd)"
  if [ -f "$HERE/WiseTeam.tar.gz" ]; then LOCAL="$HERE/WiseTeam.tar.gz"; fi
fi

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# Достаёт строковое поле из НАШЕГО манифеста.
#
# Разбор регуляркой, а не python3: на чистой macOS его нет, и первое же
# обращение вызвало бы диалог установки инструментов разработчика. Установщик,
# который просит доставить Xcode, установщиком быть перестаёт.
field() { printf '%s' "$1" | sed -n 's/.*"'"$2"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'; }
# Числовое поле идёт БЕЗ кавычек, поэтому у него свой разбор.
numfield() { printf '%s' "$1" | sed -n 's/.*"'"$2"'"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p'; }

if [ -n "$LOCAL" ]; then
  say "ставлю из архива рядом со скриптом"
  ARCHIVE="$LOCAL"
  WANT_SHA=""
  VERSION=""
else
  say "смотрю последнюю версию"
  MANIFEST="$(curl -fsSL --retry 3 --connect-timeout 15 "$MANIFEST_URL")" \
    || fail "не отвечает $MANIFEST_URL"
  VERSION="$(field "$MANIFEST" version)"
  URL="$(field "$MANIFEST" url)"
  WANT_SHA="$(field "$MANIFEST" sha256)"
  [ -n "$VERSION" ] && [ -n "$URL" ] && [ -n "$WANT_SHA" ] || fail "манифест повреждён"

  # Уже стоит нужная версия - незачем тянуть сотню мегабайт. WT_FORCE=1
  # переставляет поверх (пригодится, если бандл побит).
  HAVE=""
  if [ -d "$APP" ]; then
    HAVE="$(/usr/bin/defaults read "$APP/Contents/Info" CFBundleShortVersionString 2>/dev/null || true)"
  fi
  if [ "${WT_FORCE:-0}" != "1" ] && [ -n "$HAVE" ] && [ "$HAVE" = "$VERSION" ]; then
    say "уже стоит $VERSION, ничего делать не надо"
    open "$APP" 2>/dev/null || true
    exit 0
  fi

  SIZE="$(numfield "$MANIFEST" size)"
  say "качаю $VERSION${SIZE:+, $(human "$SIZE")}"
  ARCHIVE="$TMP/WiseTeam.tar.gz"
  download "$URL" "$ARCHIVE" "${SIZE:-0}" || fail "не скачалось: $URL"
fi

# Единственная проверка целостности: бакет публичный, и подписи, по которой
# macOS отличила бы наш бандл от чужого, у нас нет.
if [ -n "$WANT_SHA" ]; then
  GOT="$(shasum -a 256 "$ARCHIVE" | awk '{print $1}')"
  [ "$GOT" = "$WANT_SHA" ] || fail "контрольная сумма не сошлась, файл битый или подменён"
fi

# --- подмена ----------------------------------------------------------------

# Работающее приложение закрываем ПЕРЕД подменой. Снести бандл из-под живого
# процесса macOS позволяет, но процесс остаётся висеть на удалённых файлах и
# ведёт себя необъяснимо, а человек видит «обновилось» и не понимает, почему
# ничего не изменилось.
if pgrep -x WiseTeam >/dev/null 2>&1; then
  say "закрываю приложение"
  # Просим сигналом, а НЕ через osascript.
  #
  # `quit app` приложение игнорирует: у него before-quit отменяет выход всем,
  # кроме пункта в трее - защита от случайного Cmd+Q. Установщик из-за этого
  # ждал десять секунд впустую и добивал тем же сигналом. Свежие сборки на
  # SIGTERM выходят сами и сохраняют состояние; старые он просто снимает, как и
  # раньше. В обоих случаях десяти секунд тишины больше нет.
  # Просим по своей схеме, а не сигналом и не через osascript.
  #
  # `quit app` приложение игнорирует, а SIGTERM Electron проглатывает: у него
  # before-quit отменяет выход всем, кроме пункта в трее - защита от случайного
  # Cmd+Q. Установщик из-за этого ждал десять секунд впустую и добивал процесс,
  # минуя сохранение состояния. Свежие сборки понимают эту ссылку и выходят
  # сами; старые её проигнорируют, и мы снимем их ниже, как и раньше.
  open "wiseteam://quit" >/dev/null 2>&1 || true
  printf '    '
  for _ in $(seq 1 20); do
    pgrep -x WiseTeam >/dev/null 2>&1 || break
    printf '%b' "${O}·${N}"
    sleep 0.5
  done
  printf '\n'
  # Не ушло за десять секунд - снимаем жёстко: заменить бандл под работающим
  # приложением нельзя.
  pgrep -x WiseTeam >/dev/null 2>&1 && pkill -9 -x WiseTeam 2>/dev/null || true
  sleep 1
fi

# Прежнюю версию отодвигаем, а не удаляем: распаковка может оборвать место или
# права, и остаться совсем без приложения человек не должен.
BAK=""
if [ -d "$APP" ]; then
  BAK="${APP}.bak"
  rm -rf "$BAK"
  mv "$APP" "$BAK"
fi

say "распаковываю"
# tar в фоне, чтобы тикать точками: распаковка 195 МБ занимает больше десяти
# секунд и раньше проходила в полной тишине - сразу после неё человек и решал,
# что процесс повис.
tar -xzf "$ARCHIVE" -C "$APPS" &
if ! tick_while $! || [ ! -x "$APP/Contents/MacOS/WiseTeam" ]; then
  rm -rf "$APP"
  if [ -n "$BAK" ]; then mv "$BAK" "$APP"; fi
  fail "распаковать не удалось, прежняя версия на месте"
fi
if [ -n "$BAK" ]; then rm -rf "$BAK"; fi

# Снимаем карантин на случай, если архив всё же приехал браузером.
# ВНИМАНИЕ: именно НЕрекурсивно. Рекурсивный -dr падает с Operation not
# permitted на каждом файле подписанного бандла; Gatekeeper смотрит корень.
xattr -d com.apple.quarantine "$APP" 2>/dev/null || true

# Запуск - любезность, а не часть установки: приложение уже на месте, и
# осечка LaunchServices не повод показывать человеку ошибку и уйти с кодом 1.
say "готово${VERSION:+: WiseTeam $VERSION}"
open "$APP" 2>/dev/null || say "запустите WiseTeam из Launchpad"
