import {Release} from '@sentry/release-parser';
import round from 'lodash/round';
import type moment from 'moment';

import {t, tn} from 'sentry/locale';
import type {CommitAuthor, User} from 'sentry/types';
import {RATE_UNIT_LABELS, RateUnit} from 'sentry/utils/discover/fields';

export function userDisplayName(user: User | CommitAuthor, includeEmail = true): string {
  let displayName = String(user?.name ?? t('Unknown author')).trim();

  if (displayName.length <= 0) {
    displayName = t('Unknown author');
  }

  const email = String(user?.email ?? '').trim();

  if (email.length > 0 && email !== displayName && includeEmail) {
    displayName += ' (' + email + ')';
  }
  return displayName;
}

export const isSemverRelease = (rawVersion: string): boolean => {
  try {
    const parsedVersion = new Release(rawVersion);
    return !!parsedVersion.versionParsed;
  } catch {
    return false;
  }
};

export const formatVersion = (rawVersion: string, withPackage = false) => {
  try {
    const parsedVersion = new Release(rawVersion);
    const versionToDisplay = parsedVersion.describe();

    if (versionToDisplay.length) {
      return `${versionToDisplay}${
        withPackage && parsedVersion.package ? `, ${parsedVersion.package}` : ''
      }`;
    }

    return rawVersion;
  } catch {
    return rawVersion;
  }
};

function roundWithFixed(
  value: number,
  fixedDigits: number
): {label: string; result: number} {
  const label = value.toFixed(fixedDigits);
  const result = fixedDigits <= 0 ? Math.round(value) : value;

  return {label, result};
}

// in milliseconds
export const MONTH = 2629800000;
export const WEEK = 604800000;
export const DAY = 86400000;
export const HOUR = 3600000;
export const MINUTE = 60000;
export const SECOND = 1000;
export const MILLISECOND = 1;
export const MICROSECOND = 0.001;
export const NANOSECOND = 0.000001;

/**
 * Returns a human redable duration rounded to the largest unit.
 *
 * e.g. 2 days, or 3 months, or 25 seoconds
 *
 * Use `getExactDuration` for exact durations
 */

const DURATION_LABELS = {
  mo: t('mo'),
  w: t('w'),
  wk: t('wk'),
  week: t('week'),
  weeks: t('weeks'),
  d: t('d'),
  day: t('day'),
  days: t('days'),
  h: t('h'),
  hr: t('hr'),
  hour: t('hour'),
  hours: t('hours'),
  m: t('m'),
  min: t('min'),
  minute: t('minute'),
  minutes: t('minutes'),
  s: t('s'),
  sec: t('sec'),
  secs: t('secs'),
  second: t('second'),
  seconds: t('seconds'),
  ms: t('ms'),
  millisecond: t('millisecond'),
  milliseconds: t('milliseconds'),
};
export function getDuration(
  seconds: number,
  fixedDigits: number = 0,
  abbreviation: boolean = false,
  extraShort: boolean = false,
  absolute: boolean = false
): string {
  const absValue = Math.abs(seconds * 1000);

  // value in milliseconds
  const msValue = absolute ? absValue : seconds * 1000;

  if (absValue >= MONTH && !extraShort) {
    const {label, result} = roundWithFixed(msValue / MONTH, fixedDigits);
    return `${label}${
      abbreviation ? DURATION_LABELS.mo : ` ${tn('month', 'months', result)}`
    }`;
  }

  if (absValue >= WEEK) {
    const {label, result} = roundWithFixed(msValue / WEEK, fixedDigits);
    if (extraShort) {
      return `${label}${DURATION_LABELS.w}`;
    }
    if (abbreviation) {
      return `${label}${DURATION_LABELS.wk}`;
    }
    return `${label} ${tn('week', 'weeks', result)}`;
  }

  if (absValue >= DAY) {
    const {label, result} = roundWithFixed(msValue / DAY, fixedDigits);

    if (extraShort || abbreviation) {
      return `${label}${DURATION_LABELS.d}`;
    }
    return `${label} ${tn('day', 'days', result)}`;
  }

  if (absValue >= HOUR) {
    const {label, result} = roundWithFixed(msValue / HOUR, fixedDigits);
    if (extraShort) {
      return `${label}${DURATION_LABELS.h}`;
    }
    if (abbreviation) {
      return `${label}${DURATION_LABELS.hr}`;
    }
    return `${label} ${tn('hour', 'hours', result)}`;
  }

  if (absValue >= MINUTE) {
    const {label, result} = roundWithFixed(msValue / MINUTE, fixedDigits);
    if (extraShort) {
      return `${label}${DURATION_LABELS.m}`;
    }
    if (abbreviation) {
      return `${label}${DURATION_LABELS.min}`;
    }
    return `${label} ${tn('minute', 'minutes', result)}`;
  }

  if (absValue >= SECOND) {
    const {label, result} = roundWithFixed(msValue / SECOND, fixedDigits);
    if (extraShort || abbreviation) {
      return `${label}${DURATION_LABELS.s}`;
    }
    return `${label} ${tn('second', 'seconds', result)}`;
  }

  const {label, result} = roundWithFixed(msValue, fixedDigits);

  if (extraShort || abbreviation) {
    return `${label}${DURATION_LABELS.ms}`;
  }

  return `${label} ${tn('millisecond', 'milliseconds', result)}`;
}

