import { useEffect, useRef, useState } from 'react';
// eslint-disable-next-line you-dont-need-lodash-underscore/get
import get from 'lodash/get';
import isNil from 'lodash/isNil';
import isEmpty from 'lodash/isEmpty';
import debounce from 'lodash/debounce';
import { validateEmail, validateUrl } from './validation-helpers';
import { MESSAGE_DISPLAY_LOCATION } from '../../form-field';
import { MESSAGE_TYPES } from '../../message';
import { useTranslation } from '../use-translation';
export const VALIDATION_EVENTS = {
  ON_CHANGE: 0,
  ON_BLUR: 1
};
export const DEFAULT_CONFIG = {
  displayLocation: MESSAGE_DISPLAY_LOCATION.INLINE,
  validationEvent: VALIDATION_EVENTS.ON_CHANGE,
  dependentValues: [],
  debounceTime: 200
};
/**
 *
 * @param {[any, function]} stateArray Tuple of state value and setValue function.
 * @param {array} validations Array of synchronous validations to run.
 * @param {array} asyncValidations Array of asynchronous validations to run.
 * @param {object} config Options of the inputvalidation.
 * @param {string} config.displayLocation A MESSAGE_DISPLAY_LOCATION option, defaulting to INLINE.
 * @param {string} config.validationEvent Defines event on which validation
 * should be triggered. Default is ON_CHANGE
 * @param {number} config.debounceTime time for debounce async validations
 * @param {array} config.dependentValues used to trigger input validation when these values changed.
 */

export function useInputValidation([value, setValue], {
  validations = [],
  asyncValidations = []
} = {
  validations: [],
  asyncValidations: []
}, config = DEFAULT_CONFIG) {
  const {
    t
  } = useTranslation();
  const {
    displayLocation = MESSAGE_DISPLAY_LOCATION.INLINE,
    validationEvent = VALIDATION_EVENTS.ON_CHANGE,
    dependentValues = [],
    debounceTime = 200
  } = config;
  const validationErrorWarningMessage = config.validationErrorWarningMessage ?? t('Something went wrong, try again');
  const [validity, setValidity] = useState(null);
  const isPristine = useRef(true);
  const isValidating = useRef(false);
  const isRetryShown = useRef(false);
  const valueToValidate = useRef('');
  const isCommitingChange = useRef(false);
  const hasAsyncValidations = !!asyncValidations.length;
  async function checkValid(val) {
    if (hasAsyncValidations) {
      isValidating.current = true;
      isRetryShown.current = false;
      valueToValidate.current = '';
      setValidity({
        value: [],
        displayLocation,
        isPristine: isPristine.current
      });
    }
    try {
      const validatorsArray = hasAsyncValidations ? await Promise.all([...asyncValidations, ...validations].map(v => v(val))) : validations.map(v => v(val));
      isValidating.current = false;
      const validated = validatorsArray.filter(Boolean);
      if (validated.length) {
        setValidity({
          type: MESSAGE_TYPES.ERROR,
          value: validated,
          displayLocation,
          isPristine: isPristine.current
        });
        return false;
      }
      setValidity(null);
      return true;
    } catch (e) {
      if (['AbortError', 'CanceledError'].includes(e?.name)) {
        return false;
      }
      isValidating.current = false;
      isRetryShown.current = true;
      valueToValidate.current = val;
      setValidity({
        type: MESSAGE_TYPES.WARNING,
        value: [validationErrorWarningMessage],
        displayLocation,
        isPristine: isPristine.current
      });
      return false;
    }
  }
  async function onBlur(e) {
    if (validationEvent === VALIDATION_EVENTS.ON_BLUR) {
      isPristine.current = false;
      const val = e.currentTarget.value;
      const isValid = await checkValid(val);
      if (isValid) {
        isCommitingChange.current = true;
        setValue(val);
      }
    }
  }
  const debouncedCheckRef = useRef(debounce(checkValid, debounceTime));
  useEffect(() => {
    // This reference needs to be kept up to date if the validations or asyncValidations change
    debouncedCheckRef.current = debounce(checkValid, debounceTime);
  }, [validations, asyncValidations]);
  async function onChange(e) {
    if (validationEvent === VALIDATION_EVENTS.ON_CHANGE) {
      isPristine.current = false;
      const val = get(e, 'currentTarget.value', '');
      if (hasAsyncValidations) {
        debouncedCheckRef.current(val);
      } else {
        await checkValid(val);
      }
      isCommitingChange.current = true;
      setValue(val);
    }
  }
  function onRetry() {
    checkValid(valueToValidate.current);
  }
  useEffect(() => {
    // Ensure that the value changed outside of the input is still validated.
    // This could be the form being reset to an inital state, for instance.
    // If it occurs from an input change, we don't need to validate it twice.
    // If the change is from outside, treat the value as pristine.
    if (isCommitingChange.current) {
      isCommitingChange.current = false;
    } else {
      isPristine.current = true;
      checkValid(value);
    }
  }, [value]);
  useEffect(() => {
    checkValid(value);
  }, dependentValues);
  return {
    asyncValidation: {
      isValidating: isValidating.current,
      isRetryShown: isRetryShown.current,
      onRetry
    },
    value,
    validity,
    onBlur,
    onChange
  };
}

