import type { ComAtprotoRepoStrongRef } from '@atcute/atproto';
import type { AppBskyEmbedExternal, AppBskyFeedDefs, AppBskyGraphDefs } from '@atcute/bluesky';
import { type Client, ok } from '@atcute/client';
import type { ActorIdentifier, Did, ResourceUri } from '@atcute/lexicons';
import { isDid, parseResourceUri } from '@atcute/lexicons/syntax';

import { makeRecordUri } from '#/lib/at-uri';
import type { Gif } from '#/lib/gif';
import { createGIFDescription } from '#/lib/gif-alt-text';
import { getLinkMeta, type LinkMeta } from '#/lib/link-meta';
import { resolveUrlToLink } from '#/lib/links/app-url';
import { resolveShortLink } from '#/lib/links/short-link';
import { type ComposerImage, createComposerImage } from '#/lib/media/composer-image';
import { compressLinkThumbImage } from '#/lib/media/compress-image';
import { gifUrlParams, klipyHostname } from '#/lib/media/gif-embed';

type ResolvedExternalLink = {
	type: 'external';
	uri: string;
	title: string;
	description: string;
	thumb: ComposerImage | undefined;
	/**
	 * strong refs (URI and CID) of the Atmosphere records backing this external content, resolved by the
	 * appview from the page's `<link rel>` tags
	 */
	associatedRefs?: LinkMeta['associatedRefs'];
	view?: AppBskyEmbedExternal.View;
};

type ResolvedPostRecord = {
	type: 'record';
	kind: 'post';
	record: ComAtprotoRepoStrongRef.Main;
	view: AppBskyFeedDefs.PostView;
};

type ResolvedFeedRecord = {
	type: 'record';
	kind: 'feed';
	record: ComAtprotoRepoStrongRef.Main;
	view: AppBskyFeedDefs.GeneratorView;
};

type ResolvedListRecord = {
	type: 'record';
	kind: 'list';
	record: ComAtprotoRepoStrongRef.Main;
	view: AppBskyGraphDefs.ListView;
};

type ResolvedStarterPackRecord = {
	type: 'record';
	kind: 'starterPack';
	record: ComAtprotoRepoStrongRef.Main;
	view: AppBskyGraphDefs.StarterPackView;
};

export type ResolvedLink =
	| ResolvedExternalLink
	| ResolvedPostRecord
	| ResolvedFeedRecord
	| ResolvedListRecord
	| ResolvedStarterPackRecord;

export class EmbeddingDisabledError extends Error {
	constructor() {
		super('Embedding is disabled for this record');
	}
}

export async function resolveLink(appview: Client, uri: string, signal?: AbortSignal): Promise<ResolvedLink> {
	let link = resolveUrlToLink(uri);
	if (link?.kind === 'bskyStarterPackCode') {
		const expanded = await resolveShortLink(link.code, signal);

		if (expanded) {
			uri = expanded;
			link = resolveUrlToLink(expanded);
		} else {
			link = undefined;
		}
	}

	switch (link?.kind) {
		case 'feed': {
			const did = await fetchDid(link.actor);
			const feed = makeRecordUri(did, 'app.bsky.feed.generator', link.rkey);
			const res = await ok(appview.get('app.bsky.feed.getFeedGenerator', { signal, params: { feed } }));
			return {
				type: 'record',
				kind: 'feed',
				record: {
					uri: res.view.uri,
					cid: res.view.cid,
				},
				view: res.view,
			};
		}
		case 'list': {
			const did = await fetchDid(link.actor);
			const list = makeRecordUri(did, 'app.bsky.graph.list', link.rkey);
			const res = await ok(appview.get('app.bsky.graph.getList', { signal, params: { list } }));
			return {
				type: 'record',
				kind: 'list',
				record: {
					uri: res.list.uri,
					cid: res.list.cid,
				},
				view: res.list,
			};
		}
		case 'post': {
			const recordUri = makeRecordUri(link.actor, 'app.bsky.feed.post', link.rkey);
			const post = await getPost({ uri: recordUri });
			if (post.viewer?.embeddingDisabled) {
				throw new EmbeddingDisabledError();
			}
			return {
				type: 'record',
				kind: 'post',
				record: {
					cid: post.cid,
					uri: post.uri,
				},
				view: post,
			};
		}
		case 'starterPack': {
			const did = await fetchDid(link.actor);
			const starterPack = makeRecordUri(did, 'app.bsky.graph.starterpack', link.rkey);
			const res = await ok(
				appview.get('app.bsky.graph.getStarterPack', {
					signal,
					params: { starterPack },
				}),
			);
			return {
				type: 'record',
				kind: 'starterPack',
				record: {
					uri: res.starterPack.uri,
					cid: res.starterPack.cid,
				},
				view: res.starterPack,
			};
		}
	}

	// Forked from useGetPost. TODO: move into RQ.
	async function getPost({ uri: postUri }: { uri: ResourceUri }) {
		const urip = parseResourceUri(postUri);
		const repo = await fetchDid(urip.repo);
		const res = await ok(
			appview.get('app.bsky.feed.getPosts', {
				signal,
				params: { uris: [`at://${repo}/${urip.collection}/${urip.rkey}`] },
			}),
		);
		if (res.posts[0]) {
			return res.posts[0];
		}
		throw new Error('getPost: post not found');
	}

	// Forked from useFetchDid. TODO: move into RQ.
	async function fetchDid(handleOrDid: ActorIdentifier): Promise<Did> {
		if (isDid(handleOrDid)) {
			return handleOrDid;
		}
		const res = await ok(
			appview.get('com.atproto.identity.resolveHandle', { signal, params: { handle: handleOrDid } }),
		);
		return res.did;
	}

	return resolveExternal(uri, signal);
}

