/**
 * <folk-flow-river> — Sankey-style SVG flow visualization.
 * Pure renderer: receives nodes via setNodes() or falls back to demo data.
 * Parent component (folk-flows-app) handles data fetching and mapping.
 *
 * Visual vocabulary:
 *   Source  → rounded rect with flow rate label + source-type badge
 *   Funnel  → trapezoid vessel with solid fill bar + overflow threshold line
 *   Flow    → Sankey-style colored band with $ label (width proportional to amount)
 *   Outcome → rounded rect with horizontal progress bar
 */

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[];
	bands: BandLayout[];
	width: number;
	height: number;
}

interface SourceLayout { id: string; label: string; flowRate: number; x: number; y: number; w: number; h: number; sourceType: string; }
interface FunnelLayout { id: string; label: string; data: FunnelNodeData; x: number; y: number; w: number; h: number; bw: number; fillLevel: number; overflowLevel: number; sufficiency: SufficiencyState; }
interface OutcomeLayout { id: string; label: string; data: OutcomeNodeData; x: number; y: number; w: number; h: number; fillPercent: number; }
interface BandLayout { id: string; sourceId: string; targetId: string; x1: number; y1: number; x2: number; y2: number; width: number; color: string; label: string; kind: "inflow" | "spending" | "overflow"; flowAmount: number; }

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

const SOURCE_W = 140;
const SOURCE_H = 60;
const VESSEL_W = 200;
const VESSEL_H = 140;
const VESSEL_BW = 100; // bottom width of trapezoid
const POOL_W = 120;
const POOL_H = 65;
const LAYER_GAP = 200;
const H_GAP = 80;
const MIN_BAND_W = 4;
const MAX_BAND_W = 60;
const PADDING = 100;

const COLORS = {
	inflow: "#10b981",
	spending: "#8b5cf6",
	overflow: "#f59e0b",
	text: "var(--rs-text-primary)",
	textMuted: "var(--rs-text-secondary)",
	bg: "var(--rs-bg-page)",
	surface: "var(--rs-bg-surface)",
	surfaceRaised: "var(--rs-bg-surface-raised)",
	sourceType: { card: "#10b981", safe_wallet: "#8b5cf6", ridentity: "#3b82f6", metamask: "#f59e0b", unconfigured: "#64748b" } as Record<string, string>,
};

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

function fmtDollars(n: number): string {
	return n >= 1000 ? `$${(n / 1000).toFixed(1)}k` : `$${Math.floor(n)}`;
}

/** Interpolate left/right edges of trapezoid at a given Y fraction (0=top, 1=bottom) */
function vesselEdgesAtY(x: number, topW: number, bottomW: number, yFrac: number): { left: number; right: number } {
	const cx = x + topW / 2;
	const halfAtY = topW / 2 + (bottomW / 2 - topW / 2) * yFrac;
	return { left: cx - halfAtY, right: cx + halfAtY };
}

