#!/usr/bin/env bash
# Runs a dev-server + client (browser/E2E) pair together on one remote build box.
set -euo pipefail

HOME_DIR="${HOME}"
BASE_CONFIG="${BUILD_REMOTE_CONFIG:-$HOME_DIR/.claude/build-remote.json}"
LOCAL_GATE="$HOME_DIR/.claude/bin/local-gate"
NODE_BIN=/usr/bin/node
DEFAULT_WAIT_SEC=240

usage() {
  cat >&2 <<'USAGE'
usage: e2e-remote [--server <shell-cmd> --wait-port <port>] [options] -- <client argv...>

  --server <cmd>     shell command that starts the server (run on the remote box)
                     omit it when the client drives an already-deployed target;
                     the client still runs remotely, where the browser is allowed
  --wait-port <n>    TCP port on 127.0.0.1 the server must listen on
                     required with --server, rejected without it
  --wait-sec <n>     how long to wait for the port (default 240)
  --env K=V          environment for BOTH server and client (repeatable)
  --mkdir <dir>      repo-relative dir created remotely before the run (repeatable)
  --hosts a,b        remote boxes to try, in order
                     (default: the "e2e" order in ~/.claude/buildbox-hosts.json,
                     minus disabled hosts and the host this process runs on)
  --key <key>        local-gate dedupe key

exit codes: 3 port already in use, 4 server died before listening,
            5 server never listened, otherwise the client's own exit code.

Artifacts must be written inside the repo tree to be rsynced back; anything under
node_modules/.git/.cache/target is excluded from the pull.
USAGE
  exit "${1:-2}"
}

SERVER_CMD=""
WAIT_PORT=""
WAIT_SEC="$DEFAULT_WAIT_SEC"
HOSTS=""
HOSTS_PINNED=false
KEY=""
ENV_ARGS=()
MKDIRS=""
CLIENT=()

while [ $# -gt 0 ]; do
  case "$1" in
    --server)    SERVER_CMD="${2-}"; shift 2 ;;
    --wait-port) WAIT_PORT="${2-}"; shift 2 ;;
    --wait-sec)  WAIT_SEC="${2-}"; shift 2 ;;
    --hosts)     HOSTS="${2-}"; HOSTS_PINNED=true; shift 2 ;;
    --key)       KEY="${2-}"; shift 2 ;;
    --env)       ENV_ARGS+=("${2-}"); shift 2 ;;
    --mkdir)     MKDIRS="${MKDIRS}${2-}"$'\n'; shift 2 ;;
    --)          shift; CLIENT=("$@"); break ;;
    -h|--help)   usage 0 ;;
    *)           echo "e2e-remote: unknown arg: $1" >&2; usage ;;
  esac
done

if [ -n "$SERVER_CMD" ]; then
  [ -n "$WAIT_PORT" ] || { echo "e2e-remote: --wait-port is required with --server" >&2; usage; }
  case "$WAIT_PORT" in *[!0-9]*) echo "e2e-remote: --wait-port must be numeric" >&2; exit 2 ;; esac
else
  [ -z "$WAIT_PORT" ] || { echo "e2e-remote: --wait-port needs --server" >&2; usage; }
fi
[ "${#CLIENT[@]}" -gt 0 ] || { echo "e2e-remote: missing client command after --" >&2; usage; }
case "$WAIT_SEC" in *[!0-9]*|'') echo "e2e-remote: --wait-sec must be numeric" >&2; exit 2 ;; esac
for kv in "${ENV_ARGS[@]}"; do
  case "$kv" in *=*) ;; *) echo "e2e-remote: --env expects KEY=VALUE, got: $kv" >&2; exit 2 ;; esac
