import dateFormat from 'dateformat';
import { isString, mapValues, reduce, isDate, unescape } from 'lodash';
import { roundToNearestHalf } from '~/lib/utils/common_utils';
import { sanitize } from '~/lib/dompurify';
import { s__, n__, __, sprintf } from '../../../locale';

/**
 * Returns i18n month names array.
 * If `abbreviated` is provided, returns abbreviated
 * name.
 *
 * @param {Boolean} abbreviated
 */
export const getMonthNames = (abbreviated) => {
  if (abbreviated) {
    return [
      __('Jan'),
      __('Feb'),
      __('Mar'),
      __('Apr'),
      __('May'),
      __('Jun'),
      __('Jul'),
      __('Aug'),
      __('Sep'),
      __('Oct'),
      __('Nov'),
      __('Dec'),
    ];
  }
  return [
    __('January'),
    __('February'),
    __('March'),
    __('April'),
    __('May'),
    __('June'),
    __('July'),
    __('August'),
    __('September'),
    __('October'),
    __('November'),
    __('December'),
  ];
};

/**
 * Returns month name based on provided date.
 *
 * @param {Date} date
 * @param {Boolean} abbreviated
 */
export const monthInWords = (date, abbreviated = false) => {
  if (!date) {
    return '';
  }

  return getMonthNames(abbreviated)[date.getMonth()];
};

export const dateInWords = (date, abbreviated = false, hideYear = false) => {
  if (!date) return date;

  const month = date.getMonth();
  const year = date.getFullYear();

  const monthName = getMonthNames(abbreviated)[month];

  if (hideYear) {
    return `${monthName} ${date.getDate()}`;
  }

  return `${monthName} ${date.getDate()}, ${year}`;
};

/**
 * Similar to `timeIntervalInWords`, but rounds the return value
 * to 1/10th of the largest time unit. For example:
 *
 * 30 => 30 seconds
 * 90 => 1.5 minutes
 * 7200 => 2 hours
 * 86400 => 1 day
 * ... etc.
 *
 * The largest supported unit is "days".
 *
 * @param {Number} intervalInSeconds The time interval in seconds
 * @returns {String} A humanized description of the time interval
 */
export const humanizeTimeInterval = (intervalInSeconds) => {
  if (intervalInSeconds < 60 /* = 1 minute */) {
    const seconds = Math.round(intervalInSeconds * 10) / 10;
    return n__('%d second', '%d seconds', seconds);
  } else if (intervalInSeconds < 3600 /* = 1 hour */) {
    const minutes = Math.round(intervalInSeconds / 6) / 10;
    return n__('%d minute', '%d minutes', minutes);
  } else if (intervalInSeconds < 86400 /* = 1 day */) {
    const hours = Math.round(intervalInSeconds / 360) / 10;
    return n__('%d hour', '%d hours', hours);
  }

  const days = Math.round(intervalInSeconds / 8640) / 10;
  return n__('%d day', '%d days', days);
};

/**
 * Returns i18n weekday names array.
 */
export const getWeekdayNames = () => [
  __('Sunday'),
  __('Monday'),
  __('Tuesday'),
  __('Wednesday'),
  __('Thursday'),
  __('Friday'),
  __('Saturday'),
];

/**
 * Given a date object returns the day of the week in English
 * @param {date} date
 * @returns {String}
 */
export const getDayName = (date) => getWeekdayNames()[date.getDay()];

/**
 * Returns the i18n month name from a given date
 * @example
 * formatDateAsMonth(new Date('2020-06-28')) -> 'Jun'
 * @param  {String} datetime where month is extracted from
 * @param  {Object} options
 * @param  {Boolean} options.abbreviated whether to use the abbreviated month string, or not
 * @return {String} the i18n month name
 */
export function formatDateAsMonth(datetime, options = {}) {
  const { abbreviated = true } = options;
  const month = new Date(datetime).getMonth();
  return getMonthNames(abbreviated)[month];
}

