#!/usr/bin/env bash
# open-device — open the MetaMask Mobile dev client on a simulator or device.
#
# iOS: boots simulator if needed, writes EXDevMenuIsOnboardingFinished default,
# then opens via the expo-development-client deep link (disableOnboarding=1).
# Android: sets adb reverse for Metro port reachability, then opens via deep link.
# Preflight-mode guard: fast (teaching error if not installed), auto (install if missing),
# rebuild-native / clean (always rebuild), default (same as auto).
#
# Inputs:
#   --platform ios|android              (default ios)
#   --target <metamask-mobile dir>      (default $PWD)
#   --simulator <udid|name|"booted">    iOS simulator (default: IOS_SIMULATOR env, else "booted")
#   --adb-serial <serial>               Android device serial (default: ADB_SERIAL env)
#   --preflight-mode fast|auto|rebuild-native|clean  (default: fast; env MOBILE_PREFLIGHT_MODE)
#   --restart                            terminate the selected app before opening it
#   --ui-only                           iOS only: ensure Simulator.app is visible; do not relaunch the app
# Outputs:
#   Progress on stderr; exit 0 pass; 1 open failed; 2 bad args.
#
# Single device op: open the dev client. Does not start Metro or install node_modules.
set -uo pipefail

PLATFORM="ios"
TARGET="$PWD"
SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
PREFLIGHT_MODE="${MOBILE_PREFLIGHT_MODE:-fast}"
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
UI_ONLY=false
RESTART=false

while [ "$#" -gt 0 ]; do
  case "$1" in
    --platform)       PLATFORM="$2"; shift 2 ;;
    --target)         TARGET="$2"; shift 2 ;;
    --simulator)      SIMULATOR="$2"; shift 2 ;;
    --adb-serial)     ADB_SERIAL_ARG="$2"; shift 2 ;;
    --preflight-mode) PREFLIGHT_MODE="$2"; shift 2 ;;
    --port)           PORT="$2"; shift 2 ;;
    --restart)        RESTART=true; shift ;;
    --ui-only)        UI_ONLY=true; shift ;;
    -h|--help)
      printf 'Usage: open-device.sh [--platform ios|android] [--target <dir>]\n'
      printf '  [--simulator <udid|name|booted>] [--adb-serial <serial>]\n'
      printf '  [--preflight-mode fast|auto|rebuild-native|clean] [--port <n>] [--restart] [--ui-only]\n'
      exit 0
      ;;
    *) printf 'open-device: unknown arg: %s\n' "$1" >&2; exit 2 ;;
  esac
done

case "$PLATFORM" in
  ios|android) ;;
  *) printf 'open-device: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
esac
if [ "$PLATFORM" = "android" ]; then
  if [ -z "$ADB_BIN" ]; then
    ADB_BIN="$(command -v adb 2>/dev/null || true)"
  fi
  if [ -z "$ADB_BIN" ] || [ "${ADB_BIN#/}" = "$ADB_BIN" ] || [ ! -x "$ADB_BIN" ]; then
    printf 'open-device: Android SDK Platform-Tools (adb) is required but no executable was found.\n' >&2
    printf 'Next: brew install android-platform-tools\n' >&2
    exit 1
  fi
  export MM_HARNESS_ADB_PATH="$ADB_BIN"
fi
if $UI_ONLY && [ "$PLATFORM" != "ios" ]; then
  printf 'open-device: --ui-only is supported only for ios.\n' >&2
  exit 2
fi

TARGET="$(cd "$TARGET" && pwd)"

# Pin the repo's Ruby for the iOS native build (pod/gem work): a cold checkout
# under nohup/tmux otherwise uses the system Ruby and fails native gem builds.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
for _rb in "$SCRIPT_DIR/../shared/activate-repo-ruby.sh" "$SCRIPT_DIR/lib/activate-repo-ruby.sh"; do
  [ -f "$_rb" ] && { . "$_rb"; break; }
done
unset _rb

# --- iOS helpers --------------------------------------------------------------

ios_app_installed() {
  local sim_target="$1" bundle_id="$2"
  xcrun simctl get_app_container "$sim_target" "$bundle_id" app >/dev/null 2>&1
}

