import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-500/40 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-slate-900 text-slate-50 shadow-sm hover:bg-slate-800",
        secondary:
          "border border-slate-200 bg-slate-100 text-slate-700 hover:bg-slate-200",
        destructive: "bg-red-600 text-white hover:bg-red-500",
        outline:
          "border border-slate-300 bg-white text-slate-700 hover:bg-slate-100",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 px-3",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  },
);

type ButtonVariantProps = VariantProps<typeof buttonVariants>;
type EventHandler = (...args: unknown[]) => unknown;

type SlottableElementProps = {
  className?: string;
  style?: React.CSSProperties;
} & Record<string, unknown>;

type ButtonAsButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
  ButtonVariantProps & {
    asChild?: false;
  };

type ButtonAsChildProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> &
  ButtonVariantProps & {
    asChild: true;
    children: React.ReactElement<SlottableElementProps>;
  };

export type ButtonProps = ButtonAsButtonProps | ButtonAsChildProps;

type LocalSlotProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
  children: React.ReactElement<SlottableElementProps>;
};

function isEventHandlerKey(key: string): boolean {
  return /^on[A-Z]/.test(key);
}

function isEventWithDefaultPrevented(value: unknown): value is {
  defaultPrevented: boolean;
} {
  return (
    typeof value === "object" &&
    value !== null &&
    "defaultPrevented" in value &&
    typeof (value as { defaultPrevented?: unknown }).defaultPrevented ===
      "boolean"
  );
}

function composeEventHandlers(
  childHandler?: EventHandler,
  slotHandler?: EventHandler,
): EventHandler | undefined {
  if (!childHandler) return slotHandler;
  if (!slotHandler) return childHandler;

  return (...args: unknown[]) => {
    childHandler(...args);
    const firstArg = args[0];
    if (
      isEventWithDefaultPrevented(firstArg) &&
      firstArg.defaultPrevented
    ) {
      return;
    }
    slotHandler(...args);
  };
}

function mergeRefs<T>(
  ...refs: Array<React.Ref<T> | undefined>
): React.RefCallback<T> {
  return (value) => {
    for (const ref of refs) {
      if (!ref) continue;
      if (typeof ref === "function") {
        ref(value);
      } else {
        (ref as React.MutableRefObject<T | null>).current = value;
      }
    }
  };
}

function mergeSlotProps(
  slotProps: Record<string, unknown>,
  childProps: Record<string, unknown>,
): Record<string, unknown> {
  const merged: Record<string, unknown> = { ...slotProps, ...childProps };

  for (const [key, childValue] of Object.entries(childProps)) {
    const slotValue = slotProps[key];
    if (
      isEventHandlerKey(key) &&
      typeof childValue === "function" &&
      typeof slotValue === "function"
    ) {
      merged[key] = composeEventHandlers(
        childValue as EventHandler,
        slotValue as EventHandler,
      );
    }
  }

  const slotClassName =
    typeof slotProps.className === "string" ? slotProps.className : undefined;
  const childClassName =
    typeof childProps.className === "string" ? childProps.className : undefined;
  if (slotClassName || childClassName) {
    merged.className = cn(slotClassName, childClassName);
  }

  const slotStyle = slotProps.style;
  const childStyle = childProps.style;
  if (
    typeof slotStyle === "object" &&
    slotStyle !== null &&
    typeof childStyle === "object" &&
    childStyle !== null
  ) {
    merged.style = {
      ...(slotStyle as React.CSSProperties),
      ...(childStyle as React.CSSProperties),
    };
  }

  return merged;
}

const LocalSlot = React.forwardRef<HTMLElement, LocalSlotProps>(
  ({ children, ...slotProps }, ref) => {
    const element = children as React.ReactElement<SlottableElementProps> & {
      ref?: React.Ref<HTMLElement>;
    };
    const mergedProps = mergeSlotProps(
      slotProps as Record<string, unknown>,
      element.props as Record<string, unknown>,
    );

    return React.cloneElement(element, {
      ...mergedProps,
      ref: mergeRefs(ref, element.ref),
    });
  },
);
LocalSlot.displayName = "LocalSlot";

export const Button = React.forwardRef<HTMLElement, ButtonProps>(
  (props, ref) => {
    const classes = cn(
      buttonVariants({
        variant: props.variant,
        size: props.size,
        className: props.className,
      }),
    );

    if (props.asChild) {
      const {
        asChild: _asChild,
        variant: _variant,
        size: _size,
        className: _className,
        children,
        ...slotProps
      } = props;

      return (
        <LocalSlot ref={ref} className={classes} {...slotProps}>
          {children}
        </LocalSlot>
      );
    }

    const {
      asChild: _asChild,
      variant: _variant,
      size: _size,
      className: _className,
      children,
      ...buttonProps
    } = props;

    return (
      <button
        className={classes}
        ref={ref as React.Ref<HTMLButtonElement>}
        {...buttonProps}
      >
        {children}
      </button>
    );
  },
);
Button.displayName = "Button";
