#!/usr/bin/env node
/**
 * Query balances from Besu RPC nodes (VMID 2500-2502 by default)
 *
 * Note: Only RPC nodes (2500-2502) expose RPC endpoints.
 * Validators (1000-1004) and sentries (1500-1503) don't have RPC enabled.
 * 
 * Usage:
 *   RPC_URLS="http://192.168.11.23:8545,http://192.168.11.24:8545,http://192.168.11.25:8545" \
 *   WETH9_ADDRESS="0x..." \
 *   WETH10_ADDRESS="0x..." \
 *   node scripts/besu_balances_106_117.js
 * 
 * Or use template (defaults to RPC nodes 115-117):
 *   RPC_TEMPLATE="http://192.168.11.{vmid}:8545" \
 *   node scripts/besu_balances_106_117.js
 */

import { ethers } from 'ethers';

// Configuration
const WALLET_ADDRESS = '0xa55A4B57A91561e9df5a883D4883Bd4b1a7C4882';
// RPC nodes are 2500-2502; validators (1000-1004) and sentries (1500-1503) don't expose RPC
const VMID_START = process.env.VMID_START ? parseInt(process.env.VMID_START) : 2500;
const VMID_END = process.env.VMID_END ? parseInt(process.env.VMID_END) : 2502;
const CONCURRENCY_LIMIT = 4;
const REQUEST_TIMEOUT = 15000; // 15 seconds

// ERC-20 minimal ABI
const ERC20_ABI = [
    'function balanceOf(address owner) view returns (uint256)',
    'function decimals() view returns (uint8)',
    'function symbol() view returns (string)'
];

// Default token addresses
const WETH9_ADDRESS = process.env.WETH9_ADDRESS || '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
const WETH10_ADDRESS = process.env.WETH10_ADDRESS || null;

// VMID to IP mapping for better VMID detection
const VMID_TO_IP = {
    '192.168.11.13': 106,
    '192.168.11.14': 107,
    '192.168.11.15': 108,
    '192.168.11.16': 109,
    '192.168.11.18': 110,
    '192.168.11.19': 111,
    '192.168.11.20': 112,
    '192.168.11.21': 113,
    '192.168.11.22': 114,
    '192.168.11.23': 115,
    '192.168.11.24': 116,
    '192.168.11.25': 117,
};

// Reverse IP to VMID mapping (VMID -> IP)
const IP_TO_VMID = {
    106: '192.168.11.13',
    107: '192.168.11.14',
    108: '192.168.11.15',
    109: '192.168.11.16',
    110: '192.168.11.18',
    111: '192.168.11.19',
    112: '192.168.11.20',
    113: '192.168.11.21',
    114: '192.168.11.22',
    115: '192.168.11.23',
    116: '192.168.11.24',
    117: '192.168.11.25',
};

// RPC endpoint configuration
function getRpcUrls() {
    if (process.env.RPC_URLS) {
        return process.env.RPC_URLS.split(',').map(url => url.trim());
    }
    
    const template = process.env.RPC_TEMPLATE;
    const urls = [];
    for (let vmid = VMID_START; vmid <= VMID_END; vmid++) {
        if (template && template.includes('{vmid}')) {
            // Use template if provided
            urls.push(template.replace('{vmid}', vmid.toString()));
        } else {
            // Use actual IP from mapping (default behavior)
            const ip = IP_TO_VMID[vmid];
            if (ip) {
                urls.push(`http://${ip}:8545`);
            } else {
                // Fallback to template or direct VMID
                const fallbackTemplate = template || 'http://192.168.11.{vmid}:8545';
                urls.push(fallbackTemplate.replace('{vmid}', vmid.toString()));
            }
        }
    }
    return urls;
}

// Get VMID from URL
function getVmidFromUrl(url) {
    // Try IP mapping first
    const ipMatch = url.match(/(\d+\.\d+\.\d+\.\d+)/);
    if (ipMatch && VMID_TO_IP[ipMatch[1]]) {
        return VMID_TO_IP[ipMatch[1]];
    }
    // Fallback to pattern matching
    const match = url.match(/(?:\.|:)(\d{3})(?::|\/)/);
    return match ? parseInt(match[1]) : null;
}

// Format balance with decimals
function formatBalance(balance, decimals, symbol) {
    const formatted = ethers.formatUnits(balance, decimals);
    return `${formatted} ${symbol}`;
}

