import moment from 'moment-timezone';

import { PanelOptions, PanelType, PanelDefaultOptions } from '../pages/graph/Panel';
import { PanelMeta } from '../pages/graph/PanelList';

export const generateID = () => {
  return `_${Math.random()
    .toString(36)
    .substr(2, 9)}`;
};

export const byEmptyString = (p: string) => p.length > 0;

export const isPresent = <T>(obj: T): obj is NonNullable<T> => obj !== null && obj !== undefined;

export const escapeHTML = (str: string): string => {
  const entityMap: { [key: string]: string } = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
    '/': '&#x2F;',
  };

  return String(str).replace(/[&<>"'/]/g, function(s) {
    return entityMap[s];
  });
};

export const metricToSeriesName = (labels: { [key: string]: string }) => {
  if (labels === null) {
    return 'scalar';
  }
  let tsName = (labels.__name__ || '') + '{';
  const labelStrings: string[] = [];
  for (const label in labels) {
    if (label !== '__name__') {
      labelStrings.push(label + '="' + labels[label] + '"');
    }
  }
  tsName += labelStrings.join(', ') + '}';
  return tsName;
};

const rangeUnits: { [unit: string]: number } = {
  y: 60 * 60 * 24 * 365,
  w: 60 * 60 * 24 * 7,
  d: 60 * 60 * 24,
  h: 60 * 60,
  m: 60,
  s: 1,
};

export function parseRange(rangeText: string): number | null {
  const rangeRE = new RegExp('^([0-9]+)([ywdhms]+)$');
  const matches = rangeText.match(rangeRE);
  if (!matches || matches.length !== 3) {
    return null;
  }
  const value = parseInt(matches[1]);
  const unit = matches[2];
  return value * rangeUnits[unit];
}

export function formatRange(range: number): string {
  for (const unit of Object.keys(rangeUnits)) {
    if (range % rangeUnits[unit] === 0) {
      return range / rangeUnits[unit] + unit;
    }
  }
  return range + 's';
}

export function parseTime(timeText: string): number {
  return moment.utc(timeText).valueOf();
}

export function formatTime(time: number): string {
  return moment.utc(time).format('YYYY-MM-DD HH:mm:ss');
}

export const now = (): number => moment().valueOf();

export const humanizeDuration = (milliseconds: number): string => {
  const sign = milliseconds < 0 ? '-' : '';
  const unsignedMillis = milliseconds < 0 ? -1 * milliseconds : milliseconds;
  const duration = moment.duration(unsignedMillis, 'ms');
  const ms = Math.floor(duration.milliseconds());
  const s = Math.floor(duration.seconds());
  const m = Math.floor(duration.minutes());
  const h = Math.floor(duration.hours());
  const d = Math.floor(duration.asDays());
  if (d !== 0) {
    return `${sign}${d}d ${h}h ${m}m ${s}s`;
  }
  if (h !== 0) {
    return `${sign}${h}h ${m}m ${s}s`;
  }
  if (m !== 0) {
    return `${sign}${m}m ${s}s`;
  }
  if (s !== 0) {
    return `${sign}${s}.${ms}s`;
  }
  if (unsignedMillis > 0) {
    return `${sign}${unsignedMillis.toFixed(3)}ms`;
  }
  return '0s';
};

export const formatRelative = (startStr: string, end: number): string => {
  const start = parseTime(startStr);
  if (start < 0) {
    return 'Never';
  }
  return humanizeDuration(end - start);
};

const paramFormat = /^g\d+\..+=.+$/;

export const decodePanelOptionsFromQueryString = (query: string): PanelMeta[] => {
  if (query === '') {
    return [];
  }
  const urlParams = query.substring(1).split('&');

  return urlParams.reduce<PanelMeta[]>((panels, urlParam, i) => {
    const panelsCount = panels.length;
    const prefix = `g${panelsCount}.`;
    if (urlParam.startsWith(`${prefix}expr=`)) {
      const prefixLen = prefix.length;
      return [
        ...panels,
        {
          id: generateID(),
          key: `${panelsCount}`,
          options: urlParams.slice(i).reduce((opts, param) => {
            return param.startsWith(prefix) && paramFormat.test(param)
              ? { ...opts, ...parseOption(param.substring(prefixLen)) }
              : opts;
          }, PanelDefaultOptions),
        },
      ];
    }
    return panels;
  }, []);
};

export const parseOption = (param: string): Partial<PanelOptions> => {
  const [opt, val] = param.split('=');
  const decodedValue = decodeURIComponent(val.replace(/\+/g, ' '));
  switch (opt) {
    case 'expr':
      return { expr: decodedValue };

    case 'tab':
      return { type: decodedValue === '0' ? PanelType.Graph : PanelType.Table };

    case 'stacked':
      return { stacked: decodedValue === '1' };

    case 'range_input':
      const range = parseRange(decodedValue);
      return isPresent(range) ? { range } : {};

    case 'end_input':
    case 'moment_input':
      return { endTime: parseTime(decodedValue) };

    case 'step_input':
      const resolution = parseInt(decodedValue);
      return resolution > 0 ? { resolution } : {};
  }
  return {};
};

export const formatParam = (key: string) => (paramName: string, value: number | string | boolean) => {
  return `g${key}.${paramName}=${encodeURIComponent(value)}`;
};

export const toQueryString = ({ key, options }: PanelMeta) => {
  const formatWithKey = formatParam(key);
  const { expr, type, stacked, range, endTime, resolution } = options;
  const time = isPresent(endTime) ? formatTime(endTime) : false;
  const urlParams = [
    formatWithKey('expr', expr),
    formatWithKey('tab', type === PanelType.Graph ? 0 : 1),
    formatWithKey('stacked', stacked ? 1 : 0),
    formatWithKey('range_input', formatRange(range)),
    time ? `${formatWithKey('end_input', time)}&${formatWithKey('moment_input', time)}` : '',
    isPresent(resolution) ? formatWithKey('step_input', resolution) : '',
  ];
  return urlParams.filter(byEmptyString).join('&');
};

export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => {
  return `?${panels.map(toQueryString).join('&')}`;
};

export const createExpressionLink = (expr: string) => {
  return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.range_input=1h`;
};
export const mapObjEntries = <T, key extends keyof T, Z>(
  o: T,
  cb: ([k, v]: [string, T[key]], i: number, arr: [string, T[key]][]) => Z
) => Object.entries(o).map(cb);

export const callAll = (...fns: Array<(...args: any) => void>) => (...args: any) => {
  // eslint-disable-next-line prefer-spread
  fns.filter(Boolean).forEach(fn => fn.apply(null, args));
};
