#!/usr/bin/env bash
# node-canary.sh -- READ ONLY. ~30-second proof that the patched TEXITcoin node
# (Send To Many backport, tx type 7) built correctly, is running, is in
# consensus with the network, and still has the feature gate closed.
#
#   baseline:  curl -fsSL https://core.honest.money/install/node-canary.sh | bash -s BASELINE
#   check:     curl -fsSL https://core.honest.money/install/node-canary.sh | bash
#   watch:     curl -fsSL https://core.honest.money/install/node-canary.sh | bash -s WATCH 10
#
# Exit 0 = PASS (warnings allowed), 1 = FAIL.
# Nothing here sends coins, signs anything, or changes node state. Every call is
# a read: getblockchaininfo, getnetworkinfo, omni_getinfo, omni_getactivations,
# help, and the pure-function payload encoder.
#
# ---------------------------------------------------------------------------
# VERSION LOG -- bump CANARY_VERSION on EVERY change, newest entry first.
# If the banner does not show the version you expect, the site has not been
# republished yet (public/install/ is served from the published build).
#
#   v3  2026-09-19  Registration of omni_sendtomany now judged by DISPATCH
#                   instead of help text: Core 0.25 hides help for
#                   wallet-scoped RPCs even when the command exists and
#                   answers, so v2's help check false-FAILed a working node.
#                   The dry bulk-send probe moved into the registration test
#                   (one dispatch proves both registration and the guard).
#   v2  2026-09-19  Handle a node with no wallet: Core 0.25 hides help for
#                   wallet-scoped RPCs and omni_sendtomany refused with -18
#                   ("No wallet is loaded"), which looked like a missing RPC.
#                   Registration now falls back to dispatch evidence, the dry
#                   probe uses real T-addresses and property 1, and the OOM
#                   check no longer trips on grep's empty output.
#   v1  2026-09-19  Initial release for the Send To Many test node
#                   (node-omni.texitcoin.org). Sections: build identity,
#                   daemon + RPC liveness, chain sync vs the public reference
#                   height, Omni parser progress, Send To Many wiring
#                   (RPC presence + byte-exact encoder vector + gate state),
#                   host health, and baseline diff.
# ---------------------------------------------------------------------------
CANARY_VERSION="v3"
set -uo pipefail

MODE="${1:-CHECK}"
WATCH_MINS="${2:-10}"

DATADIR="${TXC_DATADIR:-$HOME/.texitcoin}"
STATE="$HOME/.node-canary"
BASE="$STATE/baseline.env"
REFERENCE_HEIGHT_URL="${TXC_REF_HEIGHT_URL:-https://pool-hme.lovable.app/api/blocks/tip/height}"
STM_VECTOR="000000070000001f0302000000003e95ba80030000000002faf080050000000059682f00"
STM_ARGS='[{"output":2,"amount":"10.5"},{"output":3,"amount":"0.5"},{"output":5,"amount":"15.0"}]'

FAILS=0
WARNS=0
mkdir -p "$STATE" 2>/dev/null || true

c_red=$'\033[31m'; c_yel=$'\033[33m'; c_grn=$'\033[32m'; c_dim=$'\033[2m'; c_off=$'\033[0m'
ok()   { echo "  ${c_grn}PASS${c_off}  $*"; }
warn() { echo "  ${c_yel}WARN${c_off}  $*"; WARNS=$((WARNS+1)); }
bad()  { echo "  ${c_red}FAIL${c_off}  $*"; FAILS=$((FAILS+1)); }
info() { echo "  ${c_dim}....${c_off}  $*"; }
head_() { echo ""; echo "== $* =========================================================="; }

# --- locate the binaries -----------------------------------------------------
CLI=""
DAEMON=""
for d in "$HOME/texitcoin/src" /usr/local/bin /usr/bin "$HOME/texitcoin"; do
  [ -z "$CLI" ] && [ -x "$d/texitcoin-cli" ] && CLI="$d/texitcoin-cli"
  [ -z "$DAEMON" ] && [ -x "$d/texitcoind" ] && DAEMON="$d/texitcoind"
done
SRC="$HOME/texitcoin"

rpc() { # rpc <method> [args...]  -> stdout, non-zero on error
  [ -n "$CLI" ] || return 1
  "$CLI" -datadir="$DATADIR" "$@" 2>/dev/null
}
rpc_err() { # same but captures stderr text
  [ -n "$CLI" ] || { echo "no texitcoin-cli found"; return 1; }
  "$CLI" -datadir="$DATADIR" "$@" 2>&1
}
jnum() { # jnum <json> <key>
  echo "$1" | grep -o "\"$2\"[[:space:]]*:[[:space:]]*-\?[0-9]\+" | head -1 | grep -o -- '-\?[0-9]\+$'
}
jstr() {
  echo "$1" | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1
}