// ─── Layout engine ──────────────────────────────────────

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");

	// Build layers by Y-position proximity
	const funnelsByY = [...funnelNodes].sort((a, b) => a.position.y - b.position.y);
	const layers: FlowNode[][] = [];
	for (const n of funnelsByY) {
		const lastLayer = layers[layers.length - 1];
		if (lastLayer && Math.abs(n.position.y - lastLayer[0].position.y) < 200) {
			lastLayer.push(n);
		} else {
			layers.push([n]);
		}
	}
	// Sort each layer by X
	layers.forEach((l) => l.sort((a, b) => a.position.x - b.position.x));

	const funnelLayerMap = new Map<string, number>();
	layers.forEach((l, i) => l.forEach((n) => funnelLayerMap.set(n.id, i)));

	// Position sources
	const sourceStartY = PADDING;
	const sourceTotalW = sourceNodes.length * SOURCE_W + (sourceNodes.length - 1) * H_GAP;
	const sourceLayouts: SourceLayout[] = sourceNodes.map((n, i) => {
		const data = n.data as SourceNodeData;
		return {
			id: n.id, label: data.label, flowRate: data.flowRate,
			x: -sourceTotalW / 2 + i * (SOURCE_W + H_GAP), y: sourceStartY,
			w: SOURCE_W, h: SOURCE_H, sourceType: data.sourceType || "unconfigured",
		};
	});

	// Position funnels — dynamic vessel width based on widest layer
	const funnelStartY = sourceStartY + SOURCE_H + LAYER_GAP;
	const maxLayerSize = Math.max(1, ...layers.map((l) => l.length));
	const dynamicVesselW = Math.min(200, Math.max(120, 800 / maxLayerSize));
	const dynamicVesselBW = dynamicVesselW * (VESSEL_BW / VESSEL_W);
	const funnelLayouts: FunnelLayout[] = [];
	layers.forEach((layer, layerIdx) => {
		const totalW = layer.length * dynamicVesselW + (layer.length - 1) * H_GAP;
		const layerY = funnelStartY + layerIdx * (VESSEL_H + LAYER_GAP);
		layer.forEach((n, i) => {
			const data = n.data as FunnelNodeData;
			const fillLevel = Math.min(1, data.currentValue / (data.capacity || 1));
			const overflowLevel = data.overflowThreshold / (data.capacity || 1);
			funnelLayouts.push({
				id: n.id, label: data.label, data,
				x: -totalW / 2 + i * (dynamicVesselW + H_GAP), y: layerY,
				w: dynamicVesselW, h: VESSEL_H, bw: dynamicVesselBW,
				fillLevel, overflowLevel,
				sufficiency: computeSufficiencyState(data),
			});
		});
	});

	// Position outcomes
	const maxLayerIdx = layers.length - 1;
	const outcomeY = funnelStartY + (maxLayerIdx + 1) * (VESSEL_H + LAYER_GAP);
	const outcomeTotalW = outcomeNodes.length * POOL_W + (outcomeNodes.length - 1) * (H_GAP / 2);
	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: -outcomeTotalW / 2 + i * (POOL_W + H_GAP / 2), y: outcomeY,
			w: POOL_W, h: POOL_H, fillPercent,
		};
	});

	// Build flow bands
	const bands: BandLayout[] = [];
	const nodeCenter = (layouts: { id: string; x: number; w: number; y: number; h: number }[], id: string): { cx: number; top: number; bottom: number } | null => {
		const l = layouts.find((n) => n.id === id);
		return l ? { cx: l.x + l.w / 2, top: l.y, bottom: l.y + l.h } : null;
	};
	const allLayouts = [...sourceLayouts.map((s) => ({ ...s })), ...funnelLayouts.map((f) => ({ ...f })), ...outcomeLayouts.map((o) => ({ ...o }))];

	// Source → funnel bands
	const maxFlowRate = Math.max(1, ...sourceNodes.map((n) => (n.data as SourceNodeData).flowRate));
	sourceNodes.forEach((sn) => {
		const data = sn.data as SourceNodeData;
		const src = nodeCenter(allLayouts, sn.id);
		data.targetAllocations?.forEach((alloc) => {
			const tgt = nodeCenter(allLayouts, alloc.targetId);
			if (!src || !tgt) return;
			const flowAmount = data.flowRate * (alloc.percentage / 100);
			const w = Math.max(MIN_BAND_W, (flowAmount / maxFlowRate) * MAX_BAND_W);
			bands.push({
				id: `src-${sn.id}-${alloc.targetId}`, sourceId: sn.id, targetId: alloc.targetId,
				x1: src.cx, y1: src.bottom, x2: tgt.cx, y2: tgt.top,
				width: w, color: alloc.color || COLORS.inflow,
				label: `${fmtDollars(flowAmount)}/mo`, kind: "inflow", flowAmount,
			});
		});
	});

	// Funnel overflow + spending bands
	funnelNodes.forEach((fn) => {
		const data = fn.data as FunnelNodeData;
		const src = funnelLayouts.find((f) => f.id === fn.id);
		if (!src) return;
		const srcCenter = src.x + src.w / 2;

		// Overflow bands (from sides)
		data.overflowAllocations?.forEach((alloc, i) => {
			const tgt = nodeCenter(allLayouts, alloc.targetId);
			if (!tgt) return;
			const excess = Math.max(0, data.currentValue - data.overflowThreshold);
			const flowAmount = excess * (alloc.percentage / 100);
			const rateAmount = data.inflowRate > data.drainRate ? (data.inflowRate - data.drainRate) * (alloc.percentage / 100) : 0;
			const displayAmount = flowAmount > 0 ? flowAmount : rateAmount;
			const w = Math.max(MIN_BAND_W, displayAmount > 0 ? Math.min(MAX_BAND_W, (displayAmount / maxFlowRate) * MAX_BAND_W) : MIN_BAND_W);
			const overflowFrac = 1 - src.overflowLevel;
			const lipY = src.y + overflowFrac * src.h;
			const side = i % 2 === 0 ? -1 : 1;
			const edges = vesselEdgesAtY(src.x, src.w, src.bw, overflowFrac);
			const lipX = side < 0 ? edges.left : edges.right;
			bands.push({
				id: `ovf-${fn.id}-${alloc.targetId}`, sourceId: fn.id, targetId: alloc.targetId,
				x1: lipX, y1: lipY, x2: tgt.cx, y2: tgt.top,
				width: w, color: alloc.color || COLORS.overflow,
				label: displayAmount > 0 ? `${fmtDollars(displayAmount)}/mo` : `${alloc.percentage}%`,
				kind: "overflow", flowAmount: displayAmount,
			});
		});

		// Spending bands (from bottom)
		data.spendingAllocations?.forEach((alloc) => {
			const tgt = nodeCenter(allLayouts, alloc.targetId);
			if (!tgt) return;
			const flowAmount = data.drainRate * (alloc.percentage / 100);
			const w = Math.max(MIN_BAND_W, (flowAmount / maxFlowRate) * MAX_BAND_W);
			bands.push({
				id: `spd-${fn.id}-${alloc.targetId}`, sourceId: fn.id, targetId: alloc.targetId,
				x1: srcCenter, y1: src.y + src.h, x2: tgt.cx, y2: tgt.top,
				width: w, color: alloc.color || COLORS.spending,
				label: `${fmtDollars(flowAmount)}/mo`, kind: "spending", flowAmount,
			});
		});
	});

	// Normalize coordinates
	const allX = [...sourceLayouts.flatMap((s) => [s.x, s.x + s.w]), ...funnelLayouts.flatMap((f) => [f.x, f.x + f.w]), ...outcomeLayouts.flatMap((o) => [o.x, o.x + o.w])];
	const allY = [...sourceLayouts.map((s) => s.y + s.h), ...funnelLayouts.map((f) => f.y + f.h), ...outcomeLayouts.map((o) => o.y + o.h + 30)];
	const minX = Math.min(...allX, 0);
	const maxX = Math.max(...allX, 0);
	const maxY = Math.max(...allY, 400);

	const offsetX = -minX + PADDING;
	const offsetY = 0;
	sourceLayouts.forEach((s) => { s.x += offsetX; s.y += offsetY; });
	funnelLayouts.forEach((f) => { f.x += offsetX; f.y += offsetY; });
	outcomeLayouts.forEach((o) => { o.x += offsetX; o.y += offsetY; });
	bands.forEach((b) => { b.x1 += offsetX; b.y1 += offsetY; b.x2 += offsetX; b.y2 += offsetY; });

	return {
		sources: sourceLayouts, funnels: funnelLayouts, outcomes: outcomeLayouts, bands,
		width: maxX - minX + PADDING * 2, height: maxY + offsetY + PADDING,
	};
}

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

