#!/usr/bin/env bash
# start-metro — ensure the Metro bundler is running for this MetaMask Mobile checkout.
#
# Checks if Metro is already running and owned by this checkout with required env vars.
# Clears stale or foreign listeners before starting. Uses the checkout's canonical
# yarn watch script and sets EXPO_NO_TYPESCRIPT_SETUP=1. Cache clearing is
# passed to Expo only for the explicit --clear option. Metro defaults to four
# workers; METRO_MAX_WORKERS remains an explicit override.
#
# Inputs:
#   --target <metamask-mobile dir>  (default $PWD)
#   --port <number>                 (default: WATCHER_PORT env, else METRO_PORT, else 8081)
#   --clear                         reset Metro bundler cache before starting
# Outputs:
#   Progress on stderr; exit 0 Metro ready; 1 failed to start; 2 bad args.
#
# Single device op: ensure Metro is running. Does not open the dev client or install deps.
set -uo pipefail

TARGET="$PWD"
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
CLEAR=false

while [ "$#" -gt 0 ]; do
  case "$1" in
    --target) TARGET="$2"; shift 2 ;;
    --port)   PORT="$2"; shift 2 ;;
    --clear)  CLEAR=true; shift ;;
    -h|--help)
      printf 'Usage: start-metro.sh [--target <dir>] [--port <port>] [--clear]\n'
      printf '  --target  MetaMask Mobile checkout directory\n'
      printf '  --port    Metro port (default: WATCHER_PORT env or 8081)\n'
      printf '  --clear   restart with cleared cache\n'
      exit 0
      ;;
    *) printf 'start-metro: unknown arg: %s\n' "$1" >&2; exit 2 ;;
  esac
done

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

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
. "$SCRIPT_DIR/../shared/harness-path.sh"
if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
  echo "start-metro: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
  exit 1
fi
# Port-scoped Metro helpers (metro_ready / *_listener_* / stop_metro_listener)
# only ever inspect or signal a Metro listening on $PORT.
# shellcheck disable=SC1091
. "$SCRIPT_DIR/lib/metro-listener.sh"
HARNESS_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
if [ "${EXPO_OVERRIDE_METRO_CONFIG:-}" = "$HARNESS_METRO_CONFIG" ]; then
  PRODUCT_METRO_CONFIG="${MM_HARNESS_METRO_BASE_CONFIG:-$TARGET/metro.config.js}"
else
  PRODUCT_METRO_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
fi
export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
export MM_HARNESS_METRO_BASE_CONFIG="$PRODUCT_METRO_CONFIG"
export EXPO_OVERRIDE_METRO_CONFIG="$HARNESS_METRO_CONFIG"
MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID METAMASK_BUILD_TYPE METAMASK_ENVIRONMENT} MM_HARNESS_METRO_PROJECT_ROOT MM_HARNESS_METRO_BASE_CONFIG EXPO_OVERRIDE_METRO_CONFIG"

# RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
# sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/metro.log"
PID_FILE="$LOG_DIR/metro.pid"
METRO_BUILD_ENV_FILE="$LOG_DIR/metro-build-env.sh"
METRO_LOG_GENERATION="$SCRIPT_DIR/metro-log-generation.cjs"
METRO_LOG_COALESCER="$SCRIPT_DIR/coalesce-metro-log.cjs"

# --- helpers ------------------------------------------------------------------

