import type { Did } from "@atcute/lexicons/syntax";
import { createBobbinClient } from "$lib/api/client";
import { fetchPage, items } from "$lib/api/pagination";
import { count } from "$lib/api/count";
import { getRepoByRepoDid, type RepoRecord } from "$lib/api/records";
import { IdentityCache } from "$lib/api/identity";
import { didFromUri, rkeyFromUri } from "$lib/api/uri";
import { toHttpError, parallel } from "$lib/api/load";
import { search } from "$lib/api/search";
import type { BobbinContext } from "$lib/api/client";
import { listStarRkeys, type VouchRecord, type FollowRecord } from "$lib/api/graph";
import type * as ShTangledFeedStar from "$lib/api/lexicons/types/sh/tangled/feed/star";
import type * as ShTangledString from "$lib/api/lexicons/types/sh/tangled/string";
import type * as ShTangledGraphFollow from "$lib/api/lexicons/types/sh/tangled/graph/follow";
import type {
	RepoCardData,
	StringCardData,
	PersonData,
	VouchData,
	StarData
} from "$lib/components/profile/types";
import type { PageLoad } from "./$types";

const PAGE_LIMIT = 50;

const TABS = [
	"overview",
	"repos",
	"starred",
	"strings",
	"followers",
	"following",
	"vouches"
] as const;
type Tab = (typeof TABS)[number];

const normalizeTab = (raw: string | null): Tab =>
	TABS.includes(raw as Tab) ? (raw as Tab) : "overview";

interface ListItem {
	uri: string;
	value: unknown;
}

const toRepoCard = (item: ListItem, ownerHandle: string): RepoCardData => {
	const value = item.value as RepoRecord;
	return {
		rkey: rkeyFromUri(item.uri),
		name: value.name ?? rkeyFromUri(item.uri),
		repoDid: value.repoDid ?? "",
		ownerHandle,
		description: value.description,
		knot: value.knot,
		createdAt: value.createdAt
	};
};

interface ResolveRepoCardOptions {
	viewerStarRkeys?: ReadonlyMap<string, string>;
}

const resolveRepoCard = async (
	ctx: BobbinContext,
	item: ListItem,
	ownerHandle: string,
	options: ResolveRepoCardOptions = {}
): Promise<RepoCardData> => {
	const repo = toRepoCard(item, ownerHandle);
	if (!repo.repoDid) return { ...repo, stars: 0, viewerStarRkey: null };
	// TODO(bobbin): instead of doing this, listing repos should return star counts
	// and most likely other stats as well.
	const stars = await count(ctx, "sh.tangled.feed.countStars", repo.repoDid);
	return {
		...repo,
		stars: stars.count,
		viewerStarRkey: options.viewerStarRkeys
			? (options.viewerStarRkeys.get(repo.repoDid) ?? null)
			: undefined
	};
};

const toStringCard = (item: ListItem, ownerHandle: string): StringCardData => {
	const value = item.value as ShTangledString.Main;
	return {
		rkey: rkeyFromUri(item.uri),
		ownerHandle,
		filename: value.filename,
		description: value.description,
		createdAt: value.createdAt,
		lines: value.contents?.split("\n").length ?? 1
	};
};