function renderBand(b: BandLayout): string {
	const hw = b.width / 2;
	const dx = b.x2 - b.x1;
	const dy = b.y2 - b.y1;

	let path: string;
	let center: string;
	let midX: number;
	let midY: number;

	if (dy > 20) {
		// Downward flows: existing cubic bezier ribbon
		const cp1y = b.y1 + dy * 0.15;
		const cp2y = b.y2 - dy * 0.15;
		path = [
			`M ${b.x1 - hw} ${b.y1}`,
			`C ${b.x1 - hw} ${cp1y}, ${b.x2 - hw} ${cp2y}, ${b.x2 - hw} ${b.y2}`,
			`L ${b.x2 + hw} ${b.y2}`,
			`C ${b.x2 + hw} ${cp2y}, ${b.x1 + hw} ${cp1y}, ${b.x1 + hw} ${b.y1}`,
			`Z`,
		].join(" ");
		center = `M ${b.x1} ${b.y1} C ${b.x1} ${cp1y}, ${b.x2} ${cp2y}, ${b.x2} ${b.y2}`;
		midX = (b.x1 + b.x2) / 2;
		midY = (b.y1 + b.y2) / 2;
	} else {
		// Upward/same-level flows: loopback arc routing outward then to target
		const loopRadius = Math.max(80, Math.abs(dy) * 0.4 + Math.abs(dx) * 0.3);
		// Route outward based on dx direction (or right if same x)
		const outDir = dx >= 0 ? 1 : -1;
		const arcX = b.x1 + outDir * loopRadius;
		const arcY = Math.min(b.y1, b.y2) - loopRadius * 0.6;

		// Ribbon left/right edges via cubic bezier through the arc point
		path = [
			`M ${b.x1 - hw} ${b.y1}`,
			`C ${arcX - hw} ${arcY}, ${arcX - hw} ${arcY}, ${b.x2 - hw} ${b.y2}`,
			`L ${b.x2 + hw} ${b.y2}`,
			`C ${arcX + hw} ${arcY}, ${arcX + hw} ${arcY}, ${b.x1 + hw} ${b.y1}`,
			`Z`,
		].join(" ");
		center = `M ${b.x1} ${b.y1} C ${arcX} ${arcY}, ${arcX} ${arcY}, ${b.x2} ${b.y2}`;
		midX = arcX;
		midY = arcY + loopRadius * 0.3;
	}

	return `
		<path d="${path}" fill="${b.color}" opacity="0.45"/>
		<path d="${path}" fill="none" stroke="${b.color}" stroke-width="0.5" opacity="0.75"/>
		<path d="${center}" fill="none" stroke="${b.color}" stroke-width="1.5" opacity="0.75" stroke-dasharray="6 8" class="flow-dash"/>
		<text x="${midX}" y="${midY - 4}" text-anchor="middle" fill="${b.color}" font-size="9" font-weight="600" opacity="0.9">${b.label}</text>`;
}

