#!/usr/bin/env bash

exit_error() {
   printf "$1" >&2
   exit 1
}

if [ $(echo ${BASH_VERSION} | cut -d '.' -f 1) -lt 4 ]; then
   exit_error "This script requires Bash 4.x or higher"
fi

log() {
   printf "%s" "$1" >&2
}

logln() {
   printf "%s\n" "$1" >&2
}

is_sourced() {
   if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
     return 0
   else
     return 1
   fi
}

start_bitcoind() {
   logln "[$$] Setting up bitcoind..."
   test -f "$BITCOIN_CONF".in || exit_error "No such file or directory: $BITCOIN_CONF"
   mkdir -p "$BITCOIN_DATA_DIR" || exit_error "Failed to create bitcoin data directory $BITCOIN_DATA_DIR"

   log "[$$]  Generating bitcoind config file..."
   sed -r \
     -e "s!@@BITCOIN_DATA_DIR@@!$BITCOIN_DATA_DIR!g" \
     "$BITCOIN_CONF".in \
     > "$BITCOIN_CONF"
   logln "ok"
   
   log "[$$] Starting bitcoind..."
   bitcoind -fallbackfee=0.0002 -conf="$BITCOIN_CONF" >"$BITCOIN_LOGFILE" 2>&1 &
   local BITCOIN_PID=$!
   logln "PID $BITCOIN_PID"

   while true; do 
      bitcoin-cli -regtest -conf="$BITCOIN_CONF" ping >/dev/null 2>&1
      if [ $? -eq 0 ]; then
         break
      fi
      sleep 1
   done

   echo "$BITCOIN_PID"
   return 0
}

start_bitcoind_controller() {
   logln "[$$] Setting up bitcoind controller..."
   test -f "$BITCOIN_CONTROLLER_CONF".in || exit_error "No such file or directory: $BITCOIN_CONTROLLER_CONF.in"

   log "[$$]  Generating bitcoind controller config file..."
   local NOW="$(date +%s)"
   sed -r \
    -e "s/@@BITCOIN_NEON_CONTROLLER_GENESIS_TIMESTAMP@@/$NOW/g" \
    "$BITCOIN_CONTROLLER_CONF".in \
    > "$BITCOIN_CONTROLLER_CONF"
   logln "ok"

   log "[$$] Starting bitcoind controller..."
   puppet-chain "$BITCOIN_CONTROLLER_CONF" >"$BITCOIN_CONTROLLER_LOGFILE" 2>&1 &
   local RC=$?
   local BITCOIN_NEON_CONTROLLER_PID=$!

   if [ $RC -ne 0 ]; then 
      logln "FAILED"
      return 1
   fi

   logln "PID $BITCOIN_NEON_CONTROLLER_PID"

   echo "$BITCOIN_NEON_CONTROLLER_PID"
   return 0
}

start_faucet() {
   logln "[$$] Setting up bitcoin faucet..."
   test -f "$BITCOIN_CONTROLLER_CONF" || exit_error "No such file or directory: $BITCOIN_CONTROLLER_CONF"
   test -f "$BITCOIN_CONF" || exit_error "No such file or directory: $BITCOIN_CONF"

   local PRIVKEY="$(cat "$BITCOIN_CONTROLLER_CONF" | grep "# WIF" | cut -d ' ' -f 3)"

   log "[$$]  Importing faucet private key..."
   bitcoin-cli -conf="$BITCOIN_CONF" importprivkey "$PRIVKEY" >"$FAUCET_LOGFILE" 2>&1
   local RC=$?
   if [ $RC -ne 0 ]; then
      logln "FAILED!"
      return 1
   fi
   logln "ok"

   local MINER_ADDR="$(cat "$BITCOIN_CONTROLLER_CONF" | grep "miner_address =" | cut -d ' ' -f 3 | sed -r 's/"//g')"

   log "[$$]  Importing miner address $MINER_ADDR..."
   
   echo "Try importing miner address $MINER_ADDR" >>"$FAUCET_LOGFILE"
   bitcoin-cli -conf="$BITCOIN_CONF" importaddress "$MINER_ADDR" >>"$FAUCET_LOGFILE" 2>&1
   RC=$?
   if [ $RC -ne 0 ]; then 
      logln "FAILED!"
      return 1
   fi

   logln "ok"

   log "[$$] Starting bitcoin faucet..."
   faucet.sh "$FAUCET_PORT" "$BITCOIN_CONF" "$STACKS_MASTER_CHAINSTATE_DIR" >"$FAUCET_LOGFILE" 2>&1 &
   local FAUCET_PID=$!
   logln "PID $FAUCET_PID"

   echo "$FAUCET_PID"
   return 0
}

