#!/bin/sh
# Emma installer — voice assistant for macOS.
# Usage: curl -fsSL https://theemmafamily.com/install.sh | sh
#
# What this does (in this order):
#   1. Preflight: macOS 14+, arch, Xcode CLT (if missing, prompts)
#   2. Fetch uv standalone (brings Python 3.12) — no Homebrew needed
#   3. Fetch Emma source tarball, extract to ~/.emma/src
#   4. uv sync (deps + venv at ~/.emma/.venv)
#   5. Download the sherpa-onnx KWS wake-word model (~18MB from GitHub)
#   6. Bootstrap TCC permissions (upfront per the permissions convention)
#   7. Register LaunchAgent (managed mode: parks unpaired) + generate Emma.app
#   8. Open Emma.app — the app owns onboarding (account + pairing + upsell)
#
# NO terminal pairing: the daemon boots unpaired-but-alive and the app walks the
# user through account + pairing. Free tier = 90s/month, so Emma works immediately
# once paired; the app upsells when minutes run low.
#
# Idempotent: safe to re-run to upgrade in place.

set -eu

# ---------- constants ----------
EMMA_VERSION="${EMMA_VERSION:-latest}"
EMMA_HOME="${HOME}/.emma"
EMMA_SRC="${EMMA_HOME}/src"
EMMA_VENV="${EMMA_HOME}/.venv"
EMMA_BIN="${HOME}/.local/bin"
# Wake engine assets: sherpa-onnx KeywordSpotter model (offline KWS). Vosk was
# dropped — it publishes no macOS arm64 wheel, so `uv sync` could never install
# it on Apple Silicon (the hardware this installer targets).
SHERPA_DIR="${EMMA_HOME}/sherpa-kws"
SHERPA_MODEL="sherpa-onnx-kws-zipformer-gigaspeech-3.3M-2024-01-01"
SHERPA_URL="https://github.com/k2-fsa/sherpa-onnx/releases/download/kws-models/${SHERPA_MODEL}.tar.bz2"
LAUNCH_AGENT_LABEL="com.emma.daemon"
LAUNCH_AGENT_PATH="${HOME}/Library/LaunchAgents/${LAUNCH_AGENT_LABEL}.plist"
# Legacy label from the pre-distribution launchd agent. Booted out on install so
# two agents never fight over the mic.
LEGACY_AGENT_LABEL="com.garcia.emma"
LEGACY_AGENT_PATH="${HOME}/Library/LaunchAgents/${LEGACY_AGENT_LABEL}.plist"
MIN_MACOS_MAJOR=14
INSTALL_LOG="${EMMA_HOME}/install.log"
# GitHub gives us free TLS + hash-addressable integrity + CDN + tag rollback.
REPO="theemmafamily/emma"
UV_INSTALL_URL="https://astral.sh/uv/install.sh"

# ---------- pretty ----------
if [ -t 1 ]; then
  C_OK='\033[0;32m'; C_WARN='\033[0;33m'; C_ERR='\033[0;31m'
  C_DIM='\033[0;90m'; C_OFF='\033[0m'
else
  C_OK=; C_WARN=; C_ERR=; C_DIM=; C_OFF=
fi
say()  { printf "%b\n" "$1"; }
ok()   { say "${C_OK}✓${C_OFF} $1"; }
warn() { say "${C_WARN}!${C_OFF} $1"; }
err()  { say "${C_ERR}✗${C_OFF} $1" >&2; exit 1; }
step() { say ""; say "${C_DIM}── $1${C_OFF}"; }

