#!/usr/bin/env bash
# demo/portable-session.sh — one payer session against any running Thunderpolt
# stack, local or hosted: only SITE_URL/RPC_URL change. Follows
# docs/agents/PAYING.md and uses ONLY public endpoints (site /meta discovery,
# dispenser, facilitator, explorer, chain RPC) — no repo binary, no private
# engine URLs, no manual /epoch/close. That makes it runnable from a machine
# that has never built this repo (needs only bash, curl, jq, openssl, cast).
#
# What it demonstrates, end to end:
#   1. OPEN CHANNEL (onchain): approve + deposit escrow, binding a hot
#      session key — the payer's only setup transactions.
#   2. APPLICATION SESSION (offchain): N cumulative EIP-712 vouchers paid
#      into the reference site's cheapest route. Zero onchain transactions
#      per payment.
#   3. SETTLEMENT (onchain, automatic): the operator's epoch cycle posts a
#      root and the merchant's claim lands; verified here by the hub's
#      onchain epochClaimed(merchant) cursor advancing by at least this
#      session's total. A global "did anything settle" check can false-pass
#      on a shared stack; the cursor delta cannot.
#
# Vocabulary: this runs one APPLICATION SESSION over a payer CHANNEL.
# Finishing the session settles value onchain via the operator's epoch
# cycle; the channel stays funded and reusable (rerun with REUSE_DIR to
# prove it). Channel exit/withdrawal is a separate lifecycle not exercised
# here.
#
# Requires a stack with AUTOMATIC epoch + claim timers (the sp7-stack
# defaults). On a stack started with EPOCH_INTERVAL=0/CLAIM_INTERVAL=0 the
# settlement wait in step 9 will time out by design.
set -euo pipefail
export PATH="$HOME/.foundry/bin:$PATH"

