import { chain, isEqual } from 'lodash';

import { PROM_KEYWORDS, OPERATORS, LOGICAL_OPERATORS } from 'app/plugins/datasource/prometheus/promql';

import { LOKI_KEYWORDS } from './syntax';

const builtInWords = [...PROM_KEYWORDS, ...OPERATORS, ...LOGICAL_OPERATORS, ...LOKI_KEYWORDS];

// We want to extract all possible metrics and also keywords
const metricsAndKeywordsRegexp = /([A-Za-z:][\w:]*)\b(?![\]{=!",])/g;

export function addLabelToQuery(
  query: string,
  key: string,
  value: string | number,
  operator?: string,
  hasNoMetrics?: boolean
): string {
  if (!key || !value) {
    throw new Error('Need label to add to query.');
  }

  // We need to make sure that we convert the value back to string because it may be a number
  const transformedValue = value === Infinity ? '+Inf' : value.toString();

  // Add empty selectors to bare metric names
  let previousWord: string;

  query = query.replace(metricsAndKeywordsRegexp, (match, word, offset) => {
    const isMetric = isWordMetric(query, word, offset, previousWord, hasNoMetrics);
    previousWord = word;

    return isMetric ? `${word}{}` : word;
  });

  //This is a RegExp for stream selector - e.g. {job="grafana"}
  const selectorRegexp = /(\$)?{([^{]*)}/g;
  const parts = [];
  let lastIndex = 0;
  let suffix = '';

  let match = selectorRegexp.exec(query);
  /* 
    There are 2 possible false positive scenarios: 
    
    1. We match Grafana's variables with ${ syntax - such as${__rate_s}. To filter these out we could use negative lookbehind,
    but Safari browser currently doesn't support it. Therefore we need to hack this by creating 2 matching groups. 
    (\$) is for the Grafana's variables and if we match it, we know this is not a stream selector and we don't want to add label.

    2. Log queries can include {{.label}} syntax when line_format is used. We need to filter these out by checking
    if match starts with "{."
  */
  while (match) {
    const prefix = query.slice(lastIndex, match.index);
    lastIndex = match.index + match[2].length + 2;
    suffix = query.slice(match.index + match[0].length);

    // Filtering our false positives
    if (match[0].startsWith('{.') || match[1]) {
      parts.push(prefix);
      parts.push(match[0]);
    } else {
      // If we didn't match first group, we are inside selector and we want to add labels
      const selector = match[2];
      const selectorWithLabel = addLabelToSelector(selector, key, transformedValue, operator);
      parts.push(prefix, selectorWithLabel);
    }

    match = selectorRegexp.exec(query);
  }

  parts.push(suffix);
  return parts.join('');
}

const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g;

export function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) {
  const parsedLabels = [];

  // Split selector into labels
  if (selector) {
    let match = labelRegexp.exec(selector);
    while (match) {
      parsedLabels.push({ key: match[1], operator: match[2], value: match[3] });
      match = labelRegexp.exec(selector);
    }
  }

  // Add new label
  const operatorForLabelKey = labelOperator || '=';
  parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });

  // Sort labels by key and put them together
  const formatted = chain(parsedLabels)
    .uniqWith(isEqual)
    .compact()
    .sortBy('key')
    .map(({ key, operator, value }) => `${key}${operator}${value}`)
    .value()
    .join(',');

  return `{${formatted}}`;
}

function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
  const nextSelectorStart = text.slice(position).indexOf(openChar);
  const nextSelectorEnd = text.slice(position).indexOf(closeChar);
  return nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd);
}

function isWordMetric(query: string, word: string, offset: number, previousWord: string, hasNoMetrics?: boolean) {
  const insideSelector = isPositionInsideChars(query, offset, '{', '}');
  // Handle "sum by (key) (metric)"
  const previousWordIsKeyWord = previousWord && OPERATORS.indexOf(previousWord) > -1;
  // Check for colon as as "word boundary" symbol
  const isColonBounded = word.endsWith(':');
  // Check for words that start with " which means that they are not metrics
  const startsWithQuote = query[offset - 1] === '"';
  // Check for template variables
  const isTemplateVariable = query[offset - 1] === '$';
  // Check for time units
  const isTimeUnit = ['s', 'm', 'h', 'd', 'w'].includes(word) && Boolean(Number(query[offset - 1]));

  if (
    !hasNoMetrics &&
    !insideSelector &&
    !isColonBounded &&
    !previousWordIsKeyWord &&
    !startsWithQuote &&
    !isTemplateVariable &&
    !isTimeUnit &&
    builtInWords.indexOf(word) === -1
  ) {
    return true;
  }
  return false;
}

export default addLabelToQuery;
