#!/usr/bin/env sh
# Установка CapyAgent одной строкой.
#
#   curl -fsSL https://capyagent.ru/install.sh | sh
#
# 🔴 На минимальном Debian или Ubuntu (контейнер, свежий VPS) нет ни curl, ни
# корневых сертификатов, и строка выше падает раньше, чем доходит до этого
# скрипта: «curl: not found». Сначала одна команда от root:
#
#   apt-get update && apt-get install -y curl ca-certificates
#
# 🔴 Скажем прямо, что тут происходит: вы запускаете у себя то, что вам прислал
# наш сервер. Это удобно и это доверие к нам и к TLS - не больше и не меньше.
# Если так не хотите (и правильно, если не хотите):
#
#   curl -fsSLO https://capyagent.ru/install.sh
#   less install.sh          # прочитать
#   sh install.sh
#
# 🔴 Ставит В ДОМАШНИЙ КАТАЛОГ и БЕЗ sudo. Службой на весь сервер - отдельным
# шагом и осознанно: программа получает доступ к вашим системам, и решение
# «пусть работает всегда от имени машины» человек принимает отдельно от
# решения «дай посмотреть».
set -eu

# 🔴 Файлы берутся с нашего канала выпуска capyagent.ru/dl (решение владельца
# 05.09). Выпуски GitHub остаются архивом версий, но репозиторий закрыт, и
# постороннему они отвечают 404 - неотличимо от «выпуска нет».
BAZA="${CAPYAGENT_BASE:-https://capyagent.ru/dl/latest}"
KUDA="${CAPYAGENT_BIN:-$HOME/.local/bin}"

skazat() { printf '%s\n' "$*"; }
upast() { printf '🔴 %s\n' "$*" >&2; exit 1; }

# ---------- какая машина ----------
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$(uname -m)" in
  x86_64|amd64) ARCH=amd64 ;;
  aarch64|arm64) ARCH=arm64 ;;
  *) upast "Не знаю сборки под $(uname -m). Есть linux amd64/arm64 и macOS arm64." ;;
esac
case "$OS" in
  linux|darwin) : ;;
  *) upast "Не знаю сборки под $OS." ;;
esac
IMYA="capyagent-$OS-$ARCH"

# ---------- чем качать ----------
if command -v curl >/dev/null 2>&1; then
  vzyat() { curl -fsSL "$1"; }
  vzyat_v() { curl -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
  vzyat() { wget -qO- "$1"; }
  vzyat_v() { wget -qO "$2" "$1"; }
else
  upast "Нет ни curl, ни wget - нечем скачать."
fi

# 🔴 Сумму проверяем обязательно, и без чем считать не ставим. «Поставлю без
# проверки» здесь означало бы: возьму у сети исполняемый файл и запущу, не
# посмотрев. Отказ честнее.
if command -v sha256sum >/dev/null 2>&1; then
  summa() { sha256sum "$1" | awk '{print $1}'; }
elif command -v shasum >/dev/null 2>&1; then
  summa() { shasum -a 256 "$1" | awk '{print $1}'; }
else
  upast "Нет ни sha256sum, ни shasum - нечем проверить, что файл дошёл целым."
fi

skazat "CapyAgent: ставлю $IMYA"
skazat "  откуда: $BAZA"
skazat "  куда:   $KUDA"

# ---------- что выпущено ----------
VREM=$(mktemp -d)
KUDA_CONNECTORS=""
KUDA_CONNECTORS_OLD=""
KUDA_CONNECTORS_STAGE=""
KUDA_BINARY_STAGE=""
KUDA_BINARY_OLD=""
CONNECTORS_PLACED=0
BINARY_PLACED=0
INSTALL_COMMITTED=0
cleanup() {
  code=$?
  trap - EXIT HUP INT TERM
  if [ "$INSTALL_COMMITTED" -ne 1 ]; then
    if [ "$BINARY_PLACED" -eq 1 ]; then rm -f "$KUDA/capyagent"; fi
    if [ -n "$KUDA_BINARY_OLD" ] && [ -e "$KUDA_BINARY_OLD" ]; then
      mv "$KUDA_BINARY_OLD" "$KUDA/capyagent" 2>/dev/null || true
    fi
    if [ "$CONNECTORS_PLACED" -eq 1 ]; then rm -rf "$KUDA_CONNECTORS"; fi
    if [ -n "$KUDA_CONNECTORS_OLD" ] && [ -e "$KUDA_CONNECTORS_OLD" ]; then
      mv "$KUDA_CONNECTORS_OLD" "$KUDA_CONNECTORS" 2>/dev/null || true
    fi
  fi
  [ -n "$KUDA_CONNECTORS_STAGE" ] && rm -rf "$KUDA_CONNECTORS_STAGE"
  [ -n "$KUDA_BINARY_STAGE" ] && rm -f "$KUDA_BINARY_STAGE"
  rm -rf "$VREM"
  exit "$code"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

# 🔴 Называем самую вероятную причину отказа. Человек, читающий «проверьте
# сеть», будет чинить сеть, даже если дело в сертификатах.
vzyat "$BAZA/checksums.txt" > "$VREM/checksums.txt" 2>/dev/null \
  || upast "Не забрать список сумм с $BAZA.
   Вероятные причины по убыванию:
   1) нет сети или закрыт доступ к $(printf '%s' "$BAZA" | sed 's#^[a-z]*://##; s#/.*##');
   2) нет корневых сертификатов (минимальный Debian/Ubuntu):
      apt-get install -y ca-certificates;
   3) свой адрес задан неверно через CAPYAGENT_BASE."

