#!/usr/bin/env bash
# post-publish device-side verification harness.
#
# Closes the verification gap where tasks can archive on source-diff alone
# without a device run, leaving fixes unshipped to actual hardware. This
# script SSHes to every device in a manifest, runs the published installer
# for that brand, then greps the install log for the canonical CDP success
# banner.
#
# Exit zero ↔ every device's installer reached "Browser automation ready
# (CDP connected)". Any other state — ssh failure, npx non-zero, missing
# banner — is a non-zero exit with the failing device named on stderr.
#
# CONTRACT
#   Archival of any task that touches packages/create-maxy-code/** is contingent
#   on this script exiting zero against the published version. The exit code
#   and the per-device summary are quoted in the close commit body.
#
# USAGE
#   $ installer-device-verify.sh <published-version>
#
#   <published-version>  e.g. 1.0.853 — appended to `npx -y @rubytech/create-<brand>@<version>`.
#
# MANIFEST
#   Path: $MAXY_VERIFY_MANIFEST (default $HOME/.maxy-verify-devices.json).
#   Format: JSON array of devices; each entry:
#     {
#       "name":    "maxy-pi-test",            // free-form display name
#       "brand":   "maxy",                    // npm package suffix (@rubytech/create-<brand>)
#       "configDir": ".maxy",                 // brand configDir; logs live in $HOME/<configDir>/logs/
#       "sshTarget": "admin@maxytest.local",  // user@host for ssh
#       "sshPass":  "password"                // optional; uses sshpass if present
#     }
#   sshTarget supports inline port via "user@host:port" — script splits if present.
#   sshPass omission → relies on existing ssh keys / agent.
#
# OUTPUT
#   Per-device summary line on stdout.
#   Detailed log: $HOME/.maxy/logs/installer-verify-<runId>.log (created if absent).
#
# DEPENDENCIES
#   ssh (always), sshpass (only if any device uses sshPass), jq (always — the
#   manifest is JSON; bash-only parse would be a CVE waiting to happen).

set -euo pipefail

