import { type ActorIdentifier, type Did, type ResourceUri } from '@atcute/lexicons';
import type { PostWithUri } from './at/fetch';
import type { BacklinksSource } from './at/constellation';
import { extractDidFromUri, repostSource } from '$lib';
import type { AppBskyGraphFollow } from '@atcute/bluesky';

export type Sort = 'recent' | 'active' | 'conversational';

export const sortFollowedUser = (
	sort: Sort,
	statsA: NonNullable<ReturnType<typeof calculateFollowedUserStats>>,
	statsB: NonNullable<ReturnType<typeof calculateFollowedUserStats>>
) => {
	if (sort === 'conversational') {
		if (Math.abs(statsB.conversationalScore! - statsA.conversationalScore!) > 0.1)
			return statsB.conversationalScore! - statsA.conversationalScore!;
	} else {
		if (sort === 'active')
			if (Math.abs(statsB.activeScore! - statsA.activeScore!) > 0.0001)
				return statsB.activeScore! - statsA.activeScore!;
	}
	return statsB.lastPostAt!.getTime() - statsA.lastPostAt!.getTime();
};

const userStatsCache = new Map<
	Did,
	{ timestamp: number; stats: ReturnType<typeof _calculateStats> }
>();
const STATS_CACHE_TTL = 60 * 1000;

export const calculateFollowedUserStats = (
	sort: Sort,
	did: Did,
	posts: Map<ResourceUri, PostWithUri>,
	postsByDid: Map<Did, Set<ResourceUri>>,
	interactionScores: Map<ActorIdentifier, number> | null,
	now: number
) => {
	if (sort === 'active') {
		const cached = userStatsCache.get(did);
		if (cached && now - cached.timestamp < STATS_CACHE_TTL) {
			const userPostUris = postsByDid.get(did);
			if (userPostUris && userPostUris.size > 0) return { ...cached.stats, did };
		}
	}

	const stats = _calculateStats(sort, did, posts, postsByDid, interactionScores, now);

	if (stats && sort === 'active') userStatsCache.set(did, { timestamp: now, stats });

	return stats;
};

const _calculateStats = (
	sort: Sort,
	did: Did,
	posts: Map<ResourceUri, PostWithUri>,
	postsByDid: Map<Did, Set<ResourceUri>>,
	interactionScores: Map<ActorIdentifier, number> | null,
	now: number
) => {
	const userPostUris = postsByDid.get(did);
	if (!userPostUris || userPostUris.size === 0) return null;

	let lastPostAtTime = 0;
	let activeScore = 0;
	let recentPostCount = 0;
	const quarterPosts = 6 * 60 * 60 * 1000;
	const gravity = 2.0;

	for (const uri of userPostUris) {
		const post = posts.get(uri);
		if (!post) continue;
		const t = new Date(post.record.createdAt).getTime();
		if (t > lastPostAtTime) lastPostAtTime = t;
		const ageMs = Math.max(0, now - t);
		if (ageMs < quarterPosts) recentPostCount++;
		if (sort === 'active') {
			const ageHours = ageMs / (1000 * 60 * 60);
			activeScore += 1 / Math.pow(ageHours + 1, gravity);
		}
	}

	let conversationalScore = 0;
	if (sort === 'conversational' && interactionScores)
		conversationalScore = interactionScores.get(did) || 0;

	return {
		did,
		lastPostAt: new Date(lastPostAtTime),
		activeScore,
		conversationalScore,
		recentPostCount
	};
};

const quoteWeight = 4;
const replyWeight = 6;
const repostWeight = 2;

const oneDay = 24 * 60 * 60 * 1000;
const halfLifeMs = 3 * oneDay;
const decayLambda = 0.693 / halfLifeMs;

const rateBaseline = 1;
const ratePower = 0.5;
const windowSize = 7 * oneDay;

const rateCache = new Map<Did, { rate: number; calculatedAt: number; postCount: number }>();

