#!/bin/bash
# ============================================================================
# BTX Keeper — one-command installer (macOS, Apple silicon)
#
#   Share your Mac with the network. Keep the chain verifiable while you sleep.
#
# What this sets up, in plain words:
#   - a small BTX node in ~/.btx-keeper (pruned: it does NOT keep the whole
#     chain — target ~10 GB, no wallet, no keys, RPC on localhost only)
#   - it follows the chain via signed confirmations (trusted mirror) and
#     SERVES those confirmations back to the network — the single scarcest
#     resource BTX peers need right now
#   - a launchd agent that runs it while your Mac is on power, pauses it on
#     battery, and never blocks your display from sleeping
#   - a watchdog that stops the node cleanly and tells you if it ever
#     misbehaves (it never force-kills, never auto-restarts)
#
# What you get out of it: the network stays verifiable, and that happened on
# your machine. That is the whole deal — there is no payment, no token, and
# nobody should tell you otherwise.
#
# Uninstall (also printed at the end): ~/.btx-keeper/uninstall.sh
#
# Run it:  curl -fsSL https://easybtx.com/install-btx-keeper.sh | bash
#          (or download it, read it, then: bash install-btx-keeper.sh)
# ============================================================================
set -euo pipefail

# Resolve where THIS script lives BEFORE anything cds elsewhere — computed
# later it resolved relative to whatever directory step 4 left us in, so a
# `./install-btx-keeper.sh` invocation aborted at the copy step.
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# Under `curl ... | bash` there is no script file on disk: $0 is "bash", so
# SCRIPT_DIR resolves to whatever directory the operator happened to be sitting
# in. Only trust sibling copies when THIS installer is really there, otherwise
# a stray uninstall.sh in the current directory would be installed as the
# Keeper's uninstaller.
HAVE_SIBLINGS=0
[ -f "$SCRIPT_DIR/install-btx-keeper.sh" ] && HAVE_SIBLINGS=1

# Where the runtime scripts come from when they are not sitting next to this
# file. Served from easybtx.com, NOT from raw.githubusercontent.com: the source
# repo is private, so every raw URL 404s for everyone outside the org, and this
# installer was published telling the public to `git clone` exactly that repo.
# Same fix the Node Guardian installer already carries. Override to mirror it.
KEEPER_BASE_URL="${KEEPER_BASE_URL:-https://easybtx.com}"

KEEPER_HOME="$HOME/.btx-keeper"
PLIST="$HOME/Library/LaunchAgents/com.btx.keeper.plist"
# The RELEASED engine this Keeper installs. v0.33.3 (2026-08-17) is the first
# OFFICIAL release carrying both keeper-critical fixes — 0ece8ef4 (mirrors may
# serve GETMMATTEST) and 1d73e70b (pruned+snapshot unclean-shutdown safety) —
# plus the ASERT floor at 191714. Official sealed binaries exist for it, so
# the installer prefers them (seconds, no toolchain) and source-builds the
# SAME TAG only as a fallback. Never track a branch again: the old
# pr/0.33.3-network-stability branch is merged upstream and can vanish or
# move at any time; a tag stays put.
RELEASE_TAG="v0.33.3"
# Primary engine: the fleet's own FULLY-STATIC arm64 build — byte-identical
# to the binaries inside the BTX Node 0.6.10 app (tag commit 6053ef71), zero
# external dylibs (verified with an exec probe + otool, 2026-08-17). Upstream's
# official arm64 asset is deliberately NOT used: it is Homebrew-linked and
# refuses to start on exactly the brew-less Macs this installer exists for.
# When the engine moves, publish a new static tarball and move URL + sha
# TOGETHER.
ENGINE_URL="https://github.com/MendeMatthias/EasyBTX-releases/releases/download/node-v0.6.10/btxd-0.33.3-arm64-static.tar.gz"
ENGINE_SHA256="8e3bf344f7c08b7184e94a2ad0d2dd52e97da6b2ec8c9538f45df75ef1f8d52a"
# The source fallback still refuses any tree without the serving fix.
MIN_SERVE_COMMIT="0ece8ef4"
SNAPSHOT_TAG="assumeutxo-191266"   # newest consensus snapshot at write time

say()  { printf '\n\033[1m%s\033[0m\n' "$*"; }
fail() { printf '\n\033[31m%s\033[0m\n' "$*" >&2; exit 1; }

