import { Text } from '@microsoft/sp-core-library';
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import { ConsoleListener, Logger, LogLevel } from '@pnp/logging';
import { SearchQuery, SearchResults, SearchSuggestQuery, Sort, SortDirection, sp, SPRest, Web } from '@pnp/sp';
import { groupBy, mapKeys, mapValues, sortBy } from 'lodash-es';
import * as moment from 'moment';
import LocalizationHelper from './../helpers/LocalizationHelper';
import { IRefinementFilter, IRefinementResult, IRefinementValue, ISearchResult, ISearchResults } from './../models/ISearchResult';
import { ISearchService } from './ISearchService';
declare var System: any;

export class SearchService implements ISearchService {
  static ParseSelectedProperties(value: string): string[] {
    return value ? value.replace(/\s|,+$/g, '').split(',') : [];
  }

  private _initialSearchResult: SearchResults = null;
  private _resultsCount: number;
  private _context: IWebPartContext;
  private _selectedProperties: string[];
  private _templateQuery: string;
  private _resultSourceId: string;
  private _sortList: string;
  private _enableQueryRules: boolean;

  public get resultsCount(): number { return this._resultsCount; }
  public set resultsCount(value: number) { this._resultsCount = value; }

  public set selectedProperties(value: string[]) { this._selectedProperties = value; }
  public get selectedProperties(): string[] { return this._selectedProperties; }

  public set templateQuery(value: string) { this._templateQuery = value; }
  public get templateQuery(): string { return this._templateQuery; }

  public set resultSourceId(value: string) { this._resultSourceId = value; }
  public get resultSourceId(): string { return this._resultSourceId; }

  public set sortList(value: string) { this._sortList = value; }
  public get sortList(): string { return this._sortList; }

  public set enableQueryRules(value: boolean) { this._enableQueryRules = value; }
  public get enableQueryRules(): boolean { return this._enableQueryRules; }

  private _localPnPSetup: SPRest;

  public constructor(webPartContext: IWebPartContext) {
    this._context = webPartContext;
    // Setup Moment Locales
    moment.locale(this._context.pageContext.cultureInfo.currentUICultureName);
    // Setup the PnP JS instance
    const consoleListener = new ConsoleListener();
    Logger.subscribe(consoleListener);

    // To limit the payload size, we set odata=nometadata
    // We just need to get list items here
    // We use a local configuration to avoid conflicts with other Web Parts
    this._localPnPSetup = sp.configure({
      headers: {
        Accept: 'application/json; odata=nometadata',
      },
    }, this._context.pageContext.web.absoluteUrl);
  }

