import { IAnalyticsClient } from './AnalyticsClient';
import { DeviceUtils } from '../../utils/DeviceUtils';
import { PendingSearchEvent } from './PendingSearchEvent';
import { PendingSearchAsYouTypeSearchEvent } from './PendingSearchAsYouTypeSearchEvent';
import { AnalyticsEndpoint } from '../../rest/AnalyticsEndpoint';
import { Assert } from '../../misc/Assert';
import { Logger } from '../../misc/Logger';
import { IAnalyticsActionCause, analyticsActionCauseList } from './AnalyticsActionListMeta';
import { IQueryResult } from '../../rest/QueryResult';
import { ITopQueries } from '../../rest/TopQueries';
import {
  IChangeableAnalyticsMetaObject,
  IChangeableAnalyticsDataObject,
  IChangeAnalyticsCustomDataEventArgs,
  IAnalyticsEventArgs
} from '../../events/AnalyticsEvents';
import { Defer } from '../../misc/Defer';
import { $$ } from '../../utils/Dom';
import { AnalyticsEvents, IAnalyticsCustomEventArgs } from '../../events/AnalyticsEvents';
import { APIAnalyticsBuilder } from '../../rest/APIAnalyticsBuilder';
import { IAnalyticsEvent } from '../../rest/AnalyticsEvent';
import { IAPIAnalyticsEventResponse } from '../../rest/APIAnalyticsEventResponse';
import { ISearchEvent } from '../../rest/SearchEvent';
import { IClickEvent } from '../../rest/ClickEvent';
import { ICustomEvent } from '../../rest/CustomEvent';
import { QueryStateModel } from '../../models/QueryStateModel';
import { Component } from '../Base/Component';
import { version } from '../../misc/Version';
import { QueryUtils } from '../../utils/QueryUtils';
import * as _ from 'underscore';
import { IComponentBindings } from '../Base/ComponentBindings';
import { AnalyticsInformation } from './AnalyticsInformation';
import { QueryController } from '../../ControllersModules';

export class LiveAnalyticsClient implements IAnalyticsClient {
  public isContextual: boolean = false;
  public originContext: string = 'Search';

  private language = <string>String['locale'];
  private device = DeviceUtils.getDeviceName();
  private mobile = DeviceUtils.isMobileDevice();
  private pendingSearchEvent: PendingSearchEvent;
  private pendingSearchAsYouTypeSearchEvent: PendingSearchAsYouTypeSearchEvent;
  private logger: Logger;

  constructor(
    public endpoint: AnalyticsEndpoint,
    public rootElement: HTMLElement,
    public userId: string,
    public userDisplayName: string,
    public anonymous: boolean,
    public splitTestRunName: string,
    public splitTestRunVersion: string,
    public originLevel1: string,
    public sendToCloud: boolean,
    public bindings: IComponentBindings
  ) {
    Assert.exists(endpoint);
    Assert.exists(rootElement);
    Assert.isNonEmptyString(this.language);
    Assert.isNonEmptyString(this.device);
    Assert.isNonEmptyString(this.originLevel1);
    this.logger = new Logger(this);
  }

  public isActivated(): boolean {
    return true;
  }

  public getCurrentVisitId(): string {
    return this.endpoint.getCurrentVisitId();
  }

  public getCurrentVisitIdPromise(): Promise<string> {
    return this.endpoint.getCurrentVisitIdPromise();
  }

  public getCurrentEventCause() {
    if (this.pendingSearchEvent != null) {
      return this.pendingSearchEvent.getEventCause();
    }
    if (this.pendingSearchAsYouTypeSearchEvent != null) {
      return this.pendingSearchAsYouTypeSearchEvent.getEventCause();
    }
    return null;
  }

  public getCurrentEventMeta() {
    if (this.pendingSearchEvent != null) {
      return this.pendingSearchEvent.getEventMeta();
    }
    if (this.pendingSearchAsYouTypeSearchEvent != null) {
      return this.pendingSearchAsYouTypeSearchEvent.getEventMeta();
    }
    return null;
  }

  public logSearchEvent<TMeta>(actionCause: IAnalyticsActionCause, meta: TMeta) {
    var metaObject = this.buildMetaObject(meta);
    this.pushSearchEvent(actionCause, metaObject);
  }

  public logSearchAsYouType<TMeta>(actionCause: IAnalyticsActionCause, meta?: TMeta) {
    var metaObject = this.buildMetaObject(meta);
    this.pushSearchAsYouTypeEvent(actionCause, metaObject);
  }

  public logClickEvent<TMeta>(
    actionCause: IAnalyticsActionCause,
    meta: TMeta,
    result: IQueryResult,
    element: HTMLElement
  ): Promise<IAPIAnalyticsEventResponse> {
    let metaObject = this.buildMetaObject(meta, result);
    return this.pushClickEvent(actionCause, metaObject, result, element);
  }

