/**
 * Mycelium Clustering Solver — finds optimal collaboration matches
 * from the intent pool using bipartite graph matching.
 *
 * Algorithm:
 * 1. Build bipartite graph: offer intents ↔ need intents (edges where skill matches)
 * 2. Filter by validity predicates (min reputation, max price, preferred members)
 * 3. Greedy cluster formation starting from highest-demand skills
 * 4. Score clusters: skillMatch×0.4 + hoursBalance×0.3 + avgReputation×0.2 + vpSatisfaction×0.1
 * 5. Output top-K results, de-duplicated by member overlap
 */

import type { Skill } from './schemas';
import type {
	Intent, SolverResult,
	IntentsDoc, SkillCurvesDoc, ReputationDoc,
} from './schemas-intent';
import { getMemberSkillReputation, DEFAULT_REPUTATION } from './reputation';
import { getSkillPrice } from './skill-curve';

// ── Config ──

/** Maximum number of results to output */
const TOP_K = 10;

/** Maximum member overlap between results (0-1) */
const MAX_OVERLAP = 0.5;

/** Scoring weights */
const W_SKILL_MATCH = 0.4;
const W_HOURS_BALANCE = 0.3;
const W_REPUTATION = 0.2;
const W_VP_SATISFACTION = 0.1;

// ── Types ──

interface Edge {
	offerId: string;
	needId: string;
	skill: Skill;
	offerHours: number;
	needHours: number;
}

interface Cluster {
	intentIds: string[];
	memberIds: Set<string>;
	skills: Set<Skill>;
	offerHours: number;
	needHours: number;
	edges: Edge[];
}

// ── Solver ──

/**
 * Run the Mycelium Clustering solver on the current intent pool.
 * Returns scored SolverResult candidates (without IDs — caller assigns).
 */
export function solve(
	intentsDoc: IntentsDoc,
	reputationDoc: ReputationDoc,
	skillCurvesDoc: SkillCurvesDoc,
): Omit<SolverResult, 'id' | 'createdAt'>[] {
	const intents = Object.values(intentsDoc.intents)
		.filter(i => i.status === 'active');

	const offers = intents.filter(i => i.type === 'offer');
	const needs = intents.filter(i => i.type === 'need');

	if (offers.length === 0 || needs.length === 0) return [];

	// Step 1: Build bipartite edges
	const edges = buildEdges(offers, needs);

	// Step 2: Filter by validity predicates
	const filtered = filterByVPs(edges, offers, needs, reputationDoc, skillCurvesDoc);

	if (filtered.length === 0) return [];

	// Step 3: Greedy clustering
	const clusters = greedyCluster(filtered, intents);

	// Step 4: Score and rank
	const scored = clusters
		.map(c => scoreCluster(c, intents, reputationDoc))
		.sort((a, b) => b.score - a.score);

	// Step 5: De-duplicate by member overlap, take top-K
	const results = deduplicateResults(scored, TOP_K);

	return results;
}

/**
 * Build bipartite edges between matching offer and need intents.
 */
function buildEdges(offers: Intent[], needs: Intent[]): Edge[] {
	const edges: Edge[] = [];

	for (const offer of offers) {
		for (const need of needs) {
			// Must match on skill
			if (offer.skill !== need.skill) continue;
			// Don't match same member with themselves
			if (offer.memberId === need.memberId) continue;

			edges.push({
				offerId: offer.id,
				needId: need.id,
				skill: offer.skill,
				offerHours: offer.hours,
				needHours: need.hours,
			});
		}
	}

	return edges;
}

/**
 * Filter edges by validity predicates.
 */
