import { clsx } from 'clsx';
import { ElementType, forwardRef, MouseEvent } from 'react';
import { useIntl } from 'react-intl';

import {
  Size,
  ControlType,
  Priority,
  ControlTypeAccent,
  ControlTypeNegative,
  ControlTypePositive,
  PriorityPrimary,
  PrioritySecondary,
  PriorityTertiary,
  SizeExtraSmall,
  SizeSmall,
  SizeMedium,
  SizeLarge,
} from '../common';
import ProcessIndicator from '../processIndicator';

import messages from './Button.messages';
import { typeClassMap, priorityClassMap } from './classMap';
import { establishNewPriority, establishNewType, logDeprecationNotices } from './legacyUtils';

/** @deprecated */
type DeprecatedTypes = 'primary' | 'pay' | 'secondary' | 'danger' | 'link';

/** @deprecated */
type DeprecatedSizes = SizeExtraSmall;

type CommonProps = {
  block?: boolean;
  disabled?: boolean;
  loading?: boolean;
  type?: ControlTypeAccent | ControlTypeNegative | ControlTypePositive | DeprecatedTypes | null;
  priority?: PriorityPrimary | PrioritySecondary | PriorityTertiary | null;
  size?: SizeSmall | SizeMedium | SizeLarge | DeprecatedSizes;
};

type ButtonProps = CommonProps &
  Omit<React.ComponentPropsWithRef<'button'>, 'type'> & {
    as?: 'button';
    htmlType?: 'submit' | 'reset' | 'button';
  };

type AnchorProps = CommonProps &
  Omit<React.ComponentPropsWithRef<'a'>, 'type'> & {
    as?: 'a';
  };

export type Props = ButtonProps | AnchorProps;

type ButtonReferenceType = HTMLButtonElement | HTMLAnchorElement;

const Button = forwardRef<ButtonReferenceType, Props>(
  (
    {
      as: component,
      block = false,
      children,
      className,
      disabled,
      loading = false,
      priority = Priority.PRIMARY,
      size = Size.MEDIUM,
      type = ControlType.ACCENT,
      onClick,
      ...rest
    }: Props,
    reference,
  ) => {
    const intl = useIntl();

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    logDeprecationNotices({ size, type });

    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const newType = establishNewType(type);
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const newPriority = establishNewPriority(priority, type);

    const classes = clsx(
      `btn btn-${size}`,
      `np-btn np-btn-${size}`,
      {
        'btn-loading': loading,
        'btn-block np-btn-block': block,
        disabled,
      },
      // @ts-expect-error fix when refactor `typeClassMap` to TypeScript
      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      typeClassMap[newType],
      // @ts-expect-error fix when refactor `typeClassMap` to TypeScript
      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      priorityClassMap[newPriority],
      className,
    );

    function processIndicatorSize() {
      return ['sm', 'xs'].includes(size) ? 'xxs' : 'xs';
    }

    const Element = (component as ElementType) ?? 'button';
    let props;

    if (Element === 'button') {
      const { htmlType = 'button', ...restProps } = rest as ButtonProps;
      props = {
        ...restProps,
        disabled,
        'aria-disabled': loading,
        type: htmlType,
      };
    } else {
      props = {
        ...rest,
        'aria-disabled': loading,
      } as AnchorProps;
    }

    /**
     * Ensures that the button cannot be activated in loading or disabled mode,
     * when `aria-disabled` might be used over the `disabled` HTML attribute
     */
    const handleClick =
      (handler: Props['onClick']) =>
      (event: MouseEvent<HTMLButtonElement> & MouseEvent<HTMLAnchorElement>) => {
        if (disabled || loading) {
          event.preventDefault();
        } else if (typeof handler === 'function') {
          handler(event);
        }
      };

    return (
      <Element
        ref={reference}
        className={classes}
        onClick={handleClick(onClick)}
        {...props}
        aria-live={loading ? 'polite' : 'off'}
        aria-busy={loading}
        aria-label={loading ? intl.formatMessage(messages.loadingAriaLabel) : rest['aria-label']}
      >
        {children}
        {loading && (
          <ProcessIndicator
            size={processIndicatorSize()}
            className="btn-loader"
            data-testid="ButtonProgressIndicator"
          />
        )}
      </Element>
    );
  },
);

export default Button;
