#!/usr/bin/env bash
# mm-harness setup-base — bootstrap the shared default MetaMask checkout layout.
#
#   npx -p @deeeed/metamask-harness mm-harness setup-base
#
# Clones the MetaMask product repos in N copies each into one standard folder
# layout and runs each repo's own dependency install:
#
#   <base>/metamask-extension-1..N
#   <base>/metamask-mobile-1..N
#   <base>/core-1..N
#
# Scope fence: clone + dependency install, nothing else. No platform toolchains,
# no simulators, no .env files, no builds. Environment readiness is a separate
# step — `mm-harness doctor`.
#
# This file is deliberately a plain, standalone bash script: it runs on a machine
# before anything else is set up, and you should be able to read the whole thing
# top to bottom and decide whether to trust it BEFORE you run it. It sources
# nothing from the package at runtime and pulls in no shell dependencies.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NODE_BIN="${MM_HARNESS_NODE:-node}"
PRIVATE_WRITER="$SCRIPT_DIR/private-atomic-write.cjs"

# Exit codes are stable and match the harness CLI, so another tool can branch on
# them: 0 ok · 1 a repo failed · 2 usage · 3 environment/disk refusal.
EXIT_OK=0
EXIT_RUNTIME=1
EXIT_USAGE=2
EXIT_INFRA=3

DEFAULT_BASE="$HOME/dev/metamask"
REPO_KEYS=(extension mobile core)
HARNESS_PACKAGE="@deeeed/metamask-harness"

COUNT_EXTENSION=2
COUNT_MOBILE=2
COUNT_CORE=2

BASE_DIR=""
BASE_SOURCE=""
SELECTED=()
SELECTION_SOURCE=""
DRY_RUN=0
FORCE=0
JSON_OUT=0
SKIP_HARNESS_UPDATE=0
SAW_ONLY=0
SAW_COUNTS=0
SHOW_CONFIG=0
RESET_CONFIG=0
RESULTS=()
FAILURES=0
EXIT_CODE="$EXIT_OK"
ERROR_CODE=""
ERROR_USER_ACTION=""

for arg in "$@"; do
  [ "$arg" = "--json" ] && JSON_OUT=1
done

# --- config location ----------------------------------------------------------
# Same shape the harness already uses for persisted state: an explicit env
# override first, otherwise an XDG base directory plus an mm-harness/ folder.
config_file() {
  if [ -n "${MM_HARNESS_SETUP_CONFIG:-}" ]; then
    printf '%s' "$MM_HARNESS_SETUP_CONFIG"
    return
  fi
  printf '%s/mm-harness/setup-base.json' "${XDG_CONFIG_HOME:-$HOME/.config}"
}

last_run_file() {
  printf '%s/last-run.json' "$(dirname "$(config_file)")"
}

CONFIG_FILE="$(config_file)"

# --- UI -----------------------------------------------------------------------
# With --json, stdout carries the machine summary and nothing else, so every
# human line goes to stderr.
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then
  C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'
  C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'
  C_BLUE=$'\033[34m'; C_CYAN=$'\033[36m'
else
  C_RESET=; C_BOLD=; C_RED=; C_GREEN=; C_YELLOW=; C_BLUE=; C_CYAN=
fi

say() { printf '%s\n' "$*" >&2; }
ui_ok()      { printf '%s[ OK ]%s %s\n'  "$C_GREEN"       "$C_RESET" "$*" >&2; }
ui_warn()    { printf '%s[WARN]%s %s\n'  "$C_YELLOW"      "$C_RESET" "$*" >&2; }
ui_fail()    { printf '%s[FAIL]%s %s\n'  "$C_RED"         "$C_RESET" "$*" >&2; }
ui_info()    { printf '%s[INFO]%s %s\n'  "$C_CYAN"        "$C_RESET" "$*" >&2; }
ui_section() { printf '\n%s== %s ==%s\n' "$C_BOLD$C_BLUE" "$*"       "$C_RESET" >&2; }
ui_next()    { printf '       %sNext:%s %s\n' "$C_BOLD" "$C_RESET" "$*" >&2; }

