/**
 * Flow simulation engine — pure function, no framework dependencies.
 * Enforces strict conservation: every dollar is accounted for (in = out).
 */

import type { FlowNode, FunnelNodeData, OutcomeNodeData, SourceNodeData, SufficiencyState } from "./types";

export interface SimulationConfig {
	tickDivisor: number;
}

export const DEFAULT_CONFIG: SimulationConfig = {
	tickDivisor: 5,
};

export function computeSufficiencyState(data: FunnelNodeData): SufficiencyState {
	const ratio = data.currentValue / (data.overflowThreshold || 1);
	if (ratio >= 1) return "overflowing";
	if (ratio >= 0.8) return "approaching";
	return "seeking";
}

export function computeSystemSufficiency(nodes: FlowNode[]): number {
	let sum = 0;
	let count = 0;

	for (const node of nodes) {
		if (node.type === "funnel") {
			const d = node.data as FunnelNodeData;
			sum += Math.min(1, d.currentValue / (d.overflowThreshold || 1));
			count++;
		} else if (node.type === "outcome") {
			const d = node.data as OutcomeNodeData;
			sum += Math.min(1, d.fundingReceived / Math.max(d.fundingTarget, 1));
			count++;
		}
	}

	return count > 0 ? sum / count : 0;
}

/**
 * Multi-pass inflow rate computation.
 * Pass 1: source→funnel direct allocations.
 * Pass 2+: propagate spending drain shares + overflow excess shares downstream.
 * Loops until convergence (max 20 iterations, delta < 0.001).
 */
export function computeInflowRates(nodes: FlowNode[]): FlowNode[] {
	// Pass 1: source → funnel direct allocations
	const computed = new Map<string, number>();
	for (const n of nodes) {
		if (n.type === "source") {
			const d = n.data as SourceNodeData;
			for (const alloc of d.targetAllocations) {
				computed.set(
					alloc.targetId,
					(computed.get(alloc.targetId) ?? 0) + d.flowRate * (alloc.percentage / 100),
				);
			}
		}
	}

	// Pass 2+: propagate spending + overflow downstream through funnel layers
	const funnelNodes = nodes.filter((n) => n.type === "funnel").sort((a, b) => a.position.y - b.position.y);
	for (let iter = 0; iter < 20; iter++) {
		let delta = 0;
		for (const fn of funnelNodes) {
			const d = fn.data as FunnelNodeData;
			const inflow = computed.get(fn.id) ?? d.inflowRate;
			if (inflow <= 0) continue;

			// Spending drain goes downstream
			const drain = Math.min(d.drainRate, inflow);
			for (const alloc of d.spendingAllocations) {
				const share = drain * (alloc.percentage / 100);
				// Only propagate to other funnels
				const target = nodes.find((n) => n.id === alloc.targetId);
				if (target?.type === "funnel") {
					const prev = computed.get(alloc.targetId) ?? 0;
					const next = prev + share;
					if (Math.abs(next - prev) > 0.001) {
						computed.set(alloc.targetId, next);
						delta += Math.abs(next - prev);
					}
				}
			}

			// Overflow excess goes downstream (estimate: inflow - drain when above threshold)
			const netExcess = Math.max(0, inflow - drain);
			if (netExcess > 0) {
				for (const alloc of d.overflowAllocations) {
					const share = netExcess * (alloc.percentage / 100);
					const target = nodes.find((n) => n.id === alloc.targetId);
					if (target?.type === "funnel") {
						const prev = computed.get(alloc.targetId) ?? 0;
						const next = prev + share;
						if (Math.abs(next - prev) > 0.001) {
							computed.set(alloc.targetId, next);
							delta += Math.abs(next - prev);
						}
					}
				}
			}
		}
		if (delta < 0.001) break;
	}

	return nodes.map((n) => {
		if (n.type === "funnel" && computed.has(n.id)) {
			return { ...n, data: { ...n.data, inflowRate: computed.get(n.id)! } };
		}
		return n;
	});
}

/**
 * Conservation-enforcing tick with convergence loop for circular overflow.
 *
 * Wraps funnel processing in up to 3 passes per tick so that upward overflow
 * (e.g. Growth→Treasury) doesn't suffer a 1-tick delay.
 */
