'use no memo'; // forwarded composition props invalidate the generated wrapper caches

import type { ComponentPropsWithoutRef, MouseEvent, ReactNode, Ref } from 'react';

import { assignInlineVars } from '@vanilla-extract/dynamic';
import { clsx } from 'clsx';

import { isMisleadingLink } from '#/lib/links/trust';
import { resolveUrl } from '#/lib/routes/app-links';
import { safeUrlParse } from '#/lib/utils/url';

import { groupChatJoinHandle, linkWarningDialogHandle } from '#/components/dialogs/handles';
import { useNavigationDisabled } from '#/components/NavigationDisabled';
import type { TextProps } from '#/components/Text';
import * as textStyles from '#/components/Text.css';
import { Button, type ButtonProps } from '#/components/web/Button';
import * as styles from '#/components/web/Link.css';

import { type RouteTarget, useRouter } from '#/router';

type AppRouter = ReturnType<typeof useRouter>;

// #region hooks

/** how a link navigates when pressed. `navigate` is a plain push; singleton routes dedupe themselves. */
type LinkAction = 'navigate' | 'push' | 'replace';

/**
 * click callback run before navigation. return `false` to cancel navigation. do not navigate from this
 * callback.
 */
type LinkOnPress = (e: MouseEvent<HTMLElement>) => false | void;

type LinkBindings = {
	href: string | undefined;
	onClick: (e: MouseEvent<HTMLElement>) => void;
	rel?: string;
	target?: string;
};

/**
 * determines whether a click should defer to the browser's default action (such as opening in a new tab)
 * rather than being intercepted for client-side navigation.
 *
 * @param event the click event to check
 */
export const isModifiedClick = (e: MouseEvent<HTMLElement>) => {
	return e.altKey || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey;
};

/**
 * navigates to an in-app route `path` via the given {@link LinkAction}.
 *
 * @param router the router to navigate
 * @param path the in-app path
 * @param action how the destination enters history
 * @param state structured-cloneable history state
 */
export const navigateTo = (router: AppRouter, path: string, action: LinkAction, state?: unknown): void => {
	router.navigate({ replace: action === 'replace', state, to: path });
};

/** Returns a function that navigates to an in-app route `path` via the given {@link LinkAction}. */
export const useNavigateToPath = () => {
	const router = useRouter();
	return (path: string, action: LinkAction) => navigateTo(router, path, action);
};

/**
 * resolves the href and onClick bindings for an in-app route link without rendering an anchor. this allows
 * non-anchor elements to preserve link semantics (e.g., middle/cmd-click to open in a new tab, plain click to
 * navigate). return false from onPress to cancel navigation.
 */
export const useInternalLink = ({
	action = 'push',
	onPress,
	to,
}: {
	action?: LinkAction;
	onPress?: LinkOnPress;
	to: RouteTarget;
}): LinkBindings => {
	const router = useRouter();
	const path = router.href(to);
	const onClick = (e: MouseEvent<HTMLElement>) => {
		if (onPress?.(e) === false) {
			e.preventDefault();
			return;
		}
		if (isModifiedClick(e)) {
			return;
		}
		e.preventDefault();
		navigateTo(router, path, action);
	};
	return { href: path, onClick };
};

// shared core for raw-URL links. `navigate` handles a plain click on a resolved internal href, and leaves an
// external or modified click to the native anchor.
const useExternalNav = (rawHref: string, action: LinkAction) => {
	const navigateToPath = useNavigateToPath();

	const resolved = resolveUrl(rawHref);
	const href = resolved ? resolved.path : safeUrlParse(rawHref)?.href;

	const isExternal = resolved === undefined;

	const navigate = (e: MouseEvent<HTMLElement>) => {
		if (!href || isExternal || isModifiedClick(e)) {
			return;
		}

		e.preventDefault();

		// a group-chat invite opens the join dialog in place rather than navigating to /chat/<code>.
		const link = resolved?.link;
		if (link?.kind === 'chatInvite') {
			groupChatJoinHandle.openWithPayload({ code: link.code });
			return;
		}

		navigateToPath(href, action);
	};

	return { href, isExternal, navigate };
};

