/**
 * CoinGecko price feed with 5-minute in-memory cache.
 * Provides USD prices for native coins and ERC-20 tokens.
 */

// CoinGecko chain ID → platform ID mapping
const CHAIN_PLATFORM: Record<string, string> = {
	"1": "ethereum",
	"10": "optimistic-ethereum",
	"100": "xdai",
	"137": "polygon-pos",
	"8453": "base",
	"42161": "arbitrum-one",
	"56": "binance-smart-chain",
	"43114": "avalanche",
	"42220": "celo",
	"324": "zksync",
	"59144": "linea",
};

// CoinGecko native coin IDs per chain
const NATIVE_COIN_ID: Record<string, string> = {
	"1": "ethereum",
	"10": "ethereum",
	"100": "dai",
	"137": "matic-network",
	"8453": "ethereum",
	"42161": "ethereum",
	"56": "binancecoin",
	"43114": "avalanche-2",
	"42220": "celo",
	"324": "ethereum",
	"59144": "ethereum",
};

interface CacheEntry {
	prices: Map<string, number>; // address (lowercase) → USD price
	nativePrice: number;
	cgAvailable: boolean; // true if CoinGecko successfully returned token data
	ts: number;
}

const TTL = 5 * 60 * 1000; // 5 minutes
const cache = new Map<string, CacheEntry>();
const inFlight = new Map<string, Promise<CacheEntry>>();

const CG_API_KEY = process.env.COINGECKO_API_KEY || "";

function cgUrl(url: string): string {
	if (!CG_API_KEY) return url;
	const sep = url.includes("?") ? "&" : "?";
	return `${url}${sep}x_cg_demo_api_key=${CG_API_KEY}`;
}

async function cgFetch(url: string): Promise<any> {
	const res = await fetch(cgUrl(url), {
		headers: { accept: "application/json" },
		signal: AbortSignal.timeout(10000),
	});
	if (res.status === 429) {
		console.warn("[price-feed] CoinGecko rate limited, waiting 60s...");
		await new Promise((r) => setTimeout(r, 60000));
		const retry = await fetch(cgUrl(url), {
			headers: { accept: "application/json" },
			signal: AbortSignal.timeout(10000),
		});
		if (!retry.ok) return null;
		return retry.json();
	}
	if (!res.ok) return null;
	return res.json();
}

/** Fetch native coin price for a chain */
export async function getNativePrice(chainId: string): Promise<number> {
	const entry = cache.get(chainId);
	if (entry && Date.now() - entry.ts < TTL) return entry.nativePrice;

	const coinId = NATIVE_COIN_ID[chainId];
	if (!coinId) return 0;

	const data = await cgFetch(
		`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`,
	);
	return data?.[coinId]?.usd ?? 0;
}

/** Fetch token prices for contract addresses on a chain */
export async function getTokenPrices(
	chainId: string,
	addresses: string[],
): Promise<{ prices: Map<string, number>; available: boolean }> {
	const platform = CHAIN_PLATFORM[chainId];
	if (!platform || addresses.length === 0) return { prices: new Map(), available: false };

	const lower = [...new Set(addresses.map((a) => a.toLowerCase()))];
	const prices = new Map<string, number>();

	// CoinGecko free tier limits to 1 address per request.
	// For 1 token, do a single lookup. For multiple, try batch (works with Pro/Demo keys).
	if (lower.length === 1) {
		const addr = lower[0];
		const data = await cgFetch(
			`https://api.coingecko.com/api/v3/simple/token_price/${platform}?contract_addresses=${addr}&vs_currencies=usd`,
		);
		if (data && !data.error_code) {
			if (data[addr]?.usd) prices.set(addr, data[addr].usd);
			return { prices, available: true };
		}
		return { prices, available: false };
	}

	// Multiple tokens: batch request (succeeds with CoinGecko Demo/Pro API key)
	const data = await cgFetch(
		`https://api.coingecko.com/api/v3/simple/token_price/${platform}?contract_addresses=${lower.join(",")}&vs_currencies=usd`,
	);
	if (data && !data.error_code) {
		for (const addr of lower) {
			if (data[addr]?.usd) prices.set(addr, data[addr].usd);
		}
		return { prices, available: true };
	}
	// Batch failed (free tier limit) — degrade gracefully, no spam filtering
	return { prices, available: false };
}

