/**
 * P2P Exchange Matching Engine — bipartite intent matching.
 *
 * Matches buy intents (want crypto, have fiat) with sell intents (have crypto, want fiat).
 *
 * Edge criteria (buy B ↔ sell S):
 * 1. Same tokenId + fiatCurrency
 * 2. Rate overlap (for market_plus_bps, evaluate against cached CoinGecko rate)
 * 3. Amount overlap: min(B.max, S.max) >= max(B.min, S.min)
 * 4. Reputation VPs satisfied both ways
 *
 * Scoring: 0.4×rateMutualness + 0.3×amountBalance + 0.2×avgReputation + 0.1×lpPriority
 */

import type {
	ExchangeIntent, ExchangeIntentsDoc,
	ExchangeReputationDoc, ExchangeTrade,
	TokenId, FiatCurrency,
} from './schemas';
import { getReputation } from './exchange-reputation';
import { getExchangeRate } from './exchange-rates';

// ── Config ──

const TOP_K = 20;

const W_RATE = 0.4;
const W_AMOUNT = 0.3;
const W_REPUTATION = 0.2;
const W_LP = 0.1;

// ── Types ──

interface Match {
	buyIntent: ExchangeIntent;
	sellIntent: ExchangeIntent;
	agreedAmount: number;    // token base units
	agreedRate: number;      // fiat per token
	fiatAmount: number;
	paymentMethod: string;
	score: number;
}

// ── Rate resolution ──

/** Resolve the effective rate (fiat per token) for an intent, given the market rate. */
function resolveRate(intent: ExchangeIntent, marketRate: number): number {
	if (intent.rateType === 'fixed' && intent.rateFixed != null) {
		return intent.rateFixed;
	}
	if (intent.rateType === 'market_plus_bps' && intent.rateMarketBps != null) {
		const bps = intent.rateMarketBps;
		const direction = intent.side === 'sell' ? 1 : -1; // seller adds spread, buyer subtracts
		return marketRate * (1 + direction * bps / 10000);
	}
	return marketRate;
}

// ── Solver ──

/**
 * Run the matching engine on active intents.
 * Returns proposed matches sorted by score.
 */
export async function solveExchange(
	intentsDoc: ExchangeIntentsDoc,
	reputationDoc: ExchangeReputationDoc,
): Promise<Omit<ExchangeTrade, 'id' | 'createdAt' | 'chatMessages'>[]> {
	const intents = Object.values(intentsDoc.intents).filter(i => i.status === 'active');
	const buys = intents.filter(i => i.side === 'buy');
	const sells = intents.filter(i => i.side === 'sell');

	if (buys.length === 0 || sells.length === 0) return [];

	// Group by tokenId+fiatCurrency for efficiency
	const pairGroups = new Map<string, { buys: ExchangeIntent[]; sells: ExchangeIntent[] }>();
	for (const b of buys) {
		const key = `${b.tokenId}:${b.fiatCurrency}`;
		if (!pairGroups.has(key)) pairGroups.set(key, { buys: [], sells: [] });
		pairGroups.get(key)!.buys.push(b);
	}
	for (const s of sells) {
		const key = `${s.tokenId}:${s.fiatCurrency}`;
		if (!pairGroups.has(key)) pairGroups.set(key, { buys: [], sells: [] });
		pairGroups.get(key)!.sells.push(s);
	}

	const allMatches: Match[] = [];

	for (const [pairKey, group] of pairGroups) {
		if (group.buys.length === 0 || group.sells.length === 0) continue;

		const [tokenId, fiatCurrency] = pairKey.split(':') as [TokenId, FiatCurrency];
		const marketRate = await getExchangeRate(tokenId, fiatCurrency);

		for (const buy of group.buys) {
			for (const sell of group.sells) {
				// Don't match same user
				if (buy.creatorDid === sell.creatorDid) continue;

				const match = evaluateMatch(buy, sell, marketRate, reputationDoc);
				if (match) allMatches.push(match);
			}
		}
	}

	// Sort by score descending, take top K
	allMatches.sort((a, b) => b.score - a.score);

	// Deduplicate: each intent can appear in at most one match (greedy)
	const usedIntents = new Set<string>();
	const results: Omit<ExchangeTrade, 'id' | 'createdAt' | 'chatMessages'>[] = [];

	for (const match of allMatches) {
		if (results.length >= TOP_K) break;
		if (usedIntents.has(match.buyIntent.id) || usedIntents.has(match.sellIntent.id)) continue;

		usedIntents.add(match.buyIntent.id);
		usedIntents.add(match.sellIntent.id);

		results.push({
			buyIntentId: match.buyIntent.id,
			sellIntentId: match.sellIntent.id,
			buyerDid: match.buyIntent.creatorDid,
			buyerName: match.buyIntent.creatorName,
			sellerDid: match.sellIntent.creatorDid,
			sellerName: match.sellIntent.creatorName,
			tokenId: match.buyIntent.tokenId,
			tokenAmount: match.agreedAmount,
			fiatCurrency: match.buyIntent.fiatCurrency,
			fiatAmount: match.fiatAmount,
			agreedRate: match.agreedRate,
			paymentMethod: match.paymentMethod,
			status: 'proposed',
			acceptances: {
				[match.buyIntent.creatorDid]: match.buyIntent.autoAccept,
				[match.sellIntent.creatorDid]: match.sellIntent.autoAccept,
			},
		});
	}

	return results;
}

