/**
 * D3 rendering functions for rWallet visualizations.
 * Ported from rwallet-online standalone HTML pages.
 *
 * All render functions take DOM elements directly (not CSS selectors)
 * for shadow DOM compatibility.
 */

import type { TimelineEntry, SankeyData, FlowEntry, ChainStats } from "./data-transform";

declare const d3: any;

// ── CDN loader (lazy, same pattern as rmaps loadMapLibre) ──

let d3Loaded = false;
let d3LoadPromise: Promise<void> | null = null;

export function loadD3(): Promise<void> {
	if (d3Loaded) return Promise.resolve();
	if (d3LoadPromise) return d3LoadPromise;

	d3LoadPromise = new Promise<void>((resolve, reject) => {
		const d3Script = document.createElement("script");
		d3Script.src = "https://d3js.org/d3.v7.min.js";
		d3Script.onload = () => {
			// Load d3-sankey after d3 core
			const sankeyScript = document.createElement("script");
			sankeyScript.src = "https://cdn.jsdelivr.net/npm/d3-sankey@0.12.3/dist/d3-sankey.min.js";
			sankeyScript.onload = () => {
				d3Loaded = true;
				resolve();
			};
			sankeyScript.onerror = () => reject(new Error("Failed to load d3-sankey"));
			document.head.appendChild(sankeyScript);
		};
		d3Script.onerror = () => reject(new Error("Failed to load D3"));
		document.head.appendChild(d3Script);
	});

	return d3LoadPromise;
}

// ── Color constants (matching wallet theme) ──

const COLORS = {
	cyan: "#00d4ff",
	green: "#4ade80",
	red: "#f87171",
	pink: "#f472b6",
	teal: "#0891b2",
	darkTeal: "#0e7490",
};

// ── Unique gradient IDs (scoped per render to avoid shadow DOM conflicts) ──

let vizIdCounter = 0;
function nextVizId(): string {
	return `wv${++vizIdCounter}`;
}

// ── Reset View button (shared across all 3 viz) ──

function addResetButton(parent: HTMLElement, svg: any, zoom: any): void {
	const btn = document.createElement("button");
	btn.textContent = "Reset View";
	Object.assign(btn.style, {
		position: "absolute", top: "12px", right: "12px", zIndex: "10",
		padding: "5px 12px", fontSize: "0.75rem", fontWeight: "600",
		background: "rgba(255,255,255,0.08)", color: "#ccc",
		border: "1px solid rgba(255,255,255,0.15)", borderRadius: "6px",
		cursor: "pointer", backdropFilter: "blur(4px)",
	});
	btn.addEventListener("pointerenter", () => { btn.style.background = "rgba(255,255,255,0.15)"; });
	btn.addEventListener("pointerleave", () => { btn.style.background = "rgba(255,255,255,0.08)"; });
	btn.addEventListener("click", () => {
		svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
	});
	parent.style.position = "relative";
	parent.appendChild(btn);
}

// ── Timeline (Balance River) ──

export interface TimelineOptions {
	width?: number;
	height?: number;
	chainColors?: Record<string, string>;
}

