import {
	type ComponentPropsWithoutRef,
	type ComponentType,
	createContext,
	type ReactNode,
	type Ref,
	type SVGProps,
	useContext,
} from 'react';

import { Button as BaseButton } from '@base-ui/react/button';
import { clsx } from 'clsx';

import { Spinner } from '#/components/Spinner';
import * as styles from '#/components/web/Button.css';

import type { RecipeVariants } from '#/styles/recipe';
import type { iconSize } from '#/styles/tokens.css';

type ButtonVariants = RecipeVariants<typeof styles.button>;

export type ButtonProps = Omit<ComponentPropsWithoutRef<typeof BaseButton>, 'className' | 'color'> & {
	/** Accessible name; becomes the `aria-label`. */
	label: string;
	variant?: ButtonVariants['variant'];
	color?: ButtonVariants['color'];
	size?: ButtonVariants['size'];
	shape?: ButtonVariants['shape'];
	className?: string;
	/** Forwarded to the underlying `<button>` so the Button can back a `Dialog.Trigger`. */
	ref?: Ref<HTMLButtonElement>;
};

// resolved (defaulted) geometry the Button shares with its ButtonIcon children. mirrors the recipe's
// defaultVariants so ButtonIcon can size its box and negative margin without re-reading the recipe.
type ButtonContextValue = {
	shape: NonNullable<ButtonVariants['shape']>;
	size: NonNullable<ButtonVariants['size']>;
};

const ButtonContext = createContext<ButtonContextValue | null>(null);
ButtonContext.displayName = 'ButtonContext';

/** The web-native button primitive, built on Base UI's headless `<button>`. */
export function Button({
	label,
	variant,
	color,
	size = 'small',
	shape = 'default',
	className,
	children,
	...rest
}: ButtonProps) {
	return (
		<BaseButton
			aria-label={label}
			className={clsx(styles.button({ color, shape, size, variant }), className)}
			{...rest}
		>
			<ButtonContext.Provider value={{ shape, size }}>{children}</ButtonContext.Provider>
		</BaseButton>
	);
}

/** Button label text. Inherits the button's color; `size` overrides the inherited font size. */
export function ButtonText({ children, size }: { children: ReactNode; size?: keyof typeof styles.textSize }) {
	// renders a web <span> that inherits the button's color/size — the RN unwrapped-text rule doesn't model this
	// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
	return <span className={size ? styles.textSize[size] : undefined}>{children}</span>;
}

export type ButtonIconProps = {
	icon: ComponentType<SVGProps<SVGSVGElement>>;
	size?: keyof typeof iconSize;
};

// default icon token per button size, so a large/tiny button's icon tracks its text rather than always
// rendering at the `small` scale.
const DEFAULT_ICON_SIZE: Record<ButtonContextValue['size'], keyof typeof iconSize> = {
	large: 'md',
	small: 'sm',
	tiny: 'xs',
};

const useIconBox = (size: keyof typeof iconSize | undefined) => {
	const ctx = useContext(ButtonContext);
	if (!ctx) {
		throw new Error('ButtonIcon must be rendered inside a Button');
	}

	const resolvedSize = size ?? DEFAULT_ICON_SIZE[ctx.size];
	return {
		className: styles.iconBox({
			narrow: resolvedSize === '_2xs',
			pull: ctx.shape !== 'round',
			size: ctx.size,
		}),
		iconToken: resolvedSize,
	};
};

/** Renders an icon that inherits the button's text color via `currentColor`. */
export function ButtonIcon({ icon: Icon, size }: ButtonIconProps) {
	const { className, iconToken } = useIconBox(size);
	return (
		<span className={className}>
			<Icon className={styles.icon[iconToken]} />
		</span>
	);
}

export type ButtonSpinnerProps = {
	color?: 'white' | 'default';
	label: string;
	size?: keyof typeof iconSize;
};

export function ButtonSpinner({ color, label, size }: ButtonSpinnerProps) {
	const { className, iconToken } = useIconBox(size);
	return (
		<span className={className}>
			<Spinner color={color} label={label} size={iconToken} />
		</span>
	);
}
