/**
 * Data Transform Module for rWallet
 * Converts Safe Global API responses into formats for D3 visualizations.
 * TypeScript port of rwallet-online/js/data-transform.js
 */

// ── Interfaces ──

export interface TimelineEntry {
	date: Date;
	type: "in" | "out";
	amount: number;
	token: string;
	usd: number;
	hasUsdEstimate: boolean;
	chain: string;
	chainId: string;
	from?: string;
	fromFull?: string;
	to?: string;
	toFull?: string;
}

export interface SankeyNode {
	name: string;
	type: "wallet" | "source" | "target";
	address: string;
}

export interface SankeyLink {
	source: number;
	target: number;
	value: number;
	token: string;
}

export interface SankeyData {
	nodes: SankeyNode[];
	links: SankeyLink[];
}

export interface ChainStats {
	transfers: number;
	inflow: string;
	outflow: string;
	addresses: string;
	period: string;
}

export interface TransferRecord {
	chainId: string;
	chainName: string;
	date: string;
	from?: string;
	fromShort?: string;
	to?: string;
	toShort?: string;
	token: string;
	amount: number;
	usd: number;
}

export interface MultichainData {
	chainStats: Record<string, ChainStats>;
	flowData: Record<string, FlowEntry[]>;
	allTransfers: { incoming: TransferRecord[]; outgoing: TransferRecord[] };
}

export interface FlowEntry {
	from: string;
	to: string;
	value: number;
	token: string;
	chain: string;
}

// ── Helpers ──

export function shortenAddress(addr: string): string {
	if (!addr || addr.length < 10) return addr || "Unknown";
	return addr.slice(0, 6) + "..." + addr.slice(-4);
}

const EXPLORER_URLS: Record<string, string> = {
	"1": "https://etherscan.io",
	"10": "https://optimistic.etherscan.io",
	"100": "https://gnosisscan.io",
	"137": "https://polygonscan.com",
	"8453": "https://basescan.org",
	"42161": "https://arbiscan.io",
	"42220": "https://celoscan.io",
	"43114": "https://snowtrace.io",
	"56": "https://bscscan.com",
	"324": "https://explorer.zksync.io",
};

export function explorerLink(address: string, chainId: string): string {
	const base = EXPLORER_URLS[chainId];
	if (!base) return "#";
	return `${base}/address/${address}`;
}

export function txExplorerLink(txHash: string, chainId: string): string {
	const base = EXPLORER_URLS[chainId];
	if (!base) return "#";
	return `${base}/tx/${txHash}`;
}

/** Convert raw wei string to human-readable number, preserving precision for large values */
function weiToHuman(raw: string, decimals: number): number {
	try {
		const wei = BigInt(raw || "0");
		const divisor = 10n ** BigInt(decimals);
		return Number(wei / divisor) + Number(wei % divisor) / Number(divisor);
	} catch {
		return parseFloat(raw || "0") / Math.pow(10, decimals);
	}
}

export function getTransferValue(transfer: any): number {
	if (transfer.type === "ERC20_TRANSFER" || transfer.transferType === "ERC20_TRANSFER") {
		const decimals = transfer.tokenInfo?.decimals || transfer.token?.decimals || 18;
		return weiToHuman(transfer.value || "0", decimals);
	}
	if (transfer.type === "ETHER_TRANSFER" || transfer.transferType === "ETHER_TRANSFER") {
		return weiToHuman(transfer.value || "0", 18);
	}
	return 0;
}

export function getTokenSymbol(transfer: any): string {
	return transfer.tokenInfo?.symbol || transfer.token?.symbol || "ETH";
}

function getTokenName(transfer: any): string {
	return transfer.tokenInfo?.name || transfer.token?.name || "Native";
}

// ── Stablecoin USD estimation ──

const STABLECOINS = new Set([
	"USDC", "USDT", "DAI", "WXDAI", "BUSD", "TUSD", "USDP", "FRAX",
	"LUSD", "GUSD", "sUSD", "USDD", "USDGLO", "USD+", "USDe", "crvUSD",
	"GHO", "PYUSD", "DOLA", "Yield-USD", "yUSD",
]);

// Approximate USD prices for major non-stablecoin tokens (updated 2026-03-25 from CoinGecko)
const NATIVE_APPROX_USD: Record<string, number> = {
	ETH: 2165, WETH: 2165, stETH: 2165, cbETH: 2165, rETH: 2500, wstETH: 2665,
	MATIC: 0.20, POL: 0.20, WMATIC: 0.20,
	BNB: 600, WBNB: 600,
	AVAX: 20, WAVAX: 20,
	xDAI: 1, WXDAI: 1,
	CELO: 0.30, GNO: 129,
	SAFE: 0.10, COW: 0.21, ENS: 6.11, LDO: 0.30, BAL: 0.15,
};

