#!/bin/bash
# Emiga Studio one-command installer for macOS.
#
# Downloads the .zip bundle with curl (curl downloads never get the
# com.apple.quarantine xattr, so Gatekeeper stays out of the way), verifies
# the SHA-512 from the official latest-mac.yml manifest (base64, same as
# electron-builder), extracts with ditto into a temp dir, and atomically
# swaps the app bundle in /Applications (rename is atomic, Squirrel.Mac
# style). No sudo, no xattr.
set -euo pipefail

BASE="${EMIGA_BASE:-https://emigadownload.yimeijia.cc}"
APP_NAME="Emiga Studio"
APP_BUNDLE="${APP_NAME}.app"
APP_DIR="${EMIGA_APP_DIR:-/Applications}"   # EMIGA_APP_DIR is a test hook only

# macOS-only commands as variables, overridable for off-Mac logic drills.
UNAME="${EMIGA_UNAME:-uname}"
DITTO="${EMIGA_DITTO:-ditto}"
OPEN="${EMIGA_OPEN:-open}"
OSASCRIPT="${EMIGA_OSASCRIPT:-osascript}"
PGREP="${EMIGA_PGREP:-pgrep}"

# Curl hardening: pin https even across redirects (curl's default redirect
# proto set allows downgrading to plain http), never leave partial files,
# retry transient failures. -f helps but is documented as "not fail-safe",
# so the SHA-512 check below is the real line of defense.
# EMIGA_CURL_OPTS is a test hook only (e.g. to drill against a local http server).
# shellcheck disable=SC2206
CURL_OPTS=(${EMIGA_CURL_OPTS:--fL --proto '=https' --proto-redir '=https' --remove-on-error --retry 3 --retry-all-errors})

TMP=""
cleanup() { [ -n "$TMP" ] && rm -rf "$TMP"; }
trap cleanup EXIT

say() { printf '%s\n' "$*"; }
die() { say "错误：$*" >&2; exit 1; }

# Explain a failed write into the app directory in plain language.
# On macOS 13+, App Management TCC (kTCCServiceSystemPolicyAppBundles) can
# silently block shell writes to app bundles with "Operation not permitted"
# even when Full Disk Access is granted and no prompt is ever shown.
report_write_failure() {
  case "$1" in
    *"peration not permitted"*)
      cat >&2 <<EOF

错误：无法写入 ${APP_DIR} 中的应用 —— macOS 的「App 管理」权限拦截了这次操作。
这在 macOS 13 及以上版本很常见，而且系统常常不弹任何提示。
注意：「完全磁盘访问」并不能覆盖这项权限。

请任选一种方式解决：
  1. 打开「系统设置 → 隐私与安全性 → App 管理」，给你正在使用的终端
     （终端 / iTerm 等）打开开关，然后重新运行这条安装命令；
  2. 或者改用访达手动安装：从下载页下载 .dmg，打开后把 Emiga Studio
     拖进「应用程序」文件夹即可。

EOF
      ;;
    *)
      say "" >&2
      say "错误：写入 ${APP_DIR} 失败：$1" >&2
      ;;
  esac
}

say "==> [1/6] 识别系统架构…"
arch="$("$UNAME" -m)"
case "$arch" in
  arm64)   target="mac-arm64" ;;
  x86_64)  target="mac-x64" ;;
  *) die "不支持的处理器架构：$arch（仅支持 Apple 芯片 arm64 和 Intel x86_64）" ;;
esac
say "    架构：$arch → $target"

TMP="$(mktemp -d)"

say "==> [2/6] 获取发布清单 latest-mac.yml…"
yml="$TMP/latest-mac.yml"
curl "${CURL_OPTS[@]}" -o "$yml" "$BASE/emiga/desktop/$target/latest-mac.yml" \
  || die "下载发布清单失败，请检查网络后重试（$BASE/emiga/desktop/$target/latest-mac.yml）"

# Pick the .zip entry (not the .dmg) from files: url / sha512 / size.
# Field lines inside an entry are indented; top-level keys are not.
read -r zip_url zip_sha zip_size < <(awk '
  /^[[:space:]]*- url:/ {
    if (collect) done = 1
    collect = 0
    if (!done && $3 ~ /\.zip$/) { url = $3; sha = ""; size = ""; collect = 1 }
    next
  }
  /^[^[:space:]]/ { if (collect) { done = 1; collect = 0 } }
  collect && /^[[:space:]]+sha512:/ { sha = $2 }
  collect && /^[[:space:]]+size:/   { size = $2 }
  END { if (url != "") print url, sha, size }
' "$yml") || true  # read exits non-zero on empty input; the checks below report it

[ -n "${zip_url:-}" ] || die "清单解析失败：latest-mac.yml 中没有 .zip 安装包条目"
[ -n "${zip_sha:-}" ] || die "清单解析失败：.zip 条目缺少 sha512 字段"
case "$zip_sha" in
  *[!A-Za-z0-9+/=]*) die "清单解析失败：.zip 条目的 sha512 字段格式异常" ;;
