#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat >&2 <<'EOF'
Usage:
  mm-recipe                         interactive menu
  mm-recipe ios                     start/reuse Metro, launch iOS dev client
  mm-recipe android                 start/reuse Metro, launch Android dev client
  mm-recipe prepare                 install harness, start/reuse runtime, optionally setup wallet
    --target <repo> --platform ios|android --port <port> --simulator <sim> --adb-serial <serial> --runtime-dir <dir> --wallet-setup
  mm-recipe setup:ios               ios + setup wallet fixture
  mm-recipe setup:android           android + setup wallet fixture
  mm-recipe refresh                 start/reuse Metro, relaunch detected dev client
  mm-recipe logs                    tail Metro log
  mm-recipe status                  app route/account status
  mm-recipe runtime-status [--json] structured Metro/bundle/bridge status
  mm-recipe route                   current route
  mm-recipe navigate <Route> [json] navigate by React Navigation route
  mm-recipe unlock [password]       unlock with fixture password if omitted
  mm-recipe setup-wallet [fixture]  apply wallet fixture
  mm-recipe accounts                list accounts
  mm-recipe select-account <addr>   switch account
  mm-recipe screenshot [path]       simulator/device screenshot
  mm-recipe stop                    stop this slot's Metro
  mm-recipe actions [--json]
  mm-recipe doctor [--json]
  mm-recipe run <recipe.json> [args] run recipe with detected slot env
  mm-recipe <recipe.json> [args]    shortcut for run

Detects Mobile root, .js.env, WATCHER_PORT, IOS_SIMULATOR/ANDROID_DEVICE.
EOF
}

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
runner="$script_dir/metamask-recipe"
runner_root="$(cd "$script_dir/.." && pwd -P)"
# shellcheck disable=SC1091
. "$runner_root/scripts/lib/harness-path.sh"
bridge="$runner_root/live-adapters/mobile/bridge-runtime/cdp-bridge.cjs"
setup_wallet_script="$runner_root/live-adapters/mobile/bridge-runtime/setup-wallet.sh"

find_mobile_root() {
  local dir="$(pwd -P)"
  while [ "$dir" != "/" ]; do
    if [ -f "$dir/package.json" ] && { grep -q 'metamask-mobile' "$dir/package.json" 2>/dev/null || [[ "$(basename "$dir")" == metamask-mobile* ]]; }; then
      printf '%s\n' "$dir"
      return 0
    fi
    dir="$(dirname "$dir")"
  done
  return 1
}

load_env_file() {
  local file="$1"
  [ -f "$file" ] || return 0
  while IFS= read -r line || [ -n "$line" ]; do
    line="${line#export }"
    case "$line" in
      WATCHER_PORT=*|CDP_PORT=*|IOS_SIMULATOR=*|SIM_UDID=*|ANDROID_DEVICE=*|ADB_SERIAL=*|ANDROID_SERIAL=*)
        key="${line%%=*}"
        val="${line#*=}"
        val="${val%%#*}"
        val="$(printf '%s' "$val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
        [ -n "${!key:-}" ] || export "$key=$val"
        ;;
    esac
  done < "$file"
}