# ── 0. Hardware + prerequisites ─────────────────────────────────────────────
say "BTX Keeper installer"
[ "$(uname -s)" = "Darwin" ] || fail "This installer is macOS-only for now."
[ "$(uname -m)" = "arm64" ]  || fail "Apple-silicon Macs only for now (arm64)."

FREE_GB=$(df -g "$HOME" | tail -1 | awk '{print $4}')
[ "$FREE_GB" -ge 25 ] || fail "Need at least 25 GB free (found ${FREE_GB} GB): ~10 GB node + snapshot + headroom."

command -v python3 >/dev/null || fail "python3 is required."

mkdir -p "$KEEPER_HOME"/{bin,logs,bootstrap,src}

# ── 1+2. The engine: the pinned static binaries, source build as fallback ───
say "1-2/6 installing the btxd engine ($RELEASE_TAG)"
ENGINE_OK=0
TMPD=$(mktemp -d)
if curl -fSL --max-time 600 -o "$TMPD/engine.tar.gz" "$ENGINE_URL" 2>/dev/null; then
    got=$(shasum -a 256 "$TMPD/engine.tar.gz" | awk '{print $1}')
    if [ "$got" = "$ENGINE_SHA256" ]; then
        tar xzf "$TMPD/engine.tar.gz" -C "$TMPD"   # flat tar: btxd, btx-cli, README.txt
        cp "$TMPD/btxd" "$TMPD/btx-cli" "$KEEPER_HOME/bin/"
        chmod +x "$KEEPER_HOME/bin/btxd" "$KEEPER_HOME/bin/btx-cli"
        # Acceptance is an EXEC PROBE, not just the checksum: it catches a bad
        # download AND any future linkage regression in this asset the same
        # way it caught upstream's Homebrew-linked one. Apple silicon refuses
        # unsigned Mach-O — ad-hoc sign once if exec is refused, then retest.
        "$KEEPER_HOME/bin/btxd" -version >/dev/null 2>&1 \
            || codesign -s - -f "$KEEPER_HOME/bin/btxd" "$KEEPER_HOME/bin/btx-cli" 2>/dev/null || true
        if "$KEEPER_HOME/bin/btxd" -version >/dev/null 2>&1; then
            ENGINE_OK=1
            say "   static $RELEASE_TAG engine installed (sha256 verified, $(printf %.8s "$ENGINE_SHA256")…)"
        fi
    else
        say "   engine tarball failed its pinned checksum — falling back to source"
    fi
fi
rm -rf "$TMPD"

if [ "$ENGINE_OK" != 1 ]; then
    say "   building $RELEASE_TAG from source instead (~5 minutes on an M-class Mac)"
    xcode-select -p >/dev/null 2>&1 || fail "Xcode or the Command Line Tools are required for the source fallback (xcode-select --install) — or retry later when the official binary download is reachable."
    command -v git >/dev/null || fail "git is required for the source fallback."
    # Toolchain (userland, no admin, no Homebrew needed).
    python3 -m pip install --user --quiet cmake ninja pkgconf
    PYBIN="$(python3 -m site --user-base)/bin"
    export PATH="$PYBIN:$PATH"
    command -v cmake >/dev/null || fail "cmake did not land on PATH ($PYBIN)."

    if [ ! -d "$KEEPER_HOME/src/btx/.git" ]; then
        git clone --depth 200 --branch "$RELEASE_TAG" https://github.com/btxchain/btx.git "$KEEPER_HOME/src/btx"
    fi
    cd "$KEEPER_HOME/src/btx"
    git fetch --depth 200 origin tag "$RELEASE_TAG" 2>/dev/null || git fetch --depth 200 origin "$RELEASE_TAG" || true
    git checkout -q "$RELEASE_TAG"
    # The clone is shallow; once upstream grows past the depth, the ancestor
    # check can fail simply because the commit fell off the shallow horizon.
    # Deepen progressively before concluding the fix is genuinely absent.
    if ! git merge-base --is-ancestor "$MIN_SERVE_COMMIT" HEAD 2>/dev/null; then
        say "   deepening the shallow clone to find $MIN_SERVE_COMMIT…"
        git fetch --depth 2000 origin tag "$RELEASE_TAG" 2>/dev/null || true
        if ! git merge-base --is-ancestor "$MIN_SERVE_COMMIT" HEAD 2>/dev/null; then
            git fetch --unshallow origin tag "$RELEASE_TAG" 2>/dev/null || true
        fi
    fi
    git merge-base --is-ancestor "$MIN_SERVE_COMMIT" HEAD \
        || fail "Checked-out tree lacks the serving fix ($MIN_SERVE_COMMIT) — refusing to build a Keeper that cannot serve."
    BUILT_COMMIT=$(git rev-parse --short HEAD)

    make -C depends -j"$(sysctl -n hw.ncpu)" NO_QT=1 NO_WALLET=1 NO_ZMQ=1 NO_USDT=1 \
        HOST=arm64-apple-darwin >"$KEEPER_HOME/logs/depends.log" 2>&1 \
        || fail "depends build failed — see $KEEPER_HOME/logs/depends.log"
    cmake -B build --toolchain depends/arm64-apple-darwin/toolchain.cmake -G Ninja \
        -DCMAKE_BUILD_TYPE=Release -DENABLE_WALLET=OFF -DBUILD_TESTS=OFF \
        -DBUILD_TX=OFF -DBUILD_UTIL=OFF -DBTX_ENABLE_METAL=OFF \
        >"$KEEPER_HOME/logs/configure.log" 2>&1 \
        || fail "configure failed — see $KEEPER_HOME/logs/configure.log"
    nice -n 10 cmake --build build -j"$(sysctl -n hw.ncpu)" \
        >"$KEEPER_HOME/logs/build.log" 2>&1 \
        || fail "build failed — see $KEEPER_HOME/logs/build.log"
    cp build/bin/btxd build/bin/btx-cli "$KEEPER_HOME/bin/"
    say "   built btxd @ $BUILT_COMMIT"
