#!/usr/bin/env bash
#
# Seed the regtest fixture with a deterministic scenario.
#
# Every amount, capacity and payment below is fixed on purpose. Re-running this
# against a fresh network must produce the same state, so that screenshots taken
# now and after a redesign differ only by the design. Do not introduce randomness.
#
# Topology:
#
#   alice  --[ 5,000,000 sat ]--> bob --[ 3,000,000 sat ]--> carol
#   cln    --[ 4,000,000 sat ]--> alice
#   eclair --[ 3,500,000 sat ]--> bob
#
# bob sits in the middle so it accrues forwarding history, which is what
# populates RTL's routing screens.
#
# Usage:  ./scripts/seed.sh          (from the docker/ directory)

set -euo pipefail

cd "$(dirname "$0")/.."

# Docker Compose reads .env by itself; bash does not. Without this the defaults
# below silently win, and the summary at the end prints a password that does not
# work.
if [ -f .env ]; then
  set -a
  # shellcheck disable=SC1091
  source .env
  set +a
fi

BITCOIN_RPC_USER="${BITCOIN_RPC_USER:-rtldev}"
BITCOIN_RPC_PASSWORD="${BITCOIN_RPC_PASSWORD:-rtldev}"
ECLAIR_API_PASSWORD="${ECLAIR_API_PASSWORD:-rtldev}"

NODES=(alice bob carol)

# Deterministic scenario constants
FUND_SATS=10000000           # on-chain funding per node
CH_ALICE_BOB=5000000         # channel capacity alice -> bob
CH_BOB_CAROL=3000000         # channel capacity bob -> carol
CH_CLN_ALICE=4000000         # channel capacity cln -> alice (Core Lightning node)
CH_ECL_BOB=3500000           # channel capacity eclair -> bob (Eclair node)
PUSH_SATS=1000000            # pushed to remote on open, so both sides have liquidity
MINE_CONFIRM=6               # blocks to confirm a funding tx
ECL_MINE_CONFIRM=8           # eclair's channel.min-depth-blocks default is 8, not 6