  public logCustomEvent<TMeta>(
    actionCause: IAnalyticsActionCause,
    meta: TMeta,
    element: HTMLElement,
    result?: IQueryResult
  ): Promise<IAPIAnalyticsEventResponse> {
    const metaObject = this.buildMetaObject(meta, result);
    return this.pushCustomEvent(actionCause, metaObject, element);
  }

  public getTopQueries(params: ITopQueries): Promise<string[]> {
    return this.endpoint.getTopQueries(params);
  }

  public sendAllPendingEvents() {
    if (this.pendingSearchAsYouTypeSearchEvent) {
      this.pendingSearchAsYouTypeSearchEvent.sendRightNow();
    }
  }

  public cancelAllPendingEvents() {
    if (this.pendingSearchAsYouTypeSearchEvent) {
      this.pendingSearchAsYouTypeSearchEvent.cancel();
      this.pendingSearchAsYouTypeSearchEvent = null;
    }
    if (this.pendingSearchEvent) {
      this.pendingSearchEvent.cancel();
      this.pendingSearchEvent = null;
    }
  }

  public getPendingSearchEvent(): PendingSearchEvent {
    if (this.pendingSearchEvent) {
      return this.pendingSearchEvent;
    } else if (this.pendingSearchAsYouTypeSearchEvent) {
      return this.pendingSearchAsYouTypeSearchEvent;
    }
    return null;
  }

  public warnAboutSearchEvent() {
    if (_.isUndefined(this.pendingSearchEvent) && _.isUndefined(this.pendingSearchAsYouTypeSearchEvent)) {
      this.logger.warn(
        'A search was triggered, but no analytics event was logged. If you wish to have consistent analytics data, consider logging a search event using the methods provided by the framework',
        'https://docs.coveo.com/en/2726/#logging-your-own-search-events'
      );
      if (window['console'] && console.trace) {
        console.trace();
      }
    }
  }

  public setOriginContext(originContext: string) {
    this.originContext = originContext;
  }

  public getOriginContext() {
    return this.originContext;
  }

  public getUserDisplayName(): string {
    return this.userDisplayName;
  }

  private pushCustomEvent(
    actionCause: IAnalyticsActionCause,
    metaObject: IChangeableAnalyticsMetaObject,
    element?: HTMLElement
  ): Promise<IAPIAnalyticsEventResponse> {
    const customEvent = this.buildCustomEvent(actionCause, metaObject, element);
    this.triggerChangeAnalyticsCustomData('CustomEvent', metaObject, customEvent);
    this.checkToSendAnyPendingSearchAsYouType(actionCause);
    $$(this.rootElement).trigger(AnalyticsEvents.customEvent, <IAnalyticsCustomEventArgs>{
      customEvent: APIAnalyticsBuilder.convertCustomEventToAPI(customEvent)
    });
    $$(this.rootElement).trigger(AnalyticsEvents.analyticsEventReady, <IAnalyticsEventArgs>{
      event: 'CoveoCustomEvent',
      coveoAnalyticsEventData: customEvent
    });
    return this.sendToCloud ? this.endpoint.sendCustomEvent(customEvent) : Promise.resolve(null);
  }

  private pushSearchEvent(actionCause: IAnalyticsActionCause, metaObject: IChangeableAnalyticsMetaObject) {
    Assert.exists(actionCause);
    if (this.pendingSearchEvent && this.pendingSearchEvent.getEventCause() !== actionCause.name) {
      this.pendingSearchEvent.stopRecording();
      this.pendingSearchEvent = null;
    }
    this.checkToSendAnyPendingSearchAsYouType(actionCause);

    if (!this.pendingSearchEvent) {
      var searchEvent = this.buildSearchEvent(actionCause, metaObject);
      this.triggerChangeAnalyticsCustomData('SearchEvent', metaObject, searchEvent);
      var pendingSearchEvent = (this.pendingSearchEvent = new PendingSearchEvent(
        this.rootElement,
        this.endpoint,
        searchEvent,
        this.sendToCloud
      ));

      Defer.defer(() => {
        // At this point all `duringQuery` events should have been fired, so we can forget
        // about the pending search event. It will finish processing automatically when
        // all the deferred that were caught terminate.
        this.pendingSearchEvent = undefined;
        pendingSearchEvent.stopRecording();
      });
    }
  }

  private checkToSendAnyPendingSearchAsYouType(actionCause: IAnalyticsActionCause) {
    if (this.eventIsNotRelatedToSearchbox(actionCause.name)) {
      this.sendAllPendingEvents();
    } else {
      this.cancelAnyPendingSearchAsYouTypeEvent();
    }
  }

