import {
	serializeEmbed,
	serializeHyperlink,
	serializeImage,
	serializePreFormatted,
	serializeSpan,
	serializeStandardTag,
} from "../lib/serializerHelpers"
import type {
	RichTextFunctionSerializer,
	RichTextMapSerializer,
	RichTextMapSerializerFunction,
} from "../richtext"
import { composeSerializers, serialize, wrapMapSerializer } from "../richtext"
import type { RichTextField } from "../types/value/richText"
import type { LinkResolverFunction } from "./asLink"

/**
 * Serializes a node from a rich text field with a function to HTML.
 *
 * Unlike a typical `@prismicio/client/richtext` function serializer, this serializer converts the
 * `children` argument to a single string rather than an array of strings.
 *
 * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text}
 */
export type HTMLRichTextFunctionSerializer = (
	type: Parameters<RichTextFunctionSerializer<string>>[0],
	node: Parameters<RichTextFunctionSerializer<string>>[1],
	text: Parameters<RichTextFunctionSerializer<string>>[2],
	children: Parameters<RichTextFunctionSerializer<string>>[3][number],
	key: Parameters<RichTextFunctionSerializer<string>>[4],
) => string | null | undefined

/**
 * Serializes a node from a rich text field with a map to HTML.
 *
 * Unlike a typical `@prismicio/client/richtext` map serializer, this serializer converts the
 * `children` property to a single string rather than an array of strings and accepts shorthand
 * declarations.
 *
 * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text}
 */
export type HTMLRichTextMapSerializer = {
	[P in keyof RichTextMapSerializer<string>]: P extends RichTextMapSerializer<string>["span"]
		? HTMLStrictRichTextMapSerializer[P]
		: HTMLStrictRichTextMapSerializer[P] | HTMLRichTextMapSerializerShorthand
}

/**
 * Serializes a node from a rich text field with a map to HTML.
 *
 * Unlike a typical `@prismicio/client/richtext` map serializer, this serializer converts the
 * `children` property to a single string rather than an array of strings but doesn't accept
 * shorthand declarations.
 *
 * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text}
 */
export type HTMLStrictRichTextMapSerializer = {
	[P in keyof RichTextMapSerializer<string>]: (payload: {
		type: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["type"]
		node: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["node"]
		text: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["text"]
		children: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["children"][number]
		key: Parameters<HTMLRichTextMapSerializerFunction<P>>[0]["key"]
	}) => string | null | undefined
}

/**
 * A {@link RichTextMapSerializerFunction} type specifically for {@link HTMLRichTextMapSerializer}.
 *
 * @typeParam BlockName - The serializer's rich text block type.
 */
type HTMLRichTextMapSerializerFunction<BlockType extends keyof RichTextMapSerializer<string>> =
	RichTextMapSerializerFunction<
		string,
		ExtractNodeGeneric<RichTextMapSerializer<string>[BlockType]>,
		ExtractTextTypeGeneric<RichTextMapSerializer<string>[BlockType]>
	>

/**
 * Returns the `Node` generic from {@link RichTextMapSerializerFunction}.
 *
 * @typeParam T - The `RichTextMapSerializerFunction` containing the needed
 * `Node` generic.
 */
type ExtractNodeGeneric<T> =
	T extends RichTextMapSerializerFunction<
		// oxlint-disable-next-line no-explicit-any
		any,
		infer U,
		// oxlint-disable-next-line no-explicit-any
		any
	>
		? U
		: never

/**
 * Returns the `TextType` generic from {@link RichTextMapSerializerFunction}.
 *
 * @typeParam T - The `RichTextMapSerializerFunction` containing the needed
 * `TextType` generic.
 */
type ExtractTextTypeGeneric<T> =
	T extends RichTextMapSerializerFunction<
		// oxlint-disable-next-line no-explicit-any
		any,
		// oxlint-disable-next-line no-explicit-any
		any,
		infer U
	>
		? U
		: never

/** A shorthand definition for {@link HTMLRichTextMapSerializer} element types. */
export type HTMLRichTextMapSerializerShorthand = {
	/** Classes to apply to the element type. */
	class?: string

	/** Other attributes to apply to the element type. */
	[Attribute: string]: string | boolean | null | undefined
}

/**
 * Serializes a node from a rich text field with a map or a function to HTML.
 *
 * @see {@link HTMLRichTextMapSerializer} and {@link HTMLRichTextFunctionSerializer}
 * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text}
 */
export type HTMLRichTextSerializer = HTMLRichTextMapSerializer | HTMLRichTextFunctionSerializer

