import { Store } from 'redux';

import { RootState } from 'mweb/common/reducers/root';
import { trackingTrackLatency } from 'mweb/common/actions/tracking';
import {
  getLatencyBandwidthSpadeData,
  getLatencyBandwidthTransitionCompleteSpadeData,
  getLatencyComponentInitSpadeData,
  getLatencyComponentInteractiveSpadeData,
  getLatencySpadeData,
  getLatencyCustomEventSpadeData,
} from 'mweb/common/selectors/tracking/latency';
import { getRandomValue } from 'mweb/common/utils/experimentUtils';

const APP_BOOT_MARK = 'appBoot';
// this is a random UUID generated for algorithmic parity and is not tracked
const COMPONENT_UUID = 'b7e5853c-4a03-5db2-9824-409ef1a53dc6';
// 5% of users report component-level timings
const COMPONENT_THRESHOLD = 0.05;

enum CustomEventGroup {
  Page = 'page',
  Chat = 'chat',
  Player = 'player',
}

interface CustomEventOpts {
  group: CustomEventGroup;
  key: string;
  label: string;
  epochTime?: number;
  duration?: number;
}

export type CustomLatencyEvent = keyof Pick<
  LatencyReporter,
  'playerReady' | 'playerPlay' | 'chatConnected'
>;

export class LatencyReporter {
  public pageTimeOriginMark: string = 'navigationStart';
  private pageTimeOrigin: number = window.performance.timing.navigationStart;
  private sessionTimeOrigin: number = window.performance.timing.navigationStart;
  private isPageInteractive: boolean = false;
  private doComponentLevelReporting: boolean = false;
  private isDocumentHidden: boolean = false;

  constructor(private store: Store<RootState>) {
    window.performance.mark(APP_BOOT_MARK);
    document.addEventListener('visibilitychange', this.toggleVisibility);

    this.reportFetchStart();
    this.reportAppBooted();
    this.reportFirstCue();
    this.reportFirstByte();

    const deviceID = this.store.getState().device.deviceID;
    if (getRandomValue(COMPONENT_UUID, deviceID) < COMPONENT_THRESHOLD) {
      this.doComponentLevelReporting = true;
    }
  }

  toggleVisibility = () => {
    this.isDocumentHidden = true;
  };

  signalRenderingRoute(pageNumber: number): void {
    this.pageTimeOriginMark = `Page${pageNumber}Start`;
    window.performance.mark(this.pageTimeOriginMark);
    this.pageTimeOrigin =
      window.performance.getEntriesByName(this.pageTimeOriginMark, 'mark')[0]
        .startTime + this.sessionTimeOrigin;
    this.isPageInteractive = false;
    this.isDocumentHidden = false;
  }

  signalComponentInit(
    timeFromPage: number,
    name: string,
    id: number,
    parentName: string,
    parentID: number,
  ): void {
    if (this.doComponentLevelReporting) {
      this.store.dispatch(
        trackingTrackLatency({
          event: 'benchmark_component_initializing',
          properties: getLatencyComponentInitSpadeData({
            state: this.store.getState(),
            epochTime: this.pageTimeOrigin + timeFromPage,
            name,
            id,
            parentName,
            parentID,
          }),
        }),
      );
    }
  }

  signalComponentInteractive(
    timeFromPage: number,
    timeFromInit: number,
    name: string,
    id: number,
    parentName: string,
    parentID: number,
  ): void {
    if (this.doComponentLevelReporting) {
      this.store.dispatch(
        trackingTrackLatency({
          event: 'benchmark_component_interactive',
          properties: getLatencyComponentInteractiveSpadeData({
            state: this.store.getState(),
            epochTime: this.pageTimeOrigin + timeFromPage,
            isCritical: !this.isPageInteractive,
            timeFromInit,
            name,
            id,
            parentName,
            parentID,
          }),
        }),
      );
    }
  }

  signalPageInteractive(timeFromPage: number): void {
    this.isPageInteractive = true;
    this.store.dispatch(
      trackingTrackLatency({
        event: 'benchmark_complete_transition',
        properties: getLatencyBandwidthTransitionCompleteSpadeData({
          state: this.store.getState(),
          epochTime: this.pageTimeOrigin + timeFromPage,
          timeFromPage,
          isFirstPage: this.isFirstPage(),
          isDocumentHidden: this.isDocumentHidden,
        }),
      }),
    );
  }

  /**
   * Custom latency benchmark for when player is playing
   */
  playerReady = (): void => {
    this.reportCustomEvent({
      group: CustomEventGroup.Player,
      key: 'player-loaded',
      label: 'loaded',
    });
  };

  /**
   * Custom latency benchmark for when player is playing
   */
  playerPlay = (): void => {
    this.reportCustomEvent({
      group: CustomEventGroup.Player,
      key: 'player-played',
      label: 'first frame',
    });
  };

  /**
   * Custom latency benchmark for when chat is connected
   */
  chatConnected = (): void => {
    this.reportCustomEvent({
      group: CustomEventGroup.Chat,
      key: 'chat-connected',
      label: 'connected',
    });
  };

  getPageTimeOriginOffset(): number {
    return this.pageTimeOrigin - this.sessionTimeOrigin;
  }

  private isFirstPage(): boolean {
    return this.pageTimeOriginMark === 'navigationStart';
  }

  private reportFetchStart(): void {
    this.store.dispatch(
      trackingTrackLatency({
        event: 'benchmark_fetch_start',
        properties: getLatencyBandwidthSpadeData({
          state: this.store.getState(),
          epochTime: this.sessionTimeOrigin,
        }),
      }),
    );
  }

  private reportAppBooted(): void {
    const mark = window.performance.getEntriesByName(APP_BOOT_MARK, 'mark')[0];
    this.store.dispatch(
      trackingTrackLatency({
        event: 'benchmark_app_booted',
        properties: getLatencySpadeData({
          state: this.store.getState(),
          epochTime: this.sessionTimeOrigin + mark.startTime,
        }),
      }),
    );
  }

  private reportFirstCue(): void {
    const firstPaintMark = window.performance.getEntriesByName(
      'first-contentful-paint',
    )[0];
    const firstPaintStartTime = firstPaintMark && firstPaintMark.startTime;
    this.reportCustomEvent({
      group: CustomEventGroup.Page,
      key: 'first-cue',
      label: firstPaintStartTime ? 'first paint' : 'dom interactive',
      epochTime: firstPaintStartTime
        ? this.sessionTimeOrigin + firstPaintStartTime
        : window.performance.timing.domInteractive,
      duration: firstPaintStartTime
        ? firstPaintStartTime
        : window.performance.timing.domInteractive - this.sessionTimeOrigin,
    });
  }

  private reportFirstByte(): void {
    this.reportCustomEvent({
      group: CustomEventGroup.Page,
      key: 'first-byte',
      label: 'first byte',
      duration:
        window.performance.timing.responseStart -
        window.performance.timing.requestStart,
    });
  }

  private reportCustomEvent({
    group,
    key,
    label,
    epochTime = new Date().valueOf(),
    duration = epochTime - this.pageTimeOrigin,
  }: CustomEventOpts): void {
    this.store.dispatch(
      trackingTrackLatency({
        event: 'benchmark_custom_event',
        properties: getLatencyCustomEventSpadeData({
          state: this.store.getState(),
          isFirstPage: this.isFirstPage(),
          isDocumentHidden: this.isDocumentHidden,
          epochTime,
          duration,
          group,
          key,
          label,
        }),
      }),
    );
  }
}
