/**
 * <folk-budget-river> — animated SVG sankey river visualization.
 * Pure renderer: receives nodes via setNodes() or falls back to demo data.
 * Parent component (folk-funds-app) handles data fetching and mapping.
 */

import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData, SufficiencyState } from "../lib/types";
import { computeSufficiencyState, computeSystemSufficiency, simulateTick, DEFAULT_CONFIG } from "../lib/simulation";
import { demoNodes } from "../lib/presets";

// ─── Layout types ───────────────────────────────────────

interface RiverLayout {
	sources: SourceLayout[];
	funnels: FunnelLayout[];
	outcomes: OutcomeLayout[];
	sourceWaterfalls: WaterfallLayout[];
	overflowBranches: BranchLayout[];
	spendingWaterfalls: WaterfallLayout[];
	width: number;
	height: number;
}

interface SourceLayout { id: string; label: string; flowRate: number; x: number; y: number; width: number; }
interface FunnelLayout { id: string; label: string; data: FunnelNodeData; x: number; y: number; riverWidth: number; segmentLength: number; layer: number; status: "healthy" | "overflow" | "critical"; sufficiency: SufficiencyState; }
interface OutcomeLayout { id: string; label: string; data: OutcomeNodeData; x: number; y: number; poolWidth: number; fillPercent: number; }
interface WaterfallLayout { id: string; sourceId: string; targetId: string; label: string; percentage: number; x: number; xSource: number; yStart: number; yEnd: number; width: number; riverEndWidth: number; farEndWidth: number; direction: "inflow" | "outflow"; color: string; flowAmount: number; }
interface BranchLayout { sourceId: string; targetId: string; percentage: number; x1: number; y1: number; x2: number; y2: number; width: number; color: string; }

// ─── Constants ───────────────────────────────────────────

const LAYER_HEIGHT = 160;
const WATERFALL_HEIGHT = 120;
const GAP = 40;
const MIN_RIVER_WIDTH = 24;
const MAX_RIVER_WIDTH = 100;
const MIN_WATERFALL_WIDTH = 4;
const SEGMENT_LENGTH = 200;
const POOL_WIDTH = 100;
const POOL_HEIGHT = 60;
const SOURCE_HEIGHT = 40;

const COLORS = {
	sourceWaterfall: "#10b981",
	riverHealthy: ["#0ea5e9", "#06b6d4"],
	riverOverflow: ["#f59e0b", "#fbbf24"],
	riverCritical: ["#ef4444", "#f87171"],
	riverSufficient: ["#fbbf24", "#10b981"],
	overflowBranch: "#f59e0b",
	spendingWaterfall: ["#8b5cf6", "#ec4899", "#06b6d4", "#3b82f6", "#10b981", "#6366f1"],
	outcomePool: "#3b82f6",
	goldenGlow: "#fbbf24",
	bg: "#0f172a",
	text: "#e2e8f0",
	textMuted: "#94a3b8",
};

function distributeWidths(percentages: number[], totalAvailable: number, minWidth: number): number[] {
	const totalPct = percentages.reduce((s, p) => s + p, 0);
	if (totalPct === 0) return percentages.map(() => minWidth);
	let widths = percentages.map((p) => (p / totalPct) * totalAvailable);
	const belowMin = widths.filter((w) => w < minWidth);
	if (belowMin.length > 0 && belowMin.length < widths.length) {
		const deficit = belowMin.reduce((s, w) => s + (minWidth - w), 0);
		const aboveMinTotal = widths.filter((w) => w >= minWidth).reduce((s, w) => s + w, 0);
		widths = widths.map((w) => {
			if (w < minWidth) return minWidth;
			return Math.max(minWidth, w - (w / aboveMinTotal) * deficit);
		});
	}
	return widths;
}

// ─── Layout engine (faithful port) ──────────────────────

