import { formatAmount } from '@transferwise/formatting';
import { clsx } from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { type ChangeEvent, type KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useIntl } from 'react-intl';

import { Props as ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
import { AnimatedNumber } from '../animatedNumber/AnimatedNumber';
import { useFocus } from '../hooks/useFocus';
import { useInputStyle } from '../hooks/useInputStyle';
import {
  getDecimalCount,
  getDecimalSeparator,
  getEnteredDecimalsCount,
  getFormattedString,
  getGroupSeparator,
  getUnformattedNumber,
  isAllowedInputKey,
  isInputPossiblyOverflowing,
} from './utils';

type Props = {
  id: string;
  describedById?: string;
  amount?: number | null;
  currency: string;
  autoFocus?: boolean;
  onChange: (amount: number | null) => void;
  onFocusChange?: (focused: boolean) => void;
} & Pick<ExpressiveMoneyInputProps, 'loading'>;

export const AmountInput = ({
  id,
  describedById,
  amount,
  currency,
  autoFocus,
  onChange,
  onFocusChange,
  loading,
}: Props) => {
  const intl = useIntl();
  const { focus, setFocus, visualFocus, setVisualFocus } = useFocus();

  const [value, setValue] = useState<string>(() =>
    amount
      ? getFormattedString({
          value: amount,
          currency,
          locale: intl.locale,
        })
      : '',
  );
  const prevAmountRef = useRef<string | number | null | undefined>(amount);
  const numericValue = useMemo(() => {
    return getUnformattedNumber({
      value,
      currency,
      locale: intl.locale,
    });
  }, [value, currency, intl.locale]);

  const valueWithFullDecimals = useMemo(() => {
    return getFormattedString({
      value: numericValue ?? 0,
      currency,
      locale: intl.locale,
      alwaysShowDecimals: true,
    });
  }, [numericValue, currency, intl.locale]);

  const ref = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (autoFocus) {
      ref.current?.focus();
    }
  }, []);

  const placeholder = getPlaceholder(currency, intl.locale);
  const groupSeparator = getGroupSeparator(currency, intl.locale);
  const decimalSeparator = getDecimalSeparator(currency, intl.locale);
  const maxDecimalCount = getDecimalCount(currency, intl.locale);

  const decimalPart = getDecimalPart(value, decimalSeparator);
  const decimalMode = decimalSeparator && value.includes(decimalSeparator);

  useEffect(() => {
    if (prevAmountRef.current !== amount) {
      prevAmountRef.current = amount;

      // Only update the displayed value if not focused (preserve user input when focused)
      if (!focus) {
        // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing external prop to internal state when unfocused
        setValue(
          amount
            ? getFormattedString({
                value: amount,
                currency,
                locale: intl.locale,
              })
            : '',
        );
      }
    }
  }, [amount, focus, currency, intl.locale]);

  useEffect(() => {
    onFocusChange?.(visualFocus);
  }, [onFocusChange, visualFocus]);

  const shouldReformatAfterUserInput = (newValue: string) => {
    // don't reformat if formatting would wipe out user's input
    if (reformatValue(newValue) === '') {
      return false;
    }

    const endsWithDecimalSeparator = decimalSeparator && newValue.endsWith(decimalSeparator);
    const endsWithGroupSeparator = groupSeparator && newValue.endsWith(groupSeparator);

    // if the user has entered a seperator to the end, formatting would delete it
    if (endsWithDecimalSeparator || endsWithGroupSeparator) {
      return false;
    }

    const containsDecimalSeparator = decimalSeparator && newValue.includes(decimalSeparator);

    if (containsDecimalSeparator) {
      const enteredDecimalsCount = getEnteredDecimalsCount(newValue, decimalSeparator);
      // don't reformat until user has entered all the allowed decimals
      // for example, we don't want 1.1 to be reformatted to 1.10 immediately
      if (enteredDecimalsCount < maxDecimalCount) {
        return false;
      }
    }

    return true;
  };

  const reformatValue = (newValue: string) => {
    const unformattedValue = getUnformattedNumber({
      value: newValue,
      currency,
      locale: intl.locale,
    });
    const formattedValue = unformattedValue
      ? getFormattedString({
          value: unformattedValue,
          currency,
          locale: intl.locale,
        })
      : '';
    return formattedValue;
  };

  const handleChange = (newValue: string) => {
    const oldCursorPosition = ref.current?.selectionStart ?? 0;

    const newFormattedString = shouldReformatAfterUserInput(newValue)
      ? reformatValue(newValue)
      : newValue;
    setValue(newFormattedString);

    const newNumber = getUnformattedNumber({
      value: newFormattedString,
      currency,
      locale: intl.locale,
    });

    if (newNumber !== numericValue) {
      if (numericValue || newNumber) {
        onChange(newNumber);
      }
    }

    const newCursorPosition = oldCursorPosition + (newFormattedString.length - newValue.length);
    requestAnimationFrame(() => {
      ref?.current?.setSelectionRange(newCursorPosition, newCursorPosition);
    });
  };

  const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
    e.preventDefault();

    const clipboardData = e.clipboardData?.getData('text/plain');
    if (!clipboardData) {
      return;
    }

    // need to sanitise the pasted value otherwise other validation logic will ignore the input entirely
    const sanitisedValue = reformatValue(clipboardData);

    handleChange(sanitisedValue);
  };

  const handleBlur = () => {
    setFocus(false);
    setValue(reformatValue(value));
  };

  const handleBackspace = (e: KeyboardEvent<HTMLInputElement>) => {
    const input = e.target as HTMLInputElement;
    // using the updated selection range after the backspace key has been processed, instead of the current selection range in state
    const { value: currentValue, selectionStart, selectionEnd } = input;

    if (selectionStart === selectionEnd && selectionStart && selectionStart > 0) {
      const charBeforeCursor = currentValue[selectionStart - 1];

      // if the user deletes a thousands separator, remove the digit before it as well
      if (charBeforeCursor === groupSeparator) {
        e.preventDefault();
        const beforeCursor = currentValue.slice(0, selectionStart - 2);
        const afterCursor = currentValue.slice(selectionStart);
        const newValue = `${beforeCursor}${afterCursor}`;
        input.setSelectionRange(beforeCursor.length, beforeCursor.length);
        handleChange(newValue);
      }
    }
  };

  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
    setFocus(true);
    if (!isAllowedInputKey(e)) {
      e.preventDefault();
    }

    if (e.key === 'Backspace') {
      handleBackspace(e);
    }
  };

  const isAllowedInput = (e: ChangeEvent<HTMLInputElement>) => {
    const hasMultipleDecimalSeparators =
      decimalSeparator && e.target.value.split(decimalSeparator).length > 2;
    if (hasMultipleDecimalSeparators) {
      return false;
    }

    const newNumericValue = getUnformattedNumber({
      value: e.target.value,
      currency,
      locale: intl.locale,
    });
    const maxLength = Number.MAX_SAFE_INTEGER.toString().length;
    if (String(newNumericValue).length > maxLength) {
      return false;
    }

    return true;
  };

  const addonContent = useMemo((): string | null | undefined => {
    // because we're using a separate "addon" element for the placeholder decimals, there is a possibility that the input itself will become scrollable
    // and the decimals will appear on top of the input. Safest thing to do is to just hide the addon if there is not enough room
    // eslint-disable-next-line react-hooks/refs -- Reading layout dimensions for overflow detection
    if (isInputPossiblyOverflowing({ ref, value })) {
      return null;
    }
    if (!decimalSeparator || !value) {
      return null;
    }

    // if the user has typed a decimal separator, show the full decimal part as a placeholder
    // this returns a string even if there is no content, typing should replace the placeholder immediately without animation
    // otherwise there is an ugly animation when going from 1.23 to 1.2 due to AnimatePresence
    if (focus && decimalMode) {
      // reuse getDecimalPart
      const fullDecimalPart = getDecimalPart(valueWithFullDecimals, decimalSeparator);
      // show only the characters that are not already displayed by the input
      return fullDecimalPart?.slice(decimalPart?.length);
    }

    // in unfocused state, always show the full decimal part unless the user has already entered decimals
    if (!focus && !decimalMode) {
      const [_, decimalPlaceholder] = placeholder.split(decimalSeparator);
      return decimalSeparator + decimalPlaceholder;
    }

    return null;
  }, [
    decimalMode,
    decimalPart?.length,
    decimalSeparator,
    focus,
    placeholder,
    value,
    valueWithFullDecimals,
  ]);

  const style = useInputStyle({
    // whenever decimals are shown, we need to account for the full decimal part for the font size calculation
    value: addonContent ? valueWithFullDecimals : value,
    focus: visualFocus,
    inputElement: ref,
    loading,
  });

  return (
    <div className="wds-amount-input-container">
      <div
        className={clsx('wds-amount-input-input-container', 'np-text-display-large--forced')}
        style={style}
      >
        <input
          ref={ref}
          className="wds-amount-input-input"
          id={id}
          autoComplete="off"
          inputMode="decimal"
          value={value}
          type="text"
          placeholder={placeholder}
          aria-describedby={describedById}
          /* without this, the input tries to keep an aspect ratio and doesn't respect CSS width rules */
          size={1}
          onChange={(e) => {
            if (isAllowedInput(e)) {
              handleChange(e.target.value);
            }
          }}
          onBlurCapture={() => handleBlur()}
          onPaste={(e) => handlePaste(e)}
          onFocus={() => {
            setFocus(true);
          }}
          onBlur={() => {
            setTimeout(() => setVisualFocus(false), 30);
          }}
          onKeyDown={(e) => handleKeyDown(e)}
        />
        <AnimatePresence initial={false}>
          {addonContent !== null && (
            <AnimatedNumber
              className={clsx(
                'wds-amount-input-placeholder',
                visualFocus && 'wds-amount-input-placeholder-focus',
              )}
              onClick={() => ref.current?.focus()}
            >
              {addonContent}
            </AnimatedNumber>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
};

const getPlaceholder = (currency: string, locale: string) => {
  return formatAmount(0, currency, locale, { alwaysShowDecimals: true });
};

const getDecimalPart = (value: string, decimalSeparator: string | null) => {
  if (!value || !decimalSeparator) {
    return undefined;
  }

  const [_, decimalPart] = value.split(decimalSeparator);
  return decimalPart ?? undefined;
};
