import { type DataFrame, type DataQueryResponse, FieldType, isValidGoDuration, type Labels } from '@grafana/data';

import { isBytesString, processLabels } from './languageUtils';
import { isLogLineJSON, isLogLineLogfmt, isLogLinePacked } from './lineParser';
import { LabelType } from './types';

export const LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX = 'the query would read too many bytes';
export const LOKI_TIMEOUT_ERROR_MSG =
  'request timed out, decrease the duration of the request or add more label matchers (prefer exact match over regex match) to reduce the amount of data processed';
export const LOKI_TIMEOUT_ERROR_REGEX = /timeout|timed out/;

export function dataFrameHasLokiError(frame: DataFrame): boolean {
  const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values ?? [];
  return labelSets.some((labels) => labels.__error__ !== undefined);
}

export function dataFrameHasLevelLabel(frame: DataFrame): boolean {
  const labelSets: Labels[] = frame.fields.find((f) => f.name === 'labels')?.values ?? [];
  return labelSets.some((labels) => labels.level !== undefined);
}

export function extractLogParserFromDataFrame(frame: DataFrame): {
  hasLogfmt: boolean;
  hasJSON: boolean;
  hasPack: boolean;
} {
  const lineField = frame.fields.find((field) => field.type === FieldType.string);
  if (lineField == null) {
    return { hasJSON: false, hasLogfmt: false, hasPack: false };
  }

  const logLines: string[] = lineField.values;

  let hasJSON = false;
  let hasLogfmt = false;
  let hasPack = false;

  logLines.forEach((line) => {
    if (isLogLineJSON(line)) {
      hasJSON = true;

      hasPack = isLogLinePacked(line);
    }
    if (isLogLineLogfmt(line)) {
      hasLogfmt = true;
    }
  });

  return { hasLogfmt, hasJSON, hasPack };
}

export function extractLabelKeysFromDataFrame(frame: DataFrame, type: LabelType = LabelType.Indexed): string[] {
  const labelsArray: Array<{ [key: string]: string }> | undefined =
    frame?.fields?.find((field) => field.name === 'labels')?.values ?? [];
  const labelTypeArray: Array<{ [key: string]: string }> | undefined =
    frame?.fields?.find((field) => field.name === 'labelTypes')?.values ?? [];

  if (!labelsArray?.length) {
    return [];
  }

  // if there are no label types and type is LabelType.Indexed return all label keys
  if (!labelTypeArray?.length) {
    if (type === LabelType.Indexed) {
      const { keys: labelKeys } = processLabels(labelsArray);
      return labelKeys;
    }
    return [];
  }

  // If we have label types, we can return only label keys that match type
  let labelsSet = new Set<string>();
  for (let i = 0; i < labelsArray.length; i++) {
    const labels = labelsArray[i];
    const labelsType = labelTypeArray[i];

    const allLabelKeys = Object.keys(labels).filter((key) => labelsType[key] === type);
    labelsSet = new Set([...labelsSet, ...allLabelKeys]);
  }

  return Array.from(labelsSet);
}

export function extractUnwrapLabelKeysFromDataFrame(frame: DataFrame): string[] {
  const labelsArray: Array<{ [key: string]: string }> | undefined =
    frame?.fields?.find((field) => field.name === 'labels')?.values ?? [];

  if (!labelsArray?.length) {
    return [];
  }

  // We do this only for first label object, because we want to consider only labels that are present in all log lines
  // possibleUnwrapLabels are labels with 1. number value OR 2. value that is valid go duration OR 3. bytes string value
  const possibleUnwrapLabels = Object.keys(labelsArray[0]).filter((key) => {
    const value = labelsArray[0][key];
    if (!value) {
      return false;
    }
    return !isNaN(Number(value)) || isValidGoDuration(value) || isBytesString(value);
  });

  // Add only labels that are present in every line to unwrapLabels
  return possibleUnwrapLabels.filter((label) => labelsArray.every((obj) => obj[label]));
}

export function extractHasErrorLabelFromDataFrame(frame: DataFrame): boolean {
  const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other);
  if (labelField == null) {
    return false;
  }

  const labels: Array<{ [key: string]: string }> = labelField.values;
  return labels.some((label) => label['__error__']);
}

export function extractLevelLikeLabelFromDataFrame(frame: DataFrame): string | null {
  const labelField = frame.fields.find((field) => field.name === 'labels' && field.type === FieldType.other);
  if (labelField == null) {
    return null;
  }

  // Depending on number of labels, this can be pretty heavy operation.
  // Let's just look at first 2 lines If needed, we can introduce more later.
  const labelsArray: Array<{ [key: string]: string }> = labelField.values.slice(0, 2);
  let levelLikeLabel: string | null = null;

  // Find first level-like label
  for (let labels of labelsArray) {
    const label = Object.keys(labels).find(
      (label) => label === 'level' || label === 'detected_level' || label === 'lvl' || label.includes('level')
    );
    if (label) {
      levelLikeLabel = label;
      break;
    }
  }
  return levelLikeLabel;
}

export function isRetriableError(errorResponse: DataQueryResponse) {
  const message = errorResponse.errors
    ? errorResponse.errors
        .map((err) => err.message ?? '')
        .join()
        .toLowerCase()
    : (errorResponse.error?.message ?? '').toLowerCase();

  // max_query_bytes_read exceeded
  if (message.includes(LOKI_MAX_QUERY_BYTES_READ_ERROR_MSG_PREFIX)) {
    throw new Error(message);
  }
  if (message.match(LOKI_TIMEOUT_ERROR_REGEX)) {
    return true;
  }
  if (errorResponse.data.length > 0 && errorResponse.data[0].fields.length > 0) {
    // Error response but we're receiving data, continue querying.
    return false;
  }
  throw new Error(message);
}