# Ищем нашу строку. Пути в файле записаны как bin/<имя>.
OZHID=$(grep "/$IMYA\$" "$VREM/checksums.txt" | awk '{print $1}' | head -1 || true)
[ -n "$OZHID" ] || upast "В выпуске нет сборки под $OS/$ARCH."

skazat "  качаю..."
# 🔴 Два адреса, и порядок не случаен. GitHub Releases хранит вложения
# ПЛОСКО: имя вложения не может содержать косую черту, и «.../download/bin/имя»
# отвечает 404 всегда - хотя в checksums.txt пути записаны с bin/. Свой сервер
# отдаёт каталог выпуска как есть: файл лежит в bin/, а рядом, в корне, та же
# жёсткая ссылка под плоским именем для старых клиентов (build-release.sh).
# Пробуем плоское имя, затем bin/ - отказ только после обоих.
vzyat_v "$BAZA/$IMYA" "$VREM/$IMYA" 2>/dev/null \
  || vzyat_v "$BAZA/bin/$IMYA" "$VREM/$IMYA" 2>/dev/null \
  || upast "Не скачать бинарник: ни $BAZA/$IMYA, ни $BAZA/bin/$IMYA не отвечает."

NASHLA=$(summa "$VREM/$IMYA")
[ "$NASHLA" = "$OZHID" ] || {
  printf '🔴 Контрольная сумма не сошлась.\n' >&2
  printf '   пришло:    %s\n' "$NASHLA" >&2
  printf '   ожидалось: %s\n' "$OZHID" >&2
  printf '   Файл по дороге изменился - запускать его нельзя.\n' >&2
  exit 1
}
skazat "  сумма сошлась"

# ---------- встроенные MCP-коннекторы ----------
# Однострочная установка кладёт бинарник отдельно; без этого архива Python
# запустился бы с путём connectors/... которого у человека нет, и MCP отвечал
# бы EOF. Архив и его размер/сумму описывает тот же манифест выпуска, который
# использует обновление ядра.
vzyat "$BAZA/manifest.json" > "$VREM/manifest.json" 2>/dev/null \
  || upast "Не забрать манифест выпуска с $BAZA; бинарник не установлен."
manifest_field() {
  awk -F'"' -v name="$1" '$2 == name { print $4; exit }' "$VREM/manifest.json"
}
KONN_VERSION=$(manifest_field version)
KONN_FILE=$(manifest_field connectors_file)
KONN_SHA=$(manifest_field connectors_sha256)
KONN_SIZE=$(awk -F: '/"connectors_size"/ { gsub(/[^0-9]/, "", $2); print $2; exit }' "$VREM/manifest.json")
case "$KONN_VERSION" in ''|*[!A-Za-z0-9._+-]*) upast "В манифесте некорректная версия набора коннекторов." ;; esac
[ "$KONN_FILE" = "connectors-$KONN_VERSION.tar.gz" ] \
  || upast "В манифесте некорректное имя архива коннекторов."
[ "${#KONN_SHA}" -eq 64 ] \
  || upast "В манифесте некорректная сумма архива коннекторов."
