#!/bin/sh
# LogiShell installer — curl -fsSL logishell.com/get | sh
#
# The address on the site is /get: one door for every system, and the worker
# behind it hands PowerShell its own script (apps/landing/src/worker.ts). This
# file is still served at /install.sh too — old copies of the command in blog
# posts and terminals must not break.
#
# THIS FILE IS THE SOURCE. The served copy lives at apps/landing/public/install.sh;
# keep them identical (docs/INSTALL.md says how). One command, every desktop:
# the script works out what this machine is and installs what actually exists
# for it — never a "coming soon" page, never a binary for another platform.
#
#   macOS arm64   → the desktop app (.dmg, current version from the updater
#                   manifest — NOT a version baked into this file) + `lsh`
#   macOS intel   → `lsh` + the local runner; the desktop bundle is Apple
#                   Silicon only so far, and this says so instead of pretending
#   Linux amd64   → the native app: .deb where dpkg exists, AppImage where it
#                   does not; both carry the runner inside
#   Linux other   → `lsh` + `lb-runner` + a `logishell` launcher that serves the
#                   workspace on loopback and opens the browser at it
#   Windows       → sh cannot do this job there; points at install.ps1
#
# POSIX sh, not bash: this runs on whatever /bin/sh a stranger's machine has.
#
# Knobs (all optional):
#   LOGISHELL_BASE_URL   artifact store root (default https://dl.logishell.com)
#   LSH_INSTALL_DIR      where binaries go (default: first writable of
#                        ~/.local/bin, /usr/local/bin)
#   LOGISHELL_NO_DESKTOP set to 1 to skip the macOS app and install tools only
set -eu

BASE_URL=${LOGISHELL_BASE_URL:-https://dl.logishell.com}
WEB_IDE=https://app.logishell.com

die() { printf '\033[31merror\033[0m %s\n' "$*" >&2; exit 1; }
say() { printf '%s\n' "$*"; }
step() { printf '\033[36m%s\033[0m\n' "$*"; }

need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required but not installed"; }
need curl
need tar

# ── platform ────────────────────────────────────────────────────────────────
uname_s=$(uname -s)
case "$uname_s" in
  Darwin) os=darwin ;;
  Linux)  os=linux ;;
  MINGW*|MSYS*|CYGWIN*)
    say ""
    say "This is Windows. Run the PowerShell installer instead:"
    say ""
    say "    irm logishell.com/install.ps1 | iex"
    say ""
    say "Or use the browser IDE right now: $WEB_IDE"
    exit 1
    ;;
  *) die "unsupported OS: $uname_s — the browser IDE works anywhere: $WEB_IDE" ;;
esac

arch=$(uname -m)
case "$arch" in
  arm64|aarch64) arch=arm64 ;;
  x86_64|amd64)  arch=amd64 ;;
  *) die "unsupported architecture: $arch — build from source: go build ./apps/runner/cmd/lsh" ;;
esac

# ── install dir: never sudo behind the user's back ──────────────────────────
if [ -n "${LSH_INSTALL_DIR:-}" ]; then
  install_dir=$LSH_INSTALL_DIR
  mkdir -p "$install_dir" || die "cannot create $install_dir"
else
  install_dir=$HOME/.local/bin
  mkdir -p "$install_dir" 2>/dev/null || true
  if [ ! -w "$install_dir" ]; then
    if [ -w /usr/local/bin ]; then
      install_dir=/usr/local/bin
    else
      die "no writable install dir — set LSH_INSTALL_DIR=/some/where"
    fi
  fi
fi

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM

