/* eslint-disable no-undef */
import { ISignalListener } from './types';
import { IRecorderCacheSync, IStorageSync } from '../storages/types';
import { fromImpressionsCollector } from '../sync/submitters/impressionsSubmitter';
import { fromImpressionCountsCollector } from '../sync/submitters/impressionCountsSubmitter';
import { IResponse, IServiceApi } from '../services/types';
import { ISettings } from '../types';
import SplitIO from '../../types/splitio';
import { ImpressionsPayload } from '../sync/submitters/types';
import { objectAssign } from '../utils/lang/objectAssign';
import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING, SUBMITTERS_PUSH_PAGE_HIDDEN } from '../logger/constants';
import { ISyncManager } from '../sync/types';
import { isConsentGranted } from '../consent';
import { ISdkFactoryContextSync } from '../sdkFactory/types';

const VISIBILITYCHANGE_EVENT = 'visibilitychange';
const PAGEHIDE_EVENT = 'pagehide';
const EVENT_NAME = 'for visibilitychange and pagehide events.';

/**
 * We'll listen for events over the window object.
 */
export class BrowserSignalListener implements ISignalListener {

  private syncManager: ISyncManager | undefined;
  private settings: ISettings;
  private storage: IStorageSync;
  private serviceApi: IServiceApi;
  private fromImpressionsCollector: (data: SplitIO.ImpressionDTO[]) => ImpressionsPayload;

  constructor({ syncManager, settings, storage, serviceApi, entityType }: ISdkFactoryContextSync) {
    this.syncManager = syncManager;
    this.settings = settings;
    this.storage = storage;
    this.serviceApi = serviceApi;
    this.flushData = this.flushData.bind(this);
    this.flushDataIfHidden = this.flushDataIfHidden.bind(this);
    this.fromImpressionsCollector = fromImpressionsCollector.bind(undefined, settings.core.labelsEnabled, entityType);
  }

  /**
   * start method.
   * Called when SplitFactory is initialized, it adds event listeners to close streaming and flush impressions and events.
   */
  start() {
    this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]);
    if (typeof document !== 'undefined' && document.addEventListener) {
      // Flush data whenever the page is hidden or unloaded.
      document.addEventListener(VISIBILITYCHANGE_EVENT, this.flushDataIfHidden);
    }
    if (typeof window !== 'undefined' && window.addEventListener) {
      // Some browsers, like Safari, does not fire the `visibilitychange` event when the page is being unloaded. Therefore, we also flush data in the `pagehide` event.
      // If both events are triggered, the latter will find the storage empty, so no duplicate data will be submitted.
      window.addEventListener(PAGEHIDE_EVENT, this.flushData);
    }
  }

  /**
   * stop method.
   * Called when client is destroyed, it removes event listeners.
   */
  stop() {
    this.settings.log.debug(CLEANUP_DEREGISTERING, [EVENT_NAME]);
    if (typeof document !== 'undefined' && document.removeEventListener) {
      document.removeEventListener(VISIBILITYCHANGE_EVENT, this.flushDataIfHidden);
    }
    if (typeof window !== 'undefined' && window.removeEventListener) {
      window.removeEventListener(PAGEHIDE_EVENT, this.flushData);
    }
  }

  /**
   * flushData method.
   * Called when pagehide event is triggered. It flushed remaining impressions and events to the backend,
   * using beacon API if possible, or falling back to regular post transport.
   */
  flushData() {
    if (!this.syncManager) return; // In consumer mode there is not sync manager and data to flush
    const { events, telemetry } = this.settings.urls;

    // Flush impressions & events data if there is user consent
    if (isConsentGranted(this.settings)) {
      const extraMetadata = {
        // sim stands for Sync/Split Impressions Mode
        sim: this.settings.sync.impressionsMode
      };

      this._flushData(events + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
      this._flushData(events + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
      this._flushData(events + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
      this._flushData(telemetry + '/v1/keys/cs/beacon', this.storage.uniqueKeys, this.serviceApi.postUniqueKeysBulkCs);
    }

    // Flush telemetry data
    if (this.storage.telemetry) this._flushData(telemetry + '/v1/metrics/usage/beacon', this.storage.telemetry, this.serviceApi.postMetricsUsage);
  }

  flushDataIfHidden() {
    // Precondition: document defined
    if (document.visibilityState === 'hidden') this.flushData(); // On a 'visibilitychange' event, flush data if state is hidden
  }

  private _flushData<T>(url: string, cache: IRecorderCacheSync<T>, postService: (body: string) => Promise<IResponse>, fromCacheToPayload?: (cacheData: T) => any, extraMetadata?: {}) {
    // if there is data in cache, send it to backend
    if (!cache.isEmpty()) {
      const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.pop()) : cache.pop();
      if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
        postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op to handle possible promise rejection
      }
      this.settings.log.debug(SUBMITTERS_PUSH_PAGE_HIDDEN, [cache.name]);
    }
  }

  /**
   * _sendBeacon method.
   * Util method that check if beacon API is available, build the payload and send it.
   * Returns true if beacon API was used successfully, false otherwise.
   */
  private _sendBeacon(url: string, data: any, extraMetadata?: {}) {
    if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
      const json = {
        entries: data,
        token: this.settings.core.authorizationKey,
        sdk: this.settings.version
      };

      // Extend with endpoint specific metadata where needed
      if (extraMetadata) objectAssign(json, extraMetadata);

      // Stringify the payload
      const payload = JSON.stringify(json);

      // https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
      try {
        return navigator.sendBeacon(url, payload);
      } catch (e) {
        return false;
      }
    }
    return false;
  }
}