echo ""
echo "################################################################"
echo "#  TEXITcoin node canary $CANARY_VERSION -- Send To Many test node"
echo "#  host: $(hostname)   mode: $MODE   $(date -u '+%Y-%m-%d %H:%M:%S UTC')"
echo "################################################################"

# =============================================================== 1. BUILD ====
head_ "1. build identity"
if [ -n "$CLI" ] && [ -n "$DAEMON" ]; then
  ok "binaries present: $DAEMON / $CLI"
  info "built $(date -u -r "$DAEMON" '+%Y-%m-%d %H:%M UTC' 2>/dev/null || echo 'unknown')"
  info "size  $(du -h "$DAEMON" 2>/dev/null | cut -f1)"
else
  bad "texitcoind / texitcoin-cli not found -- the build did not finish"
  info "expected in $SRC/src"
fi

if [ -d "$SRC/.git" ]; then
  BR=$(git -C "$SRC" rev-parse --abbrev-ref HEAD 2>/dev/null)
  CM=$(git -C "$SRC" rev-parse --short HEAD 2>/dev/null)
  SUB=$(git -C "$SRC" log -1 --pretty=%s 2>/dev/null)
  info "branch $BR @ $CM -- $SUB"
  if [ "$BR" = "feature/send-to-many" ]; then
    ok "on the Send To Many branch"
  else
    warn "branch is '$BR', expected feature/send-to-many"
  fi
  DIRTY=$(git -C "$SRC" status --porcelain 2>/dev/null | wc -l)
  [ "$DIRTY" -eq 0 ] && ok "working tree clean (binary matches committed source)" \
                      || warn "$DIRTY uncommitted file(s) -- binary may not match the commit"
  # the patch touches these; confirm the source really carries type 7
  HITS=0
  for pat in "MSC_TYPE_SEND_TO_MANY" "logicMath_SendToMany" "CreatePayload_SendToMany" "FEATURE_SEND_TO_MANY"; do
    if grep -rqs "$pat" "$SRC/src/omnicore" "$SRC/src" 2>/dev/null; then HITS=$((HITS+1)); fi
  done
  [ "$HITS" -eq 4 ] && ok "all 4 Send To Many source markers present" \
                     || bad "only $HITS/4 Send To Many source markers found -- patch not fully applied"
else
  warn "no git checkout at $SRC -- cannot verify which source built this binary"
fi

# ===================================================== 2. DAEMON + RPC ======
head_ "2. daemon + RPC"
PIDS=$(pgrep -f 'texitcoind' 2>/dev/null | wc -l)
if [ "$PIDS" -gt 0 ]; then
  ok "texitcoind running ($PIDS process)"
  ETIME=$(ps -o etime= -p "$(pgrep -f texitcoind | head -1)" 2>/dev/null | tr -d ' ')
  info "uptime $ETIME"
  RSS=$(ps -o rss= -p "$(pgrep -f texitcoind | head -1)" 2>/dev/null | tr -d ' ')
  [ -n "$RSS" ] && info "memory $(( RSS / 1024 )) MB resident"
else
  bad "texitcoind is NOT running"
  info "start it with:  $DAEMON -datadir=$DATADIR -daemon -server"
fi

NET=$(rpc getnetworkinfo)
CHAIN=$(rpc getblockchaininfo)
if [ -n "$CHAIN" ]; then
  ok "RPC answering"
else
  ERRTXT=$(rpc_err getblockchaininfo | head -3)
  bad "RPC not answering: ${ERRTXT:-no response}"
fi

SUBVER=$(jstr "$NET" subversion)
CONNS=$(jnum "$NET" connections)
[ -n "$SUBVER" ] && info "subversion $SUBVER"
if [ -n "$CONNS" ]; then
  if [ "$CONNS" -ge 4 ]; then ok "$CONNS peer connections"
  elif [ "$CONNS" -ge 1 ]; then warn "only $CONNS peer connection(s) -- still finding peers?"
  else bad "0 peers -- the node is isolated from the network"; fi
fi

