import {
	type LinkField,
	type LinkResolverFunction,
	type PrismicDocument,
	asLinkAttrs,
	type AsLinkAttrsConfig,
} from "@prismicio/client";
import { DEV } from "esm-env";
import {
	type ComponentProps,
	type ComponentType,
	type ForwardedRef,
	type HTMLAttributeAnchorTarget,
	type ReactNode,
	forwardRef,
} from "react";

import { devMsg } from "./lib/devMsg.js";

/** The default component rendered for internal and external links. */
const defaultComponent = "a";

/** Props provided to a component when rendered with `<PrismicLink>`. */
export interface LinkProps {
	/** The URL to link. */
	href: string;

	/**
	 * The `target` attribute for anchor elements. If the Prismic field is configured to open in a new
	 * window, this prop defaults to `_blank`.
	 */
	target?: HTMLAttributeAnchorTarget;

	/**
	 * The `rel` attribute for anchor elements. If the `target` prop is set to `"_blank"`, this prop
	 * defaults to `"noopener noreferrer"`.
	 */
	rel?: string;

	/** Children for the component. */
	children?: ReactNode;
}

export type PrismicLinkProps<
	InternalComponentProps = ComponentProps<typeof defaultComponent>,
	ExternalComponentProps = ComponentProps<typeof defaultComponent>,
> = Omit<InternalComponentProps & ExternalComponentProps, "rel" | "href" | "children"> & {
	/**
	 * The `rel` attribute for the link. By default, `"noreferrer"` is provided if the link's URL is
	 * external. This prop can be provided a function to use the link's metadata to determine the
	 * `rel` value.
	 */
	rel?: string | AsLinkAttrsConfig["rel"];

	/**
	 * The link resolver used to resolve links.
	 *
	 * @remarks
	 *   If your app uses route resolvers when querying for your Prismic repository's content, a link
	 *   resolver does not need to be provided.
	 * @see Learn about link resolvers and route resolvers {@link https://prismic.io/docs/routes}
	 */
	linkResolver?: LinkResolverFunction;

	/**
	 * The component rendered for internal URLs. Defaults to `<a>`.
	 *
	 * If your app uses a client-side router that requires a special Link component, provide the Link
	 * component to this prop.
	 */
	internalComponent?: ComponentType<InternalComponentProps>;

	/** The component rendered for external URLs. Defaults to `<a>`. */
	externalComponent?: ComponentType<ExternalComponentProps>;

	/**
	 * The children to render for the link. If no children are provided, the link's `text` property
	 * will be used.
	 */
	children?: ReactNode;
} & (
		| {
				document: PrismicDocument | null | undefined;
				href?: never;
				field?: never;
		  }
		| {
				field: LinkField | null | undefined;
				href?: never;
				document?: never;
		  }
		| {
				href: LinkProps["href"];
				field?: LinkField | null | undefined;
				document?: never;
		  }
	);

/**
 * Renders a link from a Prismic link field or page.
 *
 * @example
 * 	```tsx
 * 	<PrismicLink field={slice.primary.link}>Click here</PrismicLink>;
 * 	```
 *
 * @see Learn how to display links and use variants for styling: {@link https://prismic.io/docs/fields/link}
 */
export const PrismicLink = forwardRef(function PrismicLink<
	InternalComponentProps = ComponentProps<typeof defaultComponent>,
	ExternalComponentProps = ComponentProps<typeof defaultComponent>,
>(
	props: PrismicLinkProps<InternalComponentProps, ExternalComponentProps>,
	ref: ForwardedRef<Element>,
) {
	const {
		field,
		document: doc,
		linkResolver,
		internalComponent,
		externalComponent,
		children,
		...restProps
	} = props;

	if (DEV) {
		if (field) {
			if (!field.link_type) {
				console.error(
					`[PrismicLink] This "field" prop value caused an error to be thrown.\n`,
					field,
				);
				throw new Error(
					`[PrismicLink] The provided field is missing required properties to properly render a link. The link will not render. For more details, see ${devMsg(
						"missing-link-properties",
					)}`,
				);
			} else if (
				("text" in field ? Object.keys(field).length > 2 : Object.keys(field).length > 1) &&
				!("url" in field || "uid" in field || "id" in field)
			) {
				console.warn(
					`[PrismicLink] The provided field is missing required properties to properly render a link. The link may not render correctly. For more details, see ${devMsg(
						"missing-link-properties",
					)}`,
					field,
				);
			}
		} else if (doc) {
			if (!("url" in doc || "id" in doc)) {
				console.warn(
					`[PrismicLink] The provided document is missing required properties to properly render a link. The link may not render correctly. For more details, see ${devMsg(
						"missing-link-properties",
					)}`,
					doc,
				);
			}
		}
	}

	const {
		href: computedHref,
		rel: computedRel,
		...attrs
	} = asLinkAttrs(field ?? doc, {
		linkResolver,
		rel: typeof restProps.rel === "function" ? restProps.rel : undefined,
	});

	let rel: string | undefined = computedRel;
	if ("rel" in restProps && typeof restProps.rel !== "function") {
		rel = restProps.rel;
	}

	const href = ("href" in restProps ? restProps.href : computedHref) || "";

	const InternalComponent = (internalComponent || defaultComponent) as ComponentType<LinkProps>;
	const ExternalComponent = (externalComponent || defaultComponent) as ComponentType<LinkProps>;
	const Component = href
		? isInternalURL(href)
			? InternalComponent
			: ExternalComponent
		: InternalComponent;

	return (
		<Component ref={ref} {...attrs} {...restProps} href={href} rel={rel}>
			{"children" in props ? children : field?.text}
		</Component>
	);
}) as <
	InternalComponentProps = ComponentProps<typeof defaultComponent>,
	ExternalComponentProps = ComponentProps<typeof defaultComponent>,
>(
	props: PrismicLinkProps<InternalComponentProps, ExternalComponentProps> & {
		ref?: ForwardedRef<Element>;
	},
) => ReactNode;

/**
 * Determines if a URL is internal or external.
 *
 * @param url - The URL to check if internal or external.
 * @returns `true` if `url` is internal, `false` otherwise.
 */
// TODO: This does not detect all relative URLs as internal such as `about` or `./about`. This function assumes relative URLs start with a "/" or "#"`.
export function isInternalURL(url: string): boolean {
	const isInternal = /^(\/(?!\/)|#)/.test(url);
	const isSpecialLink = !isInternal && !/^https?:\/\//.test(url);

	return isInternal && !isSpecialLink;
}