run_ios_native_build() (
  local sim_target="$1" bundle_id="$2" reason="$3" build_env="" build_script="" build_type=""
  case "$bundle_id" in
    io.metamask.MetaMask|io.metamask) build_script="start:ios"; build_type="main" ;;
    io.metamask.MetaMask-Flask) build_script="start:ios:flask"; build_type="flask" ;;
    *)
      printf 'open-device: no supported local build script for iOS bundle %s\n' "$bundle_id" >&2
      return 1
      ;;
  esac
  printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
  build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-ios-build-env.XXXXXX")" || return 1
  chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
  trap 'rm -f "$build_env"' EXIT HUP INT TERM
  {
    printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
    printf 'export MM_HARNESS_BUILD_IOS_SIMULATOR=%q\n' "$sim_target"
    printf 'export MM_HARNESS_BUILD_TYPE=%q\n' "$build_type"
    cat <<'BUILD_ENV'
mm_harness_restore_build_target() {
  export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
  export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
  export IOS_SIMULATOR="$MM_HARNESS_BUILD_IOS_SIMULATOR"
  export METAMASK_BUILD_TYPE="$MM_HARNESS_BUILD_TYPE"
}
eval() {
  builtin eval "$@"
  local status=$?
  mm_harness_restore_build_target
  return "$status"
}
source() {
  builtin source "$@"
  local status=$?
  mm_harness_restore_build_target
  return "$status"
}
mm_harness_restore_build_target
BUILD_ENV
  } > "$build_env"
  (
    cd "$TARGET"
    command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
    BASH_ENV="$build_env" WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" METAMASK_BUILD_TYPE="$build_type" EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
  )
)

ensure_ios_app_for_mode() {
  local sim_target="$1" bundle_id="$2" mode="$3"
  case "$mode" in
    fast)
      if ios_app_installed "$sim_target" "$bundle_id"; then return 0; fi
      printf >&2 'open-device: fast mode requires an installed iOS dev client (%s) on %s.\n' \
        "$bundle_id" "$sim_target"
      printf >&2 'Fast mode only prewarms the bundle and opens the existing client.\n'
      return 1
      ;;
    auto|default)
      if ios_app_installed "$sim_target" "$bundle_id"; then return 0; fi
      if ! run_ios_native_build "$sim_target" "$bundle_id" "iOS dev client ${bundle_id} not installed on ${sim_target}"; then
        printf 'open-device: iOS native build failed\n' >&2
        return 1
      fi
      ;;
    rebuild-native|clean)
      if ! run_ios_native_build "$sim_target" "$bundle_id" "Rebuilding iOS dev client ${bundle_id} (--preflight-mode ${mode})"; then
        printf 'open-device: iOS native build failed\n' >&2
        return 1
      fi
      ;;
    *)
      printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
  esac
}

sim_udid_for_target() {
  local target="$1"
  xcrun simctl list devices available --json 2>/dev/null | TARGET_SIMULATOR="$target" node -e '
let input = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => { input += chunk; });
process.stdin.on("end", () => {
  const target = process.env.TARGET_SIMULATOR || "";
  try {
    const parsed = JSON.parse(input);
    const devices = Object.values(parsed.devices || {}).flatMap((value) => Array.isArray(value) ? value : [])
      .filter((device) => device && device.isAvailable !== false && device.udid);
    const matches = /^[0-9A-Fa-f-]{36}$/.test(target)
      ? devices.filter((device) => device.udid.toLowerCase() === target.toLowerCase())
      : target === "booted"
        ? devices.filter((device) => device.state === "Booted")
        : devices.filter((device) => device.name === target);
    const booted = matches.filter((device) => device.state === "Booted");
    const found = matches.length === 1 ? matches[0] : booted.length === 1 ? booted[0] : null;
    if (found) {
      process.stdout.write(`${found.udid}\n`);
      return;
    }
    if (matches.length > 1) {
      process.stderr.write(`open-device: iOS simulator target ${JSON.stringify(target)} is ambiguous; use one UDID:\n`);
      for (const device of matches) {
        process.stderr.write(`  - ${device.udid} (${device.name}) [${device.state || "unknown"}]\n`);
      }
      process.exit(2);
    }
    process.exit(1);
  } catch {
    process.exit(1);
  }
});
'
}