  private pushSearchAsYouTypeEvent(actionCause: IAnalyticsActionCause, metaObject: IChangeableAnalyticsMetaObject) {
    this.cancelAnyPendingSearchAsYouTypeEvent();
    var searchEvent = this.buildSearchEvent(actionCause, metaObject);
    this.triggerChangeAnalyticsCustomData('SearchEvent', metaObject, searchEvent);
    this.pendingSearchAsYouTypeSearchEvent = new PendingSearchAsYouTypeSearchEvent(
      this.rootElement,
      this.endpoint,
      searchEvent,
      this.sendToCloud
    );
  }

  private pushClickEvent(
    actionCause: IAnalyticsActionCause,
    metaObject: IChangeableAnalyticsMetaObject,
    result: IQueryResult,
    element: HTMLElement
  ): Promise<IAPIAnalyticsEventResponse> {
    var event = this.buildClickEvent(actionCause, metaObject, result, element);
    this.checkToSendAnyPendingSearchAsYouType(actionCause);
    this.triggerChangeAnalyticsCustomData('ClickEvent', metaObject, event, { resultData: result });
    Assert.isNonEmptyString(event.searchQueryUid);
    Assert.isNonEmptyString(event.collectionName);
    Assert.isNonEmptyString(event.sourceName);
    Assert.isNumber(event.documentPosition);

    $$(this.rootElement).trigger(AnalyticsEvents.documentViewEvent, {
      documentViewEvent: APIAnalyticsBuilder.convertDocumentViewToAPI(event)
    });
    $$(this.rootElement).trigger(AnalyticsEvents.analyticsEventReady, <IAnalyticsEventArgs>{
      event: 'CoveoClickEvent',
      coveoAnalyticsEventData: event
    });
    return this.sendToCloud ? this.endpoint.sendDocumentViewEvent(event) : Promise.resolve(null);
  }

  private buildAnalyticsEvent(actionCause: IAnalyticsActionCause, metaObject: IChangeableAnalyticsMetaObject): IAnalyticsEvent {
    return {
      searchQueryUid: undefined,
      actionCause: actionCause.name,
      actionType: actionCause.type,
      username: this.userId,
      userDisplayName: this.userDisplayName,
      anonymous: this.anonymous,
      device: this.device,
      mobile: this.mobile,
      language: this.language,
      responseTime: undefined,
      originLevel1: this.originLevel1,
      originLevel2: this.getOriginLevel2(this.rootElement),
      originLevel3: document.referrer,
      originContext: this.originContext,
      customData: _.keys(metaObject).length > 0 ? metaObject : undefined,
      userAgent: navigator.userAgent,
      clientId: new AnalyticsInformation().clientId
    };
  }

  private buildSearchEvent(actionCause: IAnalyticsActionCause, metaObject: IChangeableAnalyticsMetaObject): ISearchEvent {
    return this.merge<ISearchEvent>(this.buildAnalyticsEvent(actionCause, metaObject), {
      searchQueryUid: undefined,
      pipeline: undefined,
      splitTestRunName: this.splitTestRunName,
      splitTestRunVersion: this.splitTestRunVersion,
      queryText: undefined,
      advancedQuery: undefined,
      results: undefined,
      resultsPerPage: undefined,
      pageNumber: undefined,
      didYouMean: undefined,
      facets: undefined,
      contextual: this.isContextual
    });
  }

  private buildClickEvent(
    actionCause: IAnalyticsActionCause,
    metaObject: IChangeableAnalyticsMetaObject,
    result: IQueryResult,
    element: HTMLElement
  ): IClickEvent {
    return this.merge<IClickEvent>(this.buildAnalyticsEvent(actionCause, metaObject), {
      searchQueryUid: result.queryUid,
      queryPipeline: result.pipeline,
      splitTestRunName: this.splitTestRunName || result.splitTestRun,
      splitTestRunVersion: this.splitTestRunVersion || (result.splitTestRun != undefined ? result.pipeline : undefined),
      documentUri: result.uri,
      documentUriHash: QueryUtils.getUriHash(result),
      documentUrl: result.clickUri,
      documentTitle: result.title,
      documentCategory: QueryUtils.getObjectType(result),
      originLevel2: this.getOriginLevel2(element),
      collectionName: QueryUtils.getCollection(result),
      sourceName: QueryUtils.getSource(result),
      documentPosition: result.index + 1,
      responseTime: 0,
      viewMethod: actionCause.name,
      rankingModifier: result.rankingModifier
    });
  }

  private buildCustomEvent(
    actionCause: IAnalyticsActionCause,
    metaObject: IChangeableAnalyticsMetaObject,
    element: HTMLElement
  ): ICustomEvent {
    return this.merge<ICustomEvent>(this.buildAnalyticsEvent(actionCause, metaObject), {
      lastSearchQueryUid: this.getLastSearchQueryUid(),
      eventType: actionCause.type,
      eventValue: actionCause.name,
      originLevel2: this.getOriginLevel2(element),
      responseTime: 0
    });
  }