# ── one download path for every Go tool we ship ─────────────────────────────
# $1 tool name (lsh | lb-runner), $2 artifact-store subdirectory.
# Every archive is checked against the published sha256 before anything is
# unpacked: bytes off the internet get verified, always.
fetch_tool() {
  tool=$1
  dir=$2
  base="$BASE_URL/$dir"

  version=$(curl -fsSL "$base/stable.txt") \
    || die "cannot reach the artifact store — $base/stable.txt"
  version=$(printf '%s' "$version" | tr -d ' \t\r\n')
  [ -n "$version" ] || die "the artifact store did not name a current $tool version"
  version=${version#v}

  archive="${tool}_${version}_${os}_${arch}.tar.gz"
  step "⬇  $tool $version ($os/$arch)"
  curl -fL --progress-bar "$base/$archive" -o "$tmp/$archive" \
    || die "download failed: $base/$archive"

  curl -fsSL "$base/${tool}_${version}_checksums.txt" -o "$tmp/checksums.txt" \
    || die "checksums file missing for $tool $version — refusing to install unverified bytes"
  expected=$(awk -v f="$archive" '$2 == f || $2 == "*"f { print $1 }' "$tmp/checksums.txt")
  [ -n "$expected" ] || die "no checksum published for $archive — refusing to install"
  if command -v sha256sum >/dev/null 2>&1; then
    actual=$(sha256sum "$tmp/$archive" | awk '{print $1}')
  elif command -v shasum >/dev/null 2>&1; then
    actual=$(shasum -a 256 "$tmp/$archive" | awk '{print $1}')
  else
    die "no sha256 tool found — refusing to install unverified bytes"
  fi
  [ "$actual" = "$expected" ] || die "checksum mismatch for $archive
  expected $expected
  got      $actual"

  tar -xzf "$tmp/$archive" -C "$tmp" || die "the $tool archive did not unpack"
  [ -f "$tmp/$tool" ] || die "the archive did not contain $tool"

  # macOS quarantines anything curl brought in; these binaries are not notarized
  # yet, so Gatekeeper would refuse to exec them.
  [ "$os" = darwin ] && xattr -d com.apple.quarantine "$tmp/$tool" 2>/dev/null || true

  chmod +x "$tmp/$tool"
  mv -f "$tmp/$tool" "$install_dir/$tool" || die "cannot write $install_dir/$tool"
  say "   → $install_dir/$tool"
}

# ── the launcher: LogiShell without a native app ────────────────────────────
# On Linux (and macOS Intel) there is no bundled shell yet, so the product is
# the runner serving loopback plus a browser pointed at it. `logishell` is that
# one command: start the runner if it is not up, then open the page.
write_launcher() {
  cat >"$install_dir/logishell" <<'LAUNCHER'
#!/bin/sh
# Generated by the LogiShell installer. Starts the local runner (terminal,
# workspaces, agent sessions) and opens it in your browser.
#
#   logishell            start if needed, then open the browser
#   logishell stop       stop the runner
#   logishell status     say whether it is up, and where
set -eu

PORT=${LOGISHELL_PORT:-7681}
STATE_DIR=${LOGISHELL_STATE_DIR:-$HOME/.lb}
TOKEN_FILE=$STATE_DIR/runner-token
LOG_FILE=$STATE_DIR/runner.log
PID_FILE=$STATE_DIR/runner.pid
mkdir -p "$STATE_DIR/workspaces"

# The token is generated once and kept 0600: every working route needs it, so a
# readable token file would be a readable machine.
if [ ! -s "$TOKEN_FILE" ]; then
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -hex 32 >"$TOKEN_FILE"
  else
    head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' >"$TOKEN_FILE"
  fi
  chmod 600 "$TOKEN_FILE"
fi
TOKEN=$(cat "$TOKEN_FILE")

up() { curl -fsS --max-time 3 "http://127.0.0.1:$PORT/healthz" >/dev/null 2>&1; }

case "${1:-open}" in
  stop)
    # Killing the runner kills every shell it holds, and those shells hold
    # hours of somebody's work: agent conversations, builds, a half-written
    # command. So ask the machine what is at stake first, and say the number
    # out loud instead of taking it away silently. --force is the same command
    # with the answer already given.
    LIVE=$(curl -fsS --max-time 3 "http://127.0.0.1:$PORT/healthz" 2>/dev/null \
      | sed -n 's/.*"liveShells"[[:space:]]*:[[:space:]]*\([0-9][0-9]*\).*/\1/p')
    if [ "${2:-}" != "--force" ] && [ -n "$LIVE" ] && [ "$LIVE" -gt 0 ] 2>/dev/null; then
      echo "logishell: $LIVE shell(s) are running here — stopping now would end them."
      echo "  see them:  open http://127.0.0.1:$PORT"
      echo "  stop anyway:  logishell stop --force"
      exit 1
    fi
    [ -s "$PID_FILE" ] && kill "$(cat "$PID_FILE")" 2>/dev/null || true
    rm -f "$PID_FILE"
    echo "logishell: stopped"
    exit 0
    ;;
  status)
    if up; then echo "logishell: up at http://127.0.0.1:$PORT"; else echo "logishell: not running"; fi
    exit 0
    ;;