function renderSource(s: SourceLayout): string {
	const cx = s.x + s.w / 2;
	const typeColor = COLORS.sourceType[s.sourceType] || COLORS.sourceType.unconfigured;

	return `
		<rect x="${s.x}" y="${s.y}" width="${s.w}" height="${s.h}" rx="10" fill="${COLORS.surface}" stroke="${typeColor}" stroke-width="1.5" opacity="0.9"/>
		<circle cx="${s.x + 16}" cy="${s.y + 16}" r="6" fill="${typeColor}" opacity="0.7"/>
		<text x="${cx}" y="${s.y + 26}" text-anchor="middle" fill="${COLORS.text}" font-size="11" font-weight="600">${esc(s.label)}</text>
		<text x="${cx}" y="${s.y + 44}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">${fmtDollars(s.flowRate)}/mo</text>
		<circle cx="${s.x + s.w - 14}" cy="${s.y + 14}" r="10" fill="${COLORS.surface}" stroke="${COLORS.surfaceRaised}" stroke-width="1" data-interactive="edit-rate" data-source-id="${s.id}" style="cursor:pointer" opacity="0.9"/>
		<text x="${s.x + s.w - 14}" y="${s.y + 18}" text-anchor="middle" font-size="11" font-weight="700" fill="${typeColor}" style="pointer-events:none">$</text>`;
}