start_stacks_master_node() {
   test -d "$STACKS_MASTER_CHAINSTATE_DIR" && exit_error "Cannot start Stacks master node: Directory exists: $STACKS_MASTER_CHAINSTATE_DIR"

   logln "[$$] Setting up Stacks master node..."
   test -f "$STACKS_MASTER_CONF_IN" || exit_error "No such file or directory: $STACKS_MASTER_CONF_IN"

   log "[$$]  Generating Stacks master config file..."
   sed -r \
      -e "s!@@STACKS_CHAINSTATE_DIR@@!$STACKS_MASTER_CHAINSTATE_DIR!g" \
      -e "s!@@STACKS_PUBLIC_IP@@!$STACKS_MASTER_PUBLIC_IP!g" \
      -e "s!@@STACKS_MASTER_IS_MINER@@!$STACKS_MASTER_IS_MINER!g" \
      -e "s/@@PROCESS_EXIT_AT_BLOCK_HEIGHT@@/$PROCESS_EXIT_AT_BLOCK_HEIGHT/g" \
      -e "s/@@STACKS_DENY_NODES@@/$STACKS_MASTER_DENY_NODES/g" \
      -e "s/@@DISABLE_INBOUND_HANDSHAKES@@/$STACKS_MASTER_DISABLE_INBOUND_HANDSHAKES/g" \
      -e "s/@@DISABLE_INBOUND_WALKS@@/$STACKS_MASTER_DISABLE_INBOUND_WALKS/g" \
      -e "s/@@STACKS_MASTER_MINE_MICROBLOCKS@@/$STACKS_MASTER_MINE_MICROBLOCKS/g" \
      -e "s/@@STACKS_MASTER_MICROBLOCK_FREQUENCY@@/$STACKS_MASTER_MICROBLOCK_FREQUENCY/g" \
      -e "s/@@STACKS_MASTER_MAX_MICROBLOCKS@@/$STACKS_MASTER_MAX_MICROBLOCKS/g" \
      -e "s/@@STACKS_MASTER_WAIT_FOR_MICROBLOCKS@@/$STACKS_MASTER_WAIT_FOR_MICROBLOCKS/g" \
      "$STACKS_MASTER_CONF_IN" \
      > "$STACKS_MASTER_CONF"
   logln "ok"

   log "[$$] Starting Stacks master node..."
   BLOCKSTACK_DEBUG=1 RUST_BACKTRACE=full stacks-node start --config="$STACKS_MASTER_CONF" >"$STACKS_MASTER_LOGFILE" 2>&1 &
   local STACKS_PID=$!
   logln "PID $STACKS_PID"

   echo "$STACKS_PID"
   return 0
}