function computeLayout(nodes: FlowNode[]): RiverLayout {
	const funnelNodes = nodes.filter((n) => n.type === "funnel");
	const outcomeNodes = nodes.filter((n) => n.type === "outcome");
	const sourceNodes = nodes.filter((n) => n.type === "source");

	const overflowTargets = new Set<string>();
	const spendingTargets = new Set<string>();

	funnelNodes.forEach((n) => {
		const data = n.data as FunnelNodeData;
		data.overflowAllocations?.forEach((a) => overflowTargets.add(a.targetId));
		data.spendingAllocations?.forEach((a) => spendingTargets.add(a.targetId));
	});

	const rootFunnels = funnelNodes.filter((n) => !overflowTargets.has(n.id));

	const funnelLayers = new Map<string, number>();
	rootFunnels.forEach((n) => funnelLayers.set(n.id, 0));

	const queue = [...rootFunnels];
	while (queue.length > 0) {
		const current = queue.shift()!;
		const data = current.data as FunnelNodeData;
		const parentLayer = funnelLayers.get(current.id) ?? 0;
		data.overflowAllocations?.forEach((a) => {
			const child = funnelNodes.find((n) => n.id === a.targetId);
			if (child && !funnelLayers.has(child.id)) {
				funnelLayers.set(child.id, parentLayer + 1);
				queue.push(child);
			}
		});
	}

	const layerGroups = new Map<number, FlowNode[]>();
	funnelNodes.forEach((n) => {
		const layer = funnelLayers.get(n.id) ?? 0;
		if (!layerGroups.has(layer)) layerGroups.set(layer, []);
		layerGroups.get(layer)!.push(n);
	});

	const maxLayer = Math.max(...Array.from(layerGroups.keys()), 0);
	const sourceLayerY = GAP;
	const funnelStartY = sourceLayerY + SOURCE_HEIGHT + WATERFALL_HEIGHT + GAP;

	const funnelLayouts: FunnelLayout[] = [];

	for (let layer = 0; layer <= maxLayer; layer++) {
		const layerNodes = layerGroups.get(layer) || [];
		const layerY = funnelStartY + layer * (LAYER_HEIGHT + WATERFALL_HEIGHT + GAP);
		const totalWidth = layerNodes.length * SEGMENT_LENGTH + (layerNodes.length - 1) * GAP * 2;

		layerNodes.forEach((n, i) => {
			const data = n.data as FunnelNodeData;
			const fillRatio = Math.min(1, data.currentValue / (data.maxCapacity || 1));
			const riverWidth = MIN_RIVER_WIDTH + fillRatio * (MAX_RIVER_WIDTH - MIN_RIVER_WIDTH);
			const x = -totalWidth / 2 + i * (SEGMENT_LENGTH + GAP * 2);
			const status: "healthy" | "overflow" | "critical" =
				data.currentValue > data.maxThreshold ? "overflow" :
				data.currentValue < data.minThreshold ? "critical" : "healthy";

			funnelLayouts.push({ id: n.id, label: data.label, data, x, y: layerY, riverWidth, segmentLength: SEGMENT_LENGTH, layer, status, sufficiency: computeSufficiencyState(data) });
		});
	}

	// Source layouts
	const sourceLayouts: SourceLayout[] = sourceNodes.map((n, i) => {
		const data = n.data as SourceNodeData;
		const totalWidth = sourceNodes.length * 120 + (sourceNodes.length - 1) * GAP;
		return { id: n.id, label: data.label, flowRate: data.flowRate, x: -totalWidth / 2 + i * (120 + GAP), y: sourceLayerY, width: 120 };
	});

	// Source waterfalls
	const inflowsByFunnel = new Map<string, { sourceNodeId: string; allocIndex: number; flowAmount: number; percentage: number }[]>();
	sourceNodes.forEach((sn) => {
		const data = sn.data as SourceNodeData;
		data.targetAllocations?.forEach((alloc, i) => {
			const flowAmount = (alloc.percentage / 100) * data.flowRate;
			if (!inflowsByFunnel.has(alloc.targetId)) inflowsByFunnel.set(alloc.targetId, []);
			inflowsByFunnel.get(alloc.targetId)!.push({ sourceNodeId: sn.id, allocIndex: i, flowAmount, percentage: alloc.percentage });
		});
	});

	const sourceWaterfalls: WaterfallLayout[] = [];
	sourceNodes.forEach((sn) => {
		const data = sn.data as SourceNodeData;
		const sourceLayout = sourceLayouts.find((s) => s.id === sn.id);
		if (!sourceLayout) return;
		data.targetAllocations?.forEach((alloc, allocIdx) => {
			const targetLayout = funnelLayouts.find((f) => f.id === alloc.targetId);
			if (!targetLayout) return;
			const flowAmount = (alloc.percentage / 100) * data.flowRate;
			const allInflowsToTarget = inflowsByFunnel.get(alloc.targetId) || [];
			const totalInflowToTarget = allInflowsToTarget.reduce((s, i) => s + i.flowAmount, 0);
			const share = totalInflowToTarget > 0 ? flowAmount / totalInflowToTarget : 1;
			const riverEndWidth = Math.max(MIN_WATERFALL_WIDTH, share * targetLayout.riverWidth);
			const farEndWidth = Math.max(MIN_WATERFALL_WIDTH, (alloc.percentage / 100) * sourceLayout.width * 0.8);
			const myIndex = allInflowsToTarget.findIndex((i) => i.sourceNodeId === sn.id && i.allocIndex === allocIdx);
			const inflowWidths = distributeWidths(allInflowsToTarget.map((i) => i.flowAmount), targetLayout.segmentLength * 0.7, MIN_WATERFALL_WIDTH);
			const startX = targetLayout.x + targetLayout.segmentLength * 0.15;
			let offsetX = 0;
			for (let k = 0; k < myIndex; k++) offsetX += inflowWidths[k];
			const riverCenterX = startX + offsetX + inflowWidths[myIndex] / 2;
			const sourceCenterX = sourceLayout.x + sourceLayout.width / 2;
			sourceWaterfalls.push({ id: `src-wf-${sn.id}-${alloc.targetId}`, sourceId: sn.id, targetId: alloc.targetId, label: `${alloc.percentage}%`, percentage: alloc.percentage, x: riverCenterX, xSource: sourceCenterX, yStart: sourceLayout.y + SOURCE_HEIGHT, yEnd: targetLayout.y, width: riverEndWidth, riverEndWidth, farEndWidth, direction: "inflow", color: COLORS.sourceWaterfall, flowAmount });
		});
	});

	// Implicit waterfalls for root funnels without source nodes
	if (sourceNodes.length === 0) {
		rootFunnels.forEach((rn) => {
			const data = rn.data as FunnelNodeData;
			if (data.inflowRate <= 0) return;
			const layout = funnelLayouts.find((f) => f.id === rn.id);
			if (!layout) return;
			sourceWaterfalls.push({ id: `implicit-wf-${rn.id}`, sourceId: "implicit", targetId: rn.id, label: `$${Math.floor(data.inflowRate)}/mo`, percentage: 100, x: layout.x + layout.segmentLength / 2, xSource: layout.x + layout.segmentLength / 2, yStart: GAP, yEnd: layout.y, width: layout.riverWidth, riverEndWidth: layout.riverWidth, farEndWidth: Math.max(MIN_WATERFALL_WIDTH, layout.riverWidth * 0.4), direction: "inflow", color: COLORS.sourceWaterfall, flowAmount: data.inflowRate });
		});
	}

	// Overflow branches
	const overflowBranches: BranchLayout[] = [];
	funnelNodes.forEach((n) => {
		const data = n.data as FunnelNodeData;
		const parentLayout = funnelLayouts.find((f) => f.id === n.id);
		if (!parentLayout) return;
		data.overflowAllocations?.forEach((alloc) => {
			const childLayout = funnelLayouts.find((f) => f.id === alloc.targetId);
			if (!childLayout) return;
			const width = Math.max(MIN_WATERFALL_WIDTH, (alloc.percentage / 100) * parentLayout.riverWidth);
			overflowBranches.push({ sourceId: n.id, targetId: alloc.targetId, percentage: alloc.percentage, x1: parentLayout.x + parentLayout.segmentLength, y1: parentLayout.y + parentLayout.riverWidth / 2, x2: childLayout.x, y2: childLayout.y + childLayout.riverWidth / 2, width, color: alloc.color || COLORS.overflowBranch });
		});
	});

	// Outcome layouts
	const outcomeY = funnelStartY + (maxLayer + 1) * (LAYER_HEIGHT + WATERFALL_HEIGHT + GAP) + WATERFALL_HEIGHT;
	const totalOutcomeWidth = outcomeNodes.length * (POOL_WIDTH + GAP) - GAP;
	const outcomeLayouts: OutcomeLayout[] = outcomeNodes.map((n, i) => {
		const data = n.data as OutcomeNodeData;
		const fillPercent = data.fundingTarget > 0 ? Math.min(100, (data.fundingReceived / data.fundingTarget) * 100) : 0;
		return { id: n.id, label: data.label, data, x: -totalOutcomeWidth / 2 + i * (POOL_WIDTH + GAP), y: outcomeY, poolWidth: POOL_WIDTH, fillPercent };
	});

	// Spending waterfalls
	const spendingWaterfalls: WaterfallLayout[] = [];
	funnelNodes.forEach((n) => {
		const data = n.data as FunnelNodeData;
		const parentLayout = funnelLayouts.find((f) => f.id === n.id);
		if (!parentLayout) return;
		const allocations = data.spendingAllocations || [];
		if (allocations.length === 0) return;
		const percentages = allocations.map((a) => a.percentage);
		const slotWidths = distributeWidths(percentages, parentLayout.segmentLength * 0.7, MIN_WATERFALL_WIDTH);
		const riverEndWidths = distributeWidths(percentages, parentLayout.riverWidth, MIN_WATERFALL_WIDTH);
		const startX = parentLayout.x + parentLayout.segmentLength * 0.15;
		let offsetX = 0;
		allocations.forEach((alloc, i) => {
			const outcomeLayout = outcomeLayouts.find((o) => o.id === alloc.targetId);
			if (!outcomeLayout) return;
			const riverEndWidth = riverEndWidths[i];
			const farEndWidth = Math.max(MIN_WATERFALL_WIDTH, outcomeLayout.poolWidth * 0.6);
			const riverCenterX = startX + offsetX + slotWidths[i] / 2;
			offsetX += slotWidths[i];
			const poolCenterX = outcomeLayout.x + outcomeLayout.poolWidth / 2;
			spendingWaterfalls.push({ id: `spend-wf-${n.id}-${alloc.targetId}`, sourceId: n.id, targetId: alloc.targetId, label: `${alloc.percentage}%`, percentage: alloc.percentage, x: riverCenterX, xSource: poolCenterX, yStart: parentLayout.y + parentLayout.riverWidth + 4, yEnd: outcomeLayout.y, width: riverEndWidth, riverEndWidth, farEndWidth, direction: "outflow", color: alloc.color || COLORS.spendingWaterfall[i % COLORS.spendingWaterfall.length], flowAmount: (alloc.percentage / 100) * (data.inflowRate || 1) });
		});
	});

	// Compute bounds and normalize
	const allX = [...funnelLayouts.map((f) => f.x), ...funnelLayouts.map((f) => f.x + f.segmentLength), ...outcomeLayouts.map((o) => o.x), ...outcomeLayouts.map((o) => o.x + o.poolWidth), ...sourceLayouts.map((s) => s.x), ...sourceLayouts.map((s) => s.x + s.width)];
	const allY = [...funnelLayouts.map((f) => f.y + f.riverWidth), ...outcomeLayouts.map((o) => o.y + POOL_HEIGHT), sourceLayerY];

	const minX = Math.min(...allX, -100);
	const maxX = Math.max(...allX, 100);
	const maxY = Math.max(...allY, 400);
	const padding = 80;

	const offsetXGlobal = -minX + padding;
	const offsetYGlobal = padding;

	funnelLayouts.forEach((f) => { f.x += offsetXGlobal; f.y += offsetYGlobal; });
	outcomeLayouts.forEach((o) => { o.x += offsetXGlobal; o.y += offsetYGlobal; });
	sourceLayouts.forEach((s) => { s.x += offsetXGlobal; s.y += offsetYGlobal; });
	sourceWaterfalls.forEach((w) => { w.x += offsetXGlobal; w.xSource += offsetXGlobal; w.yStart += offsetYGlobal; w.yEnd += offsetYGlobal; });
	overflowBranches.forEach((b) => { b.x1 += offsetXGlobal; b.y1 += offsetYGlobal; b.x2 += offsetXGlobal; b.y2 += offsetYGlobal; });
	spendingWaterfalls.forEach((w) => { w.x += offsetXGlobal; w.xSource += offsetXGlobal; w.yStart += offsetYGlobal; w.yEnd += offsetYGlobal; });

	return { sources: sourceLayouts, funnels: funnelLayouts, outcomes: outcomeLayouts, sourceWaterfalls, overflowBranches, spendingWaterfalls, width: maxX - minX + padding * 2, height: maxY + offsetYGlobal + padding };
}