export function renderTimeline(
	container: HTMLElement,
	data: TimelineEntry[],
	options: TimelineOptions = {},
): void {
	container.innerHTML = "";

	if (data.length === 0) {
		container.innerHTML = '<p style="text-align:center;color:var(--rs-text-muted,#666);padding:60px;">No transaction data available.</p>';
		return;
	}

	const id = nextVizId();
	const margin = { top: 80, right: 50, bottom: 80, left: 60 };
	const width = (options.width || container.clientWidth || 1200) - margin.left - margin.right;
	const height = (options.height || 500) - margin.top - margin.bottom;
	const centerY = height / 2;

	// Create tooltip inside container (shadow DOM safe)
	const tooltip = document.createElement("div");
	Object.assign(tooltip.style, {
		position: "absolute", background: "rgba(10,10,20,0.98)", border: "1px solid rgba(255,255,255,0.2)",
		borderRadius: "10px", padding: "14px 18px", fontSize: "0.85rem", pointerEvents: "none",
		zIndex: "1000", maxWidth: "320px", boxShadow: "0 8px 32px rgba(0,0,0,0.6)", display: "none",
		color: "#e0e0e0",
	});
	container.style.position = "relative";
	container.appendChild(tooltip);

	// Stats
	let totalIn = 0, totalOut = 0, balance = 0, peak = 0;
	data.forEach(tx => {
		if (tx.type === "in") { totalIn += tx.usd; balance += tx.usd; }
		else { totalOut += tx.usd; balance -= tx.usd; }
		if (balance > peak) peak = balance;
	});

	// Stats row
	const statsRow = document.createElement("div");
	statsRow.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:16px;";
	const statItems = [
		{ label: "Total Inflow", value: `$${Math.round(totalIn).toLocaleString()}`, color: COLORS.green },
		{ label: "Total Outflow", value: `$${Math.round(totalOut).toLocaleString()}`, color: COLORS.red },
		{ label: "Net Change", value: `$${Math.round(totalIn - totalOut).toLocaleString()}`, color: COLORS.cyan },
		{ label: "Peak Balance", value: `$${Math.round(peak).toLocaleString()}`, color: COLORS.cyan },
		{ label: "Transactions", value: String(data.length), color: "#e0e0e0" },
	];
	statsRow.innerHTML = statItems.map(s => `
		<div style="background:var(--rs-bg-surface,rgba(255,255,255,0.03));border-radius:10px;padding:12px;border:1px solid var(--rs-border-subtle,rgba(255,255,255,0.1));text-align:center;">
			<div style="color:var(--rs-text-secondary,#888);font-size:10px;text-transform:uppercase;margin-bottom:4px;">${s.label}</div>
			<div style="font-size:1.1rem;font-weight:700;color:${s.color};">${s.value}</div>
		</div>
	`).join("");
	container.appendChild(statsRow);

	// Legend
	const legend = document.createElement("div");
	legend.style.cssText = "display:flex;justify-content:center;gap:24px;margin-bottom:12px;font-size:0.8rem;";
	legend.innerHTML = `
		<span style="display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:30px;height:12px;border-radius:6px;background:linear-gradient(90deg,${COLORS.green},${COLORS.cyan});"></span> Inflows</span>
		<span style="display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:30px;height:12px;border-radius:6px;background:${COLORS.cyan};border:1px solid rgba(0,212,255,0.3);"></span> Balance</span>
		<span style="display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:30px;height:12px;border-radius:6px;background:linear-gradient(90deg,${COLORS.cyan},${COLORS.red});"></span> Outflows</span>
	`;
	container.appendChild(legend);

	// SVG container
	const svgContainer = document.createElement("div");
	svgContainer.style.cssText = "overflow-x:auto;border-radius:12px;border:1px solid var(--rs-border-subtle,rgba(255,255,255,0.1));background:var(--rs-bg-surface,rgba(255,255,255,0.02));padding:8px;";
	container.appendChild(svgContainer);

	const svg = d3.select(svgContainer)
		.append("svg")
		.attr("width", width + margin.left + margin.right)
		.attr("height", height + margin.top + margin.bottom)
		.style("cursor", "grab");

	const mainGroup = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

	// Defs (scoped IDs)
	const defs = mainGroup.append("defs");

	const inflowGrad = defs.append("linearGradient").attr("id", `${id}-inflow`).attr("x1", "0%").attr("y1", "0%").attr("x2", "100%").attr("y2", "100%");
	inflowGrad.append("stop").attr("offset", "0%").attr("stop-color", COLORS.green).attr("stop-opacity", 0.4);
	inflowGrad.append("stop").attr("offset", "40%").attr("stop-color", COLORS.green).attr("stop-opacity", 0.85);
	inflowGrad.append("stop").attr("offset", "70%").attr("stop-color", "#22d3ee").attr("stop-opacity", 0.9);
	inflowGrad.append("stop").attr("offset", "100%").attr("stop-color", COLORS.cyan).attr("stop-opacity", 1);

	const outflowGrad = defs.append("linearGradient").attr("id", `${id}-outflow`).attr("x1", "0%").attr("y1", "0%").attr("x2", "100%").attr("y2", "100%");
	outflowGrad.append("stop").attr("offset", "0%").attr("stop-color", COLORS.cyan).attr("stop-opacity", 1);
	outflowGrad.append("stop").attr("offset", "30%").attr("stop-color", COLORS.pink).attr("stop-opacity", 0.9);
	outflowGrad.append("stop").attr("offset", "60%").attr("stop-color", COLORS.red).attr("stop-opacity", 0.85);
	outflowGrad.append("stop").attr("offset", "100%").attr("stop-color", COLORS.red).attr("stop-opacity", 0.4);

	const riverGrad = defs.append("linearGradient").attr("id", `${id}-river`).attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
	riverGrad.append("stop").attr("offset", "0%").attr("stop-color", COLORS.cyan).attr("stop-opacity", 0.9);
	riverGrad.append("stop").attr("offset", "30%").attr("stop-color", COLORS.teal).attr("stop-opacity", 1);
	riverGrad.append("stop").attr("offset", "50%").attr("stop-color", COLORS.darkTeal).attr("stop-opacity", 1);
	riverGrad.append("stop").attr("offset", "70%").attr("stop-color", COLORS.teal).attr("stop-opacity", 1);
	riverGrad.append("stop").attr("offset", "100%").attr("stop-color", COLORS.cyan).attr("stop-opacity", 0.9);

	defs.append("clipPath").attr("id", `${id}-clip`).append("rect")
		.attr("x", 0).attr("y", -margin.top).attr("width", width).attr("height", height + margin.top + margin.bottom);

	// Scales
	const timeExtent = d3.extent(data, (d: TimelineEntry) => d.date) as [Date, Date];
	const timePadding = (timeExtent[1].getTime() - timeExtent[0].getTime()) * 0.05;
	const xScale = d3.scaleTime()
		.domain([new Date(timeExtent[0].getTime() - timePadding), new Date(timeExtent[1].getTime() + timePadding)])
		.range([0, width]);

	// Balance data
	const balanceData: { date: Date; balance: number }[] = [];
	let runBal = 0;
	balanceData.push({ date: new Date(timeExtent[0].getTime() - timePadding), balance: 0 });
	data.forEach(tx => {
		if (tx.type === "in") runBal += tx.usd;
		else runBal -= tx.usd;
		balanceData.push({ date: tx.date, balance: Math.max(0, runBal) });
	});
	balanceData.push({ date: new Date(timeExtent[1].getTime() + timePadding), balance: Math.max(0, runBal) });

	const maxBalance = d3.max(balanceData, (d: any) => d.balance) || 1;
	const balanceScale = d3.scaleLinear().domain([0, maxBalance]).range([8, 120]);

	const contentGroup = mainGroup.append("g").attr("clip-path", `url(#${id}-clip)`);
	const xAxisGroup = mainGroup.append("g").attr("transform", `translate(0,${height + 20})`);

	function updateAxis(scale: any) {
		const domain = scale.domain();
		const days = (domain[1] - domain[0]) / (1000 * 60 * 60 * 24);
		let tickInterval, tickFormat;
		if (days < 14) { tickInterval = d3.timeDay.every(1); tickFormat = d3.timeFormat("%b %d"); }
		else if (days < 60) { tickInterval = d3.timeWeek.every(1); tickFormat = d3.timeFormat("%b %d"); }
		else if (days < 180) { tickInterval = d3.timeMonth.every(1); tickFormat = d3.timeFormat("%b %Y"); }
		else if (days < 365) { tickInterval = d3.timeMonth.every(2); tickFormat = d3.timeFormat("%b %Y"); }
		else { tickInterval = d3.timeMonth.every(3); tickFormat = d3.timeFormat("%b %Y"); }

		const xAxis = d3.axisBottom(scale).ticks(tickInterval).tickFormat(tickFormat);
		xAxisGroup.call(xAxis).selectAll("text").attr("fill", "#888").attr("font-size", "11px").attr("transform", "rotate(-30)").attr("text-anchor", "end");
		xAxisGroup.selectAll(".domain, .tick line").attr("stroke", "#444");
	}

	function drawContent(scale: any) {
		contentGroup.selectAll("*").remove();
		// curveStepAfter: balance is constant between txs, steps at each tx.
	// This ensures the river edges align exactly with the waterfall shapes.
	const smoothCurve = d3.curveStepAfter;

		// River glow
		contentGroup.append("path").datum(balanceData).attr("fill", "rgba(0,212,255,0.08)")
			.attr("d", d3.area().x((d: any) => scale(d.date)).y0((d: any) => centerY + balanceScale(d.balance) / 2 + 15).y1((d: any) => centerY - balanceScale(d.balance) / 2 - 15).curve(smoothCurve));

		// Main river
		contentGroup.append("path").datum(balanceData).attr("fill", `url(#${id}-river)`)
			.attr("d", d3.area().x((d: any) => scale(d.date)).y0((d: any) => centerY + balanceScale(d.balance) / 2).y1((d: any) => centerY - balanceScale(d.balance) / 2).curve(smoothCurve));

		// Edge highlights
		contentGroup.append("path").datum(balanceData).attr("fill", "none").attr("stroke", "rgba(255,255,255,0.3)").attr("stroke-width", 1.5)
			.attr("d", d3.line().x((d: any) => scale(d.date)).y((d: any) => centerY - balanceScale(d.balance) / 2).curve(smoothCurve));
		contentGroup.append("path").datum(balanceData).attr("fill", "none").attr("stroke", "rgba(0,0,0,0.2)").attr("stroke-width", 1)
			.attr("d", d3.line().x((d: any) => scale(d.date)).y((d: any) => centerY + balanceScale(d.balance) / 2).curve(smoothCurve));

		// Diagonal waterfall flows
		const flowHeight = 80;
		const xOffset = flowHeight * 0.7;
		const cornerR = Math.min(flowHeight * 0.25, xOffset * 0.4);
		let prevBalance = 0;

		data.forEach(tx => {
			const x = scale(tx.date);
			const balanceBefore = Math.max(0, prevBalance);
			if (tx.type === "in") prevBalance += tx.usd;
			else prevBalance -= tx.usd;
			const balanceAfter = Math.max(0, prevBalance);

			const relevantBalance = tx.type === "in" ? balanceAfter : balanceBefore;
			const riverW = balanceScale(relevantBalance);
			const proportion = relevantBalance > 0 ? Math.min(1, tx.usd / relevantBalance) : 0.5;
			const riverEndHalf = Math.max(4, (proportion * riverW) / 2);
			const farEndHalf = Math.max(2, riverEndHalf * 0.3);

			const riverTopAfter = centerY - balanceScale(balanceAfter) / 2;
			const riverBottomBefore = centerY + balanceScale(balanceBefore) / 2;

			if (tx.type === "in") {
				const endY = riverTopAfter;
				const startY = endY - flowHeight;
				const startX = x - xOffset;
				const endX = x;

				const path = d3.path();
				// Left edge: vertical down, rounded corner, horizontal right to river
				path.moveTo(startX - farEndHalf, startY);
				path.lineTo(startX - farEndHalf, endY - cornerR);
				path.quadraticCurveTo(startX - farEndHalf, endY, startX - farEndHalf + cornerR, endY);
				path.lineTo(endX - riverEndHalf, endY);
				// Across river end
				path.lineTo(endX + riverEndHalf, endY);
				// Right edge: horizontal left, rounded corner, vertical up
				path.lineTo(startX + farEndHalf + cornerR, endY);
				path.quadraticCurveTo(startX + farEndHalf, endY, startX + farEndHalf, endY - cornerR);
				path.lineTo(startX + farEndHalf, startY);
				path.closePath();

				contentGroup.append("path").attr("d", path.toString()).attr("fill", `url(#${id}-inflow)`)
					.attr("opacity", 0.85).style("cursor", "pointer").style("transition", "opacity 0.2s, filter 0.2s")
					.on("pointerenter", (event: any) => showTxTooltip(event, tx, prevBalance))
					.on("pointermove", (event: any) => moveTip(event))
					.on("pointerleave", () => { tooltip.style.display = "none"; });
			} else {
				const startY = riverBottomBefore;
				const endY = startY + flowHeight;
				const startX = x;
				const endX = x + xOffset;

				const path = d3.path();
				// Left edge: horizontal right from river, rounded corner, vertical down
				path.moveTo(startX - riverEndHalf, startY);
				path.lineTo(endX - farEndHalf - cornerR, startY);
				path.quadraticCurveTo(endX - farEndHalf, startY, endX - farEndHalf, startY + cornerR);
				path.lineTo(endX - farEndHalf, endY);
				// Across far end
				path.lineTo(endX + farEndHalf, endY);
				// Right edge: vertical up, rounded corner, horizontal left to river
				path.lineTo(endX + farEndHalf, startY + cornerR);
				path.quadraticCurveTo(endX + farEndHalf, startY, endX + farEndHalf - cornerR, startY);
				path.lineTo(startX + riverEndHalf, startY);
				path.closePath();

				contentGroup.append("path").attr("d", path.toString()).attr("fill", `url(#${id}-outflow)`)
					.attr("opacity", 0.85).style("cursor", "pointer").style("transition", "opacity 0.2s, filter 0.2s")
					.on("pointerenter", (event: any) => showTxTooltip(event, tx, prevBalance))
					.on("pointermove", (event: any) => moveTip(event))
					.on("pointerleave", () => { tooltip.style.display = "none"; });
			}
		});

		// River hover
		const riverHoverGroup = contentGroup.append("g");
		riverHoverGroup.append("rect")
			.attr("x", scale.range()[0] - 50).attr("y", centerY - 100)
			.attr("width", scale.range()[1] - scale.range()[0] + 100).attr("height", 200)
			.attr("fill", "transparent").style("cursor", "crosshair")
			.on("pointermove", function(event: any) {
				const [mouseX] = d3.pointer(event);
				const hoveredDate = scale.invert(mouseX);
				let balAtPoint = 0;
				for (const tx of data) {
					if (tx.date <= hoveredDate) {
						if (tx.type === "in") balAtPoint += tx.usd;
						else balAtPoint -= tx.usd;
					} else break;
				}
				riverHoverGroup.selectAll(".bal-ind").remove();
				const t = balanceScale(Math.max(0, balAtPoint));
				riverHoverGroup.append("line").attr("class", "bal-ind")
					.attr("x1", mouseX).attr("x2", mouseX).attr("y1", centerY - t / 2 - 5).attr("y2", centerY + t / 2 + 5)
					.attr("stroke", "#fff").attr("stroke-width", 2).attr("stroke-dasharray", "4,2").attr("opacity", 0.8).style("pointer-events", "none");
				riverHoverGroup.append("circle").attr("class", "bal-ind")
					.attr("cx", mouseX).attr("cy", centerY).attr("r", 5)
					.attr("fill", COLORS.cyan).attr("stroke", "#fff").attr("stroke-width", 2).style("pointer-events", "none");

				tooltip.innerHTML = `
					<div style="color:#888;font-size:0.75rem;margin-bottom:4px;">${hoveredDate.toLocaleDateString("en-US", { weekday: "short", year: "numeric", month: "short", day: "numeric" })}</div>
					<span style="font-weight:bold;font-size:1.3rem;color:${COLORS.cyan};">$${Math.round(Math.max(0, balAtPoint)).toLocaleString()}</span>
					<div style="margin-top:6px;font-size:0.8rem;color:#888;">Balance at this point</div>
				`;
				tooltip.style.display = "block";
				moveTip(event);
			})
			.on("pointerleave", function() {
				riverHoverGroup.selectAll(".bal-ind").remove();
				tooltip.style.display = "none";
			});

		// Labels
		mainGroup.selectAll(".viz-label").remove();
		mainGroup.append("text").attr("class", "viz-label").attr("x", 30).attr("y", -50).attr("text-anchor", "start")
			.attr("fill", COLORS.green).attr("font-size", "13px").attr("font-weight", "bold").attr("opacity", 0.8).text("INFLOWS");
		mainGroup.append("text").attr("class", "viz-label").attr("x", width - 30).attr("y", height + 55).attr("text-anchor", "end")
			.attr("fill", COLORS.red).attr("font-size", "13px").attr("font-weight", "bold").attr("opacity", 0.8).text("OUTFLOWS");
	}

	function showTxTooltip(event: any, tx: TimelineEntry, balAfter: number) {
		const chain = tx.chain || "";
		tooltip.innerHTML = `
			<div style="color:#888;font-size:0.75rem;margin-bottom:4px;">${tx.date.toLocaleDateString("en-US", { weekday: "short", year: "numeric", month: "short", day: "numeric" })}</div>
			<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.7rem;font-weight:600;background:var(--rs-bg-hover,#333);margin-bottom:6px;">${chain.charAt(0).toUpperCase() + chain.slice(1)}</span>
			<span style="font-weight:bold;font-size:1.3rem;display:block;margin-bottom:4px;color:${tx.type === "in" ? COLORS.green : COLORS.red};">
				${tx.type === "in" ? "+" : "-"}$${Math.round(tx.usd).toLocaleString()}
			</span>
			<div style="color:${COLORS.cyan};font-size:0.9rem;">${tx.amount.toLocaleString(undefined, { maximumFractionDigits: 4 })} ${tx.token}</div>
			<div style="font-family:monospace;font-size:0.75rem;color:#888;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,0.1);">
				${tx.type === "in" ? "From: " + (tx.from || "Unknown") : "To: " + (tx.to || "Unknown")}
			</div>
			<div style="margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,0.1);font-size:0.8rem;color:${COLORS.cyan};">
				Balance after: $${Math.round(Math.max(0, balAfter)).toLocaleString()}
			</div>
		`;
		tooltip.style.display = "block";
		moveTip(event);
	}

	function moveTip(event: any) {
		const rect = container.getBoundingClientRect();
		let x = event.clientX - rect.left + 15;
		let y = event.clientY - rect.top - 10;
		if (x + 300 > rect.width) x = event.clientX - rect.left - 300 - 15;
		if (y + 200 > rect.height) y = event.clientY - rect.top - 200;
		tooltip.style.left = x + "px";
		tooltip.style.top = y + "px";
	}

	updateAxis(xScale);
	drawContent(xScale);

	// Zoom
	const zoom = d3.zoom()
		.scaleExtent([0.5, 20])
		.translateExtent([[-width * 2, 0], [width * 3, height]])
		.extent([[0, 0], [width, height]])
		.on("zoom", (event: any) => {
			const newXScale = event.transform.rescaleX(xScale);
			updateAxis(newXScale);
			drawContent(newXScale);
		})
		.on("start", () => svg.style("cursor", "grabbing"))
		.on("end", () => svg.style("cursor", "grab"));

	svg.on("wheel", function(event: any) {
		if (!event.ctrlKey) return; // let normal scroll pass through to page
		event.preventDefault();
		const currentTransform = d3.zoomTransform(svg.node());
		const scaleFactor = event.deltaY > 0 ? 0.9 : 1.1;
		const [mouseX] = d3.pointer(event);
		const newScale = Math.max(0.5, Math.min(20, currentTransform.k * scaleFactor));
		const newX = mouseX - (mouseX - currentTransform.x) * (newScale / currentTransform.k);
		const newTransform = d3.zoomIdentity.translate(newX, 0).scale(newScale);
		svg.call(zoom.transform, newTransform);
	}, { passive: false });

	svg.call(zoom);
	addResetButton(svgContainer, svg, zoom);
}