infer_slot_number() {
  local root="$1" base
  base="$(basename "$root")"
  [[ "$base" =~ -([0-9]+)$ ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}

init_context() {
  root="$(find_mobile_root)" || { echo "mm-recipe: run from a metamask-mobile checkout" >&2; exit 2; }
  load_env_file "$root/.js.env"
  load_env_file "$root/.env"
  load_env_file "$root/.env.local"
  slot="$(infer_slot_number "$root" || true)"
  if [ -n "$slot" ]; then
    [ -n "${WATCHER_PORT:-}" ] || export WATCHER_PORT="806$slot"
    [ -n "${IOS_SIMULATOR:-}" ] || export IOS_SIMULATOR="mm-$slot"
  fi
  [ -n "${CDP_PORT:-}" ] || export CDP_PORT="${WATCHER_PORT:-}"
  runtime_dir="$(recipe_runtime_dir)"
  export RECIPE_RUNTIME_DIR="$runtime_dir"
  mkdir -p "$root/$runtime_dir" "$root/temp/recipe-artifacts"
}


pretty_json() {
  node -e '
let d="";
process.stdin.on("data", c => d += c).on("end", () => {
  const color = process.env.RECIPE_COLOR === "1";
  const c = (code, s) => color ? `\x1b[${code}m${s}\x1b[0m` : s;
  try {
    const v = JSON.parse(d);
    if (v.decision) {
      console.log(`${c("1;33", "Decision")}: ${c("1;32", v.decision)}${v.reasonCode ? ` ${c("2", "—")} ${c("0;37", v.reasonCode)}` : ""}`);
      if (Array.isArray(v.reasons)) for (const r of v.reasons) console.log(`  ${c("2", "-")} ${r}`);
      return;
    }
    if (v.adapter && Array.isArray(v.actions) && v.actions.every(a => a && a.name)) {
      console.log(`${c("1;36", "Actions")} ${c("2", `(${v.adapter})`)}:`);
      for (const a of v.actions) {
        console.log(`  ${c("1;32", a.name)}${a.description ? ` ${c("2", "—")} ${c("0;37", a.description)}` : ""}`);
      }
      return;
    }
    if (v.status && v.adapter) {
      const status = String(v.status).toUpperCase();
      const code = status === "PASS" || status === "OK" ? "1;32" : status === "FAIL" || status === "ERROR" ? "1;31" : "1;33";
      console.log(`${c(code, status)} ${c("1;36", v.adapter)}${v.compatibilityMode ? ` ${c("2", "—")} ${v.compatibilityMode}` : ""}`);
      return;
    }
    if (v.account || v.route || v.deviceName) {
      console.log(`${c("1;36", "Device")}: ${v.deviceName || "?"}${v.platform ? ` ${c("2", `(${v.platform})`)}` : ""}`);
      console.log(`${c("1;36", "Route")}: ${v.route && v.route.name ? v.route.name : JSON.stringify(v.route || null)}`);
      if (v.account) console.log(`${c("1;36", "Account")}: ${v.account.name || "?"} ${c("2", v.account.address || "")}`);
      return;
    }
    console.log(JSON.stringify(v, null, 2));
  } catch {
    process.stdout.write(d);
  }
})'
}

json_requested() {
  for arg in "$@"; do [ "$arg" = "--json" ] && return 0; done
  return 1
}

color_requested() {
  [ "${RECIPE_NO_COLOR:-}" != "1" ] && { [ "${RECIPE_COLOR:-}" = "1" ] || [ "${FORCE_COLOR:-}" = "1" ] || [ -t 1 ]; }
}

pipe_pretty() {
  if color_requested; then (export RECIPE_COLOR=1; pretty_json); else pretty_json; fi
}

run_pretty() {
  if json_requested "$@"; then "$@"; else "$@" | pipe_pretty; fi
}

run_json_pretty() {
  if json_requested "$@"; then "$@"; else "$@" --json | pipe_pretty; fi
}
log() { printf 'mm-recipe: %s\n' "$*" >&2; }

resolve_mobile_harness_script() {
  local candidate dir

  for candidate in \
    "${FARMSLOT_RECIPE_HARNESS_SCRIPT:-}" \
    "${RECIPE_HARNESS_SCRIPT:-}"; do
    if [ -n "$candidate" ] && [ -x "$candidate" ]; then
      printf '%s\n' "$candidate"
      return 0
    fi
  done

  for dir in \
    "$root/.agents/skills/mms-recipe-harness" \
    "$root/.claude/skills/mms-recipe-harness" \
    "$root/.cursor/rules/mms-recipe-harness"; do
    [ -n "$dir" ] || continue
    for candidate in "$dir/scripts/recipe-harness" "$dir/scripts/recipe-harness.sh"; do
      if [ -x "$candidate" ]; then
        printf '%s\n' "$candidate"
        return 0
      fi
    done
  done

  return 1
}

install_mobile_harness() {
  local harness_script
  if harness_script="$(resolve_mobile_harness_script)"; then
    log "Installing mobile recipe harness via ${harness_script}"
    RECIPE_HARNESS_ROOT="$(harness_root)" "$harness_script" mobile install --target "$root"
    return 0
  fi

  cat >&2 <<EOF
mm-recipe: mobile recipe harness skill not found.

Prepare requires the canonical recipe-harness skill install path; refusing to
use the runner-private injector because that reintroduces harness drift.

Set FARMSLOT_RECIPE_HARNESS_SCRIPT or RECIPE_HARNESS_SCRIPT to
<recipe-harness>/scripts/recipe-harness, or install the mms-recipe-harness skill
inside the target checkout.
EOF
  return 1
}

mobile_harness_patched() {
  [ -f "$root/app/core/AgenticService/AgenticService.ts" ] \
    && grep -q "AgenticService.install" "$root/app/core/NavigationService/NavigationService.ts" 2>/dev/null \
    && grep -q "AgentStepHud" "$root/app/components/Nav/App/App.tsx" 2>/dev/null
}

ensure_mobile_harness_patched() {
  init_context
  if mobile_harness_patched; then
    return 0
  fi

  local had_metro=false
  if metro_ready; then
    had_metro=true
  fi

  log "Mobile recipe harness patches missing or stale; reinstalling before launch"
  install_mobile_harness

  if ! mobile_harness_patched; then
    echo "mm-recipe: mobile recipe harness install completed but required patch markers are still missing" >&2
    return 1
  fi

  if [ "$had_metro" = true ]; then
    log "Restarting Metro so the rebuilt graph includes the refreshed harness patches"
    stop_metro
  fi
}

metro_ready() {
  curl -sf --max-time 2 "http://localhost:${WATCHER_PORT:-8081}/status" 2>/dev/null | grep -q 'packager-status:running'
}


clear_stale_metro_listener() {
  local pids
  pids="$(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
  [ -n "$pids" ] || return 0
  log "Clearing stale Metro listener on port ${WATCHER_PORT}: $pids"
  # shellcheck disable=SC2086
  kill $pids 2>/dev/null || true
  for _ in $(seq 1 20); do
    pids="$(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
    [ -z "$pids" ] && return 0
    sleep 0.25
  done
  log "Metro listener still held on port ${WATCHER_PORT}; forcing: $pids"
  # shellcheck disable=SC2086
  kill -KILL $pids 2>/dev/null || true
  for _ in $(seq 1 20); do
    pids="$(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
    [ -z "$pids" ] && return 0
    sleep 0.25
  done
  echo "mm-recipe: port ${WATCHER_PORT:-8081} is still occupied after killing stale Metro: $pids" >&2
  return 1
}


start_metro_tmux_window() {
  local metro_workers="$1" metro_log="$2" metro_pid="$3"
  local session window cmd detached_session slot_session
  command -v tmux >/dev/null 2>&1 || return 1
  window="metro-${WATCHER_PORT}"
  cmd="env CI=1 EXPO_NO_TYPESCRIPT_SETUP=1 WATCHER_PORT=${WATCHER_PORT} METRO_MAX_WORKERS=${metro_workers} yarn expo start --port ${WATCHER_PORT} > ${metro_log} 2>&1"
  session=""
  if [ -n "${slot:-}" ]; then
    slot_session="mm-${slot}"
    if tmux has-session -t "=${slot_session}" 2>/dev/null; then
      session="$slot_session"
    fi
  fi
  if [ -z "$session" ]; then
    session="$(tmux display-message -p '#S' 2>/dev/null || true)"
  fi
  if [ -n "$session" ]; then
    tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
    tmux new-window -d -t "$session" -n "$window" -c "$root" "$cmd"
    printf 'tmux:%s:%s\n' "$session" "$window" > "$metro_pid"
    log "Started Metro in tmux window ${session}:${window}"
    return 0
  fi

  detached_session="metamask-mobile-${WATCHER_PORT}-metro"
  tmux kill-session -t "=${detached_session}" >/dev/null 2>&1 || true
  tmux new-session -d -s "$detached_session" -c "$root" "$cmd"
  printf 'tmux-session:%s\n' "$detached_session" > "$metro_pid"
  log "Started Metro in tmux session ${detached_session}"
}

default_metro_workers() {
  if command -v sysctl >/dev/null 2>&1; then
    local cores
    cores="$(sysctl -n hw.perflevel0.physicalcpu 2>/dev/null || sysctl -n hw.physicalcpu 2>/dev/null || true)"
    if [[ "$cores" =~ ^[0-9]+$ ]] && [ "$cores" -gt 0 ]; then
      if [ "$cores" -gt 6 ]; then printf '6\n'; else printf '%s\n' "$cores"; fi
      return 0
    fi
  fi
  printf '4\n'
}

start_metro() {
  init_context
  if metro_ready; then
    log "Metro already running on port ${WATCHER_PORT}"
    return 0
  fi
  clear_stale_metro_listener
  local metro_workers="${METRO_MAX_WORKERS:-$(default_metro_workers)}"
  local runtime_dir="$(recipe_runtime_dir)"
  local metro_log="$runtime_dir/metro.log"
  local metro_pid="$runtime_dir/metro.pid"
  log "Starting Metro on port ${WATCHER_PORT} (METRO_MAX_WORKERS=${metro_workers})"
  (
    cd "$root"
    mkdir -p "$(dirname "$metro_log")"
    : > "$metro_log"
    start_metro_tmux_window "$metro_workers" "$metro_log" "$metro_pid" || {
      nohup env CI=1 EXPO_NO_TYPESCRIPT_SETUP=1 WATCHER_PORT="$WATCHER_PORT" METRO_MAX_WORKERS="$metro_workers" yarn expo start --port "$WATCHER_PORT" </dev/null > "$metro_log" 2>&1 &
      echo $! > "$metro_pid"
    }
  )
  for _ in $(seq 1 90); do
    metro_ready && { log "Metro ready: http://localhost:${WATCHER_PORT}/status"; log "Metro log: $metro_log (tail with: recipe logs)"; return 0; }
    sleep 1
  done
  tail -80 "$root/$metro_log" >&2 || true
  echo "mm-recipe: Metro did not become ready on port ${WATCHER_PORT}" >&2
  return 1
}

bundle_url() {
  local platform="$1"
  printf 'http://localhost:%s/index.bundle?platform=%s&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=1&transform.routerRoot=app&unstable_transformProfile=hermes-stable\n' \
    "${WATCHER_PORT:-8081}" \
    "$platform"
}

prewarm_bundle() {
  local platform="$1"
  local timeout="${MOBILE_BUNDLE_PREWARM_TIMEOUT:-900}"
  local runtime_dir="$(recipe_runtime_dir)"
  local metro_log="$root/$runtime_dir/metro.log"
  local url bundle_pid elapsed=0 last_progress=""
  [ "${MOBILE_BUNDLE_PREWARM:-1}" != "0" ] || { log "Skipping ${platform} bundle prewarm (MOBILE_BUNDLE_PREWARM=0)"; return 0; }
  url="$(bundle_url "$platform")"
  log "Prewarming ${platform} bundle before launching dev client (set MOBILE_BUNDLE_PREWARM=0 to skip)"
  curl -sfL --connect-timeout 5 --max-time "$timeout" -o /dev/null "$url" &
  bundle_pid=$!
  while kill -0 "$bundle_pid" 2>/dev/null; do
    local progress=""
    if [ -f "$metro_log" ]; then
      progress="$(grep -E "^[[:space:]]*(iOS|Android).*index\\.(js|tsx?).*%|Bundl(ed|ing)|Finished" "$metro_log" | tail -1 | tr -d '\r' || true)"
    fi
    if [ -n "$progress" ] && [ "$progress" != "$last_progress" ]; then
      log "Bundle progress: $progress"
      last_progress="$progress"
    elif [ $((elapsed % 30)) -eq 0 ]; then
      log "Waiting for ${platform} bundle (${elapsed}s/${timeout}s)${last_progress:+ — $last_progress}"
    fi
    if [ "$elapsed" -ge "$timeout" ]; then
      kill "$bundle_pid" 2>/dev/null || true
      wait "$bundle_pid" 2>/dev/null || true
      echo "mm-recipe: ${platform} bundle did not become ready before launch timeout (${timeout}s)" >&2
      tail -80 "$metro_log" >&2 || true
      return 1
    fi
    sleep 3
    elapsed=$((elapsed + 3))
  done
  if wait "$bundle_pid"; then
    log "${platform} bundle ready"
    return 0
  fi
  echo "mm-recipe: ${platform} bundle did not become ready before launch" >&2
  tail -80 "$metro_log" >&2 || true
  return 1
}

sim_target() { printf '%s\n' "${SIM_UDID:-${IOS_SIMULATOR:-booted}}"; }

sim_udid_for_target() {
  local target="$1"
  if [[ "$target" =~ ^[0-9A-Fa-f-]{36}$ ]]; then
    printf '%s\n' "$target"
    return 0
  fi
  xcrun simctl list devices available | sed -n "s/.*${target} (\([0-9A-Fa-f-]*\)) (.*/\1/p" | head -1
}


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

install_ios_app() {
  local target="$1" bundle_id="$2"
  if ios_app_installed "$target" "$bundle_id"; then
    return 0
  fi
  log "iOS dev client ${bundle_id} is not installed on ${target}; installing with yarn start:ios (WATCHER_PORT=${WATCHER_PORT})"
  (
    cd "$root"
    env WATCHER_PORT="$WATCHER_PORT" METRO_PORT="${METRO_PORT:-$WATCHER_PORT}" IOS_SIMULATOR="$target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios
  )
}

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

boot_simulator_if_needed() {
  local target="$1" state=""
  [ "$target" != "booted" ] || return 0
  local line=""
  line="$(xcrun simctl list devices available | grep -F "${target} (" | head -1 || true)"
  case "$line" in *"(Booted)"*) state="Booted" ;; *"(Shutdown)"*) state="Shutdown" ;; esac
  if [ "$state" = "Booted" ]; then return 0; fi
  log "Booting iOS simulator ${target}${state:+ (${state})}"
  xcrun simctl boot "$target" 2>/dev/null || true
  xcrun simctl bootstatus "$target" -b >/dev/null
}

launch_ios() {
  ensure_mobile_harness_patched
  start_metro
  prewarm_bundle ios
  local target url encoded bundle_id
  target="$(sim_target)"
  boot_simulator_if_needed "$target"
  show_simulator "$target"
  bundle_id="${IOS_BUNDLE_ID:-io.metamask.MetaMask}"
  install_ios_app "$target" "$bundle_id"
  xcrun simctl spawn "$target" defaults write "$bundle_id" EXDevMenuIsOnboardingFinished -bool YES 2>/dev/null || true
  encoded="$(python3 - <<PY
import urllib.parse
print(urllib.parse.quote('http://localhost:${WATCHER_PORT}?disableOnboarding=1', safe=''))
PY
)"
  url="expo-metamask://expo-development-client/?url=${encoded}"
  log "Launching iOS dev client on ${target}"
  xcrun simctl openurl "$target" "$url"
  log "Next: recipe logs   # tail Metro"
  log "Next: recipe status # app route/account once loaded"
}