function renderFunnel(f: FunnelLayout): string {
	const cx = f.x + f.w / 2;
	const tl = f.x, tr = f.x + f.w;
	const bl = cx - f.bw / 2, br = cx + f.bw / 2;

	// Vessel outline
	const vesselPath = `M ${tl} ${f.y} L ${bl} ${f.y + f.h} L ${br} ${f.y + f.h} L ${tr} ${f.y} Z`;
	const clipId = `vc-${f.id}`;

	// Fill bar (solid, no water animation)
	const fillTop = f.y + (1 - f.fillLevel) * f.h;
	const fillEdges = vesselEdgesAtY(f.x, f.w, f.bw, (fillTop - f.y) / f.h);
	const fillPath = f.fillLevel > 0.01
		? `M ${fillEdges.left} ${fillTop} L ${bl} ${f.y + f.h} L ${br} ${f.y + f.h} L ${fillEdges.right} ${fillTop} Z`
		: "";

	// Overflow threshold line
	const ovFrac = 1 - f.overflowLevel;
	const ovY = f.y + ovFrac * f.h;
	const ovEdges = vesselEdgesAtY(f.x, f.w, f.bw, ovFrac);

	const isOverflowing = f.sufficiency === "overflowing";
	const isApproaching = f.sufficiency === "approaching";
	const fillColor = isOverflowing ? COLORS.overflow : isApproaching ? "#f59e0b" : COLORS.inflow;

	// Value and drain labels
	const val = f.data.currentValue;
	const drain = f.data.drainRate;

	const approachingGlow = isApproaching ? `<path d="${vesselPath}" fill="none" stroke="#f59e0b" stroke-width="3" opacity="0.3" class="approaching-glow"/>` : "";

	return `
		<defs><clipPath id="${clipId}"><path d="${vesselPath}"/></clipPath></defs>
		${approachingGlow}
		<path d="${vesselPath}" fill="${COLORS.surface}" stroke="${COLORS.surfaceRaised}" stroke-width="1.5" opacity="0.9"/>
		${fillPath ? `<g clip-path="url(#${clipId})"><path d="${fillPath}" fill="${fillColor}" opacity="0.45"/></g>` : ""}
		<line x1="${ovEdges.left + 4}" y1="${ovY}" x2="${ovEdges.right - 4}" y2="${ovY}" stroke="${COLORS.overflow}" stroke-width="1.5" stroke-dasharray="5 4" opacity="0.6"/>
		<text x="${cx}" y="${f.y - 12}" text-anchor="middle" fill="${COLORS.text}" font-size="13" font-weight="600">${esc(f.label)}</text>
		<text x="${cx}" y="${f.y - 1}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">${fmtDollars(val)}</text>
		<rect x="${f.x + 10}" y="${f.y + f.h + 6}" width="${f.w - 20}" height="3" rx="1.5" fill="${COLORS.surfaceRaised}"/>
		<rect x="${f.x + 10}" y="${f.y + f.h + 6}" width="${(f.w - 20) * Math.min(1, f.fillLevel)}" height="3" rx="1.5" fill="${fillColor}"/>`;
}

