#!/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/run slot already in use
            4 server died before listening
            5 server never listened (timeout)
            6 the run was rejected before anything started
              (see the message above for what was missing)
            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=()
K8S=false
K8S_RUN_ID="${IPZ_E2E_RUN_ID:-}"
K8S_MIRROR_SLUG="${IPZ_E2E_MIRROR_SLUG:-}"
EXPLICIT_K8S=false
EXPLICIT_SSH=false
LANE_SOURCE="fallback"

# Detect explicit lane selection before parsing so k8s-only options remain
# unknown on the SSH lane. Stop at -- because a client may pass lane-like args.
for arg in "$@"; do
  [ "$arg" = "--" ] && break
  [ "$arg" = "--k8s" ] && EXPLICIT_K8S=true
  [ "$arg" = "--ssh" ] && EXPLICIT_SSH=true
done
if [ "$EXPLICIT_K8S" = true ] && [ "$EXPLICIT_SSH" = true ]; then
  echo "e2e-remote: --k8s and --ssh cannot be used together." >&2
  exit 2
fi

if [ "$EXPLICIT_K8S" = true ]; then
  K8S=true
  LANE_SOURCE="flag"
elif [ "$EXPLICIT_SSH" = true ]; then
  K8S=false
  LANE_SOURCE="flag"
elif [ -n "${IPZ_E2E_LANE+x}" ]; then
  case "$IPZ_E2E_LANE" in
    k8s) K8S=true ;;
    ssh) K8S=false ;;
    *) echo "e2e-remote: invalid IPZ_E2E_LANE value '$IPZ_E2E_LANE'; accepted values are k8s and ssh." >&2; exit 2 ;;
  esac
  LANE_SOURCE="environment variable IPZ_E2E_LANE"
else
  LANE_CONFIG=""
  if [ -n "${XDG_CONFIG_HOME:-}" ] && [ -f "$XDG_CONFIG_HOME/overdeck/e2e-lane.env" ]; then
    LANE_CONFIG="$XDG_CONFIG_HOME/overdeck/e2e-lane.env"
  elif [ -f "$HOME_DIR/.config/overdeck/e2e-lane.env" ]; then
    LANE_CONFIG="$HOME_DIR/.config/overdeck/e2e-lane.env"
  fi
  if [ -n "$LANE_CONFIG" ]; then
    CONFIG_LANE=""
    while IFS= read -r line || [ -n "$line" ]; do
      if [[ "$line" =~ ^IPZ_E2E_LANE=(k8s|ssh)[[:space:]]*$ ]]; then
        CONFIG_LANE="${BASH_REMATCH[1]}"
        break
      elif [[ "$line" == IPZ_E2E_LANE=* ]]; then
        bad_value="${line#IPZ_E2E_LANE=}"
        echo "e2e-remote: invalid IPZ_E2E_LANE value '$bad_value'; accepted values are k8s and ssh." >&2
        exit 2
      fi
    done <"$LANE_CONFIG"
    case "$CONFIG_LANE" in
      k8s) K8S=true; LANE_SOURCE="config file $LANE_CONFIG" ;;
      ssh) K8S=false; LANE_SOURCE="config file $LANE_CONFIG" ;;
    esac
  fi
fi

if [ "$K8S" = true ] && [ "$LANE_SOURCE" != "flag" ]; then
  echo "e2e-remote: running the k8s lane selected by $LANE_SOURCE." >&2
fi

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 ;;
    --k8s)       K8S=true; shift ;;
    --ssh)        K8S=false; shift ;;
    --run-id)
      [ "$K8S" = true ] || { echo "e2e-remote: unknown arg: $1" >&2; usage; }
      K8S_RUN_ID="${2-}"; shift 2 ;;
    --mirror-slug)
      [ "$K8S" = true ] || { echo "e2e-remote: unknown arg: $1" >&2; usage; }
      K8S_MIRROR_SLUG="${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 [ "$K8S" = true ]; then
  [ -n "$K8S_RUN_ID" ] || K8S_RUN_ID="e2ek8s-$(date -u +%Y%m%d%H%M%S)-$$"
  if [ -z "$K8S_MIRROR_SLUG" ]; then
    K8S_PROJECT_ROOT="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)"
    if [ -n "$K8S_PROJECT_ROOT" ]; then
      K8S_MIRROR_SLUG="$(basename "$K8S_PROJECT_ROOT")-$(printf '%s' "$K8S_PROJECT_ROOT" | sha256sum | cut -c1-12)"
    else
      echo "e2e-remote: --k8s requires --mirror-slug or IPZ_E2E_MIRROR_SLUG when the project root cannot be resolved" >&2
      exit 2
    fi
  fi
  case "$HOSTS" in
    ""|auto) K8S_NODE=auto ;;
    *,*) echo "e2e-remote: --k8s accepts one --hosts node; omit it for dispatcher auto-selection" >&2; exit 2 ;;
    *) K8S_NODE="$HOSTS" ;;
  esac
  if [ -n "$SERVER_CMD" ] || [ -n "$WAIT_PORT" ]; then
    echo "e2e-remote: --k8s ignores --server/--wait-port; the Job owns the server stack" >&2
  fi
  K8S_ENV_ARGS="$(printf '%s\n' "${ENV_ARGS[@]-}")"
  # Preserve the legacy receipt filename contract for the direct k8s dispatch.
  K8S_STATUS_RECEIPT=".e2e-remote-status-$$-$(printf '%s' "$K8S_NODE:$K8S_RUN_ID:$K8S_MIRROR_SLUG:$PWD" | sha256sum | cut -c1-12)"
  K8S_DISPATCH="${E2E_K8S_DISPATCH_STUB:-$HOME_DIR/.claude/bin/e2e-k8s-dispatch}"
  E2E_REMOTE_RECEIPT="$K8S_STATUS_RECEIPT" \
  IPZ_E2E_SERVER_CMD="$SERVER_CMD" \
  IPZ_E2E_WAIT_PORT="$WAIT_PORT" \
  IPZ_E2E_WAIT_SEC="$WAIT_SEC" \
  IPZ_E2E_ENV_ARGS="$K8S_ENV_ARGS" \
  IPZ_E2E_MKDIRS="$MKDIRS" \
  exec "$K8S_DISPATCH" "$K8S_RUN_ID" "$K8S_MIRROR_SLUG" "$K8S_NODE" -- "${CLIENT[@]}"
fi

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|6) 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–86 and 97—complete on their original host.
E2E_FINAL_STATUS=0
if fleet_seat_run "$HOSTS" "83,84,85,86,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 ;;
  86) exit 6 ;;
  *) exit "$status" ;;
esac