const SUFFIX_ABBR = {
  years: t('yr'),
  weeks: t('wk'),
  days: t('d'),
  hours: t('hr'),
  minutes: t('min'),
  seconds: t('s'),
  milliseconds: t('ms'),
};
/**
 * Returns a human readable exact duration.
 * 'precision' arg will truncate the results to the specified suffix
 *
 * e.g. 1 hour 25 minutes 15 seconds
 */
export function getExactDuration(
  seconds: number,
  abbreviation: boolean = false,
  precision: keyof typeof SUFFIX_ABBR = 'milliseconds'
) {
  const minSuffix = ` ${precision}`;

  const convertDuration = (secs: number, abbr: boolean): string => {
    // value in milliseconds
    const msValue = round(secs * 1000);
    const value = round(Math.abs(secs * 1000));

    const divideBy = (time: number) => {
      return {
        quotient: msValue < 0 ? Math.ceil(msValue / time) : Math.floor(msValue / time),
        remainder: msValue % time,
      };
    };

    if (value >= WEEK || (value && minSuffix === ' weeks')) {
      const {quotient, remainder} = divideBy(WEEK);
      const suffix = abbr ? t('wk') : ` ${tn('week', 'weeks', quotient)}`;

      return `${quotient}${suffix} ${
        minSuffix === suffix ? '' : convertDuration(remainder / 1000, abbr)
      }`;
    }
    if (value >= DAY || (value && minSuffix === ' days')) {
      const {quotient, remainder} = divideBy(DAY);
      const suffix = abbr ? t('d') : ` ${tn('day', 'days', quotient)}`;

      return `${quotient}${suffix} ${
        minSuffix === suffix ? '' : convertDuration(remainder / 1000, abbr)
      }`;
    }
    if (value >= HOUR || (value && minSuffix === ' hours')) {
      const {quotient, remainder} = divideBy(HOUR);
      const suffix = abbr ? t('hr') : ` ${tn('hour', 'hours', quotient)}`;

      return `${quotient}${suffix} ${
        minSuffix === suffix ? '' : convertDuration(remainder / 1000, abbr)
      }`;
    }
    if (value >= MINUTE || (value && minSuffix === ' minutes')) {
      const {quotient, remainder} = divideBy(MINUTE);
      const suffix = abbr ? t('min') : ` ${tn('minute', 'minutes', quotient)}`;

      return `${quotient}${suffix} ${
        minSuffix === suffix ? '' : convertDuration(remainder / 1000, abbr)
      }`;
    }
    if (value >= SECOND || (value && minSuffix === ' seconds')) {
      const {quotient, remainder} = divideBy(SECOND);
      const suffix = abbr ? t('s') : ` ${tn('second', 'seconds', quotient)}`;

      return `${quotient}${suffix} ${
        minSuffix === suffix ? '' : convertDuration(remainder / 1000, abbr)
      }`;
    }

    if (value === 0) {
      return '';
    }

    const suffix = abbr ? t('ms') : ` ${tn('millisecond', 'milliseconds', value)}`;

    return `${msValue}${suffix}`;
  };

  const result = convertDuration(seconds, abbreviation).trim();

  if (result.length) {
    return result;
  }

  return `0${abbreviation ? SUFFIX_ABBR[precision] : minSuffix}`;
}

export const SEC_IN_WK = 604800;
export const SEC_IN_DAY = 86400;
export const SEC_IN_HR = 3600;
export const SEC_IN_MIN = 60;

type Level = [lvlSfx: moment.unitOfTime.DurationConstructor, denominator: number];

type ParsedLargestSuffix = [val: number, suffix: moment.unitOfTime.DurationConstructor];
/**
 * Given a length of time in seconds, provide me the largest divisible suffix and value for that time period.
 * eg. 60 -> [1, 'minutes']
 * eg. 7200 -> [2, 'hours']
 * eg. 7260 -> [121, 'minutes']
 *
 * @param seconds
 * @param maxSuffix     determines the largest suffix we should pin the response to
 */
