import {
  type DisplayProcessor,
  FieldType,
  formattedValueToString,
  getDisplayProcessor,
  getFieldDisplayValuesProxy,
  getFrameDisplayName,
  type ScopedVars,
} from '@grafana/data';
import { type VariableCustomFormatterFn } from '@grafana/scenes';

import { getFieldAccessor } from './fieldAccessorCache';
import { formatVariableValue } from './formatVariableValue';
import { getTemplateProxyForField } from './templateProxies';

/**
 * ${__value.raw/nummeric/text/time} macro
 */
export function valueMacro(
  match: string,
  fieldPath?: string,
  scopedVars?: ScopedVars,
  format?: string | VariableCustomFormatterFn
) {
  const value = getValueForValueMacro(match, fieldPath, scopedVars);
  return formatVariableValue(value, format);
}

function getValueForValueMacro(match: string, fieldPath?: string, scopedVars?: ScopedVars) {
  const dataContext = scopedVars?.__dataContext;
  if (!dataContext) {
    return match;
  }

  const { frame, rowIndex, field, calculatedValue } = dataContext.value;

  if (calculatedValue) {
    switch (fieldPath) {
      case 'numeric':
        return calculatedValue.numeric.toString();
      case 'raw':
        return calculatedValue.numeric;
      case 'time':
        return '';
      case 'text':
      default:
        return formattedValueToString(calculatedValue);
    }
  }

  if (rowIndex === undefined) {
    return match;
  }

  if (fieldPath === 'time') {
    const timeField = frame.fields.find((f) => f.type === FieldType.time);
    return timeField ? timeField.values[rowIndex] : undefined;
  }

  const value = field.values[rowIndex];
  if (fieldPath === 'raw') {
    return value;
  }

  const displayProcessor = field.display ?? getFallbackDisplayProcessor();
  const result = displayProcessor(value);

  switch (fieldPath) {
    case 'numeric':
      return result.numeric;
    case 'text':
      return result.text;
    default:
      return formattedValueToString(result);
  }
}

/**
 * Macro support doing things like.
 * ${__data.name}
 * ${__data.fields[0].name}
 * ${__data.fields["Value"].labels.cluster}
 *
 * Requires rowIndex on dataContext
 */
export function dataMacro(
  match: string,
  fieldPath?: string,
  scopedVars?: ScopedVars,
  format?: string | VariableCustomFormatterFn
) {
  const dataContext = scopedVars?.__dataContext;
  if (!dataContext || !fieldPath) {
    return match;
  }

  const { frame, rowIndex } = dataContext.value;

  if (rowIndex === undefined || fieldPath === undefined) {
    return match;
  }

  const obj = {
    name: frame.name,
    refId: frame.refId,
    fields: getFieldDisplayValuesProxy({ frame, rowIndex }),
  };

  const value = getFieldAccessor(fieldPath)(obj) ?? '';
  return formatVariableValue(value, format);
}

let fallbackDisplayProcessor: DisplayProcessor | undefined;

function getFallbackDisplayProcessor() {
  if (!fallbackDisplayProcessor) {
    fallbackDisplayProcessor = getDisplayProcessor();
  }

  return fallbackDisplayProcessor;
}

/**
 * ${__series} = frame display name
 */
export function seriesNameMacro(
  match: string,
  fieldPath?: string,
  scopedVars?: ScopedVars,
  format?: string | VariableCustomFormatterFn
) {
  const dataContext = scopedVars?.__dataContext;
  if (!dataContext) {
    return match;
  }

  if (fieldPath !== 'name') {
    return match;
  }

  const { frame, frameIndex } = dataContext.value;
  const value = getFrameDisplayName(frame, frameIndex);
  return formatVariableValue(value, format);
}

/**
 * Handles expressions like
 * ${__field.name}
 * ${__field.labels.cluster}
 */
export function fieldMacro(
  match: string,
  fieldPath?: string,
  scopedVars?: ScopedVars,
  format?: string | VariableCustomFormatterFn
) {
  const dataContext = scopedVars?.__dataContext;
  if (!dataContext) {
    return match;
  }

  if (fieldPath === undefined || fieldPath === '') {
    return match;
  }

  const { frame, field, data } = dataContext.value;
  const obj = getTemplateProxyForField(field, frame, data);

  const value = getFieldAccessor(fieldPath)(obj) ?? '';
  return formatVariableValue(value, format);
}