print_missing_simulator_recovery() {
  local target="$1"
  if printf '%s' "$target" | grep -Eq '^[0-9A-Fa-f-]{36}$'; then
    printf "Next: mm-harness status --adapter mobile --target '%s' --all-devices --json\n" "$TARGET" >&2
  else
    printf "Next: mm-harness provision runway ios --adapter mobile --target '%s' --device '%s'\n" "$TARGET" "$target" >&2
  fi
}

boot_simulator_if_needed() {
  local target="$1" state=""
  local udid=""
  udid="$(sim_udid_for_target "$target")"
  if [ -z "$udid" ]; then
    printf "open-device: configured iOS simulator '%s' does not exist.\n" "$target" >&2
    print_missing_simulator_recovery "$target"
    return 1
  fi
  local line=""
  line="$(xcrun simctl list devices available 2>/dev/null | grep -F "(${udid})" | head -1 || true)"
  case "$line" in *"(Booted)"*) state="Booted" ;; *"(Shutdown)"*) state="Shutdown" ;; esac
  [ "$state" = "Booted" ] && return 0
  printf 'Booting iOS simulator %s%s\n' "$target" "${state:+ ($state)}" >&2
  xcrun simctl boot "$udid" 2>/dev/null || true
  xcrun simctl bootstatus "$udid" -b >/dev/null
}

simulator_ui_running() {
  pgrep -x Simulator >/dev/null 2>&1
}

open_simulator_in_background_if_needed() {
  local target="$1" udid=""
  [ "${MM_HARNESS_SHOW_SIMULATOR:-1}" != "0" ] || return 0
  simulator_ui_running && return 0
  printf 'Opening Simulator UI in the background\n' >&2
  if [ "$target" = "booted" ]; then
    open -g -a Simulator >/dev/null 2>&1 || {
      printf 'open-device: failed to open Simulator UI.\n' >&2
      return 1
    }
  else
    udid="$(sim_udid_for_target "$target")"
    if [ -n "$udid" ]; then
      open -g -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || {
        printf 'open-device: failed to open Simulator UI for %s.\n' "$target" >&2
        return 1
      }
    else
      open -g -a Simulator >/dev/null 2>&1 || {
        printf 'open-device: failed to open Simulator UI.\n' >&2
        return 1
      }
    fi
  fi
}

focus_simulator() {
  local target="$1" udid=""
  if [ "$target" != "booted" ]; then
    udid="$(sim_udid_for_target "$target")"
  fi
  if [ -n "$udid" ]; then
    open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
  else
    open -a Simulator >/dev/null 2>&1 || true
  fi
  osascript -e 'tell application "Simulator" to activate' >/dev/null 2>&1 || true
}

# --- Android helpers ----------------------------------------------------------

android_app_installed() {
  local pkg="$1"
  shift
  "$ADB_BIN" "$@" shell pm list packages 2>/dev/null | grep -q "package:${pkg}"
}