# ========================================================== 3. CHAIN SYNC ===
head_ "3. chain sync + consensus"
BLOCKS=$(jnum "$CHAIN" blocks)
HEADERS=$(jnum "$CHAIN" headers)
NETNAME=$(jstr "$CHAIN" chain)
BESTHASH=$(jstr "$CHAIN" bestblockhash)
[ -n "$NETNAME" ] && info "network: $NETNAME"
[ -n "$BLOCKS" ] && info "blocks $BLOCKS / headers ${HEADERS:-?}"

if [ -n "$BLOCKS" ] && [ -n "$HEADERS" ]; then
  GAP=$(( HEADERS - BLOCKS ))
  if [ "$GAP" -le 2 ]; then ok "fully synced with its own headers (gap $GAP)"
  elif [ "$GAP" -le 2000 ]; then warn "still catching up, $GAP blocks behind headers"
  else info "initial sync in progress, $GAP blocks to go (expected on a fresh box)"; fi
fi

REF=$(curl -fsS --max-time 10 "$REFERENCE_HEIGHT_URL" 2>/dev/null | tr -dc '0-9')
if [ -n "$REF" ] && [ -n "$BLOCKS" ]; then
  DELTA=$(( REF - BLOCKS ))
  [ "$DELTA" -lt 0 ] && DELTA=$(( -DELTA ))
  info "public reference tip $REF (delta $DELTA)"
  if [ "$DELTA" -le 3 ]; then
    ok "height agrees with the public network"
    # same height AND same hash = this build computes the same chain as everyone else
    if [ -n "$BESTHASH" ]; then
      REFHASH=$(curl -fsS --max-time 10 "https://pool-hme.lovable.app/api/block-height/$BLOCKS" 2>/dev/null | tr -dc '0-9a-f')
      if [ -n "$REFHASH" ] && [ ${#REFHASH} -ge 60 ]; then
        if [ "$REFHASH" = "$BESTHASH" ]; then
          ok "block hash at $BLOCKS matches the network -- IN CONSENSUS"
        else
          bad "block hash at $BLOCKS DIFFERS from the network -- this node forked"
          info "ours $BESTHASH"
          info "net  $REFHASH"
        fi
      else
        info "reference hash unavailable, height-only comparison"
      fi
    fi
  elif [ "$DELTA" -le 200 ]; then
    warn "$DELTA blocks off the public tip -- normal mid-sync, re-run in a few minutes"
  else
    info "$DELTA blocks off the public tip -- still doing initial sync"
  fi
else
  info "no public reference height available (offline check)"
fi

# ================================================================ 4. OMNI ===
head_ "4. Omni layer"
OMNI=$(rpc omni_getinfo)
if [ -n "$OMNI" ]; then
  OV=$(jstr "$OMNI" omnicoreversion)
  OB=$(jnum "$OMNI" block)
  ok "omni_getinfo answering (Omni ${OV:-?})"
  [ -n "$OB" ] && info "Omni parser at block $OB"
  if [ -n "$OB" ] && [ -n "$BLOCKS" ]; then
    OGAP=$(( BLOCKS - OB ))
    [ "$OGAP" -lt 0 ] && OGAP=$(( -OGAP ))
    if [ "$OGAP" -le 2 ]; then ok "Omni parser is level with the chain tip"
    elif [ "$OGAP" -le 5000 ]; then warn "Omni parser $OGAP blocks behind the tip"
    else info "Omni parser $OGAP blocks behind -- still processing history"; fi
  fi
else
  bad "omni_getinfo failed -- Omni is not initialised on this node"
fi

# ================================================== 5. SEND TO MANY WIRING ==
head_ "5. Send To Many (the whole point)"

WLOADED=$(rpc getwalletinfo 2>/dev/null)
[ -z "$WLOADED" ] && info "no wallet loaded -- bulk-send dispatch untestable until: ./src/texitcoin-cli createwallet wallet"

# Registration is judged by DISPATCH, not by help text: Core 0.25 hides help
# for wallet-scoped RPCs even when the command exists and answers. The dry
# bulk-send attempt doubles as the pre-activation guard test -- the node must
# refuse it, and any refusal short of "unknown command" proves the RPC is alive.
if [ "$NETNAME" = "main" ]; then
  DRY=$(rpc_err omni_sendtomany "TbMELaDs18ANkWuF21iCWt7xYdmWx7GS9S" 1 '[{"address":"Thaotk2YYWiXG9EozU5dqABx7EB26fGrhn","amount":"0.00000001"}]' | head -3)
  if echo "$DRY" | grep -qi 'unknown command\|method not found'; then
    bad "omni_sendtomany missing -- patch compiled but RPC not registered?"
  elif echo "$DRY" | grep -qi 'No wallet is loaded'; then
    info "omni_sendtomany is registered (refused: no wallet loaded)"
  elif echo "$DRY" | grep -qi 'not yet activated\|not activated\|feature'; then
    ok "RPC omni_sendtomany is registered and the pre-activation guard works (feature error)"
  elif echo "$DRY" | grep -qi 'insufficient\|invalid address\|not found\|no tokens\|sender'; then
    ok "RPC omni_sendtomany is registered and dispatches (refused on funds/address)"
    info "guard detail: ${DRY:0:90}"
  elif ! echo "$DRY" | grep -qi 'error'; then
    bad "a pre-activation bulk send was ACCEPTED -- feature 19 must NOT be active on mainnet"
  else
    warn "unexpected reply to a pre-activation bulk send: ${DRY:0:120}"
  fi
else
  ok "bulk-send dispatch test skipped on $NETNAME (only run on mainnet)"
fi

PLHELP=$(rpc_err help omni_createpayload_sendtomany)
echo "$PLHELP" | grep -qi 'omni_createpayload_sendtomany' && \
  { echo "$PLHELP" | grep -qi '^help:' && bad "omni_createpayload_sendtomany not known" || ok "RPC omni_createpayload_sendtomany is registered"; } || \
  bad "omni_createpayload_sendtomany missing"

# byte-exact encoder proof: property 31, three receivers. Pure function, no state.
ENC=$(rpc omni_createpayload_sendtomany 31 "$STM_ARGS" | tr -d '[:space:]"')
if [ -n "$ENC" ]; then
  if [ "$ENC" = "$STM_VECTOR" ]; then
    ok "encoder output matches the upstream test vector byte-for-byte"
  else
    bad "encoder output WRONG -- the wire format does not match upstream"
    info "got      $ENC"
    info "expected $STM_VECTOR"
  fi
else
  bad "encoder returned nothing: $(rpc_err omni_createpayload_sendtomany 31 "$STM_ARGS" | head -2)"
fi

# feature gate: must stay CLOSED on mainnet until operators are coordinated
ACT=$(rpc omni_getactivations)
if [ -n "$ACT" ]; then
  COMPLETED=$(echo "$ACT" | grep -c 'featureid' )
  PEND=$(echo "$ACT" | sed -n '/pendingactivations/,/]/p' | grep -c 'featureid')
  if echo "$ACT" | sed -n '/completedactivations/,/]/p' | grep -q '"featureid": *19'; then
    if [ "$NETNAME" = "main" ]; then
      warn "feature 19 (Send To Many) is ACTIVATED on mainnet -- every operator must already be upgraded"
    else
      ok "feature 19 active on $NETNAME (expected on regtest/testnet)"
    fi
  else
    ok "feature 19 gate is CLOSED -- no bulk send can be broadcast yet (correct)"
  fi
  [ "$PEND" -gt 0 ] && warn "$PEND pending activation(s) queued -- check them before they trigger" \
                     || info "no pending activations"
  info "$COMPLETED activation record(s) total"
else
  warn "omni_getactivations did not answer"
fi

# (the dispatch-based registration + guard test lives in section 5 above)

# ========================================================= 6. HOST HEALTH ===
head_ "6. host health"
DISKPCT=$(df -P "$DATADIR" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5}')
DISKAV=$(df -Ph "$DATADIR" 2>/dev/null | awk 'NR==2{print $4}')
if [ -n "$DISKPCT" ]; then
  if [ "$DISKPCT" -lt 80 ]; then ok "disk ${DISKPCT}% used, $DISKAV free"
  elif [ "$DISKPCT" -lt 92 ]; then warn "disk ${DISKPCT}% used, only $DISKAV free"
  else bad "disk ${DISKPCT}% used -- the node will stop on a full disk"; fi
fi
MEMAV=$(awk '/MemAvailable/{printf "%d", $2/1024}' /proc/meminfo 2>/dev/null)
SWAPT=$(awk '/SwapTotal/{printf "%d", $2/1024}' /proc/meminfo 2>/dev/null)
[ -n "$MEMAV" ] && info "memory available ${MEMAV} MB, swap ${SWAPT:-0} MB"
[ "${SWAPT:-0}" -lt 1024 ] && warn "less than 1 GB swap -- the compile and reindex can run short" || true
info "load$(cut -d' ' -f1-3 /proc/loadavg 2>/dev/null | sed 's/^/ /')"
OOM=$(grep -ci 'killed process' /var/log/kern.log 2>/dev/null)
OOM=$(( ${OOM:-0} + 0 ))
[ "$OOM" -gt 0 ] && warn "$OOM out-of-memory kill(s) recorded on this host" || ok "no out-of-memory kills"

DBG="$DATADIR/debug.log"
if [ -f "$DBG" ]; then
  RECENT=$(tail -n 4000 "$DBG" 2>/dev/null)
  ERRS=$(echo "$RECENT" | grep -ci 'ERROR:' || true)
  SHUT=$(echo "$RECENT" | grep -ci 'Aborted block database rebuild\|Error opening block database\|Assertion failed' || true)
  [ "${SHUT:-0}" -gt 0 ] && bad "$SHUT fatal database/assert line(s) in recent debug.log" \
                          || { [ "${ERRS:-0}" -gt 20 ] && warn "$ERRS ERROR lines in the last 4000 log lines" || ok "recent debug.log is clean (${ERRS:-0} ERROR lines)"; }
else
  info "no debug.log yet at $DBG"
fi

# ============================================================ 7. BASELINE ===
head_ "7. baseline"
if [ "$MODE" = "BASELINE" ]; then
  { echo "BASE_TIME=$(date -u +%s)"
    echo "BASE_BLOCKS=${BLOCKS:-0}"
    echo "BASE_OMNIBLOCK=${OB:-0}"
    echo "BASE_SUBVER=${SUBVER:-unknown}"
  } > "$BASE"
  ok "baseline written to $BASE"
elif [ -f "$BASE" ]; then
  # shellcheck disable=SC1090
  . "$BASE"
  AGE=$(( $(date -u +%s) - ${BASE_TIME:-0} ))
  info "baseline is $(( AGE / 60 )) min old (blocks ${BASE_BLOCKS:-?}, omni ${BASE_OMNIBLOCK:-?})"
  if [ -n "${BLOCKS:-}" ] && [ -n "${BASE_BLOCKS:-}" ]; then
    ADV=$(( BLOCKS - BASE_BLOCKS ))
    if [ "$ADV" -gt 0 ]; then ok "chain advanced $ADV block(s) since baseline"
    elif [ "$AGE" -gt 3600 ]; then bad "chain has not advanced in $(( AGE / 60 )) min -- node is stuck"
    elif [ "$AGE" -gt 900 ]; then warn "no new block in $(( AGE / 60 )) min (possible, TXC blocks are not instant)"
    else info "no new block yet, baseline is young"; fi
  fi
  [ -n "${BASE_SUBVER:-}" ] && [ -n "${SUBVER:-}" ] && [ "$BASE_SUBVER" != "$SUBVER" ] && \
    warn "subversion changed since baseline: $BASE_SUBVER -> $SUBVER" || true
else
  info "no baseline yet -- run:  curl -fsSL https://core.honest.money/install/node-canary.sh | bash -s BASELINE"
fi

# ============================================================== VERDICT =====
echo ""
echo "################################################################"
if [ "$FAILS" -gt 0 ]; then
  echo "#  ${c_red}VERDICT: FAIL${c_off}  ($FAILS failure(s), $WARNS warning(s))"
  echo "#  Copy this whole output into the chat and we will fix it."
elif [ "$WARNS" -gt 0 ]; then
  echo "#  ${c_grn}VERDICT: PASS${c_off} with $WARNS warning(s)"
  echo "#  Nothing is broken. Warnings are usually 'still syncing'."
else
  echo "#  ${c_grn}VERDICT: ALL GREEN${c_off}"
  echo "#  Patched node built, running, in consensus, gate still closed."
fi
echo "#  canary $CANARY_VERSION   $(date -u '+%H:%M:%S UTC')"
echo "################################################################"
echo ""

if [ "$MODE" = "WATCH" ]; then
  echo "WATCH mode: re-checking every 60s for $WATCH_MINS minutes. Ctrl-C to stop."
  END=$(( $(date -u +%s) + WATCH_MINS * 60 ))
  while [ "$(date -u +%s)" -lt "$END" ]; do
    sleep 60
    B=$(rpc getblockchaininfo)
    O=$(rpc omni_getinfo)
    echo "$(date -u '+%H:%M:%S') blocks=$(jnum "$B" blocks) headers=$(jnum "$B" headers) omni=$(jnum "$O" block) peers=$(jnum "$(rpc getnetworkinfo)" connections)"
  done
  echo "WATCH finished."
fi

[ "$FAILS" -gt 0 ] && exit 1
exit 0