// ── Flow Chart (Multi-Chain Force-Directed) ──

export interface FlowChartOptions {
	width?: number;
	height?: number;
	chainColors?: Record<string, string>;
	safeAddress?: string;
}

export function renderFlowChart(
	container: HTMLElement,
	flowData: FlowEntry[],
	stats: ChainStats | undefined,
	options: FlowChartOptions = {},
): void {
	container.innerHTML = "";

	if (!flowData || flowData.length === 0) {
		container.innerHTML = '<p style="text-align:center;color:var(--rs-text-muted,#666);padding:60px;">No flow data available.</p>';
		return;
	}

	// Stats row
	if (stats) {
		const statsRow = document.createElement("div");
		statsRow.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:16px;";
		statsRow.innerHTML = [
			{ label: "Transfers", value: stats.transfers, color: COLORS.cyan },
			{ label: "Inflow", value: stats.inflow, color: COLORS.green },
			{ label: "Outflow", value: stats.outflow, color: COLORS.red },
			{ label: "Addresses", value: stats.addresses, color: COLORS.cyan },
			{ label: "Period", value: stats.period, color: "#e0e0e0" },
		].map(s => `
			<div style="background:var(--rs-bg-surface,rgba(255,255,255,0.03));border-radius:10px;padding:12px;border:1px solid var(--rs-border-subtle,rgba(255,255,255,0.1));text-align:center;">
				<div style="color:var(--rs-text-secondary,#888);font-size:10px;text-transform:uppercase;margin-bottom:4px;">${s.label}</div>
				<div style="font-size:1rem;font-weight:700;color:${s.color};">${s.value}</div>
			</div>
		`).join("");
		container.appendChild(statsRow);
	}

	const chartDiv = document.createElement("div");
	chartDiv.style.cssText = "border-radius:12px;border:1px solid var(--rs-border-subtle,rgba(255,255,255,0.1));background:var(--rs-bg-surface,rgba(255,255,255,0.02));overflow:hidden;";
	container.appendChild(chartDiv);

	const w = options.width || container.clientWidth || 1000;
	const h = options.height || 400;

	const svg = d3.select(chartDiv).append("svg")
		.attr("width", "100%").attr("height", h).attr("viewBox", `0 0 ${w} ${h}`)
		.style("cursor", "grab");

	const g = svg.append("g");

	const inflows = flowData.filter(f => f.to === "Safe Wallet");
	const outflows = flowData.filter(f => f.from === "Safe Wallet");

	const walletX = w / 2, walletY = h / 2;

	// Central wallet node
	g.append("rect").attr("x", walletX - 70).attr("y", walletY - 35).attr("width", 140).attr("height", 70)
		.attr("rx", 12).attr("fill", COLORS.cyan).attr("opacity", 0.9);
	g.append("text").attr("x", walletX).attr("y", walletY - 8).attr("text-anchor", "middle")
		.attr("fill", "#000").attr("font-weight", "bold").attr("font-size", "13px").text("Safe Wallet");
	if (options.safeAddress) {
		const short = options.safeAddress.slice(0, 6) + "..." + options.safeAddress.slice(-4);
		g.append("text").attr("x", walletX).attr("y", walletY + 12).attr("text-anchor", "middle")
			.attr("fill", "#000").attr("font-family", "monospace").attr("font-size", "10px").text(short);
	}

	function getFlowColor(chainName: string): string {
		const colors: Record<string, string> = options.chainColors || {};
		return colors[chainName] || COLORS.cyan;
	}

	// Inflows (left side)
	const inflowSpacing = h / (inflows.length + 1);
	inflows.forEach((flow, i) => {
		const y = inflowSpacing * (i + 1);
		const sourceX = 120;
		const color = getFlowColor(flow.chain);

		const path = d3.path();
		path.moveTo(sourceX + 60, y);
		path.bezierCurveTo(sourceX + 150, y, walletX - 150, walletY, walletX - 70, walletY);
		g.append("path").attr("d", path.toString()).attr("fill", "none")
			.attr("stroke", COLORS.green).attr("stroke-width", Math.max(2, Math.log(flow.value + 1) * 1.2)).attr("stroke-opacity", 0.6);

		g.append("rect").attr("x", sourceX - 60).attr("y", y - 14).attr("width", 120).attr("height", 28)
			.attr("rx", 6).attr("fill", color).attr("opacity", 0.3).attr("stroke", color);
		g.append("text").attr("x", sourceX).attr("y", y + 4).attr("text-anchor", "middle")
			.attr("fill", "#e0e0e0").attr("font-family", "monospace").attr("font-size", "10px").text(flow.from);
		g.append("text").attr("x", sourceX + 100).attr("y", y - 20)
			.attr("fill", COLORS.green).attr("font-size", "9px").text(`+${flow.value.toLocaleString()} ${flow.token}`);
	});

	// Outflows (right side)
	const outflowSpacing = h / (outflows.length + 1);
	outflows.forEach((flow, i) => {
		const y = outflowSpacing * (i + 1);
		const targetX = w - 120;
		const color = getFlowColor(flow.chain);

		const path = d3.path();
		path.moveTo(walletX + 70, walletY);
		path.bezierCurveTo(walletX + 150, walletY, targetX - 150, y, targetX - 60, y);
		g.append("path").attr("d", path.toString()).attr("fill", "none")
			.attr("stroke", COLORS.red).attr("stroke-width", Math.max(2, Math.log(flow.value + 1) * 1.2)).attr("stroke-opacity", 0.6);

		g.append("rect").attr("x", targetX - 60).attr("y", y - 14).attr("width", 120).attr("height", 28)
			.attr("rx", 6).attr("fill", color).attr("opacity", 0.3).attr("stroke", color);
		g.append("text").attr("x", targetX).attr("y", y + 4).attr("text-anchor", "middle")
			.attr("fill", "#e0e0e0").attr("font-family", "monospace").attr("font-size", "10px").text(flow.to);
		g.append("text").attr("x", targetX - 100).attr("y", y - 20)
			.attr("fill", COLORS.red).attr("font-size", "9px").text(`-${flow.value.toLocaleString()} ${flow.token}`);
	});

	// Zoom (only intercept Ctrl+wheel / pinch-to-zoom, let normal scroll pass through)
	const flowZoom = d3.zoom()
		.scaleExtent([0.3, 5])
		.filter((event: any) => event.type !== "wheel" || event.ctrlKey)
		.on("zoom", (event: any) => g.attr("transform", event.transform))
		.on("start", () => svg.style("cursor", "grabbing"))
		.on("end", () => svg.style("cursor", "grab"));
	svg.call(flowZoom);
	addResetButton(chartDiv, svg, flowZoom);
}