launch_android() {
  ensure_mobile_harness_patched
  start_metro
  prewarm_bundle android
  local serial_args=() package_id encoded url
  package_id="${ANDROID_PACKAGE_ID:-io.metamask}"
  if [ -n "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-}}}" ]; then
    serial_args=(-s "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE}}}")
  fi
  adb "${serial_args[@]}" reverse "tcp:${WATCHER_PORT}" "tcp:${WATCHER_PORT}" >/dev/null 2>&1 || true
  encoded="$(python3 - <<PY
import urllib.parse
print(urllib.parse.quote('http://localhost:${WATCHER_PORT}?disableOnboarding=1', safe=''))
PY
)"
  url="expo-metamask://expo-development-client/?url=${encoded}"
  log "Launching Android dev client"
  adb "${serial_args[@]}" shell am start -a android.intent.action.VIEW -d "$url" >/dev/null
  log "Next: recipe logs   # tail Metro"
  log "Next: recipe status # app route/account once loaded"
}

bridge_cmd() {
  init_context
  (cd "$root" && APP_ROOT="$root" node "$bridge" "$@")
}

fixture_password() {
  init_context
  node - "$root" <<'NODE'
const fs = require('fs');
const path = require('path');
const root = process.argv[2];
const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
for (const rel of [`${runtimeDir}/wallet-fixture.json`]) {
  try {
    const data = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
    if (data.password) { process.stdout.write(String(data.password)); process.exit(0); }
  } catch {}
}
process.exit(1);
NODE
}

