#!/bin/bash
# 配布された BundleCode を /Applications に入れる。**使う側の Mac で**実行する。
#
#   bash bundlecode-install.sh ~/Downloads/BundleCode-darwin-arm64-<短縮commit>.zip
#
# `bundlecode-release.sh --dist` が作る zip を受け取る側の手順。やることはインストール
# 経路（`bundlecode-release.sh` を引数なしで実行した場合）が自分にやっているのと同じで、
# 別のマシンに渡ると手順書に頼るしかなくなり、抜けが出る。実際に quarantine と
# lsregister を飛ばして Electron の既定ウィンドウが出た。だからスクリプトにしてある。
#
# リポジトリのチェックアウトは要らない。このファイルと zip だけあれば動く。
set -euo pipefail

APP_NAME="BundleCode"
INSTALLED_APP="/Applications/${APP_NAME}.app"
USER_DATA_DIR="${HOME}/Library/Application Support/${APP_NAME}"
CLI_LINK="/opt/homebrew/bin/bcode"
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"

ZIP="${1:-}"
if [ -z "$ZIP" ] || [ ! -f "$ZIP" ]; then
	echo "使い方: bash $0 <配布された zip>" >&2
	exit 1
fi

WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT

echo "==> 展開"
# unzip では壊れる。.app はシンボリックリンクと framework 構造を持ち、それを保てないと
# 署名が壊れて起動そのものが失敗する。ditto は署名も拡張属性も保つ。
ditto -x -k "$ZIP" "$WORK"

NEW_APP="${WORK}/${APP_NAME}.app"
if [ ! -d "$NEW_APP" ]; then
	echo "zip の中に ${APP_NAME}.app が見つかりません" >&2
	exit 1
fi

# 中身が欠けたものを入れてしまうと、起動前に dyld が abort して原因が見えない。
# 先に確かめる。
if [ ! -d "${NEW_APP}/Contents/Frameworks/Electron Framework.framework" ] \
	|| [ ! -f "${NEW_APP}/Contents/Info.plist" ]; then
	echo "zip の中身が不完全です" >&2
	exit 1
fi

if codesign --verify --deep --strict "$NEW_APP" 2>/dev/null; then
	echo "    署名は有効です"
else
	echo "    警告: 署名の検証に失敗しました。起動できない可能性があります" >&2
fi

# 起動中だと差し替えに失敗する。強制終了はロックとソケットを残し、次の起動が
# 「既存インスタンスが居る」と誤認して無言で終わるので、消えるまで見る。
if pgrep -f "${INSTALLED_APP}/Contents/MacOS" >/dev/null 2>&1; then
	echo "==> 起動中の ${APP_NAME} を終了"
	osascript -e "quit app \"${APP_NAME}\"" 2>/dev/null || true
	for _ in $(seq 1 20); do
		pgrep -f "${INSTALLED_APP}/Contents/MacOS" >/dev/null 2>&1 || break
		sleep 1
	done
	if pgrep -f "${INSTALLED_APP}/Contents/MacOS" >/dev/null 2>&1; then
		echo "    終了しないので強制終了します"
		pkill -f "${INSTALLED_APP}/Contents/MacOS" 2>/dev/null || true
		sleep 2
	fi
fi
if ! pgrep -f "${INSTALLED_APP}/Contents/MacOS" >/dev/null 2>&1; then
	rm -f "${USER_DATA_DIR}/code.lock" "${USER_DATA_DIR}"/*-main.sock 2>/dev/null || true
fi

echo "==> ${INSTALLED_APP} を差し替え"
# 残したまま重ねると古い中身と混ざる。消してから入れる。
if [ -e "$INSTALLED_APP" ] && ! rm -rf "$INSTALLED_APP" 2>/dev/null; then
	echo "    権限が足りないので sudo で消します"
	sudo rm -rf "$INSTALLED_APP"
fi
if ! mv "$NEW_APP" "$INSTALLED_APP" 2>/dev/null; then
	sudo mv "$NEW_APP" "$INSTALLED_APP"
fi

# 転送経由だと quarantine が付く。macOS 15 以降は右クリック → 開く の回避策が
# 廃止されているので、外さないと初回起動がブロックされてシステム設定へ飛ばされる。
echo "==> quarantine を外す"
xattr -dr com.apple.quarantine "$INSTALLED_APP" 2>/dev/null || true

# 公証していないので spctl は必ず rejected になる。恒久的に黙らせるには登録が要るが、
# sudo を求めるので、通せなければ案内だけ出して続ける。
if sudo -n spctl --add --label "${APP_NAME}" "$INSTALLED_APP" 2>/dev/null; then
	echo "    Gatekeeper に登録しました"
else
	echo "    Gatekeeper への登録は飛ばしました。ブロックされたら次を一度実行:"
	echo "      sudo spctl --add --label \"${APP_NAME}\" ${INSTALLED_APP}"
fi

# open -a は名前で解決する。古い登録が残っていると、リブランド前の署名
# （Identifier=Electron）の方を掴んで Electron の既定ウィンドウが出たり、
# 成功を返すのにウィンドウが出なかったりする。
echo "==> LaunchServices に登録し直す"
[ -x "$LSREGISTER" ] && "$LSREGISTER" -f "$INSTALLED_APP" 2>/dev/null || true

# commit を upstream のベースに固定しているので、焼き直しても鍵が変わらず中身だけが
# 変わる。翻訳は文字列をインデックスの配列で持つため、捨てないと無関係な訳文が出たり
# プレースホルダが {0} のまま残ったりする。
echo "==> commit 単位のキャッシュを破棄"
rm -rf "${USER_DATA_DIR}/clp" "${USER_DATA_DIR}/CachedData"

# darwin ビルドは CLI を applicationName ではなく bin/code という固定名で出力する
# ことがある。ln -sf は対象が無くてもリンクを作ってしまうので、実際に入っている
# 名前を見る。
BIN_SOURCE="${INSTALLED_APP}/Contents/Resources/app/bin/bcode"
[ -f "$BIN_SOURCE" ] || BIN_SOURCE="${INSTALLED_APP}/Contents/Resources/app/bin/code"
if [ -f "$BIN_SOURCE" ]; then
	echo "==> bcode コマンドをリンク"
	if mkdir -p "$(dirname "$CLI_LINK")" 2>/dev/null && ln -sf "$BIN_SOURCE" "$CLI_LINK" 2>/dev/null; then
		echo "    $CLI_LINK -> $BIN_SOURCE"
	else
		echo "    $(dirname "$CLI_LINK") に書けないので飛ばしました" >&2
	fi
fi

echo
PRODUCT="${INSTALLED_APP}/Contents/Resources/app/product.json"
if [ -f "$PRODUCT" ] && command -v node >/dev/null; then
	node -e '
		const p = require(process.argv[1]);
		console.log("完了: " + p.nameLong + " " + p.version);
		console.log("    commit: " + p.commit);
		console.log("    date:   " + p.date);
	' "$PRODUCT"
else
	echo "完了: $INSTALLED_APP"
fi
echo "起動: open -a ${APP_NAME}   /   bcode ."