/**
 * Creates a HTML rich text serializer with a given link resolver and provide sensible and safe
 * defaults for every node type
 *
 * @internal
 */
const createHTMLRichTextSerializer = (
	linkResolver: LinkResolverFunction | undefined | null,
	serializer?: HTMLRichTextMapSerializer | null,
): RichTextFunctionSerializer<string> => {
	const useSerializerOrDefault = <BlockType extends keyof RichTextMapSerializer<string>>(
		nodeSerializerOrShorthand: HTMLRichTextMapSerializer[BlockType],
		defaultWithShorthand: NonNullable<HTMLStrictRichTextMapSerializer[BlockType]>,
	): NonNullable<HTMLStrictRichTextMapSerializer[BlockType]> => {
		if (typeof nodeSerializerOrShorthand === "function") {
			return ((payload) => {
				return (
					(nodeSerializerOrShorthand as HTMLStrictRichTextMapSerializer[BlockType])?.(payload) ||
					defaultWithShorthand(payload)
				)
			}) as NonNullable<HTMLStrictRichTextMapSerializer[BlockType]>
		}

		return defaultWithShorthand
	}

	const mapSerializer: Required<HTMLStrictRichTextMapSerializer> = {
		heading1: useSerializerOrDefault<"heading1">(
			serializer?.heading1,
			serializeStandardTag<"heading1">("h1", serializer?.heading1),
		),
		heading2: useSerializerOrDefault<"heading2">(
			serializer?.heading2,
			serializeStandardTag<"heading2">("h2", serializer?.heading2),
		),
		heading3: useSerializerOrDefault<"heading3">(
			serializer?.heading3,
			serializeStandardTag<"heading3">("h3", serializer?.heading3),
		),
		heading4: useSerializerOrDefault<"heading4">(
			serializer?.heading4,
			serializeStandardTag<"heading4">("h4", serializer?.heading4),
		),
		heading5: useSerializerOrDefault<"heading5">(
			serializer?.heading5,
			serializeStandardTag<"heading5">("h5", serializer?.heading5),
		),
		heading6: useSerializerOrDefault<"heading6">(
			serializer?.heading6,
			serializeStandardTag<"heading6">("h6", serializer?.heading6),
		),
		paragraph: useSerializerOrDefault<"paragraph">(
			serializer?.paragraph,
			serializeStandardTag<"paragraph">("p", serializer?.paragraph),
		),
		preformatted: useSerializerOrDefault<"preformatted">(
			serializer?.preformatted,
			serializePreFormatted(serializer?.preformatted),
		),
		strong: useSerializerOrDefault<"strong">(
			serializer?.strong,
			serializeStandardTag<"strong">("strong", serializer?.strong),
		),
		em: useSerializerOrDefault<"em">(
			serializer?.em,
			serializeStandardTag<"em">("em", serializer?.em),
		),
		listItem: useSerializerOrDefault<"listItem">(
			serializer?.listItem,
			serializeStandardTag<"listItem">("li", serializer?.listItem),
		),
		oListItem: useSerializerOrDefault<"oListItem">(
			serializer?.oListItem,
			serializeStandardTag<"oListItem">("li", serializer?.oListItem),
		),
		list: useSerializerOrDefault<"list">(
			serializer?.list,
			serializeStandardTag<"list">("ul", serializer?.list),
		),
		oList: useSerializerOrDefault<"oList">(
			serializer?.oList,
			serializeStandardTag<"oList">("ol", serializer?.oList),
		),
		image: useSerializerOrDefault<"image">(
			serializer?.image,
			serializeImage(linkResolver, serializer?.image),
		),
		embed: useSerializerOrDefault<"embed">(serializer?.embed, serializeEmbed(serializer?.embed)),
		hyperlink: useSerializerOrDefault<"hyperlink">(
			serializer?.hyperlink,
			serializeHyperlink(linkResolver, serializer?.hyperlink),
		),
		label: useSerializerOrDefault<"label">(
			serializer?.label,
			serializeStandardTag<"label">("span", serializer?.label),
		),
		span: useSerializerOrDefault<"span">(serializer?.span, serializeSpan()),
	}

	return wrapMapSerializerWithStringChildren(mapSerializer)
}

/**
 * Wraps a map serializer into a regular function serializer. The given map serializer should accept
 * children as a string, not as an array of strings like `@prismicio/client/richtext`'s
 * `wrapMapSerializer`.
 *
 * @param mapSerializer - Map serializer to wrap
 * @returns A regular function serializer
 */