fi

# ── 3. Config — the LIGHT profile, with the peer lines that actually matter ─
say "3/6 writing the node configuration"

# ── Authority-transition guard (field report on PR #105, 2026-08-17) ────────
# btxd PERSISTS which trusted-signer set validated the chain. Changing
# matmultrustedpubkey over an existing block database makes it refuse to start
# ("MatMul replay authority context changed… restart with -reindex"), and the
# -reindex-chainstate that message suggests DELETES the snapshot chainstate
# and then fails the same way — an unstartable node. A pruned keeper cannot
# full-reindex either (the bodies are gone). So when this reinstall would
# CHANGE the signer set over an existing chain, the only clean path is a
# fresh bootstrap — asked for explicitly, never sprung on the operator.
# Mechanism source-verified at engine branch tip 1932613f: context persisted
# + reconciled in src/node/blockstorage.cpp:1127-1153; the wipe-order trap
# (snapshot chainstate deleted before the check re-runs) in
# src/node/chainstate.cpp:189-198. The originating field report was later
# deleted — these cites are the durable record.
NEW_KEYS="03d90c148db37da28ce47ce15bade88a177728d663da4bc9ba765943b7d4e4f0aa
028995b25c887ee03eb53a41312d33c8eccf48f261ecf9e91fe2b1e8e50373258a"
if [ -f "$KEEPER_HOME/btx.conf" ] && { [ -d "$KEEPER_HOME/chainstate" ] || [ -d "$KEEPER_HOME/chainstate_snapshot" ]; }; then
    OLD_KEYS=$(grep '^matmultrustedpubkey=' "$KEEPER_HOME/btx.conf" | cut -d= -f2- | sort)
    if [ "$OLD_KEYS" != "$(sort <<<"$NEW_KEYS")" ]; then
        echo
        echo "This update changes the trusted signer keys, and your Keeper's existing"
        echo "chain database was validated under the OLD set. Keeping the chain would"
        echo "leave the node refusing to start (and btxd's own suggested repair path"
        echo "destroys the snapshot state — a known upstream bug)."
        echo
        echo "The clean fix is a fresh bootstrap: wipe the chain data (~10 GB), keep"
        echo "the verified snapshot, and re-sync from it. No keys or wallet live in a"
        echo "Keeper — nothing of yours is lost."
        echo
        printf "Type REBOOTSTRAP to wipe the chain data and continue (anything else aborts): "
        # Read the operator, not the script. Piped in with `curl ... | bash`,
        # stdin IS this file, so a plain `read` would swallow a line of source
        # and then abort a legitimate reinstall. Ask the terminal when there is
        # one; an EOF here must abort, never crash out under set -e.
        answer=""
        if [ -r /dev/tty ]; then read -r answer </dev/tty || answer=""
        else read -r answer || answer=""; fi
        [ "$answer" = "REBOOTSTRAP" ] || fail "Aborted — nothing was changed; your Keeper keeps its current key set (btx.conf untouched)."
        # Never delete under a live btxd (the uninstall lesson): stop it first.
        if pgrep -f "^$KEEPER_HOME/bin/btxd" >/dev/null; then
            say "   stopping the running node cleanly first (can take a few minutes)…"
            "$KEEPER_HOME/bin/btx-cli" -datadir="$KEEPER_HOME" stop 2>/dev/null || true
            for _ in $(seq 1 60); do
                pgrep -f "^$KEEPER_HOME/bin/btxd" >/dev/null || break
                sleep 5
            done
            pgrep -f "^$KEEPER_HOME/bin/btxd" >/dev/null \
                && fail "btxd is still flushing — wait a few minutes and run the installer again."
        fi
        say "   removing chain data for a clean re-bootstrap"
        rm -rf "$KEEPER_HOME/blocks" "$KEEPER_HOME/chainstate" "$KEEPER_HOME/chainstate_snapshot" "$KEEPER_HOME/shielded_state"
        rm -f "$KEEPER_HOME/.snapshot-loaded" "$KEEPER_HOME/mempool.dat" "$KEEPER_HOME/peers.dat" "$KEEPER_HOME/banlist.json" "$KEEPER_HOME/fee_estimates.dat"
    fi
