/**
 * Yield rate fetching — DeFi Llama primary, Morpho GraphQL supplementary,
 * on-chain fallback for Aave. 5-minute in-memory cache.
 */

import type { YieldOpportunity, SupportedYieldChain, YieldProtocol, StablecoinSymbol } from "./yield-protocols";
import {
	AAVE_V3_POOL, AAVE_ATOKENS, MORPHO_VAULTS,
	STABLECOIN_ADDRESSES, STABLECOIN_DECIMALS,
	SELECTORS, padAddress, YIELD_CHAIN_NAMES,
} from "./yield-protocols";
import { getRpcUrl } from "../mod";

// ── Cache ──
interface CacheEntry {
	data: YieldOpportunity[];
	timestamp: number;
}

const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
let ratesCache: CacheEntry | null = null;

export async function getYieldRates(filters?: {
	chainId?: string;
	protocol?: string;
	asset?: string;
}): Promise<YieldOpportunity[]> {
	// Return cached if fresh
	if (ratesCache && Date.now() - ratesCache.timestamp < CACHE_TTL) {
		return applyFilters(ratesCache.data, filters);
	}

	// Try primary source, fall back to stale cache on error
	try {
		const rates = await fetchAllRates();
		ratesCache = { data: rates, timestamp: Date.now() };
		return applyFilters(rates, filters);
	} catch (err) {
		console.warn("yield-rates: fetch failed, returning stale cache", err);
		if (ratesCache) return applyFilters(ratesCache.data, filters);
		return [];
	}
}

function applyFilters(
	rates: YieldOpportunity[],
	filters?: { chainId?: string; protocol?: string; asset?: string },
): YieldOpportunity[] {
	if (!filters) return rates;
	let result = rates;
	if (filters.chainId) result = result.filter((r) => r.chainId === filters.chainId);
	if (filters.protocol) result = result.filter((r) => r.protocol === filters.protocol);
	if (filters.asset) result = result.filter((r) => r.asset === filters.asset);
	return result;
}

// ── Primary: DeFi Llama ──

interface LlamaPool {
	pool: string;
	chain: string;
	project: string;
	symbol: string;
	tvlUsd: number;
	apy: number;
	apyBase?: number;
	apyReward?: number | null;
	apyPct7D?: number;
	apyMean30d?: number;
	underlyingTokens?: string[];
}

const LLAMA_CHAIN_MAP: Record<string, SupportedYieldChain> = {
	Ethereum: "1",
	Base: "8453",
};

const LLAMA_PROJECT_MAP: Record<string, YieldProtocol> = {
	"aave-v3": "aave-v3",
	"morpho-blue": "morpho-blue",
};

const STABLECOIN_SYMBOLS = new Set(["USDC", "USDT", "DAI"]);

async function fetchDefiLlamaRates(): Promise<YieldOpportunity[]> {
	const res = await fetch("https://yields.llama.fi/pools", {
		signal: AbortSignal.timeout(10000),
	});
	if (!res.ok) throw new Error(`DeFi Llama ${res.status}`);
	const { data } = (await res.json()) as { data: LlamaPool[] };

	const opportunities: YieldOpportunity[] = [];

	for (const pool of data) {
		const chainId = LLAMA_CHAIN_MAP[pool.chain];
		const protocol = LLAMA_PROJECT_MAP[pool.project];
		if (!chainId || !protocol) continue;

		// Match stablecoin symbol from pool symbol (e.g. "USDC", "USDC-WETH" → skip non-pure)
		const symbol = pool.symbol.split("-")[0] as StablecoinSymbol;
		if (!STABLECOIN_SYMBOLS.has(symbol)) continue;
		// Skip multi-asset pools
		if (pool.symbol.includes("-")) continue;

		const assetAddress = STABLECOIN_ADDRESSES[chainId]?.[symbol];
		if (!assetAddress || assetAddress === "0x0000000000000000000000000000000000000000") continue;

		// Find vault/aToken address
		let vaultAddress = "";
		if (protocol === "aave-v3") {
			vaultAddress = AAVE_ATOKENS[chainId]?.[symbol] || "";
		} else {
			const vault = MORPHO_VAULTS[chainId]?.find((v) => v.asset === symbol);
			vaultAddress = vault?.address || "";
		}
		if (!vaultAddress) continue;

		opportunities.push({
			protocol,
			chainId,
			asset: symbol,
			assetAddress,
			vaultAddress,
			apy: pool.apy || 0,
			apyBase: pool.apyBase ?? undefined,
			apy7d: pool.apyPct7D != null ? pool.apy + pool.apyPct7D : undefined,
			apy30d: pool.apyMean30d ?? undefined,
			tvl: pool.tvlUsd,
			poolId: pool.pool,
			vaultName: protocol === "morpho-blue"
				? MORPHO_VAULTS[chainId]?.find((v) => v.asset === symbol)?.name
				: undefined,
		});
	}

	return opportunities;
}

// ── Supplementary: Morpho GraphQL (vault-specific APY) ──