const useExternalLink = ({
	action = 'push',
	href: rawHref,
	onPress,
}: {
	action?: LinkAction;
	href: string;
	onPress?: LinkOnPress;
}): LinkBindings => {
	const { href, isExternal, navigate } = useExternalNav(rawHref, action);
	const onClick = (e: MouseEvent<HTMLElement>) => {
		if (onPress?.(e) === false) {
			e.preventDefault();
			return;
		}
		navigate(e);
	};
	return {
		href,
		onClick,
		// external links open in a new tab; an `<a>` to a resolved in-app path needs neither.
		rel: isExternal ? 'noopener noreferrer' : undefined,
		target: isExternal ? '_blank' : undefined,
	};
};

// like useExternalLink, but for links whose visible text is untrusted (post content): before navigating it
// verifies the text against the destination and, on a mismatch, opens the warning dialog instead.
const useContentLink = ({
	action = 'push',
	displayText,
	href: rawHref,
	onPress,
}: {
	action?: LinkAction;
	displayText: string;
	href: string;
	onPress?: LinkOnPress;
}): LinkBindings => {
	const { href, isExternal, navigate } = useExternalNav(rawHref, action);
	const onClick = (e: MouseEvent<HTMLElement>) => {
		if (onPress?.(e) === false) {
			e.preventDefault();
			return;
		}

		if (!href) {
			return;
		}

		if (displayText && isExternal && isMisleadingLink(href, displayText)) {
			e.preventDefault();
			linkWarningDialogHandle.openWithPayload({ displayText, href });
			return;
		}

		navigate(e);
	};
	return {
		href,
		onClick,
		// external links open in a new tab; an `<a>` to a resolved in-app path needs neither.
		rel: isExternal ? 'noopener noreferrer' : undefined,
		target: isExternal ? '_blank' : undefined,
	};
};

// #endregion

// #region presentational shells

type BlockAnchorProps = Omit<ComponentPropsWithoutRef<'a'>, 'href' | 'onClick' | 'rel' | 'target'> & {
	children: ReactNode;
	/** Accessible name; becomes the anchor's `aria-label`. */
	label?: string;
	/** Forwarded to the `<a>` so the link can back a headless trigger (e.g. a hover card). */
	ref?: Ref<HTMLAnchorElement>;
};

const useAnchorProps = (bindings: LinkBindings) => {
	if (useNavigationDisabled()) {
		return undefined;
	}
	return {
		href: bindings.href,
		onClick: bindings.onClick,
		rel: bindings.rel,
		target: bindings.target,
	};
};

// a block `<a>` wrapping arbitrary children with no styling of its own — pass `className` for layout.
const BlockAnchor = ({
	bindings,
	children,
	label,
	...rest
}: { bindings: LinkBindings } & BlockAnchorProps) => (
	<a {...rest} {...useAnchorProps(bindings)} aria-label={label}>
		{children}
	</a>
);

/** Underline timing for an inline link: `hover` (default), `always`, or `none`. */
export type InlineLinkUnderline = 'always' | 'hover' | 'none';

type InlineAnchorProps = Pick<
	TextProps,
	'align' | 'color' | 'leading' | 'numberOfLines' | 'selectable' | 'size' | 'weight'
> &
	Omit<ComponentPropsWithoutRef<'a'>, 'color' | 'href' | 'onClick' | 'rel' | 'style' | 'target'> & {
		children: ReactNode;
		/** Accessible name; becomes the anchor's `aria-label`. */
		label?: string;
		/** Forwarded to the `<a>` so the link can back a headless trigger (e.g. a Base UI tooltip). */
		ref?: Ref<HTMLAnchorElement>;
		/** Underline timing; defaults to `hover`. */
		underline?: InlineLinkUnderline;
	};