write_metro_build_env() {
  local temporary name staged_name product_values build_type environment sentry_dsn clear_value=0
  for name in $MOBILE_METRO_REQUIRED_ENV; do
    if ! printf '%s' "$name" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]*$'; then
      printf 'start-metro: invalid required environment variable name: %s\n' "$name" >&2
      return 1
    fi
  done
  temporary="$(mktemp "$LOG_DIR/.metro-build-env.XXXXXX")" || return 1
  chmod 600 "$temporary" || { rm -f "$temporary"; return 1; }
  product_values="$(
    cd "$TARGET" || exit 1
    set -a
    # shellcheck disable=SC1091
    [ ! -f .js.env ] || eval "$(tr -d '\r' < .js.env)"
    set +a
    METAMASK_BUILD_TYPE="${METAMASK_BUILD_TYPE:-main}"
    METAMASK_ENVIRONMENT="${METAMASK_ENVIRONMENT:-dev}"
    printf 'build_type=%q\n' "$METAMASK_BUILD_TYPE"
    printf 'environment=%q\n' "$METAMASK_ENVIRONMENT"
    printf 'sentry_dsn=%q\n' "${MM_SENTRY_DSN:-${MM_SENTRY_DSN_DEV:-}}"
    for name in $MOBILE_METRO_REQUIRED_ENV; do
      printf 'required_%s=%q\n' "$name" "${!name-}"
    done
  )" || { rm -f "$temporary"; return 1; }
  eval "$product_values"
  required_MM_HARNESS_METRO_PROJECT_ROOT="$MM_HARNESS_METRO_PROJECT_ROOT"
  required_MM_HARNESS_METRO_BASE_CONFIG="$MM_HARNESS_METRO_BASE_CONFIG"
  required_EXPO_OVERRIDE_METRO_CONFIG="$EXPO_OVERRIDE_METRO_CONFIG"
  [ "$CLEAR" = true ] && clear_value=1
  {
    printf 'export MM_HARNESS_METRO_WATCHER_PORT=%q\n' "$PORT"
    printf 'export MM_HARNESS_METRO_BUILD_TYPE=%q\n' "$build_type"
    printf 'export MM_HARNESS_METRO_ENVIRONMENT=%q\n' "$environment"
    printf 'export MM_HARNESS_METRO_SENTRY_DSN=%q\n' "$sentry_dsn"
    printf 'export MM_HARNESS_METRO_CLEAR=%q\n' "$clear_value"
    cat <<'BUILD_ENV'
mm_harness_restore_metro_env() {
  export WATCHER_PORT="$MM_HARNESS_METRO_WATCHER_PORT"
  export METRO_PORT="$MM_HARNESS_METRO_WATCHER_PORT"
  export METAMASK_BUILD_TYPE="$MM_HARNESS_METRO_BUILD_TYPE"
  export METAMASK_ENVIRONMENT="$MM_HARNESS_METRO_ENVIRONMENT"
  export MM_SENTRY_DSN="$MM_HARNESS_METRO_SENTRY_DSN"
BUILD_ENV
    for name in $MOBILE_METRO_REQUIRED_ENV; do
      staged_name="required_$name"
      printf '  export %s=%q\n' "$name" "${!staged_name}"
    done
    for name in \
      SEGMENT_PROXY_URL \
      SEGMENT_WRITE_KEY \
      SEGMENT_FLUSH_INTERVAL \
      SEGMENT_FLUSH_EVENT_LIMIT
    do
      if declare -p "$name" >/dev/null 2>&1; then
        printf '  export %s=%q\n' "$name" "${!name}"
      fi
    done
    cat <<'BUILD_ENV'
}
eval() {
  builtin eval "$@"
  local status=$?
  mm_harness_restore_metro_env
  return "$status"
}
source() {
  builtin source "$@"
  local status=$?
  mm_harness_restore_metro_env
  return "$status"
}
yarn() {
  if [ "$MM_HARNESS_METRO_CLEAR" = 1 ] && [ "${1:-}" = expo ] && [ "${2:-}" = start ]; then
    command yarn "$@" --clear
  else
    command yarn "$@"
  fi
}
mm_harness_restore_metro_env
BUILD_ENV
  } > "$temporary" || { rm -f "$temporary"; return 1; }
  mv -f "$temporary" "$METRO_BUILD_ENV_FILE"
}

# Log-tail viewer window (session-safe): sourced so its run-owned-session rule is
# unit-testable and can never leak a window into an unrelated user session.
# shellcheck disable=SC1091
. "$SCRIPT_DIR/lib/tmux-viewer.sh"

write_metro_runner() {
  local runner="$1"
  local watcher_script="watch"
  local worker_env_line='unset METRO_MAX_WORKERS'
  if [ -n "$METRO_WORKERS" ]; then
    worker_env_line="export METRO_MAX_WORKERS=$(printf '%q' "$METRO_WORKERS")"
  fi
  cat > "$runner" <<EOF
#!/usr/bin/env bash
set -uo pipefail
cd "$(printf '%q' "$TARGET")"
export EXPO_NO_TYPESCRIPT_SETUP=1
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
$worker_env_line
BASH_ENV=$(printf '%q' "$METRO_BUILD_ENV_FILE") command yarn $(printf '%q' "$watcher_script") 2>&1 | node $(printf '%q' "$METRO_LOG_COALESCER") | tee -a "$(printf '%q' "$LOG_FILE")"
EOF
  chmod +x "$runner"
}