function renderOutcome(o: OutcomeLayout): string {
	const cx = o.x + o.w / 2;
	const color = o.data.status === "completed" ? "#10b981" : o.data.status === "blocked" ? "#ef4444" : "#3b82f6";
	const barW = o.w - 16;
	const filledW = barW * Math.min(1, o.fillPercent / 100);

	return `
		<rect x="${o.x}" y="${o.y}" width="${o.w}" height="${o.h}" rx="10" fill="${COLORS.surface}" stroke="${COLORS.surfaceRaised}" stroke-width="1"/>
		<text x="${cx}" y="${o.y + 22}" text-anchor="middle" fill="${COLORS.text}" font-size="10" font-weight="600">${esc(o.label)}</text>
		<rect x="${o.x + 8}" y="${o.y + 32}" width="${barW}" height="6" rx="3" fill="${COLORS.surfaceRaised}"/>
		<rect x="${o.x + 8}" y="${o.y + 32}" width="${filledW}" height="6" rx="3" fill="${color}" opacity="0.7"/>
		<text x="${cx}" y="${o.y + 54}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="9">${fmtDollars(o.data.fundingReceived)} / ${fmtDollars(o.data.fundingTarget)}</text>`;
}

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

	return `
		<g transform="translate(${x}, ${y})">
			<circle cx="24" cy="24" r="22" fill="${COLORS.surface}" stroke="${COLORS.surfaceRaised}" stroke-width="1.5"/>
			<circle cx="24" cy="24" r="18" fill="none" stroke="${COLORS.surfaceRaised}" stroke-width="3"/>
			<circle cx="24" cy="24" r="18" fill="none" stroke="${color}" stroke-width="3" stroke-dasharray="${circ}" stroke-dashoffset="${circ * (1 - score)}" 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">HEALTH</text>
		</g>`;
}

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

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

	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 scheduleRender() {
		if (this.renderScheduled) return;
		this.renderScheduled = true;
		requestAnimationFrame(() => { this.renderScheduled = false; this.render(); });
	}

	private startSimulation() {
		if (this.simTimer) return;
		// Snapshot initial state for loop reset
		if (!this.initialNodes) {
			this.initialNodes = this.nodes.map(n => ({ ...n, data: { ...n.data } }));
		}
		this.steadyCount = 0;
		this.simTimer = setInterval(() => {
			const prev = this.nodes;
			this.nodes = simulateTick(this.nodes, DEFAULT_CONFIG);

			// Detect steady state: check if values stopped changing
			let changed = false;
			for (let i = 0; i < this.nodes.length; i++) {
				const a = prev[i].data as Record<string, unknown>;
				const b = this.nodes[i].data as Record<string, unknown>;
				if (a.currentValue !== b.currentValue || a.fundingReceived !== b.fundingReceived) {
					changed = true;
					break;
				}
			}
			if (!changed) this.steadyCount++;
			else this.steadyCount = 0;

			this.render();

			// If looping and steady for 3 ticks, reset after a brief pause
			if (this.looping && this.steadyCount >= 3 && this.initialNodes) {
				this.stopSimulation();
				setTimeout(() => {
					if (!this.looping) return;
					this.nodes = this.initialNodes!.map(n => ({ ...n, data: { ...n.data } }));
					this.render();
					setTimeout(() => {
						if (!this.looping) return;
						this.simulating = true;
						this.startSimulation();
						this.render();
					}, 500);
				}, 1500);
			}
		}, 500);
	}

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

	private showAmountPopover(sourceId: string, anchorX: number, anchorY: number) {
		this.closePopover();
		const sourceNode = this.nodes.find((n) => n.id === sourceId);
		if (!sourceNode) return;
		const data = sourceNode.data as SourceNodeData;

		const popover = document.createElement("div");
		popover.className = "amount-popover";
		popover.style.left = `${anchorX}px`;
		popover.style.top = `${anchorY}px`;
		popover.innerHTML = `
			<label style="font-size:11px;color:var(--rs-text-secondary)">Flow Rate ($/mo)</label>
			<input type="number" class="pop-rate" value="${data.flowRate}" min="0" step="100"/>
			<label style="font-size:11px;color:var(--rs-text-secondary);margin-top:4px">Effective Date</label>
			<input type="date" class="pop-date" value="${data.effectiveDate || ""}"/>
			<div style="display:flex;gap:6px;margin-top:6px">
				<button class="pop-apply">Apply</button>
				<button class="pop-cancel">Cancel</button>
			</div>`;

		const container = this.shadow.querySelector(".container") as HTMLElement;
		container.appendChild(popover);
		this.activePopover = popover;

		const rateInput = popover.querySelector(".pop-rate") as HTMLInputElement;
		const dateInput = popover.querySelector(".pop-date") as HTMLInputElement;
		rateInput.focus();
		rateInput.select();

		const apply = () => {
			const newRate = parseFloat(rateInput.value) || 0;
			const newDate = dateInput.value || undefined;
			this.updateSourceFlowRate(sourceId, Math.max(0, newRate), newDate);
			this.closePopover();
		};

		popover.querySelector(".pop-apply")!.addEventListener("click", apply);
		popover.querySelector(".pop-cancel")!.addEventListener("click", () => this.closePopover());
		rateInput.addEventListener("keydown", (e) => { if (e.key === "Enter") apply(); if (e.key === "Escape") this.closePopover(); });
		dateInput.addEventListener("keydown", (e) => { if (e.key === "Enter") apply(); if (e.key === "Escape") this.closePopover(); });
	}

	private closePopover() {
		if (this.activePopover) { this.activePopover.remove(); this.activePopover = null; }
	}

	private updateSourceFlowRate(sourceId: string, flowRate: number, effectiveDate?: string) {
		this.nodes = this.nodes.map((n) => {
			if (n.id === sourceId && n.type === "source") {
				return { ...n, data: { ...n.data, flowRate, effectiveDate } as SourceNodeData };
			}
			return n;
		});
		this.dispatchEvent(new CustomEvent("flow-rate-change", { detail: { sourceId, flowRate, effectiveDate }, bubbles: true }));
		this.scheduleRender();
	}

	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 var(--rs-bg-surface-raised); max-height: 90vh; 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 var(--rs-bg-surface-raised); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 12px; }
			.controls button:hover { border-color: var(--rs-primary-hover); color: var(--rs-text-primary); }
			.controls button.active { background: var(--rs-primary); border-color: var(--rs-primary-hover); color: #fff; }
			.legend { position: absolute; bottom: 12px; left: 12px; background: var(--rs-glass-bg); border: 1px solid var(--rs-bg-surface-raised); border-radius: 8px; padding: 8px 12px; font-size: 10px; color: var(--rs-text-secondary); }
			.legend-item { display: flex; align-items: center; gap: 6px; margin: 2px 0; }
			.legend-dot { width: 8px; height: 8px; border-radius: 2px; }
			.amount-popover { position: absolute; background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-bg-surface-raised, #334155); border-radius: 8px; padding: 10px; display: flex; flex-direction: column; gap: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.4); z-index: 100; min-width: 160px; }
			.amount-popover input { background: var(--rs-bg-page, #0f172a); border: 1px solid var(--rs-bg-surface-raised, #334155); border-radius: 4px; padding: 4px 8px; color: var(--rs-text-primary, #f1f5f9); font-size: 13px; outline: none; }
			.amount-popover input:focus { border-color: var(--rs-primary, #3b82f6); }
			.amount-popover button { padding: 4px 10px; border-radius: 4px; border: 1px solid var(--rs-bg-surface-raised, #334155); background: var(--rs-bg-surface, #1e293b); color: var(--rs-text-secondary, #94a3b8); cursor: pointer; font-size: 11px; flex: 1; }
			.amount-popover button.pop-apply { background: var(--rs-primary, #3b82f6); color: #fff; border-color: var(--rs-primary, #3b82f6); }
			.amount-popover button:hover { opacity: 0.85; }
			.flow-dash { animation: dashFlow 1s linear infinite; }
			@keyframes dashFlow { to { stroke-dashoffset: -14; } }
			.approaching-glow { animation: approachingPulse 1.5s ease-in-out infinite; }
			@keyframes approachingPulse { 0%,100% { opacity:0.15 } 50% { opacity:0.45 } }
		</style>
		<div class="container">
			<svg viewBox="0 0 ${layout.width} ${layout.height}" width="${layout.width}" height="${layout.height}">
				${layout.bands.map(renderBand).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>
				<button class="${this.looping ? "active" : ""}" data-action="toggle-loop" title="Loop simulation">&#x1f501;</button>
			</div>
			<div class="legend">
				<div class="legend-item"><div class="legend-dot" style="background:${COLORS.inflow}"></div> Inflow</div>
				<div class="legend-item"><div class="legend-dot" style="background:${COLORS.spending}"></div> Spending</div>
				<div class="legend-item"><div class="legend-dot" style="background:${COLORS.overflow}"></div> Overflow</div>
			</div>
		</div>`;

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

		// Event: toggle loop
		this.shadow.querySelector("[data-action=toggle-loop]")?.addEventListener("click", () => {
			this.looping = !this.looping;
			if (this.looping && !this.simulating) {
				// Auto-start simulation when enabling loop
				this.initialNodes = null; // fresh snapshot
				this.simulating = true;
				this.startSimulation();
			}
			if (!this.looping) this.initialNodes = null;
			this.render();
		});

		// Event delegation for interactive elements + drag-to-pan
		const container = this.shadow.querySelector(".container") as HTMLElement;
		if (!container) return;

		container.addEventListener("pointerdown", (e: PointerEvent) => {
			const target = e.target as Element;
			if (target.closest("button")) return;
			if (target.closest(".amount-popover")) return;

			const interactive = target.closest("[data-interactive]") as Element | null;
			if (interactive) {
				const action = interactive.getAttribute("data-interactive");
				const sourceId = interactive.getAttribute("data-source-id");

				if (action === "edit-rate" && sourceId) {
					const rect = container.getBoundingClientRect();
					const popX = e.clientX - rect.left + container.scrollLeft + 10;
					const popY = e.clientY - rect.top + container.scrollTop + 10;
					this.showAmountPopover(sourceId, popX, popY);
					e.preventDefault();
					return;
				}
			}

			this.closePopover();

			// Start pan drag
			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-flow-river", FolkFlowRiver);
