#!/bin/bash
# Setup wallet from a JSON fixture via CDP + AgenticService.setupWallet().
#
# Expects a FRESH app (no existing vault). For a clean environment, run:
#   yarn a:setup:ios   # full clean setup + wallet
#
# Usage:
#   setup-wallet.sh [--fixture path/to/fixture.json]
#
# Fixture JSON format:
#   {
#     "password": "yourpassword",
#     "accounts": [
#       { "type": "mnemonic", "value": "word1 word2 ...", "name": "Primary" },
#       { "type": "privateKey", "value": "0xabc...", "name": "dev1" },
#       { "type": "privateKey", "value": "0xdef...", "name": "dev2" }
#     ],
#     "settings": { "metametrics": true, "skipGtmModals": true, "skipPerpsTutorial": true, "autoLockNever": true, "deviceAuthEnabled": true }
#   }

set -euo pipefail

# Resolve the script directory to an absolute path BEFORE cd, so sourcing and
# helper paths work regardless of the caller's CWD (e.g. ./setup-wallet.sh).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Run from the target product checkout; the runner invokes this with cwd=projectRoot.
APP_ROOT="${APP_ROOT:-$PWD}"
cd "$APP_ROOT"

# Runtime-dir resolver: wallet-fixture.json lands under the same per-run dir as
# other mobile runtime files, controlled by RECIPE_RUNTIME_DIR (relative, validated).
# shellcheck disable=SC1091
. "$SCRIPT_DIR/../../shared/harness-path.sh"
if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
  echo "setup-wallet: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
  exit 1
fi