esac

if ! up; then
  # No Claude CLI on this machine? Then the terminal is your shell — a browser
  # terminal that opens on nothing would be a broken promise. TERMINAL_COMMAND,
  # not CLAUDE_BIN: the runner reports which agents are installed, and pointing
  # its claude at bash would make that report lie.
  TERM_CMD=$(command -v claude 2>/dev/null || printf '%s' "${SHELL:-/bin/sh}")
  TERMINAL_COMMAND=$TERM_CMD \
  TERMINAL_ADDR=127.0.0.1:$PORT \
  TERMINAL_TOKEN=$TOKEN \
  WORKSPACES_DIR=$STATE_DIR/workspaces \
    nohup lb-runner >"$LOG_FILE" 2>&1 &
  echo $! >"$PID_FILE"

  n=0
  while [ $n -lt 40 ]; do
    up && break
    n=$((n + 1))
    sleep 0.25
  done
  if ! up; then
    echo "logishell: the runner did not come up — see $LOG_FILE" >&2
    exit 1
  fi
fi

URL="http://127.0.0.1:$PORT/?token=$TOKEN"
echo "logishell: $URL"
if command -v xdg-open >/dev/null 2>&1; then
  xdg-open "$URL" >/dev/null 2>&1 || true
elif command -v open >/dev/null 2>&1; then
  open "$URL" || true
fi
LAUNCHER
  chmod +x "$install_dir/logishell"
  say "   → $install_dir/logishell"
}

# ── the macOS app ───────────────────────────────────────────────────────────
# The version comes from the updater manifest, never from a constant in this
# file: the previous installer had 0.7.2 baked in and kept installing it long
# after 0.7.31 shipped. One source of truth, and it is the one the app itself
# updates against.
install_desktop_mac() {
  step "⬇  LogiShell desktop"
  manifest=$(curl -fsSL "$BASE_URL/desktop/latest.json") || die "cannot reach $BASE_URL/desktop/latest.json"
  version=$(printf '%s' "$manifest" | tr -d ' \n' | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
  [ -n "$version" ] || die "the updater manifest does not name a version"

  dmg_url="$BASE_URL/desktop/LogiShell_${version}_aarch64.dmg"
  dmg="$tmp/LogiShell.dmg"
  curl -fL --progress-bar "$dmg_url" -o "$dmg" || die "download failed: $dmg_url"

  # A disk image must start with its own magic, not with an HTML error page.
  case "$(file -b "$dmg")" in
    *HTML*|*text*) die "the downloaded file is not a disk image (got: $(file -b "$dmg"))" ;;
  esac

  # This build is not notarized yet, so Gatekeeper would refuse it outright.
  xattr -d com.apple.quarantine "$dmg" 2>/dev/null || true

  # NOT -quiet: the mount point comes from this output. It is the tab-separated
  # last field of the line that names a /Volumes path.
  mount=$(hdiutil attach "$dmg" -nobrowse | sed -n 's/.*\(\/Volumes\/.*\)$/\1/p' | head -1)
  [ -n "$mount" ] || die "could not mount the disk image"
  # shellcheck disable=SC2064
  trap "hdiutil detach '$mount' -quiet 2>/dev/null || true; rm -rf '$tmp'" EXIT INT TERM
  [ -d "$mount/LogiShell.app" ] || die "the image mounted but LogiShell.app is not inside it"

  rm -rf "/Applications/LogiShell.app"
  cp -R "$mount/LogiShell.app" /Applications/ || die "could not copy into /Applications (permissions?)"
  xattr -cr "/Applications/LogiShell.app"
  hdiutil detach "$mount" -quiet 2>/dev/null || true
  trap 'rm -rf "$tmp"' EXIT INT TERM
  say "   → /Applications/LogiShell.app ($version)"
  desktop_installed=$version
}