/**
 * @example
 * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am UTC"
 * @param {date} datetime
 * @param {String} format
 * @param {Boolean} UTC convert local time to UTC
 * @returns {String}
 */
export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z', utc = false) => {
  if (isString(datetime) && datetime.match(/\d+-\d+\d+ /)) {
    throw new Error(__('Invalid date'));
  }
  return dateFormat(datetime, format, utc);
};

/**
 * Formats milliseconds as timestamp (e.g. 01:02:03).
 * This takes durations longer than a day into account (e.g. two days would be 48:00:00).
 *
 * @param milliseconds
 * @returns {string}
 */
export const formatTime = (milliseconds) => {
  const remainingSeconds = Math.floor(milliseconds / 1000) % 60;
  const remainingMinutes = Math.floor(milliseconds / 1000 / 60) % 60;
  const remainingHours = Math.floor(milliseconds / 1000 / 60 / 60);
  let formattedTime = '';
  if (remainingHours < 10) formattedTime += '0';
  formattedTime += `${remainingHours}:`;
  if (remainingMinutes < 10) formattedTime += '0';
  formattedTime += `${remainingMinutes}:`;
  if (remainingSeconds < 10) formattedTime += '0';
  formattedTime += remainingSeconds;
  return formattedTime;
};

/**
 * Port of ruby helper time_interval_in_words.
 *
 * @param  {Number} seconds
 * @return {String}
 */
export const timeIntervalInWords = (intervalInSeconds) => {
  const secondsInteger = parseInt(intervalInSeconds, 10);
  const minutes = Math.floor(secondsInteger / 60);
  const seconds = secondsInteger - minutes * 60;
  const secondsText = n__('%d second', '%d seconds', seconds);
  return minutes >= 1
    ? [n__('%d minute', '%d minutes', minutes), secondsText].join(' ')
    : secondsText;
};

/**
 * Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
 * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
 * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
 */
export const stringifyTime = (timeObject, fullNameFormat = false) => {
  const reducedTime = reduce(
    timeObject,
    (memo, unitValue, unitName) => {
      const isNonZero = Boolean(unitValue);

      if (fullNameFormat && isNonZero) {
        // Remove traling 's' if unit value is singular
        const formattedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
        return `${memo} ${unitValue} ${formattedUnitName}`;
      }

      return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
    },
    '',
  ).trim();
  return reducedTime.length ? reducedTime : '0m';
};

/**
 * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
 * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
 * or week length.
 */
export const parseSeconds = (
  seconds,
  { daysPerWeek = 5, hoursPerDay = 8, limitToHours = false, limitToDays = false } = {},
) => {
  const DAYS_PER_WEEK = daysPerWeek;
  const HOURS_PER_DAY = hoursPerDay;
  const SECONDS_PER_MINUTE = 60;
  const MINUTES_PER_HOUR = 60;
  const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
  const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;

  const timePeriodConstraints = {
    weeks: MINUTES_PER_WEEK,
    days: MINUTES_PER_DAY,
    hours: MINUTES_PER_HOUR,
    minutes: 1,
  };

  if (limitToDays || limitToHours) {
    timePeriodConstraints.weeks = 0;
  }

  if (limitToHours) {
    timePeriodConstraints.days = 0;
  }

  let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE);

  return mapValues(timePeriodConstraints, (minutesPerPeriod) => {
    if (minutesPerPeriod === 0) {
      return 0;
    }

    const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);

    unorderedMinutes -= periodCount * minutesPerPeriod;

    return periodCount;
  });
};

/**
 * Pads given items with zeros to reach a length of 2 characters.
 *
 * @param  {...any} args Items to be padded.
 * @returns {Array<String>} Padded items.
 */
export const padWithZeros = (...args) => args.map((arg) => `${arg}`.padStart(2, '0'));