write_summary() {
  local target="$1" code="$2" message="$3" rows="" generator idx
  for ((idx = 0; idx < ${#RESULTS[@]}; idx++)); do
    rows="$rows${RESULTS[$idx]}"$'\n'
  done
  generator='
    const repos = (process.env.MM_ROWS || "").split("\n").filter(Boolean).map((line) => {
      const [directory, clone, install] = line.split("\t");
      return { directory, clone, install };
    });
    process.stdout.write(JSON.stringify({
      schemaVersion: 1,
      command: "setup-base",
      baseDir: process.env.MM_BASE || null,
      configFile: process.env.MM_CONFIG,
      exitCode: Number(process.env.MM_CODE),
      status: Number(process.env.MM_CODE) === 0 ? "ok" : "fail",
      message: process.env.MM_MESSAGE || undefined,
      error: process.env.MM_ERROR_CODE ? {
        code: process.env.MM_ERROR_CODE,
        message: process.env.MM_MESSAGE,
        userAction: process.env.MM_ERROR_USER_ACTION,
      } : undefined,
      repos,
      finishedAt: new Date().toISOString(),
    }, null, 2) + "\n");
  '
  if [ "$target" = "-" ]; then
    MM_ROWS="$rows" MM_BASE="$BASE_DIR" MM_CONFIG="$CONFIG_FILE" \
    MM_CODE="$code" MM_MESSAGE="$message" MM_ERROR_CODE="$ERROR_CODE" \
      MM_ERROR_USER_ACTION="$ERROR_USER_ACTION" "$NODE_BIN" -e "$generator"
    return
  fi
  MM_ROWS="$rows" MM_BASE="$BASE_DIR" MM_CONFIG="$CONFIG_FILE" \
    MM_CODE="$code" MM_MESSAGE="$message" MM_ERROR_CODE="$ERROR_CODE" \
      MM_ERROR_USER_ACTION="$ERROR_USER_ACTION" "$NODE_BIN" -e "$generator" \
      | "$NODE_BIN" "$PRIVATE_WRITER" "$target"
}

finish() {
  local code="$1" message="${2:-}"
  EXIT_CODE="$code"
  local last_run
  last_run="$(last_run_file)"
  write_summary "$last_run" "$code" "$message" 2>/dev/null \
    || ui_warn "could not write the run summary to $last_run"
  if [ "$JSON_OUT" = 1 ]; then
    write_summary - "$code" "$message"
  fi
  exit "$code"
}

# die <exit-code> <message> <next-command...> — no failure is allowed to leave
# the caller without a concrete command to run next.
die() {
  local code="$1" msg="$2"; shift 2
  if [ "$code" = "$EXIT_USAGE" ]; then
    case "$msg" in
      "unknown option "*) ERROR_CODE="CLI_UNKNOWN_OPTION" ;;
      *" needs a "*) ERROR_CODE="CLI_MISSING_OPTION_VALUE" ;;
      *) ERROR_CODE="CLI_INVALID_OPTION_VALUE" ;;
    esac
    ERROR_USER_ACTION="${1:-mm-harness setup-base --help}"
  fi
  if [ "$JSON_OUT" != 1 ] || [ "$code" != "$EXIT_USAGE" ]; then
    ui_fail "$msg"
    local next
    for next in "$@"; do ui_next "$next"; done
  fi
  finish "$code" "$msg"
}

# --- numbered steps -----------------------------------------------------------
STEP_N=0
STEP_TOTAL=0
step() {
  STEP_N=$((STEP_N + 1))
  printf '\n%s[%s/%s]%s %s%s%s\n' \
    "$C_CYAN" "$STEP_N" "$STEP_TOTAL" "$C_RESET" "$C_BOLD" "$1" "$C_RESET" >&2
}

