#!/usr/bin/env bash
# OpenUI Installer / Updater
#
# Idempotent by design:
#   - Fresh install  -> everything is written.
#   - Re-run         -> code is refreshed to the latest published version, while
#                       .config/ (password, settings, plugins) and data/
#                       (conversations) are left untouched. Those are the user's,
#                       not ours, and an update that silently resets a password
#                       is worse than no update.
#   - --force        -> wipe everything, including user data, and reinstall.
#
# Linux (x64/arm64), macOS, Windows (WSL/Git Bash).

set -euo pipefail

GATEWAY="https://ai.zelai.qzz.io"
CDN="${GATEWAY}/_openui/cdn"
INSTALL_DIR="./openui"
DEFAULT_MODEL="big-pickle"
VERSION_FILE="${INSTALL_DIR}/VERSION"

# The published version, read from the signed manifest. If the manifest cannot
# be read the installer still proceeds; it just treats the install as current,
# which is safer than refusing to install at all.
published_version() {
  local url="${GATEWAY}/webhook" v
  if command -v curl &>/dev/null; then
    v="$(curl -fsSL --max-time 8 "$url" 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
  elif command -v wget &>/dev/null; then
    v="$(wget -qO- --timeout=8 "$url" 2>/dev/null | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
  fi
  printf '%s' "${v:-}"
}

installed_version() {
  [ -f "${VERSION_FILE}" ] && tr -d '[:space:]' < "${VERSION_FILE}" || printf ''
}

# Non-zero when the local copy is older than what the gateway publishes.
is_behind() {
  local local_v="$1" remote_v="$2"
  [ -n "${remote_v}" ] || return 1
  [ -n "${local_v}" ] || return 0   # nothing installed counts as behind
  [ "${local_v}" = "${remote_v}" ] && return 1
  # dotted compare; "2.10.0" > "2.9.0" unlike a string compare
  local IFS=.
  local -a a=(${local_v}) b=(${remote_v})
  local i
  for i in 0 1 2; do
    local x=$((10#${a[i]:-0})) y=$((10#${b[i]:-0}))
    [ "${x}" -lt "${y}" ] && return 0
    [ "${x}" -gt "${y}" ] && return 1
  done
  return 1
}

FORCE=0
for arg in "$@"; do
  case "$arg" in
    --force|-f) FORCE=1 ;;
    --help|-h)
      echo "Usage: install.sh [--force]"
      echo "  (no flag)  install if missing, otherwise update the code and keep your data"
      echo "  --force    wipe everything, including conversations and password, and reinstall"
      exit 0
      ;;
  esac
done

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
log()  { echo -e "${CYAN}[openui]${NC} $1"; }
ok()   { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err()  { echo -e "${RED}[✗]${NC} $1" >&2; exit 1; }

# ─── Platform ───
detect_platform() {
  local os arch
  os="$(uname -s)"; arch="$(uname -m)"
  case "$os" in
    Linux*)  OS="linux" ;;
    Darwin*) OS="darwin" ;;
    MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
    *) err "OS tidak didukung: $os" ;;
  esac
  case "$arch" in
    x86_64|amd64)   ARCH="x64" ;;
    aarch64|arm64) ARCH="arm64" ;;
    armv7l|armhf)  ARCH="arm" ;;
    *) err "Arsitektur tidak didukung: $arch" ;;
  esac
}

# ─── Bun ───
ensure_bun() {
  export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
  export PATH="${BUN_INSTALL}/bin:${PATH}"
  if command -v bun &>/dev/null; then return; fi
  log "Memasang Bun..."
  if command -v curl &>/dev/null; then
    curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1 || true
  elif command -v wget &>/dev/null; then
    wget -qO- https://bun.sh/install | bash >/dev/null 2>&1 || true
  else
    err "Butuh curl atau wget untuk memasang Bun"
  fi
  command -v bun &>/dev/null || err "Bun gagal dipasang. Pasang manual: https://bun.sh"
  ok "Bun terpasang"
}

