import { type Did, type ResourceUri } from '@atcute/lexicons';
import type { Account } from './accounts';
import type { PostWithUri } from './at/fetch';
import { getBlockRelationship, postsByRootUri } from './state.svelte';
import { timestampFromCursor } from '$lib';

export type ThreadPost = {
	data: PostWithUri;
	account: Did;
	did: Did;
	rkey: string;
	parentUri: ResourceUri | null;
	depth: number;
	newestTime: number;
	blockRelationship?: { userBlocked: boolean; blockedByTarget: boolean };
	isMuted?: boolean;
};

export type Thread = {
	rootUri: ResourceUri;
	posts: ThreadPost[];
	newestTime: number;
	branchParentPost?: ThreadPost;
};

export const isOwnPost = (post: ThreadPost, accounts: Account[]) =>
	accounts.some((account) => account.did === post.did);
export const hasNonOwnPost = (posts: ThreadPost[], accounts: Account[]) =>
	posts.some((post) => !isOwnPost(post, accounts));

export type FilterOptions = {
	viewOwnPosts: boolean;
	filterReplies?: boolean;
	filterRootsToDids?: Set<Did>;
};

type ThreadGroup = {
	rootUri: ResourceUri;
	posts: ThreadPost[];
	newestTime: number;
	isReply: boolean;
	rootDid: Did | null;
};