if [[ $# -ne 1 ]]; then
  echo "ERROR: missing argument <published-version>" >&2
  echo "Usage: $0 <published-version>" >&2
  echo "Example: $0 1.0.853" >&2
  exit 2
fi

VERSION="$1"
# Pattern-validate version before any remote-shell interpolation. The version
# flows into `npx -y @rubytech/create-<brand>@$VERSION` over SSH; a stray
# semicolon or backtick would land inside the remote shell. Operator-owned
# input but the principle is validate-at-boundary.
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
  echo "ERROR: invalid version '$VERSION' — expected semver (e.g. 1.0.853 or 1.0.853-rc.1)" >&2
  exit 2
fi

MANIFEST="${MAXY_VERIFY_MANIFEST:-$HOME/.maxy-verify-devices.json}"
RUN_ID="$(date +%Y%m%dT%H%M%SZ)-$$"
SUMMARY_DIR="$HOME/.maxy/logs"
SUMMARY_LOG="$SUMMARY_DIR/installer-verify-$RUN_ID.log"

if [[ ! -f "$MANIFEST" ]]; then
  cat >&2 <<EOF
ERROR: device manifest not found at $MANIFEST.

Create one with this shape (JSON array, one entry per device):
[
  {
    "name":      "maxy-pi-test",
    "brand":     "maxy",
    "configDir": ".maxy",
    "sshTarget": "admin@maxytest.local",
    "sshPass":   "password"
  }
]

Or set \$MAXY_VERIFY_MANIFEST to point elsewhere.
Device list reference: ~/.claude/projects/-Users-neo-getmaxy/memory/reference_device_ssh.md
EOF
  exit 2
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "ERROR: jq is required (manifest is JSON)." >&2
  echo "  macOS:    brew install jq" >&2
  echo "  Ubuntu:   sudo apt-get install -y jq" >&2
  exit 2
fi

mkdir -p "$SUMMARY_DIR"
: > "$SUMMARY_LOG"

# Resolve sshpass once: required only if any manifest entry has sshPass set.
NEEDS_SSHPASS=$(jq -r 'any(.[]; .sshPass != null and .sshPass != "")' "$MANIFEST")
if [[ "$NEEDS_SSHPASS" == "true" ]] && ! command -v sshpass >/dev/null 2>&1; then
  echo "ERROR: manifest references sshPass but sshpass is not installed." >&2
  echo "  macOS:    brew install hudochenkov/sshpass/sshpass" >&2
  echo "  Ubuntu:   sudo apt-get install -y sshpass" >&2
  exit 2
fi

DEVICE_COUNT=$(jq 'length' "$MANIFEST")
FAILED=0

# Common ssh hardening: short timeouts, no host-key prompt that would block
# when adding a new Pi to the fleet, and StrictHostKeyChecking=accept-new so
# fingerprints are recorded the first time but rejected on mismatch after.
SSH_OPTS=(
  -o "ConnectTimeout=10"
  -o "ServerAliveInterval=15"
  -o "ServerAliveCountMax=2"
  -o "StrictHostKeyChecking=accept-new"
  -o "BatchMode=no"
)

run_ssh() {
  local target="$1" pass="$2"
  shift 2
  if [[ -n "$pass" ]]; then
    SSHPASS="$pass" sshpass -e ssh "${SSH_OPTS[@]}" "$target" "$@"
  else
    ssh "${SSH_OPTS[@]}" "$target" "$@"
  fi
}

# Allowed brand pattern matches what bundle.js validates: lowercase
# alphanumeric, hyphens. Reject anything else before interpolating into
# remote shell commands. configDir mirrors the brand convention (a leading
# dot then the same character class), so the same shape passes through.
# `$NAME` is interpolated into `--hostname "$NAME"` over SSH — same boundary
# discipline applies; allow dots so it can carry mDNS hostnames like
# `maxytest.local`, but never quote characters or shell metacharacters.
ALLOWED='^[a-z0-9][a-z0-9-]*$'
ALLOWED_HOSTNAME='^[a-zA-Z0-9][a-zA-Z0-9.-]*$'

echo "installer-device-verify run=$RUN_ID version=$VERSION devices=$DEVICE_COUNT" | tee -a "$SUMMARY_LOG"

for i in $(seq 0 $((DEVICE_COUNT - 1))); do
  NAME=$(jq -r ".[$i].name"      "$MANIFEST")
  BRAND=$(jq -r ".[$i].brand"     "$MANIFEST")
  CONFIGDIR=$(jq -r ".[$i].configDir" "$MANIFEST")
  TARGET=$(jq -r ".[$i].sshTarget" "$MANIFEST")
  PASS=$(jq -r ".[$i].sshPass // \"\"" "$MANIFEST")

  if [[ ! "$BRAND" =~ $ALLOWED ]]; then
    echo "FAIL  $NAME — invalid brand '$BRAND' (must match $ALLOWED)" | tee -a "$SUMMARY_LOG"
    FAILED=1
    continue
  fi

  if [[ ! "$NAME" =~ $ALLOWED_HOSTNAME ]]; then
    echo "FAIL  device name '$NAME' rejected — must match $ALLOWED_HOSTNAME" | tee -a "$SUMMARY_LOG"
    FAILED=1
    continue
  fi

  # configDir always begins with '.' (e.g. .maxy); strip the dot and validate
  # the remainder with the same allowed-character class.
  CD_TAIL="${CONFIGDIR#.}"
  if [[ "$CD_TAIL" == "$CONFIGDIR" || ! "$CD_TAIL" =~ $ALLOWED ]]; then
    echo "FAIL  $NAME — invalid configDir '$CONFIGDIR' (must be a leading dot + $ALLOWED)" | tee -a "$SUMMARY_LOG"
    FAILED=1
    continue
  fi

  echo "" | tee -a "$SUMMARY_LOG"
  echo "[$NAME] brand=$BRAND target=$TARGET version=$VERSION" | tee -a "$SUMMARY_LOG"

  # Step 1 — install. Run npx with auto-yes; redirect to the device's own
  # install log path so the summary log on the operator side stays clean,
  # and so the device-side log matches what reference_device_ssh.md would
  # tail.
  set +e
  run_ssh "$TARGET" "$PASS" "npx -y @rubytech/create-$BRAND@$VERSION --hostname \"$NAME\"" >>"$SUMMARY_LOG" 2>&1
  INSTALL_RC=$?
  set -e

  if [[ $INSTALL_RC -ne 0 ]]; then
    echo "FAIL  $NAME — install exited $INSTALL_RC (see $SUMMARY_LOG)" | tee -a "$SUMMARY_LOG"
    FAILED=1
    continue
  fi

  # Step 2 — confirm a terminal-success banner in the latest install log.
  # Pick the newest install-*.log by mtime (`ls -t`) and grep for either of
  # the installer's two terminal-success markers emitted by
  # packages/create-maxy-code/src/index.ts:
  #   • DISPLAY_MODE=virtual (Pi, headless VNC) →
  #     "Browser automation ready (CDP connected)"  (index.ts:3012)
  #   • DISPLAY_MODE=native  (laptop, on-demand Chromium) →
  #     "[cdp-check] skipped reason=native-display"  (index.ts:3004)
  # Both terminate the install successfully; either is a pass. Hard-coding
  # only the virtual-mode banner would mark every laptop install FAIL even
  # though the laptop install correctly skips CDP probing because there is
  # no persistent Chromium running for CDP to talk to.
  REMOTE_CHECK=$(cat <<'REMOTE'
set -euo pipefail
LOG_DIR="$HOME/__CONFIGDIR__/logs"
LATEST=$(ls -t "$LOG_DIR"/install-*.log 2>/dev/null | head -n1 || true)
if [[ -z "${LATEST:-}" ]]; then
  echo "no-install-log"
  exit 1
fi
echo "log=$LATEST"
if grep -F -q "Browser automation ready (CDP connected)" "$LATEST"; then
  echo "marker=cdp-connected"
  exit 0
fi
if grep -F -q "[cdp-check] skipped reason=native-display" "$LATEST"; then
  echo "marker=native-display-skip"
  exit 0
fi
echo "banner-not-found"
tail -n 30 "$LATEST"
exit 1
REMOTE
)
  REMOTE_CHECK="${REMOTE_CHECK//__CONFIGDIR__/$CONFIGDIR}"

  set +e
  run_ssh "$TARGET" "$PASS" "$REMOTE_CHECK" >>"$SUMMARY_LOG" 2>&1
  CHECK_RC=$?
  set -e

  if [[ $CHECK_RC -eq 0 ]]; then
    echo "OK    $NAME — CDP banner present" | tee -a "$SUMMARY_LOG"
  else
    echo "FAIL  $NAME — CDP banner missing (rc=$CHECK_RC, see $SUMMARY_LOG)" | tee -a "$SUMMARY_LOG"
    FAILED=1
  fi
done

echo "" | tee -a "$SUMMARY_LOG"
if [[ $FAILED -eq 0 ]]; then
  echo "PASS — all $DEVICE_COUNT devices installed and reported CDP banner" | tee -a "$SUMMARY_LOG"
  echo "summary: $SUMMARY_LOG"
  exit 0
else
  echo "FAIL — at least one device did not reach CDP banner; details in $SUMMARY_LOG" | tee -a "$SUMMARY_LOG"
  echo "summary: $SUMMARY_LOG"
  exit 1
fi
