#!/usr/bin/env bash
# Reconcile cW* token roles so only the configured per-network bridge keeps MINTER/BURNER.
#
# The script is intentionally conservative:
# - It grants MINTER/BURNER to the configured bridge if missing
# - It revokes MINTER/BURNER from the deployer/admin by default
# - It can revoke MINTER/BURNER from an explicit denylist of extra addresses
# - It can optionally freeze future operational role changes on newer cW* contracts
#
# Usage:
#   bash scripts/deployment/cw-enforce-bridge-only-roles.sh --dry-run
#   bash scripts/deployment/cw-enforce-bridge-only-roles.sh
#
# Optional env:
#   CW_ROLE_CHAINS="1 10 25 56 100 137 42161 42220 43114 8453"
#   CW_ROLE_TOKENS="CWUSDT CWUSDC CWEURC"
#   CW_ROLE_EXTRA_REVOKE="0xabc...,0xdef..."
#   CW_ROLE_KEEP_ALLOWLIST="0xabc...,0xdef..."
#   CW_ROLE_REVOKE_DEPLOYER=1      # default 1
#   CW_ROLE_FREEZE_IF_SUPPORTED=1  # default 1
#   CW_ROLE_FROM_BLOCK=earliest     # optional log scan start block for role event discovery
#   PRIVATE_KEY                    # required unless --dry-run

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
cd "$SMOM_ROOT"

DRY_RUN=0
if [[ "${1:-}" == "--dry-run" ]]; then
  DRY_RUN=1
fi

if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
  # shellcheck disable=SC1090
  PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
elif [[ -f .env ]]; then
  set -a && source .env && set +a
fi

if ! command -v cast >/dev/null 2>&1; then
  echo "cast is required" >&2
  exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "jq is required" >&2
  exit 1
fi

CHAIN_ROWS=(
  "1|MAINNET|ETHEREUM_MAINNET_RPC|CW_BRIDGE_MAINNET"
  "10|OPTIMISM|OPTIMISM_MAINNET_RPC|CW_BRIDGE_OPTIMISM"
  "25|CRONOS|CRONOS_MAINNET_RPC|CW_BRIDGE_CRONOS"
  "56|BSC|BSC_MAINNET_RPC|CW_BRIDGE_BSC"
  "100|GNOSIS|GNOSIS_MAINNET_RPC|CW_BRIDGE_GNOSIS"
  "137|POLYGON|POLYGON_MAINNET_RPC|CW_BRIDGE_POLYGON"
  "42161|ARBITRUM|ARBITRUM_MAINNET_RPC|CW_BRIDGE_ARBITRUM"
  "42220|CELO|CELO_MAINNET_RPC|CW_BRIDGE_CELO"
  "43114|AVALANCHE|AVALANCHE_MAINNET_RPC|CW_BRIDGE_AVALANCHE"
  "8453|BASE|BASE_MAINNET_RPC|CW_BRIDGE_BASE"
)

TOKENS=(
  "CWUSDT"
  "CWUSDC"
  "CWAUSDT"
  "CWUSDW"
  "CWEURC"
  "CWEURT"
  "CWGBPC"
  "CWGBPT"
  "CWAUDC"
  "CWJPYC"
  "CWCHFC"
  "CWCADC"
  "CWXAUC"
  "CWXAUT"
)

CHAIN_FILTER="${CW_ROLE_CHAINS:-1 10 25 56 100 137 42161 42220 43114 8453}"
TOKEN_FILTER="${CW_ROLE_TOKENS:-}"
REVOKE_DEPLOYER="${CW_ROLE_REVOKE_DEPLOYER:-1}"
FREEZE_IF_SUPPORTED="${CW_ROLE_FREEZE_IF_SUPPORTED:-1}"
EXTRA_REVOKE_RAW="${CW_ROLE_EXTRA_REVOKE:-}"
KEEP_ALLOWLIST_RAW="${CW_ROLE_KEEP_ALLOWLIST:-}"
ROLE_FROM_BLOCK="${CW_ROLE_FROM_BLOCK:-earliest}"