setup_wallet() {
  init_context
  local fixture="${1:-$(recipe_runtime_dir)/wallet-fixture.json}"
  (cd "$root" && APP_ROOT="$root" "$setup_wallet_script" --fixture "$fixture")
}

screenshot() {
  init_context
  local out="${1:-temp/recipe-artifacts/screenshot-$(date -u +%Y%m%dT%H%M%SZ).png}"
  mkdir -p "$root/$(dirname "$out")"
  if [ -n "${ANDROID_DEVICE:-${ADB_SERIAL:-${ANDROID_SERIAL:-}}}" ]; then
    local serial_args=()
    [ -n "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-}}}" ] && serial_args=(-s "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE}}}")
    adb "${serial_args[@]}" exec-out screencap -p > "$root/$out"
  else
    xcrun simctl io "$(sim_target)" screenshot "$root/$out" >/dev/null
  fi
  log "screenshot: $root/$out"
}

tail_logs() {
  init_context
  local log_file="$root/$(recipe_runtime_dir)/metro.log"
  [ -f "$log_file" ] || { echo "mm-recipe: Metro log missing: $(recipe_runtime_dir)/metro.log. Start with: recipe ios|android" >&2; return 1; }
  log "tailing $(recipe_runtime_dir)/metro.log (Ctrl-C to stop)"
  tail -f "$log_file"
}

