import type { AppBskyRichtextFacet } from '@atcute/bluesky';
import RichtextBuilder from '@atcute/bluesky-richtext-builder';
import { type Token, tokenize } from '@atcute/bluesky-richtext-parser';
import { segmentize } from '@atcute/bluesky-richtext-segmenter';
import type { Did, GenericUri, Handle } from '@atcute/lexicons';
import { isHandle } from '@atcute/lexicons/syntax';
import { getGraphemeLength } from '@atcute/util-text';

import { mapDefined, unique } from '@mary/array-fns';

import { isMisleadingLink } from '#/lib/links/trust';
import { toShortUrl } from '#/lib/utils/url';

export type RichtextFacet = AppBskyRichtextFacet.Main;

/** A post's rich text, decomposed into its plain text and the facets that decorate it. */
export type Richtext = {
	text: string;
	facets?: RichtextFacet[];
};

/** a parsed or segmented rich-text run. */
type RichtextSegment =
	| { type: 'link'; text: string; uri: GenericUri }
	| { type: 'mention'; text: string; did: Did }
	| { type: 'tag'; text: string; tag: string }
	| { type: 'text'; text: string }
	| { type: 'unresolvedMention'; text: string; handle: Handle };

const toSegment = (token: Token): RichtextSegment => {
	switch (token.type) {
		case 'mention': {
			if (!isHandle(token.handle)) {
				return { type: 'text', text: token.raw };
			}

			return { type: 'unresolvedMention', text: token.raw, handle: token.handle };
		}
		case 'topic': {
			return { type: 'tag', text: token.raw, tag: token.name };
		}
		case 'autolink': {
			// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the parser only emits `autolink` for `http(s)://` runs
			return { type: 'link', text: token.raw, uri: token.url as GenericUri };
		}
		default: {
			// unsupported facets stay literal; `content` can omit merged or invalid syntax
			return { type: 'text', text: token.raw };
		}
	}
};

/**
 * joins segment display text.
 *
 * @param segments rich-text segments
 * @returns joined display text
 */
export function toPlainText(segments: RichtextSegment[]): string {
	return segments.reduce((text, segment) => text + segment.text, '');
}

/**
 * parses inline syntax into rich-text segments with unresolved mentions.
 *
 * @param text the source text
 * @returns parsed segments
 */
export function parseRichtext(text: string): RichtextSegment[] {
	return tokenize(text).map(toSegment);
}

/**
 * converts network rich text to segments without parsing inline syntax.
 *
 * @param richtext network rich text
 * @returns segmented rich text
 */
export function segmentizeRichtext({ text, facets }: Richtext): RichtextSegment[] {
	if (!facets?.length) {
		return [{ type: 'text', text }];
	}

	return segmentize(text, facets).map((segment): RichtextSegment => {
		// use the first supported feature in wire order
		for (const feature of segment.features ?? []) {
			switch (feature.$type) {
				case 'app.bsky.richtext.facet#link': {
					return { type: 'link', text: segment.text, uri: feature.uri };
				}
				case 'app.bsky.richtext.facet#mention': {
					return { type: 'mention', text: segment.text, did: feature.did };
				}
				case 'app.bsky.richtext.facet#tag': {
					return { type: 'tag', text: segment.text, tag: feature.tag };
				}
			}
		}
		return { type: 'text', text: segment.text };
	});
}

/**
 * resolves valid mention handles and leaves failed resolutions unresolved.
 *
 * @param segments rich-text segments
 * @param resolve the handle resolver
 * @returns resolved segments
 */
export async function resolveMentions(
	segments: RichtextSegment[],
	resolve: (handle: Handle) => Promise<Did | undefined>,
): Promise<RichtextSegment[]> {
	const handles = unique(
		mapDefined(segments, (segment) => {
			if (segment.type === 'unresolvedMention') {
				return segment.handle;
			}
		}),
	);

	const resolved = new Map<string, Did | undefined>();
	await Promise.all(
		handles.map(async (handle) => {
			resolved.set(handle, await resolve(handle));
		}),
	);

	return segments.map((segment) => {
		if (segment.type !== 'unresolvedMention') {
			return segment;
		}
		const did = resolved.get(segment.handle);
		return did ? { type: 'mention', text: segment.text, did } : segment;
	});
}

/**
 * shortens link display text.
 *
 * @param segments rich-text segments
 * @returns segments with shortened links
 */
export function shortenLinks(segments: RichtextSegment[]): RichtextSegment[] {
	return segments.map((segment) => {
		return segment.type === 'link' ? { ...segment, text: toShortUrl(segment.text) } : segment;
	});
}

/**
 * restores full URLs in link text that represents its target.
 *
 * @param segments rich-text segments
 * @returns segments with full link text
 */
export function unshortenLinks(segments: RichtextSegment[]): RichtextSegment[] {
	return segments.map((segment) => {
		return segment.type === 'link' && !isMisleadingLink(segment.uri, segment.text)
			? { ...segment, text: segment.uri }
			: segment;
	});
}

/**
 * converts segments to network rich text; unresolved mentions become plain text.
 *
 * @param segments rich-text segments
 * @returns network rich text
 */
export function bakeRichtext(segments: RichtextSegment[]): Richtext {
	const builder = new RichtextBuilder();
	for (const segment of segments) {
		switch (segment.type) {
			case 'link': {
				builder.addLink(segment.text, segment.uri);
				break;
			}
			case 'mention': {
				builder.addMention(segment.text, segment.did);
				break;
			}
			case 'tag': {
				builder.addTag(segment.text, segment.tag);
				break;
			}
			default: {
				builder.addText(segment.text);
			}
		}
	}
	return builder.build();
}

/**
 * counts graphemes after shortening links.
 *
 * @param text source text
 * @returns displayed grapheme count
 */
export function getShortenedLength(text: string): number {
	return getGraphemeLength(toPlainText(shortenLinks(parseRichtext(text))));
}

/**
 * converts rich text to publishable source text.
 *
 * @param rt the rich text
 * @returns source text with full link targets
 */
export function richTextToSourceText(rt: Richtext): string {
	return toPlainText(unshortenLinks(segmentizeRichtext(rt)));
}

/**
 * converts rich text to copyable text with explicit link targets.
 *
 * @param rt the rich text
 * @returns copyable text
 */
export function richTextToCopyableText(rt: Richtext): string {
	let result = '';

	for (const segment of segmentizeRichtext(rt)) {
		if (segment.type !== 'link') {
			result += segment.text;
		} else if (isMisleadingLink(segment.uri, segment.text)) {
			result += `[${segment.text}](${segment.uri})`;
		} else {
			result += segment.uri;
		}
	}

	return result;
}
