import { isEmpty, isNumber, isNull } from '@transferwise/neptune-validation';
import { Flag } from '@wise/art';
import { clsx } from 'clsx';
import { Component } from 'react';
import { injectIntl, WrappedComponentProps } from 'react-intl';

import {
  Typography,
  Size,
  SizeLarge,
  SizeMedium,
  SizeSmall,
  getLocaleCurrencyName,
} from '../common';
import { withInputAttributes, WithInputAttributesProps } from '../inputs/contexts';
import { Input } from '../inputs/Input';
import {
  SelectInput,
  SelectInputItem,
  SelectInputOptionContent,
  SelectInputOptionItem,
  SelectInputProps,
} from '../inputs/SelectInput';
import Title from '../title';

import messages from './MoneyInput.messages';
import { formatAmount, formatNumber, getCurrencyDecimals, parseAmount } from './currencyFormatting';
import withId from '../withId';

export interface CurrencyOptionItem {
  header?: never;
  value: string;
  label: string;
  currency: string;
  note?: string;
  searchable?: string;
}

export interface CurrencyHeaderItem {
  header: string;
}

export type CurrencyItem = CurrencyOptionItem | CurrencyHeaderItem;

const isNumberOrNull = (v: unknown): v is number | null => isNumber(v) || isNull(v);

const formatAmountIfSet = ({
  amount,
  currency,
  locale,
  decimals,
}: {
  amount: number | null | undefined;
  currency: string;
  locale: string;
  decimals?: number;
}) => {
  if (typeof amount !== 'number') {
    return '';
  }
  if (decimals != null && getCurrencyDecimals(currency) !== 0) {
    return formatNumber(amount, locale, decimals);
  }
  return formatAmount(amount, currency, locale);
};

const parseNumber = ({
  amount,
  currency,
  locale,
  decimals,
}: {
  amount: string;
  currency: string;
  locale: string;
  decimals?: number;
}) => {
  return parseAmount(amount, currency, locale, decimals);
};

const allowedInputKeys = new Set([
  'Backspace',
  'Delete',
  ',',
  '.',
  'ArrowDown',
  'ArrowUp',
  'ArrowLeft',
  'ArrowRight',
  'Enter',
  'Escape',
  'Tab',
]);

export interface MoneyInputProps extends WrappedComponentProps {
  id?: string;
  'aria-labelledby'?: string;
  currencies: readonly CurrencyItem[];
  selectedCurrency: CurrencyOptionItem;
  onCurrencyChange?: (value: CurrencyOptionItem) => void;
  placeholder?: number;
  amount: number | null;
  size?: SizeSmall | SizeMedium | SizeLarge;
  onAmountChange?: (value: number | null) => void;
  addon?: React.ReactNode;
  searchPlaceholder?: string;
  /**
   * Allows the consumer to react to searching, while the search itself is handled internally.
   */
  onSearchChange?: (value: { searchQuery: string; filteredOptions: CurrencyItem[] }) => void;
  customActionLabel?: React.ReactNode;
  onCustomAction?: () => void;
  classNames?: Record<string, string>;
  selectProps?: Partial<SelectInputProps<CurrencyOptionItem>>;
  /**
   * Specify the number of decimal places to format the amount. When not specified, the number of
   * decimals is determined by the selected currency (e.g. 2 for EUR, 0 for JPY, 3 for BHD).
   * This override is ignored for zero-decimal currencies (e.g. JPY, KRW, HUF), which always use 0.
   */
  decimals?: number;
}

export type MoneyInputPropsWithInputAttributes = MoneyInputProps &
  Partial<WithInputAttributesProps>;

interface MoneyInputState {
  searchQuery: string;
  formattedAmount: string;
  locale: string;
}

class MoneyInput extends Component<MoneyInputPropsWithInputAttributes, MoneyInputState> {
  declare props: MoneyInputPropsWithInputAttributes &
    Required<Pick<MoneyInputPropsWithInputAttributes, keyof typeof MoneyInput.defaultProps>>;