refresh_mobile() {
  if [ -n "${ANDROID_DEVICE:-${ADB_SERIAL:-${ANDROID_SERIAL:-}}}" ]; then
    launch_android
  else
    launch_ios
  fi
}

stop_metro() {
  init_context
  local pids=""
  local metro_pid="$root/$(recipe_runtime_dir)/metro.pid"
  local marker=""
  [ -f "$metro_pid" ] && marker="$(cat "$metro_pid" 2>/dev/null || true)"
  case "$marker" in
    tmux-session:*)
      local target_session="${marker#tmux-session:}"
      log "Stopping Metro tmux session ${target_session}"
      tmux kill-session -t "=${target_session}" >/dev/null 2>&1 || true
      ;;
    tmux:*)
      local target="${marker#tmux:}"
      log "Stopping Metro tmux window ${target}"
      tmux kill-window -t "$target" >/dev/null 2>&1 || true
      ;;
    ''|*[!0-9]*)
      ;;
    *)
      pids="$marker"
      ;;
  esac
  pids="$pids $(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
  if [ -n "$(printf '%s' "$pids" | tr -d ' ')" ]; then
    log "Stopping Metro on port ${WATCHER_PORT}: $pids"
    kill $pids 2>/dev/null || true
    for _ in $(seq 1 20); do
      pids="$(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
      [ -z "$pids" ] && break
      sleep 0.25
    done
    if [ -n "$(printf '%s' "$pids" | tr -d ' ')" ]; then
      log "Metro listener still held on port ${WATCHER_PORT}; forcing: $pids"
      kill -KILL $pids 2>/dev/null || true
    fi
  else
    log "No Metro listener on port ${WATCHER_PORT:-8081}"
  fi
  rm -f "$metro_pid"
}