start_stacks_miner_node() {
   test -d "$STACKS_MINER_CHAINSTATE_DIR" && exit_error "Cannot start Stacks miner node: Directory exists: $STACKS_MASTER_CHAINSTATE_DIR"

   logln "[$$] Setting up Stacks miner node ($STACKS_MINER_CONF)..."
   test -f "$STACKS_MINER_CONF_IN" || exit_error "No such file or directory: $STACKS_MINER_CONF_IN"

   log "[$$]  Generating Stacks miner config file..."
   local STACKS_MINER_SEED="$(blockstack-cli generate-sk | jq -r '.secretKey')"

   sed -r \
      -e "s/@@STACKS_BOOTSTRAP_IP@@/$STACKS_MINER_BOOTSTRAP_IP/g" \
      -e "s/@@STACKS_BOOTSTRAP_PORT@@/$STACKS_MINER_BOOTSTRAP_PORT/g" \
      -e "s/@@STACKS_DENY_NODES@@/$STACKS_MINER_DENY_NODES/g" \
      -e "s/@@BITCOIN_IP@@/$BITCOIN_PUBLIC_IP/g" \
      -e "s!@@STACKS_PUBLIC_IP@@!$STACKS_MINER_PUBLIC_IP!g" \
      -e "s/@@STACKS_MINER_SEED@@/$STACKS_MINER_SEED/g" \
      -e "s!@@STACKS_CHAINSTATE_DIR@@!$STACKS_MINER_CHAINSTATE_DIR!g" \
      -e "s!@@STACKS_MINER_P2P_PORT@@!$STACKS_MINER_P2P_PORT!g" \
      -e "s!@@STACKS_MINER_RPC_PORT@@!$STACKS_MINER_RPC_PORT!g" \
      -e "s/@@PROCESS_EXIT_AT_BLOCK_HEIGHT@@/$PROCESS_EXIT_AT_BLOCK_HEIGHT/g" \
      -e "s/@@DISABLE_INBOUND_HANDSHAKES@@/$STACKS_MINER_DISABLE_INBOUND_HANDSHAKES/g" \
      -e "s/@@DISABLE_INBOUND_WALKS@@/$STACKS_MINER_DISABLE_INBOUND_WALKS/g" \
      -e "s/@@STACKS_MINER_MINE_MICROBLOCKS@@/$STACKS_MINER_MINE_MICROBLOCKS/g" \
      -e "s/@@STACKS_MINER_MICROBLOCK_FREQUENCY@@/$STACKS_MINER_MICROBLOCK_FREQUENCY/g" \
      -e "s/@@STACKS_MINER_MAX_MICROBLOCKS@@/$STACKS_MINER_MAX_MICROBLOCKS/g" \
      -e "s/@@STACKS_MINER_WAIT_FOR_MICROBLOCKS@@/$STACKS_MINER_WAIT_FOR_MICROBLOCKS/g" \
      "$STACKS_MINER_CONF_IN" \
      > "$STACKS_MINER_CONF"
   logln "ok"

   local BTCADDR="$(blockstack-cli --testnet addresses "$STACKS_MINER_SEED" | jq -r '.BTC')"
   local TIMEOUT=2
   local TXID=""
   local CONFIRMATIONS=0

   log "[$$]  Fetching BTC from the faucet for this miner ($BTCADDR)..."
   for i in $(seq 1 10); do
       TXID="$(curl -sf -X POST -d $BTCADDR$'\n' -H "content-type: text/plain" "$FAUCET_URL"/bitcoin/fund)"
       local RC=$?
       if [ $RC -ne 0 ]; then 
          # curl failed, or we didn't get 200.  Try again
          logln "FAILED!  Try again in $TIMEOUT seconds..."
          sleep $TIMEOUT

          TIMEOUT=$((TIMEOUT + $RANDOM % TIMEOUT))
          continue
       fi
       break
   done
   logln "txid $TXID"

   log "[$$]  Waiting for BTC to get confirmed..."
   TIMEOUT=2
   for i in $(seq 1 20); do
       CONFIRMATIONS="$(curl -sf "$FAUCET_URL"/bitcoin/confirmations/"$TXID")"
       local RC=$?
       if [ $RC -ne 0 ]; then 
          logln "FAILED!  Try again (attempt $i)..."
          sleep $TIMEOUT

          TIMEOUT=$((TIMEOUT + $RANDOM % TIMEOUT))
          continue
       fi
       if (( $CONFIRMATIONS > 0 )); then 
          break
       else
          log ".wait $TIMEOUT."
          sleep $TIMEOUT

          TIMEOUT=$((TIMEOUT + $RANDOM % TIMEOUT))
       fi
   done

   if (( $CONFIRMATIONS == 0 )); then 
      logln "FAILED"
      return 1
   fi
   logln "ok"
   
   log "[$$] Starting Stacks miner node..."
   BLOCKSTACK_DEBUG=1 RUST_BACKTRACE=full stacks-node start --config="$STACKS_MINER_CONF" >"$STACKS_MINER_LOGFILE" 2>&1 &
   local STACKS_PID=$!
   logln "PID $STACKS_PID"
   
   echo "$STACKS_PID"
   return 0
}