  /**
   * Performs a search query against SharePoint
   * @param query The search query in KQL format
   * @return The search results
   */
  public async search(query: string, refiners?: string, refinementFilters?: IRefinementFilter[], pageNumber?: number): Promise<ISearchResults> {

    const searchQuery: SearchQuery = {};
    let sortedRefiners: string[] = [];

    // Search paging option is one based
    const page = pageNumber ? pageNumber : 1;

    searchQuery.ClientType = 'ContentSearchRegular';
    searchQuery.Querytext = query;

    // Disable query rules by default if not specified
    searchQuery.EnableQueryRules = this._enableQueryRules ? this._enableQueryRules : false;

    if (this._resultSourceId) {
      searchQuery.SourceId = this._resultSourceId;
    } else {
      // To be able to use search query variable according to the current context
      // http://www.techmikael.com/2015/07/sharepoint-rest-do-support-query.html
      searchQuery.QueryTemplate = this._templateQuery;
    }

    const defaultRowLimit = 50;
    searchQuery.RowLimit = this._resultsCount ? this._resultsCount : defaultRowLimit;
    searchQuery.SelectProperties = this._selectedProperties;
    searchQuery.TrimDuplicates = false;

    let sortList: Sort[] = [
      {
        Direction: SortDirection.Descending,
        Property: 'Created'
      },
      {
        Direction: SortDirection.Ascending,
        Property: 'Size'
      }
    ];

    if (this._sortList) {
      const sortOrders = this._sortList.split(',');
      sortList = sortOrders.map((sorter) => {
        const sort = sorter.split(':');
        const s: Sort = { Property: sort[0].trim(), Direction: SortDirection.Descending };
        if (sort.indexOf('[') !== -1) {
          s.Direction = SortDirection.FQLFormula;
        } else if (sort.length > 1) {
          const direction = sort[1].trim().toLocaleLowerCase();
          s.Direction = direction === "ascending" ? SortDirection.Ascending : SortDirection.Descending;
        }
        return s;
      });
    }

    searchQuery.SortList = sortList;

    if (refiners) {
      // Get the refiners order specified in the property pane
      sortedRefiners = refiners.split(',');
      searchQuery.Refiners = refiners ? refiners : '';
    }

    if (refinementFilters) {
      if (refinementFilters.length > 0) {
        searchQuery.RefinementFilters = [this._buildRefinementQueryString(refinementFilters)];
      }
    }

    const results: ISearchResults = {
      RefinementResults: [],
      RelevantResults: [],
      TotalRows: 0
    };

    try {
      if (!this._initialSearchResult || page === 1) {
        this._initialSearchResult = await this._localPnPSetup.search(searchQuery);
      }

      const allItemsPromises: Array<Promise<any>> = [];
      let refinementResults: IRefinementResult[] = [];

      // Need to do this check
      // More info here: https://github.com/SharePoint/PnP-JS-Core/issues/337
      if (this._initialSearchResult.RawSearchResults.PrimaryQueryResult) {

        // Be careful, there was an issue with paging calculation under 2.0.8 version of sp-pnp-js library
        // More info https://github.com/SharePoint/PnP-JS-Core/issues/535
        let r2 = this._initialSearchResult;
        if (page > 1) {
          r2 = await this._initialSearchResult.getPage(page, this._resultsCount);
        }

        const resultRows = r2.RawSearchResults.PrimaryQueryResult.RelevantResults.Table.Rows;
        const refinementResultsRows = r2.RawSearchResults.PrimaryQueryResult.RefinementResults;

        const refinementRows = refinementResultsRows ? refinementResultsRows['Refiners'] : [];
        if (refinementRows.length > 0) {
          // const component = await System.import(
          //     /* webpackChunkName: 'search-handlebars-helpers' */
          //     'handlebars-helpers'
          // );
          //
          // this._helper = component({
          //     handlebars: Handlebars
          // });
        }

        // Map search results
        resultRows.map((elt) => {

          const p1 = new Promise<ISearchResult>((resolvep1, rejectp1) => {

            // Build item result dynamically
            // We can't type the response here because search results are by definition too heterogeneous so we treat them as key-value object
            const result: ISearchResult = {};

            elt.Cells.map((item) => {
              result[item.Key] = item.Value;
            });

            // Get the icon source URL
            this._mapToIcon(result.Filename ? result.Filename : Text.format('.{0}', result.FileExtension)).then((IconUrl) => {

              result.IconSrc = IconUrl;
              resolvep1(result);

            }).catch((error) => {
              rejectp1(error);
            });
          });

          allItemsPromises.push(p1);
        });

        // Map refinement results
        refinementRows.map((refiner) => {

          refinementResults.push({
            FilterName: refiner.Name,
            Values: [{
              RefinementCount: parseInt(refiner.Entries.RefinementCount, 10),
              RefinementName: this._formatDate(refiner.Entries.RefinementName), // This value will appear in the selected filter bar
              RefinementToken: refiner.Entries.RefinementToken,
              RefinementValue: this._formatDate(refiner.Entries.RefinementValue), // This value will appear in the filter panel
            }],
          });
        });

        // Resolve all the promises once to get news
        const relevantResults: ISearchResult[] = await Promise.all(allItemsPromises);

        // Sort refiners according to the property pane value
        refinementResults = sortBy(refinementResults, (refinement) => {

          // Get the index of the corresponding filter name
          return sortedRefiners.indexOf(refinement.FilterName);
        });

        results.RelevantResults = relevantResults;
        results.RefinementResults = refinementResults;
        results.TotalRows = this._initialSearchResult.TotalRows;
      }
      return results;

    } catch (error) {
      Logger.write('[SharePointDataProvider.search()]: Error: ' + error, LogLevel.Error);
      throw error;
    }
  }

