// Balance fetching: ETH balances via RPC, ERC-20 token balances via
// Blockscout, ENS reverse lookup via RPC.

const {
    JsonRpcProvider,
    Network,
    Contract,
    formatEther,
    formatUnits,
} = require("ethers");
const { ERC20_ABI } = require("./constants");
const { log, debugFetch } = require("./log");
const { deriveAddressFromXpub } = require("./wallet");
const { KNOWN_SYMBOLS, TOKEN_BY_ADDRESS } = require("./tokenList");

// Use a static network to skip auto-detection (which can fail and cause
// "could not coalesce error" on some RPC endpoints like Cloudflare).
// Accepts an optional networkName ("mainnet" or "sepolia") for the static
// network hint so ethers picks the right chain parameters.  When omitted,
// reads the currently selected network from extension state.
function getProvider(rpcUrl, networkName) {
    // Lazy require to avoid circular dependency issues at module scope.
    const { currentNetwork } = require("./state");
    const name = networkName || currentNetwork().id;
    const net = Network.from(name);
    return new JsonRpcProvider(rpcUrl, net, { staticNetwork: net });
}

function formatBalance(wei) {
    const eth = formatEther(wei);
    const parts = eth.split(".");
    if (parts.length === 1) return eth + ".0";
    const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
    return parts[0] + "." + dec;
}

function formatTokenBalance(raw, decimals) {
    const val = formatUnits(raw, decimals);
    const parts = val.split(".");
    if (parts.length === 1) return val + ".0";
    const dec = parts[1].slice(0, 6).replace(/0+$/, "") || "0";
    return parts[0] + "." + dec;
}

// Fetch token balances for a single address from Blockscout.
// Returns [{ address, symbol, decimals, balance }].
// Filters out spam: only shows tokens that are in the known token list,
// explicitly tracked by the user, or have >= 1000 holders.
async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
    try {
        const resp = await debugFetch(
            blockscoutUrl + "/addresses/" + address + "/token-balances",
        );
        if (!resp.ok) {
            log.errorf("blockscout token-balances:", resp.status);
            return null;
        }
        const items = await resp.json();
        if (!Array.isArray(items)) return null;

        const trackedSet = new Set(
            (trackedTokens || []).map((t) => t.address.toLowerCase()),
        );

        const balances = [];
        for (const item of items) {
            if (item.token?.type !== "ERC-20") continue;
            const decimals = parseInt(item.token.decimals || "18", 10);
            const bal = formatTokenBalance(item.value || "0", decimals);
            if (bal === "0.0") continue;

            const tokenAddr = (item.token.address_hash || "").toLowerCase();
            const holders = parseInt(item.token.holders_count || "0", 10);
            const isKnown = TOKEN_BY_ADDRESS.has(tokenAddr);
            const isTracked = trackedSet.has(tokenAddr);
            const hasEnoughHolders = holders >= 1000;

            // Skip spam tokens the user never asked to see
            if (!isKnown && !isTracked && !hasEnoughHolders) continue;

            // Skip tokens spoofing a known symbol from a different address
            const sym = (item.token.symbol || "").toUpperCase();
            const legitAddr = KNOWN_SYMBOLS.get(sym);
            if (
                legitAddr !== undefined &&
                legitAddr !== null &&
                tokenAddr !== legitAddr
            )
                continue;

            balances.push({
                address: item.token.address_hash,
                name: item.token.name || "",
                symbol: item.token.symbol || "???",
                decimals: decimals,
                balance: bal,
                holders: holders,
            });
        }
        return balances;
    } catch (e) {
        log.errorf("fetchTokenBalances failed:", e.message);
        return null;
    }
}

// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
async function refreshBalances(wallets, rpcUrl, blockscoutUrl, trackedTokens) {
    log.debugf("refreshBalances start, rpc:", rpcUrl);
    const provider = getProvider(rpcUrl);
    const updates = [];

    for (const wallet of wallets) {
        for (const addr of wallet.addresses) {
            // ETH balance
            updates.push(
                provider
                    .getBalance(addr.address)
                    .then((bal) => {
                        addr.balance = formatBalance(bal);
                        log.debugf("ETH balance", addr.address, addr.balance);
                    })
                    .catch((e) => {
                        log.errorf(
                            "ETH balance failed",
                            addr.address,
                            e.shortMessage || e.message,
                        );
                    }),
            );

            // ENS reverse lookup — only overwrite on success so that
            // transient RPC errors don't wipe a previously resolved name.
            updates.push(
                provider
                    .lookupAddress(addr.address)
                    .then((name) => {
                        addr.ensName = name || null;
                        log.debugf(
                            "ENS reverse",
                            addr.address,
                            "->",
                            addr.ensName,
                        );
                    })
                    .catch((e) => {
                        log.errorf(
                            "ENS reverse failed",
                            addr.address,
                            e.message,
                        );
                        // Keep existing addr.ensName if we had one
                    }),
            );

            // ERC-20 token balances via Blockscout
            updates.push(
                fetchTokenBalances(
                    addr.address,
                    blockscoutUrl,
                    trackedTokens,
                ).then((balances) => {
                    if (balances !== null) {
                        addr.tokenBalances = balances;
                        log.debugf(
                            "Token balances",
                            addr.address,
                            balances.length,
                            "tokens",
                        );
                    }
                }),
            );
        }
    }

    await Promise.all(updates);
    log.debugf("refreshBalances done");
}

// Look up token metadata from its contract.
// Calls symbol() and decimals() to verify it implements ERC-20.
async function lookupTokenInfo(contractAddress, rpcUrl) {
    log.debugf("lookupTokenInfo", contractAddress, "rpc:", rpcUrl);
    const provider = getProvider(rpcUrl);
    const contract = new Contract(contractAddress, ERC20_ABI, provider);

    let name, symbol, decimals;
    try {
        symbol = await contract.symbol();
        log.debugf("symbol() =", symbol);
    } catch (e) {
        log.errorf("symbol() failed:", e.shortMessage || e.message);
        throw new Error("Not a valid ERC-20 token (symbol() failed).");
    }

    try {
        decimals = await contract.decimals();
        log.debugf("decimals() =", decimals);
    } catch (e) {
        log.errorf("decimals() failed:", e.shortMessage || e.message);
        throw new Error("Not a valid ERC-20 token (decimals() failed).");
    }

    try {
        name = await contract.name();
        log.debugf("name() =", name);
    } catch (e) {
        log.warnf("name() failed, using symbol as name:", e.message);
        name = symbol;
    }

    // Truncate to prevent storage of excessively long values from RPC
    name = String(name).slice(0, 64);
    symbol = String(symbol).slice(0, 12);

    log.infof("Token resolved:", symbol, "decimals", Number(decimals));
    return { name, symbol, decimals: Number(decimals) };
}

// Derive HD addresses starting from index 0 and check for on-chain activity.
// Checks gapLimit addresses in parallel per batch.  Stops when an entire
// batch has no used addresses (i.e. gapLimit consecutive empty addresses).
// Returns { addresses: [{ address, index }], nextIndex }.
async function scanForAddresses(xpub, rpcUrl, gapLimit = 5) {
    log.debugf("scanForAddresses start, gapLimit:", gapLimit);
    const provider = getProvider(rpcUrl);
    const used = [];
    let checked = 0;
    let checkUpTo = gapLimit;

    while (checked < checkUpTo) {
        const batch = [];
        for (let i = checked; i < checkUpTo; i++) {
            const addr = deriveAddressFromXpub(xpub, i);
            batch.push({ addr, index: i });
        }

        const results = await Promise.all(
            batch.map(async ({ addr, index }) => {
                try {
                    const [balance, txCount] = await Promise.all([
                        provider.getBalance(addr),
                        provider.getTransactionCount(addr),
                    ]);
                    return { addr, index, isUsed: balance > 0n || txCount > 0 };
                } catch (e) {
                    log.errorf(
                        "scanForAddresses check failed",
                        addr,
                        e.shortMessage || e.message,
                    );
                    return { addr, index, isUsed: false };
                }
            }),
        );

        checked = checkUpTo;

        for (const r of results) {
            if (r.isUsed) {
                used.push({ address: r.addr, index: r.index });
                log.debugf("scanForAddresses used", r.addr, "index:", r.index);
                checkUpTo = Math.max(checkUpTo, r.index + 1 + gapLimit);
            }
        }
    }

    used.sort((a, b) => a.index - b.index);
    const nextIndex = used.length > 0 ? used[used.length - 1].index + 1 : 1;
    log.infof(
        "scanForAddresses done, found:",
        used.length,
        "nextIndex:",
        nextIndex,
    );
    return { addresses: used, nextIndex };
}

module.exports = {
    refreshBalances,
    lookupTokenInfo,
    getProvider,
    scanForAddresses,
};
