import { isString } from 'lodash';
import { from, merge, type Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';

import {
  type AnnotationQuery,
  type AnnotationQueryRequest,
  DataFrameView,
  type DataQueryRequest,
  type DataQueryResponse,
  type DataSourceInstanceSettings,
  type TestDataSourceResponse,
  isValidLiveChannelAddress,
  MutableDataFrame,
  parseLiveChannelAddress,
  dataFrameFromJSON,
  LoadingState,
} from '@grafana/data';
import {
  DataSourceWithBackend,
  getDataSourceSrv,
  getGrafanaLiveSrv,
  getTemplateSrv,
  type StreamingFrameOptions,
} from '@grafana/runtime';
import { type DataSourceRef } from '@grafana/schema';
import { annotationServer } from 'app/features/annotations/api';
import { migrateDatasourceNameToRef } from 'app/features/dashboard/state/DashboardMigrator';

import { getDashboardSrv } from '../../../features/dashboard/services/DashboardSrv';

import AnnotationQueryEditor from './components/AnnotationQueryEditor';
import { randomWalk } from './randomWalk';
import { doTimeRegionQuery } from './timeRegions';
import { type GrafanaAnnotationQuery, GrafanaAnnotationType, type GrafanaQuery, GrafanaQueryType } from './types';

let counter = 100;

export class GrafanaDatasource extends DataSourceWithBackend<GrafanaQuery> {
  constructor(instanceSettings: DataSourceInstanceSettings) {
    super(instanceSettings);
    this.annotations = {
      QueryEditor: AnnotationQueryEditor,
      prepareAnnotation(json): AnnotationQuery<GrafanaAnnotationQuery> {
        // Previously, these properties lived outside of target
        // This should handle migrating them
        json.target = json.target ?? {
          type: json.type ?? GrafanaAnnotationType.Dashboard,
          limit: json.limit ?? 100,
          tags: json.tags ?? [],
          matchAny: json.matchAny ?? false,
        }; // using spread syntax caused an infinite loop in StandardAnnotationQueryEditor
        return json;
      },
      prepareQuery(anno: AnnotationQuery<GrafanaAnnotationQuery>): GrafanaQuery {
        let datasource: DataSourceRef | undefined | null = undefined;
        if (isString(anno.datasource)) {
          const ref = migrateDatasourceNameToRef(anno.datasource, { returnDefaultAsNull: false });
          if (ref) {
            datasource = ref;
          }
        } else {
          datasource = anno.datasource;
        }

        // Filter from streaming query conflicts with filter from annotations
        const { filter, ...rest } = anno;

        return { ...rest, refId: anno.name, queryType: GrafanaQueryType.Annotations, datasource };
      },
    };
  }

  getDefaultQuery(): Partial<GrafanaQuery> {
    return {
      queryType: GrafanaQueryType.RandomWalk,
    };
  }

  query(request: DataQueryRequest<GrafanaQuery>): Observable<DataQueryResponse> {
    const results: Array<Observable<DataQueryResponse>> = [];
    const targets: GrafanaQuery[] = [];
    const templateSrv = getTemplateSrv();
    for (const target of request.targets) {
      if (target.queryType === GrafanaQueryType.Annotations) {
        return from(
          this.getAnnotations({
            range: request.range,
            rangeRaw: request.range.raw,
            annotation: target as unknown as AnnotationQuery<GrafanaAnnotationQuery>,
            dashboard: getDashboardSrv().getCurrent(),
          })
        );
      }
      if (target.hide) {
        continue;
      }
      if (target.queryType === GrafanaQueryType.Snapshot) {
        results.push(
          of({
            // NOTE refId is intentionally missing because:
            // 1) there is only one snapshot
            // 2) the payload will reference original refIds
            data: (target.snapshot ?? []).map((v) => dataFrameFromJSON(v)),
            state: LoadingState.Done,
          })
        );
        continue;
      }
      if (target.queryType === GrafanaQueryType.TimeRegions) {
        const frame = doTimeRegionQuery('', target.timeRegion!, request.range);
        results.push(
          of({
            data: frame ? [frame] : [],
            state: LoadingState.Done,
          })
        );
        continue;
      }
      if (target.queryType === GrafanaQueryType.LiveMeasurements) {
        let channel = templateSrv.replace(target.channel, request.scopedVars);
        const { filter } = target;

        const addr = parseLiveChannelAddress(channel);
        if (!isValidLiveChannelAddress(addr)) {
          continue;
        }
        const buffer: Partial<StreamingFrameOptions> = {
          maxLength: request.maxDataPoints ?? 500,
        };
        if (target.buffer) {
          buffer.maxDelta = target.buffer;
          buffer.maxLength = buffer.maxLength! * 2; //??
        } else if (request.rangeRaw?.to === 'now') {
          buffer.maxDelta = request.range.to.valueOf() - request.range.from.valueOf();
        }

        results.push(
          getGrafanaLiveSrv().getDataStream({
            key: `${request.requestId}.${counter++}`,
            addr: addr!,
            filter,
            buffer,
          })
        );
      } else if (target.queryType === GrafanaQueryType.RandomWalk || !target.queryType) {
        results.push(
          of({
            data: randomWalk(target, request),
            state: LoadingState.Done,
          })
        );
      } else {
        targets.push(target);
      }
    }

    if (targets.length) {
      results.push(
        super.query({
          ...request,
          targets,
        })
      );
    }

    if (results.length) {
      // With a single query just return the results
      if (results.length === 1) {
        return results[0];
      }
      return merge(...results);
    }
    return of(); // nothing
  }

  listFiles(path: string, maxDataPoints?: number): Observable<DataFrameView<FileElement>> {
    return this.query({
      targets: [
        {
          refId: 'A',
          queryType: GrafanaQueryType.List,
          path,
        },
      ],
      maxDataPoints,
    } as DataQueryRequest<GrafanaQuery>).pipe(
      map((v) => {
        const frame = v.data[0] ?? new MutableDataFrame();
        return new DataFrameView<FileElement>(frame);
      })
    );
  }

  metricFindQuery() {
    return Promise.resolve([]);
  }

  async getAnnotations(options: AnnotationQueryRequest<GrafanaQuery>): Promise<DataQueryResponse> {
    const query = options.annotation.target as GrafanaQuery;
    if (query?.queryType === GrafanaQueryType.TimeRegions) {
      const frame = doTimeRegionQuery(options.annotation.name, query.timeRegion!, options.range);
      return Promise.resolve({ data: frame ? [frame] : [] });
    }

    const annotation = options.annotation as unknown as AnnotationQuery<GrafanaAnnotationQuery>;
    const target = annotation.target!;
    const params: any = {
      from: options.range.from.valueOf(),
      to: options.range.to.valueOf(),
      limit: target.limit,
      tags: target.tags,
      matchAny: target.matchAny,
    };

    if (target.type === GrafanaAnnotationType.Dashboard) {
      // if no dashboard id yet return
      if (!options.dashboard?.uid) {
        return Promise.resolve({ data: [] });
      }
      // filter by dashboard id
      params.dashboardUID = options.dashboard.uid;
      // remove tags filter if any
      delete params.tags;
    } else {
      // require at least one tag
      if (!Array.isArray(target.tags) || target.tags.length === 0) {
        return Promise.resolve({ data: [] });
      }
      const templateSrv = getTemplateSrv();
      const delimiter = '__delimiter__';
      const tags = [];
      for (const t of params.tags) {
        const renderedValues = templateSrv.replace(t, {}, (value: string | string[]) => {
          if (typeof value === 'string') {
            return value;
          }

          return value.join(delimiter);
        });
        for (const tt of renderedValues.split(delimiter)) {
          tags.push(tt);
        }
      }
      params.tags = tags;
    }

    const df = await annotationServer().query(
      params,
      `grafana-data-source-annotations-${annotation.name}-${options.dashboard?.uid}`
    );
    return { data: [df] };
  }

  testDatasource(): Promise<TestDataSourceResponse> {
    return Promise.resolve({ message: '', status: '' });
  }
}

/** Get the GrafanaDatasource instance */
export async function getGrafanaDatasource() {
  return (await getDataSourceSrv().get('-- Grafana --')) as GrafanaDatasource;
}

export interface FileElement {
  name: string;
  ['media-type']: string;
}