// resolves the className + inline-var style shared by inline elements styled through the text recipe with the
// inline-link treatment (an `<a>` or a `<button>`).
const inlineTextStyle = ({
	align,
	className,
	color,
	leading,
	numberOfLines,
	selectable,
	size,
	underline,
	weight,
}: Pick<
	InlineAnchorProps,
	| 'align'
	| 'className'
	| 'color'
	| 'leading'
	| 'numberOfLines'
	| 'selectable'
	| 'size'
	| 'underline'
	| 'weight'
>) => {
	const clamped = numberOfLines != null;
	const singleLine = numberOfLines === 1;
	return {
		className: clsx(
			textStyles.text({ align, color, leading, size, weight }),
			styles.inlineLink({ underline }),
			clamped && (singleLine ? textStyles.clampSingleLine : textStyles.clampMultiLine),
			selectable === true && textStyles.userSelect.text,
			selectable === false && textStyles.userSelect.none,
			className,
		),
		style:
			clamped && !singleLine
				? assignInlineVars({ [textStyles.lineClampVar]: String(numberOfLines) })
				: undefined,
	};
};

// an inline text `<a>` styled through the text recipe, defaulting to the primary color with an underline on
// hover/focus.
const InlineAnchor = ({
	align,
	bindings,
	children,
	className,
	color = 'textLink',
	label,
	leading,
	numberOfLines,
	selectable,
	size,
	underline = 'hover',
	weight,
	...rest
}: { bindings: LinkBindings } & InlineAnchorProps) => {
	const styled = inlineTextStyle({
		align,
		className,
		color,
		leading,
		numberOfLines,
		selectable,
		size,
		underline,
		weight,
	});

	return (
		<a
			{...rest}
			{...useAnchorProps(bindings)}
			aria-label={label}
			className={styled.className}
			style={styled.style}
		>
			{children}
		</a>
	);
};

type ButtonAnchorProps = Omit<ButtonProps, 'nativeButton' | 'onClick' | 'render'>;

// a {@link Button} that renders an `<a>`, so it keeps real anchor semantics (middle/cmd-click, copy link
// address) while looking and laying out like a button.
const ButtonAnchor = ({ bindings, children, ...rest }: { bindings: LinkBindings } & ButtonAnchorProps) => {
	return (
		<Button
			{...rest}
			nativeButton={false}
			onClick={bindings.onClick}
			render={<a href={bindings.href} rel={bindings.rel} target={bindings.target} />}
		>
			{children}
		</Button>
	);
};

// #endregion

// #region internal links — `to` is an in-app route path

type InternalNavProps = {
	/** How the link navigates when pressed. */
	action?: LinkAction;
	onPress?: LinkOnPress;
	/** The in-app destination, e.g. `{ name: 'Profile', actor }`. */
	to: RouteTarget;
};

export type LinkProps = InternalNavProps & BlockAnchorProps;
export type InlineLinkTextProps = InternalNavProps & InlineAnchorProps;
export type LinkButtonProps = InternalNavProps & ButtonAnchorProps;

/**
 * a block link to an in-app route that navigates using the router on click, falling back to browser-native
 * behavior on modified clicks.
 *
 * @param onPress callback to intercept the navigation; return false to cancel it.
 */
export const Link = ({ action, onPress, to, ...rest }: LinkProps) => {
	const bindings = useInternalLink({ action, onPress, to });
	return <BlockAnchor bindings={bindings} {...rest} />;
};

/** A web-native inline text link to an in-app route, styled through the text recipe. */
export const InlineLinkText = ({ action, onPress, to, ...rest }: InlineLinkTextProps) => {
	const bindings = useInternalLink({ action, onPress, to });
	return <InlineAnchor bindings={bindings} {...rest} />;
};