start_metro_tmux() {
  local runner="$1"
  command -v tmux >/dev/null 2>&1 || return 1
  local session window
  session="$(resolve_run_tmux_session "$LOG_DIR")"
  { [ -n "$session" ] && tmux has-session -t "=$session" 2>/dev/null; } || return 1
  window="metro-${PORT}"
  tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
  tmux new-window -d -t "$session" -n "$window" "exec $(printf '%q' "$runner")" || return 1
  printf '%s:%s\n' "$session" "$window" > "$LOG_DIR/metro.tmux"
  printf 'Metro server → tmux %s:%s\n' "$session" "$window" >&2
  return 0
}

# --- main ---------------------------------------------------------------------

# Detect stale metro.pid: if the pid file names a dead process, clean it up.
# A stale file survives when Metro is killed externally (session close, SIGKILL)
# and the normal stop path never ran. Cleaning it here keeps metro_ready and the
# log from attributing a new start to the old process id.
if [ -f "$PID_FILE" ]; then
  stale_pid="$(cat "$PID_FILE" 2>/dev/null || true)"
  if [ -n "$stale_pid" ] && ! kill -0 "$stale_pid" 2>/dev/null; then
    printf 'start-metro: stale metro.pid (pid %s no longer alive); cleaning up\n' "$stale_pid" >&2
    rm -f "$PID_FILE"
  fi
fi

# Decide whether to (re)start or skip.
if metro_ready; then
  if [ "$CLEAR" = true ]; then
    printf 'Restarting Metro on port %s with cleared cache\n' "$PORT" >&2
    stop_metro_listener || exit 1
    rm -f "$PID_FILE"
  elif metro_listener_valid; then
    printf 'Metro already running and valid on port %s\n' "$PORT" >&2
    exit 0
  else
    stop_metro_listener || exit 1
    rm -f "$PID_FILE"
  fi
fi

stop_metro_listener || exit 1

METRO_WORKERS="${METRO_MAX_WORKERS:-4}"
if ! printf '%s' "$METRO_WORKERS" | grep -Eq '^[1-9][0-9]*$'; then
  printf 'start-metro: METRO_MAX_WORKERS must be a positive integer, got %s\n' "$METRO_WORKERS" >&2
  exit 2
fi
READY_TIMEOUT="${MOBILE_METRO_READY_TIMEOUT_SECONDS:-180}"
if ! printf '%s' "$READY_TIMEOUT" | grep -Eq '^[1-9][0-9]*$'; then
  printf 'start-metro: MOBILE_METRO_READY_TIMEOUT_SECONDS must be a positive integer, got %s\n' "$READY_TIMEOUT" >&2
  exit 2
fi
CLEAR_LABEL=""
[ "$CLEAR" = true ] && CLEAR_LABEL=", clear"

printf 'Starting Metro on port %s (workers=%s%s)\n' \
  "$PORT" "$METRO_WORKERS" "$CLEAR_LABEL" >&2
printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2

[ -f "$METRO_LOG_GENERATION" ] && [ -f "$METRO_LOG_COALESCER" ] || {
  printf 'start-metro: Metro log generation helpers are missing; reinstall the runner\n' >&2
  exit 1
}
ROTATION_REASON="metro-start"
[ "$CLEAR" = true ] && ROTATION_REASON="clear-cache-restart"
node "$METRO_LOG_GENERATION" "$LOG_DIR" "$PORT" "$ROTATION_REASON" >/dev/null || exit 1

write_metro_build_env || {
  printf 'start-metro: could not stage the scoped Metro environment\n' >&2
  exit 1
}

STARTED_METRO_PID=""
STARTED_METRO_TMUX=""
METRO_RUNNER="$LOG_DIR/run-metro-${PORT}.sh"
write_metro_runner "$METRO_RUNNER"
if start_metro_tmux "$METRO_RUNNER"; then
  STARTED_METRO_TMUX="$(cat "$LOG_DIR/metro.tmux" 2>/dev/null || true)"
else
  # Metro runs detached, writing to the log. The tmux window is a read-only tail.
  (
    cd "$TARGET"
    export EXPO_NO_TYPESCRIPT_SETUP=1
    export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
    if [ -n "$METRO_WORKERS" ]; then
      export METRO_MAX_WORKERS="${METRO_WORKERS}"
    else
      unset METRO_MAX_WORKERS
    fi
    launcher_args=(
      --target "$TARGET"
      --port "$PORT"
      --log "$LOG_FILE"
      --pid-file "$PID_FILE"
      --build-env "$METRO_BUILD_ENV_FILE"
      --runner "$METRO_RUNNER"
    )
    [ -z "$METRO_WORKERS" ] || launcher_args+=(--workers "$METRO_WORKERS")
    [ "$CLEAR" = true ] && launcher_args+=(--clear)
    node "$SCRIPT_DIR/launch-metro.cjs" "${launcher_args[@]}"
  ) || exit 1
  STARTED_METRO_PID="$(cat "$PID_FILE" 2>/dev/null || true)"

  start_viewer_window "$LOG_FILE"