case "$KONN_SHA" in *[!0123456789abcdefABCDEF]*) upast "В манифесте некорректная сумма архива коннекторов." ;; esac
case "$KONN_SIZE" in ''|*[!0-9]*) upast "В манифесте некорректный размер архива коннекторов." ;; esac
[ "$KONN_SIZE" -gt 0 ] || upast "В манифесте пустой архив коннекторов."
vzyat_v "$BAZA/$KONN_FILE" "$VREM/$KONN_FILE" 2>/dev/null \
  || upast "Не скачать архив встроенных коннекторов; бинарник не установлен."
KONN_GOT_SIZE=$(wc -c < "$VREM/$KONN_FILE" | tr -d '[:space:]')
[ "$KONN_GOT_SIZE" = "$KONN_SIZE" ] \
  || upast "Размер архива встроенных коннекторов не сошёлся; бинарник не установлен."
KONN_GOT_SHA=$(summa "$VREM/$KONN_FILE")
[ "$KONN_GOT_SHA" = "$KONN_SHA" ] \
  || upast "Сумма архива встроенных коннекторов не сошлась; бинарник не установлен."
tar -tzf "$VREM/$KONN_FILE" > "$VREM/connectors.list" 2>/dev/null \
  || upast "Архив встроенных коннекторов повреждён; бинарник не установлен."
while IFS= read -r member; do
  case "$member" in connectors|connectors/*) : ;; *) upast "В архиве коннекторов неожиданное содержимое; бинарник не установлен." ;; esac
  case "$member" in /*|../*|*/../*|*/..|..) upast "В архиве коннекторов небезопасный путь; бинарник не установлен." ;; esac
done < "$VREM/connectors.list"
tar -tvzf "$VREM/$KONN_FILE" > "$VREM/connectors.verbose" 2>/dev/null \
  || upast "Не проверить состав архива коннекторов; бинарник не установлен."
awk 'substr($1, 1, 1) !~ /^[-d]$/ { bad=1 } END { exit bad }' "$VREM/connectors.verbose" \
  || upast "В архиве коннекторов есть ссылка или специальный файл; бинарник не установлен."
mkdir -p "$VREM/connectors-stage"
tar -xzf "$VREM/$KONN_FILE" -C "$VREM/connectors-stage" --no-same-owner \
  || upast "Не распаковать архив встроенных коннекторов; бинарник не установлен."
[ -d "$VREM/connectors-stage/connectors" ] \
  || upast "В архиве нет каталога connectors/; бинарник не установлен."
if ! find "$VREM/connectors-stage/connectors" -type f -print | grep -q .; then
  upast "В архиве нет файлов коннекторов; бинарник не установлен."
fi

mkdir -p "$KUDA"
KONN_ROOT="${CAPYAGENT_CONNECTORS_DIR:-$KUDA}"
case "${KONN_ROOT##*/}" in
  connectors) KUDA_CONNECTORS="$KONN_ROOT" ;;
  *) KUDA_CONNECTORS="$KONN_ROOT/connectors" ;;
esac
KONN_PARENT=$(dirname "$KUDA_CONNECTORS")
mkdir -p "$KONN_PARENT"
KUDA_CONNECTORS_OLD="$KONN_PARENT/.connectors.old.$$"
KUDA_CONNECTORS_STAGE="$KONN_PARENT/.connectors.new.$$"
KUDA_BINARY_STAGE="$KUDA/.capyagent.new.$$"
KUDA_BINARY_OLD="$KUDA/.capyagent.old.$$"
[ ! -e "$KUDA_CONNECTORS_OLD" ] && [ ! -L "$KUDA_CONNECTORS_OLD" ] \
  && [ ! -e "$KUDA_CONNECTORS_STAGE" ] && [ ! -L "$KUDA_CONNECTORS_STAGE" ] \
  && [ ! -e "$KUDA_BINARY_STAGE" ] && [ ! -L "$KUDA_BINARY_STAGE" ] \
  && [ ! -e "$KUDA_BINARY_OLD" ] && [ ! -L "$KUDA_BINARY_OLD" ] \
  || upast "В каталоге установки остались промежуточные файлы; сначала проверьте $KUDA."
if { [ -e "$KUDA_CONNECTORS" ] || [ -L "$KUDA_CONNECTORS" ]; } && [ ! -d "$KUDA_CONNECTORS" ]; then
  upast "На месте каталога connectors уже есть не каталог; прежняя установка сохранена."
fi
mkdir "$KUDA_CONNECTORS_STAGE"
if [ -d "$KUDA_CONNECTORS" ]; then
  if find "$KUDA_CONNECTORS" ! -type f ! -type d -print | grep -q .; then
    upast "В каталоге connectors есть ссылка или специальный файл; прежняя установка сохранена."
  fi
  cp -R "$KUDA_CONNECTORS/." "$KUDA_CONNECTORS_STAGE/" \
    || upast "Не сохранить прежние файлы коннекторов; прежняя установка сохранена."