export function estimateUSD(value: number, symbol: string): number | null {
	if (STABLECOINS.has(symbol)) return value;
	const price = NATIVE_APPROX_USD[symbol];
	if (price !== undefined) return value * price;
	return null;
}

// ── Native token symbols per chain ──

const CHAIN_NATIVE_SYMBOL: Record<string, string> = {
	"1": "ETH", "10": "ETH", "100": "xDAI", "137": "MATIC",
	"8453": "ETH", "42161": "ETH", "42220": "CELO", "43114": "AVAX",
	"56": "BNB", "324": "ETH",
};

// ── Transform: Timeline Data (for Balance River) ──

export function transformToTimelineData(
	chainDataMap: Map<string, any>,
	safeAddress: string,
	chainNames: Record<string, string>,
): TimelineEntry[] {
	const timeline: any[] = [];

	for (const [chainId, data] of chainDataMap) {
		const chainName = (chainNames[chainId] || `chain-${chainId}`).toLowerCase();

		// Incoming transfers
		if (data.incoming) {
			for (const transfer of data.incoming) {
				const value = getTransferValue(transfer);
				const symbol = getTokenSymbol(transfer);
				if (value <= 0) continue;

				const usd = estimateUSD(value, symbol);
				timeline.push({
					date: transfer.executionDate || transfer.blockTimestamp || transfer.timestamp,
					type: "in",
					amount: value,
					token: symbol,
					usd: usd !== null ? usd : 0,
					hasUsdEstimate: usd !== null,
					chain: chainName,
					chainId,
					from: transfer._fromLabel || shortenAddress(transfer.from),
					fromFull: transfer.from,
				});
			}
		}

		// Outgoing multisig transactions
		if (data.outgoing) {
			for (const tx of data.outgoing) {
				if (!tx.isExecuted) continue;

				const txTransfers: { to: string; value: number; symbol: string; usd: number | null; label?: string }[] = [];

				// Check transfers array if available
				if (tx.transfers && tx.transfers.length > 0) {
					for (const t of tx.transfers) {
						if (t.from?.toLowerCase() === safeAddress.toLowerCase()) {
							const value = getTransferValue(t);
							const symbol = getTokenSymbol(t);
							if (value > 0) {
								txTransfers.push({ to: t.to, value, symbol, usd: estimateUSD(value, symbol), label: t._toLabel });
							}
						}
					}
				}

				// Fallback: parse from dataDecoded or direct value
				if (txTransfers.length === 0) {
					if (tx.value && tx.value !== "0") {
						const val = weiToHuman(tx.value, 18);
						const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
						txTransfers.push({ to: tx.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
					}

					if (tx.dataDecoded?.method === "transfer") {
						const params = tx.dataDecoded.parameters || [];
						const to = params.find((p: any) => p.name === "to")?.value;
						const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
						const val = weiToHuman(rawVal, 18);
						txTransfers.push({ to, value: val, symbol: "Token", usd: null });
					}

					if (tx.dataDecoded?.method === "multiSend") {
						const txsParam = tx.dataDecoded.parameters?.find((p: any) => p.name === "transactions");
						if (txsParam?.valueDecoded) {
							for (const inner of txsParam.valueDecoded) {
								if (inner.value && inner.value !== "0") {
									const val = weiToHuman(inner.value, 18);
									const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
									txTransfers.push({ to: inner.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
								}
								if (inner.dataDecoded?.method === "transfer") {
									const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
									const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
									const val2 = weiToHuman(raw2, 18);
									txTransfers.push({ to: to2, value: val2, symbol: "Token", usd: null });
								}
							}
						}
					}
				}

				for (const t of txTransfers) {
					const usd = t.usd !== null ? t.usd : 0;
					timeline.push({
						date: tx.executionDate,
						type: "out",
						amount: t.value,
						token: t.symbol,
						usd,
						hasUsdEstimate: t.usd !== null,
						chain: chainName,
						chainId,
						to: t.label || shortenAddress(t.to),
						toFull: t.to,
					});
				}
			}
		}
	}

	return timeline
		.filter((t) => t.date)
		.map((t) => ({ ...t, date: new Date(t.date) }))
		.sort((a, b) => a.date.getTime() - b.date.getTime());
}

// ── Transform: Sankey Data (for single-chain flow) ──

export function transformToSankeyData(chainData: any, safeAddress: string, chainId?: string): SankeyData {
	const nodeMap = new Map<string, number>();
	const nodes: SankeyNode[] = [];
	const links: SankeyLink[] = [];
	const walletLabel = chainId === "local" ? "My Wallet" : "Safe Wallet";

	function getNodeIndex(address: string, type: "wallet" | "source" | "target", label?: string): number {
		const key = address.toLowerCase() === safeAddress.toLowerCase()
			? "wallet"
			: `${type}:${address.toLowerCase()}`;

		if (!nodeMap.has(key)) {
			const idx = nodes.length;
			nodeMap.set(key, idx);
			const name = address.toLowerCase() === safeAddress.toLowerCase()
				? walletLabel
				: (label || shortenAddress(address));
			nodes.push({ name, type, address });
		}
		return nodeMap.get(key)!;
	}

	// Wallet node always first
	getNodeIndex(safeAddress, "wallet");

	// Aggregate inflows by source address + token
	const inflowAgg = new Map<string, { from: string; value: number; symbol: string; label?: string }>();
	if (chainData.incoming) {
		for (const transfer of chainData.incoming) {
			const value = getTransferValue(transfer);
			const symbol = getTokenSymbol(transfer);
			if (value <= 0 || !transfer.from) continue;

			const key = `${transfer.from.toLowerCase()}:${symbol}`;
			const existing = inflowAgg.get(key) || { from: transfer.from, value: 0, symbol, label: transfer._fromLabel };
			existing.value += value;
			inflowAgg.set(key, existing);
		}
	}

	// Add inflow links
	for (const [, agg] of inflowAgg) {
		const sourceIdx = getNodeIndex(agg.from, "source", agg.label);
		const walletIdx = nodeMap.get("wallet")!;
		links.push({ source: sourceIdx, target: walletIdx, value: agg.value, token: agg.symbol });
	}

	// Aggregate outflows by target address + token
	const outflowAgg = new Map<string, { to: string; value: number; symbol: string; label?: string }>();
	if (chainData.outgoing) {
		for (const tx of chainData.outgoing) {
			if (!tx.isExecuted) continue;

			// Extract label from transfers if available (CRDT entries)
			const txLabel = tx.transfers?.[0]?._toLabel;

			if (tx.value && tx.value !== "0" && tx.to) {
				const val = weiToHuman(tx.value, 18);
				const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
				const key = `${tx.to.toLowerCase()}:${sym}`;
				const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym, label: txLabel };
				existing.value += val;
				outflowAgg.set(key, existing);
			}

			if (tx.dataDecoded?.method === "transfer") {
				const params = tx.dataDecoded.parameters || [];
				const to = params.find((p: any) => p.name === "to")?.value;
				const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
				if (to) {
					const val = weiToHuman(rawVal, 18);
					const key = `${to.toLowerCase()}:Token`;
					const existing = outflowAgg.get(key) || { to, value: 0, symbol: "Token" };
					existing.value += val;
					outflowAgg.set(key, existing);
				}
			}

			if (tx.dataDecoded?.method === "multiSend") {
				const txsParam = tx.dataDecoded.parameters?.find((p: any) => p.name === "transactions");
				if (txsParam?.valueDecoded) {
					for (const inner of txsParam.valueDecoded) {
						if (inner.value && inner.value !== "0" && inner.to) {
							const val = weiToHuman(inner.value, 18);
							const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
							const key = `${inner.to.toLowerCase()}:${sym}`;
							const existing = outflowAgg.get(key) || { to: inner.to, value: 0, symbol: sym };
							existing.value += val;
							outflowAgg.set(key, existing);
						}
						if (inner.dataDecoded?.method === "transfer") {
							const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
							const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
							if (to2) {
								const val2 = weiToHuman(raw2, 18);
								const key = `${to2.toLowerCase()}:Token`;
								const existing = outflowAgg.get(key) || { to: to2, value: 0, symbol: "Token" };
								existing.value += val2;
								outflowAgg.set(key, existing);
							}
						}
					}
				}
			}

			// Fallback: process embedded transfers (CRDT entries use this path)
			if (tx.transfers && tx.transfers.length > 0 && (!tx.value || tx.value === "0") && !tx.dataDecoded) {
				for (const t of tx.transfers) {
					if (t.from?.toLowerCase() === safeAddress.toLowerCase()) {
						const value = getTransferValue(t);
						const symbol = getTokenSymbol(t);
						if (value > 0 && t.to) {
							const key = `${t.to.toLowerCase()}:${symbol}`;
							const existing = outflowAgg.get(key) || { to: t.to, value: 0, symbol, label: t._toLabel };
							existing.value += value;
							outflowAgg.set(key, existing);
						}
					}
				}
			}
		}
	}

	// Add outflow links
	const walletIdx = nodeMap.get("wallet")!;
	for (const [, agg] of outflowAgg) {
		const targetIdx = getNodeIndex(agg.to, "target", agg.label);
		links.push({ source: walletIdx, target: targetIdx, value: agg.value, token: agg.symbol });
	}

	// Filter out tiny values (noise)
	const maxValue = Math.max(...links.map((l) => l.value), 1);
	const threshold = maxValue * 0.001;
	const filteredLinks = links.filter((l) => l.value >= threshold);

	return { nodes, links: filteredLinks };
}

// ── Transform: Multi-Chain Flow Data ──

function formatUSDValue(value: number): string {
	if (value >= 1000000) return `~$${(value / 1000000).toFixed(1)}M`;
	if (value >= 1000) return `~$${Math.round(value / 1000)}K`;
	return `~$${Math.round(value)}`;
}

export function transformToMultichainData(
	chainDataMap: Map<string, any>,
	safeAddress: string,
	chainNames: Record<string, string>,
): MultichainData {
	const chainStats: Record<string, ChainStats> = {};
	const flowData: Record<string, FlowEntry[]> = {};
	const allTransfers: { incoming: TransferRecord[]; outgoing: TransferRecord[] } = { incoming: [], outgoing: [] };
	let totalTransfers = 0;
	let totalInflow = 0;
	let totalOutflow = 0;
	const allAddresses = new Set<string>();
	let minDate: Date | null = null;
	let maxDate: Date | null = null;

	for (const [chainId, data] of chainDataMap) {
		const chainName = (chainNames[chainId] || `chain-${chainId}`).toLowerCase();
		let chainTransfers = 0;
		let chainInflow = 0;
		let chainOutflow = 0;
		const chainAddresses = new Set<string>();
		let chainMinDate: Date | null = null;
		let chainMaxDate: Date | null = null;
		const flows: FlowEntry[] = [];

		// Incoming
		const inflowAgg = new Map<string, { from: string; value: number; token: string }>();
		if (data.incoming) {
			for (const transfer of data.incoming) {
				const value = getTransferValue(transfer);
				const symbol = getTokenSymbol(transfer);
				if (value <= 0) continue;

				const usd = estimateUSD(value, symbol);
				const usdVal = usd !== null ? usd : 0;
				chainTransfers++;
				chainInflow += usdVal;
				if (transfer.from) {
					chainAddresses.add(transfer.from.toLowerCase());
					allAddresses.add(transfer.from.toLowerCase());
				}

				const date = transfer.executionDate || transfer.blockTimestamp;
				if (date) {
					const d = new Date(date);
					if (!chainMinDate || d < chainMinDate) chainMinDate = d;
					if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d;
				}

				const from = transfer.from || "Unknown";
				const fromName = transfer._fromLabel || shortenAddress(from);
				const key = fromName;
				const existing = inflowAgg.get(key) || { from: fromName, value: 0, token: symbol };
				existing.value += usdVal;
				inflowAgg.set(key, existing);

				allTransfers.incoming.push({
					chainId, chainName,
					date: date || "",
					from: transfer.from,
					fromShort: transfer._fromLabel || shortenAddress(transfer.from),
					token: symbol,
					amount: value,
					usd: usdVal,
				});
			}
		}

		for (const [, agg] of inflowAgg) {
			flows.push({ from: agg.from, to: "Safe Wallet", value: Math.round(agg.value), token: agg.token, chain: chainName });
		}

		// Outgoing
		const outflowAgg = new Map<string, { to: string; value: number; token: string }>();
		if (data.outgoing) {
			for (const tx of data.outgoing) {
				if (!tx.isExecuted) continue;
				chainTransfers++;

				const date = tx.executionDate;
				if (date) {
					const d = new Date(date);
					if (!chainMinDate || d < chainMinDate) chainMinDate = d;
					if (!chainMaxDate || d > chainMaxDate) chainMaxDate = d;
				}

				const outTransfers: { to: string; value: number; symbol: string; label?: string }[] = [];
				const txLabel = tx.transfers?.[0]?._toLabel;

				if (tx.value && tx.value !== "0" && tx.to) {
					const val = weiToHuman(tx.value, 18);
					const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
					outTransfers.push({ to: tx.to, value: val, symbol: sym, label: txLabel });
				}

				if (tx.dataDecoded?.method === "transfer") {
					const params = tx.dataDecoded.parameters || [];
					const to = params.find((p: any) => p.name === "to")?.value;
					const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
					if (to) outTransfers.push({ to, value: weiToHuman(rawVal, 18), symbol: "Token" });
				}

				if (tx.dataDecoded?.method === "multiSend") {
					const txsParam = tx.dataDecoded.parameters?.find((p: any) => p.name === "transactions");
					if (txsParam?.valueDecoded) {
						for (const inner of txsParam.valueDecoded) {
							if (inner.value && inner.value !== "0" && inner.to) {
								const val = weiToHuman(inner.value, 18);
								const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
								outTransfers.push({ to: inner.to, value: val, symbol: sym });
							}
							if (inner.dataDecoded?.method === "transfer") {
								const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
								const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
								if (to2) outTransfers.push({ to: to2, value: weiToHuman(raw2, 18), symbol: "Token" });
							}
						}
					}
				}

				// Fallback: embedded transfers (CRDT entries)
				if (outTransfers.length === 0 && tx.transfers && tx.transfers.length > 0) {
					for (const t of tx.transfers) {
						if (t.from?.toLowerCase() === safeAddress.toLowerCase()) {
							const value = getTransferValue(t);
							const symbol = getTokenSymbol(t);
							if (value > 0) {
								outTransfers.push({ to: t.to, value, symbol, label: t._toLabel });
							}
						}
					}
				}

				for (const t of outTransfers) {
					const usd = estimateUSD(t.value, t.symbol);
					const usdVal = usd !== null ? usd : 0;
					chainOutflow += usdVal;
					if (t.to) {
						chainAddresses.add(t.to.toLowerCase());
						allAddresses.add(t.to.toLowerCase());
					}

					const toName = t.label || shortenAddress(t.to);
					const key = toName;
					const existing = outflowAgg.get(key) || { to: toName, value: 0, token: t.symbol };
					existing.value += usdVal;
					outflowAgg.set(key, existing);

					allTransfers.outgoing.push({
						chainId, chainName,
						date: date || "",
						to: t.to,
						toShort: t.label || shortenAddress(t.to),
						token: t.symbol,
						amount: t.value,
						usd: usdVal,
					});
				}
			}
		}

		for (const [, agg] of outflowAgg) {
			flows.push({ from: "Safe Wallet", to: agg.to, value: Math.round(agg.value), token: agg.token, chain: chainName });
		}

		const fmt = (d: Date) => d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
		const period = (chainMinDate && chainMaxDate)
			? `${fmt(chainMinDate)} - ${fmt(chainMaxDate)}`
			: "No data";

		chainStats[chainName] = {
			transfers: chainTransfers,
			inflow: formatUSDValue(chainInflow),
			outflow: formatUSDValue(chainOutflow),
			addresses: String(chainAddresses.size),
			period,
		};

		flowData[chainName] = flows;

		totalTransfers += chainTransfers;
		totalInflow += chainInflow;
		totalOutflow += chainOutflow;
		if (chainMinDate && (!minDate || chainMinDate < minDate)) minDate = chainMinDate;
		if (chainMaxDate && (!maxDate || chainMaxDate > maxDate)) maxDate = chainMaxDate;
	}

	// Aggregate "all" stats
	const fmtAll = (d: Date) => d.toLocaleDateString("en-US", { month: "short", year: "numeric" });
	chainStats["all"] = {
		transfers: totalTransfers,
		inflow: formatUSDValue(totalInflow),
		outflow: formatUSDValue(totalOutflow),
		addresses: String(allAddresses.size),
		period: (minDate && maxDate) ? `${fmtAll(minDate)} - ${fmtAll(maxDate)}` : "No data",
	};

	// Aggregate "all" flows: top 15 by value
	const allFlows: FlowEntry[] = [];
	for (const flows of Object.values(flowData)) {
		allFlows.push(...flows);
	}
	allFlows.sort((a, b) => b.value - a.value);
	flowData["all"] = allFlows.slice(0, 15);

	// Sort transfers by date
	allTransfers.incoming.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
	allTransfers.outgoing.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

	return { chainStats, flowData, allTransfers };
}