export function simulateTick(
	nodes: FlowNode[],
	config: SimulationConfig = DEFAULT_CONFIG,
): FlowNode[] {
	const { tickDivisor } = config;

	const funnelNodes = nodes
		.filter((n) => n.type === "funnel")
		.sort((a, b) => a.position.y - b.position.y);

	const funnelIds = new Set(funnelNodes.map((n) => n.id));
	const overflowIncoming = new Map<string, number>();
	const spendingIncoming = new Map<string, number>();
	const updatedFunnels = new Map<string, FunnelNodeData>();

	// Initialize funnel data
	for (const node of funnelNodes) {
		updatedFunnels.set(node.id, { ...(node.data as FunnelNodeData) });
	}

	// Convergence loop: re-process funnels that receive new overflow after being processed
	const processed = new Set<string>();
	for (let pass = 0; pass < 3; pass++) {
		const needsProcessing = pass === 0
			? funnelNodes
			: funnelNodes.filter((n) => {
				// Only re-process if new overflow arrived after we processed it
				const incoming = overflowIncoming.get(n.id) ?? 0;
				return incoming > 0 && processed.has(n.id);
			});

		if (pass > 0 && needsProcessing.length === 0) break;

		for (const node of needsProcessing) {
			const data = pass === 0
				? updatedFunnels.get(node.id)!
				: { ...updatedFunnels.get(node.id)! };

			// 1. Inflow: source rate + overflow received from upstream this tick
			const inflow = (pass === 0 ? data.inflowRate / tickDivisor : 0)
				+ (overflowIncoming.get(node.id) ?? 0);

			if (pass > 0) {
				// Clear consumed overflow
				overflowIncoming.delete(node.id);
			}

			let value = data.currentValue + inflow;

			// 2. Drain: flat rate capped by available funds (only on first pass)
			let drain = 0;
			if (pass === 0) {
				drain = Math.min(data.drainRate / tickDivisor, value);
				value -= drain;
			}

			// 3. Overflow: route excess above threshold to downstream
			if (value > data.overflowThreshold && data.overflowAllocations.length > 0) {
				const excess = value - data.overflowThreshold;
				for (const alloc of data.overflowAllocations) {
					const share = excess * (alloc.percentage / 100);
					overflowIncoming.set(alloc.targetId, (overflowIncoming.get(alloc.targetId) ?? 0) + share);
				}
				value = data.overflowThreshold;
			}

			// 4. Distribute drain to spending targets (funnel or outcome)
			if (drain > 0 && data.spendingAllocations.length > 0) {
				for (const alloc of data.spendingAllocations) {
					const share = drain * (alloc.percentage / 100);
					if (funnelIds.has(alloc.targetId)) {
						// Spending to another funnel: add as overflow incoming for convergence
						overflowIncoming.set(alloc.targetId, (overflowIncoming.get(alloc.targetId) ?? 0) + share);
					} else {
						spendingIncoming.set(alloc.targetId, (spendingIncoming.get(alloc.targetId) ?? 0) + share);
					}
				}
			}

			// 5. Clamp
			data.currentValue = Math.max(0, Math.min(value, data.capacity));
			updatedFunnels.set(node.id, data);
			processed.add(node.id);
		}
	}

	// Process outcomes in Y-order so overflow can cascade
	const outcomeNodes = nodes
		.filter((n) => n.type === "outcome")
		.sort((a, b) => a.position.y - b.position.y);

	const outcomeOverflowIncoming = new Map<string, number>();
	const updatedOutcomes = new Map<string, OutcomeNodeData>();

	for (const node of outcomeNodes) {
		const src = node.data as OutcomeNodeData;
		const data: OutcomeNodeData = { ...src };

		const incoming = (spendingIncoming.get(node.id) ?? 0)
			+ (overflowIncoming.get(node.id) ?? 0)
			+ (outcomeOverflowIncoming.get(node.id) ?? 0);

		if (incoming > 0) {
			let newReceived = data.fundingReceived + incoming;

			// Overflow: if fully funded and has overflow allocations, distribute excess
			const allocs = data.overflowAllocations;
			if (allocs && allocs.length > 0 && data.fundingTarget > 0 && newReceived > data.fundingTarget) {
				const excess = newReceived - data.fundingTarget;
				for (const alloc of allocs) {
					const share = excess * (alloc.percentage / 100);
					outcomeOverflowIncoming.set(alloc.targetId, (outcomeOverflowIncoming.get(alloc.targetId) ?? 0) + share);
				}
				newReceived = data.fundingTarget;
			}

			// Cap at 105% if no overflow allocations
			if (!allocs || allocs.length === 0) {
				newReceived = Math.min(
					data.fundingTarget > 0 ? data.fundingTarget * 1.05 : Infinity,
					newReceived,
				);
			}

			data.fundingReceived = newReceived;

			if (data.fundingTarget > 0 && data.fundingReceived >= data.fundingTarget && data.status !== "blocked") {
				data.status = "completed";
			} else if (data.fundingReceived > 0 && data.status === "not-started") {
				data.status = "in-progress";
			}
		}

		updatedOutcomes.set(node.id, data);
	}

	return nodes.map((node) => {
		if (node.type === "funnel" && updatedFunnels.has(node.id)) {
			return { ...node, data: updatedFunnels.get(node.id)! };
		}

		if (node.type === "outcome" && updatedOutcomes.has(node.id)) {
			return { ...node, data: updatedOutcomes.get(node.id)! };
		}

		return node;
	});
}