async function fetchMorphoVaultRates(): Promise<Map<string, { apy: number; tvl: number }>> {
	const results = new Map<string, { apy: number; tvl: number }>();

	const allVaults = [
		...MORPHO_VAULTS["1"].map((v) => ({ ...v, chainId: "1" as SupportedYieldChain })),
		...MORPHO_VAULTS["8453"].map((v) => ({ ...v, chainId: "8453" as SupportedYieldChain })),
	];

	const query = `{
		vaults(where: { address_in: [${allVaults.map((v) => `"${v.address.toLowerCase()}"`).join(",")}] }) {
			items {
				address
				state { apy totalAssetsUsd }
			}
		}
	}`;

	try {
		const res = await fetch("https://blue-api.morpho.org/graphql", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ query }),
			signal: AbortSignal.timeout(8000),
		});
		if (!res.ok) return results;
		const data = await res.json() as any;
		for (const vault of data?.data?.vaults?.items || []) {
			if (vault.state) {
				results.set(vault.address.toLowerCase(), {
					apy: (vault.state.apy || 0) * 100, // Morpho returns decimal
					tvl: vault.state.totalAssetsUsd || 0,
				});
			}
		}
	} catch {
		// Non-critical, DeFi Llama is primary
	}

	return results;
}

// ── Fallback: On-chain Aave getReserveData ──

const RAY = 10n ** 27n;

async function fetchAaveOnChainRate(
	chainId: SupportedYieldChain,
	asset: StablecoinSymbol,
): Promise<number | null> {
	const rpcUrl = getRpcUrl(chainId);
	if (!rpcUrl) return null;

	const assetAddr = STABLECOIN_ADDRESSES[chainId]?.[asset];
	if (!assetAddr) return null;

	const data = `${SELECTORS.getReserveData}${padAddress(assetAddr)}`;

	try {
		const res = await fetch(rpcUrl, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				jsonrpc: "2.0", id: 1,
				method: "eth_call",
				params: [{ to: AAVE_V3_POOL[chainId], data }, "latest"],
			}),
			signal: AbortSignal.timeout(5000),
		});
		const result = (await res.json()).result;
		if (!result || result === "0x") return null;

		// liquidityRate is the 4th field (index 3) in the returned tuple, each 32 bytes
		// Struct: (uint256 config, uint128 liquidityIndex, uint128 currentLiquidityRate, ...)
		// Actually in Aave V3 getReserveData returns ReserveData struct where
		// currentLiquidityRate is at offset 0x80 (4th 32-byte slot)
		const hex = result.slice(2);
		const liquidityRateHex = hex.slice(128, 192); // 4th slot
		const liquidityRate = BigInt("0x" + liquidityRateHex);

		// Convert RAY rate to APY: ((1 + rate/RAY/SECONDS_PER_YEAR)^SECONDS_PER_YEAR - 1) * 100
		// Simplified: APY ≈ (rate / RAY) * 100 (for low rates, compound effect is small)
		const apyApprox = Number(liquidityRate * 10000n / RAY) / 100;
		return apyApprox;
	} catch {
		return null;
	}
}

// ── Aggregator ──

async function fetchAllRates(): Promise<YieldOpportunity[]> {
	const [llamaRates, morphoVaultRates] = await Promise.allSettled([
		fetchDefiLlamaRates(),
		fetchMorphoVaultRates(),
	]);

	let opportunities = llamaRates.status === "fulfilled" ? llamaRates.value : [];
	const morphoRates = morphoVaultRates.status === "fulfilled" ? morphoVaultRates.value : new Map();

	// Enrich Morpho opportunities with vault-specific data
	for (const opp of opportunities) {
		if (opp.protocol === "morpho-blue") {
			const vaultData = morphoRates.get(opp.vaultAddress.toLowerCase());
			if (vaultData) {
				if (vaultData.apy > 0) opp.apy = vaultData.apy;
				if (vaultData.tvl > 0) opp.tvl = vaultData.tvl;
			}
		}
	}

	// If DeFi Llama failed entirely, try on-chain fallback for Aave
	if (opportunities.length === 0) {
		const chains: SupportedYieldChain[] = ["1", "8453"];
		const assets: StablecoinSymbol[] = ["USDC", "USDT", "DAI"];

		const fallbackPromises = chains.flatMap((chainId) =>
			assets.map(async (asset) => {
				const aToken = AAVE_ATOKENS[chainId]?.[asset];
				const assetAddr = STABLECOIN_ADDRESSES[chainId]?.[asset];
				if (!aToken || !assetAddr || assetAddr === "0x0000000000000000000000000000000000000000") return null;

				const apy = await fetchAaveOnChainRate(chainId, asset);
				if (apy === null) return null;

				return {
					protocol: "aave-v3" as YieldProtocol,
					chainId,
					asset,
					assetAddress: assetAddr,
					vaultAddress: aToken,
					apy,
				} satisfies YieldOpportunity;
			}),
		);

		const results = await Promise.allSettled(fallbackPromises);
		for (const r of results) {
			if (r.status === "fulfilled" && r.value) {
				opportunities.push(r.value);
			}
		}
	}

	// Sort by APY descending
	opportunities.sort((a, b) => b.apy - a.apy);
	return opportunities;
}