SITE_URL=${SITE_URL:-http://127.0.0.1:8594}
RPC_URL=${RPC_URL:-http://127.0.0.1:8590}
N_PAYMENTS=${N_PAYMENTS:-10}
HUB_INDEX=${HUB_INDEX:-0}
SETTLE_DEADLINE_SECS=${SETTLE_DEADLINE_SECS:-420}
# REUSE_DIR: persist the wallet there and, on the next run, skip funding and
# deposit entirely — the second application session reuses the funded channel.
REUSE_DIR=${REUSE_DIR:-}

WORK="$(mktemp -d "${TMPDIR:-/tmp}/tp-session.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT   # REUSE_DIR (if set) is NOT cleaned — it holds the reusable channel keys
step() { printf '\n==== %s ====\n' "$*"; }
num() { awk '{print $1}'; }

step "1. discover platform from the site's /meta (the site, not the facilitator — it carries the full handout)"
META=$(curl -sf "$SITE_URL/meta")
CHAIN_ID=$(jq -r .chainId <<<"$META")
USDC=$(jq -r .usdc <<<"$META")
HUB=$(jq -r --argjson i "$HUB_INDEX" '.hubs[$i].hub' <<<"$META")
DISPENSER_URL=$(jq -r .urls.dispenser <<<"$META")
EXPLORER_URL=$(jq -r .urls.explorer <<<"$META")
FACILITATOR_URL=$(jq -r .urls.facilitator <<<"$META")
echo "chainId=$CHAIN_ID usdc=$USDC hub[$HUB_INDEX]=$HUB dispenser=$DISPENSER_URL explorer=$EXPLORER_URL"

KEYDIR="$WORK"
REUSING=""
if [ -n "$REUSE_DIR" ]; then
  mkdir -p "$REUSE_DIR" && chmod 700 "$REUSE_DIR"
  KEYDIR="$REUSE_DIR"
  [ -s "$REUSE_DIR/payer.key" ] && [ -s "$REUSE_DIR/payer.addr" ] && REUSING=1
fi

if [ -n "$REUSING" ]; then
  step "2. REUSE funded channel: loading persisted wallet (no new funding, no new deposit)"
  PAYER=$(cat "$KEYDIR/payer.addr"); SESSION_ADDR=$(cat "$KEYDIR/session.addr")
  echo "payer=$PAYER session=$SESSION_ADDR (channel opened in a previous run)"
else
  step "2. wallet: root key (funds) + hot session key (vouchers) — devnet keys, never printed"
  W="$(cast wallet new)"
  PAYER=$(grep -oE '0x[0-9a-fA-F]{40}' <<<"$W" | head -1)
  grep -oE '0x[0-9a-fA-F]{64}' <<<"$W" | head -1 > "$KEYDIR/payer.key" && chmod 600 "$KEYDIR/payer.key"
  S="$(cast wallet new)"
  SESSION_ADDR=$(grep -oE '0x[0-9a-fA-F]{40}' <<<"$S" | head -1)
  grep -oE '0x[0-9a-fA-F]{64}' <<<"$S" | head -1 > "$KEYDIR/session.key" && chmod 600 "$KEYDIR/session.key"
  echo "$PAYER" > "$KEYDIR/payer.addr"; echo "$SESSION_ADDR" > "$KEYDIR/session.addr"
  unset W S
  echo "payer=$PAYER session=$SESSION_ADDR"
fi

if [ -n "$REUSING" ]; then
  step "3-5. skipped: channel already funded and open (fund once, pay many)"
else
step "3. get funded (dispenser), then VERIFY balances before signing anything"
curl -s -X POST "$DISPENSER_URL/fund" -H 'content-type: application/json' -d "{\"address\":\"$PAYER\"}" | jq -c 'del(.pol_tx,.usdc_tx)'
DEADLINE=$(( $(date +%s) + 90 ))
while :; do
  POL=$(cast balance "$PAYER" --rpc-url "$RPC_URL" 2>/dev/null || echo 0)
  BAL=$(cast call "$USDC" 'balanceOf(address)(uint256)' "$PAYER" --rpc-url "$RPC_URL" 2>/dev/null | num || echo 0)
  [ "${POL:-0}" != "0" ] && [ "${BAL:-0}" != "0" ] && break
  [ "$(date +%s)" -lt "$DEADLINE" ] || { echo "funding never confirmed (pol=$POL usdc=$BAL)"; exit 1; }
  sleep 2
done
echo "funded: $BAL USDC base units + gas"

step "4. OPEN CHANNEL: onchain approve + deposit (binds the session key). The payer's only setup txs."
DEPOSIT=$(( BAL / 2 ))
cast send "$USDC" 'approve(address,uint256)' "$HUB" "$DEPOSIT" \
     --private-key "$(cat "$KEYDIR/payer.key")" --rpc-url "$RPC_URL" >/dev/null
DEP_TX=$(cast send "$HUB" 'deposit(address,address,uint256)' "$PAYER" "$SESSION_ADDR" "$DEPOSIT" \
     --private-key "$(cat "$KEYDIR/payer.key")" --rpc-url "$RPC_URL" --json | jq -r .transactionHash)
echo "deposit tx: $DEP_TX ($DEPOSIT base units escrowed)"

step "5. wait for the deposit to finalize (explorer feed — engine URLs are private by design)"
DEADLINE=$(( $(date +%s) + 120 )); SEEN=""
while [ -z "$SEEN" ] && [ "$(date +%s)" -lt "$DEADLINE" ]; do
  curl -s "$EXPLORER_URL/api/address/$PAYER" \
    | jq -e '[.chainEvents[]?|select(.kind=="deposit" and .status=="final")]|length>0' >/dev/null 2>&1 && SEEN=1 || sleep 2
done
[ -n "$SEEN" ] && echo "channel funded and final onchain" || { echo "deposit never confirmed"; exit 1; }
fi

step "6. merchant 402: price + payTo from the site's paid route"
CACHED_402=$(curl -s "$SITE_URL/api/paid/time")
CH=$(jq -c --arg hub "$HUB" '.accepts[]|select(.scheme=="batch-settlement")|select((.extra.contract|ascii_downcase)==($hub|ascii_downcase))' <<<"$CACHED_402")
PRICE=$(jq -r .maxAmountRequired <<<"$CH"); MERCHANT=$(jq -r .payTo <<<"$CH")
RESOURCE=$(jq -r .resource <<<"$CH")
echo "resource=$RESOURCE price=$PRICE payTo=$MERCHANT"

step "7. bootstrap channel state (ONE personalized quote) + record the settlement baseline"
Q=$(curl -sf -X POST "$FACILITATOR_URL/quote" -H 'content-type: application/json' \
    -d "$(jq -nc --arg r "$RESOURCE" --arg pr "$PRICE" --arg me "$MERCHANT" --arg p "$PAYER" --arg h "$HUB" \
         '{resource:$r, price:$pr, payTo:$me, payer:$p, hub:$h}')")
CHQ=$(jq -c --arg hub "$HUB" '.accepts[]|select(.scheme=="batch-settlement")|select((.extra.contract|ascii_downcase)==($hub|ascii_downcase))' <<<"$Q")
EPOCH=$(jq -r '.extra["thunderpolt-hub/v1"].epoch' <<<"$CHQ")
OLD=$(jq -r '.extra["thunderpolt-hub/v1"].cumulative // "0"' <<<"$CHQ")
BASE_CLAIMED=$(cast call "$HUB" 'epochClaimed(address)(uint256)' "$MERCHANT" --rpc-url "$RPC_URL" | num)
echo "channel epoch=$EPOCH; starting cumulative=$OLD; merchant's onchain claim cursor baseline=$BASE_CLAIMED"

step "8. APPLICATION SESSION: stream $N_PAYMENTS offchain payments (cumulative vouchers — ZERO onchain txs here)"
SESSION_START=$OLD
for i in $(seq 1 "$N_PAYMENTS"); do
  NEW=$((OLD + PRICE))
  PAYID="pay_session_$(openssl rand -hex 12)"
  cat > "$WORK/voucher.json" <<EOF
{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],
"Voucher":[{"name":"payer","type":"address"},{"name":"merchant","type":"address"},{"name":"epoch","type":"uint64"},{"name":"cumulative","type":"uint256"}]},
"primaryType":"Voucher",
"domain":{"name":"ThunderpoltHub","version":"1","chainId":$CHAIN_ID,"verifyingContract":"$HUB"},
"message":{"payer":"$PAYER","merchant":"$MERCHANT","epoch":"$EPOCH","cumulative":"$NEW"}}
EOF
  VSIG=$(cast wallet sign --private-key "$(cat "$KEYDIR/session.key")" --data --from-file "$WORK/voucher.json")
  XP=$(jq -nc --arg net "eip155:$CHAIN_ID" --arg p "$PAYER" --arg m "$MERCHANT" --arg e "$EPOCH" \
       --arg c "$NEW" --arg sig "$VSIG" --arg hub "$HUB" --arg id "$PAYID" \
   '{x402Version:2,scheme:"batch-settlement",network:$net,
     payload:{binding:"thunderpolt-hub/v1",voucher:{payer:$p,merchant:$m,epoch:$e,cumulative:$c},signature:$sig,hub:$hub},
     extensions:{"payment-identifier":{id:$id}}}' | base64 | tr -d '\n')
  # First payment only: the hub's own deposit watcher can lag the explorer's
  # confirmation by a few seconds; a too-early channel payment declines
  # politely (PAYING.md) — bounded retry instead of failing the run. The
  # decline reads as invalid-signature because the hub has no session-key
  # binding for the payer yet.
  TRIES=1; [ "$i" = 1 ] && TRIES=15
  for attempt in $(seq 1 "$TRIES"); do
    CODE=$(curl -s -o "$WORK/pay.json" -w '%{http_code}' "$RESOURCE" -H "X-PAYMENT: $XP")
    [ "$CODE" = 200 ] && break
    [ "$attempt" -lt "$TRIES" ] && sleep 3
  done
  [ "$CODE" = 200 ] || { echo "payment $i FAILED http=$CODE"; cat "$WORK/pay.json"; exit 1; }
  OLD=$NEW
  echo "  payment $i/$N_PAYMENTS: 200 OK  cumulative=$NEW"
