import dateformat from 'dateformat';
import { pick, omit, isEqual, isEmpty } from 'lodash';
import { DATETIME_RANGE_TYPES } from './constants';
import { secondsToMilliseconds } from './datetime_utility';

const MINIMUM_DATE = new Date(0);

const DEFAULT_DIRECTION = 'before';

const durationToMillis = (duration) => {
  if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) {
    return secondsToMilliseconds(duration.seconds);
  }
  // eslint-disable-next-line @gitlab/require-i18n-strings
  throw new Error('Invalid duration: only `seconds` is supported');
};

const dateMinusDuration = (date, duration) => new Date(date.getTime() - durationToMillis(duration));

const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration));

const isValidDuration = (duration) => Boolean(duration && Number.isFinite(duration.seconds));

const isValidDateString = (dateString) => {
  if (typeof dateString !== 'string' || !dateString.trim()) {
    return false;
  }

  try {
    // dateformat throws error that can be caught.
    // This is better than using `new Date()`
    dateformat(dateString, 'isoUtcDateTime');
    return true;
  } catch (e) {
    return false;
  }
};

const handleRangeDirection = ({ direction = DEFAULT_DIRECTION, anchorDate, minDate, maxDate }) => {
  let startDate;
  let endDate;

  if (direction === DEFAULT_DIRECTION) {
    startDate = minDate;
    endDate = anchorDate;
  } else {
    startDate = anchorDate;
    endDate = maxDate;
  }

  return {
    startDate,
    endDate,
  };
};

/**
 * Converts a fixed range to a fixed range
 * @param {Object} fixedRange - A range with fixed start and
 * end (e.g. "midnight January 1st 2020 to midday January31st 2020")
 */
const convertFixedToFixed = ({ start, end }) => ({
  start,
  end,
});

/**
 * Converts an anchored range to a fixed range
 * @param {Object} anchoredRange - A duration of time
 * relative to a fixed point in time (e.g., "the 30 minutes
 * before midnight January 1st 2020", or "the 2 days
 * after midday on the 11th of May 2019")
 */
const convertAnchoredToFixed = ({ anchor, duration, direction }) => {
  const anchorDate = new Date(anchor);

  const { startDate, endDate } = handleRangeDirection({
    minDate: dateMinusDuration(anchorDate, duration),
    maxDate: datePlusDuration(anchorDate, duration),
    direction,
    anchorDate,
  });

  return {
    start: startDate.toISOString(),
    end: endDate.toISOString(),
  };
};

/**
 * Converts a rolling change to a fixed range
 *
 * @param {Object} rollingRange - A time range relative to
 * now (e.g., "last 2 minutes", or "next 2 days")
 */
const convertRollingToFixed = ({ duration, direction }) => {
  // Use Date.now internally for easier mocking in tests
  const now = new Date(Date.now());

  return convertAnchoredToFixed({
    duration,
    direction,
    anchor: now.toISOString(),
  });
};

/**
 * Converts an open range to a fixed range
 *
 * @param {Object} openRange - A time range relative
 * to an anchor (e.g., "before midnight on the 1st of
 * January 2020", or "after midday on the 11th of May 2019")
 */
const convertOpenToFixed = ({ anchor, direction }) => {
  // Use Date.now internally for easier mocking in tests
  const now = new Date(Date.now());

  const { startDate, endDate } = handleRangeDirection({
    minDate: MINIMUM_DATE,
    maxDate: now,
    direction,
    anchorDate: new Date(anchor),
  });

  return {
    start: startDate.toISOString(),
    end: endDate.toISOString(),
  };
};

/**
 * Handles invalid date ranges
 */
const handleInvalidRange = () => {
  // eslint-disable-next-line @gitlab/require-i18n-strings
  throw new Error('The input range does not have the right format.');
};

const handlers = {
  invalid: handleInvalidRange,
  fixed: convertFixedToFixed,
  anchored: convertAnchoredToFixed,
  rolling: convertRollingToFixed,
  open: convertOpenToFixed,
};

/**
 * Validates and returns the type of range
 *
 * @param {Object} Date time range
 * @returns {String} `key` value for one of the handlers
 */
export function getRangeType(range) {
  const { start, end, anchor, duration } = range;

  if ((start || end) && !anchor && !duration) {
    return isValidDateString(start) && isValidDateString(end)
      ? DATETIME_RANGE_TYPES.fixed
      : DATETIME_RANGE_TYPES.invalid;
  }
  if (anchor && duration) {
    return isValidDateString(anchor) && isValidDuration(duration)
      ? DATETIME_RANGE_TYPES.anchored
      : DATETIME_RANGE_TYPES.invalid;
  }
  if (duration && !anchor) {
    return isValidDuration(duration) ? DATETIME_RANGE_TYPES.rolling : DATETIME_RANGE_TYPES.invalid;
  }
  if (anchor && !duration) {
    return isValidDateString(anchor) ? DATETIME_RANGE_TYPES.open : DATETIME_RANGE_TYPES.invalid;
  }
  return DATETIME_RANGE_TYPES.invalid;
}

