#!/usr/bin/env bash
set -euo pipefail
# ---- lib/logging.sh ----
# kit/lib/logging.sh
# shellcheck shell=bash
log_info()  { printf '[INFO] %s\n'  "$*"; }
log_warn()  { printf '[WARN] %s\n'  "$*" >&2; }
log_error() { printf '[ERROR] %s\n' "$*" >&2; }
die()       { log_error "$*"; exit 1; }
# In RL1_TEST mode, record privileged commands instead of running them.
run_priv() {
  if [ "${RL1_TEST:-0}" = "1" ]; then
    printf '%s\n' "$*" >> "$RL1_HOME/../.rl1_test_cmds"
  else
    eval "$*"
  fi
}
# ---- lib/constants.sh ----
# kit/lib/constants.sh — single source of truth, mirrored in README.md
# shellcheck shell=bash
: "${RL1_CHAIN_ID:=rogue_4221-1}"
: "${RL1_EVM_CHAIN_ID:=4221}"
: "${RL1_DENOM:=arogue}"            # 18 decimals; 1 ROGUE = 1e18 arogue
: "${RL1_MIN_GAS:=0.0001arogue}"
: "${RL1_HOME_DEFAULT:=$HOME/.evmd}"
: "${RL1_HOME:=$RL1_HOME_DEFAULT}"
: "${RL1_MONIKER:=rogue-validator}"
: "${RL1_COSMOVISOR_VERSION:=v1.7.1}"
: "${RL1_GENESIS_SHA256:=1899469fc55dc292891e4d3d12d997524939a5bd7f1ddb1a37f4a831a8e5bf4d}"
: "${RL1_BINARY_SHA256_AMD64:=a7f310a3211b49093e0ba2b30d93a0025ccbf08c5de11d1907a596638818b086}"
: "${RL1_BINARY_SHA256_ARM64:=3ad2e0130c6e504a12f79d993c404484d606b035801626695ee0008aa06ad729}"
: "${RL1_BINARY_SHA256:=$RL1_BINARY_SHA256_AMD64}"
: "${RL1_SEED:=a4a5cdc25a5809acaedc68325019855f01fa8b35@seed.roguelayer.one:26656}"
: "${RL1_PERSISTENT_PEERS:=$RL1_SEED}"
: "${RL1_COMETBFT_RPC:=https://cometbft.roguelayer.one}"
: "${RL1_NETWORKS_RAW:=https://raw.githubusercontent.com/Genesisapps11/networks/main/rogue_4221-1}"
: "${RL1_BINARY_BASE:=http://dl.roguelayer.one}"
: "${RL1_BINARY_URL:=$RL1_BINARY_BASE/rogued-linux-amd64}"
: "${RL1_GENESIS_URL:=$RL1_BINARY_BASE/genesis.json}"
# ---- lib/deps.sh ----
# kit/lib/deps.sh
# shellcheck shell=bash
install_deps() {
  run_priv "sudo apt-get update"
  run_priv "sudo apt-get install -y git make jq curl build-essential"
  # Go (upstream; Ubuntu's apt Go is too old to build rogued)
  run_priv "curl -sSL https://go.dev/dl/go1.24.1.linux-amd64.tar.gz -o /tmp/go.tgz"
  run_priv "sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf /tmp/go.tgz"
  export PATH="$PATH:/usr/local/go/bin:$HOME/go/bin"
  run_priv "go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@${RL1_COSMOVISOR_VERSION}"
  log_info "dependencies installed (go + cosmovisor ${RL1_COSMOVISOR_VERSION})"
}
# ---- lib/binary.sh ----
# kit/lib/binary.sh
# shellcheck shell=bash
verify_sha256() {  # <file> <expected-hex>
  local file="$1" expected="$2" actual
  actual="$(sha256sum "$file" | awk '{print $1}')"
  if [ "$actual" != "$expected" ]; then
    log_error "sha256 mismatch for $file: got $actual expected $expected"
    return 1
  fi
  log_info "sha256 OK: $file"
}