/**
 * Evaluate a single buy/sell pair. Returns a Match if compatible, null otherwise.
 */
function evaluateMatch(
	buy: ExchangeIntent,
	sell: ExchangeIntent,
	marketRate: number,
	reputationDoc: ExchangeReputationDoc,
): Match | null {
	// 1. Rate overlap
	const buyRate = resolveRate(buy, marketRate);   // max rate buyer will pay
	const sellRate = resolveRate(sell, marketRate);  // min rate seller will accept

	// Buyer's rate is the ceiling, seller's rate is the floor
	// For a match, buyer must be willing to pay >= seller's ask
	if (buyRate < sellRate) return null;

	const agreedRate = (buyRate + sellRate) / 2; // midpoint

	// 2. Amount overlap
	const overlapMin = Math.max(buy.tokenAmountMin, sell.tokenAmountMin);
	const overlapMax = Math.min(buy.tokenAmountMax, sell.tokenAmountMax);
	if (overlapMax < overlapMin) return null;

	const agreedAmount = overlapMax; // fill as much as possible

	// 3. Reputation VPs
	const buyerRep = getReputation(buy.creatorDid, reputationDoc);
	const sellerRep = getReputation(sell.creatorDid, reputationDoc);

	if (sell.minCounterpartyReputation != null && buyerRep.score < sell.minCounterpartyReputation) return null;
	if (buy.minCounterpartyReputation != null && sellerRep.score < buy.minCounterpartyReputation) return null;

	// Preferred counterparties (if set, counterparty must be in list)
	if (buy.preferredCounterparties?.length && !buy.preferredCounterparties.includes(sell.creatorDid)) return null;
	if (sell.preferredCounterparties?.length && !sell.preferredCounterparties.includes(buy.creatorDid)) return null;

	// 4. Payment method overlap
	const commonMethods = buy.paymentMethods.filter(m => sell.paymentMethods.includes(m));
	if (commonMethods.length === 0) return null;

	// 5. Scoring
	// Rate mutualness: how much slack between buyer's max and seller's min (0 = barely, 1 = generous)
	const rateSlack = marketRate > 0
		? Math.min(1, (buyRate - sellRate) / (marketRate * 0.05)) // normalize to 5% of market
		: 0.5;

	// Amount balance: how well the agreed amount fills both sides
	const buyFill = agreedAmount / buy.tokenAmountMax;
	const sellFill = agreedAmount / sell.tokenAmountMax;
	const amountBalance = (buyFill + sellFill) / 2;

	// Reputation average (0-1)
	const avgRep = ((buyerRep.score + sellerRep.score) / 2) / 100;

	// LP priority: standing orders get a boost
	const lpBoost = (buy.isStandingOrder || sell.isStandingOrder) ? 1.0 : 0.0;

	const score = Number((
		W_RATE * rateSlack +
		W_AMOUNT * amountBalance +
		W_REPUTATION * avgRep +
		W_LP * lpBoost
	).toFixed(4));

	const fiatAmount = (agreedAmount / 1_000_000) * agreedRate;

	return {
		buyIntent: buy,
		sellIntent: sell,
		agreedAmount,
		agreedRate,
		fiatAmount,
		paymentMethod: commonMethods[0],
		score,
	};
}