fi

# Attach a log-tui watcher for compact event display (best-effort; non-blocking).
LOG_TUI="$(dirname "$0")/../shared/log-tui.mjs"
TAIL_PID=""
if [ -f "$LOG_TUI" ] && command -v node >/dev/null 2>&1 \
   && [ "${METAMASK_RECIPE_AUTO_HUD:-1}" != "0" ]; then
  node "$LOG_TUI" watch \
    --log "$LOG_FILE" \
    --events "${RECIPE_LOG_EVENTS:-12}" \
    --mode compact \
    --label "Metro bundler" \
    --success-pattern 'Dev server ready' &
  TAIL_PID=$!
fi

# Cold main bundles can spend more than 90 seconds initializing Metro before the
# status listener opens. Keep the wait bounded, but do not trigger a duplicate
# recovery process while the checkout-owned launch is still making progress.
READY=false
for _i in $(seq 1 "$READY_TIMEOUT"); do
  if metro_ready; then
    READY=true
    break
  fi
  sleep 1
done

[ -n "$TAIL_PID" ] && { kill "$TAIL_PID" 2>/dev/null; wait "$TAIL_PID" 2>/dev/null || true; }

if [ "$READY" = true ]; then
  ready_pids="$(metro_listener_pids)"
  first_ready_pid="${ready_pids%% *}"
  [ -z "$first_ready_pid" ] || printf '%s\n' "$first_ready_pid" > "$PID_FILE"
  printf 'Metro ready on port %s\n' "$PORT" >&2
  printf '  Next: mm-harness logs  (tail Metro)\n' >&2
  exit 0
fi

cleanup_timed_out_metro() {
  local pid pgid i group_alive=false owned_listener_alive=false listener_pid command
  pid="$STARTED_METRO_PID"
  # launch-metro creates a detached group whose PGID equals its initial child
  # PID. The leader can exit before Expo, so probe the group rather than the PID.
  pgid="$pid"
  if [ -n "$pgid" ] && kill -0 -- "-$pgid" 2>/dev/null; then
    printf 'start-metro: stopping timed-out Metro process group %s\n' "$pgid" >&2
    kill -TERM -- "-$pgid" 2>/dev/null || true
    for i in $(seq 1 20); do
      kill -0 -- "-$pgid" 2>/dev/null || break
      sleep 0.25
    done
    if kill -0 -- "-$pgid" 2>/dev/null; then
      kill -KILL -- "-$pgid" 2>/dev/null || true
      for i in $(seq 1 20); do
        kill -0 -- "-$pgid" 2>/dev/null || break
        sleep 0.25
      done
    fi
    kill -0 -- "-$pgid" 2>/dev/null && group_alive=true
  fi
  if [ -n "$STARTED_METRO_TMUX" ] && command -v tmux >/dev/null 2>&1; then
    tmux kill-window -t "$STARTED_METRO_TMUX" >/dev/null 2>&1 || true
  fi
  for listener_pid in $(metro_listener_pids); do
    command="$(ps eww -p "$listener_pid" -o command= 2>/dev/null || true)"
    case "$command" in *"$TARGET"*) owned_listener_alive=true ;; esac
  done
  if [ "$group_alive" = true ] || [ "$owned_listener_alive" = true ]; then
    printf 'start-metro: timed-out checkout-owned process is still alive; retaining launch metadata for recovery\n' >&2
    return 1
  fi
  rm -f "$PID_FILE" "$LOG_DIR/metro-launch.json" "$LOG_DIR/metro-build-env.sh" "$LOG_DIR/metro.tmux"
  return 0
}

# Failure: dump log events for diagnosis.
if [ -f "$LOG_TUI" ] && command -v node >/dev/null 2>&1; then
  node "$LOG_TUI" events --log "$LOG_FILE" --events 20 >&2 || true
else
  tail -40 "$LOG_FILE" >&2 || true
fi
cleanup_timed_out_metro || true
printf 'start-metro: Metro did not become ready on port %s within %ss\n' "$PORT" "$READY_TIMEOUT" >&2
printf '  Next: check %s for errors, or run mm-harness logs\n' "$LOG_FILE" >&2
exit 1