// ─── SVG Rendering ──────────────────────────────────────

function renderWaterfall(wf: WaterfallLayout): string {
	const isInflow = wf.direction === "inflow";
	const height = wf.yEnd - wf.yStart;
	if (height <= 0) return "";

	const topWidth = isInflow ? wf.farEndWidth : wf.riverEndWidth;
	const bottomWidth = isInflow ? wf.riverEndWidth : wf.farEndWidth;
	const topCx = isInflow ? wf.xSource : wf.x;
	const bottomCx = isInflow ? wf.x : wf.xSource;

	const cpFrac1 = isInflow ? 0.55 : 0.2;
	const cpFrac2 = isInflow ? 0.75 : 0.45;
	const cpY1 = wf.yStart + height * cpFrac1;
	const cpY2 = wf.yStart + height * cpFrac2;

	const tl = topCx - topWidth / 2;
	const tr = topCx + topWidth / 2;
	const bl = bottomCx - bottomWidth / 2;
	const br = bottomCx + bottomWidth / 2;

	const shapePath = `M ${tl} ${wf.yStart} C ${tl} ${cpY1}, ${bl} ${cpY2}, ${bl} ${wf.yEnd} L ${br} ${wf.yEnd} C ${br} ${cpY2}, ${tr} ${cpY1}, ${tr} ${wf.yStart} Z`;
	const clipId = `sankey-clip-${wf.id}`;
	const gradId = `sankey-grad-${wf.id}`;
	const pathMinX = Math.min(tl, bl) - 5;
	const pathMaxW = Math.max(topWidth, bottomWidth) + 10;

	return `
		<defs>
			<clipPath id="${clipId}"><path d="${shapePath}"/></clipPath>
			<linearGradient id="${gradId}" x1="0" y1="0" x2="0" y2="1">
				<stop offset="0%" stop-color="${wf.color}" stop-opacity="${isInflow ? 0.85 : 0.5}"/>
				<stop offset="50%" stop-color="${wf.color}" stop-opacity="0.65"/>
				<stop offset="100%" stop-color="${wf.color}" stop-opacity="${isInflow ? 0.35 : 0.85}"/>
			</linearGradient>
		</defs>
		<path d="${shapePath}" fill="${wf.color}" opacity="0.08"/>
		<path d="${shapePath}" fill="url(#${gradId})"/>
		<g clip-path="url(#${clipId})">
			${[0, 1, 2].map((i) => `<rect x="${pathMinX}" y="${wf.yStart - height}" width="${pathMaxW}" height="${height}" fill="${wf.color}" opacity="0.12" style="animation:waterFlow ${1.4 + i * 0.3}s linear infinite;animation-delay:${i * -0.4}s"/>`).join("")}
		</g>
		<path d="M ${tl} ${wf.yStart} C ${tl} ${cpY1}, ${bl} ${cpY2}, ${bl} ${wf.yEnd}" fill="none" stroke="${wf.color}" stroke-width="1" opacity="0.3" stroke-dasharray="4 6" style="animation:riverCurrent 1s linear infinite"/>`;
}