wait_for_bridge() {
  init_context
  local log_file="$root/$(recipe_runtime_dir)/bridge-status.log"
  local metro_log="$root/$(recipe_runtime_dir)/metro.log"
  mkdir -p "$(dirname "$log_file")"
  local attempt max_polls
  max_polls="${MOBILE_BRIDGE_READY_POLLS:-90}"
  for attempt in $(seq 1 "$max_polls"); do
    if bridge_cmd status > "$log_file" 2>&1 && node - "$log_file" <<'NODE'
const fs = require('fs');
const value = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const targets = Array.isArray(value) ? value : [value];
if (!targets.some((target) => target && typeof target === 'object' && target.route)) process.exit(1);
NODE
    then
      log "Mobile bridge ready"
      return 0
    fi
    if [ "$attempt" = "1" ] || [ $((attempt % 5)) -eq 0 ]; then
      log "Waiting for Mobile bridge (${attempt}/${max_polls}): $(bridge_wait_reason "$log_file" "$metro_log")"
    fi
    sleep 2
  done
  cat "$log_file" >&2 || true
  echo "mm-recipe: Mobile bridge did not become ready on port ${WATCHER_PORT:-8081}" >&2
  return 1
}

bridge_wait_reason() {
  local status_file="$1"
  local metro_log="$2"
  if ! metro_ready; then
    printf 'Metro is not reachable on port %s' "${WATCHER_PORT:-8081}"
    return 0
  fi
  local targets
  targets="$(curl -sf --max-time 2 "http://localhost:${WATCHER_PORT:-8081}/json/list" 2>/dev/null || true)"
  if [ "$targets" = "[]" ] || [ -z "$targets" ]; then
    printf 'Metro is running; no React Native debug target yet'
    bridge_bundle_progress "$metro_log"
    return 0
  fi
  if [ -s "$status_file" ] && grep -qxF '[]' "$status_file"; then
    printf 'React Native target is visible; in-app bridge is not ready yet'
    bridge_bundle_progress "$metro_log"
    return 0
  fi
  printf 'bridge probe failed'
  bridge_bundle_progress "$metro_log"
}

bridge_bundle_progress() {
  local metro_log="$1"
  [ -f "$metro_log" ] || return 0
  local progress
  progress="$(grep -E '^[[:space:]]*(iOS|Android).*index\\.(js|tsx?).*%|Bundl(ed|ing)|Finished' "$metro_log" | tail -1 | tr -d '\r' || true)"
  [ -n "$progress" ] && printf ' (%s)' "$progress"
}

runtime_status() {
  local target="" port="" runtime_dir_arg=""
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --target) target="$2"; shift 2 ;;
      --port|--cdp-port) port="$2"; shift 2 ;;
      --runtime-dir) runtime_dir_arg="$2"; shift 2 ;;
      --json) shift ;;
      -h|--help)
        echo "Usage: mm-recipe runtime-status [--target <repo>] [--port <port>] [--runtime-dir <dir>] [--json]"
        return 0
        ;;
      *) echo "mm-recipe runtime-status: unknown arg: $1" >&2; return 2 ;;
    esac
  done
  [ -n "$target" ] && cd "$target"
  [ -n "$runtime_dir_arg" ] && export RECIPE_RUNTIME_DIR="$runtime_dir_arg"
  [ -n "$port" ] && export WATCHER_PORT="$port" CDP_PORT="${CDP_PORT:-$port}"
  init_context
  local runtime_dir="$(recipe_runtime_dir)"
  local runtime_abs="$root/$runtime_dir"
  local status_file="$runtime_abs/runtime-status.json"
  local metro_log="$runtime_abs/metro.log"
  mkdir -p "$runtime_abs"
  node - "$root" "$runtime_dir" "${WATCHER_PORT:-8081}" "$metro_log" "$bridge" "$status_file" <<'NODE'
const fs = require('fs');
const http = require('http');
const cp = require('child_process');
const [target, runtimeDir, port, metroLog, bridge, statusFile] = process.argv.slice(2);

function get(path) {
  return new Promise((resolve) => {
    const req = http.get(`http://localhost:${port}${path}`, { timeout: 1500 }, (res) => {
      let body = '';
      res.on('data', (chunk) => { body += chunk; });
      res.on('end', () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, body }));
    });
    req.on('timeout', () => { req.destroy(); resolve({ ok: false, body: '' }); });
    req.on('error', () => resolve({ ok: false, body: '' }));
  });
}

function bundleStatus() {
  if (!fs.existsSync(metroLog)) return { phase: 'unknown', source: null };
  const lines = fs.readFileSync(metroLog, 'utf8').split(/\r?\n/u).filter(Boolean);
  const latest = [...lines].reverse().find((line) =>
    /^\s*(iOS|Android).*index\.(js|tsx?).*%/u.test(line) ||
    /Bundl(ed|ing)|Finished/u.test(line)
  );
  if (!latest) return { phase: 'unknown', source: metroLog };
  const progress = /^(?:\s*)(iOS|Android).*?([0-9]+(?:\.[0-9]+)?)%/u.exec(latest);
  if (progress) {
    const percent = Number(progress[2]);
    return {
      phase: percent >= 100 ? 'bundled' : 'bundling',
      platform: progress[1].toLowerCase(),
      percent,
      detail: latest.trim(),
      source: metroLog,
    };
  }
  return { phase: /Finished|Bundled/u.test(latest) ? 'bundled' : 'bundling', detail: latest.trim(), source: metroLog };
}