start_stacks_follower_node() {
   test -d "$STACKS_FOLLOWER_CHAINSTATE_DIR" && exit_error "Cannot start Stacks miner node: Directory exists: $STACKS_MASTER_CHAINSTATE_DIR"

   logln "[$$] Setting up Stacks follower node ($STACKS_FOLLOWER_CONF)..."
   test -f "$STACKS_FOLLOWER_CONF_IN" || exit_error "No such file or directory: $STACKS_FOLLOWER_CONF_IN"

   log "[$$]  Generating Stacks follower config file..."
   sed -r \
      -e "s/@@STACKS_BOOTSTRAP_IP@@/$STACKS_FOLLOWER_BOOTSTRAP_IP/g" \
      -e "s/@@STACKS_BOOTSTRAP_PORT@@/$STACKS_FOLLOWER_BOOTSTRAP_PORT/g" \
      -e "s/@@BITCOIN_IP@@/$BITCOIN_PUBLIC_IP/g" \
      -e "s!@@STACKS_PUBLIC_IP@@!$STACKS_FOLLOWER_PUBLIC_IP!g" \
      -e "s/@@STACKS_DENY_NODES@@/$STACKS_FOLLOWER_DENY_NODES/g" \
      -e "s!@@STACKS_CHAINSTATE_DIR@@!$STACKS_FOLLOWER_CHAINSTATE_DIR!g" \
      -e "s!@@STACKS_FOLLOWER_P2P_PORT@@!$STACKS_FOLLOWER_P2P_PORT!g" \
      -e "s!@@STACKS_FOLLOWER_RPC_PORT@@!$STACKS_FOLLOWER_RPC_PORT!g" \
      -e "s/@@PROCESS_EXIT_AT_BLOCK_HEIGHT@@/$PROCESS_EXIT_AT_BLOCK_HEIGHT/g" \
      -e "s/@@DISABLE_INBOUND_HANDSHAKES@@/$STACKS_FOLLOWER_DISABLE_INBOUND_HANDSHAKES/g" \
      -e "s/@@DISABLE_INBOUND_WALKS@@/$STACKS_FOLLOWER_DISABLE_INBOUND_WALKS/g" \
      "$STACKS_FOLLOWER_CONF_IN" \
      > "$STACKS_FOLLOWER_CONF"
   logln "ok"
   
   log "[$$] Starting Stacks follower node..."
   BLOCKSTACK_DEBUG=1 RUST_BACKTRACE=full stacks-node start --config="$STACKS_FOLLOWER_CONF" >"$STACKS_FOLLOWER_LOGFILE" 2>&1 &
   local STACKS_PID=$!
   logln "PID $STACKS_PID"

   echo "$STACKS_PID"
   return 0
}

check_if_running() {
   local PID=$1
   if [ $PID -gt 0 ]; then 
       kill -s 0 "$PID" 2>/dev/null
       return $?
   else
       return 0
   fi
}

kill_if_running() {
   local SIG="$1"
   local PID=$2

   if [ $PID -gt 0 ]; then
      check_if_running $PID
      if [ $? -eq 0 ]; then
         logln "[$$] Send $SIG to PID $PID"
         kill -s "$SIG" "$PID" 2>/dev/null || true
      fi
   fi
}

BITCOIN_PID=0
BITCOIN_NEON_CONTROLLER_PID=0
STACKS_NODE_PID=0
FAUCET_PID=0

wait_pids() {
   while true; do
      wait
      sleep 5

      local ALL_GOOD=0
      local PID=0
      for PID in $@; do
         check_if_running $PID
         if [ $? -ne 0 ]; then
            ALL_GOOD=1
            break
         fi
      done

      if [ $ALL_GOOD -ne 0 ]; then
         break
      fi
   done
   return 0
}

shutdown() {
   echo "[$$] Sending SIGTERM to all processes"
   for PID in $BITCOIN_PID $BITCOIN_NEON_CONTROLLER_PID $STACKS_NODE_PID $FAUCET_PID; do
       kill_if_running SIGTERM $PID
   done

   sleep 5
   echo "[$$] Sending SIGKILL to all processes"
   for PID in $BITCOIN_PID $BITCOIN_NEON_CONTROLLER_PID $STACKS_NODE_PID $FAUCET_PID; do
       kill_if_running SIGKILL $PID
   done

   exit 0
}

usage() {
   exit_error "Usage: $0 [master|miner|follower]"
}

start_node() {
   local MODE="$CONFIG_MODE"
   if [ $# -gt 0 ]; then
      MODE="$1"
   fi
   if [ -z "$MODE" ]; then
      usage
   fi

   set -uo pipefail
   trap 'shutdown' INT

   case "$MODE" in
      master)
         BITCOIN_PID="$(start_bitcoind)"

         sleep 5
         BITCOIN_NEON_CONTROLLER_PID="$(start_bitcoind_controller)"

         sleep 5
         FAUCET_PID="$(start_faucet)"

         sleep 5
         STACKS_NODE_PID="$(start_stacks_master_node)"
         ;;

      miner)
         STACKS_NODE_PID="$(start_stacks_miner_node)"
         ;;

      follower)
         STACKS_NODE_PID="$(start_stacks_follower_node)"
         ;;
      *)
         usage
         ;;
   esac

   echo >&2 "[$$] Running!"

   # reap children
   wait_pids $BITCOIN_PID $BITCOIN_NEON_CONTROLLER_PID $STACKS_NODE_PID $FAUCET_PID

   echo >&2 "[$$] Shutting down"
   shutdown
}