# One file, no root, its own GTK inside — the answer for every Linux that is not
# Debian-shaped. Lands in the install dir with a desktop entry beside it, so it
# shows up in the launcher like an installed app rather than a file in Downloads.
install_appimage_linux() {
  appimage_url="$BASE_URL/desktop/LogiShell_${1}_amd64.AppImage"
  curl -fsS -r 0-0 -o /dev/null "$appimage_url" 2>/dev/null || return 1

  step "⬇  LogiShell desktop $1 (AppImage)"
  curl -fL --progress-bar "$appimage_url" -o "$tmp/LogiShell.AppImage" || return 1
  chmod +x "$tmp/LogiShell.AppImage"
  mv -f "$tmp/LogiShell.AppImage" "$install_dir/LogiShell.AppImage" || return 1

  apps_dir=$HOME/.local/share/applications
  mkdir -p "$apps_dir"
  cat >"$apps_dir/LogiShell.desktop" <<DESKTOP
[Desktop Entry]
Type=Application
Name=LogiShell
Comment=The native LogiShell IDE
Exec=$install_dir/LogiShell.AppImage
Icon=logishell
Terminal=false
Categories=Development;IDE;
DESKTOP
  update-desktop-database "$apps_dir" >/dev/null 2>&1 || true

  say "   → $install_dir/LogiShell.AppImage"
  desktop_command="$install_dir/LogiShell.AppImage"
  # An AppImage mounts itself through FUSE, and Ubuntu 24.04 ships without
  # libfuse2. Say it here rather than let the first double-click fail silently.
  if ! ldconfig -p 2>/dev/null | grep -q libfuse.so.2; then
    say "   note: AppImages need FUSE 2. If it refuses to start, either install"
    say "         libfuse2, or run it as: LogiShell.AppImage --appimage-extract-and-run"
  fi
  desktop_installed=$1
}

# ── the Linux app ───────────────────────────────────────────────────────────
# Native window on Linux, built in a container (Tauri does not cross-compile;
# see docs/INSTALL.md). Returns non-zero WITHOUT installing anything if this
# machine has no bundle to install — the caller then falls back to the runner,
# which is a working product, not a consolation prize.
install_desktop_linux() {
  [ "$arch" = amd64 ] || return 1

  # Свой указатель, а НЕ latest.json: манифест апдейтера двигает mac-релиз, а
  # Linux-бандл пока собирается вручную в контейнере и отстаёт на релиз-другой.
  # Пока указатели были общими, установщик спрашивал версию, которой под Linux
  # не существует, и молча уходил в фолбэк.
  version=$(curl -fsSL "$BASE_URL/desktop/linux-stable.txt" 2>/dev/null | tr -d ' \t\r\n')
  [ -n "$version" ] || return 1

  # No dpkg (Fedora, Arch, Nix, a live USB): the AppImage is the same app in one
  # file — no root, no package manager, and it carries its own GTK libraries.
  if ! command -v dpkg >/dev/null 2>&1; then
    install_appimage_linux "$version"
    return $?
  fi

  deb_url="$BASE_URL/desktop/LogiShell_${version}_amd64.deb"
  # Range-GET, not HEAD: the artifact store is byte-exact about GET and this is
  # the cheapest honest "does it exist" — one byte.
  curl -fsS -r 0-0 -o /dev/null "$deb_url" 2>/dev/null || return 1

  step "⬇  LogiShell desktop $version"
  deb="$tmp/logishell.deb"
  curl -fL --progress-bar "$deb_url" -o "$deb" || return 1
  case "$(file -b "$deb" 2>/dev/null)" in
    *HTML*|*ASCII*) return 1 ;;
  esac

  # Installing a package needs root. Ask openly — via /dev/tty, because stdin is
  # the pipe this script arrived through and would swallow the prompt.
  if [ "$(id -u)" = 0 ]; then
    # `</dev/null` — не украшение. Под `curl … | sh` стандартный ввод скрипта
    # это САМА ТРУБА, из которой sh ещё дочитывает себя. apt-get и dpkg читают
    # stdin (триггеры, вопросы конфигурации), съедают кусок скрипта, и sh потом
    # спотыкается на середине строки: «Syntax error: Unterminated quoted
    # string» через полтора экрана после успешной установки. Ветка с sudo это
    # уже учитывала (`</dev/tty`), root-ветка — нет.
    apt-get install -y "$deb" </dev/null >/dev/null 2>&1 \
      || dpkg -i "$deb" </dev/null >/dev/null 2>&1 \
      || return 1
  elif command -v sudo >/dev/null 2>&1 && [ -r /dev/tty ]; then
    say "   installing the package (sudo will ask for your password)"
    sudo -p "   [sudo] password for %u: " apt-get install -y "$deb" </dev/tty >/dev/null 2>&1 \
      || sudo dpkg -i "$deb" </dev/tty >/dev/null 2>&1 \
      || return 1
  else
    return 1
  fi

  say "   → /usr/bin/logishell ($version)"
  desktop_command=logishell
  desktop_installed=$version
}

