import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import {
  forkJoin,
  Observable,
  of,
} from 'rxjs';
import {
  catchError,
  map,
  mergeMap,
  take,
} from 'rxjs/operators';

import { SuggestionConfig } from '../../config/suggestion-config.interfaces';
import { environment } from '../../environments/environment';
import {
  SortDirection,
  SortOptions,
} from '../core/cache/models/sort-options.model';
import { FindListOptions } from '../core/data/find-list-options.model';
import { PaginatedList } from '../core/data/paginated-list.model';
import { RemoteData } from '../core/data/remote-data';
import { Suggestion } from '../core/notifications/suggestions/models/suggestion.model';
import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model';
import { SuggestionDataService } from '../core/notifications/suggestions/suggestion-data.service';
import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service';
import { ResearcherProfile } from '../core/profile/model/researcher-profile.model';
import { ResearcherProfileDataService } from '../core/profile/researcher-profile-data.service';
import { NoContent } from '../core/shared/NoContent.model';
import {
  getFinishedRemoteData,
  getFirstCompletedRemoteData,
  getFirstSucceededRemoteDataPayload,
  getFirstSucceededRemoteListPayload,
} from '../core/shared/operators';
import { WorkspaceItem } from '../core/submission/models/workspaceitem.model';
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
import {
  hasNoValue,
  hasValue,
  isNotEmpty,
} from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
import { getSuggestionPageRoute } from '../suggestions-page/suggestions-page-routing-paths';

/**
 * useful for multiple approvals and ignores operation
 * */
export interface SuggestionBulkResult {
  success: number;
  fails: number;
}

/**
 * The service handling all Suggestion Target  requests to the REST service.
 */
@Injectable({ providedIn: 'root' })
export class SuggestionsService {

  /**
   * Initialize the service variables.
   * @param {ResearcherProfileDataService} researcherProfileService
   * @param {SuggestionTargetDataService} suggestionTargetDataService
   * @param {SuggestionDataService} suggestionsDataService
   * @param translateService
   */
  constructor(
    private researcherProfileService: ResearcherProfileDataService,
    private suggestionsDataService: SuggestionDataService,
    private suggestionTargetDataService: SuggestionTargetDataService,
    private translateService: TranslateService,
  ) {
  }

  /**
   * Return the list of Suggestion Target managing pagination and errors.
   *
   * @param source
   *    The source for which to retrieve targets
   * @param elementsPerPage
   *    The number of the target per page
   * @param currentPage
   *    The page number to retrieve
   * @return Observable<PaginatedList<OpenaireReciterSuggestionTarget>>
   *    The list of Suggestion Targets.
   */
  public getTargets(source, elementsPerPage, currentPage): Observable<PaginatedList<SuggestionTarget>> {
    const sortOptions = new SortOptions('display', SortDirection.ASC);

    const findListOptions: FindListOptions = {
      elementsPerPage: elementsPerPage,
      currentPage: currentPage,
      sort: sortOptions,
    };

    return this.suggestionTargetDataService.getTargetsBySource(source, findListOptions).pipe(
      getFinishedRemoteData(),
      take(1),
      map((rd: RemoteData<PaginatedList<SuggestionTarget>>) => {
        if (rd.hasSucceeded) {
          return rd.payload;
        } else {
          throw new Error('Can\'t retrieve Suggestion Target from the Search Target REST service');
        }
      }),
    );
  }

  /**
   * Return the list of review suggestions Target managing pagination and errors.
   *
   * @param targetId
   *    The target id for which to find suggestions.
   * @param elementsPerPage
   *    The number of the target per page
   * @param currentPage
   *    The page number to retrieve
   * @param sortOptions
   *    The sort options
   * @return Observable<RemoteData<PaginatedList<Suggestion>>>
   *    The list of Suggestion.
   */
  public getSuggestions(targetId: string, elementsPerPage, currentPage, sortOptions: SortOptions): Observable<RemoteData<PaginatedList<Suggestion>>> {
    const [source, target] = targetId.split(':');

    const findListOptions: FindListOptions = {
      elementsPerPage: elementsPerPage,
      currentPage: currentPage,
      sort: sortOptions,
    };

    return this.suggestionsDataService.getSuggestionsByTargetAndSource(target, source, findListOptions);
  }

  /**
   * Clear suggestions requests from cache
   */
  public clearSuggestionRequests() {
    this.suggestionsDataService.clearSuggestionRequests();
  }

  /**
   * Used to delete Suggestion
   * @suggestionId
   */
  public deleteReviewedSuggestion(suggestionId: string): Observable<RemoteData<NoContent>> {
    return this.suggestionsDataService.deleteSuggestion(suggestionId).pipe(
      map((response: RemoteData<NoContent>) => {
        if (response.isSuccess) {
          return response;
        } else {
          throw new Error('Can\'t delete Suggestion from the Search Target REST service');
        }
      }),
      take(1),
    );
  }