  static defaultProps = {
    size: Size.LARGE,
    classNames: {},
    selectProps: {},
  } satisfies Partial<MoneyInputPropsWithInputAttributes>;

  amountFocused = false;

  constructor(props: MoneyInputProps) {
    super(props);
    this.state = {
      searchQuery: '',
      formattedAmount: formatAmountIfSet({
        amount: props.amount,
        currency: props.selectedCurrency.currency,
        locale: props.intl.locale,
        decimals: props.decimals,
      }),
      locale: props.intl.locale,
    };
  }

  UNSAFE_componentWillReceiveProps(nextProps: MoneyInputProps) {
    this.setState({ locale: nextProps.intl.locale });

    if (!this.amountFocused) {
      this.setState({
        formattedAmount: formatAmountIfSet({
          amount: nextProps.amount,
          currency: nextProps.selectedCurrency.currency,
          locale: nextProps.intl.locale,
          decimals: nextProps.decimals,
        }),
      });
    }
  }

  isInputAllowedForKeyEvent = (event: React.KeyboardEvent<HTMLInputElement>) => {
    const { metaKey, key, ctrlKey } = event;
    const isNumberKey = isNumber(Number.parseInt(key, 10));

    return isNumberKey || metaKey || ctrlKey || allowedInputKeys.has(key);
  };

  handleKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (event) => {
    if (!this.isInputAllowedForKeyEvent(event)) {
      event.preventDefault();
    }
  };

  handlePaste: React.ClipboardEventHandler<HTMLInputElement> = (event) => {
    const paste = event.clipboardData.getData('text');
    const { locale } = this.state;
    const parsed = isEmpty(paste)
      ? null
      : parseNumber({
          amount: paste,
          currency: this.props.selectedCurrency.currency,
          locale,
          decimals: this.props.decimals,
        });

    if (isNumberOrNull(parsed)) {
      this.setState({
        formattedAmount: formatAmountIfSet({
          amount: parsed,
          currency: this.props.selectedCurrency.currency,
          locale,
          decimals: this.props.decimals,
        }),
      });
      this.props.onAmountChange?.(parsed);
    }

    event.preventDefault();
  };

  onAmountChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
    const { value } = event.target;
    this.setState({
      formattedAmount: value,
    });
    const parsed = isEmpty(value)
      ? null
      : parseNumber({
          amount: value,
          currency: this.props.selectedCurrency.currency,
          locale: this.state.locale,
          decimals: this.props.decimals,
        });
    if (isNumberOrNull(parsed)) {
      this.props.onAmountChange?.(parsed);
    }
  };

  onAmountBlur = () => {
    this.amountFocused = false;
    this.setAmount();
  };

  onAmountFocus = () => {
    this.amountFocused = true;
  };

  getSelectOptions() {
    const selectOptions = filterCurrenciesForQuery(this.props.currencies, this.state.searchQuery);

    const formattedOptions: SelectInputItem<CurrencyOptionItem>[] = [];
    let currentGroupOptions: SelectInputOptionItem<CurrencyOptionItem>[] | undefined;

    selectOptions.forEach((item) => {
      if (item.header != null) {
        currentGroupOptions = [];
        formattedOptions.push({
          type: 'group',
          label: item.header,
          options: currentGroupOptions,
        });
      } else {
        (currentGroupOptions ?? formattedOptions).push({
          type: 'option',
          value: item,
          filterMatchers: [item.value, item.label, item.note ?? '', item.searchable ?? ''],
        });
      }
    });

    return formattedOptions;
  }

  setAmount() {
    this.setState((previousState) => {
      const parsed = parseNumber({
        amount: previousState.formattedAmount,
        currency: this.props.selectedCurrency.currency,
        locale: previousState.locale,
        decimals: this.props.decimals,
      });
      if (!isNumberOrNull(parsed)) {
        return {
          formattedAmount: previousState.formattedAmount,
        };
      }
      return {
        formattedAmount: formatAmountIfSet({
          amount: parsed,
          currency: this.props.selectedCurrency.currency,
          locale: previousState.locale,
          decimals: this.props.decimals,
        }),
      };
    });
  }

  handleSelectChange = (value: CurrencyOptionItem) => {
    this.handleSearchChange('');
    this.props.onCurrencyChange?.(value);
  };

  handleSearchChange = (searchQuery: string) => {
    this.setState({ searchQuery });
    this.props.onSearchChange?.({
      searchQuery,
      filteredOptions: filterCurrenciesForQuery(this.props.currencies, searchQuery),
    });
  };

  style = (className: string) => this.props.classNames[className] || className;

  render() {
    const {
      inputAttributes,
      id: amountInputId,
      'aria-labelledby': ariaLabelledByProp,
      selectedCurrency,
      onCurrencyChange,
      size,
      addon,
      selectProps,
    } = this.props;
    const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes?.['aria-labelledby'];
    const selectOptions = this.getSelectOptions();

    const hasSingleCurrency = () => {
      if (selectOptions.length !== 0) {
        const firstItem = selectOptions[0];

        if (selectOptions.length === 1) {
          if (firstItem.type === 'option') {
            return firstItem.value.currency === selectedCurrency.currency;
          }
          if (firstItem.type === 'group') {
            return (
              firstItem.options.length === 1 &&
              !(this.props.onCustomAction && this.props.customActionLabel)
            );
          }
        }
      } else if (selectedCurrency?.currency) {
        return true;
      }

      return false;
    };

    const isFixedCurrency = (!this.state.searchQuery && hasSingleCurrency()) || !onCurrencyChange;
    const disabled = !this.props.onAmountChange;
    const selectedCurrencyElementId = `${inputAttributes?.id ?? amountInputId}SelectedCurrency`;

    return (
      <fieldset
        aria-invalid={inputAttributes?.['aria-invalid']}
        aria-describedby={inputAttributes?.['aria-describedby']}
        aria-labelledby={inputAttributes?.id ?? amountInputId}
        className={clsx(
          this.style('tw-money-input'),
          this.style('input-group'),
          this.style(`input-group-${size}`),
        )}
      >
        <Input
          id={inputAttributes?.id ?? amountInputId}
          aria-labelledby={ariaLabelledBy}
          value={this.state.formattedAmount}
          inputMode="decimal"
          disabled={disabled}
          placeholder={formatAmountIfSet({
            amount: this.props.placeholder,
            currency: this.props.selectedCurrency.currency,
            locale: this.state.locale,
            decimals: this.props.decimals,
          })}
          autoComplete="off"
          aria-describedby={selectedCurrencyElementId}
          onKeyDown={this.handleKeyDown}
          onChange={this.onAmountChange}
          onFocus={this.onAmountFocus}
          onBlur={this.onAmountBlur}
          onPaste={this.handlePaste}
        />
        {addon && (
          <span
            className={clsx(
              this.style('input-group-addon'),
              this.style(`input-${size}`),
              disabled ? this.style('disabled') : '',
            )}
          >
            {addon}
          </span>
        )}
        {isFixedCurrency ? (
          <div
            className={clsx(
              this.style('input-group-addon'),
              this.style(`input-${size}`),
              this.style('tw-money-input__fixed-currency'),
              disabled ? this.style('disabled') : '',
            )}
            id={selectedCurrencyElementId}
          >
            {(size === 'lg' || size === 'md') && (
              <span className={clsx(this.style('money-input-currency-flag'), this.style('m-r-2'))}>
                <Flag code={selectedCurrency.currency.toLowerCase()} intrinsicSize={24} />
              </span>
            )}
            <Title
              as="span"
              type={Typography.TITLE_SUBSECTION}
              className={size === 'lg' ? this.style('m-r-1') : ''}
              aria-label={getLocaleCurrencyName(this.props.intl, selectedCurrency.currency)}
            >
              {selectedCurrency.currency.toUpperCase()}
            </Title>
          </div>
        ) : (
          <div
            translate="no"
            className={clsx(
              this.style('input-group-btn'),
              this.style('amount-currency-select-btn'),
            )}
          >
            <SelectInput
              UNSAFE_triggerButtonProps={{
                id: undefined,
                'aria-labelledby': undefined,
                'aria-describedby': ariaLabelledBy,
                'aria-invalid': undefined,
                'aria-label': this.props.intl.formatMessage(messages.selectCurrencyLabel),
              }}
              id={selectedCurrencyElementId}
              items={selectOptions}
              value={selectedCurrency}
              compareValues="currency"
              renderValue={(currency, withinTrigger) => {
                return (
                  <SelectInputOptionContent
                    title={
                      withinTrigger ? (
                        <span
                          aria-label={getLocaleCurrencyName(this.props.intl, currency.currency)}
                        >
                          {currency.currency.toUpperCase()}
                        </span>
                      ) : (
                        currency.label
                      )
                    }
                    note={withinTrigger ? undefined : currency.note}
                    icon={<Flag code={currency.currency} intrinsicSize={24} />}
                  />
                );
              }}
              renderFooter={
                this.props.onCustomAction
                  ? () => (
                      // eslint-disable-next-line jsx-a11y/click-events-have-key-events
                      <div role="button" tabIndex={0} onClick={this.props.onCustomAction}>
                        {this.props.customActionLabel}
                      </div>
                    )
                  : undefined
              }
              placeholder={this.props.intl.formatMessage(messages.selectPlaceholder)}
              filterable
              filterPlaceholder={
                this.props.searchPlaceholder ||
                this.props.intl.formatMessage(messages.searchPlaceholder)
              }
              disabled={disabled}
              size={size}
              onChange={this.handleSelectChange}
              onFilterChange={({ queryNormalized }) => {
                this.handleSearchChange(queryNormalized ?? '');
              }}
              {...selectProps}
            />
          </div>
        )}
      </fieldset>
    );
  }
}