// resolve dids -> handle/avatar, deduped, preserving input order.
const resolvePeople = async (
	ctx: BobbinContext,
	dids: string[],
	viewerDid?: string
): Promise<PersonData[]> => {
	const cache = new IdentityCache(ctx);
	const unique = [...new Set(dids)];

	const docs = await Promise.all(unique.map((did) => cache.resolve(did).catch(() => null)));
	// TODO(bobbin): need bobbin to return follower / following stats when listing follows..
	const counts = await parallel(
		unique.reduce(
			(acc, did) => {
				acc[`${did}-followers`] = count(ctx, "sh.tangled.graph.countFollows", did)
					.then((result) => result.count)
					.catch(() => 0);
				acc[`${did}-following`] = count(ctx, "sh.tangled.graph.countFollowsBy", did)
					.then((result) => result.count)
					.catch(() => 0);
				return acc;
			},
			{} as Record<string, Promise<number>>
		)
	);

	const viewerFollowRkeys = new Map<string, string>();
	if (viewerDid) {
		for await (const item of items(
			ctx,
			"sh.tangled.graph.listFollowsBy",
			{ subject: viewerDid as Did },
			{ maxPages: 10 }
		)) {
			const value = item.value as FollowRecord;
			viewerFollowRkeys.set(value.subject, rkeyFromUri(item.uri));
		}
	}

	const byDid = new Map<string, PersonData>();
	unique.forEach((did, index) => {
		const doc = docs[index];
		const followers = counts[`${did}-followers`];
		const following = counts[`${did}-following`];
		const isSelf = viewerDid === did;
		const viewerFollowRkey = viewerDid ? (viewerFollowRkeys.get(did) ?? null) : undefined;
		byDid.set(
			did,
			doc
				? {
						did: doc.did,
						handle: doc.handle,
						avatar: doc.avatar,
						followers,
						following,
						isSelf,
						viewerFollowRkey
					}
				: { did, handle: did, followers, following, isSelf, viewerFollowRkey }
		);
	});
	return unique.map((did) => byDid.get(did) as PersonData);
};

const resolveVouches = async (
	ctx: BobbinContext,
	items: ListItem[],
	direction: "incoming" | "outgoing"
): Promise<VouchData[]> => {
	const cache = new IdentityCache(ctx);
	return Promise.all(
		items.map(async (item): Promise<VouchData> => {
			const value = item.value as VouchRecord;
			const otherDid = direction === "incoming" ? didFromUri(item.uri) : rkeyFromUri(item.uri);
			const doc = await cache.resolve(otherDid).catch(() => null);
			return {
				uri: item.uri,
				did: otherDid,
				handle: doc?.handle ?? otherDid,
				avatar: doc?.avatar,
				kind: value.kind === "denounce" ? "denounce" : "vouch",
				direction,
				reason: value.reason,
				createdAt: value.createdAt
			};
		})
	);
};

const resolveStars = async (
	ctx: BobbinContext,
	items: ListItem[],
	options: ResolveRepoCardOptions
): Promise<StarData[]> => {
	const cache = new IdentityCache(ctx);
	const resolved = await Promise.all(
		items.map(async (item): Promise<StarData | null> => {
			const value = item.value as ShTangledFeedStar.Main;
			const subject = value.subject;
			if (subject && "did" in subject && subject.did) {
				try {
					const repo = await getRepoByRepoDid(ctx, subject.did);
					const ownerDid = didFromUri(repo.uri);
					const owner = await cache.resolve(ownerDid).catch(() => null);
					return {
						kind: "repo",
						uri: item.uri,
						createdAt: value.createdAt,
						repo: await resolveRepoCard(ctx, repo, owner?.handle ?? ownerDid, options)
					};
				} catch {
					return null;
				}
			}
			if (subject && "uri" in subject && subject.uri) {
				const ownerDid = didFromUri(subject.uri);
				const owner = await cache.resolve(ownerDid).catch(() => null);
				return {
					kind: "string",
					uri: item.uri,
					createdAt: value.createdAt,
					ownerHandle: owner?.handle ?? ownerDid,
					rkey: rkeyFromUri(subject.uri)
				};
			}
			return null;
		})
	);
	return resolved.filter((star): star is StarData => star !== null);
};