  /**
   * Retrieve suggestion targets for the given user
   *
   * @param userUuid
   *   The EPerson id for which to retrieve suggestion targets
   */
  public retrieveCurrentUserSuggestions(userUuid: string): Observable<SuggestionTarget[]> {
    if (hasNoValue(userUuid)) {
      return of([]);
    }
    return this.researcherProfileService.findById(userUuid, true, true,  followLink('item')).pipe(
      getFirstCompletedRemoteData(),
      mergeMap((profile: RemoteData<ResearcherProfile> ) => {
        if (isNotEmpty(profile) && profile.hasSucceeded && isNotEmpty(profile.payload)) {
          return this.researcherProfileService.findRelatedItemId(profile.payload).pipe(
            mergeMap((itemId: string) => {
              return this.suggestionTargetDataService.getTargetsByUser(itemId).pipe(
                getFirstSucceededRemoteListPayload(),
              );
            }),
          );
        } else {
          return of([]);
        }
      }),
      catchError(() => of([])),
    );
  }

  /**
   * Perform the approve and import operation over a single suggestion
   * @param suggestion target suggestion
   * @param collectionId the collectionId
   * @param workspaceitemService injected dependency
   * @private
   */
  public approveAndImport(workspaceitemService: WorkspaceitemDataService,
    suggestion: Suggestion,
    collectionId: string): Observable<WorkspaceItem> {

    const resolvedCollectionId = this.resolveCollectionId(suggestion, collectionId);
    return workspaceitemService.importExternalSourceEntry(suggestion.externalSourceUri, resolvedCollectionId)
      .pipe(
        getFirstSucceededRemoteDataPayload(),
        catchError(() => of(null)),
      );
  }

  /**
   * Perform the delete operation over a single suggestion.
   * @param suggestionId
   */
  public ignoreSuggestion(suggestionId): Observable<RemoteData<NoContent>> {
    return this.deleteReviewedSuggestion(suggestionId).pipe(
      catchError(() => of(null)),
    );
  }

  /**
   * Perform a bulk approve and import operation.
   * @param workspaceitemService injected dependency
   * @param suggestions the array containing the suggestions
   * @param collectionId the collectionId
   */
  public approveAndImportMultiple(workspaceitemService: WorkspaceitemDataService,
    suggestions: Suggestion[],
    collectionId: string): Observable<SuggestionBulkResult> {

    return forkJoin(suggestions.map((suggestion: Suggestion) =>
      this.approveAndImport(workspaceitemService, suggestion, collectionId)))
      .pipe(map((results: WorkspaceItem[]) => {
        return {
          success: results.filter((result) => result != null).length,
          fails: results.filter((result) => result == null).length,
        };
      }), take(1));
  }

  /**
   * Perform a bulk ignoreSuggestion operation.
   * @param suggestions the array containing the suggestions
   */
  public ignoreSuggestionMultiple(suggestions: Suggestion[]): Observable<SuggestionBulkResult> {
    return forkJoin(suggestions.map((suggestion: Suggestion) => this.ignoreSuggestion(suggestion.id)))
      .pipe(map((results: RemoteData<NoContent>[]) => {
        return {
          success: results.filter((result) => result != null).length,
          fails: results.filter((result) => result == null).length,
        };
      }), take(1));
  }

  /**
   * Get the researcher uuid (for navigation purpose) from a target instance.
   * TODO Find a better way
   * @param target
   * @return the researchUuid
   */
  public getTargetUuid(target: SuggestionTarget): string {
    const tokens = target.id.split(':');
    return tokens.length === 2 ? tokens[1] : null;
  }

  /**
   * Interpolated params to build the notification suggestions notification.
   * @param suggestionTarget
   */
  public getNotificationSuggestionInterpolation(suggestionTarget: SuggestionTarget): any {
    return {
      count: suggestionTarget.total,
      source: this.translateService.instant(this.translateSuggestionSource(suggestionTarget.source)),
      type:  this.translateService.instant(this.translateSuggestionType(suggestionTarget.source)),
      suggestionId: suggestionTarget.id,
      displayName: suggestionTarget.display,
      url: getSuggestionPageRoute(suggestionTarget.id),
    };
  }

  public translateSuggestionType(source: string): string {
    return 'suggestion.type.' + source;
  }

  public translateSuggestionSource(source: string): string {
    return 'suggestion.source.' + source;
  }

  /**
   * If the provided collectionId ha no value, tries to resolve it by suggestion source.
   * @param suggestion
   * @param collectionId
   */
  public resolveCollectionId(suggestion: Suggestion, collectionId): string {
    if (hasValue(collectionId)) {
      return collectionId;
    }
    return environment.suggestion
      .find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source)
      .collectionId;
  }

  /**
   * Return true if all the suggestion are configured with the same fixed collection
   * in the configuration.
   * @param suggestions
   */
  public isCollectionFixed(suggestions: Suggestion[]): boolean {
    return this.getFixedCollectionIds(suggestions).length === 1;
  }

  private getFixedCollectionIds(suggestions: Suggestion[]): string[] {
    const collectionIds = {};
    suggestions.forEach((suggestion: Suggestion) => {
      const conf = environment.suggestion.find((suggestionConf: SuggestionConfig) => suggestionConf.source === suggestion.source);
      if (hasValue(conf)) {
        collectionIds[conf.collectionId] = true;
      }
    });
    return Object.keys(collectionIds);
  }
}