esac
case "$zip_url" in
  *..*|/**) die "清单中的文件路径异常：$zip_url" ;;
esac
say "    安装包：$zip_url"

say "==> [3/6] 下载安装包…"
zipfile="$TMP/app.zip"
curl "${CURL_OPTS[@]}" -o "$zipfile" "$BASE/emiga/desktop/$target/$zip_url" \
  || die "下载安装包失败，请检查网络后重试"

say "==> [4/6] 校验 SHA-512（与官方清单逐字节比对）…"
# Manifest sha512 is base64 (electron-builder format), not hex.
# tr -d '\n': GNU base64 wraps at 76 columns, macOS base64 does not.
actual_sha="$(openssl dgst -sha512 -binary "$zipfile" | base64 | tr -d '\n')"
if [ "$actual_sha" != "$zip_sha" ]; then
  die "文件校验失败，可能被篡改或下载损坏（SHA-512 与官方清单不匹配）。已中止安装并删除临时文件，请重试；若反复失败请勿继续安装"
fi
say "    校验通过"

say "==> [5/6] 解压并安装到 $APP_DIR …"
# Never extract straight into /Applications: ditto merges into existing
# bundles instead of replacing them, leaving stale files behind.
"$DITTO" -x -k "$zipfile" "$TMP/extract" || die "解压安装包失败"
[ -d "$TMP/extract/$APP_BUNDLE" ] || die "解压结果异常：未找到 $APP_BUNDLE"

# Quit a running instance first; overwriting a live bundle would let `open`
# silently reactivate the old process.
if "$PGREP" -f "$APP_BUNDLE" >/dev/null 2>&1; then
  say "    检测到 Emiga Studio 正在运行，正在请求其退出…"
  "$OSASCRIPT" -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true
  i=0
  while [ "$i" -lt 40 ]; do
    "$PGREP" -f "$APP_BUNDLE" >/dev/null 2>&1 || break
    sleep 0.5
    i=$((i + 1))
  done
  ! "$PGREP" -f "$APP_BUNDLE" >/dev/null 2>&1 \
    || die "Emiga Studio 未能在 20 秒内退出，请手动退出（⌘Q）后重新运行本命令"
  say "    已退出"
fi

# /Applications is root:admin 775 — non-admin users cannot write it.
# Fall back to ~/Applications instead of asking for sudo.
if [ ! -w "$APP_DIR" ]; then
  say "    提示：当前账户没有 $APP_DIR 的写入权限（需要管理员账户）。"
  say "    将改为安装到当前用户目录：$HOME/Applications"
  mkdir -p "$HOME/Applications" 2>/dev/null \
    || die "无法创建 $HOME/Applications，请改用管理员账户运行本命令"
  APP_DIR="$HOME/Applications"
fi
target_app="$APP_DIR/$APP_BUNDLE"

# Atomic swap, Squirrel.Mac style ("rename() is atomic"): move the old
# bundle aside (never rm -rf first), move the new one in, and only then
# delete the backup. Any failure rolls the backup back.
backup=""
if [ -e "$target_app" ]; then
  backup="$TMP/old"
  if ! mv_err="$(mv "$target_app" "$backup" 2>&1)"; then
    report_write_failure "$mv_err"
    exit 1
  fi
  say "    旧版本已移开（备份于临时目录）"
fi

if ! mv_err="$(mv "$TMP/extract/$APP_BUNDLE" "$target_app" 2>&1)"; then
  say "    安装失败，正在恢复旧版本…" >&2
  if [ -n "$backup" ]; then
    if mv "$backup" "$target_app" 2>/dev/null; then
      say "    旧版本已恢复，未造成影响。" >&2
    else
      trap - EXIT  # keep the temp dir so the backup survives
      say "    自动恢复失败，旧版本保留在：$backup（请手动拖回 $APP_DIR）" >&2
    fi
  fi
  report_write_failure "$mv_err"
  exit 1
fi

# New bundle is in place — only now is it safe to drop the backup.
[ -z "$backup" ] || rm -rf "$backup"
say "    已安装：$target_app"

say "==> [6/6] 启动 Emiga Studio…"
"$OPEN" -a "$target_app" || die "安装已完成，但自动启动失败，请从「$APP_DIR」手动打开 Emiga Studio"

say ""
say "完成！Emiga Studio 已安装并启动。之后的更新会在应用内自动完成，无需再运行本命令。"