run_android_native_build() (
  local serial="$1" expo_device="$2" pkg="$3" reason="$4" build_env="" build_script=""
  case "$pkg" in
    io.metamask) build_script="start:android" ;;
    io.metamask.flask) build_script="start:android:flask" ;;
    *)
      printf 'open-device: no supported local build script for Android package %s\n' "$pkg" >&2
      return 1
      ;;
  esac
  printf '%s; installing with yarn %s (WATCHER_PORT=%s)\n' "$reason" "$build_script" "$PORT" >&2
  build_env="$(mktemp "${TMPDIR:-/tmp}/mm-harness-android-build-env.XXXXXX")" || return 1
  chmod 600 "$build_env" || { rm -f "$build_env"; return 1; }
  trap 'rm -f "$build_env"' EXIT HUP INT TERM
  {
    printf 'export MM_HARNESS_BUILD_ANDROID_SERIAL=%q\n' "$serial"
    printf 'export MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE=%q\n' "$expo_device"
    printf 'export MM_HARNESS_BUILD_WATCHER_PORT=%q\n' "$PORT"
    cat <<'BUILD_ENV'
mm_harness_restore_android_build_target() {
  export WATCHER_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
  export METRO_PORT="$MM_HARNESS_BUILD_WATCHER_PORT"
  export ADB_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
  export ANDROID_SERIAL="$MM_HARNESS_BUILD_ANDROID_SERIAL"
}
eval() {
  builtin eval "$@"
  local status=$?
  mm_harness_restore_android_build_target
  return "$status"
}
source() {
  builtin source "$@"
  local status=$?
  mm_harness_restore_android_build_target
  return "$status"
}
yarn() {
  if [ "${1:-}" = expo ] && [ "${2:-}" = run:android ]; then
    command yarn "$@" "$MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE"
  else
    command yarn "$@"
  fi
}
mm_harness_restore_android_build_target
BUILD_ENV
  } > "$build_env"
  (
    cd "$TARGET"
    BASH_ENV="$build_env" \
      WATCHER_PORT="$PORT" METRO_PORT="$PORT" \
      ADB_SERIAL="$serial" ANDROID_SERIAL="$serial" \
      EXPO_NO_TYPESCRIPT_SETUP=1 yarn "$build_script" 2>&1
  )
)

resolve_android_expo_device() {
  local serial="$1" rows="" selected_line="" expo_device="" row_serial="" row_state="" row_name="" matches=0
  [ -n "$serial" ] || return 0
  rows="$($ADB_BIN devices -l 2>/dev/null || true)"
  selected_line="$(printf '%s\n' "$rows" | awk -v wanted="$serial" '$1 == wanted { print; exit }')"
  if [ -z "$selected_line" ]; then
    printf 'open-device: configured Android device %s is not attached\n' "$serial" >&2
    return 1
  fi
  row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
  if [ "$row_state" != "device" ]; then
    printf 'open-device: configured Android device %s is not ready (state: %s)\n' "$serial" "$row_state" >&2
    return 1
  fi
  if printf '%s' "$serial" | grep -q '^emulator-'; then
    expo_device="$($ADB_BIN -s "$serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
  else
    expo_device="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
  fi
  [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
  expo_device="${expo_device// /_}"
  if [ -z "$expo_device" ]; then
    printf 'open-device: could not resolve Expo device name for Android serial %s\n' "$serial" >&2
    return 1
  fi

  while IFS= read -r selected_line; do
    row_serial="$(printf '%s\n' "$selected_line" | awk '{print $1}')"
    row_state="$(printf '%s\n' "$selected_line" | awk '{print $2}')"
    [ "$row_state" = "device" ] || continue
    if printf '%s' "$row_serial" | grep -q '^emulator-'; then
      row_name="$($ADB_BIN -s "$row_serial" emu avd name 2>/dev/null | tr -d '\r' | sed '/^OK$/d' | head -1 || true)"
    else
      row_name="$(printf '%s\n' "$selected_line" | awk '{ for (i = 3; i <= NF; i += 1) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit } }')"
    fi
    row_name="${row_name// /_}"
    [ "$row_name" = "$expo_device" ] && matches=$((matches + 1))
  done <<EOF
$rows
EOF
  if [ "$matches" -ne 1 ]; then
    printf 'open-device: Expo device name %s is ambiguous across %s attached Android devices; Expo cannot select serial %s safely\n' \
      "$expo_device" "$matches" "$serial" >&2
    return 1
  fi
  printf '%s\n' "$expo_device"
}

ensure_android_app_for_mode() {
  local pkg="$1" mode="$2"
  local expo_device=""
  shift 2
  case "$mode" in
    fast)
      if android_app_installed "$pkg" "$@"; then return 0; fi
      printf >&2 'open-device: fast mode requires an installed Android dev client (%s).\n' "$pkg"
      return 1
      ;;
    auto|default)
      if android_app_installed "$pkg" "$@"; then return 0; fi
      if [ -n "$ADB_SERIAL_ARG" ]; then
        expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
      fi
      [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
      expo_device="${expo_device// /_}"
      if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Android dev client ${pkg} not installed"; then
        printf 'open-device: Android native build failed\n' >&2
        return 1
      fi
      ;;
    rebuild-native|clean)
      if [ -n "$ADB_SERIAL_ARG" ]; then
        expo_device="$(resolve_android_expo_device "$ADB_SERIAL_ARG")" || return 1
      fi
      [ -n "$expo_device" ] || expo_device="${ANDROID_TARGET_DEVICE_NAME:-}"
      expo_device="${expo_device// /_}"
      if ! run_android_native_build "$ADB_SERIAL_ARG" "$expo_device" "$pkg" "Rebuilding Android dev client ${pkg} (--preflight-mode ${mode})"; then
        printf 'open-device: Android native build failed\n' >&2
        return 1
      fi
      ;;
    *)
      printf 'open-device: unknown --preflight-mode: %s\n' "$mode" >&2; return 2 ;;
  esac
}