// Query single RPC endpoint
async function queryRpc(url, walletAddress) {
    const vmid = getVmidFromUrl(url) || 'unknown';
    const result = {
        vmid,
        url,
        success: false,
        chainId: null,
        blockNumber: null,
        ethBalance: null,
        ethBalanceWei: null,
        weth9: null,
        weth10: null,
        errors: []
    };

    try {
        // Create provider with timeout
        const provider = new ethers.JsonRpcProvider(url, undefined, {
            staticNetwork: false,
            batchMaxCount: 1,
            batchMaxSize: 250000,
            staticNetworkCode: false,
        });

        // Set timeout
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);

        try {
            // Health checks
            const [chainId, blockNumber] = await Promise.all([
                provider.getNetwork().then(n => Number(n.chainId)),
                provider.getBlockNumber()
            ]);
            
            clearTimeout(timeoutId);
            result.chainId = chainId;
            result.blockNumber = blockNumber;

            // Query ETH balance
            const ethBalance = await provider.getBalance(walletAddress);
            result.ethBalanceWei = ethBalance.toString();
            result.ethBalance = ethers.formatEther(ethBalance);

            // Query WETH9 balance
            try {
                const weth9Contract = new ethers.Contract(WETH9_ADDRESS, ERC20_ABI, provider);
                const [balance, decimals, symbol] = await Promise.all([
                    weth9Contract.balanceOf(walletAddress),
                    weth9Contract.decimals(),
                    weth9Contract.symbol()
                ]);
                result.weth9 = {
                    balance: balance.toString(),
                    decimals: Number(decimals),
                    symbol: symbol,
                    formatted: formatBalance(balance, decimals, symbol)
                };
            } catch (err) {
                result.errors.push(`WETH9: ${err.message}`);
            }

            // Query WETH10 balance (if address provided)
            if (WETH10_ADDRESS) {
                try {
                    const weth10Contract = new ethers.Contract(WETH10_ADDRESS, ERC20_ABI, provider);
                    const [balance, decimals, symbol] = await Promise.all([
                        weth10Contract.balanceOf(walletAddress),
                        weth10Contract.decimals(),
                        weth10Contract.symbol()
                    ]);
                    result.weth10 = {
                        balance: balance.toString(),
                        decimals: Number(decimals),
                        symbol: symbol,
                        formatted: formatBalance(balance, decimals, symbol)
                    };
                } catch (err) {
                    result.errors.push(`WETH10: ${err.message}`);
                }
            }

            result.success = true;
        } catch (err) {
            clearTimeout(timeoutId);
            throw err;
        }
    } catch (err) {
        result.errors.push(err.message);
        result.success = false;
    }

    return result;
}

// Process URLs with concurrency limit
async function processWithConcurrencyLimit(urls, walletAddress, limit) {
    const results = [];
    const executing = [];

    for (const url of urls) {
        const promise = queryRpc(url, walletAddress).then(result => {
            results.push(result);
        });

        executing.push(promise);

        if (executing.length >= limit) {
            await Promise.race(executing);
            executing.splice(executing.findIndex(p => p === promise), 1);
        }
    }

    await Promise.all(executing);
    return results.sort((a, b) => (a.vmid === 'unknown' ? 999 : a.vmid) - (b.vmid === 'unknown' ? 999 : b.vmid));
}

// Format and print results
function printResults(results) {
    let successCount = 0;

    for (const result of results) {
        console.log(`VMID: ${result.vmid}`);
        console.log(`RPC: ${result.url}`);
        
        if (result.success) {
            successCount++;
            console.log(`chainId: ${result.chainId}`);
            console.log(`block: ${result.blockNumber}`);
            console.log(`ETH: ${result.ethBalance} (wei: ${result.ethBalanceWei})`);
            
            if (result.weth9) {
                console.log(`WETH9: ${result.weth9.formatted} (raw: ${result.weth9.balance})`);
            } else {
                console.log(`WETH9: error (see errors below)`);
            }
            
            if (WETH10_ADDRESS) {
                if (result.weth10) {
                    console.log(`WETH10: ${result.weth10.formatted} (raw: ${result.weth10.balance})`);
                } else {
                    console.log(`WETH10: error (see errors below)`);
                }
            } else {
                console.log(`WETH10: skipped (missing address)`);
            }
            
            if (result.errors.length > 0) {
                console.log(`Errors: ${result.errors.join('; ')}`);
            }
        } else {
            console.log(`Status: FAILED`);
            if (result.errors.length > 0) {
                console.log(`Errors: ${result.errors.join('; ')}`);
            }
        }
        
        console.log('---');
    }

    return successCount;
}

// Main execution
async function main() {
    console.log('Querying balances from Besu RPC nodes...');
    console.log(`Wallet: ${WALLET_ADDRESS}`);
    console.log(`WETH9: ${WETH9_ADDRESS}`);
    console.log(`WETH10: ${WETH10_ADDRESS || 'not set (will skip)'}`);
    console.log('');

    const rpcUrls = getRpcUrls();
    console.log(`Checking ${rpcUrls.length} RPC endpoints (concurrency: ${CONCURRENCY_LIMIT})...`);
    console.log('');

    const results = await processWithConcurrencyLimit(rpcUrls, WALLET_ADDRESS, CONCURRENCY_LIMIT);
    const successCount = printResults(results);

    console.log('');
    console.log(`Summary: ${successCount} out of ${results.length} RPC endpoints succeeded`);

    // Exit code: 0 if at least one succeeded, 1 if all failed
    process.exit(successCount > 0 ? 0 : 1);
}

main().catch(err => {
    console.error('Fatal error:', err);
    process.exit(1);
});

