import type { Did } from "@atcute/lexicons/syntax";
import { createBobbinClient } from "$lib/api/client";
import { fetchPage } from "$lib/api/pagination";
import { enrich, countOf, viewerUriOf, type Stats, type LinkDescriptor } from "$lib/api/enrich";
import { type RepoRecord, type RecordList } from "$lib/api/records";
import { IdentityCache } from "$lib/api/identity";
import { didFromUri, rkeyFromUri } from "$lib/api/uri";
import { toHttpError } from "$lib/api/load";
import type { SearchPage } from "$lib/api/search";
import type { BobbinContext } from "$lib/api/client";
import { type VouchRecord } 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 STAR_COUNT: LinkDescriptor = { source: "sh.tangled.feed.star:subject", type: "count" };
const STAR_VIEWER: LinkDescriptor = { source: "sh.tangled.feed.star:subject", type: "viewer" };
const FOLLOW_STATS: LinkDescriptor[] = [
	{ source: "sh.tangled.graph.follow:subject", type: "count" },
	{ source: "sh.tangled.graph.follow:.repo", type: "count" }
];
const FOLLOW_VIEWER: LinkDescriptor = { source: "sh.tangled.graph.follow:subject", type: "viewer" };

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
	};
};

const resolveRepoCard = (item: ListItem, ownerHandle: string, stats: Stats): RepoCardData => {
	const repo = toRepoCard(item, ownerHandle);
	if (!repo.repoDid) return { ...repo, stars: 0, viewerStarRkey: null };
	const stars = countOf(stats, repo.repoDid, STAR_COUNT.source);
	const viewerUri = viewerUriOf(stats, repo.repoDid, STAR_VIEWER.source);
	return { ...repo, stars, viewerStarRkey: viewerUri ? rkeyFromUri(viewerUri) : viewerUri };
};

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
	};
};

// the sidecar already carries follower counts and viewer status, so this costs
// no extra requests
const resolvePeople = async (
	ctx: BobbinContext,
	dids: string[],
	stats: Stats,
	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)));

	const byDid = new Map<string, PersonData>();
	unique.forEach((did, index) => {
		const doc = docs[index];
		const followers = countOf(stats, did, "sh.tangled.graph.follow:subject");
		const following = countOf(stats, did, "sh.tangled.graph.follow:.repo");
		const isSelf = viewerDid === did;
		const viewerUri = viewerUriOf(stats, did, FOLLOW_VIEWER.source);
		const viewerFollowRkey = viewerUri ? rkeyFromUri(viewerUri) : viewerUri;
		byDid.set(
			did,
			doc
				? {
						did: doc.did,
						handle: doc.handle,
						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,
				kind: value.kind === "denounce" ? "denounce" : "vouch",
				direction,
				reason: value.reason,
				createdAt: value.createdAt
			};
		})
	);
};

const resolveStars = async (
	ctx: BobbinContext,
	items: ListItem[],
	viewerDid?: string
): Promise<StarData[]> => {
	const cache = new IdentityCache(ctx);
	const repoDids = [
		...new Set(
			items
				.map((item) => (item.value as ShTangledFeedStar.Main).subject)
				.flatMap((s) => (s && "did" in s && s.did ? [s.did] : []))
		)
	];
	const enriched =
		repoDids.length > 0
			? await enrich<RecordList<RepoRecord>>(ctx, {
					xrpc: "sh.tangled.repo.getReposByRepoDids",
					params: { dids: repoDids },
					enrich: viewerDid ? [STAR_COUNT, STAR_VIEWER] : [STAR_COUNT],
					...(viewerDid ? { viewer: viewerDid } : {})
				})
			: { output: { items: [] }, stats: {} as Stats };
	const reposByDid = new Map(
		enriched.output.items.map((item) => [(item.value as RepoRecord).repoDid, item])
	);
	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) {
				const repo = reposByDid.get(subject.did);
				if (!repo) return null;
				const ownerDid = didFromUri(repo.uri);
				const owner = await cache.resolve(ownerDid).catch(() => null);
				return {
					kind: "repo",
					uri: item.uri,
					createdAt: value.createdAt,
					repo: resolveRepoCard(repo, owner?.handle ?? ownerDid, enriched.stats)
				};
			}
			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 viewerDid = parent.auth?.did;
				const viewerEnrich = viewerDid ? [STAR_COUNT, STAR_VIEWER] : [STAR_COUNT];
				if (!q) {
					const enriched = await enrich<RecordList<RepoRecord>>(ctx, {
						xrpc: "sh.tangled.repo.listRepos",
						params: { subject: did, limit: PAGE_LIMIT },
						enrich: viewerEnrich,
						...(viewerDid ? { viewer: viewerDid } : {})
					});
					return {
						tab,
						repos: enriched.output.items.map((item) =>
							resolveRepoCard(item, handle, enriched.stats)
						)
					};
				}
				const enriched = await enrich<SearchPage>(ctx, {
					xrpc: "sh.tangled.search.query",
					params: { q, nsid: "sh.tangled.repo", author: did, limit: PAGE_LIMIT },
					enrich: viewerEnrich,
					...(viewerDid ? { viewer: viewerDid } : {})
				});
				return {
					tab,
					repos: enriched.output.hits.map((item) => resolveRepoCard(item, handle, enriched.stats))
				};
			}
			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 viewerDid = parent.auth?.did;
				const enriched = await enrich<RecordList<ShTangledGraphFollow.Main>>(ctx, {
					xrpc: "sh.tangled.graph.listFollows",
					params: { subject: did, limit: PAGE_LIMIT },
					enrich: viewerDid ? [...FOLLOW_STATS, FOLLOW_VIEWER] : FOLLOW_STATS,
					...(viewerDid ? { viewer: viewerDid } : {})
				});
				const dids = enriched.output.items.map((item) => didFromUri(item.uri));
				return {
					tab,
					people: await resolvePeople(ctx, dids, enriched.stats, viewerDid)
				};
			}
			case "following": {
				const viewerDid = parent.auth?.did;
				const enriched = await enrich<RecordList<ShTangledGraphFollow.Main>>(ctx, {
					xrpc: "sh.tangled.graph.listFollowsBy",
					params: { subject: did, limit: PAGE_LIMIT },
					enrich: viewerDid ? [...FOLLOW_STATS, FOLLOW_VIEWER] : FOLLOW_STATS,
					...(viewerDid ? { viewer: viewerDid } : {})
				});
				const dids = enriched.output.items.map(
					(item) => (item.value as ShTangledGraphFollow.Main).subject
				);
				return {
					tab,
					people: await resolvePeople(ctx, dids, enriched.stats, viewerDid)
				};
			}
			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 = await fetchPage(ctx, "sh.tangled.feed.listStarsBy", {
					subject: did,
					limit: PAGE_LIMIT
				});
				return {
					tab,
					stars: await resolveStars(ctx, page.items, parent.auth?.did)
				};
			}
			case "overview":
			default: {
				const viewerDid = parent.auth?.did;
				const enriched = await enrich<RecordList<RepoRecord>>(ctx, {
					xrpc: "sh.tangled.repo.listRepos",
					params: { subject: did, limit: PAGE_LIMIT },
					enrich: viewerDid ? [STAR_COUNT, STAR_VIEWER] : [STAR_COUNT],
					...(viewerDid ? { viewer: viewerDid } : {})
				});

				const pinnedKeys = parent.profile?.pinnedRepositories ?? [];
				const byKey = new Map<string, ListItem>();
				for (const item of enriched.output.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 = pinnedItems.map((item) => resolveRepoCard(item, handle, enriched.stats));

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