/**
 * convertToFixedRange Transforms a `range of time` into a `fixed range of time`.
 *
 * The following types of a `ranges of time` can be represented:
 *
 * Fixed Range: A range with fixed start and end (e.g. "midnight January 1st 2020 to midday January 31st 2020")
 * Anchored Range: A duration of time relative to a fixed point in time (e.g., "the 30 minutes before midnight January 1st 2020", or "the 2 days after midday on the 11th of May 2019")
 * Rolling Range: A time range relative to now (e.g., "last 2 minutes", or "next 2 days")
 * Open Range: A time range relative to an anchor (e.g., "before midnight on the 1st of January 2020", or "after midday on the 11th of May 2019")
 *
 * @param {Object} dateTimeRange - A Time Range representation
 * It contains the data needed to create a fixed time range plus
 * a label (recommended) to indicate the range that is covered.
 *
 * A definition via a TypeScript notation is presented below:
 *
 *
 * type Duration = { // A duration of time, always in seconds
 *   seconds: number;
 * }
 *
 * type Direction = 'before' | 'after'; // Direction of time relative to an anchor
 *
 * type FixedRange = {
 *   start: ISO8601;
 *   end: ISO8601;
 *   label: string;
 * }
 *
 * type AnchoredRange = {
 *   anchor: ISO8601;
 *   duration: Duration;
 *   direction: Direction; // defaults to 'before'
 *   label: string;
 * }
 *
 * type RollingRange = {
 *   duration: Duration;
 *   direction: Direction; // defaults to 'before'
 *   label: string;
 * }
 *
 * type OpenRange = {
 *   anchor: ISO8601;
 *   direction: Direction; // defaults to 'before'
 *   label: string;
 * }
 *
 * type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange;
 *
 *
 * @returns {FixedRange} An object with a start and end in ISO8601 format.
 */
export const convertToFixedRange = (dateTimeRange) =>
  handlers[getRangeType(dateTimeRange)](dateTimeRange);

/**
 * Returns a copy of the object only with time range
 * properties relevant to time range calculation.
 *
 * Filtered properties are:
 * - 'start'
 * - 'end'
 * - 'anchor'
 * - 'duration'
 * - 'direction': if direction is already the default, its removed.
 *
 * @param {Object} timeRange - A time range object
 * @returns Copy of time range
 */
const pruneTimeRange = (timeRange) => {
  const res = pick(timeRange, ['start', 'end', 'anchor', 'duration', 'direction']);
  if (res.direction === DEFAULT_DIRECTION) {
    return omit(res, 'direction');
  }
  return res;
};

/**
 * Returns true if the time ranges are equal according to
 * the time range calculation properties
 *
 * @param {Object} timeRange - A time range object
 * @param {Object} other - Time range object to compare with.
 * @returns true if the time ranges are equal, false otherwise
 */
export const isEqualTimeRanges = (timeRange, other) => {
  const tr1 = pruneTimeRange(timeRange);
  const tr2 = pruneTimeRange(other);
  return isEqual(tr1, tr2);
};

/**
 * Searches for a time range in a array of time ranges using
 * only the properies relevant to time ranges calculation.
 *
 * @param {Object} timeRange - Time range to search (needle)
 * @param {Array} timeRanges - Array of time tanges (haystack)
 */
export const findTimeRange = (timeRange, timeRanges) =>
  timeRanges.find((element) => isEqualTimeRanges(element, timeRange));

// Time Ranges as URL Parameters Utils

/**
 * List of possible time ranges parameters
 */
export const timeRangeParamNames = ['start', 'end', 'anchor', 'duration_seconds', 'direction'];

/**
 * Converts a valid time range to a flat key-value pairs object.
 *
 * Duration is flatted to avoid having nested objects.
 *
 * @param {Object} A time range
 * @returns key-value pairs object that can be used as parameters in a URL.
 */
export const timeRangeToParams = (timeRange) => {
  let params = pruneTimeRange(timeRange);
  if (timeRange.duration) {
    const durationParms = {};
    Object.keys(timeRange.duration).forEach((key) => {
      durationParms[`duration_${key}`] = timeRange.duration[key].toString();
    });
    params = { ...durationParms, ...params };
    params = omit(params, 'duration');
  }
  return params;
};

/**
 * Converts a valid set of flat params to a time range object
 *
 * Parameters that are not part of time range object are ignored.
 *
 * @param {params} params - key-value pairs object.
 */
export const timeRangeFromParams = (params) => {
  const timeRangeParams = pick(params, timeRangeParamNames);
  let range = Object.entries(timeRangeParams).reduce((acc, [key, val]) => {
    // unflatten duration
    if (key.startsWith('duration_')) {
      acc.duration = acc.duration || {};
      acc.duration[key.slice('duration_'.length)] = parseInt(val, 10);
      return acc;
    }
    return { [key]: val, ...acc };
  }, {});
  range = pruneTimeRange(range);
  return !isEmpty(range) ? range : null;
};