wait_node() {
   local MODE="$CONFIG_MODE"
   if [ $# -gt 0 ]; then
       MODE="$1"
   fi
   local RPC_HOST=""
   local RPC_PORT=""
   case "$MODE" in
      master)
         RPC_HOST="$STACKS_MASTER_PUBLIC_IP"
         RPC_PORT="20443"
         ;;
      miner)
         RPC_HOST="$STACKS_MINER_PUBLIC_IP"
         RPC_PORT="$STACKS_MINER_RPC_PORT"
         ;;
      follower)
         RPC_HOST="$STACKS_FOLLOWER_PUBLIC_IP"
         RPC_PORT="$STACKS_FOLLOWER_RPC_PORT"
         ;;
      *)
         return 1
         ;;
   esac
   local CNT=0
   for CNT in $(seq 1 360); do
      curl -sLf "http://$RPC_HOST:$RPC_PORT/v2/info" >/dev/null 2>&1
      if [ $? -eq 0 ]; then
         return 0
      fi
      sleep 5
   done
   return 1
}

wait_until_burn_block() {
   local RPC_HOST=""
   local RPC_PORT=""
   local BURN_BLOCK_HEIGHT="$1"
   local MODE="$CONFIG_MODE"
   if [ $# -gt 1 ]; then
      MODE="$2"
   fi

   case "$MODE" in
      master)
         RPC_HOST="$STACKS_MASTER_PUBLIC_IP"
         RPC_PORT="20443"
         ;;
      miner)
         RPC_HOST="$STACKS_MINER_PUBLIC_IP"
         RPC_PORT="$STACKS_MINER_RPC_PORT"
         ;;
      follower)
         RPC_HOST="$STACKS_FOLLOWER_PUBLIC_IP"
         RPC_PORT="$STACKS_FOLLOWER_RPC_PORT"
         ;;
      *)
         return 1
         ;;
   esac
   local INFO_URL="http://$RPC_HOST:$RPC_PORT/v2/info"
   while true; do
      curl -sLf "$INFO_URL" >/dev/null 2>&1
      if [ $? -ne 0 ]; then
         sleep 5
         continue
      fi

      local BURN_HEIGHT="$(curl -sLf "$INFO_URL" | jq -r '.burn_block_height')"
      if (( $BURN_HEIGHT >= $BURN_BLOCK_HEIGHT )); then
         break
      fi
      sleep 5
   done
}

stop_node() {
   local PID="$1"
   kill -s SIGINT "$PID"
   return $?
}

report() {
   local REPORT_NAME="$1"
   local CHAINSTATE_DIR=""
   case "$CONFIG_MODE" in
      master)
         CHAINSTATE_DIR="$STACKS_MASTER_CHAINSTATE_DIR"
         ;;
      miner)
         CHAINSTATE_DIR="$STACKS_MINER_CHAINSTATE_DIR"
         ;;
      follower)
         CHAINSTATE_DIR="$STACKS_FOLLOWER_CHAINSTATE_DIR"
         ;;
      *)
         return 1
         ;;
   esac
   faucet.sh report "$BITCOIN_CONF" "$CHAINSTATE_DIR" "$REPORT_NAME"
}

is_sourced
if [ $? -ne 0 ]; then 
   for cmd in bitcoind bitcoin-cli puppet-chain stacks-node blockstack-cli date jq grep sed kill cat curl faucet.sh seq; do
      which $cmd 2>&1 >/dev/null || exit_error "Missing \"$cmd\""
   done

   CONF_FILE=""
   if [ -n "$CONFIG" ]; then
      CONF_FILE="$CONFIG"
   fi
   if [ -z "$CONF_FILE" ]; then
      exit_error "$CONFIG environ not set"
   fi
   if ! [ -f "$CONF_FILE" ]; then
      exit_error "No such file or directory: $CONF_FILE"
   fi
   source "$CONF_FILE"
   start_node $@
fi