  /**
   * Retrieves search query suggestions
   * @param query the term to suggest from
   */
  public async suggest(query: string): Promise<string[]> {

    let suggestions: string[] = [];

    const searchSuggestQuery: SearchSuggestQuery = {
      count: 10,
      culture: LocalizationHelper.getLocaleId(this._context.pageContext.cultureInfo.currentUICultureName).toString(),
      hitHighlighting: true,
      preQuery: true,
      prefixMatch: true,
      querytext: query
    };

    try {
      const response = await this._localPnPSetup.searchSuggest(searchSuggestQuery);

      if (response.Queries.length > 0) {

        // Get only the suggesiton string value
        suggestions = response.Queries.map((elt) => {
          return elt.Query;
        });
      }

      return suggestions;

    } catch (error) {
      Logger.write("[SharePointDataProvider.suggest()]: Error: " + error, LogLevel.Error);
      throw error;
    }
  }

  /**
   * Gets the icon corresponding to the file name extension
   * @param filename The file name (ex: file.pdf)
   */
  private async _mapToIcon(filename: string): Promise<string> {

    const webAbsoluteUrl = this._context.pageContext.web.absoluteUrl;
    const web = new Web(webAbsoluteUrl);

    try {
      const encodedFileName = filename ? filename.replace(/['']/g, '') : '';
      const iconFileName = await web.mapToIcon(encodedFileName, 1);
      const iconUrl = webAbsoluteUrl + '/_layouts/15/images/' + iconFileName;

      return iconUrl;

    } catch (error) {
      Logger.write('[SharePointDataProvider._mapToIcon()]: Error: ' + error, LogLevel.Error);
      throw error;
    }
  }

  /**
   * Find and eeplace ISO 8601 dates in the string by a friendly value
   * @param inputValue The string to format
   */
  private _formatDate(inputValue: string): string {

    const iso8061rgx = /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/g;
    const matches = inputValue.match(iso8061rgx);

    let updatedInputValue = inputValue;

    if (matches) {
      matches.map((match) => {
        updatedInputValue = updatedInputValue.replace(match, moment(match).format("LL"));
      });
    }

    return updatedInputValue;
  }

  /**
   * Build the refinement condition in FQL format
   * @param selectedFilters The selected filter array
   */
  private _buildRefinementQueryString(selectedFilters: IRefinementFilter[]): string {

    const refinementQueryConditions: string[] = [];
    let refinementQueryString: string = null;

    const refinementFilters = mapValues(groupBy(selectedFilters, 'FilterName'), (values) => {
      const refinementFilter = values.map((filter) => {
        return filter.Value.RefinementToken;
      });

      return refinementFilter.length > 1 ? Text.format('or({0})', refinementFilter) : refinementFilter.toString();
    });

    mapKeys(refinementFilters, (value, key) => {
      refinementQueryConditions.push(key + ':' + value);
    });

    const conditionsCount = refinementQueryConditions.length;

    switch (true) {

      // No filters
      case (conditionsCount === 0): {
        refinementQueryString = null;
        break;
      }

      // Just one filter
      case (conditionsCount === 1): {
        refinementQueryString = refinementQueryConditions[0].toString();
        break;
      }

      // Multiple filters
      case (conditionsCount > 1): {
        refinementQueryString = Text.format('and({0})', refinementQueryConditions.toString());
        break;
      }
    }

    return refinementQueryString;
  }
}