/**
 * This removes the timezone from an ISO date string.
 * This can be useful when populating date/time fields along with a distinct timezone selector, in
 * which case we'd want to ignore the timezone's offset when populating the date and time.
 *
 * Examples:
 * stripTimezoneFromISODate('2021-08-16T00:00:00.000-02:00') => '2021-08-16T00:00:00.000'
 * stripTimezoneFromISODate('2021-08-16T00:00:00.000Z') => '2021-08-16T00:00:00.000'
 *
 * @param {String} date The ISO date string representation.
 * @returns {String} The ISO date string without the timezone.
 */
export const stripTimezoneFromISODate = (date) => {
  if (Number.isNaN(Date.parse(date))) {
    return null;
  }
  return date.replace(/(Z|[+-]\d{2}:\d{2})$/, '');
};

/**
 * Extracts the year, month and day from a Date instance and returns them in an object.
 * For example:
 * dateToYearMonthDate(new Date('2021-08-16')) => { year: '2021', month: '08', day: '16' }
 *
 * @param {Date} date The date to be parsed
 * @returns {Object} An object containing the extracted year, month and day.
 */
export const dateToYearMonthDate = (date) => {
  if (!isDate(date)) {
    // eslint-disable-next-line @gitlab/require-i18n-strings
    throw new Error('Argument should be a Date instance');
  }
  const [month, day] = padWithZeros(date.getMonth() + 1, date.getDate());
  return {
    year: `${date.getFullYear()}`,
    month,
    day,
  };
};

/**
 * Extracts the hours and minutes from a string representing a time.
 * For example:
 * timeToHoursMinutes('12:46') => { hours: '12', minutes: '46' }
 *
 * @param {String} time The time to be parsed in the form HH:MM.
 * @returns {Object} An object containing the hours and minutes.
 */
export const timeToHoursMinutes = (time = '') => {
  if (!time || !time.match(/\d{1,2}:\d{1,2}/)) {
    // eslint-disable-next-line @gitlab/require-i18n-strings
    throw new Error('Invalid time provided');
  }
  const [hours, minutes] = padWithZeros(...time.split(':'));
  return { hours, minutes };
};

/**
 * This combines a date and a time and returns the computed Date's ISO string representation.
 *
 * @param {Date} date Date object representing the base date.
 * @param {String} time String representing the time to be used, in the form HH:MM.
 * @param {String} offset An optional Date-compatible offset.
 * @returns {String} The combined Date's ISO string representation.
 */
export const dateAndTimeToISOString = (date, time, offset = '') => {
  const { year, month, day } = dateToYearMonthDate(date);
  const { hours, minutes } = timeToHoursMinutes(time);
  const dateString = `${year}-${month}-${day}T${hours}:${minutes}:00.000${offset || 'Z'}`;
  if (Number.isNaN(Date.parse(dateString))) {
    // eslint-disable-next-line @gitlab/require-i18n-strings
    throw new Error('Could not initialize date');
  }
  return dateString;
};

/**
 * Converts a Date instance to time input-compatible value consisting in a 2-digits hours and
 * minutes, separated by a semi-colon, in the 24-hours format.
 *
 * @param {Date} date Date to be converted
 * @returns {String} time input-compatible string in the form HH:MM.
 */
export const dateToTimeInputValue = (date) => {
  if (!isDate(date)) {
    // eslint-disable-next-line @gitlab/require-i18n-strings
    throw new Error('Argument should be a Date instance');
  }
  return date.toLocaleTimeString([], {
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  });
};

export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, months }) => {
  if (months) {
    return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
      value: roundToNearestHalf(months),
    });
  } else if (weeks) {
    return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
      value: roundToNearestHalf(weeks),
    });
  } else if (days) {
    return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
      value: roundToNearestHalf(days),
    });
  } else if (hours) {
    return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
  } else if (minutes) {
    return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
  } else if (seconds) {
    return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
  }
  return '-';
};
