/**
 * rCred Grain Engine — token setup + distribution.
 *
 * Creates a GRAIN token per space and distributes it proportional
 * to CredRank scores using an 80/20 slow/fast split.
 *
 * Orchestrates the full recompute pipeline: collect → rank → store → distribute.
 */

import * as Automerge from '@automerge/automerge';
import type { SyncServer } from '../../server/local-first/sync-server';
import { mintTokens, getTokenDoc, getBalance, getAllBalances } from '../../server/token-service';
import { tokenDocId, tokenLedgerSchema } from '../../server/token-schemas';
import type { TokenLedgerDoc } from '../../server/token-schemas';
import { collectContribGraph } from './graph-collector';
import { computeCredRank } from './credrank';
import type {
	CredConfigDoc, CredScoresDoc, ContribGraphDoc,
	CredScore, CredNode,
} from './schemas';
import {
	graphDocId, scoresDocId, configDocId,
	graphSchema, scoresSchema, configSchema,
} from './schemas';

/** In-memory lock to prevent concurrent recomputes per space. */
const runningSpaces = new Set<string>();

/**
 * Ensure the grain token doc exists for a space.
 * Returns the token ID (grain-{space}).
 */
export function ensureGrainToken(space: string, syncServer: SyncServer): string {
	const tokenId = `grain-${space}`;
	const docId = tokenDocId(tokenId);
	let doc = syncServer.getDoc<TokenLedgerDoc>(docId);

	if (!doc) {
		doc = Automerge.change(Automerge.init<TokenLedgerDoc>(), 'init grain ledger', (d) => {
			const init = tokenLedgerSchema.init();
			Object.assign(d, init);
		});
		syncServer.setDoc(docId, doc);
	}

	if (!doc.token.name) {
		syncServer.changeDoc<TokenLedgerDoc>(docId, `define GRAIN token for ${space}`, (d) => {
			d.token.id = tokenId;
			d.token.name = `Grain (${space})`;
			d.token.symbol = 'GRAIN';
			d.token.decimals = 0;
			d.token.description = `Contribution recognition token for the ${space} space — earned via CredRank`;
			d.token.icon = '🌾';
			d.token.color = '#d97706';
			d.token.createdAt = Date.now();
			d.token.createdBy = 'rcred';
		});
	}

	return tokenId;
}

/**
 * Distribute grain to contributors based on cred scores.
 * Uses 80/20 slow/fast split per SourceCred grain distribution model.
 */
export function distributeGrain(
	space: string,
	syncServer: SyncServer,
	config: CredConfigDoc,
	scores: CredScoresDoc,
): { distributed: number; recipients: number } {
	const tokenId = config.grainTokenId || `grain-${space}`;
	const tokenDoc = getTokenDoc(tokenId);
	if (!tokenDoc) return { distributed: 0, recipients: 0 };

	const pool = config.grainPerEpoch;
	const slowPool = Math.floor(pool * config.slowFraction);
	const fastPool = pool - slowPool;

	const scoreEntries = Object.values(scores.scores).filter(s => s.cred > 0);
	if (scoreEntries.length === 0) return { distributed: 0, recipients: 0 };

	// ── Fast distribution: proportional to current epoch cred ──
	const totalEpochCred = scoreEntries.reduce((s, e) => s + e.cred, 0);
	const fastPayouts = new Map<string, number>();
	if (totalEpochCred > 0) {
		for (const entry of scoreEntries) {
			const payout = Math.floor(fastPool * (entry.cred / totalEpochCred));
			if (payout > 0) fastPayouts.set(entry.did, payout);
		}
	}

	// ── Slow distribution: lifetime equity catch-up ──
	// Total slow grain ever minted for this epoch calculation
	const existingBalances = getAllBalances(tokenDoc);
	const totalLifetimeCred = scoreEntries.reduce((s, e) => s + (e.rawScore || e.cred), 0);
	// Estimate total slow grain minted so far from existing supplies
	const totalSlowMinted = (tokenDoc.token.totalSupply || 0) * config.slowFraction + slowPool;

	const slowPayouts = new Map<string, number>();
	if (totalLifetimeCred > 0) {
		let totalSlowDue = 0;
		const dues = new Map<string, number>();

		for (const entry of scoreEntries) {
			const lifetimeFraction = (entry.rawScore || entry.cred) / totalLifetimeCred;
			const due = totalSlowMinted * lifetimeFraction;
			const alreadyReceived = entry.grainLifetime;
			const payout = Math.max(0, due - alreadyReceived);
			dues.set(entry.did, payout);
			totalSlowDue += payout;
		}

		// Scale down if total due exceeds pool
		const scale = totalSlowDue > slowPool ? slowPool / totalSlowDue : 1;
		for (const [did, payout] of dues) {
			const scaled = Math.floor(payout * scale);
			if (scaled > 0) slowPayouts.set(did, scaled);
		}
	}

	// ── Mint combined payouts ──
	let distributed = 0;
	let recipients = 0;
	const epochId = scores.epochId;

	const allDids = new Set([...fastPayouts.keys(), ...slowPayouts.keys()]);
	for (const did of allDids) {
		const fast = fastPayouts.get(did) || 0;
		const slow = slowPayouts.get(did) || 0;
		const total = fast + slow;
		if (total <= 0) continue;

		const label = scores.scores[did]?.label || did.slice(0, 16);
		const success = mintTokens(
			tokenId, did, label, total,
			`Grain epoch ${epochId}: ${fast} fast + ${slow} slow`,
			'rcred',
		);

		if (success) {
			distributed += total;
			recipients++;
		}
	}

	return { distributed, recipients };
}