MINTER_ROLE="$(cast keccak "MINTER_ROLE")"
BURNER_ROLE="$(cast keccak "BURNER_ROLE")"
DEFAULT_ADMIN_ROLE="0x0000000000000000000000000000000000000000000000000000000000000000"

send_cmd() {
  if [[ "$DRY_RUN" -eq 1 ]]; then
    local rendered=()
    local redact_next=0
    local arg
    for arg in "$@"; do
      if [[ "$redact_next" -eq 1 ]]; then
        rendered+=("<redacted>")
        redact_next=0
        continue
      fi
      if [[ "$arg" == "--private-key" ]]; then
        rendered+=("$arg")
        redact_next=1
        continue
      fi
      rendered+=("$arg")
    done
    echo "[dry-run] ${rendered[*]}"
    return 0
  fi
  "$@"
}

bool_call() {
  local rpc="$1"
  local contract="$2"
  local sig="$3"
  shift 3
  cast call "$contract" "$sig" "$@" --rpc-url "$rpc" 2>/dev/null
}

normalize_address() {
  local value="${1,,}"
  value="${value#0x}"
  if [[ ${#value} -lt 40 ]]; then
    return 1
  fi
  echo "0x${value: -40}"
}

append_unique_address() {
  local __var_name="$1"
  local candidate="${2,,}"
  [[ -n "$candidate" ]] || return 0
  local current="${!__var_name:-}"
  if [[ " $current " != *" $candidate "* ]]; then
    printf -v "$__var_name" '%s%s ' "$current" "$candidate"
  fi
}

load_csv_addresses() {
  local raw="$1"
  local __out_var="$2"
  local item normalized
  IFS=',' read -r -a _addr_items <<< "$raw"
  for item in "${_addr_items[@]}"; do
    item="${item//[[:space:]]/}"
    [[ -n "$item" ]] || continue
    normalized="$(normalize_address "$item" || true)"
    [[ -n "$normalized" ]] || continue
    append_unique_address "$__out_var" "$normalized"
  done
}

discover_role_holders() {
  local rpc="$1"
  local token="$2"
  local role="$3"
  cast logs "RoleGranted(bytes32,address,address)" "$role" \
    --from-block "$ROLE_FROM_BLOCK" --to-block latest --address "$token" --rpc-url "$rpc" --json 2>/dev/null |
    jq -r '.[].topics[2] // empty' |
    while read -r topic; do
      normalize_address "$topic" || true
    done
}

function_exists() {
  local rpc="$1"
  local contract="$2"
  local sig="$3"
  cast call "$contract" "$sig" --rpc-url "$rpc" >/dev/null 2>&1
}

grant_role_if_supported() {
  local rpc="$1"
  local token="$2"
  local role="$3"
  local target="$4"
  local current
  current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$target" || true)"
  if [[ "$current" != "true" ]]; then
    send_cmd cast send "$token" "grantRole(bytes32,address)" "$role" "$target" \
      --rpc-url "$rpc" --private-key "$PRIVATE_KEY"
  fi
}

revoke_role_if_present() {
  local rpc="$1"
  local token="$2"
  local role="$3"
  local target="$4"
  local current
  current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$target" || true)"
  if [[ "$current" == "true" ]]; then
    send_cmd cast send "$token" "revokeRole(bytes32,address)" "$role" "$target" \
      --rpc-url "$rpc" --private-key "$PRIVATE_KEY"
  fi
}

role_label() {
  case "$1" in
    "$DEFAULT_ADMIN_ROLE") echo "DEFAULT_ADMIN_ROLE" ;;
    "$MINTER_ROLE") echo "MINTER_ROLE" ;;
    "$BURNER_ROLE") echo "BURNER_ROLE" ;;
    *) echo "$1" ;;
  esac
}