function filterCurrenciesForQuery(
  currencies: readonly CurrencyItem[],
  query: string,
): CurrencyItem[] {
  if (!query) {
    return [...currencies];
  }

  const options = currencies.filter(
    (option): option is CurrencyOptionItem => option.header == null,
  );
  const filteredOptions = removeDuplicateValueOptions(options).filter((option) =>
    currencyOptionFitsQuery(option, query),
  );

  return sortOptionsLabelsToFirst(filteredOptions, query);
}

function removeDuplicateValueOptions(options: readonly CurrencyOptionItem[]) {
  const uniqueValues = new Set<string>();
  return options.filter((option) => {
    if (!uniqueValues.has(option.value)) {
      uniqueValues.add(option.value);
      return true;
    }
    return false;
  });
}

function currencyOptionFitsQuery(option: CurrencyOptionItem, query: string) {
  if (!option.value) {
    return false;
  }

  return (
    contains(option.label, query) ||
    contains(option.searchable, query) ||
    contains(option.note, query)
  );
}

function contains(property: string | undefined, query: string) {
  return property?.toLowerCase().includes(query.toLowerCase());
}

function sortOptionsLabelsToFirst(options: readonly CurrencyOptionItem[], query: string) {
  return [...options].sort((first, second) => {
    const firstContains = contains(first.label, query);
    const secondContains = contains(second.label, query);

    if (firstContains && secondContains) {
      return 0;
    }
    if (firstContains) {
      return -1;
    }
    if (secondContains) {
      return 1;
    }
    return 0;
  });
}

export default injectIntl(withId(withInputAttributes(MoneyInput, { nonLabelable: true })));