function bridgeStatus() {
  try {
    const out = cp.execFileSync(process.execPath, [bridge, 'status'], {
      cwd: target,
      env: { ...process.env, APP_ROOT: target, WATCHER_PORT: port },
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'pipe'],
      timeout: 5000,
    });
    const value = JSON.parse(out || 'null');
    const targets = Array.isArray(value) ? value : [value];
    const readyTarget = targets.find((item) => item && typeof item === 'object' && item.route);
    return { ready: Boolean(readyTarget), targetCount: targets.filter(Boolean).length, status: value, error: null };
  } catch (error) {
    return { ready: false, targetCount: 0, status: null, error: error.stderr?.toString?.() || error.message };
  }
}

(async () => {
  const metroRaw = await get('/status');
  const metroReachable = metroRaw.ok && /packager-status:running/u.test(metroRaw.body);
  const listRaw = await get('/json/list');
  let targets = [];
  let debugTargetError = null;
  try {
    targets = JSON.parse(listRaw.body || '[]');
  } catch (error) {
    debugTargetError = error.message;
    targets = [];
  }
  if (!Array.isArray(targets)) {
    debugTargetError = 'Metro /json/list did not return an array';
    targets = [];
  }
  const bridge = bridgeStatus();
  const bundle = bundleStatus();
  const reason = !metroReachable
    ? 'metro-down'
    : targets.length === 0
      ? (bundle.phase === 'bundling' ? 'bundle-in-progress' : 'debug-target-missing')
      : !bridge.ready
        ? 'bridge-not-ready'
        : 'ready';
  const status = {
    schemaVersion: 1,
    adapter: 'mobile',
    target,
    runtimeDir,
    updatedAt: new Date().toISOString(),
    ready: reason === 'ready',
    reason,
    metro: { reachable: metroReachable, port: Number(port) },
    bundle,
    debugTarget: { present: targets.length > 0, count: targets.length, error: debugTargetError },
    bridge: { ready: bridge.ready, targetCount: bridge.targetCount, error: bridge.error, status: bridge.status },
  };
  fs.writeFileSync(statusFile, `${JSON.stringify(status, null, 2)}\n`);
  console.log(JSON.stringify(status, null, 2));
})();
NODE
}

assert_runtime_ready() {
  init_context
  local status_json attempt max_polls
  max_polls="${MOBILE_RUNTIME_READY_POLLS:-60}"
  for attempt in $(seq 1 "$max_polls"); do
    status_json="$(runtime_status --target "$root" --port "${WATCHER_PORT:-}" --runtime-dir "$(recipe_runtime_dir)" --json)"
    if STATUS_JSON="$status_json" python3 -c 'import json, os, sys
status = json.loads(os.environ["STATUS_JSON"])
if status.get("ready"):
    sys.exit(0)
print(
    "mm-recipe: runtime is not ready after prepare "
    f"(reason={status.get('"'"'reason'"'"')}, metro={status.get('"'"'metro'"'"')}, "
    f"debugTarget={status.get('"'"'debugTarget'"'"')}, bridge={status.get('"'"'bridge'"'"')})",
    file=sys.stderr,
)
sys.exit(1)'
    then
      log "Runtime ready after prepare"
      return 0
    fi
    if [ "$attempt" = "1" ] || [ $((attempt % 5)) -eq 0 ]; then
      log "Waiting for runtime ready after prepare (${attempt}/${max_polls})"
    fi
    sleep 2
  done
  STATUS_JSON="$status_json" python3 -c 'import json, os, sys
status = json.loads(os.environ["STATUS_JSON"])
print(
    "mm-recipe: runtime is not ready after prepare "
    f"(reason={status.get('"'"'reason'"'"')}, metro={status.get('"'"'metro'"'"')}, "
    f"debugTarget={status.get('"'"'debugTarget'"'"')}, bridge={status.get('"'"'bridge'"'"')})",
    file=sys.stderr,
)'
  return 1
}