for row in "${CHAIN_ROWS[@]}"; do
  IFS='|' read -r chain_id chain_key rpc_var bridge_var <<< "$row"
  if [[ " $CHAIN_FILTER " != *" $chain_id "* ]]; then
    continue
  fi

  rpc="${!rpc_var:-}"
  bridge="${!bridge_var:-}"
  deployer="${DEPLOYER_ADDRESS:-${DEPLOYER_WALLET:-${DEPLOYER_EOA:-${DEFAULT_FROM_ADDRESS:-}}}}"
  if [[ -z "$deployer" && -n "${PRIVATE_KEY:-}" ]]; then
    deployer="$(cast wallet address --private-key "$PRIVATE_KEY" 2>/dev/null || true)"
  fi
  bridge="$(normalize_address "$bridge" || true)"
  deployer="$(normalize_address "$deployer" || true)"

  if [[ -z "$rpc" || -z "$bridge" ]]; then
    echo "Skip chain $chain_id ($chain_key): missing rpc or bridge env"
    continue
  fi

  echo "=== Chain $chain_id ($chain_key) ==="
  echo "Bridge: $bridge"

  for token_prefix in "${TOKENS[@]}"; do
    if [[ -n "$TOKEN_FILTER" && " $TOKEN_FILTER " != *" $token_prefix "* ]]; then
      continue
    fi
    token_var="${token_prefix}_${chain_key}"
    token="${!token_var:-}"
    if [[ -z "$token" || "$token" == "0x0000000000000000000000000000000000000000" ]]; then
      continue
    fi

    echo "-- $token_var $token"

    grant_role_if_supported "$rpc" "$token" "$MINTER_ROLE" "$bridge"
    grant_role_if_supported "$rpc" "$token" "$BURNER_ROLE" "$bridge"

    keep_allowlist=""
    append_unique_address keep_allowlist "$bridge"
    load_csv_addresses "$KEEP_ALLOWLIST_RAW" keep_allowlist

    revoke_candidates=""
    while read -r discovered; do
      [[ -n "$discovered" ]] || continue
      append_unique_address revoke_candidates "$discovered"
    done < <(discover_role_holders "$rpc" "$token" "$MINTER_ROLE")
    while read -r discovered; do
      [[ -n "$discovered" ]] || continue
      append_unique_address revoke_candidates "$discovered"
    done < <(discover_role_holders "$rpc" "$token" "$BURNER_ROLE")

    if [[ "$REVOKE_DEPLOYER" == "1" && -n "$deployer" ]]; then
      append_unique_address revoke_candidates "$deployer"
    fi
    load_csv_addresses "$EXTRA_REVOKE_RAW" revoke_candidates

    for addr in $revoke_candidates; do
      [[ " $keep_allowlist " == *" ${addr,,} "* ]] && continue
      revoke_role_if_present "$rpc" "$token" "$MINTER_ROLE" "$addr"
      revoke_role_if_present "$rpc" "$token" "$BURNER_ROLE" "$addr"
    done

    if [[ "$FREEZE_IF_SUPPORTED" == "1" ]]; then
      if function_exists "$rpc" "$token" "operationalRolesFrozen()(bool)"; then
        frozen="$(cast call "$token" "operationalRolesFrozen()(bool)" --rpc-url "$rpc" 2>/dev/null || true)"
        if [[ "$frozen" != "true" ]]; then
          send_cmd cast send "$token" "freezeOperationalRoles()" \
            --rpc-url "$rpc" --private-key "$PRIVATE_KEY" --gas-limit 80000
        fi
      fi
    fi

    for role in "$DEFAULT_ADMIN_ROLE" "$MINTER_ROLE" "$BURNER_ROLE"; do
      summary_holders=""
      while read -r discovered; do
        [[ -n "$discovered" ]] || continue
        current="$(bool_call "$rpc" "$token" "hasRole(bytes32,address)(bool)" "$role" "$discovered" || true)"
        if [[ "$current" == "true" ]]; then
          append_unique_address summary_holders "$discovered"
        fi
      done < <(discover_role_holders "$rpc" "$token" "$role")
      echo "   $(role_label "$role"): ${summary_holders:-<none discovered>}"
    done
  done
done