# ---------- main (must be a single function; guards against partial delivery) ----------
main() {
  mkdir -p "$EMMA_HOME"
  : > "$INSTALL_LOG"

  # -- 1. Preflight --
  step "1/8  Verificando tu Mac"
  [ "$(uname)" = "Darwin" ] || err "Emma solo corre en macOS. Sistema: $(uname)."
  mac_ver=$(sw_vers -productVersion | cut -d. -f1)
  [ "$mac_ver" -ge "$MIN_MACOS_MAJOR" ] || err "macOS ${MIN_MACOS_MAJOR}+ requerido. Encontrado: $(sw_vers -productVersion)."
  arch=$(uname -m)
  [ "$arch" = "arm64" ] || warn "Apple Silicon recomendado. arch=$arch — sigo, más lento."
  ok "macOS $(sw_vers -productVersion), $arch"

  if ! xcode-select -p >/dev/null 2>&1; then
    warn "Xcode CLT no instalado. Voy a pedirlo."
    xcode-select --install 2>/dev/null || true
    say "  Termina la instalación de Xcode CLT en la ventana que abrió, luego re-ejecuta este script."
    exit 0
  fi
  ok "Xcode CLT: $(xcode-select -p)"

  # -- 2. uv standalone --
  step "2/8  Preparando uv (trae Python 3.12 portable)"
  if ! command -v uv >/dev/null 2>&1; then
    say "  Instalando uv…"
    curl --fail --show-error --location --silent "$UV_INSTALL_URL" | sh >>"$INSTALL_LOG" 2>&1 \
      || err "uv no se instaló. Ver $INSTALL_LOG."
    # uv installer puts the binary in ~/.local/bin; add to PATH for this session:
    export PATH="${EMMA_BIN}:$PATH"
  fi
  command -v uv >/dev/null 2>&1 || { export PATH="${EMMA_BIN}:$PATH"; }
  command -v uv >/dev/null 2>&1 || err "uv no está en el PATH tras instalar. Ver $INSTALL_LOG."
  ok "uv: $(uv --version | head -n1)"

  # -- 3. Fetch Emma source --
  step "3/8  Descargando Emma"
  mkdir -p "$EMMA_SRC"
  # "latest"/"main" -> track the main branch; anything else -> a pinned release tag.
  case "$EMMA_VERSION" in
    latest|main) url="https://github.com/${REPO}/archive/refs/heads/main.tar.gz" ;;
    *)           url="https://github.com/${REPO}/archive/refs/tags/${EMMA_VERSION}.tar.gz" ;;
  esac
  tmpdir=$(mktemp -d)
  say "  $url"
  curl --fail --show-error --location --progress-bar -o "$tmpdir/emma.tar.gz" "$url" \
    || err "No pude bajar Emma. Revisa tu conexión (o que la versión '${EMMA_VERSION}' exista)."
  tar -xzf "$tmpdir/emma.tar.gz" -C "$EMMA_SRC" --strip-components=1 \
    || err "Tarball corrupto — re-ejecuta el instalador."
  rm -rf "$tmpdir"
  # Safety net: strip quarantine attribute if any (curl doesn't set it, but cheap
  # insurance against a Gatekeeper prompt on the venv binaries).
  xattr -dr com.apple.quarantine "$EMMA_HOME" 2>/dev/null || true
  ok "Source en $EMMA_SRC"

  # -- 4. uv sync (Python + deps) --
  step "4/8  Instalando dependencias (2-3 min primera vez)"
  # Force uv to build the venv at ~/.emma/.venv (not the in-tree default) so the
  # LaunchAgent path is stable across source upgrades.
  ( cd "$EMMA_SRC" && UV_PROJECT_ENVIRONMENT="$EMMA_VENV" uv sync ) >>"$INSTALL_LOG" 2>&1 \
    || err "Dependencias no instalaron. Ver $INSTALL_LOG."
  [ -x "$EMMA_VENV/bin/python" ] || err "Venv missing at $EMMA_VENV — uv sync silenciosamente falló. Ver $INSTALL_LOG."
  ok "Deps + Python $(${EMMA_VENV}/bin/python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"

  # -- 5. Wake-word model (sherpa-onnx KWS) --
  step "5/8  Descargando modelo de palabra clave (sherpa-onnx KWS, ~18MB)"
  if [ -f "${SHERPA_DIR}/${SHERPA_MODEL}/tokens.txt" ]; then
    ok "Wake word ya presente (${SHERPA_MODEL})"
  else
    mkdir -p "$SHERPA_DIR"
    stmp=$(mktemp -d)
    curl --fail --show-error --location --progress-bar -o "$stmp/kws.tar.bz2" "$SHERPA_URL" \
      || err "No pude bajar el modelo de palabra clave. Ver $SHERPA_URL."
    tar -xjf "$stmp/kws.tar.bz2" -C "$SHERPA_DIR" \
      || err "Modelo de palabra clave corrupto — re-ejecuta el instalador."
    rm -rf "$stmp"
    [ -f "${SHERPA_DIR}/${SHERPA_MODEL}/tokens.txt" ] \
      || err "Modelo no quedó en ${SHERPA_DIR}/${SHERPA_MODEL}."
    ok "Wake word listo (${SHERPA_MODEL})"
  fi

  # -- 6. TCC permissions --
  step "6/8  Solicitando permisos macOS (mic, automatización, accesibilidad)"
  say "    macOS abrirá varios diálogos. Acepta cada uno."
  ( cd "$EMMA_SRC" && "$EMMA_VENV/bin/python" -m emma.permissions bootstrap ) || \
    warn "Algunos permisos no confirmados. Los das después en Ajustes del Sistema → Privacidad."
  ok "Permisos solicitados"

  # -- 7. LaunchAgent (managed mode: the daemon parks unpaired; the app pairs it) --
  # NO terminal pairing (PAID-ONBOARDING): the app owns onboarding. The daemon boots
  # unpaired, publishes needs_onboarding, runs the menubar UI, and waits — it never
  # opens a Realtime session until the app writes a device token.
  step "7/8  Registrando Emma como servicio (LaunchAgent)"
  mkdir -p "$HOME/Library/LaunchAgents"
  # Write plist with concrete paths (no template variables).
  cat > "$LAUNCH_AGENT_PATH" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>${LAUNCH_AGENT_LABEL}</string>
  <key>ProgramArguments</key>
  <array>
    <string>${EMMA_VENV}/bin/python</string>
    <string>-m</string><string>emma</string>
  </array>
  <key>WorkingDirectory</key><string>${EMMA_SRC}</string>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
  <key>StandardErrorPath</key><string>${EMMA_HOME}/stderr.log</string>
  <key>StandardOutPath</key><string>${EMMA_HOME}/stdout.log</string>
  <key>EnvironmentVariables</key><dict>
    <key>EMMA_HOME</key><string>${EMMA_HOME}</string>
    <key>PATH</key><string>${EMMA_VENV}/bin:${EMMA_BIN}:/usr/local/bin:/usr/bin:/bin</string>
    <!-- Pin the sherpa-onnx KWS wake engine step 5 installed, and point it at the
         downloaded model. The settings default is already "sherpa"; pinning here
         keeps the daemon deterministic regardless of any stray .env. -->
    <key>WAKE_WORD_ENGINE</key><string>sherpa</string>
    <key>SHERPA_KWS_MODEL_PATH</key><string>${SHERPA_DIR}/${SHERPA_MODEL}</string>
    <!-- Managed mode: this is a client daemon with NO local OpenAI key. It reaches
         OpenAI ONLY through the managed proxy using its paired device bearer, and
         it parks unpaired-but-alive until the app pairs it (the proxy + plan is the
         gate, never the download). Unset for a dev/BYOK daemon with its own key. -->
    <key>EMMA_REQUIRE_PAIRING</key><string>1</string>
    <!-- Run the dashboard/control server IN the daemon so the menubar app can
         talk back to it (unmute, parar, dormir) AND own onboarding. Without this the
         /control socket lives in a separate process and can't touch the wake loop. -->
    <key>EMMA_DASHBOARD</key><string>1</string>
  </dict>