  protected getOriginLevel2(element: HTMLElement): string {
    return this.resolveActiveTabFromElement(element) || 'default';
  }

  private getLastSearchQueryUid() {
    const queryController = Component.resolveBinding(Component.resolveRoot(this.rootElement), QueryController) as QueryController;
    if (!queryController) {
      return;
    }
    const lastResults = queryController.getLastResults();
    if (!lastResults) {
      return;
    }
    return lastResults.searchUid;
  }

  private buildMetaObject<TMeta>(meta: TMeta, result?: IQueryResult): IChangeableAnalyticsMetaObject {
    let modifiedMeta: IChangeableAnalyticsMetaObject = _.extend({}, meta);
    modifiedMeta['JSUIVersion'] = version.lib + ';' + version.product;

    const contentIDsAreAlreadySet = modifiedMeta['contentIDKey'] && modifiedMeta['contentIDValue'];

    if (!contentIDsAreAlreadySet && result) {
      let uniqueId = QueryUtils.getPermanentId(result);
      modifiedMeta['contentIDKey'] = uniqueId.fieldUsed;
      modifiedMeta['contentIDValue'] = uniqueId.fieldValue;
    }

    return modifiedMeta;
  }

  private cancelAnyPendingSearchAsYouTypeEvent() {
    if (this.pendingSearchAsYouTypeSearchEvent) {
      this.pendingSearchAsYouTypeSearchEvent.cancel();
      this.pendingSearchAsYouTypeSearchEvent = undefined;
    }
  }

  private resolveActiveTabFromElement(element: HTMLElement): string {
    Assert.exists(element);
    var queryStateModel = this.resolveQueryStateModel(element);
    return queryStateModel && <string>queryStateModel.get(QueryStateModel.attributesEnum.t);
  }

  private resolveQueryStateModel(rootElement: HTMLElement): QueryStateModel {
    return <QueryStateModel>Component.resolveBinding(rootElement, QueryStateModel);
  }

  private eventIsNotRelatedToSearchbox(event: string) {
    return event !== analyticsActionCauseList.searchboxSubmit.name && event !== analyticsActionCauseList.searchboxClear.name;
  }

  private triggerChangeAnalyticsCustomData(type: string, metaObject: IChangeableAnalyticsMetaObject, event: IAnalyticsEvent, data?: any) {
    // This is for backward compatibility. Before the analytics were using either numbered
    // metas in `metaDataAsNumber` of later on named metas in `metaDataAsString`. Thus we still
    // provide those properties in a deprecated way. Below we are moving any data that put
    // in them to the root.
    (<any>metaObject)['metaDataAsString'] = {};
    (<any>metaObject)['metaDataAsNumber'] = {};

    var changeableAnalyticsDataObject: IChangeableAnalyticsDataObject = {
      language: event.language,
      originLevel1: event.originLevel1,
      originLevel2: event.originLevel2,
      originLevel3: event.originLevel3,
      metaObject: metaObject
    };

    var args: IChangeAnalyticsCustomDataEventArgs = _.extend(
      {},
      {
        type: type,
        actionType: event.actionType,
        actionCause: event.actionCause
      },
      changeableAnalyticsDataObject,
      data
    );
    $$(this.rootElement).trigger(AnalyticsEvents.changeAnalyticsCustomData, args);

    event.language = args.language;
    event.originLevel1 = args.originLevel1;
    event.originLevel2 = args.originLevel2;
    event.originLevel3 = args.originLevel3;
    event.customData = metaObject;

    // This is for backward compatibility. Before the analytics were using either numbered
    // metas in `metaDataAsNumber` of later on named metas in `metaDataAsString`. We are now putting
    // them all at the root, and if I encounter the older properties I move them to the top
    // level after issuing a warning.

    var metaDataAsString = event.customData['metaDataAsString'];
    if (_.keys(metaDataAsString).length > 0) {
      this.logger.warn(
        "Using deprecated 'metaDataAsString' key to log custom analytics data. Custom meta should now be put at the root of the object."
      );
      _.extend(event.customData, metaDataAsString);
    }
    delete event.customData['metaDataAsString'];

    var metaDataAsNumber = event.customData['metaDataAsNumber'];
    if (_.keys(metaDataAsNumber).length > 0) {
      this.logger.warn(
        "Using deprecated 'metaDataAsNumber' key to log custom analytics data. Custom meta should now be put at the root of the object."
      );
      _.extend(event.customData, metaDataAsNumber);
    }
    delete event.customData['metaDataAsNumber'];
  }

  private merge<T extends IAnalyticsEvent>(first: IAnalyticsEvent, second: any): T {
    return _.extend({}, first, second);
  }
}