fi

# Honest regeneration: the conf is REWRITTEN on every (re)install so upgrades
# can fix critical lines (the signer set below is exactly such a fix). Any
# previous conf is kept next to it for the operator to re-apply edits from.
if [ -f "$KEEPER_HOME/btx.conf" ]; then
    cp "$KEEPER_HOME/btx.conf" "$KEEPER_HOME/btx.conf.prev"
    say "   existing btx.conf kept at btx.conf.prev — re-apply personal edits from there"
fi
cat > "$KEEPER_HOME/btx.conf" <<'CONF'
# BTX Keeper — LIGHT profile (regenerated on reinstall; previous conf kept at btx.conf.prev)
server=1
daemon=0
disablewallet=1
dbcache=512
maxconnections=24
listen=1
# Pruned: keep ~10 GB of recent blocks. Serving signed confirmations does NOT
# need the whole chain — the confirmation store is separate and tiny.
prune=10000

# Trusted mirror: follow the chain via signed confirmations.
# BOTH signer keys are REQUIRED (with M=1): the two operators attest
# DIFFERENT blocks, so a single-key conf rejects roughly half of everything
# it receives (measured: 03d90c14 alone rejected 219 blocks on a parked
# datadir; both keys rejected zero). Keep in sync with btx-core's
# BTX_TRUSTED_ATTESTATION_PUBKEYS — a btx-core test cross-checks this file.
matmulvalidation=trusted
matmultrustedpubkey=03d90c148db37da28ce47ce15bade88a177728d663da4bc9ba765943b7d4e4f0aa
matmultrustedpubkey=028995b25c887ee03eb53a41312d33c8eccf48f261ecf9e91fe2b1e8e50373258a
matmultrustedthreshold=1

# THE POINT: serve confirmations back to the network.
matmulattestationserve=1

# Archive peers + the authority blessing (both lines per peer class are
# REQUIRED on a trusted mirror; addnode=manual is half the gate, noban is the
# other half, and bare whitelist would be incoming-only).
addnode=207.56.229.99:19335
addnode=185.204.25.227:19335
addnode=195.137.245.82:20982
addnode=node.btx.dev:19335
addnode=node.btxchain.org:19335
addnode=node.btx.tools:19335
whitelist=in,out,noban@207.56.229.99
whitelist=in,out,noban@185.204.25.227
whitelist=in,out,noban@195.137.245.82
whitelist=in,out,noban@146.190.179.86
whitelist=in,out,noban@206.189.253.106
whitelist=in,out,noban@164.90.246.229
CONF

# ── 4. Snapshot bootstrap material ──────────────────────────────────────────
say "4/6 downloading the consensus snapshot (~450 MB, verified)"
cd "$KEEPER_HOME/bootstrap"
ASSET_BASE="https://github.com/btxchain/btx/releases/download/$SNAPSHOT_TAG"
# The small files are always refetched (cheap, and a cached half-written
# SHA256SUMS would poison verification); only the big snapshot is cached.
for f in SHA256SUMS snapshot.manifest.json; do
    curl -fsSL -o "$f" "$ASSET_BASE/$f"