# URL-encode using python3 (same as baseline).
urlencode() {
  python3 - "$1" <<'PY'
import sys, urllib.parse
print(urllib.parse.quote(sys.argv[1], safe=''))
PY
}

# --- dispatch -----------------------------------------------------------------

if [ -n "${IOS_BUNDLE_ID:-}" ]; then
  BUNDLE_IDS=("$IOS_BUNDLE_ID")
else
  BUNDLE_IDS=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
fi
if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
  ANDROID_PKGS=("$ANDROID_PACKAGE_ID")
else
  ANDROID_PKGS=("io.metamask" "io.metamask.flask" "io.metamask.qa")
fi

if [ "$PLATFORM" = "ios" ]; then
  SIM_TARGET="$(sim_udid_for_target "$SIMULATOR")"
  SIM_RESOLVE_STATUS=$?
  if [ "$SIM_RESOLVE_STATUS" -ne 0 ]; then
    if [ "$SIM_RESOLVE_STATUS" -eq 1 ]; then
      printf "open-device: configured iOS simulator '%s' does not exist.\n" "$SIMULATOR" >&2
      print_missing_simulator_recovery "$SIMULATOR"
    else
      printf "Next: mm-harness status --adapter mobile --target '%s' --all-devices --json\n" "$TARGET" >&2
    fi
    exit 1
  fi
  # Expo dev-client URL scheme — drives both the deep link and the scheme-approval key.
  DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"

  boot_simulator_if_needed "$SIM_TARGET" || exit 1
  open_simulator_in_background_if_needed "$SIM_TARGET" || exit 1
  if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
    focus_simulator "$SIM_TARGET"
  fi
  if $UI_ONLY; then
    exit 0
  fi

  LAUNCHED=false
  for BUNDLE_ID in "${BUNDLE_IDS[@]}"; do
    if ! ensure_ios_app_for_mode "$SIM_TARGET" "$BUNDLE_ID" "$PREFLIGHT_MODE"; then
      case "$PREFLIGHT_MODE" in
        fast) continue ;;
        *) exit 1 ;;
      esac
    fi
    if $RESTART; then
      xcrun simctl terminate "$SIM_TARGET" "$BUNDLE_ID" >/dev/null 2>&1 || true
      printf 'Restarting iOS dev client %s on %s\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
    fi

    # Dismiss Expo dev-menu onboarding for clean headless runs.
    xcrun simctl spawn "$SIM_TARGET" defaults write "$BUNDLE_ID" \
      EXDevMenuIsOnboardingFinished -bool YES 2>/dev/null || true

    # Pre-approve the dev-client deep-link scheme so openurl never pauses on the
    # "Open in <App>?" dialog. The key encodes the CoreSimulator bridge + scheme;
    # the value is the bundle ID of the approved handler.
    xcrun simctl spawn "$SIM_TARGET" defaults write \
      com.apple.launchservices.schemeapproval \
      "com.apple.CoreSimulator.CoreSimulatorBridge-->${DEV_CLIENT_SCHEME}" \
      -string "$BUNDLE_ID" 2>/dev/null || true

    # Open via expo-development-client deep link so the app connects to our Metro.
    ENCODED_URL="$(urlencode "http://localhost:${PORT}?disableOnboarding=1" 2>/dev/null \
      || printf 'http%%3A%%2F%%2Flocalhost%%3A%s%%3FdisableOnboarding%%3D1' "$PORT")"
    DEEP_LINK="${DEV_CLIENT_SCHEME}://expo-development-client/?url=${ENCODED_URL}"

    printf 'Launching iOS dev client %s on %s\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
    if xcrun simctl openurl "$SIM_TARGET" "$DEEP_LINK" 2>/dev/null; then
      LAUNCHED=true
      printf 'open-device: iOS dev client opened (%s on %s)\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
      break
    fi
    # Fallback: plain launch (older Expo / non-development-client build).
    if xcrun simctl launch "$SIM_TARGET" "$BUNDLE_ID" 2>/dev/null; then
      LAUNCHED=true
      printf 'open-device: iOS dev client launched (fallback) (%s on %s)\n' "$BUNDLE_ID" "$SIM_TARGET" >&2
      break
    fi
  done

  if [ "$LAUNCHED" = false ]; then
    printf 'open-device: no MetaMask bundle found on simulator %s\n' "$SIM_TARGET" >&2
    printf '  tried: %s\n' "${BUNDLE_IDS[*]}" >&2
    printf "Next: mm-harness provision runway ios --adapter mobile --target '%s' --device '%s' --force\n" "$TARGET" "$SIM_TARGET" >&2
    exit 1
  fi