</dict></plist>
EOF
  # Boot out any prior agent (new label AND the legacy com.garcia.emma) so we never
  # end up with two daemons fighting over the microphone. No-op if absent.
  launchctl bootout "gui/$(id -u)/${LEGACY_AGENT_LABEL}" 2>/dev/null || true
  [ -f "$LEGACY_AGENT_PATH" ] && rm -f "$LEGACY_AGENT_PATH"
  launchctl bootout "gui/$(id -u)/${LAUNCH_AGENT_LABEL}" 2>/dev/null || true
  launchctl bootstrap "gui/$(id -u)" "$LAUNCH_AGENT_PATH" \
    || err "LaunchAgent no cargó. Log: ${EMMA_HOME}/stderr.log"
  ok "Servicio arrancado — Emma corre en background"

  # -- 8.5 Menubar app bundle --
  # GENERATE the .app locally (never download it): Gatekeeper only checks files
  # carrying com.apple.quarantine, which curl/mkdir never set — so a bundle we
  # create ourselves never triggers the "cannot verify" dialog. The launcher in
  # Contents/MacOS is a shell script (Gatekeeper only inspects Mach-O binaries).
  # The daemon also spawns the UI, so the icon appears without opening this; the
  # bundle is for a manual re-launch. Idempotent: rebuilt every run.
  step "7.5/8  Creando la app de menú (Emma.app)"
  APP="${EMMA_HOME}/Emma.app"
  rm -rf "$APP"
  mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
  cat > "$APP/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>CFBundleName</key><string>Emma</string>
  <key>CFBundleDisplayName</key><string>Emma</string>
  <key>CFBundleIdentifier</key><string>com.emma.ui</string>
  <key>CFBundleExecutable</key><string>emma-ui</string>
  <key>CFBundlePackageType</key><string>APPL</string>
  <key>CFBundleShortVersionString</key><string>1.0</string>
  <key>LSUIElement</key><true/>
  <key>NSHighResolutionCapable</key><true/>
</dict></plist>
EOF
  cat > "$APP/Contents/MacOS/emma-ui" <<EOF
#!/bin/sh
# Launch the menubar UI against the installed venv.
cd "${EMMA_SRC}" 2>/dev/null || true
exec "${EMMA_VENV}/bin/python" -m emma.ui
EOF
  chmod +x "$APP/Contents/MacOS/emma-ui"
  ok "Emma.app creada — barra de menú (~/.emma/Emma.app)"

  # -- 8. Open the app — it owns onboarding (account + pairing + upsell) --
  step "8/8  Abriendo Emma para terminar de configurar"
  open "$APP" 2>/dev/null || true
  say ""
  say "  ${C_OK}Termina de configurar Emma en la ventana que abrió.${C_OFF}"
  say "  Crea tu cuenta o inicia sesión, y vincula esta Mac. Incluye"
  say "  ${C_OK}90 segundos gratis al mes${C_OFF} para probarla — luego di \"Emma\"."
  say ""
  say "  Para desinstalar: ${C_DIM}curl -fsSL https://theemmafamily.com/uninstall.sh | sh${C_OFF}"
  say ""
}

# Invoke at last line — if the download was truncated, this closing brace won't be
# seen and sh refuses to execute a partial function.
main "$@"