function renderBranch(b: BranchLayout): string {
	const dx = b.x2 - b.x1;
	const dy = b.y2 - b.y1;
	const cpx = b.x1 + dx * 0.5;
	const halfW = b.width / 2;

	return `
		<path d="M ${b.x1} ${b.y1 - halfW} C ${cpx} ${b.y1 - halfW}, ${cpx} ${b.y2 - halfW}, ${b.x2} ${b.y2 - halfW} L ${b.x2} ${b.y2 + halfW} C ${cpx} ${b.y2 + halfW}, ${cpx} ${b.y1 + halfW}, ${b.x1} ${b.y1 + halfW} Z" fill="${b.color}" opacity="0.35"/>
		<text x="${(b.x1 + b.x2) / 2}" y="${(b.y1 + b.y2) / 2 - 8}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">${b.percentage}%</text>`;
}

function renderSource(s: SourceLayout): string {
	return `
		<rect x="${s.x}" y="${s.y}" width="${s.width}" height="${SOURCE_HEIGHT}" rx="8" fill="#1e293b" stroke="#334155"/>
		<text x="${s.x + s.width / 2}" y="${s.y + 16}" text-anchor="middle" fill="${COLORS.text}" font-size="11" font-weight="600">${esc(s.label)}</text>
		<text x="${s.x + s.width / 2}" y="${s.y + 30}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="9">$${s.flowRate.toLocaleString()}/mo</text>`;
}