log()  { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
info() { printf '    %s\n' "$*"; }
die()  { printf '\n\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; }

bcli() {
  docker compose exec -T bitcoind bitcoin-cli -regtest \
    -rpcuser="$BITCOIN_RPC_USER" -rpcpassword="$BITCOIN_RPC_PASSWORD" "$@"
}

# 'docker compose exec' lands as root, whose HOME is /root, but lnd's datadir is
# /home/lnd/.lnd -- so lncli must be told where to find the cert and macaroon.
lncli() {
  local node=$1; shift
  docker compose exec -T "$node" lncli --network=regtest --lnddir=/home/lnd/.lnd "$@"
}

# Core Lightning cli. Runs inside the cln container against the regtest node.
clncli() {
  docker compose exec -T cln lightning-cli --network=regtest "$@"
}

# Eclair cli. Runs inside the eclair container; auths with the API password.
ecli() {
  docker compose exec -T eclair eclair-cli -p "$ECLAIR_API_PASSWORD" "$@"
}

# Extract the first value for a JSON key from lncli output.
# 'first' matters: walletbalance reports confirmed_balance at the top level AND
# again under account_balance.default, and lncli emits no --json flag we can use.
json_first() {
  grep -o "\"$1\": *\"[^\"]*\"" | head -1 | sed -e 's/^[^:]*: *"//' -e 's/"$//'
}

# Wait for a command to succeed, up to N attempts.
wait_for() {
  local desc=$1 attempts=$2; shift 2
  local i=1
  while (( i <= attempts )); do
    if "$@" >/dev/null 2>&1; then
      info "$desc ready (${i}s)"
      return 0
    fi
    sleep 1
    (( i++ ))
  done
  die "timed out after ${attempts}s waiting for: $desc"
}

# ---------------------------------------------------------------- bitcoind

log "Waiting for bitcoind"
wait_for "bitcoind RPC" 60 bcli getblockchaininfo

log "Preparing wallet"
if ! bcli listwallets | grep -q '"rtldev"'; then
  bcli createwallet rtldev >/dev/null 2>&1 || bcli loadwallet rtldev >/dev/null
fi
info "wallet rtldev present"

MINE_ADDR=$(bcli -rpcwallet=rtldev getnewaddress)
info "mining address: $MINE_ADDR"

HEIGHT=$(bcli getblockcount)
if (( HEIGHT < 101 )); then
  log "Mining 101 blocks (coinbase maturity)"
  bcli -rpcwallet=rtldev generatetoaddress 101 "$MINE_ADDR" >/dev/null
else
  info "chain already at height $HEIGHT, skipping initial mine"
fi

# ---------------------------------------------------------------- lnd nodes

log "Waiting for LND nodes"
for n in "${NODES[@]}"; do
  wait_for "$n" 120 lncli "$n" getinfo
done

# This script is deterministic, not idempotent: running it twice would fund every
# node again and open a second set of channels. Refuse rather than corrupt the
# fixture, since the whole point is that a fresh run reproduces identical state.
if lncli alice listchannels | grep -q '"chan_id"'; then
  die "network is already seeded -- re-running would double-fund it.
    Reset with:  docker compose down -v && docker compose up -d && ./scripts/seed.sh"
fi

log "Funding nodes (${FUND_SATS} sats each)"
BTC_AMOUNT=$(awk "BEGIN{printf \"%.8f\", $FUND_SATS/100000000}")
for n in "${NODES[@]}"; do
  addr=$(lncli "$n" newaddress p2wkh | json_first address)
  [ -n "$addr" ] || die "could not get address for $n"
  bcli -rpcwallet=rtldev sendtoaddress "$addr" "$BTC_AMOUNT" >/dev/null
  info "$n <- $BTC_AMOUNT BTC ($addr)"
done

bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null
info "mined $MINE_CONFIRM blocks to confirm funding"

log "Waiting for confirmed on-chain balances"
for n in "${NODES[@]}"; do
  for i in $(seq 1 60); do
    bal=$(lncli "$n" walletbalance | json_first confirmed_balance)
    bal=${bal:-0}
    (( bal > 0 )) && { info "$n confirmed balance: $bal sats"; break; }
    sleep 1
    (( i == 60 )) && die "$n never saw confirmed funds"
  done
done

# ---------------------------------------------------------------- peers

pubkey_of() {
  lncli "$1" getinfo | json_first identity_pubkey
}

log "Connecting peers"
ALICE_PUB=$(pubkey_of alice)
BOB_PUB=$(pubkey_of bob)
CAROL_PUB=$(pubkey_of carol)
info "alice pubkey: $ALICE_PUB"
info "bob   pubkey: $BOB_PUB"
info "carol pubkey: $CAROL_PUB"

lncli alice connect "${BOB_PUB}@bob:9735"   >/dev/null 2>&1 || info "alice->bob already connected"
lncli bob   connect "${CAROL_PUB}@carol:9735" >/dev/null 2>&1 || info "bob->carol already connected"
info "peers connected"

# ---------------------------------------------------------------- channels

log "Opening channels"
lncli alice openchannel --node_key="$BOB_PUB" \
  --local_amt="$CH_ALICE_BOB" --push_amt="$PUSH_SATS" >/dev/null
info "alice -> bob   ${CH_ALICE_BOB} sats (push ${PUSH_SATS})"

lncli bob openchannel --node_key="$CAROL_PUB" \
  --local_amt="$CH_BOB_CAROL" --push_amt="$PUSH_SATS" >/dev/null
info "bob   -> carol ${CH_BOB_CAROL} sats (push ${PUSH_SATS})"

bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null
info "mined $MINE_CONFIRM blocks to confirm channels"

log "Waiting for channels to become active"
for n in alice bob; do
  for i in $(seq 1 60); do
    active=$(lncli "$n" listchannels | grep -c '"active":  *true' || true)
    (( active > 0 )) && { info "$n has $active active channel(s)"; break; }
    sleep 1
    (( i == 60 )) && die "$n has no active channels"
  done
done

# ---------------------------------------------------------------- payments

# alice can only route to carol once the bob->carol channel has been announced and
# reached her graph. Channels are confirmed by now, but gossip is not instant --
# --trickledelay alone is 5s. Paying before this lands fails with "no route".
log "Waiting for the channel graph to reach alice"
for i in $(seq 1 90); do
  edges=$(lncli alice describegraph | grep -c '"channel_id"' || true)
  (( ${edges:-0} >= 2 )) && { info "alice sees ${edges} channels in her graph"; break; }
  sleep 1
  (( i == 90 )) && die "channel graph never propagated to alice"
done

# Fixed amounts. alice -> carol routes through bob, generating forwarding history.
log "Sending payments (alice -> carol, routed via bob)"
for amt in 10000 25000 50000 75000 100000; do
  inv=$(lncli carol addinvoice --amt="$amt" --memo="seed payment ${amt} sats" \
        | json_first payment_request)
  if lncli alice payinvoice --force --pay_req="$inv" >/dev/null 2>&1; then
    info "alice -> carol  ${amt} sats  (routed)"
  else
    info "alice -> carol  ${amt} sats  FAILED (route not ready?)"
  fi
done

log "Sending direct payments (alice -> bob)"
for amt in 5000 15000; do
  inv=$(lncli bob addinvoice --amt="$amt" --memo="direct payment ${amt} sats" \
        | json_first payment_request)
  lncli alice payinvoice --force --pay_req="$inv" >/dev/null 2>&1 \
    && info "alice -> bob    ${amt} sats" \
    || info "alice -> bob    ${amt} sats  FAILED"
done

# Unsettled invoices, so the invoice list shows more than one state.
log "Creating open (unpaid) invoices on carol"
for amt in 20000 40000; do
  lncli carol addinvoice --amt="$amt" --memo="open invoice ${amt} sats" >/dev/null
  info "carol open invoice ${amt} sats"
done

# ---------------------------------------------------------------- core lightning

# A Core Lightning node with one active channel, so RTL's CLN screens have data.
# An active (peer_connected) channel is what exercises the connection-status column.
log "Waiting for Core Lightning node"
wait_for "cln" 120 clncli getinfo

log "Funding Core Lightning (${FUND_SATS} sats)"
CLN_ADDR=$(clncli newaddr | json_first bech32)
[ -n "$CLN_ADDR" ] || die "could not get address for cln"
bcli -rpcwallet=rtldev sendtoaddress "$CLN_ADDR" "$BTC_AMOUNT" >/dev/null
info "cln <- $BTC_AMOUNT BTC ($CLN_ADDR)"
bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null
info "mined $MINE_CONFIRM blocks to confirm cln funding"

log "Waiting for cln confirmed on-chain balance"
for i in $(seq 1 60); do
  clncli listfunds | grep -q '"status": "confirmed"' && { info "cln funds confirmed"; break; }
  sleep 1
  (( i == 60 )) && die "cln never saw confirmed funds"
done

log "Opening channel cln -> alice"
clncli connect "${ALICE_PUB}@alice:9735" >/dev/null 2>&1 || info "cln->alice already connected"
clncli fundchannel "$ALICE_PUB" "$CH_CLN_ALICE" >/dev/null
info "cln -> alice ${CH_CLN_ALICE} sats"
bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null
info "mined $MINE_CONFIRM blocks to confirm the cln channel"

log "Waiting for the cln channel to become active"
for i in $(seq 1 90); do
  if clncli listpeerchannels | grep -o '"state": "[A-Z_]*"' | grep -q "CHANNELD_NORMAL"; then
    info "cln channel is CHANNELD_NORMAL"; break
  fi
  sleep 1
  (( i == 90 )) && die "cln channel never reached CHANNELD_NORMAL"
done

# ---------------------------------------------------------------- eclair

# An Eclair node with one active channel to bob plus a couple of settled and
# open invoices, so RTL's Eclair screens have data. Eclair's on-chain wallet is
# the dedicated "eclair" bitcoind wallet (created by eclair-wallet-init), but
# funding still goes through eclair's own API so its balances update.
log "Waiting for Eclair node"
wait_for "eclair" 180 ecli getinfo

log "Funding Eclair (${FUND_SATS} sats)"
ECL_ADDR=$(ecli getnewaddress | tr -d '"')
[ -n "$ECL_ADDR" ] || die "could not get address for eclair"
bcli -rpcwallet=rtldev sendtoaddress "$ECL_ADDR" "$BTC_AMOUNT" >/dev/null
info "eclair <- $BTC_AMOUNT BTC ($ECL_ADDR)"
bcli -rpcwallet=rtldev generatetoaddress "$MINE_CONFIRM" "$MINE_ADDR" >/dev/null
info "mined $MINE_CONFIRM blocks to confirm eclair funding"

log "Waiting for eclair confirmed on-chain balance"
for i in $(seq 1 60); do
  ecli onchainbalance | grep -q '"confirmed": *[1-9]' && { info "eclair funds confirmed"; break; }
  sleep 1
  (( i == 60 )) && die "eclair never saw confirmed funds"
done

log "Opening channel eclair -> bob"
ecli connect --uri="${BOB_PUB}@bob:9735" >/dev/null 2>&1 || info "eclair->bob already connected"
ecli open --nodeId="$BOB_PUB" --fundingSatoshis="$CH_ECL_BOB" --pushMsat=$(( PUSH_SATS * 1000 )) >/dev/null
info "eclair -> bob ${CH_ECL_BOB} sats (push ${PUSH_SATS})"

# 'open' returns before eclair broadcasts the funding tx; mining too early would
# confirm nothing and leave the channel waiting forever.
for i in $(seq 1 30); do
  bcli getrawmempool | grep -q '"' && { info "funding tx in mempool"; break; }
  sleep 1
  (( i == 30 )) && die "eclair funding tx never reached the mempool"
done
bcli -rpcwallet=rtldev generatetoaddress "$ECL_MINE_CONFIRM" "$MINE_ADDR" >/dev/null
info "mined $ECL_MINE_CONFIRM blocks to confirm the eclair channel"

log "Waiting for the eclair channel to become active"
for i in $(seq 1 90); do
  if ecli channels | grep -q '"state" *: *"NORMAL"'; then
    info "eclair channel is NORMAL"; break
  fi
  sleep 1
  (( i == 90 )) && die "eclair channel never reached NORMAL"
done

# Direct payments over the eclair->bob channel; no routing, so no gossip wait.
# payinvoice is asynchronous -- confirm settlement on bob's side.
log "Sending direct payments (eclair -> bob)"
for amt in 8000 18000; do
  inv_out=$(lncli bob addinvoice --amt="$amt" --memo="eclair payment ${amt} sats")
  inv=$(echo "$inv_out" | json_first payment_request)
  rhash=$(echo "$inv_out" | json_first r_hash)
  ecli payinvoice --invoice="$inv" >/dev/null 2>&1
  paid=""
  for i in $(seq 1 30); do
    if lncli bob lookupinvoice "$rhash" | grep -q '"state": *"SETTLED"'; then
      paid=1; break
    fi
    sleep 1
  done
  [ -n "$paid" ] && info "eclair -> bob  ${amt} sats" \
                 || info "eclair -> bob  ${amt} sats  FAILED (never settled)"
done

# An unpaid invoice, so the eclair invoice list shows more than one state.
log "Creating an open (unpaid) invoice on eclair"
ecli createinvoice --amountMsat=$(( 30000 * 1000 )) --description="open invoice 30000 sats" >/dev/null
info "eclair open invoice 30000 sats"

# ---------------------------------------------------------------- summary

log "Seed complete"
for n in "${NODES[@]}"; do
  chans=$(lncli "$n" listchannels | grep -c '"active":  *true' || true)
  bal=$(lncli "$n" walletbalance | json_first confirmed_balance)
  printf '    %-6s  channels: %-3s  on-chain: %s sats\n' "$n" "${chans:-0}" "${bal:-0}"
done
cln_chans=$(clncli listpeerchannels | grep -o '"state": "[A-Z_]*"' | grep -c "CHANNELD_NORMAL" || true)
printf '    %-6s  channels: %-3s  (Core Lightning)\n' "cln" "${cln_chans:-0}"
ecl_chans=$(ecli channels | grep -o '"state" *: *"NORMAL"' | grep -c NORMAL || true)
printf '    %-6s  channels: %-3s  (Eclair)\n' "eclair" "${ecl_chans:-0}"
# '|| echo 0' would be wrong here: grep -c already prints 0 when it finds nothing
# and then exits 1, so the echo would append a second line.
fwds=$(lncli bob fwdinghistory | grep -c '"chan_id_in"' || true)
printf '    bob forwarded %s payment(s)\n' "${fwds:-0}"
printf '\n    RTL: http://localhost:%s  (password: %s)\n\n' "${RTL_PORT:-3000}" "${RTL_PASSWORD:-password}"
