// @design-system: primitives/Button
// Registered at /design-system#button-primitive - Phase 3 (Agent 3E) will render the gallery.

import { Slot } from '@radix-ui/react-slot';
import type { VariantProps } from 'class-variance-authority';
import {
  Children,
  cloneElement,
  forwardRef,
  isValidElement,
  type ReactElement,
  type ReactNode,
} from 'react';
import { cn } from '@/lib/cn';
import { buttonVariants } from './variants';

/** Props for the Button component */
export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
  /** When true, renders the Radix Slot (merges props onto child element). */
  asChild?: boolean;
  /**
   * Marks the button as busy: disables interaction and sets `aria-busy="true"`.
   *
   * Multideal hard rule: spinners NEVER render inline inside buttons. For visual
   * feedback during async work, drive the global `<LoadingOverlay />` via
   * `useLoadingOverlay().show(label)` / `.hide()` instead.
   */
  loading?: boolean;
  /** Icon rendered at the inline-start of the button label. */
  iconStart?: ReactNode;
  /** Icon rendered at the inline-end of the button label. */
  iconEnd?: ReactNode;
}

/**
 * Multideal Button primitive.
 *
 * Variants: `primary` (blue brand) | `secondary` (light blue) | `ghost` | `ghost-inverted` | `danger`
 * Sizes: `sm` | `md` | `lg`
 *
 * Supports `asChild` (Radix Slot), `loading` (disables + sets `aria-busy`; visual
 * spinner is provided by the global `<LoadingOverlay />`, NOT inline), and icon slots.
 *
 * When asChild is true, the rendered element MUST be a button-role element
 * (`<button>`, `<a>`, or component forwarding to one). Slot does not enforce
 * the role — passing a non-interactive child loses keyboard + screen-reader
 * semantics.
 *
 * @example
 * ```tsx
 * const { show, hide } = useLoadingOverlay();
 * const onSubmit = async () => {
 *   show(t('saving'));
 *   try { await save(); } finally { hide(); }
 * };
 * <Button variant="primary" loading={mutation.isPending} onClick={onSubmit}>
 *   {t('save')}
 * </Button>
 * ```
 */
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  {
    className,
    variant,
    size,
    asChild = false,
    loading = false,
    iconStart,
    iconEnd,
    disabled,
    children,
    ...props
  },
  ref,
) {
  const isDisabled = disabled || loading;
  const classes = cn(buttonVariants({ variant, size }), className);

  // Hard rule: NEVER render an inline spinner inside a Button. `loading` only sets
  // `disabled` + `aria-busy`. Visual feedback comes from the global LoadingOverlay.
  const iconStartNode = iconStart ? (
    <span className="shrink-0" aria-hidden="true">
      {iconStart}
    </span>
  ) : null;
  const iconEndNode = iconEnd ? (
    <span className="shrink-0" aria-hidden="true">
      {iconEnd}
    </span>
  ) : null;

  // asChild mode uses Radix Slot, which requires exactly one React element child.
  // Inject icon spans by cloning the consumer's element and re-wrapping its children,
  // preserving href/onClick/etc. so the rendered markup is e.g. <a><span/>text<span/></a>.
  if (asChild) {
    const child = Children.only(children);
    if (!isValidElement(child)) {
      return null;
    }
    const childEl = child as ReactElement<{ children?: ReactNode }>;
    const merged = cloneElement(childEl, {}, iconStartNode, childEl.props.children, iconEndNode);
    return (
      <Slot
        ref={ref}
        className={classes}
        aria-busy={loading || undefined}
        data-fx="button"
        data-fx-variant={variant ?? 'primary'}
        {...props}
      >
        {merged}
      </Slot>
    );
  }

  return (
    <button
      ref={ref}
      className={classes}
      disabled={isDisabled}
      aria-busy={loading || undefined}
      data-fx="button"
      data-fx-variant={variant ?? 'primary'}
      {...props}
      type={props.type ?? 'button'}
    >
      {iconStartNode}
      {children}
      {iconEndNode}
    </button>
  );
});