/**
 * Requires some kind of value.  Does not pass on `null`, `undefined`, `''`, or `' '`.
 * @param {string} errorMessage Error message to show.
 */
export function required(errorMessage) {
  return value => {
    if (isNil(value) || typeof value === 'string' && !value.trim()) {
      return errorMessage;
    }
  };
}

/**
 * Requires a float value between 0 and 1, having 4 or less digits of precision.
 * So, `0.1234` is valid, but `0.12345` is not.
 * @param {string} errorMessage Error message to show.
 */
export function floatBetween0And1(errorMessage) {
  return value => {
    const floatValue = parseFloat(value);
    if (Number.isNaN(floatValue) || floatValue < 0 || floatValue > 1 || value.length > 6) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if the value is a finite number.
 * @param {string} errorMessage Error message to show.
 */
export function isFiniteNumber(errorMessage) {
  return value => {
    if (!Number.isFinite(parseFloat(value))) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if the value is an URL.
 * @param {string} errorMessage Error message to show.
 * @param {boolean} allowEmpty prop to allow empty value.
 */
export function isUrl(errorMessage, allowEmpty = false) {
  return value => {
    if (!value && allowEmpty) {
      return;
    }
    if (!validateUrl(value)) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if the value is an email.
 * @param {string} errorMessage Error message to show.
 * @param {boolean} allowEmpty prop to allow empty email.
 */
export function isEmail(errorMessage, allowEmpty = false) {
  return value => {
    if (!value && allowEmpty) {
      return;
    }
    if (!validateEmail(value)) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if the value is no longer than X.
 * @param {string} errorMessage Error message to show.
 * @param {number} maxLength Max length allowed
 */
export function notLongerThanX(errorMessage, maxLength = 100) {
  return value => {
    if (value.length > maxLength) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if number is less than X
 * @param {string} errorMessage Error message to show.
 * @param {number} number The number
 * @param {boolean} allowEmpty prop to allow empty value
 */
export function isNumberLessThan(errorMessage, number, allowEmpty = false) {
  return value => {
    if (allowEmpty && !value) {
      return;
    }
    const floatValue = parseFloat(value);
    const isNaN = Number.isNaN(floatValue);
    if (isNaN || floatValue < number) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if number is greater than X
 * @param {string} errorMessage Error message to show.
 * @param {number} number The number
 * @param {boolean} allowEmpty prop to allow empty value
 */
export function isNumberGreaterThan(errorMessage, number, allowEmpty = false) {
  return value => {
    if (allowEmpty && !value) {
      return;
    }
    const floatValue = parseFloat(value);
    const isNaN = Number.isNaN(floatValue);
    if (isNaN || floatValue > number) {
      return errorMessage;
    }
  };
}

/**
 * Checks if string matches provided regular expression.
 * @param {string} errorMessage Error message to show.
 * @param {RegExp} customPattern RegExp to validate string.
 */
export function matchCustomPattern(errorMessage, customPattern) {
  return value => {
    if (!customPattern.test(value)) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see if the value is an integer.
 * @param {string} errorMessage Error message to show.
 */
export function isInteger(errorMessage) {
  return value => {
    if (!Number.isInteger(parseFloat(value))) {
      return errorMessage;
    }
  };
}

/**
 * Checks to see that there no empty values in comma separated string.
 * String itself CAN be empty, "" is allowed, but not "val1,,val2"
 * @param {string} errorMessage Error message to show.
 */
export function noEmptyValuesInCommaSeparatedString(errorMessage) {
  return value => {
    if (isEmpty(value)) {
      return;
    }
    if (value.split(',').map(c => c.trim()).includes('')) {
      return errorMessage;
    }
  };
}