import { partition } from 'lodash';

import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data';
import { safeStringifyValue } from 'app/core/utils/explore';
import { ExploreFieldLinkModel } from 'app/features/explore/utils/links';

import { parseLogsFrame } from '../logsFrame';

export type FieldDef = {
  keys: string[];
  values: string[];
  links?: Array<LinkModel<Field>> | ExploreFieldLinkModel[];
  fieldIndex: number;
};

/**
 * Returns all fields for log row which consists of fields we parse from the message itself and additional fields
 * found in the dataframe (they may contain links).
 */
export const getAllFields = (
  row: LogRowModel,
  getFieldLinks?: (
    field: Field,
    rowIndex: number,
    dataFrame: DataFrame
  ) => Array<LinkModel<Field>> | ExploreFieldLinkModel[]
) => {
  return getDataframeFields(row, getFieldLinks);
};

/**
 * A log line may contain many links that would all need to go on their own logs detail row
 * This iterates through and creates a FieldDef (row) per link.
 */
export const createLogLineLinks = (hiddenFieldsWithLinks: FieldDef[]): FieldDef[] => {
  let fieldsWithLinksFromVariableMap: FieldDef[] = [];
  hiddenFieldsWithLinks.forEach((linkField) => {
    linkField.links?.forEach((link: LinkModel | ExploreFieldLinkModel) => {
      if ('variables' in link && link.variables.length > 0) {
        // convert ExploreFieldLinkModel to LinkModel by omitting variables field
        const fieldDefFromLink: LinkModel = {
          href: link.href,
          title: link.title,
          origin: link.origin,
          onClick: link.onClick,
          target: link.target,
        };
        const variableKeys = link.variables.map((variable) => {
          const varName = variable.variableName;
          const fieldPath = variable.fieldPath ? `.${variable.fieldPath}` : '';
          return `${varName}${fieldPath}`;
        });
        const variableValues = link.variables.map((variable) => (variable.found ? variable.value : ''));
        fieldsWithLinksFromVariableMap.push({
          keys: variableKeys,
          values: variableValues,
          links: [fieldDefFromLink],
          fieldIndex: linkField.fieldIndex,
        });
      }
    });
  });
  return fieldsWithLinksFromVariableMap;
};

/**
 * creates fields from the dataframe-fields, adding data-links, when field.config.links exists
 */
export const getDataframeFields = (
  row: LogRowModel,
  getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>
): FieldDef[] => {
  const nonEmptyVisibleFields = getNonEmptyVisibleFields(row);
  return nonEmptyVisibleFields.map((field) => {
    const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : [];
    const fieldVal = field.values[row.rowIndex];
    const outputVal =
      typeof fieldVal === 'string' || typeof fieldVal === 'number' ? fieldVal.toString() : safeStringifyValue(fieldVal);
    return {
      keys: [field.name],
      values: [outputVal],
      links: links,
      fieldIndex: field.index,
    };
  });
};

type VisOptions = {
  keepTimestamp?: boolean;
  keepBody?: boolean;
};

// return the fields (their indices to be exact) that should be visible
// based on the logs dataframe structure
function getVisibleFieldIndices(frame: DataFrame, opts: VisOptions): Set<number> {
  const logsFrame = parseLogsFrame(frame);
  if (logsFrame === null) {
    // should not really happen
    return new Set();
  }

  // we want to show every "extra" field
  const visibleFieldIndices = new Set(logsFrame.extraFields.map((f) => f.index));

  // we always show the severity field
  if (logsFrame.severityField !== null) {
    visibleFieldIndices.add(logsFrame.severityField.index);
  }

  if (opts.keepBody) {
    visibleFieldIndices.add(logsFrame.bodyField.index);
  }

  if (opts.keepTimestamp) {
    visibleFieldIndices.add(logsFrame.timeField.index);
  }

  return visibleFieldIndices;
}

// split the dataframe's fields into visible and hidden arrays.
// note: does not do any row-level checks,
// for example does not check if the field's values are nullish
// or not at a givn row.
export function separateVisibleFields(
  frame: DataFrame,
  opts?: VisOptions
): { visible: FieldWithIndex[]; hidden: FieldWithIndex[] } {
  const fieldsWithIndex: FieldWithIndex[] = frame.fields.map((field, index) => ({ ...field, index }));

  const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {});

  const [visible, hidden] = partition(fieldsWithIndex, (f) => {
    // hidden fields are always hidden
    if (f.config.custom?.hidden) {
      return false;
    }

    // fields with data-links are visible
    if ((f.config.links ?? []).length > 0) {
      return true;
    }

    return visibleFieldIndices.has(f.index);
  });

  return { visible, hidden };
}

// Optimized version of separateVisibleFields() to only return visible fields for getAllFields()
function getNonEmptyVisibleFields(row: LogRowModel, opts?: VisOptions): FieldWithIndex[] {
  const frame = row.dataFrame;
  const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {});
  const visibleFields: FieldWithIndex[] = [];
  for (let index = 0; index < frame.fields.length; index++) {
    const field = frame.fields[index];
    // ignore empty fields
    if (field.values[row.rowIndex] == null) {
      continue;
    }
    // hidden fields are always hidden
    if (field.config.custom?.hidden) {
      continue;
    }

    // fields with data-links are visible
    if ((field.config.links && field.config.links.length > 0) || visibleFieldIndices.has(index)) {
      visibleFields.push({ ...field, index });
    }
  }
  return visibleFields;
}