export function parseLargestSuffix(
  seconds: number,
  maxSuffix: string = 'days'
): ParsedLargestSuffix {
  const levels: Level[] = [
    ['minutes', SEC_IN_MIN],
    ['hours', SEC_IN_HR],
    ['days', SEC_IN_DAY],
    ['weeks', SEC_IN_WK],
  ];
  let val = seconds;
  let suffix: moment.unitOfTime.DurationConstructor = 'seconds';
  if (val === 0) {
    return [val, suffix];
  }
  for (const [lvlSfx, denominator] of levels) {
    if (seconds % denominator) {
      break;
    }
    val = seconds / denominator;
    suffix = lvlSfx;
    if (lvlSfx === maxSuffix) {
      break;
    }
  }
  return [val, suffix];
}

export function formatSecondsToClock(
  seconds: number,
  {padAll}: {padAll: boolean} = {padAll: true}
) {
  if (seconds === 0 || isNaN(seconds)) {
    return padAll ? '00:00' : '0:00';
  }

  const divideBy = (msValue: number, time: number) => {
    return {
      quotient: msValue < 0 ? Math.ceil(msValue / time) : Math.floor(msValue / time),
      remainder: msValue % time,
    };
  };

  // value in milliseconds
  const absMSValue = round(Math.abs(seconds * 1000));

  const {quotient: hours, remainder: rMins} = divideBy(absMSValue, HOUR);
  const {quotient: minutes, remainder: rSeconds} = divideBy(rMins, MINUTE);
  const {quotient: secs, remainder: milliseconds} = divideBy(rSeconds, SECOND);

  const fill = (num: number) => (num < 10 ? `0${num}` : String(num));

  const parts = hours
    ? [padAll ? fill(hours) : hours, fill(minutes), fill(secs)]
    : [padAll ? fill(minutes) : minutes, fill(secs)];

  const ms = `000${milliseconds}`.slice(-3);
  return milliseconds ? `${parts.join(':')}.${ms}` : parts.join(':');
}

export function parseClockToSeconds(clock: string) {
  const [rest, milliseconds] = clock.split('.');
  const parts = rest.split(':');

  let seconds = 0;
  const progression = [MONTH, WEEK, DAY, HOUR, MINUTE, SECOND].slice(parts.length * -1);
  for (let i = 0; i < parts.length; i++) {
    const num = Number(parts[i]) || 0;
    const time = progression[i] / 1000;
    seconds += num * time;
  }
  const ms = Number(milliseconds) || 0;
  return seconds + ms / 1000;
}

export function formatFloat(number: number, places: number) {
  const multi = Math.pow(10, places);
  return parseInt((number * multi).toString(), 10) / multi;
}

/**
 * Format a value between 0 and 1 as a percentage
 */
export function formatPercentage(
  value: number,
  places: number = 2,
  options: {
    minimumValue?: number;
  } = {}
) {
  if (value === 0) {
    return '0%';
  }

  const minimumValue = options.minimumValue ?? 0;

  if (Math.abs(value) <= minimumValue) {
    return `<${minimumValue * 100}%`;
  }

  return (
    round(value * 100, places).toLocaleString(undefined, {
      maximumFractionDigits: places,
    }) + '%'
  );
}

const numberFormatSteps = [
  [1_000_000_000, 'b'],
  [1_000_000, 'm'],
  [1_000, 'k'],
] as const;

/**
 * Formats a number with an abbreviation e.g. 1000 -> 1k.
 *
 * @param number the number to format
 * @param maximumSignificantDigits the number of significant digits to include
 * @param includeDecimals when true, formatted number will always include non trailing zero decimal places
 */
export function formatAbbreviatedNumber(
  number: number | string,
  maximumSignificantDigits?: number,
  includeDecimals?: boolean
): string {
  number = Number(number);

  const prefix = number < 0 ? '-' : '';
  const numAbsValue = Math.abs(number);

  for (const step of numberFormatSteps) {
    const [suffixNum, suffix] = step;
    const shortValue = Math.floor(numAbsValue / suffixNum);
    const fitsBound = numAbsValue % suffixNum === 0;

    if (shortValue <= 0) {
      continue;
    }

    const useShortValue = !includeDecimals && (shortValue > 10 || fitsBound);

    if (useShortValue) {
      if (maximumSignificantDigits === undefined) {
        return `${prefix}${shortValue}${suffix}`;
      }
      const formattedNumber = parseFloat(
        shortValue.toPrecision(maximumSignificantDigits)
      ).toString();
      return `${prefix}${formattedNumber}${suffix}`;
    }

    const formattedNumber = formatFloat(
      numAbsValue / suffixNum,
      maximumSignificantDigits || 1
    ).toLocaleString(undefined, {
      maximumSignificantDigits,
    });

    return `${prefix}${formattedNumber}${suffix}`;
  }

  return number.toLocaleString(undefined, {maximumSignificantDigits});
}