prepare_mobile() {
  local target="" platform="${PLATFORM:-ios}" port="${WATCHER_PORT:-${METRO_PORT:-}}" simulator="" adb_serial="" wallet_setup=false runtime_dir="$(recipe_runtime_dir)"
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --target) target="$2"; shift 2 ;;
      --platform) platform="$2"; shift 2 ;;
      --port) port="$2"; shift 2 ;;
      --simulator) simulator="$2"; shift 2 ;;
      --adb-serial) adb_serial="$2"; shift 2 ;;
      --runtime-dir) runtime_dir="$2"; shift 2 ;;
      --wallet-setup) wallet_setup=true; shift ;;
      -h|--help)
        echo "Usage: mm-recipe prepare [--target <repo>] [--platform ios|android] [--port <port>] [--simulator <sim>] [--adb-serial <serial>] [--runtime-dir <dir>] [--wallet-setup]"
        return 0
        ;;
      *) echo "mm-recipe prepare: unknown arg: $1" >&2; return 2 ;;
    esac
  done
  [ -n "$target" ] && cd "$target"
  export RECIPE_RUNTIME_DIR="$runtime_dir"
  [ -n "$port" ] && export WATCHER_PORT="$port" METRO_PORT="$port" CDP_PORT="${CDP_PORT:-$port}"
  [ -n "$simulator" ] && export IOS_SIMULATOR="$simulator"
  if [ -n "$adb_serial" ] && [ "$adb_serial" != "{{adb_serial}}" ]; then
    export ADB_SERIAL="$adb_serial" ANDROID_SERIAL="$adb_serial"
  fi
  init_context
  install_mobile_harness
  case "$platform" in
    ios) launch_ios ;;
    android) launch_android ;;
    *) echo "mm-recipe prepare: unknown platform: $platform" >&2; return 2 ;;
  esac
  wait_for_bridge
  if $wallet_setup; then
    setup_wallet
  fi
  assert_runtime_ready
}

run_recipe() {
  ensure_mobile_harness_patched
  local recipe="$1"; shift
  [ -f "$recipe" ] || { echo "mm-recipe: recipe not found: $recipe" >&2; exit 2; }
  local out="$root/temp/recipe-artifacts/$(basename "$recipe" .json)-$(date -u +%Y%m%dT%H%M%SZ)"
  log "root=$root port=${WATCHER_PORT:-?} sim=${IOS_SIMULATOR:-${ANDROID_DEVICE:-${ADB_SERIAL:-?}}} out=$out"
  exec "$runner" run "$recipe" --adapter mobile --project-root "$root" --watcher-port "$WATCHER_PORT" --artifacts-dir "$out" "$@"
}

interactive() {
  init_context
  echo "mm-recipe (${root}, port=${WATCHER_PORT:-?}, sim=${IOS_SIMULATOR:-${ANDROID_DEVICE:-${ADB_SERIAL:-?}}})"
  PS3='choose> '
  select choice in refresh ios android logs status runtime-status setup-wallet unlock accounts screenshot doctor actions stop quit; do
    case "$choice" in
      refresh) refresh_mobile ;;
      ios) launch_ios ;;
      android) launch_android ;;
      logs) tail_logs ;;
      status) bridge_cmd status | pipe_pretty ;;
      runtime-status) runtime_status | pipe_pretty ;;
      setup-wallet) setup_wallet ;;
      unlock) bridge_cmd unlock "$(fixture_password)" ;;
      accounts) bridge_cmd list-accounts | pipe_pretty ;;
      screenshot) screenshot ;;
      doctor) run_json_pretty "$runner" doctor --adapter mobile --target "$root" ;;
      actions) run_json_pretty "$runner" actions --adapter mobile ;;
      stop) stop_metro ;;
      quit|'') break ;;
    esac
    echo
  done
}

cmd="${1:-interactive}"
[ "$cmd" = "interactive" ] || shift || true
case "$cmd" in
  -h|--help) usage; exit 0 ;;
  interactive) interactive ;;
  prepare) prepare_mobile "$@" ;;
  runtime-status) runtime_status "$@" ;;
  ios|start) launch_ios ;;
  android) launch_android ;;
  setup:ios) launch_ios; setup_wallet ;;
  setup:android) launch_android; setup_wallet ;;
  refresh|reload|relaunch) refresh_mobile ;;
  logs|tail) tail_logs ;;
  status) bridge_cmd status | pipe_pretty ;;
  route|get-route) bridge_cmd get-route | pipe_pretty ;;
  navigate) bridge_cmd navigate "$@" ;;
  back|go-back) bridge_cmd go-back ;;
  unlock) bridge_cmd unlock "${1:-$(fixture_password)}" ;;
  setup-wallet|wallet-setup) setup_wallet "${1:-$(recipe_runtime_dir)/wallet-fixture.json}" ;;
  accounts|list-accounts) bridge_cmd list-accounts | pipe_pretty ;;
  select-account|switch-account) bridge_cmd switch-account "${1:?address required}" ;;
  screenshot) screenshot "${1:-}" ;;
  stop) stop_metro ;;
  actions) init_context; run_json_pretty "$runner" actions --adapter mobile "$@" ;;
  doctor) init_context; run_json_pretty "$runner" doctor --adapter mobile --target "$root" "$@" ;;
  run) recipe="${1:-}"; [ -n "$recipe" ] || { usage; exit 2; }; shift; run_recipe "$recipe" "$@" ;;
  *.json) run_recipe "$cmd" "$@" ;;
  *) echo "mm-recipe: unknown command or missing recipe: $cmd" >&2; usage; exit 2 ;;
esac
