import { type DataFrame, DataFrameView, FieldType, getDisplayProcessor, type SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { type TermCount } from 'app/core/components/TagFilter/TagFilter';
import { backendSrv } from 'app/core/services/backend_srv';
import { type PermissionLevel } from 'app/types/acl';

import { DEFAULT_MAX_VALUES, GENERAL_FOLDER_UID, TYPE_KIND_MAP } from '../constants';
import { type DashboardSearchHit, type DashboardSearchItemType } from '../types';

import { deletedDashboardsCache } from './deletedDashboardsCache';
import {
  type DashboardQueryResult,
  type GrafanaSearcher,
  type LocationInfo,
  type QueryResponse,
  type SearchQuery,
  type SortOptions,
} from './types';
import { filterSearchResults, replaceCurrentFolderQuery, searchHitsToDashboardSearchHits } from './utils';

interface APIQuery {
  query?: string;
  tag?: string[];
  limit?: number;
  page?: number;
  type?: DashboardSearchItemType;
  dashboardUID?: string[];
  folderUIDs?: string[];
  sort?: string;
  starred?: boolean;
  permission?: PermissionLevel;
  deleted?: boolean;
}

// Internal object to hold folderId
interface LocationInfoEXT extends LocationInfo {
  folderUid?: string;
}

export class SQLSearcher implements GrafanaSearcher {
  locationInfo: Record<string, LocationInfoEXT> = {
    general: {
      kind: 'folder',
      name: 'Dashboards',
      url: '/dashboards',
    },
  }; // share location info with everyone

  private async composeQuery(apiQuery: APIQuery, searchOptions: SearchQuery): Promise<APIQuery> {
    const query = await replaceCurrentFolderQuery(searchOptions);

    if (query.query?.length && query.query !== '*') {
      apiQuery.query = query.query;
    }

    // search v1 supports only one kind
    if (query.kind?.length === 1 && TYPE_KIND_MAP[query.kind[0]]) {
      apiQuery.type = TYPE_KIND_MAP[query.kind[0]];
    }

    if (query.uid) {
      apiQuery.dashboardUID = query.uid;
    } else if (query.location?.length) {
      apiQuery.folderUIDs = [query.location];
    }

    return apiQuery;
  }

  async search(query: SearchQuery): Promise<QueryResponse> {
    if (query.facet?.length) {
      throw new Error('facets not supported!');
    }

    if (query.from !== undefined) {
      if (!query.limit) {
        throw new Error('Must specify non-zero limit parameter when using from');
      }

      if ((query.from / query.limit) % 1 !== 0) {
        throw new Error('From parameter must be a multiple of limit');
      }
    }

    const limit = query.limit ?? (query.from !== undefined ? 1 : DEFAULT_MAX_VALUES);
    const page =
      query.from !== undefined
        ? // prettier-ignore
          (query.from / limit) + 1 // pages are 1-indexed, so need to +1 to get there
        : undefined;

    const q = await this.composeQuery(
      {
        limit: limit,
        tag: query.tags,
        sort: query.sort,
        permission: query.permission,
        page,
        deleted: query.deleted,
      },
      query
    );

    return this.doAPIQuery(q);
  }

  async starred(query: SearchQuery): Promise<QueryResponse> {
    if (query.facet?.length) {
      throw new Error('facets not supported!');
    }

    const q = await this.composeQuery(
      {
        limit: query.limit ?? DEFAULT_MAX_VALUES, // default 1k max values
        tag: query.tags,
        sort: query.sort,
        starred: query.starred,
      },
      query
    );

    return this.doAPIQuery(q);
  }

  // returns the appropriate sorting options
  async getSortOptions(): Promise<SelectableValue[]> {
    const opts = await backendSrv.get<SortOptions>('/api/search/sorting');
    return opts.sortOptions.map((v) => ({
      value: v.name,
      label: v.displayName,
    }));
  }

  // NOTE: the bluge query will find tags within the current results, the SQL based one does not
  async tags(query: SearchQuery): Promise<TermCount[]> {
    const terms = await backendSrv.get<TermCount[]>('/api/dashboards/tags');
    return terms.sort((a, b) => b.count - a.count);
  }

  async getLocationInfo() {
    return this.locationInfo;
  }

  async doAPIQuery(query: APIQuery): Promise<QueryResponse> {
    let rsp: DashboardSearchHit[];

    if (query.deleted) {
      const allDeletedHits = await deletedDashboardsCache.get();
      rsp = searchHitsToDashboardSearchHits(filterSearchResults(allDeletedHits, query));
    } else {
      rsp = await backendSrv.get<DashboardSearchHit[]>('/api/search', query);
    }

    // Field values (columnar)
    const kind: string[] = [];
    const name: string[] = [];
    const uid: string[] = [];
    const url: string[] = [];
    const tags: string[][] = [];
    const location: string[] = [];
    const sortBy: number[] = [];
    const isDeleted: boolean[] = [];
    const permanentlyDeleteDate: Array<Date | undefined> = [];
    let sortMetaName: string | undefined;

    for (let hit of rsp) {
      const k = hit.type === 'dash-folder' ? 'folder' : 'dashboard';
      kind.push(k);
      name.push(hit.title);
      uid.push(hit.uid);
      url.push(hit.url);
      tags.push(hit.tags);
      sortBy.push(hit.sortMeta!);
      isDeleted.push(hit.isDeleted ?? false);
      permanentlyDeleteDate.push(hit.permanentlyDeleteDate ? new Date(hit.permanentlyDeleteDate) : undefined);

      let v = hit.folderUid;
      if (!v && k === 'dashboard') {
        v = GENERAL_FOLDER_UID;
      }
      location.push(v!);

      if (hit.sortMetaName?.length) {
        sortMetaName = hit.sortMetaName;
      }

      if (hit.folderUid && hit.folderTitle) {
        this.locationInfo[hit.folderUid] = {
          kind: 'folder',
          name: hit.folderTitle,
          url: hit.folderUrl!,
          folderUid: hit.folderUid,
        };
      } else if (k === 'folder') {
        this.locationInfo[hit.uid] = {
          kind: k,
          name: hit.title!,
          url: hit.url,
          folderUid: hit.folderUid,
        };
      }
    }

    const data: DataFrame = {
      fields: [
        { name: 'kind', type: FieldType.string, config: {}, values: kind },
        { name: 'name', type: FieldType.string, config: {}, values: name },
        { name: 'uid', type: FieldType.string, config: {}, values: uid },
        { name: 'url', type: FieldType.string, config: {}, values: url },
        { name: 'tags', type: FieldType.other, config: {}, values: tags },
        { name: 'location', type: FieldType.string, config: {}, values: location },
        { name: 'isDeleted', type: FieldType.boolean, config: {}, values: isDeleted },
        { name: 'permanentlyDeleteDate', type: FieldType.time, config: {}, values: permanentlyDeleteDate },
      ],
      length: name.length,
      meta: {
        custom: {
          count: name.length,
          max_score: 1,
          locationInfo: this.locationInfo,
        },
      },
    };

    // Add enterprise sort fields as a field in the frame
    if (sortMetaName?.length && sortBy.length) {
      data.meta!.custom!.sortBy = sortMetaName;
      data.fields.push({
        name: sortMetaName, // Used in display
        type: FieldType.number,
        config: {},
        values: sortBy,
      });
    }

    for (const field of data.fields) {
      field.display = getDisplayProcessor({ field, theme: config.theme2 });
    }

    const view = new DataFrameView<DashboardQueryResult>(data);
    return {
      totalRows: data.length,
      view,

      // Paging not supported with this version
      loadMoreItems: async (startIndex: number, stopIndex: number): Promise<void> => {},
      isItemLoaded: (index: number): boolean => true,
    };
  }

  getFolderViewSort = () => {
    // sorts alphabetically in memory after retrieving the folders from the database
    return '';
  };
}