PORT="${WATCHER_PORT:-8081}"
[[ "$PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: WATCHER_PORT must be numeric (got: $PORT)" >&2; exit 1; }
SCRIPTS="$SCRIPT_DIR"
# Account derivation (applyWalletFixture / setupWallet) is far slower than a
# normal eval — a 10+ account fixture easily exceeds 30s. The value is a cap, not
# a delay, so a high default is harmless for the fast evals. Override via env.
export CDP_TIMEOUT="${CDP_TIMEOUT:-120000}"
export CDP_DISCOVERY_RETRIES="${CDP_DISCOVERY_RETRIES:-3}"
WALLET_SETUP_SETTLE_TIMEOUT="${WALLET_SETUP_SETTLE_TIMEOUT:-180}"
CDP="node $SCRIPTS/cdp-bridge.cjs"
FIXTURE_PATH=""

cdp_json_command() {
  local mode="$1"
  local expression="$2"
  local output=""
  if ! output=$($CDP "$mode" "$expression" 2>&1); then
    echo "ERROR: CDP ${mode} failed: $output" >&2
    return 1
  fi
  if ! printf '%s\n' "$output" | jq -r '.'; then
    echo "ERROR: CDP ${mode} returned non-JSON output: $output" >&2
    return 1
  fi
}
cdp_eval() { cdp_json_command eval "$1"; }
cdp_eval_async() { cdp_json_command eval-async "$1"; }

# -- Parse args --
while [[ $# -gt 0 ]]; do
  case "$1" in
    --fixture)
      [ $# -ge 2 ] || { echo "ERROR: --fixture requires a path argument"; exit 2; }
      FIXTURE_PATH="$2"; shift 2 ;;
    -h|--help) echo "Usage: setup-wallet.sh [--fixture path.json]"; exit 0 ;;
    *)         echo "Unknown arg: $1"; exit 2 ;;
  esac
done

# -- Resolve + validate fixture --
[ -z "$FIXTURE_PATH" ] && FIXTURE_PATH="${WALLET_FIXTURE:-$(recipe_runtime_dir)/wallet-fixture.json}"

if [ ! -f "$FIXTURE_PATH" ]; then
  echo "ERROR: Fixture not found: $FIXTURE_PATH"
  echo "  create $(recipe_runtime_dir)/wallet-fixture.json"
  exit 1
fi
echo "Reading fixture: $FIXTURE_PATH"

jq empty "$FIXTURE_PATH" 2>/dev/null || { echo "ERROR: Invalid JSON"; exit 1; }

PASSWORD=$(jq -r '.password // empty' "$FIXTURE_PATH")
[ -z "$PASSWORD" ] && { echo "ERROR: fixture missing 'password'"; exit 1; }

if jq -e '.import' "$FIXTURE_PATH" >/dev/null 2>&1 && ! jq -e '.accounts' "$FIXTURE_PATH" >/dev/null 2>&1; then
  echo "ERROR: Old format. Use an accounts array in the wallet fixture."
  exit 1
fi

ACCOUNT_COUNT=$(jq -r '.accounts | length // 0' "$FIXTURE_PATH" 2>/dev/null || echo 0)
for i in $(seq 0 $((ACCOUNT_COUNT - 1))); do
  ACC_TYPE=$(jq -r ".accounts[$i].type // empty" "$FIXTURE_PATH")
  ACC_VALUE=$(jq -r ".accounts[$i].value // empty" "$FIXTURE_PATH")
  if [ -z "$ACC_TYPE" ] || { [ "$ACC_TYPE" != "mnemonic" ] && [ "$ACC_TYPE" != "privateKey" ]; }; then
    echo "ERROR: accounts[$i].type must be 'mnemonic' or 'privateKey' (got '${ACC_TYPE:-empty}')"
    echo "  See the Recipe runner Mobile fixture format."
    exit 1
  fi
  [ -z "$ACC_VALUE" ] && { echo "ERROR: accounts[$i].value is empty"; exit 1; }
done
# Expected EVM account total = sum of mnemonic counts + one per private key.
# A mnemonic entry with count=N materializes N HD accounts, not one, so the
# entry count alone (ACCOUNT_COUNT) under-counts and would let setup pass while
# silently missing HD accounts.
EXPECTED_ETH_TOTAL=$(jq -r '[.accounts[] | if .type == "mnemonic" then (.count // .numberOfAccounts // 1) else 1 end] | add // 0' "$FIXTURE_PATH")
echo "Fixture OK: password + ${ACCOUNT_COUNT} entry(ies), ${EXPECTED_ETH_TOTAL} expected EVM account(s)"

wait_for_cdp_bridge() {
  local waited=0
  local max="${CDP_READY_TIMEOUT:-30}"
  local probe_timeout="${CDP_READY_PROBE_TIMEOUT:-1000}"
  local started="$SECONDS"
  local output=""
  while [ "$((SECONDS - started))" -lt "$max" ]; do
    if output=$(CDP_TIMEOUT="$probe_timeout" CDP_DISCOVERY_RETRIES=1 $CDP eval "JSON.stringify({ok:true})" 2>&1); then
      echo "CDP bridge connected."
      return 0
    fi
    sleep 1
    waited=$((SECONDS - started))
    [ "$waited" -eq 5 ] && echo "Waiting for CDP bridge..."
  done
  echo "ERROR: CDP not reachable after ${max}s"
  [ -n "$output" ] && echo "$output" >&2
  return 1
}

# -- Check CDP --
wait_for_cdp_bridge

wait_for_engine() {
  local waited=0
  local max="${ENGINE_READY_TIMEOUT:-60}"
  local engine_ok=""
  while [ "$waited" -lt "$max" ]; do
    engine_ok=$(cdp_eval "(function(){ var engine = globalThis.Engine; return engine && engine.context && engine.context.KeyringController ? 'ready' : 'waiting'; })()" 2>/dev/null || echo "")
    [ "$engine_ok" = "ready" ] && return 0
    sleep 1
    waited=$((waited + 1))
    [ "$waited" -eq 5 ] && echo "Waiting for Engine to initialize..."
  done
  echo "ERROR: Engine.context.KeyringController not available after ${max}s"
  return 1
}

# -- Wait for Engine to be ready (KeyringController must exist) --
wait_for_engine

# The backup subscriber reads the Engine *class* static
# `disableAutomaticVaultBackup`. AgenticService.setupWallet/applyWalletFixture
# set that static authoritatively before any account import/rename. The
# CDP-exposed `Engine` is the facade object, not the class, so we cannot set the
# static from here — only record harness intent for observability.
if ! DISABLE_BACKUP=$(cdp_eval "(function(){ globalThis.__AGENTIC_DISABLE_VAULT_BACKUP = true; return JSON.stringify({agenticDisableVaultBackup: globalThis.__AGENTIC_DISABLE_VAULT_BACKUP === true}); })()"); then
  echo "WARN: could not record vault backup guard intent before wallet setup"
  DISABLE_BACKUP='{}'
fi
echo "Vault backup guard intent: $DISABLE_BACKUP"
wait_for_engine

# Read fixture JSON and escape it for safe embedding in a JS string literal.
FIXTURE_JSON=$(jq -c '.' "$FIXTURE_PATH")
ESCAPED_FIXTURE=$(node -p "JSON.stringify(JSON.stringify(JSON.parse(process.argv[1])))" "$FIXTURE_JSON")
EXPECTED_ADDRESSES=$(node - "$FIXTURE_PATH" <<'NODE'
const fs = require('fs');
const { Wallet } = require('ethers');
const fixture = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const addresses = [];
for (const account of fixture.accounts || []) {
  if (account.type === 'mnemonic') {
    const rawCount =
      account.count != null
        ? account.count
        : account.numberOfAccounts != null
        ? account.numberOfAccounts
        : 1;
    const count = Number(rawCount);
    for (let i = 0; i < count; i += 1) {
      addresses.push(
        Wallet.fromMnemonic(account.value, `m/44'/60'/0'/0/${i}`).address.toLowerCase(),
      );
    }
  } else if (account.type === 'privateKey') {
    const key = account.value.startsWith('0x') ? account.value : `0x${account.value}`;
    addresses.push(new Wallet(key).address.toLowerCase());
  }
}
console.log(JSON.stringify(addresses));
NODE
)
FIRST_EXPECTED_ADDRESS=$(EXPECTED_JSON="$EXPECTED_ADDRESSES" node <<'NODE'
const addresses = JSON.parse(process.env.EXPECTED_JSON || '[]');
process.stdout.write(addresses[0] || '');
NODE
)

read_wallet_state() {
  cdp_eval "(function(){ try { var engine = globalThis.Engine; var ctx = engine && engine.context ? engine.context : {}; var accountsController = ctx.AccountsController || {}; var keyringController = ctx.KeyringController || {}; var state = accountsController.state || {}; var internalAccounts = state.internalAccounts || {}; var accountsById = internalAccounts.accounts || {}; var accs = Object.values(accountsById); var eth = accs.filter(function(a){ return String(a && a.address || '').indexOf('0x') === 0; }); var route = globalThis.__AGENTIC__ && globalThis.__AGENTIC__.getRoute ? globalThis.__AGENTIC__.getRoute() : null; var selected = null; if (typeof accountsController.getSelectedAccount === 'function') { selected = accountsController.getSelectedAccount(); } else if (internalAccounts.selectedAccount && accountsById[internalAccounts.selectedAccount]) { selected = accountsById[internalAccounts.selectedAccount]; } return JSON.stringify({ok:true, unlocked: typeof keyringController.isUnlocked === 'function' ? keyringController.isUnlocked() : null, routeName: route && route.name, selected: selected ? {name: selected.metadata && selected.metadata.name, address: selected.address} : null, total: accs.length, ethAccounts: eth.length, accounts: eth.map(function(a){ return {name: a && a.metadata && a.metadata.name, address: a && a.address}; }), first3: eth.slice(0,3).map(function(a){ return {name: a && a.metadata && a.metadata.name, address: a && a.address}; })}); } catch (e) { return JSON.stringify({ok:false, error: e && (e.stack || e.message) || String(e)}); } })()"
}

missing_fixture_addresses() {
  ACCOUNTS_JSON="$1" EXPECTED_JSON="$EXPECTED_ADDRESSES" node <<'NODE'
const accounts = JSON.parse(process.env.ACCOUNTS_JSON);
const expected = JSON.parse(process.env.EXPECTED_JSON);
const actual = new Set((accounts.accounts || accounts.first3 || []).map((a) => String(a.address || '').toLowerCase()));
const missing = expected.filter((address) => !actual.has(address));
console.log(JSON.stringify(missing));
NODE
}

wallet_state_issue() {
  local accounts_json="$1"
  if [ -z "$accounts_json" ]; then
    echo "wallet state unavailable"
    return
  fi

  local ok unlocked eth_count missing_count
  ok=$(echo "$accounts_json" | jq -r '.ok // false' 2>/dev/null || echo false)
  if [ "$ok" != "true" ]; then
    echo "$(echo "$accounts_json" | jq -r '.error // "wallet state read failed"' 2>/dev/null || echo "wallet state read failed")"
    return
  fi

  unlocked=$(echo "$accounts_json" | jq -r '.unlocked')
  eth_count=$(echo "$accounts_json" | jq -r '.ethAccounts')
  if [ "$unlocked" != "true" ]; then
    echo "vault still locked"
    return
  fi
  if [ "$eth_count" -lt "$EXPECTED_ETH_TOTAL" ]; then
    echo "$eth_count ETH account(s), expected at least $EXPECTED_ETH_TOTAL"
    return
  fi

  missing_count=$(missing_fixture_addresses "$accounts_json" | jq -r 'length')
  if [ "$missing_count" != "0" ]; then
    echo "$missing_count expected fixture address(es) missing"
    return
  fi

  echo "ready"
}

wallet_state_ready() {
  [ "$(wallet_state_issue "$1")" = "ready" ]
}

wallet_fixture_present() {
  local accounts_json="$1"
  local ok eth_count missing_count
  [ -n "$accounts_json" ] || return 1
  ok=$(echo "$accounts_json" | jq -r '.ok // false' 2>/dev/null || echo false)
  [ "$ok" = "true" ] || return 1
  eth_count=$(echo "$accounts_json" | jq -r '.ethAccounts // 0' 2>/dev/null || echo 0)
  [ "$eth_count" -ge "$EXPECTED_ETH_TOTAL" ] || return 1
  missing_count=$(missing_fixture_addresses "$accounts_json" | jq -r 'length')
  [ "$missing_count" = "0" ]
}

PRECHECK_ACCOUNTS=$(read_wallet_state || true)
if wallet_state_ready "$PRECHECK_ACCOUNTS"; then
  echo "Wallet fixture already valid; skipping wallet mutation."
else

wait_for_wallet_state_after_eval_timeout() {
  local label="$1"
  local rc="$2"
  local elapsed=0
  local interval=5
  local last_accounts=""
  local issue=""

  echo "WARN: ${label} eval did not return (rc=$rc) before ${CDP_TIMEOUT}ms; validating app wallet state for up to ${WALLET_SETUP_SETTLE_TIMEOUT}s."
  while [ "$elapsed" -le "$WALLET_SETUP_SETTLE_TIMEOUT" ]; do
    last_accounts=$(read_wallet_state 2>/dev/null || true)
    if wallet_state_ready "$last_accounts"; then
      echo "Wallet state validated after ${label} eval timeout."
      return 0
    fi

    issue=$(wallet_state_issue "$last_accounts")
    [ "$elapsed" -eq 0 ] && echo "  Still waiting for fixture completion: $issue"
    sleep "$interval"
    elapsed=$((elapsed + interval))
  done

  echo "ERROR: ${label} eval failed or timed out (rc=$rc), and wallet state did not validate after ${WALLET_SETUP_SETTLE_TIMEOUT}s."
  echo "Last observed state:"
  if [ -n "$last_accounts" ]; then
    echo "$last_accounts" | jq .
  else
    echo "  unavailable"
  fi
  return 1
}

# -- Check vault state --
VAULT_STATE=$(cdp_eval "(function(){ var engine = globalThis.Engine; var ctx = engine && engine.context; var keyringController = ctx && ctx.KeyringController; if (!keyringController) return JSON.stringify({ready:false, hasVault:false, isUnlocked:false}); var v = keyringController.state || {}; return JSON.stringify({ready:true, hasVault: v.vault !== undefined && v.vault !== null, isUnlocked: keyringController.isUnlocked()}); })()")
HAS_VAULT=$(echo "$VAULT_STATE" | jq -r '.hasVault')
IS_UNLOCKED=$(echo "$VAULT_STATE" | jq -r '.isUnlocked')
echo "Vault state: hasVault=$HAS_VAULT, isUnlocked=$IS_UNLOCKED"

if [ "$HAS_VAULT" = "true" ]; then
  if wallet_fixture_present "$PRECHECK_ACCOUNTS"; then
    if [ "$IS_UNLOCKED" = "true" ]; then
      echo "Wallet fixture already present; vault is unlocked. Skipping wallet mutation."
    else
      echo "Wallet fixture already present; unlocking existing vault without wallet mutation."
      $CDP unlock "$PASSWORD" >/dev/null
      for _ in $(seq 1 30); do
        POST_UNLOCK_ACCOUNTS=$(read_wallet_state 2>/dev/null || true)
        if wallet_state_ready "$POST_UNLOCK_ACCOUNTS"; then
          echo "Existing fixture vault unlocked."
          break
        fi
        sleep 1
      done
      if ! wallet_state_ready "${POST_UNLOCK_ACCOUNTS:-}"; then
        echo "WARN: existing fixture vault did not validate after unlock; falling back to fixture apply."
      fi
    fi
  fi

  if wallet_state_ready "${POST_UNLOCK_ACCOUNTS:-$PRECHECK_ACCOUNTS}"; then
    :
  else
  # Do NOT unlock here with a bare KeyringController.submitPassword(): that
  # bypasses the real auth flow (multichain init + dispatchLogin/password state)
  # and can leave Redux/auth state stale. applyWalletFixture tries the real
  # Authentication.unlockWallet() path first, then recovers interrupted fixture
  # state with a keyring fallback inside AgenticService.
  if [ "$IS_UNLOCKED" = "true" ]; then
    echo "Vault already unlocked."
  else
    echo "Vault locked — applyWalletFixture will unlock via auth flow with fixture recovery fallback."
  fi

  echo "Applying fixture accounts/names to existing vault..."
  set +e
  APPLY_RESULT=$(cdp_eval_async "(function(){ var fixture = JSON.parse($ESCAPED_FIXTURE); if (!globalThis.__AGENTIC__ || typeof globalThis.__AGENTIC__.applyWalletFixture !== 'function') { return JSON.stringify({ok:false, error:'__AGENTIC__.applyWalletFixture is not installed; reload the app from Metro'}); } return globalThis.__AGENTIC__.applyWalletFixture(fixture).then(function(r){ return JSON.stringify(r); }).catch(function(e){ return JSON.stringify({ok:false, error: e.message || String(e)}); }); })()")
  APPLY_RC=$?
  set -e
  if [ "$APPLY_RC" -ne 0 ] || [ -z "$APPLY_RESULT" ]; then
    wait_for_wallet_state_after_eval_timeout "applyWalletFixture" "$APPLY_RC" || exit 1
    echo "Wallet fixture apply result recovered from validated wallet state."
  else
    APPLY_OK=$(echo "$APPLY_RESULT" | jq -r '.ok')
    if [ "$APPLY_OK" != "true" ]; then
      APPLY_ERR=$(echo "$APPLY_RESULT" | jq -r '.error // "unknown error"')
      echo "ERROR: applyWalletFixture failed — $APPLY_ERR"
      exit 1
    fi
    echo "Wallet fixture apply result:"
    echo "$APPLY_RESULT" | jq -r '.accounts[]? | "  \(.name): \(.address)"'
  fi
  fi
else
  # -- Call AgenticService.setupWallet() on fresh app only --
  echo "Calling __AGENTIC__.setupWallet()..."

  set +e
  SETUP_RESULT=$(cdp_eval_async "(function(){ var fixture = JSON.parse($ESCAPED_FIXTURE); if (!globalThis.__AGENTIC__ || typeof globalThis.__AGENTIC__.setupWallet !== 'function') { return JSON.stringify({ok:false, error:'__AGENTIC__.setupWallet is not installed'}); } return globalThis.__AGENTIC__.setupWallet(fixture).then(function(r){ return JSON.stringify(r); }).catch(function(e){ return JSON.stringify({ok:false, error: e.message || String(e)}); }); })()")
  SETUP_RC=$?
  set -e
  if [ "$SETUP_RC" -ne 0 ] || [ -z "$SETUP_RESULT" ]; then
    wait_for_wallet_state_after_eval_timeout "setupWallet" "$SETUP_RC" || exit 1
    echo "Wallet setup result recovered from validated wallet state."
  else
    SETUP_OK=$(echo "$SETUP_RESULT" | jq -r '.ok')
    if [ "$SETUP_OK" != "true" ]; then
      SETUP_ERR=$(echo "$SETUP_RESULT" | jq -r '.error // "unknown error"')
      SETUP_STEP=$(echo "$SETUP_RESULT" | jq -r '.step // "unknown-step"')
      echo "ERROR: setupWallet failed at ${SETUP_STEP} — $SETUP_ERR"
      exit 1
    fi

    echo "Wallet setup result:"
    echo "$SETUP_RESULT" | jq -r '.accounts[]? | "  \(.name): \(.address)"'
  fi
  sleep 2
fi
fi

# -- Ask the app to leave auth/onboarding after unlock.
# HomeNav matches the product auth reset path, but some warm/onboarding states
# land on intermediate post-onboarding screens. Follow with WalletView so the
# harness proves the user-visible unlocked wallet, not just a populated vault.
$CDP navigate HomeNav >/dev/null 2>&1 || true
sleep 1
$CDP navigate WalletView >/dev/null 2>&1 || true
sleep 1
if [ -n "$FIRST_EXPECTED_ADDRESS" ]; then
  cdp_eval "(function(){ try { if (globalThis.__AGENTIC__ && typeof globalThis.__AGENTIC__.switchAccount === 'function') { return JSON.stringify(globalThis.__AGENTIC__.switchAccount('$FIRST_EXPECTED_ADDRESS')); } return JSON.stringify({switched:false, error:'__AGENTIC__.switchAccount unavailable'}); } catch (e) { return JSON.stringify({switched:false, error:e && (e.message || String(e))}); } })()" >/dev/null 2>&1 || true
fi

# -- Summary + hard validation --
ACCOUNTS=$(read_wallet_state)
ACCOUNTS_OK=$(echo "$ACCOUNTS" | jq -r '.ok // false')
if [ "$ACCOUNTS_OK" != "true" ]; then
  echo "ERROR: Unable to read wallet state after setupWallet"
  echo "$ACCOUNTS" | jq .
  exit 1
fi
TOTAL=$(echo "$ACCOUNTS" | jq -r '.total')
ETH_COUNT=$(echo "$ACCOUNTS" | jq -r '.ethAccounts')
UNLOCKED=$(echo "$ACCOUNTS" | jq -r '.unlocked')
ROUTE_NAME=$(echo "$ACCOUNTS" | jq -r '.routeName // empty')
MISSING_ADDRESSES=$(missing_fixture_addresses "$ACCOUNTS")
if [ "$UNLOCKED" != "true" ]; then
  echo "ERROR: Wallet setup did not unlock the vault"
  echo "$ACCOUNTS" | jq .
  exit 1
fi
if [ "$ETH_COUNT" -lt "$EXPECTED_ETH_TOTAL" ]; then
  echo "ERROR: Wallet setup produced $ETH_COUNT ETH account(s), expected at least $EXPECTED_ETH_TOTAL from fixture"
  echo "$ACCOUNTS" | jq .
  exit 1
fi
if [ "$(echo "$MISSING_ADDRESSES" | jq 'length')" != "0" ]; then
  echo "ERROR: Wallet setup did not import/unlock the expected fixture account(s)"
  echo "Missing addresses:"
  echo "$MISSING_ADDRESSES" | jq -r '.[] | "  " + .'
  echo "Actual wallet state:"
  echo "$ACCOUNTS" | jq .
  exit 1
fi
case "$ROUTE_NAME" in
  Login|Onboarding|ExperienceEnhancer|FoxLoader|"")
    echo "ERROR: Wallet setup did not reach the unlocked wallet UI (route=${ROUTE_NAME:-empty})"
    echo "$ACCOUNTS" | jq .
    exit 1
    ;;
esac
echo ""
echo "=== Wallet Ready ==="
echo "Route: ${ROUTE_NAME:-unknown}"
echo "Unlocked: $UNLOCKED"
echo "Accounts: $ETH_COUNT ETH (${TOTAL} total)"
echo "$ACCOUNTS" | jq -r '.first3[] | "  \(.name): \(.address)"'
echo "Selected:"
echo "$ACCOUNTS" | jq -r '.selected | "  \(.name): \(.address)"'
echo ""
echo "Done."
exit 0