function renderFunnel(f: FunnelLayout): string {
	const colors = f.status === "overflow" ? COLORS.riverOverflow : f.status === "critical" ? COLORS.riverCritical : f.sufficiency === "sufficient" || f.sufficiency === "abundant" ? COLORS.riverSufficient : COLORS.riverHealthy;
	const gradId = `river-grad-${f.id}`;
	const fillRatio = f.data.currentValue / (f.data.maxCapacity || 1);
	const threshold = f.data.sufficientThreshold ?? f.data.maxThreshold;
	const isSufficient = f.sufficiency === "sufficient" || f.sufficiency === "abundant";

	return `
		<defs>
			<linearGradient id="${gradId}" x1="0" y1="0" x2="1" y2="0">
				<stop offset="0%" stop-color="${colors[0]}" stop-opacity="0.7"/>
				<stop offset="50%" stop-color="${colors[1] || colors[0]}" stop-opacity="0.9"/>
				<stop offset="100%" stop-color="${colors[0]}" stop-opacity="0.7"/>
			</linearGradient>
		</defs>
		${isSufficient ? `<rect x="${f.x - 4}" y="${f.y - 4}" width="${f.segmentLength + 8}" height="${f.riverWidth + 8}" rx="6" fill="none" stroke="${COLORS.goldenGlow}" stroke-width="2" opacity="0.6" style="animation:shimmer 2s ease-in-out infinite"/>` : ""}
		<rect x="${f.x}" y="${f.y}" width="${f.segmentLength}" height="${f.riverWidth}" rx="4" fill="url(#${gradId})"/>
		${[0, 1, 2].map((i) => `<rect x="${f.x}" y="${f.y + (f.riverWidth / 4) * i}" width="${f.segmentLength}" height="${f.riverWidth / 4}" fill="${colors[0]}" opacity="0.08" style="animation:waterFlow ${2 + i * 0.5}s linear infinite;animation-delay:${i * -0.6}s"/>`).join("")}
		<text x="${f.x + f.segmentLength / 2}" y="${f.y - 12}" text-anchor="middle" fill="${COLORS.text}" font-size="13" font-weight="600">${esc(f.label)}</text>
		<text x="${f.x + f.segmentLength / 2}" y="${f.y - 2}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">$${Math.floor(f.data.currentValue).toLocaleString()} / $${Math.floor(threshold).toLocaleString()} ${isSufficient ? "✨" : ""}</text>
		<rect x="${f.x}" y="${f.y + f.riverWidth + 4}" width="${f.segmentLength}" height="3" rx="1.5" fill="#334155"/>
		<rect x="${f.x}" y="${f.y + f.riverWidth + 4}" width="${f.segmentLength * fillRatio}" height="3" rx="1.5" fill="${colors[0]}"/>`;
}