install_binary() {
  local arch sha
  case "$(uname -m)" in
    x86_64|amd64)  arch=amd64; sha="$RL1_BINARY_SHA256_AMD64";;
    aarch64|arm64) arch=arm64; sha="$RL1_BINARY_SHA256_ARM64";;
    *) die "unsupported CPU arch: $(uname -m) (need x86_64 or aarch64)";;
  esac
  command -v gunzip >/dev/null 2>&1 || die "gunzip not found — install the 'gzip' package and re-run"
  # The binary is served gzip-compressed (~3.5x smaller, so far less exposure to
  # in-transit corruption on large downloads). gunzip's built-in CRC catches a
  # corrupted transfer loudly; the sha256 below is the final authority on the
  # decompressed binary (same hash as the uncompressed artifact). Retry a few
  # times so a one-off corrupted download self-heals instead of aborting install.
  local url="$RL1_BINARY_BASE/rogued-linux-$arch.gz"
  local n=0 ok=0
  while [ "$n" -lt 3 ]; do
    n=$((n + 1))
    rm -f /tmp/rogued.gz /tmp/rogued
    if curl -fsSL "$url" -o /tmp/rogued.gz \
       && gunzip -f /tmp/rogued.gz \
       && verify_sha256 /tmp/rogued "$sha"; then
      ok=1; break
    fi
    log_info "binary download failed integrity check (attempt $n/3) — retrying"
    sleep 2
  done
  [ "$ok" = "1" ] || die "refusing to install unverified binary (download corrupted after 3 attempts — check your network)"
  if [ "${RL1_TEST:-0}" = "1" ]; then
    run_priv "sudo install -m 0755 /tmp/rogued /usr/local/bin/rogued"
    return 0
  fi
  sudo install -m 0755 /tmp/rogued /usr/local/bin/rogued
  rogued --help >/dev/null 2>&1 || die "rogued failed to run after install"
  log_info "rogued ($arch static) installed and verified"
}
# ---- lib/node.sh ----
# kit/lib/node.sh
# shellcheck shell=bash
init_node() {
  if [ -f "$RL1_HOME/config/genesis.json" ] || [ -f "$RL1_HOME/config/priv_validator_key.json" ]; then
    log_warn "$RL1_HOME already initialized — skipping init (see guard.sh)"; return 0
  fi
  if [ "${RL1_TEST:-0}" = "1" ]; then run_priv "rogued init $RL1_MONIKER --chain-id $RL1_CHAIN_ID --home $RL1_HOME"; return 0; fi
  rogued init "$RL1_MONIKER" --chain-id "$RL1_CHAIN_ID" --home "$RL1_HOME"
}

# Print the literal keys that exist in the freshly-generated config so set_config can
# assert/sed against reality instead of guesses.
config_probe() {
  grep -hoE '^[a-zA-Z0-9_-]+ =|^\[[a-zA-Z0-9_.-]+\]' \
    "$RL1_HOME/config/config.toml" "$RL1_HOME/config/app.toml" 2>/dev/null | sort -u
}

download_genesis() {
  local url="${RL1_GENESIS_URL:-$RL1_NETWORKS_RAW/genesis.json}"
  curl -fsSL "$url" -o "$RL1_HOME/config/genesis.json"
  verify_sha256 "$RL1_HOME/config/genesis.json" "$RL1_GENESIS_SHA256" \
    || die "genesis.json sha256 mismatch — refusing to continue"
}

start() {
  if [ "${RL1_TEST:-0}" = "1" ]; then run_priv "sudo systemctl enable --now rogued"; return 0; fi
  sudo systemctl enable --now rogued
  log_info "rogued started. Watch: journalctl -u rogued -f ; rogued status | jq .sync_info"
}
# ---- lib/config.sh ----
# Section-scoped, idempotent key setter for the flat TOML cosmos emits.