function filterByVPs(
	edges: Edge[],
	offers: Intent[],
	needs: Intent[],
	reputationDoc: ReputationDoc,
	skillCurvesDoc: SkillCurvesDoc,
): Edge[] {
	const offerMap = new Map(offers.map(o => [o.id, o]));
	const needMap = new Map(needs.map(n => [n.id, n]));

	return edges.filter(edge => {
		const offer = offerMap.get(edge.offerId)!;
		const need = needMap.get(edge.needId)!;

		// Check need's minReputation against offer's reputation
		if (need.minReputation != null) {
			const offerRep = getMemberSkillReputation(offer.memberId, edge.skill, reputationDoc);
			if (offerRep < need.minReputation) return false;
		}

		// Check offer's minReputation against need's reputation
		if (offer.minReputation != null) {
			const needRep = getMemberSkillReputation(need.memberId, edge.skill, reputationDoc);
			if (needRep < offer.minReputation) return false;
		}

		// Check need's maxPrice against current skill price
		if (need.maxPrice != null) {
			const price = getSkillPrice(edge.skill, skillCurvesDoc);
			if (price > need.maxPrice) return false;
		}

		// Check preferred members (if specified, counterparty must be in list)
		if (need.preferredMembers?.length) {
			if (!need.preferredMembers.includes(offer.memberId)) return false;
		}
		if (offer.preferredMembers?.length) {
			if (!offer.preferredMembers.includes(need.memberId)) return false;
		}

		return true;
	});
}

/**
 * Greedy cluster formation — starting from highest-demand skills.
 */
function greedyCluster(edges: Edge[], allIntents: Intent[]): Cluster[] {
	const intentMap = new Map(allIntents.map(i => [i.id, i]));

	// Count demand per skill to prioritize
	const skillDemand = new Map<Skill, number>();
	for (const edge of edges) {
		skillDemand.set(edge.skill, (skillDemand.get(edge.skill) || 0) + edge.needHours);
	}

	// Sort skills by demand (highest first)
	const skillOrder = Array.from(skillDemand.entries())
		.sort((a, b) => b[1] - a[1])
		.map(([skill]) => skill);

	const clusters: Cluster[] = [];
	const usedIntents = new Set<string>();

	for (const skill of skillOrder) {
		// Get edges for this skill, excluding already-used intents
		const skillEdges = edges.filter(e =>
			e.skill === skill &&
			!usedIntents.has(e.offerId) &&
			!usedIntents.has(e.needId)
		);

		if (skillEdges.length === 0) continue;

		// Group by need — each need tries to find the best offer
		const needIds = [...new Set(skillEdges.map(e => e.needId))];

		for (const needId of needIds) {
			if (usedIntents.has(needId)) continue;

			const need = intentMap.get(needId)!;
			const candidateEdges = skillEdges.filter(e =>
				e.needId === needId && !usedIntents.has(e.offerId)
			);

			if (candidateEdges.length === 0) continue;

			// Greedy: pick the offer with the closest hour match
			const bestEdge = candidateEdges.reduce((best, e) => {
				const bestDiff = Math.abs(best.offerHours - best.needHours);
				const eDiff = Math.abs(e.offerHours - e.needHours);
				return eDiff < bestDiff ? e : best;
			});

			const offer = intentMap.get(bestEdge.offerId)!;

			const cluster: Cluster = {
				intentIds: [bestEdge.offerId, bestEdge.needId],
				memberIds: new Set([offer.memberId, need.memberId]),
				skills: new Set([skill]),
				offerHours: bestEdge.offerHours,
				needHours: bestEdge.needHours,
				edges: [bestEdge],
			};

			usedIntents.add(bestEdge.offerId);
			usedIntents.add(bestEdge.needId);
			clusters.push(cluster);
		}
	}

	// Second pass: try to merge clusters that share members (multi-skill collaborations)
	return mergeClusters(clusters);
}

/**
 * Merge clusters that share members into multi-skill collaborations.
 */