/**
 * Formats a number with an abbreviation and rounds to 2
 * decimal digits without forcing trailing zeros.
 * e. g. 1000 -> 1k, 1234 -> 1.23k
 */
export function formatAbbreviatedNumberWithDynamicPrecision(
  value: number | string
): string {
  const number = Number(value);

  if (number === 0) {
    return '0';
  }

  const log10 = Math.log10(Math.abs(number));
  // numbers less than 1 will have a negative log10
  const numOfDigits = log10 < 0 ? 1 : Math.floor(log10) + 1;

  const maxStep = numberFormatSteps[0][0];

  // if the number is larger than the largest step, we determine the number of digits
  // by dividing the number by the largest step, otherwise the number of formatted
  // digits is the number of digits in the number modulo 3 (the number of zeroes between steps)
  const numOfFormattedDigits =
    number > maxStep
      ? Math.floor(Math.log10(number / maxStep))
      : Math.max(numOfDigits % 3 === 0 ? 3 : numOfDigits % 3, 0);

  const maximumSignificantDigits = numOfFormattedDigits + 2;

  return formatAbbreviatedNumber(value, maximumSignificantDigits, true);
}

/**
 * Rounds to specified number of decimal digits (defaults to 2) without forcing trailing zeros
 * Will preserve significant decimals for very small numbers
 * e.g. 0.0001234 -> 0.00012
 * @param value number to format
 */
export function formatNumberWithDynamicDecimalPoints(
  value: number,
  maxFractionDigits = 2
): string {
  if ([0, Infinity, -Infinity, NaN].includes(value)) {
    return value.toLocaleString();
  }

  const exponent = Math.floor(Math.log10(Math.abs(value)));

  const maximumFractionDigits =
    exponent >= 0 ? maxFractionDigits : Math.abs(exponent) + 1;
  const numberFormat = {
    maximumFractionDigits,
    minimumFractionDigits: 0,
  };

  return value.toLocaleString(undefined, numberFormat);
}

export function formatRate(
  value: number,
  unit: RateUnit = RateUnit.PER_SECOND,
  options: {
    minimumValue?: number;
    significantDigits?: number;
  } = {}
) {
  // NOTE: `Intl` doesn't support unitless-per-unit formats (i.e.,
  // `"-per-minute"` is not valid) so we have to concatenate the unit manually, since our rates are usually just "/min" or "/s".
  // Because of this, the unit is not internationalized.

  // 0 is special!
  if (value === 0) {
    return `${0}${RATE_UNIT_LABELS[unit]}`;
  }

  const minimumValue = options.minimumValue ?? 0;
  const significantDigits = options.significantDigits ?? 3;

  const numberFormatOptions: ConstructorParameters<typeof Intl.NumberFormat>[1] = {
    notation: 'compact',
    compactDisplay: 'short',
    minimumSignificantDigits: significantDigits,
    maximumSignificantDigits: significantDigits,
  };

  if (value <= minimumValue) {
    return `<${minimumValue}${RATE_UNIT_LABELS[unit]}`;
  }

  return `${value.toLocaleString(undefined, numberFormatOptions)}${
    RATE_UNIT_LABELS[unit]
  }`;
}

export function formatSpanOperation(
  operation?: string,
  length: 'short' | 'long' = 'short'
) {
  if (length === 'long') {
    return getLongSpanOperationDescription(operation);
  }

  return getShortSpanOperationDescription(operation);
}

function getLongSpanOperationDescription(operation?: string) {
  if (operation?.startsWith('http')) {
    return t('URL request');
  }

  if (operation === 'db.redis') {
    return t('cache query');
  }

  if (operation?.startsWith('db')) {
    return t('database query');
  }

  if (operation?.startsWith('task')) {
    return t('application task');
  }

  if (operation?.startsWith('serialize')) {
    return t('serializer');
  }

  if (operation?.startsWith('middleware')) {
    return t('middleware');
  }

  if (operation === 'resource') {
    return t('resource');
  }

  if (operation === 'resource.script') {
    return t('JavaScript file');
  }

  if (operation === 'resource.css') {
    return t('stylesheet');
  }

  if (operation === 'resource.img') {
    return t('image');
  }

  return t('span');
}

function getShortSpanOperationDescription(operation?: string) {
  if (operation?.startsWith('http')) {
    return t('request');
  }

  if (operation?.startsWith('db')) {
    return t('query');
  }

  if (operation?.startsWith('task')) {
    return t('task');
  }

  if (operation?.startsWith('serialize')) {
    return t('serializer');
  }

  if (operation?.startsWith('middleware')) {
    return t('middleware');
  }

  if (operation?.startsWith('resource')) {
    return t('resource');
  }

  return t('span');
}