done
# Verify ONE named file against SHA256SUMS. Deliberately not `shasum -c` on
# the whole listing: the release lists assets we never download, and a batch
# check fails a perfectly good snapshot over their absence.
verify_asset() {
    local want got
    want=$(awk -v f="$1" '$2 == f || $2 == "*"f {print $1; exit}' SHA256SUMS)
    [ -n "$want" ] || fail "SHA256SUMS has no entry for $1"
    got=$(shasum -a 256 "$1" | awk '{print $1}')
    [ "$want" = "$got" ]
}
# A cached snapshot is verified BEFORE being trusted — an interrupted earlier
# run leaves a partial snapshot.dat that the old existence check accepted
# silently, and the node then fails loadtxoutset hours later.
if [ -f snapshot.dat ] && ! verify_asset snapshot.dat; then
    say "   cached snapshot.dat fails its checksum (interrupted download?) — refetching"
    rm -f snapshot.dat
fi
if [ ! -f snapshot.dat ]; then
    curl -fSL -o snapshot.dat "$ASSET_BASE/snapshot.dat"
fi
verify_asset snapshot.dat || { rm -f snapshot.dat; fail "snapshot checksum FAILED"; }

# ── 5. Runtime scripts + launchd agent ──────────────────────────────────────
say "5/6 installing the run wrapper, watchdog and launchd agent"
# Sibling file if there is one (the in-repo path, unchanged), otherwise fetch
# it from KEEPER_BASE_URL. The uninstaller is published under a prefixed name
# because site/public is one flat namespace; it still lands as uninstall.sh.
install_script() {
    local local_name="$1" remote_name="$2" dest="$KEEPER_HOME/$1" first=""
    if [ "$HAVE_SIBLINGS" = 1 ] && [ -f "$SCRIPT_DIR/$local_name" ]; then
        cp "$SCRIPT_DIR/$local_name" "$dest"
    else
        curl -fsSL --max-time 120 -o "$dest.new" "$KEEPER_BASE_URL/$remote_name" \
            || fail "Could not download $remote_name from $KEEPER_BASE_URL. Check this Mac can reach easybtx.com, then run the installer again."
        # Read the first line without a pipeline: `head | grep -q` can lose the
        # race under `set -o pipefail` (grep exits on match, head takes SIGPIPE)
        # and would reject a perfectly good download.
        first=$(head -1 "$dest.new" || true)
        case "$first" in
            '#!/bin/bash'*) ;;
            *) rm -f "$dest.new"
               fail "The download for $remote_name does not look like a script. Nothing was installed." ;;
        esac
        mv -f "$dest.new" "$dest"
    fi
    chmod +x "$dest"
}
install_script btx-keeper-run.sh      btx-keeper-run.sh
install_script btx-keeper-watchdog.sh btx-keeper-watchdog.sh
install_script uninstall.sh           btx-keeper-uninstall.sh
# A reinstall is a human decision: clear any watchdog pause marker so the
# run wrapper starts the node again.
rm -f "$KEEPER_HOME/keeper-paused"

cat > "$PLIST" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>com.btx.keeper</string>
  <key>ProgramArguments</key><array>
    <string>$KEEPER_HOME/btx-keeper-run.sh</string>
  </array>
  <key>RunAtLoad</key><true/>
  <!-- The wrapper supervises the node (power-aware) and exits only on
       uninstall; launchd never hard-restarts btxd itself. -->
  <key>KeepAlive</key><false/>
  <key>StandardOutPath</key><string>$KEEPER_HOME/logs/agent.log</string>
  <key>StandardErrorPath</key><string>$KEEPER_HOME/logs/agent.err</string>
</dict></plist>
PLIST
launchctl unload "$PLIST" 2>/dev/null || true
launchctl load "$PLIST"

# ── 6. Done ─────────────────────────────────────────────────────────────────
say "6/6 done — the Keeper is starting"
cat <<DONE

  Your Mac is now a BTX Keeper.

  First start: it downloads the chain's headers, loads the verified snapshot,
  catches up (typically well under an hour on this build), and then serves
  signed confirmations whenever it is on power. Battery => it pauses.

  Watch it:      tail -f ~/.btx-keeper/debug.log
  Status:        ~/.btx-keeper/bin/btx-cli -datadir=\$HOME/.btx-keeper getmatmultrustedstatus
  Stop once:     ~/.btx-keeper/bin/btx-cli -datadir=\$HOME/.btx-keeper stop
  UNINSTALL:     ~/.btx-keeper/uninstall.sh        (removes everything)

  No keys live here, nothing is bought or earned here, and you can remove it
  all with one command. Thank you for keeping the chain verifiable.
DONE
