import type { PlayerEmbed } from '../..';
import type { VideoEventDetail } from '@vouchfor/media-player-legacy';
import type { ReactiveController, ReactiveControllerHost } from 'lit';

import { findVouchId, getReportingMetadata, getUids } from './utils';
import { getEnvUrls } from '~/utils/env';

const MINIMUM_SEND_THRESHOLD = 1;

type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed;

type TrackingEvent = 'VOUCH_LOADED' | 'VOUCH_RESPONSE_VIEWED' | 'VIDEO_PLAYED' | 'VIDEO_STREAMED';
type TrackingPayload = {
  vouchId?: string;
  answerId?: string;
  streamStart?: number;
  streamEnd?: number;
  senderId?: string;
};

type BatchEvent = {
  event: TrackingEvent;
  payload: TrackingPayload & {
    time: string;
  };
};

type TimeMap = {
  [key: string]: number;
};

type BooleanMap = {
  [key: string]: boolean;
};

class TrackingController implements ReactiveController {
  host: PlayerEmbedHost;

  private _batchedEvents: BatchEvent[] = [];
  private _hasPlayed = false;
  private _hasLoaded: BooleanMap = {};
  private _answersViewed: BooleanMap = {};
  private _streamStartTime: TimeMap = {};
  private _streamLatestTime: TimeMap = {};
  private _currentlyPlayingVideo: VideoEventDetail | null = null;

  constructor(host: PlayerEmbedHost) {
    this.host = host;
    host.addController(this);
  }

  private _createTrackingEvent = (event: TrackingEvent, payload?: TrackingPayload) => {
    const vouchId = findVouchId(payload, this.host.vouch);

    if (!vouchId || this.host.disableTracking) {
      return;
    }

    this._batchedEvents.push({
      event,
      payload: {
        ...payload,
        senderId: this.host.senderId,
        vouchId,
        time: new Date().toISOString()
      }
    });
  };

  private _sendTrackingEvent = () => {
    if (this._batchedEvents.length <= 0) {
      return;
    }

    const { publicApiUrl } = getEnvUrls(this.host.env);
    const { client, tab, request, visitor } = getUids(this.host.env);

    navigator.sendBeacon(
      `${publicApiUrl}/api/batchevents`,
      JSON.stringify({
        payload: {
          events: this._batchedEvents
        },
        context: {
          'x-uid-client': client,
          'x-uid-tab': tab,
          'x-uid-request': request,
          'x-uid-visitor': visitor,
          'x-reporting-metadata': getReportingMetadata(this.host.trackingSource)
        }
      })
    );

    this._batchedEvents = [];
  };

  private _streamEnded = () => {
    if (this._currentlyPlayingVideo) {
      const { id, key } = this._currentlyPlayingVideo;
      // Don't send a tracking event when seeking backwards
      if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
        // Send a video streamed event any time the stream ends to capture the time between starting
        // the video and the video stopping for any reason (pausing, deleting the embed node or closing the browser)
        this._createTrackingEvent('VIDEO_STREAMED', {
          answerId: id,
          streamStart: this._streamStartTime[key],
          streamEnd: this._streamLatestTime[key]
        });
      }

      // Make sure these events are only sent once by deleting the start and latest times
      delete this._streamStartTime[key];
      delete this._streamLatestTime[key];
    }
  };

  private _handleVouchLoaded = ({ detail: vouchId }: CustomEvent<string>) => {
    if (!vouchId) {
      return;
    }

    // Only send loaded event once per session
    if (!this._hasLoaded[vouchId]) {
      this._createTrackingEvent('VOUCH_LOADED', { vouchId });
      this._hasLoaded[vouchId] = true;
    }
  };

  private _handlePlay = () => {
    // Only send the video played event once per session
    if (!this._hasPlayed) {
      this._createTrackingEvent('VIDEO_PLAYED', { streamStart: this.host.currentTime });
      this._hasPlayed = true;
    }
  };

  private _handleVideoPlay = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
    // Only increment play count once per session
    if (!this._answersViewed[key]) {
      this._createTrackingEvent('VOUCH_RESPONSE_VIEWED', {
        answerId: id
      });
      this._answersViewed[key] = true;
    }
  };

  private _handleVideoTimeUpdate = ({ detail: { id, key, node } }: CustomEvent<VideoEventDetail>) => {
    if (
      // We only want to count any time that the video is actually playing
      !this.host.paused &&
      // Only update the latest time if this event fires for the currently active video
      id === this.host.scene?.video?.id
    ) {
      this._currentlyPlayingVideo = { id, key, node };
      this._streamLatestTime[key] = node.currentTime;

      if (!this._streamStartTime[key]) {
        this._streamStartTime[key] = node.currentTime;
        this._streamLatestTime[key] = node.currentTime;
      }
    }
  };

  private _handleVideoPause = ({ detail: { id, key } }: CustomEvent<VideoEventDetail>) => {
    if (this._streamLatestTime[key] > this._streamStartTime[key] + MINIMUM_SEND_THRESHOLD) {
      this._createTrackingEvent('VIDEO_STREAMED', {
        answerId: id,
        streamStart: this._streamStartTime[key],
        streamEnd: this._streamLatestTime[key]
      });
    }
    delete this._streamStartTime[key];
    delete this._streamLatestTime[key];
  };

  private _pageUnloading = () => {
    this._streamEnded();
    this._sendTrackingEvent();
  };

  private _handleVisibilityChange = () => {
    if (document.visibilityState === 'hidden') {
      this._pageUnloading();
    }
  };

  private _handlePageHide = () => {
    this._pageUnloading();
  };

  hostConnected() {
    requestAnimationFrame(() => {
      if ('onvisibilitychange' in document) {
        document.addEventListener('visibilitychange', this._handleVisibilityChange);
      } else {
        window.addEventListener('pagehide', this._handlePageHide);
      }
      this.host.addEventListener('vouch:loaded', this._handleVouchLoaded);
      this.host.mediaPlayer?.addEventListener('play', this._handlePlay);
      this.host.mediaPlayer?.addEventListener('video:play', this._handleVideoPlay);
      this.host.mediaPlayer?.addEventListener('video:pause', this._handleVideoPause);
      this.host.mediaPlayer?.addEventListener('video:timeupdate', this._handleVideoTimeUpdate);
    });
  }

  hostDisconnected() {
    // Send events if DOM node is destroyed
    this._pageUnloading();

    if ('onvisibilitychange' in document) {
      document.removeEventListener('visibilitychange', this._handleVisibilityChange);
    } else {
      window.removeEventListener('pagehide', this._handlePageHide);
    }
    this.host.removeEventListener('vouch:loaded', this._handleVouchLoaded);
    this.host.mediaPlayer?.removeEventListener('play', this._handlePlay);
    this.host.mediaPlayer?.removeEventListener('video:play', this._handleVideoPlay);
    this.host.mediaPlayer?.removeEventListener('video:pause', this._handleVideoPause);
    this.host.mediaPlayer?.removeEventListener('video:timeupdate', this._handleVideoTimeUpdate);
  }
}

export { TrackingController };
export type { TrackingEvent, TrackingPayload };
