// Price fetching with 5-minute cache, USD formatting, value aggregation.

const { getTopTokenPrices } = require("./tokenList");

const PRICE_CACHE_TTL = 300000; // 5 minutes

const prices = {};
let lastFetchedAt = 0;

async function refreshPrices() {
    // Testnet tokens have no real market value — skip price fetching
    // and clear any stale mainnet prices so the UI shows no USD values.
    const { currentNetwork } = require("./state");
    if (currentNetwork().isTestnet) {
        clearPrices();
        return;
    }
    const now = Date.now();
    if (now - lastFetchedAt < PRICE_CACHE_TTL) return;
    try {
        const fetched = await getTopTokenPrices(25);
        Object.assign(prices, fetched);
        lastFetchedAt = now;
    } catch {
        // prices stay stale on error
    }
}

// Clear all cached prices and reset the fetch timestamp so the
// next refreshPrices() call will fetch fresh data.
function clearPrices() {
    for (const key of Object.keys(prices)) {
        delete prices[key];
    }
    lastFetchedAt = 0;
}

// Return the USD price for a symbol, or null on testnet / unknown.
function getPrice(symbol) {
    const { currentNetwork } = require("./state");
    if (currentNetwork().isTestnet) return null;
    return prices[symbol] || null;
}

function formatUsd(amount) {
    if (amount === null || amount === undefined || isNaN(amount)) return "";
    if (amount === 0) return "$0.00";
    if (amount < 0.01) return "< $0.01";
    return (
        "$" +
        amount.toLocaleString("en-US", {
            minimumFractionDigits: 2,
            maximumFractionDigits: 2,
        })
    );
}

// What an address is worth, as { usd, partial }.
//
// Prices are fetched for the top 25 tokens only, so an address can hold real
// assets this code has no price for. Adding up the priced ones and calling the
// result the total states a number the holdings do not support: an address
// holding nothing but unpriced tokens comes out at $0.00, which tells the user
// their address is worth nothing when it may hold a great deal. Worth zero and
// worth an unknown amount are separate facts and get separate fields, the same
// way an absent holders_count is not a count of zero.
//
//   usd:     the value of the holdings a price is known for, or null when
//            nothing is knowable at all — testnet, or before the first fetch.
//   partial: the address also holds a token with no price, so usd is a floor
//            and not the total.
//
// Render it through formatAddressTotal() rather than reading usd alone.
function getAddressValue(addr) {
    const { currentNetwork } = require("./state");
    if (currentNetwork().isTestnet) return { usd: null, partial: false };
    if (!prices.ETH) return { usd: null, partial: false };
    let usd = parseFloat(addr.balance || "0") * prices.ETH;
    let partial = false;
    for (const token of addr.tokenBalances || []) {
        // A null balance is a holding whose scale nothing knows, so it has no
        // quantity to price — but it is still a holding, and a total that
        // silently omits it would read as complete. That is exactly what
        // `partial` is for (https://git.eeqj.de/sneak/AutistMask/issues/349).
        if (token.balance == null) {
            partial = true;
            continue;
        }
        const tokenBal = parseFloat(token.balance);
        // A balance of zero is not a holding: it can neither add to the total
        // nor make it incomplete. Anything that is not a number at all is not
        // a holding this can price either, and is left to the same rule.
        if (!(tokenBal > 0)) continue;
        if (prices[token.symbol]) {
            usd += tokenBal * prices[token.symbol];
        } else {
            partial = true;
        }
    }
    return { usd, partial };
}

// The same pair for a whole wallet, and for every wallet at once. One
// unpriced holding anywhere makes the sum a floor, so partial carries up.
function getWalletValue(wallet) {
    return sumValues(wallet.addresses.map(getAddressValue));
}

function getTotalValue(wallets) {
    return sumValues(wallets.map(getWalletValue));
}

function sumValues(values) {
    let usd = null;
    let partial = false;
    for (const value of values) {
        if (value.usd === null) continue;
        usd = (usd === null ? 0 : usd) + value.usd;
        partial = partial || value.partial;
    }
    return { usd, partial };
}

// The one rendering of an address total, so no screen says it differently.
//
// A partial total is shown and named as partial: the figure is the ETH and
// priced tokens the user does hold, which is worth having, and suppressing it
// would throw away a number that is correct as far as it goes. What is never
// shown is a figure covering no holdings at all — the $0.00 sum of an empty
// set beside a list of tokens is the bug this replaces.
function formatAddressTotal(value) {
    if (!value || value.usd === null) return "";
    if (!value.partial) return "Total: " + formatUsd(value.usd);
    if (value.usd > 0) {
        return "Total: " + formatUsd(value.usd) + " plus unpriced tokens";
    }
    return "Total: unpriced tokens only";
}

module.exports = {
    prices,
    refreshPrices,
    clearPrices,
    getPrice,
    formatUsd,
    formatAddressTotal,
    getAddressValue,
    getWalletValue,
    getTotalValue,
};