download() {
  local url="$1" dest="$2"
  mkdir -p "$(dirname "${dest}")"
  if command -v curl &>/dev/null; then
    curl -fsSL "$url" -o "${dest}" || err "Gagal mengunduh: $url"
  elif command -v wget &>/dev/null; then
    wget -q "$url" -O "${dest}" || err "Gagal mengunduh: $url"
  else
    err "Butuh curl atau wget"
  fi
}

setup_dirs() {
  mkdir -p "${INSTALL_DIR}/public/assets"
  mkdir -p "${INSTALL_DIR}/src"
  mkdir -p "${INSTALL_DIR}/examples"
  mkdir -p "${INSTALL_DIR}/docs"
  mkdir -p "${INSTALL_DIR}/test"
  # The update-signing helpers land here.
  mkdir -p "${INSTALL_DIR}/scripts"
  # Batas sandbox untuk agent. Semua file yang boleh disentuh model ada di
  # dalam folder ini; isi dengan symlink kalau mau memberi akses ke project.
  mkdir -p "${INSTALL_DIR}/workspace"
  mkdir -p "${INSTALL_DIR}/data/conversations"
  mkdir -p "${INSTALL_DIR}/.config/openui/plugins"
}

# The application payload, as "path-on-CDN|path-in-install-dir".
#
# The CDN mirror is flat for the single files at the top (index.html, app.js,
# app.css, the docs) and keeps subdirectories for src/, test/ and scripts/.
# Writing both sides out, rather than assuming they match, is what stops a
# download from landing in the wrong directory.
#
# This list is the single source of truth for what an install/update replaces.
# sync-cdn.sh parses it and fails if the mirror and this list disagree, so the
# two cannot drift apart unnoticed.
#
# Nothing in .config/ or data/ appears here, and never will: the password,
# settings, plugins and conversations belong to the user.
APP_FILES=(
  "server.js|server.js"
  "package.json|package.json"
  "index.html|public/index.html"
  "llama.png|public/llama.png"
  "app.js|public/assets/app.js"
  "app.css|public/assets/app.css"
  "src/sandbox.js|src/sandbox.js"
  "src/tools.js|src/tools.js"
  "src/tools-system.js|src/tools-system.js"
  "src/tools-web.js|src/tools-web.js"
  "src/plugins.js|src/plugins.js"
  "src/agent.js|src/agent.js"
  "src/prompt.js|src/prompt.js"
  "src/updates.js|src/updates.js"
  "example-plugin.js|examples/example-plugin.js"
  "PLUGINS.md|docs/PLUGINS.md"
  "UPDATES.md|docs/UPDATES.md"
  "gen-update-keys.mjs|scripts/gen-update-keys.mjs"
  "publish-update.mjs|scripts/publish-update.mjs"
  "test/run.mjs|test/run.mjs"
  "test/agent.test.mjs|test/agent.test.mjs"
  "test/agent-loop.test.mjs|test/agent-loop.test.mjs"
  "test/catalog-install.test.mjs|test/catalog-install.test.mjs"
  "test/plugins.test.mjs|test/plugins.test.mjs"
  "test/frontend.test.mjs|test/frontend.test.mjs"
  "test/free-mode.test.mjs|test/free-mode.test.mjs"
  "test/system-tools.test.mjs|test/system-tools.test.mjs"
  "test/trace-ui.test.mjs|test/trace-ui.test.mjs"
  "test/wire-format.test.mjs|test/wire-format.test.mjs"
  "test/updates.test.mjs|test/updates.test.mjs"
)

# Only these must be present, and non-empty, for the app to boot.
REQUIRED_FILES=(
  "server.js" "package.json"
  "public/index.html" "public/assets/app.js" "public/assets/app.css"
  "src/sandbox.js" "src/tools.js" "src/tools-system.js" "src/tools-web.js"
  "src/plugins.js" "src/agent.js" "src/prompt.js" "src/updates.js"
)