# assert_key_present <file> <section|-> <key>
# Dies with a clear message if the key is not found in the file (within its section when
# section != "-"). Calls die() on failure so callers abort on a renamed/removed key.
assert_key_present() {
  local file="$1" section="$2" key="$3"
  if [ "$section" = "-" ]; then
    # top-level key: must appear as "key = ..." at the start of a line
    grep -qE "^[[:space:]]*${key} =" "$file" && return 0
  else
    # section-scoped: must appear after the [section] header and before the next [header]
    awk -v sec="[$section]" -v key="$key" '
      $0==sec { ins=1; next }
      ins && /^\[/ { exit }
      ins && $0 ~ ("^[[:space:]]*" key " =") { found=1; exit }
      END { exit !found }
    ' "$file" && return 0
  fi
  die "config key '$key' not found in section '$section' of $file — cosmos-evm may have renamed it"
}

set_toml_key() {  # <file> <section|-> <key> <value-with-quotes-as-needed>
  local file="$1" section="$2" key="$3" value="$4"
  if [ "$section" = "-" ]; then
    sed -i -E "s|^([[:space:]]*)${key} = .*|\1${key} = ${value}|" "$file"
  else
    awk -v sec="[$section]" -v key="$key" -v val="$value" '
      BEGIN {
        # Escape \ and & in val so awk sub() does not treat them specially.
        # In sub() replacement: \\ = one literal backslash, \& = literal &.
        # So we need each \ in val to become \\ and each & to become \&.
        # The awk string "\\\\&" represents the two-char sequence \& which sub() reads as literal &.
        gsub(/\\/, "\\\\", val)
        gsub(/&/,  "\\\\&", val)
      }
      $0==sec { ins=1 }
      ins && $0 ~ ("^" key " =") && !done { sub("^" key " = .*", key " = " val); done=1 }
      /^\[/ && $0!=sec { ins=0 }
      { print }
    ' "$file" > "$file.tmp" && mv "$file.tmp" "$file"
  fi
}

set_config() {
  if [ "${RL1_ROLE:-validator}" = "backbone" ]; then set_config_backbone; else set_config_validator; fi
}

set_config_validator() {
  local C="$RL1_HOME/config/config.toml" A="$RL1_HOME/config/app.toml"
  # Probe every key we intend to edit — aborts loudly if cosmos-evm renamed one.
  assert_key_present "$C" p2p          seeds
  assert_key_present "$C" p2p          persistent_peers
  assert_key_present "$C" p2p          external_address
  assert_key_present "$C" p2p          pex
  assert_key_present "$C" p2p          max_num_outbound_peers
  assert_key_present "$C" tx_index     indexer
  assert_key_present "$C" instrumentation prometheus
  assert_key_present "$C" consensus    double_sign_check_height
  assert_key_present "$A" -            minimum-gas-prices
  assert_key_present "$A" -            pruning
  assert_key_present "$A" json-rpc     enable
  # P2P — NAT-friendly outbound-only validator
  set_toml_key "$C" p2p seeds "\"$RL1_SEED\""
  set_toml_key "$C" p2p persistent_peers "\"$RL1_PERSISTENT_PEERS\""
  set_toml_key "$C" p2p external_address '""'          # empty unless they port-forward
  set_toml_key "$C" p2p pex 'true'
  set_toml_key "$C" p2p max_num_outbound_peers '20'
  # Consensus-only role: no cosmos tx index, double-sign guard, metrics on
  set_toml_key "$C" tx_index indexer '"null"'
  set_toml_key "$C" instrumentation prometheus 'true'
  set_toml_key "$C" consensus double_sign_check_height '10'
  # app.toml — gas, pruning, EVM RPC OFF on validators
  set_toml_key "$A" - minimum-gas-prices "\"$RL1_MIN_GAS\""
  set_toml_key "$A" - pruning '"default"'
  set_toml_key "$A" json-rpc enable 'false'
  log_info "validator-role config applied"
}

set_config_backbone() {
  local C="$RL1_HOME/config/config.toml" A="$RL1_HOME/config/app.toml"
  # Probe every key we intend to edit — aborts loudly if cosmos-evm renamed one.
  assert_key_present "$C" p2p          seed_mode
  assert_key_present "$C" p2p          pex
  assert_key_present "$C" p2p          addr_book_strict
  assert_key_present "$C" p2p          allow_duplicate_ip
  assert_key_present "$C" p2p          max_num_inbound_peers
  assert_key_present "$C" p2p          external_address
  assert_key_present "$C" tx_index     indexer
  assert_key_present "$C" instrumentation prometheus
  assert_key_present "$A" -            minimum-gas-prices
  assert_key_present "$A" -            pruning
  assert_key_present "$A" -            pruning-keep-recent
  assert_key_present "$A" -            pruning-interval
  assert_key_present "$A" state-sync   snapshot-interval
  assert_key_present "$A" state-sync   snapshot-keep-recent
  assert_key_present "$A" json-rpc     enable
  assert_key_present "$A" json-rpc     address
  # P2P — full node serving state-sync chunks and EVM RPC, NOT seed_mode
  set_toml_key "$C" p2p seed_mode 'false'
  set_toml_key "$C" p2p pex 'true'
  set_toml_key "$C" p2p addr_book_strict 'false'
  set_toml_key "$C" p2p allow_duplicate_ip 'true'
  set_toml_key "$C" p2p max_num_inbound_peers '200'
  set_toml_key "$C" p2p external_address "\"${RL1_BACKBONE_EXTERNAL:-}\""
  # Serve cosmos tx queries + metrics
  set_toml_key "$C" tx_index indexer '"kv"'
  set_toml_key "$C" instrumentation prometheus 'true'
  # app.toml — gas, custom pruning to retain state-sync chunks
  set_toml_key "$A" - minimum-gas-prices "\"$RL1_MIN_GAS\""
  set_toml_key "$A" - pruning '"custom"'
  set_toml_key "$A" - pruning-keep-recent '"100000"'
  set_toml_key "$A" - pruning-interval '"100"'
  # State-sync snapshot production
  set_toml_key "$A" state-sync snapshot-interval '1000'
  set_toml_key "$A" state-sync snapshot-keep-recent '5'
  # EVM JSON-RPC ON for the backbone (public endpoint)
  set_toml_key "$A" json-rpc enable 'true'
  set_toml_key "$A" json-rpc address '"0.0.0.0:8545"'
  log_info "backbone-role config applied"
}
# ---- lib/cosmovisor.sh ----
setup_cosmovisor() {
  export DAEMON_NAME=rogued DAEMON_HOME="$RL1_HOME"
  run_priv "DAEMON_NAME=rogued DAEMON_HOME=$RL1_HOME cosmovisor init $(which rogued)"
  log_info "cosmovisor tree initialized under $RL1_HOME/cosmovisor"
}
# ---- lib/service.sh ----
write_service() {
  local user="${RL1_USER:-$(id -un)}"
  local out="${RL1_SERVICE_OUT:-/etc/systemd/system/rogued.service}"
  local cosmovisor_bin
  cosmovisor_bin="$(command -v cosmovisor || echo "/home/${user}/go/bin/cosmovisor")"
  local unit
  unit="$(cat <<EOF
[Unit]
Description=RL1 (rogue_4221-1) validator
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=${user}
ExecStart=${cosmovisor_bin} run start --home ${RL1_HOME}
Restart=always
RestartSec=3
LimitNOFILE=65535
Environment="DAEMON_NAME=rogued"
Environment="DAEMON_HOME=${RL1_HOME}"
Environment="DAEMON_ALLOW_DOWNLOAD_BINARIES=false"
Environment="DAEMON_RESTART_AFTER_UPGRADE=true"
Environment="UNSAFE_SKIP_BACKUP=false"
Environment="PATH=/usr/local/bin:/home/${user}/go/bin:/usr/bin:/bin"

[Install]
WantedBy=multi-user.target
EOF
)"
  if [ "${RL1_TEST:-0}" = "1" ] || [ "$out" != "/etc/systemd/system/rogued.service" ]; then
    printf '%s\n' "$unit" > "$out"
  else
    printf '%s\n' "$unit" | sudo tee "$out" >/dev/null
    run_priv "sudo systemctl daemon-reload"
  fi
  log_info "wrote systemd unit: $out"
}
# ---- lib/statesync.sh ----
# kit/lib/statesync.sh — installer phase wrapper for state-sync (reset-safe ordering)

# compute_statesync_anchor <rpc-url>
# Fetches the live latest height, computes trust_height = latest - 1000,
# fetches the trust hash, and writes all four [statesync] keys into
# $RL1_HOME/config/config.toml via set_toml_key.
compute_statesync_anchor() {
  local rpc="${1:-$RL1_COMETBFT_RPC}"
  local C="$RL1_HOME/config/config.toml"
  local LATEST TRUST_HEIGHT TRUST_HASH

  LATEST="$(curl -s "$rpc/block" | jq -r .result.block.header.height)"
  if [ -z "$LATEST" ] || [ "$LATEST" = "null" ]; then
    die "could not read latest height from $rpc"
  fi
  TRUST_HEIGHT=$((LATEST - 1000))
  TRUST_HASH="$(curl -s "$rpc/block?height=$TRUST_HEIGHT" | jq -r .result.block_id.hash)"
  if [ -z "$TRUST_HASH" ] || [ "$TRUST_HASH" = "null" ]; then
    die "could not read trust hash"
  fi

  set_toml_key "$C" statesync enable       'true'
  set_toml_key "$C" statesync rpc_servers  "\"$rpc,$rpc\""   # CometBFT needs >=2
  set_toml_key "$C" statesync trust_height "$TRUST_HEIGHT"
  set_toml_key "$C" statesync trust_hash   "\"$TRUST_HASH\""
  log_info "state-sync set: height=$TRUST_HEIGHT hash=$TRUST_HASH"
}

state_sync() {
  if [ "${RL1_TEST:-0}" = "1" ]; then
    # In test mode record the privileged commands (no external script needed)
    run_priv "compute_statesync_anchor $RL1_COMETBFT_RPC"
    return 0
  fi
  local pvs="$RL1_HOME/data/priv_validator_state.json"
  local bak="$RL1_HOME/priv_validator_state.json.statesync-bak"
  rm -f "$bak"
  [ -f "$pvs" ] && cp "$pvs" "$bak"
  rogued comet unsafe-reset-all --home "$RL1_HOME" --keep-addr-book || true
  mkdir -p "$RL1_HOME/data"
  if [ -f "$bak" ]; then cp "$bak" "$pvs" && rm -f "$bak"; fi
  curl -fsSL "$RL1_NETWORKS_RAW/addrbook.json" -o "$RL1_HOME/config/addrbook.json" || log_warn "no addrbook published yet"
  compute_statesync_anchor "$RL1_COMETBFT_RPC"
}
# ---- lib/hardening.sh ----
harden() {
  run_priv "sudo apt-get install -y fail2ban chrony unattended-upgrades"
  run_priv "sudo systemctl enable --now fail2ban chrony"
  run_priv "sudo ufw allow 22/tcp"
  run_priv "sudo ufw limit 22/tcp"
  run_priv "sudo ufw allow 26656/tcp"
  run_priv "sudo ufw --force enable"
  # SSH hardening — always disable root login
  run_priv "sudo sed -i 's/^#\\?PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config"
  # Only disable password auth if an authorized_keys file is non-empty; otherwise the
  # operator would be locked out before they have added their key.
  local ak="${HOME}/.ssh/authorized_keys"
  if [ -s "$ak" ]; then
    run_priv "sudo sed -i 's/^#\\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config"
    run_priv "sudo systemctl restart ssh"
    log_info "host hardened (ufw 22+26656, fail2ban, chrony, ssh key-only)"
  else
    log_warn "\$HOME/.ssh/authorized_keys is missing or empty — skipping PasswordAuthentication no to avoid SSH lockout; add your public key then re-run harden"
    log_info "host hardened (ufw 22+26656, fail2ban, chrony, PermitRootLogin no — password auth left enabled)"
  fi
}
# ---- lib/guard.sh ----
ensure_safe_home() {
  if [ -f "$RL1_HOME/config/priv_validator_key.json" ] && [ "${RL1_FORCE:-0}" != "1" ]; then
    log_error "$RL1_HOME already initialized (has a consensus key)."
    log_error "Re-running could destroy keys/state. Set RL1_FORCE=1 only if you are SURE."
    return 1
  fi
}
usage() {
  cat <<EOF
RL1 validator installer — phases run in order by default:
  install_deps  install_binary  init_node  download_genesis
  set_config    setup_cosmovisor  write_service  harden  state_sync  start
Flags:
  --home PATH     node home (default: \$HOME/.evmd)
  --moniker NAME  validator moniker
  --only PHASE    run a single phase
  --print-home    print the resolved home and exit
  --help          this text
EOF
}

main() {
  local only="" print_home=0
  while [ $# -gt 0 ]; do
    case "$1" in
      --home) RL1_HOME="$2"; shift 2;;
      --moniker) RL1_MONIKER="$2"; shift 2;;
      --only) only="$2"; shift 2;;
      --print-home) print_home=1; shift;;
      --help|-h) usage; exit 0;;
      *) die "unknown arg: $1";;
    esac
  done
  export RL1_HOME RL1_MONIKER
  if [ "$print_home" = "1" ]; then echo "$RL1_HOME"; exit 0; fi
  [ "$(id -u)" -ne 0 ] || die "Run as a non-root user (with sudo available), not as root."
  # Guard only the full default install (the real clobber risk). A targeted --only
  # phase (e.g. start, state_sync) is an explicit op the operator runs on a live node.
  if [ -z "$only" ]; then ensure_safe_home || exit 1; fi
  local phases=(install_deps install_binary init_node download_genesis set_config setup_cosmovisor write_service harden start)
  if [ -n "$only" ]; then phases=("$only"); fi
  for p in "${phases[@]}"; do
    if declare -F "$p" >/dev/null; then log_info "phase: $p"; "$p"; else log_warn "phase $p not implemented yet"; fi
  done
}
main "$@"
