import {
  type DataFrame,
  type Field,
  FieldType,
  getFieldDisplayName,
  type GrafanaTheme2,
  outerJoinDataFrames,
  type TimeRange,
} from '@grafana/data';
import { maybeSortFrame } from '@grafana/data/internal';
import { t } from '@grafana/i18n';
import { findField } from 'app/features/dimensions/utils';

import { prepareGraphableFields } from '../timeseries/utils';

import { type Options, type CandlestickFieldMap, VizDisplayMode } from './panelcfg.gen';

export interface FieldPickerInfo {
  /** property name */
  key: keyof CandlestickFieldMap;

  /** The display name */
  name: string;

  /** by default pick these fields */
  defaults: string[];

  /** How is the field used */
  description: string;
}

export const getCandlestickFieldsInfo: () => Record<keyof CandlestickFieldMap, FieldPickerInfo> = () => {
  return {
    open: {
      key: 'open',
      name: t('candlestick.name-open', 'Open'),
      defaults: ['open', 'o'],
      description: t('candlestick.description-open', 'Value at the start of the period'),
    },
    high: {
      key: 'high',
      name: t('candlestick.name-high', 'High'),
      defaults: ['high', 'h', 'max'],
      description: t('candlestick.description-high', 'Maximum value within the period'),
    },
    low: {
      key: 'low',
      name: t('candlestick.name-low', 'Low'),
      defaults: ['low', 'l', 'min'],
      description: t('candlestick.description-low', 'Minimum value within the period'),
    },
    close: {
      key: 'close',
      name: t('candlestick.name-close', 'Close'),
      defaults: ['close', 'c'],
      description: t('candlestick.description-close', 'Value at the end of the period'),
    },
    volume: {
      key: 'volume',
      name: t('candlestick.name-volume', 'Volume'),
      defaults: ['volume', 'v'],
      description: t('candlestick.description-volume', 'Sample count within the period'),
    },
  };
};

export interface CandlestickData {
  autoOpenClose?: boolean;

  // Special fields
  open?: Field;
  high?: Field;
  low?: Field;
  close?: Field;
  volume?: Field;

  // All incoming values
  aligned: DataFrame;

  // The stuff passed to GraphNG
  frame: DataFrame;

  // The real names used
  names: CandlestickFieldMap;
}

function findFieldOrAuto(frame: DataFrame, info: FieldPickerInfo, options: CandlestickFieldMap): Field | undefined {
  const field = findField(frame, options[info.key]);
  if (!field) {
    for (const field of frame.fields) {
      const name = getFieldDisplayName(field, frame).toLowerCase();
      if (info.defaults.includes(name) || info.defaults.includes(field.name)) {
        return field;
      }
    }
  }
  return field;
}

export function prepareCandlestickFields(
  series: DataFrame[] | undefined,
  options: Partial<Options>,
  theme: GrafanaTheme2,
  timeRange?: TimeRange
): CandlestickData | null {
  if (!series?.length) {
    return null;
  }
  const candlestickFieldsInfo = getCandlestickFieldsInfo();

  // All fields
  const fieldMap = options.fields ?? {};
  const aligned =
    series.length === 1
      ? maybeSortFrame(
          series[0],
          series[0].fields.findIndex((f) => f.type === FieldType.time)
        )
      : outerJoinDataFrames({ frames: series });
  if (!aligned?.length) {
    return null;
  }

  const data: CandlestickData = { aligned, frame: aligned, names: {} };

  // Apply same filter as everything else in timeseries
  const timeSeriesFrames = prepareGraphableFields([aligned], theme, timeRange);
  if (!timeSeriesFrames) {
    return null;
  }

  const frame = (data.frame = timeSeriesFrames[0]);
  const timeIndex = frame.fields.findIndex((f) => f.type === FieldType.time);

  if (timeIndex < 0) {
    return null;
  }

  // Find the known fields
  const used = new Set<Field>();
  for (const info of Object.values(candlestickFieldsInfo)) {
    const field = findFieldOrAuto(frame, info, fieldMap);
    if (field) {
      data[info.key] = field;
      used.add(field);
    }
  }

  // Use first numeric value as open
  if (!data.open && !data.close) {
    data.open = frame.fields.find((f) => f.type === FieldType.number);
    if (data.open) {
      used.add(data.open);
    }
  }

  // Use next open as 'close' value
  if (data.open && !data.close && !fieldMap.close) {
    const values = data.open.values.slice(1);
    values.push(values[values.length - 1]); // duplicate last value
    data.close = {
      ...data.open,
      values: values,
      name: 'Next open',
      state: undefined,
    };
    used.add(data.close);
    frame.fields.push(data.close);
    data.autoOpenClose = true;
  }

  // Use previous close as 'open' value
  if (data.close && !data.open && !fieldMap.open) {
    const values = data.close.values.slice();
    values.unshift(values[0]); // duplicate first value
    values.length = frame.length;
    data.open = {
      ...data.close,
      values: values,
      name: 'Previous close',
      state: undefined,
    };
    used.add(data.open);
    frame.fields.push(data.open);
    data.autoOpenClose = true;
  }

  // Use the open field for min/max if nothing is set
  if (!data.high && !fieldMap.high) {
    data.high = data.open;
  }
  if (!data.low && !fieldMap.low) {
    data.low = data.open;
  }

  // unmap low and high fields in volume-only mode, and volume field in candles-only mode
  // so they fall through to unmapped fields and get appropriate includeAllFields treatment
  if (options.mode === VizDisplayMode.Volume) {
    if (data.high) {
      if (data.high !== data.open) {
        used.delete(data.high);
      }
      data.high = undefined;
    }
    if (data.low) {
      if (data.low !== data.open) {
        used.delete(data.low);
      }
      data.low = undefined;
    }
  } else if (options.mode === VizDisplayMode.Candles) {
    if (data.volume) {
      used.delete(data.volume);
      data.volume = undefined;
    }
  }

  // Register the name of each mapped field
  for (const info of Object.values(candlestickFieldsInfo)) {
    const f = data[info.key];
    if (f) {
      data.names[info.key] = getFieldDisplayName(f, data.frame);
    }
  }

  const timeField = frame.fields[timeIndex];

  // Make sure first field is time!
  const fields: Field[] = [timeField];

  if (!options.includeAllFields) {
    fields.push(...used);
  } else {
    fields.push(...frame.fields.filter((f) => f !== timeField));
  }

  data.frame = {
    ...data.frame,
    fields,
  };

  // Force update all the indicies
  for (let i = 0; i < data.frame.fields.length; i++) {
    const field = data.frame.fields[i];

    field.state = {
      ...field.state,

      // time is unused (-1), y series enumerate from 0
      seriesIndex: i - 1,

      origin: {
        fieldIndex: i,
        frameIndex: 0,
      },
    };
  }

  return data;
}