done
SESSION_TOTAL=$(( OLD - SESSION_START ))

step "9. FINISH APPLICATION SESSION: await onchain settlement (operator epoch cycle; cursor-delta check)"
echo "waiting for epochClaimed($MERCHANT) to advance >= $SESSION_TOTAL over baseline $BASE_CLAIMED ..."
DEADLINE=$(( $(date +%s) + SETTLE_DEADLINE_SECS ))
while :; do
  CLAIMED=$(cast call "$HUB" 'epochClaimed(address)(uint256)' "$MERCHANT" --rpc-url "$RPC_URL" | num)
  DELTA=$(( CLAIMED - BASE_CLAIMED ))
  [ "$DELTA" -ge "$SESSION_TOTAL" ] && break
  [ "$(date +%s)" -lt "$DEADLINE" ] || { echo "settlement not observed in ${SETTLE_DEADLINE_SECS}s (cursor delta=$DELTA)"; exit 1; }
  sleep 10
done
echo "settled: claim cursor $BASE_CLAIMED -> $CLAIMED (delta $DELTA >= session total $SESSION_TOTAL)"
echo "(the delta can exceed this session's total if other payers' accruals settled in the same epoch — the cursor is per-merchant)"
echo
echo "SESSION COMPLETE. Invariant shown: ZERO onchain transactions per payment."
echo "Chain txs this run: 2 payer setup (approve+deposit) + the operator's epoch root + one merchant claim (auto), regardless of $N_PAYMENTS payments."
echo "The channel remains funded and reusable; exit/withdrawal is a separate lifecycle action."