/**
 * Ensure the config doc exists with defaults.
 */
export function ensureConfigDoc(space: string, syncServer: SyncServer): CredConfigDoc {
	const docId = configDocId(space);
	let doc = syncServer.getDoc<CredConfigDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<CredConfigDoc>(), 'init rcred config', (d) => {
			const init = configSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.grainTokenId = `grain-${space}`;
		});
		syncServer.setDoc(docId, doc);
	}
	return doc;
}

/**
 * Full recompute pipeline for a space:
 * 1. Ensure config + grain token
 * 2. Collect contribution graph
 * 3. Run CredRank
 * 4. Post-process into CredScores
 * 5. Store graph + scores docs
 * 6. Distribute grain
 */
export function recomputeSpace(
	space: string,
	syncServer: SyncServer,
): { success: boolean; scores: number; grain: number; error?: string } {
	// Prevent concurrent runs
	if (runningSpaces.has(space)) {
		return { success: false, scores: 0, grain: 0, error: 'Already running' };
	}

	runningSpaces.add(space);
	try {
		// 1. Config + token
		const config = ensureConfigDoc(space, syncServer);
		if (!config.enabled) {
			return { success: false, scores: 0, grain: 0, error: 'CredRank disabled' };
		}
		ensureGrainToken(space, syncServer);

		// 2. Collect graph
		const { nodes, edges } = collectContribGraph(space, syncServer, config);

		if (nodes.length === 0) {
			return { success: true, scores: 0, grain: 0 };
		}

		// 3. Run CredRank
		const rawScores = computeCredRank(nodes, edges, config);

		// 4. Post-process: filter to contributors, normalize, build breakdown
		const contributors = nodes.filter(n => n.type === 'contributor');
		const maxRaw = Math.max(...contributors.map(c => rawScores.get(c.id) || 0), 1e-10);

		const epochId = `epoch-${Date.now()}`;
		const prevScores = syncServer.getDoc<CredScoresDoc>(scoresDocId(space));

		const credScores: Record<string, CredScore> = {};
		let totalCred = 0;

		for (const contributor of contributors) {
			const raw = rawScores.get(contributor.id) || 0;
			const cred = Math.round((raw / maxRaw) * 1000);
			if (cred === 0) continue;

			const did = contributor.did || contributor.id.replace('contributor:', '');

			// Breakdown by source module: sum raw scores of connected contribution nodes
			const breakdown: Record<string, number> = {};
			for (const edge of edges) {
				if (edge.from !== contributor.id) continue;
				const targetNode = nodes.find(n => n.id === edge.to);
				if (targetNode?.sourceModule) {
					breakdown[targetNode.sourceModule] = (breakdown[targetNode.sourceModule] || 0) + (rawScores.get(targetNode.id) || 0);
				}
			}
			// Normalize breakdown to percentages
			const breakdownTotal = Object.values(breakdown).reduce((s, v) => s + v, 0) || 1;
			for (const mod of Object.keys(breakdown)) {
				breakdown[mod] = Math.round((breakdown[mod] / breakdownTotal) * 100);
			}

			// Carry forward lifetime grain from previous epoch
			const prevEntry = prevScores?.scores[did];
			const grainLifetime = prevEntry?.grainLifetime || 0;

			credScores[did] = {
				did,
				label: contributor.label || did.slice(0, 16),
				cred,
				rawScore: raw,
				grainLifetime,
				epochScores: { ...(prevEntry?.epochScores || {}), [epochId]: cred },
				breakdown,
				lastActive: contributor.timestamp || Date.now(),
			};
			totalCred += cred;
		}

		// 5. Store graph doc
		const gDocId = graphDocId(space);
		let gDoc = syncServer.getDoc<ContribGraphDoc>(gDocId);
		if (!gDoc) {
			gDoc = Automerge.change(Automerge.init<ContribGraphDoc>(), 'init rcred graph', (d) => {
				const init = graphSchema.init();
				Object.assign(d, init);
				d.meta.spaceSlug = space;
			});
			syncServer.setDoc(gDocId, gDoc);
		}

		syncServer.changeDoc<ContribGraphDoc>(gDocId, 'Update contribution graph', (d) => {
			// Clear and repopulate
			const nodeKeys = Object.keys(d.nodes);
			for (const k of nodeKeys) delete d.nodes[k];
			const edgeKeys = Object.keys(d.edges);
			for (const k of edgeKeys) delete d.edges[k];

			for (const node of nodes) d.nodes[node.id] = node;
			for (const edge of edges) d.edges[edge.id] = edge;
			d.lastBuiltAt = Date.now();
			d.stats = {
				nodeCount: nodes.length,
				edgeCount: edges.length,
				contributorCount: contributors.length,
				contributionCount: nodes.length - contributors.length,
			};
		});

		// 6. Store scores doc
		const sDocId = scoresDocId(space);
		let sDoc = syncServer.getDoc<CredScoresDoc>(sDocId);
		if (!sDoc) {
			sDoc = Automerge.change(Automerge.init<CredScoresDoc>(), 'init rcred scores', (d) => {
				const init = scoresSchema.init();
				Object.assign(d, init);
				d.meta.spaceSlug = space;
			});
			syncServer.setDoc(sDocId, sDoc);
		}

		syncServer.changeDoc<CredScoresDoc>(sDocId, `CredRank epoch ${epochId}`, (d) => {
			const scoreKeys = Object.keys(d.scores);
			for (const k of scoreKeys) delete d.scores[k];
			for (const [did, score] of Object.entries(credScores)) {
				d.scores[did] = score;
			}
			d.totalCred = totalCred;
			d.computedAt = Date.now();
			d.epochId = epochId;
		});

		// 7. Distribute grain
		const updatedScores = syncServer.getDoc<CredScoresDoc>(sDocId)!;
		const grainResult = distributeGrain(space, syncServer, config, updatedScores);

		// Update grain lifetime in scores
		if (grainResult.distributed > 0) {
			const tokenId = config.grainTokenId || `grain-${space}`;
			const tokenDoc = getTokenDoc(tokenId);
			if (tokenDoc) {
				syncServer.changeDoc<CredScoresDoc>(sDocId, 'Update grain lifetime', (d) => {
					for (const did of Object.keys(d.scores)) {
						d.scores[did].grainLifetime = getBalance(tokenDoc!, did);
					}
				});
			}
		}

		// Update config last epoch
		syncServer.changeDoc<CredConfigDoc>(configDocId(space), 'Update last epoch', (d) => {
			d.lastEpochAt = Date.now();
		});

		console.log(`[rCred] Recomputed ${space}: ${Object.keys(credScores).length} contributors, ${totalCred} total cred, ${grainResult.distributed} grain to ${grainResult.recipients} recipients`);

		return {
			success: true,
			scores: Object.keys(credScores).length,
			grain: grainResult.distributed,
		};
	} catch (err) {
		console.error(`[rCred] Recompute failed for ${space}:`, err);
		return { success: false, scores: 0, grain: 0, error: String(err) };
	} finally {
		runningSpaces.delete(space);
	}
}