fi
cp -R "$VREM/connectors-stage/connectors/." "$KUDA_CONNECTORS_STAGE/" \
  || upast "Не подготовить файлы встроенных коннекторов; прежняя установка сохранена."
chmod 755 "$VREM/$IMYA"
cp "$VREM/$IMYA" "$KUDA_BINARY_STAGE" \
  || upast "Не подготовить бинарник; прежняя установка сохранена."
chmod 755 "$KUDA_BINARY_STAGE"
if [ -e "$KUDA_CONNECTORS" ] || [ -L "$KUDA_CONNECTORS" ]; then
  mv "$KUDA_CONNECTORS" "$KUDA_CONNECTORS_OLD" \
    || upast "Не отложить прежние коннекторы; установка не изменена."
fi
if ! mv "$KUDA_CONNECTORS_STAGE" "$KUDA_CONNECTORS"; then
  [ ! -e "$KUDA_CONNECTORS_OLD" ] || mv "$KUDA_CONNECTORS_OLD" "$KUDA_CONNECTORS" || true
  upast "Не установить файлы коннекторов; прежняя установка сохранена."
fi
CONNECTORS_PLACED=1
if [ -e "$KUDA/capyagent" ] || [ -L "$KUDA/capyagent" ]; then
  if [ ! -f "$KUDA/capyagent" ] && [ ! -L "$KUDA/capyagent" ]; then
    upast "На месте бинарника capyagent уже есть не файл; прежняя установка сохранена."
  fi
  mv "$KUDA/capyagent" "$KUDA_BINARY_OLD" \
    || upast "Не отложить прежний бинарник; установка не изменена."
fi
if ! mv "$KUDA_BINARY_STAGE" "$KUDA/capyagent"; then
  [ ! -e "$KUDA_BINARY_OLD" ] || mv "$KUDA_BINARY_OLD" "$KUDA/capyagent" || true
  upast "Не установить бинарник; прежняя установка сохранена."
fi
BINARY_PLACED=1
INSTALL_COMMITTED=1
rm -rf "$KUDA_CONNECTORS_OLD"
rm -f "$KUDA_BINARY_OLD"

# ---------- виден ли он ----------
# 🔴 Каталог с программой дописываем в PATH САМИ (02.10, первый запуск
# новичка): строка «добавьте в ~/.profile» оставляла человека без команды
# capyagent, а мастер в конце советует именно «capyagent chat». У root даже
# вход через login shell не находил ~/.local/bin. Пишем в ~/.profile и в файл
# своей оболочки, один раз (повтор установки строку не множит) и одной строкой
# говорим об этом. Блок между метками исполняет тест установщика отдельно.
# >>> put-v-path
# dopisat_put ФАЙЛ СТРОКА - строка в файле ровно один раз; успех, если она там
# есть. Отказ оболочки («cannot create», «Is a directory») молчит: о том, что
# записать не вышло, говорим своими словами ниже.
dopisat_put() {
  if [ -f "$1" ] && grep -qF "$2" "$1" 2>/dev/null; then
    return 0
  fi
  {
    if [ -s "$1" ] && [ -n "$(tail -c1 "$1")" ]; then
      printf '\n' >> "$1" || return 1
    fi
    printf '%s\n' "$2" >> "$1"
  } 2>/dev/null
}
if ! command -v capyagent >/dev/null 2>&1; then
  PUT_STROKA="export PATH=\"\$PATH:$KUDA\" # capyagent"
  PUT_ZAPISANO=""
  PUT_FAYLY=".profile"
  PUT_OBOLOCHKA=$(basename "${SHELL:-sh}")
  case "$PUT_OBOLOCHKA" in
    zsh) PUT_FAYLY="$PUT_FAYLY .zshrc" ;;
    bash) PUT_FAYLY="$PUT_FAYLY .bashrc" ;;
  esac
  [ -f "$HOME/.bash_profile" ] && PUT_FAYLY="$PUT_FAYLY .bash_profile"
  # Имена файлов без пробелов, поэтому разбиение по словам здесь безопасно;
  # домашний каталог с пробелом стоит в кавычках.
  for PUT_FAYL in $PUT_FAYLY; do
    dopisat_put "$HOME/$PUT_FAYL" "$PUT_STROKA" && PUT_ZAPISANO="$PUT_ZAPISANO ~/$PUT_FAYL"
  done
  # 🔴 fish не читает ни ~/.profile, ни ~/.bashrc, а POSIX export в его
  # конфиге - ошибка разбора. Для fish своя строка его синтаксисом в conf.d
  # (fish читает этот каталог сам, config.fish не трогаем). Путь с ' или \ в
  # строку fish не кладём: проще сказать словами.
  PUT_FISH=""
  if [ "$PUT_OBOLOCHKA" = fish ]; then
    PUT_FISH=" (fish: fish_add_path $KUDA)"
    case "$KUDA" in
      *\'*|*\\*) : ;;
      *)
        PUT_FISH_KAT="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d"
        if mkdir -p "$PUT_FISH_KAT" 2>/dev/null &&
          dopisat_put "$PUT_FISH_KAT/capyagent.fish" "contains -- '$KUDA' \$PATH; or set -gx PATH \$PATH '$KUDA' # capyagent"; then
          PUT_ZAPISANO="$PUT_ZAPISANO $PUT_FISH_KAT/capyagent.fish"
          PUT_FISH=""
        fi
        ;;
    esac
  fi
  skazat ""
  if [ -n "$PUT_ZAPISANO" ]; then
    skazat "PATH: дописал $KUDA в$PUT_ZAPISANO; в этом окне зовите $KUDA/capyagent, в новом - просто capyagent$PUT_FISH."
  else
    skazat "🔴 $KUDA не в вашем PATH, и записать его не вышло. Добавьте строку в ~/.profile:"
    skazat "     export PATH=\"\$PATH:$KUDA\""
    skazat "   Или зовите по полному пути: $KUDA/capyagent$PUT_FISH"
  fi