function mergeClusters(clusters: Cluster[]): Cluster[] {
	const merged: Cluster[] = [];
	const consumed = new Set<number>();

	for (let i = 0; i < clusters.length; i++) {
		if (consumed.has(i)) continue;

		const current = { ...clusters[i], memberIds: new Set(clusters[i].memberIds), skills: new Set(clusters[i].skills) };

		for (let j = i + 1; j < clusters.length; j++) {
			if (consumed.has(j)) continue;

			// Check if clusters share any members
			const overlap = [...clusters[j].memberIds].some(m => current.memberIds.has(m));
			if (!overlap) continue;

			// Merge
			for (const id of clusters[j].intentIds) current.intentIds.push(id);
			for (const m of clusters[j].memberIds) current.memberIds.add(m);
			for (const s of clusters[j].skills) current.skills.add(s);
			current.offerHours += clusters[j].offerHours;
			current.needHours += clusters[j].needHours;
			current.edges.push(...clusters[j].edges);
			consumed.add(j);
		}

		merged.push(current);
	}

	return merged;
}

/**
 * Score a cluster and convert to SolverResult shape.
 */
function scoreCluster(
	cluster: Cluster,
	allIntents: Intent[],
	reputationDoc: ReputationDoc,
): Omit<SolverResult, 'id' | 'createdAt'> {
	const intentMap = new Map(allIntents.map(i => [i.id, i]));
	const clusterIntents = cluster.intentIds.map(id => intentMap.get(id)!);

	// 1. Skill match score (1.0 = all intents match on skill)
	const skillMatchScore = 1.0; // edges only exist where skills match

	// 2. Hours balance (1.0 = perfect offer/need balance, 0 = severe imbalance)
	const hoursBalance = cluster.offerHours > 0 && cluster.needHours > 0
		? 1 - Math.abs(cluster.offerHours - cluster.needHours) / Math.max(cluster.offerHours, cluster.needHours)
		: 0;

	// 3. Average reputation of participants
	const memberIds = [...cluster.memberIds];
	const repScores = memberIds.map(memberId => {
		const memberIntents = clusterIntents.filter(i => i.memberId === memberId);
		const skills = [...new Set(memberIntents.map(i => i.skill))];
		if (skills.length === 0) return DEFAULT_REPUTATION;
		const avg = skills.reduce((sum, skill) =>
			sum + getMemberSkillReputation(memberId, skill, reputationDoc), 0) / skills.length;
		return avg;
	});
	const avgReputation = repScores.length > 0
		? repScores.reduce((a, b) => a + b, 0) / repScores.length / 100
		: 0.5;

	// 4. VP satisfaction (what fraction of VPs are satisfied — already pre-filtered, so mostly 1.0)
	const vpSatisfaction = 1.0;

	const score = Number((
		W_SKILL_MATCH * skillMatchScore +
		W_HOURS_BALANCE * hoursBalance +
		W_REPUTATION * avgReputation +
		W_VP_SATISFACTION * vpSatisfaction
	).toFixed(4));

	return {
		intents: cluster.intentIds,
		members: memberIds,
		skills: [...cluster.skills],
		totalHours: cluster.offerHours + cluster.needHours,
		score,
		status: 'proposed',
		acceptances: Object.fromEntries(memberIds.map(m => [m, false])),
	};
}

/**
 * De-duplicate results by member overlap.
 * Two results overlap if they share > MAX_OVERLAP fraction of members.
 */
function deduplicateResults(
	results: Omit<SolverResult, 'id' | 'createdAt'>[],
	limit: number,
): Omit<SolverResult, 'id' | 'createdAt'>[] {
	const kept: Omit<SolverResult, 'id' | 'createdAt'>[] = [];

	for (const result of results) {
		if (kept.length >= limit) break;

		const resultMembers = new Set(result.members);
		const overlapping = kept.some(existing => {
			const existingMembers = new Set(existing.members);
			const overlap = [...resultMembers].filter(m => existingMembers.has(m)).length;
			const minSize = Math.min(resultMembers.size, existingMembers.size);
			return minSize > 0 && overlap / minSize > MAX_OVERLAP;
		});

		if (!overlapping) kept.push(result);
	}

	return kept;
}
