/**
 * Flow simulation engine — pure function, no framework dependencies.
 * Ported from rfunds-online/lib/simulation.ts.
 */

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

export interface SimulationConfig {
	tickDivisor: number;
	spendingRateHealthy: number;
	spendingRateOverflow: number;
	spendingRateCritical: number;
}

export const DEFAULT_CONFIG: SimulationConfig = {
	tickDivisor: 10,
	spendingRateHealthy: 0.5,
	spendingRateOverflow: 0.8,
	spendingRateCritical: 0.1,
};

export function computeSufficiencyState(data: FunnelNodeData): SufficiencyState {
	const threshold = data.sufficientThreshold ?? data.maxThreshold;
	if (data.currentValue >= data.maxCapacity) return "abundant";
	if (data.currentValue >= threshold) return "sufficient";
	return "seeking";
}

export function computeNeedWeights(
	targetIds: string[],
	allNodes: FlowNode[],
): Map<string, number> {
	const nodeMap = new Map(allNodes.map((n) => [n.id, n]));
	const needs = new Map<string, number>();

	for (const tid of targetIds) {
		const node = nodeMap.get(tid);
		if (!node) { needs.set(tid, 0); continue; }

		if (node.type === "funnel") {
			const d = node.data as FunnelNodeData;
			const threshold = d.sufficientThreshold ?? d.maxThreshold;
			const need = Math.max(0, 1 - d.currentValue / (threshold || 1));
			needs.set(tid, need);
		} else if (node.type === "outcome") {
			const d = node.data as OutcomeNodeData;
			const need = Math.max(0, 1 - d.fundingReceived / Math.max(d.fundingTarget, 1));
			needs.set(tid, need);
		} else {
			needs.set(tid, 0);
		}
	}

	const totalNeed = Array.from(needs.values()).reduce((s, n) => s + n, 0);
	const weights = new Map<string, number>();
	if (totalNeed === 0) {
		const equal = targetIds.length > 0 ? 100 / targetIds.length : 0;
		targetIds.forEach((id) => weights.set(id, equal));
	} else {
		needs.forEach((need, id) => {
			weights.set(id, (need / totalNeed) * 100);
		});
	}
	return weights;
}

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;
			const threshold = d.sufficientThreshold ?? d.maxThreshold;
			sum += Math.min(1, d.currentValue / (threshold || 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;
}

export function simulateTick(
	nodes: FlowNode[],
	config: SimulationConfig = DEFAULT_CONFIG,
): FlowNode[] {
	const { tickDivisor, spendingRateHealthy, spendingRateOverflow, spendingRateCritical } = config;

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

	const overflowIncoming = new Map<string, number>();
	const spendingIncoming = new Map<string, number>();
	const updatedFunnels = new Map<string, FunnelNodeData>();

	for (const node of funnelNodes) {
		const src = node.data as FunnelNodeData;
		const data: FunnelNodeData = { ...src };

		let value = data.currentValue + data.inflowRate / tickDivisor;
		value += overflowIncoming.get(node.id) ?? 0;
		value = Math.min(value, data.maxCapacity);

		if (value > data.maxThreshold && data.overflowAllocations.length > 0) {
			const excess = value - data.maxThreshold;

			if (data.dynamicOverflow) {
				const targetIds = data.overflowAllocations.map((a) => a.targetId);
				const needWeights = computeNeedWeights(targetIds, nodes);
				for (const alloc of data.overflowAllocations) {
					const weight = needWeights.get(alloc.targetId) ?? 0;
					const share = excess * (weight / 100);
					overflowIncoming.set(alloc.targetId, (overflowIncoming.get(alloc.targetId) ?? 0) + share);
				}
			} else {
				for (const alloc of data.overflowAllocations) {
					const share = excess * (alloc.percentage / 100);
					overflowIncoming.set(alloc.targetId, (overflowIncoming.get(alloc.targetId) ?? 0) + share);
				}
			}
			value = data.maxThreshold;
		}

		if (value > 0 && data.spendingAllocations.length > 0) {
			let rateMultiplier: number;
			if (value > data.maxThreshold) {
				rateMultiplier = spendingRateOverflow;
			} else if (value >= data.minThreshold) {
				rateMultiplier = spendingRateHealthy;
			} else {
				rateMultiplier = spendingRateCritical;
			}

			let drain = (data.inflowRate / tickDivisor) * rateMultiplier;
			drain = Math.min(drain, value);
			value -= drain;

			for (const alloc of data.spendingAllocations) {
				const share = drain * (alloc.percentage / 100);
				spendingIncoming.set(alloc.targetId, (spendingIncoming.get(alloc.targetId) ?? 0) + share);
			}
		}

		data.currentValue = Math.max(0, value);
		updatedFunnels.set(node.id, data);
	}

	// Process outcomes in Y-order (like funnels) 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;
	});
}