export const buildThreadsFiltered = (
	account: Did,
	timeline: Set<ResourceUri>,
	posts: Map<ResourceUri, PostWithUri>,
	mutes: Set<Did>,
	accounts: Account[],
	opts: FilterOptions,
	limit?: number
): Thread[] => {
	const blockCache = new Map<Did, { userBlocked: boolean; blockedByTarget: boolean }>();
	const getBlockRel = (target: Did) => {
		let rel = blockCache.get(target);
		if (!rel) {
			rel = getBlockRelationship(account, target);
			blockCache.set(target, rel);
		}
		return rel;
	};

	// phase 1: find distinct root URIs from timeline and build groups using pre-computed index
	const rootUrisFromTimeline = new Set<ResourceUri>();
	for (const uri of timeline) {
		const did = extractDidFromUri(uri);
		if (!did) continue;
		const data = posts.get(uri);
		if (!data) continue;
		const rootUri = (data.record.reply?.root.uri as ResourceUri) || uri;
		rootUrisFromTimeline.add(rootUri);
	}

	const groups = new Map<ResourceUri, ThreadGroup>();
	for (const rootUri of rootUrisFromTimeline) {
		const postUris = postsByRootUri.get(rootUri);
		if (!postUris) continue;

		let group: ThreadGroup | undefined;

		for (const uri of postUris) {
			if (!timeline.has(uri)) continue;

			const did = extractDidFromUri(uri);
			if (!did) continue;
			const parts = uri.split('/');
			if (parts.length < 5) continue;
			const rkey = parts[4];

			const data = posts.get(uri);
			if (!data) continue;

			const parentUri = (data.record.reply?.parent.uri as ResourceUri) || null;
			const isReply = !!data.record.reply;

			const cursorTime = timestampFromCursor(rkey);
			const postTime = cursorTime ? cursorTime / 1000 : new Date(data.record.createdAt).getTime();

			if (!group) {
				group = {
					rootUri,
					posts: [],
					newestTime: postTime,
					isReply,
					rootDid: extractDidFromUri(rootUri)
				};
				groups.set(rootUri, group);
			}

			const blockRel = getBlockRel(did);
			group.posts.push({
				data,
				account,
				did,
				rkey,
				parentUri,
				depth: 0,
				newestTime: postTime,
				blockRelationship: blockRel,
				isMuted: mutes.has(did)
			});

			if (postTime > group.newestTime) group.newestTime = postTime;
		}
	}

	// phase 2: sort groups by newest time descending
	const sortedGroups = Array.from(groups.values()).sort((a, b) => b.newestTime - a.newestTime);

	// phase 3: process groups with pre-filtering and early exit
	const threads: Thread[] = [];

	const shouldIncludeGroup = (group: ThreadGroup): boolean => {
		if (opts.filterReplies && group.isReply) return false;

		if (opts.filterRootsToDids) {
			if (
				group.rootDid &&
				!opts.filterRootsToDids.has(group.rootDid) &&
				!accounts.some((a) => a.did === group.rootDid)
			) {
				return false;
			}
		}

		return true;
	};

	const processGroup = (group: ThreadGroup): Thread[] => {
		const groupPosts = group.posts;
		const uriToPost = new Map(groupPosts.map((p) => [p.data.uri, p]));
		const childrenMap = new Map<ResourceUri | null, ThreadPost[]>();

		for (const post of groupPosts) {
			let depth = 0;
			let currentUri = post.parentUri;
			while (currentUri && uriToPost.has(currentUri)) {
				depth++;
				currentUri = uriToPost.get(currentUri)!.parentUri;
			}
			post.depth = depth;

			if (!childrenMap.has(post.parentUri)) childrenMap.set(post.parentUri, []);
			childrenMap.get(post.parentUri)!.push(post);
		}

		childrenMap
			.values()
			.forEach((children) => children.sort((a, b) => b.newestTime - a.newestTime));

		const createThread = (
			posts: ThreadPost[],
			rootUri: ResourceUri,
			branchParentUri?: ResourceUri
		): Thread => ({
			rootUri,
			posts,
			newestTime: Math.max(...posts.map((p) => p.newestTime)),
			branchParentPost: branchParentUri ? uriToPost.get(branchParentUri) : undefined
		});

		const collectSubtree = (startPost: ThreadPost): ThreadPost[] => {
			const result: ThreadPost[] = [];
			const addWithChildren = (post: ThreadPost) => {
				result.push(post);
				const children = childrenMap.get(post.data.uri) || [];
				children.forEach(addWithChildren);
			};
			addWithChildren(startPost);
			return result;
		};

		const branchingPoints = Array.from(childrenMap.entries())
			.filter(([, children]) => children.length > 1)
			.map(([uri]) => uri);

		const result: Thread[] = [];

		if (branchingPoints.length === 0) {
			const roots = childrenMap.get(null) || [];
			const allPosts = roots.flatMap((root) => collectSubtree(root));
			result.push(createThread(allPosts, group.rootUri));
		} else {
			for (const branchParentUri of branchingPoints) {
				const branches = childrenMap.get(branchParentUri) || [];
				const sortedBranches = [...branches].sort((a, b) => a.newestTime - b.newestTime);

				sortedBranches.forEach((branchRoot, index) => {
					const isOldestBranch = index === 0;
					const branchPosts: ThreadPost[] = [];

					if (isOldestBranch && branchParentUri !== null) {
						const parentChain: ThreadPost[] = [];
						let currentUri: ResourceUri | null = branchParentUri;
						while (currentUri && uriToPost.has(currentUri)) {
							parentChain.unshift(uriToPost.get(currentUri)!);
							currentUri = uriToPost.get(currentUri)!.parentUri;
						}
						branchPosts.push(...parentChain);
					}

					branchPosts.push(...collectSubtree(branchRoot));

					const minDepth = Math.min(...branchPosts.map((p) => p.depth));
					branchPosts.forEach((p) => (p.depth = p.depth - minDepth));

					result.push(
						createThread(
							branchPosts,
							branchRoot.data.uri,
							isOldestBranch ? undefined : (branchParentUri ?? undefined)
						)
					);
				});
			}
		}

		return result;
	};

	const passesPostFilter = (thread: Thread): boolean => {
		if (thread.posts.length === 0) return false;
		if (!opts.viewOwnPosts && hasNonOwnPost(thread.posts, accounts)) return false;
		return true;
	};

	for (const group of sortedGroups) {
		if (!shouldIncludeGroup(group)) continue;

		const groupThreads = processGroup(group);

		for (const thread of groupThreads) {
			if (!passesPostFilter(thread)) continue;
			threads.push(thread);
			if (limit && threads.length >= limit) return threads;
		}
	}

	return threads;
};

const extractDidFromUri = (uri: ResourceUri): Did | null => {
	const match = uri.match(/^at:\/\/(did:plc:[a-z0-9]+)/);
	return match ? (match[1] as Did) : null;
};