/** A web-native in-app-route link styled as a {@link Button}; renders an `<a>` so it keeps anchor semantics. */
export const LinkButton = ({ action, onPress, to, ...rest }: LinkButtonProps) => {
	const bindings = useInternalLink({ action, onPress, to });
	return <ButtonAnchor bindings={bindings} {...rest} />;
};

// #endregion

// #region external links — `href` is a raw URL

type ExternalNavProps = {
	/** How the link navigates when an `href` that resolves in-app is pressed. */
	action?: LinkAction;
	/** A raw URL. A known client's URL is collapsed to an in-app path; anything else opens in a new tab. */
	href: string;
	onPress?: LinkOnPress;
};

export type ExternalLinkProps = ExternalNavProps & BlockAnchorProps;
export type ExternalInlineLinkTextProps = ExternalNavProps & InlineAnchorProps;
export type ExternalLinkButtonProps = ExternalNavProps & ButtonAnchorProps;

/** opens a raw URL in a new tab, or navigates internally if it resolves to an in-app route. */
export const ExternalLink = ({ action, href, onPress, ...rest }: ExternalLinkProps) => {
	const bindings = useExternalLink({ action, href, onPress });
	return <BlockAnchor bindings={bindings} {...rest} />;
};

/** A web-native inline text link to a raw URL, behaving like {@link ExternalLink}. */
export const ExternalInlineLinkText = ({ action, href, onPress, ...rest }: ExternalInlineLinkTextProps) => {
	const bindings = useExternalLink({ action, href, onPress });
	return <InlineAnchor bindings={bindings} {...rest} />;
};

/** A web-native raw-URL link styled as a {@link Button}; renders an `<a>` so it keeps anchor semantics. */
export const ExternalLinkButton = ({ action, href, onPress, ...rest }: ExternalLinkButtonProps) => {
	const bindings = useExternalLink({ action, href, onPress });
	return <ButtonAnchor bindings={bindings} {...rest} />;
};

// #endregion

// #region content links — `href` is a raw URL whose visible text is untrusted

export type ContentLinkTextProps = ExternalNavProps & InlineAnchorProps;

/**
 * an inline text link for untrusted content. behaves like {@link ExternalInlineLinkText}, but verifies
 * attacker-controlled children against the destination URL and opens a link-warning dialog on mismatch.
 */
export const ContentLinkText = ({ action, href, onPress, ...rest }: ContentLinkTextProps) => {
	const bindings = useContentLink({
		action,
		displayText: typeof rest.children === 'string' ? rest.children : '',
		href,
		onPress,
	});
	return <InlineAnchor bindings={bindings} {...rest} />;
};

// #endregion

// #region inline button — an in-place action styled like inline link text

export type InlineButtonProps = Pick<TextProps, 'align' | 'color' | 'leading' | 'size' | 'weight'> &
	Omit<ComponentPropsWithoutRef<'button'>, 'color' | 'style'> & {
		children: ReactNode;
		/** Accessible name; becomes the button's `aria-label`. */
		label?: string;
		/** Forwarded to the `<button>` so it can back a headless trigger (e.g. a Base UI dialog). */
		ref?: Ref<HTMLButtonElement>;
		/** Underline timing; defaults to `hover`. */
		underline?: InlineLinkUnderline;
	};

/**
 * a web-native `<button>` styled identically to {@link InlineLinkText}, for in-place actions that sit inline
 * in text flow (e.g. an "add feeds" action inside a sentence). use this instead of a dummy-target link for
 * actions that do not navigate.
 */
export const InlineButton = ({
	align,
	children,
	className,
	color = 'textLink',
	label,
	leading,
	size,
	type = 'button',
	underline = 'hover',
	weight,
	...rest
}: InlineButtonProps) => {
	const styled = inlineTextStyle({ align, className, color, leading, size, underline, weight });
	return (
		<button {...rest} aria-label={label} className={styled.className} style={styled.style} type={type}>
			{children}
		</button>
	);
};

// #endregion