export async function resolveGif(gif: Gif, signal: AbortSignal): Promise<ResolvedExternalLink> {
	const gifUrl = gif.media_formats.gif.url;
	const params = new URLSearchParams();
	params.set(gifUrlParams.height, String(gif.media_formats.gif.dims[1]));
	params.set(gifUrlParams.width, String(gif.media_formats.gif.dims[0]));

	// Klipy uses a different filename for each video format.
	try {
		const url = new URL(gifUrl);
		if (url.hostname === klipyHostname) {
			const mp4Slug = getFileSlug(gif.media_formats.mp4?.url);
			const webmSlug = getFileSlug(gif.media_formats.webm?.url);
			if (mp4Slug) {
				params.set(gifUrlParams.mp4, mp4Slug);
			}
			if (webmSlug) {
				params.set(gifUrlParams.webm, webmSlug);
			}
		}
	} catch {}

	const uri = `${gifUrl}?${params.toString()}`;
	const altText = gif.content_description || gif.title;
	return {
		type: 'external',
		uri,
		title: altText,
		description: createGIFDescription(altText),
		thumb: await imageToThumb(gif.media_formats.preview.url, signal),
	};
}

function getFileSlug(url: string | undefined): string | undefined {
	if (!url) {
		return undefined;
	}
	const filename = url.split('/').pop();
	if (!filename) {
		return undefined;
	}
	const dotIndex = filename.lastIndexOf('.');
	return dotIndex > 0 ? filename.slice(0, dotIndex) : undefined;
}

async function resolveExternal(uri: string, signal?: AbortSignal): Promise<ResolvedExternalLink> {
	const result = await getLinkMeta(uri, { signal });
	return {
		type: 'external',
		uri: result.url,
		title: result.title ?? '',
		description: result.description ?? '',
		thumb: result.image ? await imageToThumb(result.image, signal) : undefined,
		// standard.site fields the worker hydrates via the appview; the rest come from the page's opengraph/twitter
		// meta tags.
		associatedRefs: result.associatedRefs,
		view: result.view,
	};
}

export async function imageToThumb(
	imageUri: string,
	signal?: AbortSignal,
): Promise<ComposerImage | undefined> {
	try {
		const timeoutSignal = AbortSignal.timeout(15e3);
		const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
		const res = await fetch(imageUri, { signal: requestSignal });
		const source = await res.blob();

		const { blob } = await compressLinkThumbImage(source);
		signal?.throwIfAborted();
		const image = await createComposerImage(blob);
		signal?.throwIfAborted();
		return image;
	} catch {
		signal?.throwIfAborted();
	}
}