done
[ -x "$LOCAL_GATE" ] || { echo "e2e-remote: local-gate not found at $LOCAL_GATE" >&2; exit 2; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SEAT_ATTEST_LIB="$SCRIPT_DIR/../lib/seat-attest.sh"
# shellcheck source=../lib/seat-attest.sh
[ -f "$SEAT_ATTEST_LIB" ] || { echo "e2e-remote: seat attestation lib missing at $SEAT_ATTEST_LIB" >&2; exit 2; }
# shellcheck disable=SC1090
. "$SEAT_ATTEST_LIB"

FLEET_SEAT_LIB="$SCRIPT_DIR/../lib/fleet-seat.sh"
[ -f "$FLEET_SEAT_LIB" ] || { echo "e2e-remote: fleet seat lib missing at $FLEET_SEAT_LIB" >&2; exit 2; }
# shellcheck source=../lib/fleet-seat.sh
# shellcheck disable=SC1090
. "$FLEET_SEAT_LIB"

E2E_PAIR_LIB="$SCRIPT_DIR/../lib/e2e-pair.sh"
[ -f "$E2E_PAIR_LIB" ] || { echo "e2e-remote: pair orchestrator missing at $E2E_PAIR_LIB" >&2; exit 2; }
# Shipped to the remote box inside the payload, so it is read as text here rather
# than sourced.
ORCHESTRATOR="$(cat "$E2E_PAIR_LIB")"

if [ -n "${OVERDECK_SEAT_HOST:-}" ]; then
  overdeck_seat_attest_or_die
  export E2E_REMOTE_OK=1
  for kv in "${ENV_ARGS[@]}"; do export "$kv"; done
  eval "$ORCHESTRATOR"
  e2e_remote_run "$WAIT_PORT" "$WAIT_SEC" "$SERVER_CMD" "$MKDIRS" "${CLIENT[@]}"
  exit $?
fi

[ -x "$NODE_BIN" ] || { echo "e2e-remote: node runtime not found at $NODE_BIN" >&2; exit 2; }

if [ -z "$HOSTS" ]; then
  HOSTS="$("$NODE_BIN" "$SCRIPT_DIR/../lib/buildbox-registry.mjs" hosts --order e2e)" || {
    echo "e2e-remote: no usable buildbox host — refusing to run the pair locally" >&2
    exit 97
  }
else
  HOSTS="$("$NODE_BIN" "$SCRIPT_DIR/../lib/buildbox-registry.mjs" check "$HOSTS")" || {
    echo "e2e-remote: --hosts rejected by the buildbox registry — refusing to run the pair locally" >&2
    exit 97
  }
fi

# The pair is only self-contained on one host, so a local fallback would put the
# server and the browser back on the laptop. Spill across boxes, never downward.
if [ "$HOSTS_PINNED" = false ]; then
  BUILD_HOSTS="$("$NODE_BIN" "$SCRIPT_DIR/../lib/buildbox-registry.mjs" hosts --order build)" || {
    echo "e2e-remote: no usable buildbox host — refusing to run the pair locally" >&2
    exit 97
  }
  if [ "$HOSTS" != "$BUILD_HOSTS" ]; then
    echo "e2e-remote: e2e host order differs from remote-build host order — refusing an un-pinned dispatch" >&2
    exit 97
  fi
fi
CONFIG_DIR="$(mktemp -d -t e2e-remote-cfg.XXXXXX)"
trap 'rm -rf "$CONFIG_DIR"' EXIT

# systemd-run on the box expands ${...} in the job command line against the
# unit's (empty) environment, so any brace reaching it is silently blanked.
# Base64 keeps the whole payload opaque to every quoting layer in between.
PAYLOAD="$ORCHESTRATOR"$'\n'
# Opens the fail-closed browser guard (bin/install-headless-guard) for this run.
# Not a --env: the caller must not be able to forge or omit it.
PAYLOAD+=$'export E2E_REMOTE_OK=1\n'
for kv in "${ENV_ARGS[@]}"; do
  PAYLOAD+="export $(printf '%q' "$kv")"$'\n'
done
PAYLOAD+="e2e_remote_run $(printf '%q' "$WAIT_PORT") $(printf '%q' "$WAIT_SEC")"
PAYLOAD+=" $(printf '%q' "$SERVER_CMD") $(printf '%q' "$MKDIRS")"
for arg in "${CLIENT[@]}"; do
  PAYLOAD+=" $(printf '%q' "$arg")"
done
PAYLOAD+=$'\n'

if [ -z "$KEY" ]; then
  # Receipt names are per invocation, not work identity. Hash the semantic pair
  # before adding receipt bookkeeping so identical concurrent runs still dedupe.
  KEY="e2e_$(basename "$PWD")_$(printf '%s' "$PAYLOAD" | sha256sum | cut -c1-12)"
  KEY="${KEY//[^A-Za-z0-9_.-]/_}"
fi
STATUS_RECEIPT=".e2e-remote-status-$$-$(printf '%s' "$HOSTS:$KEY:$PWD" | sha256sum | cut -c1-12)"
trap 'rm -rf "$CONFIG_DIR"; rm -f "$STATUS_RECEIPT"' EXIT
PAYLOAD+=$'rc=$?\nprintf \'%s\\t%s\\n\' "${E2E_REMOTE_OUTCOME:-unknown}" "$rc" > '
PAYLOAD+="$(printf '%q' "$STATUS_RECEIPT")"
PAYLOAD+=$'\nexit "$rc"\n'
PAYLOAD_B64="$(printf '%s' "$PAYLOAD" | base64 -w0)"

e2e_run_host() {
  local host="$1" config_file host_key gate_status outcome result_status
  config_file="$CONFIG_DIR/build-remote-${host//[^A-Za-z0-9_.-]/_}.json"
  host_key="${KEY}_${host//[^A-Za-z0-9_.-]/_}"
  "$NODE_BIN" -e '
    const fs = require("node:fs");
    const [base, out, host] = process.argv.slice(1);
    const cfg = JSON.parse(fs.readFileSync(base, "utf8"));
    cfg.local_fallback = false;
    cfg.dispatch_hosts = [host];
    fs.writeFileSync(out, JSON.stringify(cfg));
  ' "$BASE_CONFIG" "$config_file" "$host" || {
    gate_status=$?
    return "$gate_status"
  }

  rm -f "$STATUS_RECEIPT"
  if E2E_REMOTE_RECEIPT="$STATUS_RECEIPT" BUILD_REMOTE_CONFIG="$config_file" \
    "$LOCAL_GATE" --remote-only --key "$host_key" -- \
    /home/user/.rb/e2e-remote-payload "$PAYLOAD_B64" "${CLIENT[@]}"; then
    gate_status=0
  else
    gate_status=$?
  fi

  if [ -f "$STATUS_RECEIPT" ]; then
    IFS=$'\t' read -r outcome result_status <"$STATUS_RECEIPT" || true
    rm -f "$STATUS_RECEIPT"
    case "$result_status" in ''|*[!0-9]*) outcome=unknown ;; esac
    case "$outcome" in
      client)
        E2E_FINAL_STATUS="$result_status"
        return 0
        ;;
      admission)
        case "$result_status" in
          3|4|5) return "$((result_status + 80))" ;;
        esac
        ;;
    esac
    E2E_FINAL_STATUS="$gate_status"
    return 0
  fi

  [ "$gate_status" -eq 97 ] && return 97
  E2E_FINAL_STATUS="$gate_status"
  return 0
}

# The receipt is the retry boundary: only pre-client admission outcomes spill.
# Raw client statuses—including 83–85 and 97—complete on their original host.
E2E_FINAL_STATUS=0
if fleet_seat_run "$HOSTS" "83,84,85,97" e2e_run_host; then
  status=0
else
  status=$?
fi
case "$status" in
  0) exit "$E2E_FINAL_STATUS" ;;
  83) exit 3 ;;
  84) exit 4 ;;
  85) exit 5 ;;
  *) exit "$status" ;;
esac