else
  ADB_ARGS=()
  [ -n "$ADB_SERIAL_ARG" ] && ADB_ARGS+=("-s" "$ADB_SERIAL_ARG")

  LAUNCHED=false
  for PKG in "${ANDROID_PKGS[@]}"; do
    if ! ensure_android_app_for_mode "$PKG" "$PREFLIGHT_MODE" "${ADB_ARGS[@]}"; then
      case "$PREFLIGHT_MODE" in
        fast) continue ;;
        *) exit 1 ;;
      esac
    fi
    if $RESTART; then
      "$ADB_BIN" "${ADB_ARGS[@]}" shell am force-stop "$PKG" >/dev/null 2>&1 || true
      printf 'Restarting Android dev client %s\n' "$PKG" >&2
    fi

    # Installing/rebuilding a dev client can invalidate adb reverse mappings.
    # Restore Metro reachability only after the app is ensured, immediately
    # before opening the development-client URL.
    "$ADB_BIN" "${ADB_ARGS[@]}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1 || \
      printf 'open-device: adb reverse failed (non-fatal — device may already be reachable)\n' >&2

    ENCODED_URL="$(urlencode "http://localhost:${PORT}?disableOnboarding=1" 2>/dev/null \
      || printf 'http%%3A%%2F%%2Flocalhost%%3A%s%%3FdisableOnboarding%%3D1' "$PORT")"
    DEEP_LINK="expo-metamask://expo-development-client/?url=${ENCODED_URL}"

    printf 'Launching Android dev client %s\n' "$PKG" >&2
    if "$ADB_BIN" "${ADB_ARGS[@]}" shell am start \
        -a android.intent.action.VIEW \
        -d "$DEEP_LINK" >/dev/null 2>&1; then
      LAUNCHED=true
      printf 'open-device: Android dev client opened (%s)\n' "$PKG" >&2
      break
    fi
  done

  if [ "$LAUNCHED" = false ]; then
    printf 'open-device: no MetaMask package found on Android device %s\n' \
      "${ADB_SERIAL_ARG:-<default>}" >&2
    printf '  tried: %s\n' "${ANDROID_PKGS[*]}" >&2
    if [ -n "$ADB_SERIAL_ARG" ]; then
      printf "Next: mm-harness launch android --build --target '%s' --device '%s'\n" "$TARGET" "$ADB_SERIAL_ARG" >&2
    else
      printf "Next: mm-harness launch android --build --target '%s'\n" "$TARGET" >&2
    fi
    exit 1
  fi
fi

printf '  Next: mm-harness logs   # tail Metro\n' >&2