usage() {
  if [ "$JSON_OUT" = 1 ]; then
    cat >&2 <<USAGE
mm-harness setup-base — bootstrap the shared default MetaMask checkout layout.

Usage:
  mm-harness setup-base [--dir <base>] [--counts <spec>] [--only <repos>]
                [--dry-run] [--force] [--json] [--show-config]
                [--reset-config] [--skip-harness-update] [--help]
USAGE
    return
  fi
  cat <<USAGE
mm-harness setup-base — bootstrap the shared default MetaMask checkout layout.

Clones the MetaMask product repos in several copies into one standard folder
layout and runs each repo's own dependency install. Nothing else.

Usage:
  mm-harness setup-base [--dir <base>] [--counts <spec>] [--only <repos>]
                [--dry-run] [--force] [--json] [--show-config]
                [--reset-config] [--skip-harness-update] [--help]

Flags:
  --dir <base>            Where the clones go.
  --counts <spec>         Copies per repo, e.g. extension=2,mobile=2,core=2 (min 1).
  --only <repos>          Comma-separated subset of: extension,mobile,core.
  --dry-run               Print the plan and disk estimate; change nothing.
  --force                 Proceed despite an insufficient-disk estimate.
  --json                  Emit the run summary as JSON on stdout (human output
                          goes to stderr). For scripted/parent-tool use.
  --show-config           Print the saved preferences and exit.
  --reset-config          Delete the saved preferences and exit.
  --skip-harness-update   Do not check whether $HARNESS_PACKAGE is current.
  -h, --help              Show this help and exit.

Layout produced under <base>:
  metamask-extension-1..N   metamask-mobile-1..N   core-1..N

Base directory precedence (first match wins):
  1. --dir <base>
  2. \$MM_HARNESS_BASE_DIR
  3. saved preferences file
  4. $DEFAULT_BASE

Saved preferences:
  Path:   $(config_file)
          (override with \$MM_HARNESS_SETUP_CONFIG)
  Format: JSON — { "schemaVersion": 1, "baseDir": "<path>",
          "counts": { "extension": N, "mobile": N, "core": N },
          "only": ["extension", ...], "updatedAt": "<iso-8601>" }
  A successful run saves the base dir and counts it used; later runs adopt them
  as defaults. Inspect with --show-config, clear with --reset-config.

  The last run's summary is written beside it as:
  $(last_run_file)

Selecting what to install:
  With no --only/--counts and no saved preferences, an interactive prompt asks
  which projects and how many copies. Passing --only or --counts skips the
  prompt entirely, which is how non-interactive and parent-tool runs should
  invoke this.

Exit codes:
  0 success · 1 a repo failed · 2 usage error · 3 environment or disk refusal

Scope: clone + dependency install only. Toolchains, simulators, .env files and
builds are out of scope — run \`mm-harness doctor\` for environment readiness.
USAGE
}

# --- repo table (case functions, not associative arrays: macOS ships bash 3.2) -
repo_slug() {
  case "$1" in
    extension) printf 'metamask-extension' ;;
    mobile)    printf 'metamask-mobile' ;;
    core)      printf 'core' ;;
  esac
}

# Disk estimate per clone in GB, after clone + dependency install, rounded up
# from a measurement for headroom.
#   extension  measured 5.1G (1.3G .git + 3.1G node_modules)
#   core       measured 1.6G (257M .git + 1.1G node_modules)
#   mobile     estimated — .git alone measures 1.8G and a working checkout
#              measures 6.3G, so 8 is deliberately conservative.
# Dependencies only; a native build adds more and is out of scope here.
repo_gb() {
  case "$1" in
    extension) printf '6' ;;
    mobile)    printf '8' ;;
    core)      printf '2' ;;
  esac
}

count_for() {
  case "$1" in
    extension) printf '%s' "$COUNT_EXTENSION" ;;
    mobile)    printf '%s' "$COUNT_MOBILE" ;;
    core)      printf '%s' "$COUNT_CORE" ;;
  esac
}

set_count() {
  case "$1" in
    extension) COUNT_EXTENSION="$2" ;;
    mobile)    COUNT_MOBILE="$2" ;;
    core)      COUNT_CORE="$2" ;;
  esac
}

is_repo_key() {
  local k
  for k in "${REPO_KEYS[@]}"; do [ "$k" = "$1" ] && return 0; done
  return 1
}

# --- argument parsing ---------------------------------------------------------
# Expand a leading ~/ that reached us quoted, so --dir "~/x" never creates a
# directory literally named "~".
expand_tilde() {
  # The quoted ~/ below is the literal text being matched, not a path the shell
  # should expand — expanding it is exactly this function's job.
  # shellcheck disable=SC2088
  case "$1" in
    "~/"*) printf '%s/%s' "$HOME" "${1#"~/"}" ;;
    "~")   printf '%s' "$HOME" ;;
    *)     printf '%s' "$1" ;;
  esac
}

parse_counts() {
  local spec="$1" pair key value
  local -a pairs=()
  IFS=',' read -r -a pairs <<< "$spec"
  [ "${#pairs[@]}" -gt 0 ] || die "$EXIT_USAGE" "--counts got an empty value" \
    "mm-harness setup-base --counts extension=2,mobile=2,core=2"
  for pair in "${pairs[@]}"; do
    case "$pair" in
      *=*) ;;
      *) die "$EXIT_USAGE" "--counts entry '$pair' is not <repo>=<n>" \
           "mm-harness setup-base --counts extension=2,mobile=2,core=2" ;;
    esac
    key="${pair%%=*}"
    value="${pair#*=}"
    is_repo_key "$key" || die "$EXIT_USAGE" "--counts names unknown repo '$key'" \
      "mm-harness setup-base --counts extension=2,mobile=2,core=2   # valid: ${REPO_KEYS[*]}"
    case "$value" in
      "" | *[!0-9]*) die "$EXIT_USAGE" "--counts value for '$key' is not a whole number: '$value'" \
        "mm-harness setup-base --counts $key=2" ;;
    esac
    [ "$value" -ge 1 ] || die "$EXIT_USAGE" "--counts value for '$key' must be at least 1 (got $value)" \
      "mm-harness setup-base --only $key   # or drop '$key' from --only to skip it"
    set_count "$key" "$value"
  done
}

parse_only() {
  local spec="$1" key
  local -a keys=()
  IFS=',' read -r -a keys <<< "$spec"
  [ "${#keys[@]}" -gt 0 ] || die "$EXIT_USAGE" "--only got an empty value" \
    "mm-harness setup-base --only extension,core"
  for key in "${keys[@]}"; do
    is_repo_key "$key" || die "$EXIT_USAGE" "--only names unknown repo '$key'" \
      "mm-harness setup-base --only extension,core   # valid: ${REPO_KEYS[*]}"
  done
  SELECTED=("${keys[@]}")
}

require_value() {
  local flag="$1" value="${2:-}" example="$3"
  case "$value" in
    "" | -*) die "$EXIT_USAGE" "$flag needs a value" "$example" ;;
  esac
}

FLAG_DIR=""
while [ "$#" -gt 0 ]; do
  case "$1" in
    --dir)
      require_value "--dir" "${2:-}" "mm-harness setup-base --dir ~/dev/metamask"
      FLAG_DIR="$(expand_tilde "$2")"; shift 2 ;;
    --dir=*)
      require_value "--dir" "${1#*=}" "mm-harness setup-base --dir ~/dev/metamask"
      FLAG_DIR="$(expand_tilde "${1#*=}")"; shift ;;
    --counts)
      require_value "--counts" "${2:-}" \
        "mm-harness setup-base --counts extension=2,mobile=2,core=2"
      parse_counts "$2"; SAW_COUNTS=1; shift 2 ;;
    --counts=*)
      require_value "--counts" "${1#*=}" \
        "mm-harness setup-base --counts extension=2,mobile=2,core=2"
      parse_counts "${1#*=}"; SAW_COUNTS=1; shift ;;
    --only)
      require_value "--only" "${2:-}" "mm-harness setup-base --only extension,core"
      parse_only "$2"; SAW_ONLY=1; shift 2 ;;
    --only=*)
      require_value "--only" "${1#*=}" "mm-harness setup-base --only extension,core"
      parse_only "${1#*=}"; SAW_ONLY=1; shift ;;
    --dry-run) DRY_RUN=1; shift ;;
    --force) FORCE=1; shift ;;
    --json) JSON_OUT=1; shift ;;
    --show-config) SHOW_CONFIG=1; shift ;;
    --reset-config) RESET_CONFIG=1; shift ;;
    --skip-harness-update) SKIP_HARNESS_UPDATE=1; shift ;;
    -h | --help) usage; finish "$EXIT_OK" "help" ;;
    *) die "$EXIT_USAGE" "unknown option '$1'" "mm-harness setup-base --help" ;;
  esac
done

# --- saved preferences --------------------------------------------------------
# node owns the JSON so quoting and escaping are correct in both directions. It
# ships with the package that ships this script, so it is always present; a
# missing node is caught by the prerequisite step with a teaching error.
config_get() {
  [ -f "$CONFIG_FILE" ] && [ ! -L "$CONFIG_FILE" ] || return 1
  "$NODE_BIN" -e '
    const fs = require("fs");
    try {
      const c = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
      const key = process.argv[2];
      if (key === "baseDir") { if (typeof c.baseDir === "string") process.stdout.write(c.baseDir); }
      else if (key === "only") { if (Array.isArray(c.only)) process.stdout.write(c.only.join(",")); }
      else if (typeof c.counts?.[key] === "number") process.stdout.write(String(c.counts[key]));
    } catch { process.exit(1); }
  ' "$CONFIG_FILE" "$1" 2>/dev/null
}

config_save() {
  MM_BASE="$BASE_DIR" MM_ONLY="$(IFS=,; printf '%s' "${SELECTED[*]}")" \
  MM_EXT="$COUNT_EXTENSION" MM_MOB="$COUNT_MOBILE" MM_CORE="$COUNT_CORE" \
  "$NODE_BIN" -e '
    process.stdout.write(JSON.stringify({
      schemaVersion: 1,
      baseDir: process.env.MM_BASE,
      counts: {
        extension: Number(process.env.MM_EXT),
        mobile: Number(process.env.MM_MOB),
        core: Number(process.env.MM_CORE),
      },
      only: process.env.MM_ONLY ? process.env.MM_ONLY.split(",") : [],
      updatedAt: new Date().toISOString(),
    }, null, 2) + "\n");
  ' | "$NODE_BIN" "$PRIVATE_WRITER" "$CONFIG_FILE" 2>/dev/null \
    || ui_warn "could not write $CONFIG_FILE — preferences not saved."
}

if [ "$SHOW_CONFIG" = 1 ]; then
  [ ! -L "$CONFIG_FILE" ] || die "$EXIT_INFRA" "$CONFIG_FILE is a symbolic link" \
    "mm-harness setup-base --reset-config"
  if [ -f "$CONFIG_FILE" ]; then
    say "$CONFIG_FILE"
    cat "$CONFIG_FILE" >&2
  else
    ui_info "No saved preferences yet ($CONFIG_FILE)."
    ui_next "mm-harness setup-base --only core --counts core=1   # a successful run saves them"
  fi
  finish "$EXIT_OK" "show-config"
fi

if [ "$RESET_CONFIG" = 1 ]; then
  if [ -e "$CONFIG_FILE" ] || [ -L "$CONFIG_FILE" ]; then
    rm -f "$CONFIG_FILE"
    ui_ok "Removed $CONFIG_FILE"
  else
    ui_info "Nothing to reset ($CONFIG_FILE does not exist)."
  fi
  finish "$EXIT_OK" "reset-config"
fi

# --- resolve the base directory (flag > env > saved > default) ----------------
if [ -n "$FLAG_DIR" ]; then
  BASE_DIR="$FLAG_DIR"; BASE_SOURCE="--dir"
elif [ -n "${MM_HARNESS_BASE_DIR:-}" ]; then
  BASE_DIR="$(expand_tilde "$MM_HARNESS_BASE_DIR")"; BASE_SOURCE="\$MM_HARNESS_BASE_DIR"
elif SAVED_BASE="$(config_get baseDir)" && [ -n "$SAVED_BASE" ]; then
  BASE_DIR="$SAVED_BASE"; BASE_SOURCE="saved preferences"
else
  BASE_DIR="$DEFAULT_BASE"; BASE_SOURCE="default"
fi

# --- resolve the selection (flags > saved > interactive) ----------------------
HAVE_SAVED_SELECTION=0
if [ "$SAW_ONLY" = 0 ] && [ -f "$CONFIG_FILE" ]; then
  if SAVED_ONLY="$(config_get only)" && [ -n "$SAVED_ONLY" ]; then
    parse_only "$SAVED_ONLY"
    HAVE_SAVED_SELECTION=1
  fi
fi
if [ "$SAW_COUNTS" = 0 ] && [ -f "$CONFIG_FILE" ]; then
  for key in "${REPO_KEYS[@]}"; do
    if saved_count="$(config_get "$key")" && [ -n "$saved_count" ]; then
      set_count "$key" "$saved_count"
      HAVE_SAVED_SELECTION=1
    fi
  done
fi

if [ "$SAW_ONLY" = 1 ] || [ "$SAW_COUNTS" = 1 ]; then
  SELECTION_SOURCE="flags"
  [ "$SAW_ONLY" = 1 ] || SELECTED=("${REPO_KEYS[@]}")
elif [ "$HAVE_SAVED_SELECTION" = 1 ]; then
  SELECTION_SOURCE="saved preferences"
  [ "${#SELECTED[@]}" -gt 0 ] || SELECTED=("${REPO_KEYS[@]}")
else
  SELECTION_SOURCE="interactive"
fi

# A prompt needs someone able to answer it. stdin being a terminal is the signal
# that distinguishes a human at a shell from a script, a pipe, or CI. Questions
# are asked on stderr and answered on stdin, so nothing here competes with the
# --json summary on stdout.
have_tty() { [ -t 0 ]; }

if [ "$SELECTION_SOURCE" = "interactive" ] && ! have_tty; then
  die "$EXIT_USAGE" "no terminal available and nothing selected to set up" \
    "mm-harness setup-base --only extension,mobile,core --counts extension=2,mobile=2,core=2" \
    "mm-harness setup-base --only core --counts core=1   # a smaller start" \
    "mm-harness setup-base --help                        # every flag"
fi

# --- step plan ----------------------------------------------------------------
STEP_TOTAL=4
[ "$SKIP_HARNESS_UPDATE" = 1 ] || STEP_TOTAL=$((STEP_TOTAL + 1))
[ "$SELECTION_SOURCE" = "interactive" ] && STEP_TOTAL=$((STEP_TOTAL + 1))

# --- step: prerequisites ------------------------------------------------------
step "Prerequisites"
command -v git >/dev/null 2>&1 || die "$EXIT_INFRA" "git is not installed — nothing can be cloned" \
  "xcode-select --install   # macOS" \
  "brew install git"
ui_ok "git $(git --version 2>/dev/null | awk '{print $3}')"

[ -x "$NODE_BIN" ] || command -v "$NODE_BIN" >/dev/null 2>&1 \
  || die "$EXIT_INFRA" "node is not installed" \
  "brew install node" \
  "asdf install nodejs latest   # or: nvm install --lts"
ui_ok "node $("$NODE_BIN" -v 2>/dev/null)"

YARN_CMD=()
if command -v yarn >/dev/null 2>&1 && yarn --version >/dev/null 2>&1; then
  YARN_CMD=(yarn)
  ui_ok "yarn $(yarn --version 2>/dev/null)"
elif command -v corepack >/dev/null 2>&1; then
  if corepack yarn --version >/dev/null 2>&1; then
    YARN_CMD=(corepack yarn)
    ui_ok "corepack yarn $(corepack yarn --version 2>/dev/null)"
  else
    die "$EXIT_INFRA" "corepack cannot run yarn" \
      "corepack enable" \
      "corepack prepare yarn@stable --activate"
  fi
else
  die "$EXIT_INFRA" "neither yarn nor a usable corepack is on PATH" \
    "npm install -g corepack && corepack enable"
fi

# --- step: harness currency ---------------------------------------------------
# This script is the first thing a new machine runs, so it is also the natural
# place to make sure the tool it hands off to is present and current.
if [ "$SKIP_HARNESS_UPDATE" = 0 ]; then
  step "Harness"
  if command -v mm-harness >/dev/null 2>&1; then
    ui_ok "mm-harness $(mm-harness --version 2>/dev/null | head -1)"
    ui_info "Update it any time with: npm install -g $HARNESS_PACKAGE@latest"
  else
    ui_warn "mm-harness is not on PATH."
    ui_next "npm install -g $HARNESS_PACKAGE@latest"
    say "       (this script still works without it; you need it for the next step,"
    say "        \`mm-harness doctor\`.)"
  fi
fi

# --- step: selection ----------------------------------------------------------
if [ "$SELECTION_SOURCE" = "interactive" ]; then
  step "What to set up"
  say "Which projects do you want? Enter numbers separated by commas."
  say "  1) extension   2) mobile   3) core"
  printf '%sProjects [default: all]:%s ' "$C_BOLD" "$C_RESET" >&2
  reply=""; read -r reply || reply=""
  if [ -z "$reply" ]; then
    SELECTED=("${REPO_KEYS[@]}")
  else
    chosen=()
    IFS=',' read -r -a picks <<< "$reply"
    for pick in "${picks[@]}"; do
      pick="$(printf '%s' "$pick" | tr -d '[:space:]')"
      case "$pick" in
        1 | extension) chosen+=(extension) ;;
        2 | mobile)    chosen+=(mobile) ;;
        3 | core)      chosen+=(core) ;;
        "") ;;
        *) die "$EXIT_USAGE" "'$pick' is not one of 1, 2, or 3" \
             "mm-harness setup-base --only extension,core   # skip the prompt entirely" ;;
      esac
    done
    [ "${#chosen[@]}" -gt 0 ] || die "$EXIT_USAGE" "nothing selected" \
      "mm-harness setup-base --only extension,mobile,core"
    SELECTED=("${chosen[@]}")
  fi
  for key in "${SELECTED[@]}"; do
    printf '%sHow many copies of %s? [%s]:%s ' "$C_BOLD" "$key" "$(count_for "$key")" "$C_RESET" >&2
    reply=""; read -r reply || reply=""
    if [ -n "$reply" ]; then
      case "$reply" in
        "" | *[!0-9]*) die "$EXIT_USAGE" "'$reply' is not a whole number" \
          "mm-harness setup-base --counts $key=2" ;;
      esac
      [ "$reply" -ge 1 ] || die "$EXIT_USAGE" "copies for '$key' must be at least 1" \
        "mm-harness setup-base --only $key --counts $key=1"
      set_count "$key" "$reply"
    fi
  done
fi

# --- step: plan + disk sanity -------------------------------------------------
step "Plan"
say "  Base directory: $BASE_DIR   ($BASE_SOURCE)"
[ "$SELECTION_SOURCE" = "saved preferences" ] \
  && ui_info "Using saved preferences — override with --dir/--only/--counts, inspect with --show-config."

PLAN_DIRS=()
PLAN_KEYS=()
NEW_CLONES=0
EST_GB=0
for key in "${SELECTED[@]}"; do
  slug="$(repo_slug "$key")"
  count="$(count_for "$key")"
  for i in $(seq 1 "$count"); do
    dest="$BASE_DIR/$slug-$i"
    PLAN_KEYS+=("$key")
    PLAN_DIRS+=("$dest")
    if [ ! -e "$dest" ]; then
      NEW_CLONES=$((NEW_CLONES + 1))
      EST_GB=$((EST_GB + $(repo_gb "$key")))
    fi
  done
  say "  $(printf '%-10s' "$key") $count copies  ->  $BASE_DIR/$slug-1..$count"
done

[ "${#PLAN_DIRS[@]}" -gt 0 ] || die "$EXIT_USAGE" "nothing selected to set up" \
  "mm-harness setup-base --only extension,mobile,core"

say "  $NEW_CLONES of ${#PLAN_DIRS[@]} target directories need a fresh clone (~${EST_GB}GB)."

probe="$BASE_DIR"
while [ ! -d "$probe" ] && [ "$probe" != "/" ]; do probe="$(dirname "$probe")"; done
avail_kb="$(df -Pk "$probe" 2>/dev/null | awk 'NR==2 {print $4}')"
if [ "$EST_GB" -eq 0 ]; then
  ui_info "Every target directory already exists — no new clones, so no disk check needed."
elif [ -z "${avail_kb:-}" ]; then
  ui_warn "Could not read free space for $probe — skipping the disk check."
else
  avail_gb=$((avail_kb / 1024 / 1024))
  say "  Free space on $probe: ${avail_gb}GB"
  if [ "$avail_gb" -lt "$EST_GB" ]; then
    if [ "$FORCE" = 1 ]; then
      ui_warn "Only ${avail_gb}GB free for an estimated ${EST_GB}GB — continuing because --force was given."
    else
      die "$EXIT_INFRA" "not enough disk space: ~${EST_GB}GB needed, ${avail_gb}GB free on $probe" \
        "mm-harness setup-base --counts extension=1,mobile=1,core=1   # fewer copies" \
        "mm-harness setup-base --only core                            # smaller subset" \
        "mm-harness setup-base --force                                # override this check"
    fi
  fi
fi

if [ "$DRY_RUN" = 1 ]; then
  ui_section "Dry run"
  for idx in "${!PLAN_DIRS[@]}"; do
    dest="${PLAN_DIRS[$idx]}"
    slug="$(repo_slug "${PLAN_KEYS[$idx]}")"
    if [ -e "$dest" ]; then
      say "  fetch + install   $dest"
      RESULTS+=("$(basename "$dest")	planned-existing	planned")
    else
      say "  clone + install   $dest   (git@github.com:MetaMask/$slug.git)"
      RESULTS+=("$(basename "$dest")	planned-clone	planned")
    fi
  done
  ui_info "Dry run — nothing was created, fetched, or installed."
  finish "$EXIT_OK" "dry-run"
fi

# --- step: clone + install ----------------------------------------------------
step "Clone + install"
mkdir -p "$BASE_DIR" 2>/dev/null || die "$EXIT_INFRA" "cannot create $BASE_DIR" \
  "mkdir -p '$BASE_DIR'   # then re-run, or pass a writable --dir"
[ -w "$BASE_DIR" ] || die "$EXIT_INFRA" "$BASE_DIR is not writable" \
  "mm-harness setup-base --dir \"\$HOME/dev/metamask\""

# clone_repo <slug> <dest> — clone into private staging, then publish atomically.
clone_repo() {
  local slug="$1" dest="$2" staging candidate
  staging="$(mktemp -d "$BASE_DIR/.mm-harness-clone.XXXXXX")" || return 1
  chmod 700 "$staging"
  candidate="$staging/ssh"
  if ! git clone "git@github.com:MetaMask/$slug.git" "$candidate" >&2; then
    ui_warn "SSH clone of $slug failed — retrying over HTTPS."
    candidate="$staging/https"
    if ! git clone "https://github.com/MetaMask/$slug.git" "$candidate" >&2; then
      rm -rf "$staging"
      return 1
    fi
  fi

  if ! "$NODE_BIN" -e '
    const fs = require("fs");
    const source = process.argv[1];
    const destination = process.argv[2];
    const sourceMode = fs.statSync(source).mode & 0o777;
    let reservation;

    try {
      fs.mkdirSync(destination, { mode: sourceMode });
      reservation = fs.lstatSync(destination);
    } catch (error) {
      if (error && error.code === "EEXIST") process.exit(2);
      throw error;
    }

    try {
      const current = fs.lstatSync(destination);
      if (current.dev !== reservation.dev || current.ino !== reservation.ino) {
        throw new Error("destination reservation changed before publication");
      }
      fs.renameSync(source, destination);
    } catch (error) {
      try {
        const current = fs.lstatSync(destination);
        if (current.dev === reservation.dev && current.ino === reservation.ino) {
          fs.rmdirSync(destination);
        }
      } catch {}
      throw error;
    }
  ' "$candidate" "$dest" 2>/dev/null; then
    ui_fail "$(basename "$dest") appeared while cloning — left untouched."
    rm -rf "$staging"
    return 1
  fi
  rm -rf "$staging"
}

origin_matches() {
  local slug="$1" origin="$2"
  case "$origin" in
    "git@github.com:MetaMask/$slug.git" | \
    "ssh://git@github.com/MetaMask/$slug.git" | \
    "https://github.com/MetaMask/$slug.git" | \
    "https://github.com/MetaMask/$slug") return 0 ;;
    *) return 1 ;;
  esac
}

# install_deps <dest> — run the repo's own dependency install, following the
# lockfile it ships. Dependencies only.
install_deps() {
  local dest="$1"
  if [ -f "$dest/yarn.lock" ]; then
    (cd "$dest" && "${YARN_CMD[@]}" install --immutable >&2)
  elif [ -f "$dest/package-lock.json" ]; then
    (cd "$dest" && npm ci >&2)
  elif [ -f "$dest/pnpm-lock.yaml" ]; then
    (cd "$dest" && pnpm install --frozen-lockfile >&2)
  elif [ -f "$dest/package.json" ]; then
    ui_warn "$(basename "$dest"): no lockfile — skipping install."
  else
    ui_warn "$(basename "$dest"): no package.json — skipping install."
  fi
}

for idx in "${!PLAN_DIRS[@]}"; do
  dest="${PLAN_DIRS[$idx]}"
  key="${PLAN_KEYS[$idx]}"
  slug="$(repo_slug "$key")"
  name="$(basename "$dest")"
  clone_state=""
  install_state=""

  if [ -L "$dest" ]; then
    ui_fail "$name is a symbolic link — left untouched."
    ui_next "move it aside, then re-run: mv $dest $dest.bak"
    RESULTS+=("$name"$'\t'"blocked"$'\t'"skipped")
    FAILURES=$((FAILURES + 1))
    continue
  elif [ -e "$dest" ]; then
    if [ ! -d "$dest/.git" ]; then
      ui_fail "$name exists but is not a git checkout — left untouched."
      ui_next "move it aside, then re-run: mv $dest $dest.bak"
      RESULTS+=("$name	blocked	skipped")
      FAILURES=$((FAILURES + 1))
      continue
    fi
    origin_url="$(git -C "$dest" remote get-url origin 2>/dev/null || true)"
    if ! origin_matches "$slug" "$origin_url"; then
      ui_fail "$name has origin '$origin_url', expected MetaMask/$slug — left untouched."
      ui_next "point this run elsewhere: mm-harness setup-base --dir <other-base>"
      RESULTS+=("$name	wrong-remote	skipped")
      FAILURES=$((FAILURES + 1))
      continue
    fi
    ui_info "$name already exists — fetching (no reset, no checkout change)."
    if git -C "$dest" fetch --prune origin >&2; then
      clone_state="existing"
    else
      ui_warn "$name: fetch failed — continuing with the local state."
      ui_next "git -C $dest fetch --prune origin"
      clone_state="existing (stale)"
    fi
  else
    ui_info "Cloning MetaMask/$slug into $name"
    if clone_repo "$slug" "$dest"; then
      clone_state="cloned"
    else
      ui_fail "could not clone MetaMask/$slug over SSH or HTTPS."
      ui_next "ssh -T git@github.com          # verify your SSH key"
      ui_next "gh auth login                  # or authenticate for HTTPS"
      RESULTS+=("$name	failed	skipped")
      FAILURES=$((FAILURES + 1))
      continue
    fi
  fi

  ui_info "$name: installing dependencies"
  if install_deps "$dest"; then
    install_state="installed"
    ui_ok "$name ready"
  else
    install_state="failed"
    FAILURES=$((FAILURES + 1))
    ui_fail "$name: dependency install failed."
    # A wrong Node version is by far the most common cause, and these repos pin
    # the version they want in .nvmrc. Note the version your package manager
    # runs is what counts: a version manager can hand yarn a different Node than
    # this shell resolves, so the pin is named but the current version is not.
    if [ -f "$dest/.nvmrc" ]; then
      pinned="$(tr -d '[:space:]' < "$dest/.nvmrc")"
      ui_next "$name pins Node $pinned (.nvmrc) — switch to it, then re-run"
      ui_next "nvm use $pinned   # or: asdf set nodejs ${pinned#v}"
    fi
    ui_next "cd $dest && yarn install   # re-run to see the full error"
  fi

  RESULTS+=("$name	$clone_state	$install_state")
done

# --- step: summary ------------------------------------------------------------
step "Summary"
printf '  %-28s %-16s %s\n' 'Directory' 'Clone' 'Install' >&2
for row in "${RESULTS[@]}"; do
  IFS=$'\t' read -r r_name r_clone r_install <<< "$row"
  printf '  %-28s %-16s %s\n' "$r_name" "$r_clone" "$r_install" >&2
done
say ""
say "  Base: $BASE_DIR"

EXIT_CODE="$EXIT_OK"
if [ "$FAILURES" -gt 0 ]; then
  EXIT_CODE="$EXIT_RUNTIME"
  ui_warn "$FAILURES of ${#PLAN_DIRS[@]} directories need attention (see the Next: lines above)."
else
  ui_ok "All ${#PLAN_DIRS[@]} checkouts are cloned and installed."
fi

# Preferences persist only after a clean run, so a half-broken setup is never
# adopted as the new default.
if [ "$FAILURES" -eq 0 ]; then
  config_save
  ui_info "Saved preferences to $CONFIG_FILE (--show-config to inspect, --reset-config to clear)."
fi

{
  printf '\n'
  printf 'This script does clones and dependency installs only. It does NOT install\n'
  printf 'platform toolchains, create simulators, write .env files, or run builds.\n'
  printf '\n'
  printf 'Next, check whether this machine can build and run what you just cloned:\n'
  printf '\n'
  printf '  mm-harness doctor\n'
} >&2

finish "$EXIT_CODE"