const wrapMapSerializerWithStringChildren = (
	mapSerializer: HTMLStrictRichTextMapSerializer,
): RichTextFunctionSerializer<string> => {
	const modifiedMapSerializer = {} as RichTextMapSerializer<string>

	for (const tag in mapSerializer) {
		const tagSerializer = mapSerializer[tag as keyof typeof mapSerializer]

		if (tagSerializer) {
			modifiedMapSerializer[tag as keyof typeof mapSerializer] = (payload) => {
				return tagSerializer({
					...payload,
					// @ts-expect-error - merging blockSerializer types causes TS to bail to a never type
					children: payload.children.join(""),
				})
			}
		}
	}

	return wrapMapSerializer(modifiedMapSerializer)
}

/** Configuration that determines the output of `asHTML()`. */
type AsHTMLConfig = {
	/**
	 * An optional link resolver function to resolve links. Without it, you're expected to use the
	 * `routes` option from the API.
	 */
	linkResolver?: LinkResolverFunction | null

	/** An optional rich text serializer. Unhandled cases will fall back to the default serializer. */
	serializer?: HTMLRichTextSerializer | null
}

// TODO: Remove when we remove support for deprecated tuple-style configuration.
/** @deprecated Use object-style configuration instead. */
type AsHTMLDeprecatedTupleConfig = [
	linkResolver?: LinkResolverFunction | null,
	serializer?: HTMLRichTextSerializer | null,
]

/** The return type of `asHTML()`. */
type AsHTMLReturnType<Field extends RichTextField | null | undefined> = Field extends RichTextField
	? string
	: null

// TODO: Remove overload when we remove support for deprecated tuple-style configuration.
export const asHTML: {
	/**
	 * Converts a rich text field to an HTML string.
	 *
	 * @example
	 * 	;```ts
	 * 	const html = asHTML(document.data.content)
	 * 	// => "<p>Hello world</p>"
	 * 	```
	 *
	 * @param richTextField - A rich text field from Prismic.
	 * @param config - Configuration that determines the output of `asHTML()`.
	 * @returns HTML equivalent of the rich text field, or `null` if the field is
	 * empty.
	 * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text}
	 */
	<Field extends RichTextField | null | undefined>(
		richTextField: Field,
		config?: AsHTMLConfig,
	): AsHTMLReturnType<Field>

	/**
	 * Converts a rich text field to an HTML string.
	 *
	 * @deprecated Use object-style configuration instead.
	 * @param richTextField - A rich text field from Prismic.
	 * @param linkResolver - An optional link resolver function to resolve links. Without it, you're
	 *   expected to use the `routes` option from the API.
	 * @param serializer - An optional rich text serializer. Unhandled cases will fall back to the
	 *   default serializer.
	 * @returns HTML equivalent of the rich text field, or `null` if the field is
	 * empty.
	 * @see Learn how to style rich text and customize rendering: {@link https://prismic.io/docs/fields/rich-text}
	 */
	<Field extends RichTextField | null | undefined>(
		richTextField: Field,
		...config: AsHTMLDeprecatedTupleConfig
	): AsHTMLReturnType<Field>
} = <Field extends RichTextField | null | undefined>(
	richTextField: Field,
	// TODO: Rename to `config` when we remove support for deprecated tuple-style configuration.
	...configObjectOrTuple: [config?: AsHTMLConfig] | AsHTMLDeprecatedTupleConfig
): AsHTMLReturnType<Field> => {
	if (richTextField) {
		// TODO: Remove when we remove support for deprecated tuple-style configuration.
		const [configObjectOrLinkResolver, maybeSerializer] = configObjectOrTuple
		let config: AsHTMLConfig
		if (typeof configObjectOrLinkResolver === "function" || configObjectOrLinkResolver == null) {
			config = {
				linkResolver: configObjectOrLinkResolver,
				serializer: maybeSerializer,
			}
		} else {
			config = { ...configObjectOrLinkResolver }
		}

		let serializer: RichTextFunctionSerializer<string>
		if (config.serializer) {
			if (typeof config.serializer === "function") {
				serializer = composeSerializers(
					(type, node, text, children, key) =>
						// TypeScript doesn't narrow the type correctly here since it is now in a callback function, so we have to cast it here.
						(config.serializer as HTMLRichTextFunctionSerializer)(
							type,
							node,
							text,
							children.join(""),
							key,
						),
					createHTMLRichTextSerializer(config.linkResolver),
				)
			} else {
				serializer = createHTMLRichTextSerializer(config.linkResolver, config.serializer)
			}
		} else {
			serializer = createHTMLRichTextSerializer(config.linkResolver)
		}

		return serialize(richTextField, serializer).join("") as AsHTMLReturnType<Field>
	} else {
		return null as AsHTMLReturnType<Field>
	}
}