export const load: PageLoad = async (event) => {
	const parent = await event.parent();
	const tab = normalizeTab(event.url.searchParams.get("tab"));

	if (parent.notJoined) return { tab: "overview" as const, overview: { pinned: [] } };

	const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch });
	const did = parent.identity.did as Did;
	const handle = parent.identity.handle;

	try {
		switch (tab) {
			case "repos": {
				const q = event.url.searchParams.get("q")?.trim();
				const [found, viewerStarRkeys] = await Promise.all([
					q
						? search(ctx, { q, nsid: "sh.tangled.repo", author: did, limit: PAGE_LIMIT }).then(
								(page) => page.hits
							)
						: fetchPage(ctx, "sh.tangled.repo.listRepos", { subject: did, limit: PAGE_LIMIT }).then(
								(page) => page.items
							),
					parent.auth?.did ? listStarRkeys(ctx, parent.auth.did) : undefined
				]);
				return {
					tab,
					repos: await Promise.all(
						found.map((item) => resolveRepoCard(ctx, item, handle, { viewerStarRkeys }))
					)
				};
			}
			case "strings": {
				const page = await fetchPage(ctx, "sh.tangled.string.listStrings", {
					subject: did,
					limit: PAGE_LIMIT
				});
				return { tab, strings: page.items.map((item) => toStringCard(item, handle)) };
			}
			case "followers": {
				const page = await fetchPage(ctx, "sh.tangled.graph.listFollows", {
					subject: did,
					limit: PAGE_LIMIT
				});
				const dids = page.items.map((item) => didFromUri(item.uri));
				return {
					tab,
					people: await resolvePeople(ctx, dids, parent.auth?.did)
				};
			}
			case "following": {
				const page = await fetchPage(ctx, "sh.tangled.graph.listFollowsBy", {
					subject: did,
					limit: PAGE_LIMIT
				});
				const dids = page.items.map((item) => (item.value as ShTangledGraphFollow.Main).subject);
				return {
					tab,
					people: await resolvePeople(ctx, dids, parent.auth?.did)
				};
			}
			case "vouches": {
				const [incomingPage, outgoingPage] = await Promise.all([
					fetchPage(ctx, "sh.tangled.graph.listVouches", { subject: did, limit: PAGE_LIMIT }),
					fetchPage(ctx, "sh.tangled.graph.listVouchesBy", { subject: did, limit: PAGE_LIMIT })
				]);
				const [incoming, outgoing] = await Promise.all([
					resolveVouches(ctx, incomingPage.items, "incoming"),
					resolveVouches(ctx, outgoingPage.items, "outgoing")
				]);
				const vouches = [...incoming, ...outgoing].sort(
					(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
				);
				return {
					tab,
					vouches,
					isSelf: parent.auth?.did === did,
					profileHandle: handle
				};
			}
			case "starred": {
				const [page, viewerStarRkeys] = await Promise.all([
					fetchPage(ctx, "sh.tangled.feed.listStarsBy", { subject: did, limit: PAGE_LIMIT }),
					parent.auth?.did ? listStarRkeys(ctx, parent.auth.did) : undefined
				]);
				return {
					tab,
					stars: await resolveStars(ctx, page.items, { viewerStarRkeys })
				};
			}
			case "overview":
			default: {
				const [page, viewerStarRkeys] = await Promise.all([
					fetchPage(ctx, "sh.tangled.repo.listRepos", { subject: did, limit: PAGE_LIMIT }),
					parent.auth?.did ? listStarRkeys(ctx, parent.auth.did) : undefined
				]);

				const pinnedKeys = parent.profile?.pinnedRepositories ?? [];
				const byKey = new Map<string, ListItem>();
				for (const item of page.items) {
					const value = item.value as RepoRecord;
					if (value.repoDid) byKey.set(value.repoDid, item);
					byKey.set(item.uri, item);
				}
				const pinnedItems = pinnedKeys
					.map((key) => byKey.get(key))
					.filter((item): item is ListItem => item !== undefined);
				const pinned = await Promise.all(
					pinnedItems.map((item) => resolveRepoCard(ctx, item, handle, { viewerStarRkeys }))
				);

				return { tab: "overview" as const, overview: { pinned } };
			}
		}
	} catch (cause) {
		console.error("Page load error:", cause);
		toHttpError(cause, "Could not load profile data");
	}
};
