import { Observable, of, OperatorFunction } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import {
  AnnotationEvent,
  AnnotationEventFieldSource,
  AnnotationEventMappings,
  AnnotationQuery,
  AnnotationSupport,
  DataFrame,
  Field,
  FieldType,
  getFieldDisplayName,
  KeyValue,
  standardTransformers,
} from '@grafana/data';

import { isString } from 'lodash';

export const standardAnnotationSupport: AnnotationSupport = {
  /**
   * Assume the stored value is standard model.
   */
  prepareAnnotation: (json: any) => {
    if (isString(json?.query)) {
      const { query, ...rest } = json;
      return {
        ...rest,
        target: {
          query,
        },
        mappings: {},
      };
    }
    return json as AnnotationQuery;
  },

  /**
   * Convert the stored JSON model and environment to a standard data source query object.
   * This query will be executed in the data source and the results converted into events.
   * Returning an undefined result will quietly skip query execution
   */
  prepareQuery: (anno: AnnotationQuery) => anno.target,

  /**
   * When the standard frame > event processing is insufficient, this allows explicit control of the mappings
   */
  processEvents: (anno: AnnotationQuery, data: DataFrame[]) => {
    return getAnnotationsFromData(data, anno.mappings);
  },
};

/**
 * Flatten all panel data into a single frame
 */

export function singleFrameFromPanelData(): OperatorFunction<DataFrame[], DataFrame | undefined> {
  return (source) =>
    source.pipe(
      mergeMap((data) => {
        if (!data?.length) {
          return of(undefined);
        }

        if (data.length === 1) {
          return of(data[0]);
        }

        return of(data).pipe(
          standardTransformers.mergeTransformer.operator({}),
          map((d) => d[0])
        );
      })
    );
}

interface AnnotationEventFieldSetter {
  key: keyof AnnotationEvent;
  field?: Field;
  text?: string;
  regex?: RegExp;
  split?: string; // for tags
}

export interface AnnotationFieldInfo {
  key: keyof AnnotationEvent;

  split?: string;
  field?: (frame: DataFrame) => Field | undefined;
  placeholder?: string;
  help?: string;
}

// These fields get added to the standard UI
export const annotationEventNames: AnnotationFieldInfo[] = [
  {
    key: 'time',
    field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.time),
    placeholder: 'time, or the first time field',
  },
  { key: 'timeEnd', help: 'When this field is defined, the annotation will be treated as a range' },
  {
    key: 'title',
  },
  {
    key: 'text',
    field: (frame: DataFrame) => frame.fields.find((f) => f.type === FieldType.string),
    placeholder: 'text, or the first text field',
  },
  { key: 'tags', split: ',', help: 'The results will be split on comma (,)' },
  {
    key: 'id',
  },
];

// Given legacy infrastructure, alert events are passed though the same annotation
// pipeline, but include fields that should not be exposed generally
const alertEventAndAnnotationFields: AnnotationFieldInfo[] = [
  ...annotationEventNames,
  { key: 'userId' },
  { key: 'login' },
  { key: 'email' },
  { key: 'prevState' },
  { key: 'newState' },
  { key: 'data' as any },
  { key: 'panelId' },
  { key: 'alertId' },
];

export function getAnnotationsFromData(
  data: DataFrame[],
  options?: AnnotationEventMappings
): Observable<AnnotationEvent[]> {
  return of(data).pipe(
    singleFrameFromPanelData(),
    map((frame) => {
      if (!frame?.length) {
        return [];
      }

      let hasTime = false;
      let hasText = false;
      const byName: KeyValue<Field> = {};

      for (const f of frame.fields) {
        const name = getFieldDisplayName(f, frame);
        byName[name.toLowerCase()] = f;
      }

      if (!options) {
        options = {};
      }

      const fields: AnnotationEventFieldSetter[] = [];

      for (const evts of alertEventAndAnnotationFields) {
        const opt = options[evts.key] || {}; //AnnotationEventFieldMapping

        if (opt.source === AnnotationEventFieldSource.Skip) {
          continue;
        }

        const setter: AnnotationEventFieldSetter = { key: evts.key, split: evts.split };

        if (opt.source === AnnotationEventFieldSource.Text) {
          setter.text = opt.value;
        } else {
          const lower = (opt.value || evts.key).toLowerCase();
          setter.field = byName[lower];

          if (!setter.field && evts.field) {
            setter.field = evts.field(frame);
          }
        }

        if (setter.field || setter.text) {
          fields.push(setter);
          if (setter.key === 'time') {
            hasTime = true;
          } else if (setter.key === 'text') {
            hasText = true;
          }
        }
      }

      if (!hasTime || !hasText) {
        return []; // throw an error?
      }

      // Add each value to the string
      const events: AnnotationEvent[] = [];

      for (let i = 0; i < frame.length; i++) {
        const anno: AnnotationEvent = {
          type: 'default',
          color: 'red',
        };

        for (const f of fields) {
          let v: any = undefined;

          if (f.text) {
            v = f.text; // TODO support templates!
          } else if (f.field) {
            v = f.field.values.get(i);
            if (v !== undefined && f.regex) {
              const match = f.regex.exec(v);
              if (match) {
                v = match[1] ? match[1] : match[0];
              }
            }
          }

          if (v !== null && v !== undefined) {
            if (f.split && typeof v === 'string') {
              v = v.split(',');
            }
            (anno as any)[f.key] = v;
          }
        }

        events.push(anno);
      }

      return events;
    })
  );
}