function renderOutcome(o: OutcomeLayout): string {
	const filled = (o.fillPercent / 100) * POOL_HEIGHT;
	const color = o.data.status === "completed" ? "#10b981" : o.data.status === "blocked" ? "#ef4444" : "#3b82f6";

	return `
		<rect x="${o.x}" y="${o.y}" width="${o.poolWidth}" height="${POOL_HEIGHT}" rx="8" fill="#1e293b" stroke="#334155"/>
		<rect x="${o.x + 2}" y="${o.y + POOL_HEIGHT - filled}" width="${o.poolWidth - 4}" height="${filled}" rx="6" fill="${color}" opacity="0.4"/>
		${filled > 5 ? `<rect x="${o.x + 2}" y="${o.y + POOL_HEIGHT - filled}" width="${o.poolWidth - 4}" height="3" rx="1.5" fill="${color}" opacity="0.6" style="animation:waveFloat 2s ease-in-out infinite"/>` : ""}
		<text x="${o.x + o.poolWidth / 2}" y="${o.y + POOL_HEIGHT + 14}" text-anchor="middle" fill="${COLORS.text}" font-size="10" font-weight="500">${esc(o.label)}</text>
		<text x="${o.x + o.poolWidth / 2}" y="${o.y + POOL_HEIGHT + 26}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="9">${Math.round(o.fillPercent)}%</text>`;
}