verify_files() {
  local missing=()
  for f in "${REQUIRED_FILES[@]}"; do
    [ -s "${INSTALL_DIR}/${f}" ] || missing+=("${f}")
  done
  if [ ${#missing[@]} -gt 0 ]; then
    err "Instalasi tidak lengkap. File hilang:"
    for f in "${missing[@]}"; do echo -e "    ${RED}${f}${NC}" >&2; done
    echo "" >&2
    echo -e "  ${YELLOW}CDN sedang publish. Tunggu sebentar lalu ulangi:${NC}" >&2
    echo -e "    curl -fsSL ${GATEWAY}/install.sh | bash" >&2
    exit 1
  fi
}

download_files() {
  local entry remote dest
  for entry in "${APP_FILES[@]}"; do
    remote="${entry%%|*}"
    dest="${entry#*|}"
    # A missing optional file is a warning, not a failure: docs, tests and
    # the example plugin are not needed to boot. The required ones are
    # enforced by verify_files below.
    if download "${CDN}/${remote}" "${INSTALL_DIR}/${dest}"; then
      :
    else
      warn "lewati ${remote}"
    fi
  done
  verify_files
  chmod +x "${INSTALL_DIR}/server.js" 2>/dev/null || true
}

# Remove app files that the new version no longer ships, so a renamed or
# retired module cannot linger and be imported by a stale reference. Scoped to
# the directories the payload owns; .config/ and data/ are not touched.
prune_stale() {
  local keep=" " entry dest f rel
  for entry in "${APP_FILES[@]}"; do
    keep="${keep}${entry#*|} "
  done
  keep="${keep}install.sh start.sh start.bat VERSION "
  for d in "${INSTALL_DIR}/src" "${INSTALL_DIR}/test" "${INSTALL_DIR}/docs" \
           "${INSTALL_DIR}/scripts" "${INSTALL_DIR}/examples" "${INSTALL_DIR}/public/assets"; do
    [ -d "$d" ] || continue
    for f in "$d"/*; do
      [ -f "$f" ] || continue
      rel="${f#"${INSTALL_DIR}/"}"
      case "${keep}" in
        *" ${rel} "*) ;;
        *) rm -f "$f"; log "hapus file usang: ${rel}" ;;
      esac
    done
  done
}

# Remove app files that the new version no longer ships, so a renamed or
# retired module cannot linger and be imported by a stale reference. Scoped to
# the known directories; .config/ and data/ are not touched.

create_scripts() {
  cat > "${INSTALL_DIR}/start.sh" << 'STARTEOF'
#!/usr/bin/env bash
cd "$(dirname "$0")"
export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
export PATH="${BUN_INSTALL}/bin:${PATH}"
if ! command -v bun &>/dev/null; then
  echo "[!] Bun tidak ditemukan. Pasang: curl -fsSL https://bun.sh/install | bash"
  exit 1
fi

missing=""
for f in src/sandbox.js src/tools.js src/tools-system.js src/tools-web.js \
         src/plugins.js src/agent.js src/prompt.js src/updates.js; do
  [ -s "$f" ] || missing="$missing $f"
done
if [ -n "$missing" ]; then
  echo "[!] Instalasi tidak lengkap. File hilang:$missing"
  echo ""
  echo "    Perbarui kode (data & password tetap aman):"
  echo "      curl -fsSL https://ai.zelai.qzz.io/install.sh | bash"
  echo ""
  echo "    Atau pasang ulang total:"
  echo "      rm -rf \"$(pwd)\""
  echo "      curl -fsSL https://ai.zelai.qzz.io/install.sh | bash"
  exit 1
fi

echo "  OpenUI $(cat VERSION 2>/dev/null || echo v?)  ->  http://localhost:3000"
exec bun run server.js
STARTEOF
  chmod +x "${INSTALL_DIR}/start.sh"

  cat > "${INSTALL_DIR}/start.bat" << 'BATEOF'
@echo off
cd /d "%~dp0"
where bun >nul 2>nul
if %errorlevel% neq 0 (
  echo [!] Bun tidak ditemukan. Pasang: curl -fsSL https://bun.sh/install ^| bash
  pause
  exit /b 1
)
echo   OpenUI   ->  http://localhost:3000
bun run server.js
BATEOF
  ok "Script start dibuat"
}

create_defaults() {
  # Only on a fresh install. An existing settings.json is the user's.
  if [ ! -f "${INSTALL_DIR}/.config/openui/settings.json" ]; then
    cat > "${INSTALL_DIR}/.config/openui/settings.json" << SETTINGS
{
  "theme": "light",
  "model": "${DEFAULT_MODEL}",
  "gateway": "${GATEWAY}",
  "systemPrompt": "",
  "enabledTools": [],
  "plugins": []
}
SETTINGS
  fi
}

write_version() {
  local v="$1"
  [ -n "${v}" ] || v="unknown"
  printf '%s\n' "${v}" > "${VERSION_FILE}"
}

# ─── main ───

echo ""
echo -e "${CYAN}  OpenUI Installer${NC}"
echo ""

detect_platform
ensure_bun

REMOTE_VERSION="$(published_version)"
LOCAL_VERSION="$(installed_version)"

if [ "${FORCE}" -eq 1 ]; then
  warn "--force: menghapus instalasi lama termasuk percakapan & password"
  rm -rf "${INSTALL_DIR}"
  LOCAL_VERSION=""
  FORCE=0
fi

IS_UPDATE=0
if [ -d "${INSTALL_DIR}/src" ] && [ -n "${LOCAL_VERSION}" ]; then
  IS_UPDATE=1
  if is_behind "${LOCAL_VERSION}" "${REMOTE_VERSION}"; then
    log "update: ${LOCAL_VERSION} -> ${REMOTE_VERSION}"
  else
    log "sudah versi terbaru (${LOCAL_VERSION}) —Tetap cek file & unduh ulang"
  fi
fi

setup_dirs
download_files
prune_stale
(cd "${INSTALL_DIR}" && bun install --production >/dev/null 2>&1) || true
create_scripts
create_defaults
write_version "${REMOTE_VERSION:-${LOCAL_VERSION}}"

if [ "${IS_UPDATE}" -eq 1 ]; then
  ok "Kode diperbarui. Password, settings, plugin & percakapan TIDAK disentuh."
else
  ok "OpenUI terpasang di ${INSTALL_DIR}/"
fi

echo ""
echo -e "  ${GREEN}Mulai:${NC}    cd ${INSTALL_DIR} && ./start.sh"
echo ""
if [ "${IS_UPDATE}" -eq 0 ]; then
  echo -e "  ${YELLOW}Password${NC}  dibuat otomatis saat pertama kali start."
  echo -e "            hanya ditampilkan SEKALI di terminal — simpan."
  echo -e "  ${YELLOW}Akses:${NC}    http://localhost:3000  lalu masuk pakai password itu."
  echo ""
fi
echo -e "  ${CYAN}Versi:${NC}      ${REMOTE_VERSION:-unknown}"
echo -e "  ${CYAN}Perbarui:${NC}   curl -fsSL ${GATEWAY}/install.sh | bash"
echo -e "  ${CYAN}Full reset:${NC} rm -rf ${INSTALL_DIR} && curl -fsSL ${GATEWAY}/install.sh | bash"
echo ""
echo -e "  ${CYAN}Tools${NC}       file, search, shell, plan, memory, skills, web."
echo -e "            17 tools. Agent bebas akses seluruh filesystem."
echo ""
echo -e "  ${CYAN}Workspace agent:${NC} ${INSTALL_DIR}/workspace"
echo -e "            Agent hanya boleh baca/tulis di dalam folder ini."
echo -e "            Isi dengan symlink ke project yang mau dikerjakannya:"
echo -e "              ln -s /path/proyek ${INSTALL_DIR}/workspace/proyek"
echo ""
echo -e "  ${CYAN}Plugins:${NC} ${INSTALL_DIR}/.config/openui/plugins/"
echo -e "            Contoh: ${INSTALL_DIR}/examples/example-plugin.js"
echo -e "            Docs:   ${INSTALL_DIR}/docs/PLUGINS.md"
echo ""