// ── Sankey Diagram ──

export interface SankeyOptions {
	width?: number;
	height?: number;
}

export function renderSankey(
	container: HTMLElement,
	sankeyData: SankeyData,
	options: SankeyOptions = {},
): void {
	container.innerHTML = "";

	if (!sankeyData || sankeyData.links.length === 0) {
		container.innerHTML = '<p style="text-align:center;color:var(--rs-text-muted,#666);padding:60px;">No transaction flow data available.</p>';
		return;
	}

	// Legend
	const legend = document.createElement("div");
	legend.style.cssText = "display:flex;justify-content:center;gap:20px;margin-bottom:12px;font-size:0.85rem;";
	legend.innerHTML = `
		<span style="display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:16px;height:16px;border-radius:3px;background:${COLORS.green};"></span> Inflow Sources</span>
		<span style="display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:16px;height:16px;border-radius:3px;background:${COLORS.cyan};"></span> Wallet</span>
		<span style="display:flex;align-items:center;gap:6px;"><span style="display:inline-block;width:16px;height:16px;border-radius:3px;background:${COLORS.red};"></span> Outflow Targets</span>
	`;
	container.appendChild(legend);

	const chartDiv = document.createElement("div");
	chartDiv.style.cssText = "border-radius:12px;border:1px solid var(--rs-border-subtle,rgba(255,255,255,0.1));background:var(--rs-bg-surface,rgba(255,255,255,0.02));overflow:hidden;cursor:grab;";
	container.appendChild(chartDiv);

	const width = options.width || 1200;
	const height = options.height || Math.max(400, sankeyData.nodes.length * 35);
	const margin = { top: 20, right: 200, bottom: 20, left: 200 };

	const svg = d3.select(chartDiv).append("svg")
		.attr("width", "100%").attr("height", height).attr("viewBox", `0 0 ${width} ${height}`)
		.style("cursor", "grab");

	const zoomGroup = svg.append("g");

	const sankey = d3.sankey()
		.nodeWidth(20)
		.nodePadding(15)
		.extent([[margin.left, margin.top], [width - margin.right, height - margin.bottom]]);

	const { nodes, links } = sankey({
		nodes: sankeyData.nodes.map((d: any) => Object.assign({}, d)),
		links: sankeyData.links.map((d: any) => Object.assign({}, d)),
	});

	// Links
	zoomGroup.append("g")
		.selectAll("path")
		.data(links)
		.join("path")
		.attr("d", d3.sankeyLinkHorizontal())
		.attr("fill", "none")
		.attr("stroke", (d: any) => {
			const sourceNode = nodes[d.source.index !== undefined ? d.source.index : d.source];
			return sourceNode?.type === "source" ? COLORS.green : COLORS.red;
		})
		.attr("stroke-opacity", 0.4)
		.attr("stroke-width", (d: any) => Math.max(1, d.width))
		.style("transition", "stroke-opacity 0.2s")
		.on("pointerenter", function(this: any) { d3.select(this).attr("stroke-opacity", 0.7); })
		.on("pointerleave", function(this: any) { d3.select(this).attr("stroke-opacity", 0.4); })
		.append("title")
		.text((d: any) => `${d.source.name} -> ${d.target.name}\n${d.value.toLocaleString()} ${d.token}`);

	// Nodes
	const node = zoomGroup.append("g").selectAll("g").data(nodes).join("g");

	node.append("rect")
		.attr("x", (d: any) => d.x0)
		.attr("y", (d: any) => d.y0)
		.attr("height", (d: any) => d.y1 - d.y0)
		.attr("width", (d: any) => d.x1 - d.x0)
		.attr("fill", (d: any) => d.type === "wallet" ? COLORS.cyan : d.type === "source" ? COLORS.green : COLORS.red)
		.attr("rx", 3);

	node.append("text")
		.attr("x", (d: any) => d.x0 < width / 2 ? d.x0 - 6 : d.x1 + 6)
		.attr("y", (d: any) => (d.y1 + d.y0) / 2)
		.attr("dy", "0.35em")
		.attr("text-anchor", (d: any) => d.x0 < width / 2 ? "end" : "start")
		.text((d: any) => d.name)
		.style("font-family", "monospace")
		.style("font-size", (d: any) => d.type === "wallet" ? "14px" : "11px")
		.style("font-weight", (d: any) => d.type === "wallet" ? "bold" : "normal")
		.style("fill", "#e0e0e0");

	// Zoom (only intercept Ctrl+wheel / pinch-to-zoom, let normal scroll pass through)
	const sankeyZoom = d3.zoom()
		.scaleExtent([0.3, 5])
		.filter((event: any) => event.type !== "wheel" || event.ctrlKey)
		.on("zoom", (event: any) => zoomGroup.attr("transform", event.transform))
		.on("start", () => svg.style("cursor", "grabbing"))
		.on("end", () => svg.style("cursor", "grab"));
	svg.call(sankeyZoom);
	addResetButton(chartDiv, svg, sankeyZoom);
}