function renderSufficiencyBadge(score: number, x: number, y: number): string {
	const pct = Math.round(score * 100);
	const color = pct >= 90 ? COLORS.goldenGlow : pct >= 60 ? "#10b981" : pct >= 30 ? "#f59e0b" : "#ef4444";
	const circumference = 2 * Math.PI * 18;
	const dashoffset = circumference * (1 - score);

	return `
		<g transform="translate(${x}, ${y})">
			<circle cx="24" cy="24" r="22" fill="#1e293b" stroke="#334155" stroke-width="1.5"/>
			<circle cx="24" cy="24" r="18" fill="none" stroke="#334155" stroke-width="3"/>
			<circle cx="24" cy="24" r="18" fill="none" stroke="${color}" stroke-width="3" stroke-dasharray="${circumference}" stroke-dashoffset="${dashoffset}" transform="rotate(-90 24 24)" stroke-linecap="round"/>
			<text x="24" y="22" text-anchor="middle" fill="${color}" font-size="11" font-weight="700">${pct}%</text>
			<text x="24" y="34" text-anchor="middle" fill="${COLORS.textMuted}" font-size="7">ENOUGH</text>
		</g>`;
}

function esc(s: string): string {
	return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

// ─── Web Component ──────────────────────────────────────

class FolkBudgetRiver extends HTMLElement {
	private shadow: ShadowRoot;
	private nodes: FlowNode[] = [];
	private simulating = false;
	private simTimer: ReturnType<typeof setInterval> | null = null;
	private dragging = false;
	private dragStartX = 0;
	private dragStartY = 0;
	private scrollStartX = 0;
	private scrollStartY = 0;

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
	}

	static get observedAttributes() { return ["simulate"]; }

	connectedCallback() {
		this.simulating = this.getAttribute("simulate") === "true";
		if (this.nodes.length === 0) {
			this.nodes = [...demoNodes.map((n) => ({ ...n, data: { ...n.data } }))];
		}
		this.render();
		if (this.simulating) this.startSimulation();
	}

	disconnectedCallback() {
		this.stopSimulation();
	}

	attributeChangedCallback(name: string, _: string, newVal: string) {
		if (name === "simulate") {
			this.simulating = newVal === "true";
			if (this.simulating) this.startSimulation();
			else this.stopSimulation();
		}
	}

	setNodes(nodes: FlowNode[]) {
		this.nodes = nodes;
		this.render();
	}

	private startSimulation() {
		if (this.simTimer) return;
		this.simTimer = setInterval(() => {
			this.nodes = simulateTick(this.nodes, DEFAULT_CONFIG);
			this.render();
		}, 500);
	}

	private stopSimulation() {
		if (this.simTimer) { clearInterval(this.simTimer); this.simTimer = null; }
	}

	private render() {
		const layout = computeLayout(this.nodes);
		const score = computeSystemSufficiency(this.nodes);

		this.shadow.innerHTML = `
		<style>
			:host { display: block; }
			.container { position: relative; overflow: auto; background: ${COLORS.bg}; border-radius: 12px; border: 1px solid #334155; max-height: 85vh; cursor: grab; }
			.container.dragging { cursor: grabbing; user-select: none; }
			svg { display: block; }
			.controls { position: absolute; top: 12px; left: 12px; display: flex; gap: 8px; }
			.controls button { padding: 6px 12px; border-radius: 6px; border: 1px solid #334155; background: #1e293b; color: #94a3b8; cursor: pointer; font-size: 12px; }
			.controls button:hover { border-color: #6366f1; color: #f1f5f9; }
			.controls button.active { background: #4f46e5; border-color: #6366f1; color: #fff; }
			.legend { position: absolute; bottom: 12px; left: 12px; background: rgba(15,23,42,0.9); border: 1px solid #334155; border-radius: 8px; padding: 8px 12px; font-size: 10px; color: #94a3b8; }
			.legend-item { display: flex; align-items: center; gap: 6px; margin: 2px 0; }
			.legend-dot { width: 8px; height: 8px; border-radius: 2px; }
			@keyframes waterFlow { 0% { transform: translateY(0); } 100% { transform: translateY(100%); } }
			@keyframes riverCurrent { 0% { stroke-dashoffset: 10; } 100% { stroke-dashoffset: 0; } }
			@keyframes shimmer { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.8; } }
			@keyframes waveFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
		</style>
		<div class="container">
			<svg viewBox="0 0 ${layout.width} ${layout.height}" width="${layout.width}" height="${layout.height}">
				${layout.sourceWaterfalls.map(renderWaterfall).join("")}
				${layout.spendingWaterfalls.map(renderWaterfall).join("")}
				${layout.overflowBranches.map(renderBranch).join("")}
				${layout.sources.map(renderSource).join("")}
				${layout.funnels.map(renderFunnel).join("")}
				${layout.outcomes.map(renderOutcome).join("")}
				${renderSufficiencyBadge(score, layout.width - 70, 10)}
			</svg>
			<div class="controls">
				<button class="${this.simulating ? "active" : ""}" data-action="toggle-sim">${this.simulating ? "⏸ Pause" : "▶ Simulate"}</button>
			</div>
			<div class="legend">
				<div class="legend-item"><div class="legend-dot" style="background:#10b981"></div> Inflow</div>
				<div class="legend-item"><div class="legend-dot" style="background:#0ea5e9"></div> Healthy</div>
				<div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div> Overflow</div>
				<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div> Critical</div>
				<div class="legend-item"><div class="legend-dot" style="background:#8b5cf6"></div> Spending</div>
				<div class="legend-item"><div class="legend-dot" style="background:#fbbf24"></div> Sufficient</div>
			</div>
		</div>`;

		this.shadow.querySelector("[data-action=toggle-sim]")?.addEventListener("click", () => {
			this.simulating = !this.simulating;
			if (this.simulating) this.startSimulation();
			else this.stopSimulation();
			this.render();
		});

		// Drag-to-pan
		const container = this.shadow.querySelector(".container") as HTMLElement;
		if (container) {
			container.addEventListener("pointerdown", (e: PointerEvent) => {
				if ((e.target as HTMLElement).closest("button")) return;
				this.dragging = true;
				this.dragStartX = e.clientX;
				this.dragStartY = e.clientY;
				this.scrollStartX = container.scrollLeft;
				this.scrollStartY = container.scrollTop;
				container.classList.add("dragging");
				container.setPointerCapture(e.pointerId);
			});
			container.addEventListener("pointermove", (e: PointerEvent) => {
				if (!this.dragging) return;
				container.scrollLeft = this.scrollStartX - (e.clientX - this.dragStartX);
				container.scrollTop = this.scrollStartY - (e.clientY - this.dragStartY);
			});
			container.addEventListener("pointerup", (e: PointerEvent) => {
				this.dragging = false;
				container.classList.remove("dragging");
				container.releasePointerCapture(e.pointerId);
			});

			// Auto-center on initial render
			container.scrollLeft = (container.scrollWidth - container.clientWidth) / 2;
			container.scrollTop = 0;
		}
	}
}

customElements.define("folk-budget-river", FolkBudgetRiver);