# ── run ─────────────────────────────────────────────────────────────────────
say ""
say "LogiShell — $os/$arch"
say ""

desktop_installed=""
desktop_command=""
fetch_tool lsh cli

if [ "${LOGISHELL_NO_DESKTOP:-}" = 1 ]; then
  fetch_tool lb-runner runner
  write_launcher
elif [ "$os" = darwin ] && [ "$arch" = arm64 ]; then
  install_desktop_mac
elif [ "$os" = linux ] && install_desktop_linux; then
  : # native window installed; the runner rides inside the package
else
  fetch_tool lb-runner runner
  write_launcher
fi

say ""
say "✅ installed"
say ""

# Only claim a tool is usable if it is actually reachable — a binary in a
# directory nobody's PATH mentions is not an install, it is a file.
case ":$PATH:" in
  *":$install_dir:"*) on_path=1 ;;
  *) on_path=0 ;;
esac

if [ "$on_path" = 0 ]; then
  say "⚠  $install_dir is not on your PATH. Add it:"
  say ""
  say "    echo 'export PATH=\"$install_dir:\$PATH\"' >> ~/.bashrc && exec bash"
  say ""
fi

if [ -n "$desktop_installed" ] && [ "$os" = darwin ]; then
  say "Open the app:      open -a LogiShell"
  say "Engine in a shell: lsh help"
  open -a LogiShell 2>/dev/null || true
elif [ -n "$desktop_installed" ]; then
  say "Start LogiShell:   ${desktop_command:-logishell}"
  say "                   (or find LogiShell in your applications)"
  say "Engine in a shell: lsh help"
  # Launch it right away only when there is a desktop session to launch into —
  # over SSH this would just print an X11 error at someone.
  if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ] && [ -n "$desktop_command" ]; then
    # stdin у фонового приложения — та же труба со скриптом; отвязываем и её.
    (setsid "$desktop_command" </dev/null >/dev/null 2>&1 &) || true
  fi
else
  say "Start LogiShell:   logishell        (local workspace in your browser)"
  say "Engine in a shell: lsh help"
  say "Cloud IDE:         $WEB_IDE"
  if [ "$os" = darwin ] && [ "$arch" != arm64 ]; then
    say ""
    say "The desktop bundle is Apple Silicon only so far — this Intel Mac gets"
    say "the same engine over loopback."
  fi
fi
say ""
say "Next, in any project of yours:"
say ""
say "    cd <your project> && lsh onboard"
say ""
say "One command: it starts the engine if needed, puts the folder on the"
say "shelf, and opens LogiShell on it. Safe to run again any time."
say ""