const getPostRate = (did: Did, posts: Map<ResourceUri, PostWithUri>, now: number): number => {
	const cached = rateCache.get(did);
	if (cached && cached.postCount === posts.size && now - cached.calculatedAt < 5 * 60 * 1000)
		return cached.rate;

	let volume = 0;
	let minTime = now;
	let maxTime = 0;
	let hasRecentPosts = false;

	for (const [, post] of posts) {
		const t = new Date(post.record.createdAt).getTime();
		if (now - t < windowSize) {
			volume += 1;
			if (t < minTime) minTime = t;
			if (t > maxTime) maxTime = t;
			hasRecentPosts = true;
		}
	}

	let rate = 0;
	if (hasRecentPosts) {
		const days = Math.max((maxTime - minTime) / oneDay, 1);
		rate = volume / days;
	}

	rateCache.set(did, { rate, calculatedAt: now, postCount: posts.size });
	return rate;
};

export const calculateInteractionScores = (
	user: Did,
	allPosts: Map<ResourceUri, PostWithUri>,
	postsByDid: Map<Did, Set<ResourceUri>>,
	allBacklinks: Map<BacklinksSource, Map<ResourceUri, Map<Did, Set<string>>>>,
	replyIndex: Map<Did, Set<ResourceUri>>,
	now: number
) => {
	const scores = new Map<Did, number>();

	const decay = (time: number) => {
		const age = Math.max(0, now - time);
		return Math.exp(-decayLambda * age);
	};

	const addScore = (did: Did, weight: number, time: number) => {
		const current = scores.get(did) ?? 0;
		scores.set(did, current + weight * decay(time));
	};

	// 1. process my posts (me -> others)
	const myPostUris = postsByDid.get(user);
	if (myPostUris) {
		const seenRoots = new Set<ResourceUri>();
		for (const uri of myPostUris) {
			const post = allPosts.get(uri);
			if (!post) continue;
			const t = new Date(post.record.createdAt).getTime();

			if (post.record.reply) {
				const parentUri = post.record.reply.parent.uri;
				const rootUri = post.record.reply.root.uri;

				const targetDid = extractDidFromUri(parentUri);
				if (targetDid && targetDid !== user) addScore(targetDid, replyWeight, t);

				if (parentUri !== rootUri && !seenRoots.has(rootUri)) {
					const rootDid = extractDidFromUri(rootUri);
					if (rootDid && rootDid !== user) addScore(rootDid, replyWeight, t);
					seenRoots.add(rootUri);
				}
			}

			if (post.record.embed?.$type === 'app.bsky.embed.record') {
				const targetDid = extractDidFromUri(post.record.embed.record.uri);
				if (targetDid && targetDid !== user) addScore(targetDid, quoteWeight, t);
			}
		}
	}

	// 2. process others -> me (using reply index)
	const repliesToMe = replyIndex.get(user);
	if (repliesToMe) {
		for (const uri of repliesToMe) {
			const authorDid = extractDidFromUri(uri);
			if (!authorDid || authorDid === user) continue;

			const post = allPosts.get(uri);
			if (!post) continue;

			const t = new Date(post.record.createdAt).getTime();
			addScore(authorDid, replyWeight, t);
		}
	}

	// 3. process reposts on my posts
	const repostBacklinks = allBacklinks.get(repostSource);
	if (repostBacklinks && myPostUris) {
		for (const uri of myPostUris) {
			const myPost = allPosts.get(uri);
			if (!myPost) continue;
			const didMap = repostBacklinks.get(uri);
			if (!didMap) continue;

			const t = new Date(myPost.record.createdAt).getTime();
			const adds = new Map<Did, { score: number; repostCount: number }>();

			for (const [did, rkeys] of didMap) {
				if (did === user) continue;

				let add = adds.get(did) ?? { score: 0, repostCount: 0 };
				const diminishFactor = 9;

				// each rkey is a separate repost record, apply diminishing returns
				for (let i = 0; i < rkeys.size; i++) {
					const weight = repostWeight * (diminishFactor / (add.repostCount + diminishFactor));
					add = {
						score: add.score + weight,
						repostCount: add.repostCount + 1
					};
				}
				adds.set(did, add);
			}

			for (const [did, add] of adds.entries()) addScore(did, add.score, t);
		}
	}

	// normalize by posting rate
	for (const [did, score] of scores) {
		const userPostUris = postsByDid.get(did);
		let rate = 0;
		if (userPostUris) {
			const userPosts = new Map<ResourceUri, PostWithUri>();
			for (const uri of userPostUris) {
				const post = allPosts.get(uri);
				if (post) userPosts.set(uri, post);
			}
			rate = getPostRate(did, userPosts, now);
		}
		scores.set(did, score / Math.pow(rate + rateBaseline, ratePower));
	}

	return scores;
};