/** Fetch and cache all prices for a chain (native + tokens) */
async function fetchChainPrices(
	chainId: string,
	tokenAddresses: string[],
): Promise<CacheEntry> {
	const existing = cache.get(chainId);
	if (existing && Date.now() - existing.ts < TTL) return existing;

	// Deduplicate concurrent requests for same chain
	const key = chainId;
	const pending = inFlight.get(key);
	if (pending) return pending;

	const promise = (async (): Promise<CacheEntry> => {
		try {
			const [nativePrice, tokenResult] = await Promise.all([
				getNativePrice(chainId),
				getTokenPrices(chainId, tokenAddresses),
			]);
			const entry: CacheEntry = {
				prices: tokenResult.prices,
				nativePrice,
				cgAvailable: tokenResult.available,
				ts: Date.now(),
			};
			cache.set(chainId, entry);
			return entry;
		} finally {
			inFlight.delete(key);
		}
	})();

	inFlight.set(key, promise);
	return promise;
}

interface BalanceItem {
	tokenAddress: string | null;
	token: { name: string; symbol: string; decimals: number };
	balance: string;
	fiatBalance: string;
	fiatConversion: string;
}

/**
 * Enrich balance items with USD prices from CoinGecko.
 * Fills in fiatBalance and fiatConversion for items that have "0" values.
 */
export async function enrichWithPrices(
	balances: BalanceItem[],
	chainId: string,
	options?: { filterSpam?: boolean },
): Promise<BalanceItem[]> {
	// Skip testnets and unsupported chains — no CoinGecko data to verify against
	if (!CHAIN_PLATFORM[chainId] && !NATIVE_COIN_ID[chainId]) return balances;

	// Check if any balance actually needs pricing
	const needsPricing = balances.some(
		(b) =>
			BigInt(b.balance || "0") > 0n &&
			(b.fiatBalance === "0" || b.fiatBalance === "" || !b.fiatBalance),
	);
	if (!needsPricing) return balances;

	const tokenAddresses = balances
		.filter((b) => b.tokenAddress && b.tokenAddress !== "0x0000000000000000000000000000000000000000")
		.map((b) => b.tokenAddress!);

	try {
		const priceData = await fetchChainPrices(chainId, tokenAddresses);

		const enriched = balances.map((b) => {
			// Skip if already has a real fiat value
			if (b.fiatBalance && b.fiatBalance !== "0" && parseFloat(b.fiatBalance) > 0) {
				return b;
			}

			const balWei = BigInt(b.balance || "0");
			if (balWei === 0n) return b;

			let price = 0;
			if (!b.tokenAddress || b.tokenAddress === "0x0000000000000000000000000000000000000000") {
				// Native token
				price = priceData.nativePrice;
			} else {
				price = priceData.prices.get(b.tokenAddress.toLowerCase()) ?? 0;
			}

			if (price === 0) return b;

			const decimals = b.token?.decimals ?? 18;
			// Split BigInt to preserve precision for values > 2^53
			const divisor = 10n ** BigInt(decimals);
			const intPart = balWei / divisor;
			const fracPart = balWei % divisor;
			const balHuman = Number(intPart) + Number(fracPart) / Number(divisor);
			const fiatValue = balHuman * price;

			return {
				...b,
				fiatConversion: String(price),
				fiatBalance: String(fiatValue),
			};
		});

		// Only filter spam when CoinGecko data is available to verify against
		if (options?.filterSpam && priceData.cgAvailable) {
			return enriched.filter((b) => {
				// Native tokens always pass
				if (!b.tokenAddress || b.tokenAddress === "0x0000000000000000000000000000000000000000") return true;
				// CoinGecko recognized this token
				if (priceData.prices.has(b.tokenAddress.toLowerCase())) return true;
				// Safe API independently valued it at >= $1
				if (parseFloat(b.fiatBalance || "0") >= 1) return true;
				// Unknown ERC-20 with no verified value = spam
				return false;
			});
		}

		return enriched;
	} catch (e) {
		console.warn(`[price-feed] Failed to enrich prices for chain ${chainId}:`, e);
		return balances;
	}
}