fi
# <<< put-v-path

skazat ""
skazat "Поставлено: $("$KUDA/capyagent" version 2>/dev/null || echo "$KUDA/capyagent")"
skazat ""

# 🔴 Мастер запускаем САМИ, если человек сидит за терминалом.
#
# Установка, которая заканчивается словами «а теперь наберите ещё команду», -
# это лишняя ступенька, и на ней половина уходит. У соседей мастер открывается
# сам, и это не украшение: человек, поставивший программу одной строкой, ждёт,
# что дальше с ним заговорят.
#
# 🔴 Проверяем /dev/tty, а не стандартный ввод: при `curl ... | sh` ввод занят
# самим скриптом, и мастер, смотрящий только на него, откажется работать.
# Проверяем ОТКРЫТИЕМ, а не правами: в контейнере без терминала /dev/tty
# лежит с правами на чтение, но не открывается, и `[ -r /dev/tty ]` пускал
# сюда, чтобы оболочка напечатала «cannot open /dev/tty» вместо нашей
# подсказки.
#
# 🔴 Файл настроек мастер выбирает сам, и выбирает тот, который читает
# программа: без службы - файл каталога данных (`capyagent config path`), а
# если человек согласится запустить агента службой (мастер предлагает это,
# когда настроен бот), - файл окружения службы. Называть ему файл отсюда
# нельзя: до 01.10 скрипт передавал --файл каталога данных, и root, выбравший
# службу, получал настройки там, куда системная служба не смотрит.
#
# 🔴 Провал мастера - не «ничего не записано»: он мог записать настройки и не
# запустить службу (модель не поднялась, бот не ответил). Что именно не так,
# мастер пишет сам строкой выше.
if [ -n "${CAPYAGENT_SETUP_ANSWERS:-}" ]; then
  # Файл ответов: вопросов нет, setup берёт путь из переменной сам.
  "$KUDA/capyagent" setup || upast "Настройка по файлу ответов не закончена: причина выше."
elif ( : </dev/tty ) 2>/dev/null; then
  skazat "Настраиваю. Спрошу про модель и проверю её живьём."
  skazat ""
  "$KUDA/capyagent" setup < /dev/tty || {
    skazat ""
    skazat "Мастер не закончил: что не так, написано выше. Продолжить, когда удобно:"
    skazat "  $KUDA/capyagent setup"
  }
else
  skazat "Дальше одна команда - она спросит про модель, проверит её живьём"
  skazat "и, если настроите бота, запустит агента службой:"
  skazat "  $KUDA/capyagent setup"
fi

skazat ""
skazat "🔴 Что программа умеет и куда ходит - покажет она сама:"
skazat "  $KUDA/capyagent doctor --prove"
