import { KalturaPlayer as Player } from '../../kaltura-player';
import { Ad } from '../ads';
import { AdBreak } from '../ads';
import {
  Error,
  EventManager,
  AdEventType,
  FakeEvent,
  FakeEventTarget,
  Html5EventType,
  CustomEventType,
  getLogger,
  BaseMiddleware,
  Utils
} from '@playkit-js/playkit-js';
import { PrebidManager } from '../ads/prebid-manager';
import { AdLayoutMiddleware } from '../ads/ad-layout-middleware';
import { KPAdBreakObject, KPAdObject, KPAdPod } from '../../types/ads/advertising';
import { IAdsController } from '../../types/ads/ads-controller';
import { IAdsPluginController } from '../../types/ads/ads-plugin-controller';

interface RunTimeAdBreakObject extends KPAdBreakObject {
  played: boolean;
  loadedPromise: Promise<any>;
}

/**
 * @class AdsController
 * @param {Player} player - The player.
 * @param {IAdsController} adsPluginController - The controller of the current ads plugin instance.
 */
class AdsController extends FakeEventTarget implements IAdsController {
  private static _logger: any = getLogger('AdsController');

  private readonly _player: Player;
  private _adsPluginControllers: Array<IAdsPluginController>;
  private _allAdsCompleted!: boolean;
  private _eventManager: EventManager;
  private _liveEventManager: EventManager;
  private _adBreaksLayout!: Array<number | string>;
  private _adBreak: AdBreak | undefined;
  private _ad: Ad | undefined;
  private _adPlayed!: boolean;
  private _snapback!: number;
  private _configAdBreaks!: Array<RunTimeAdBreakObject>;
  private _adIsLoading!: boolean;
  private _isAdPlaying!: boolean;
  private _middleware!: AdLayoutMiddleware;
  private readonly _prebidManager!: PrebidManager;
  private _liveSeeking!: boolean;
  public prerollReady!: Promise<any>;

  constructor(player: Player, adsPluginControllers: Array<IAdsPluginController>) {
    super();
    this._player = player;
    this._eventManager = new EventManager();
    this._liveEventManager = new EventManager();
    this._adsPluginControllers = adsPluginControllers;
    this._prebidManager = new PrebidManager(this._player.config.advertising && this._player.config.advertising.prebid);
    this._init();
  }

  /**
   * @instance
   * @memberof AdsController
   * @returns {boolean} - Whether all ads completed.
   */
  public get allAdsCompleted(): boolean {
    return this._allAdsCompleted;
  }

  /**
   * @instance
   * @memberof AdsController
   * @returns {boolean} - Whether an ad is playing.
   */
  public isAdPlaying(): boolean {
    return this.isAdBreak() && this._isAdPlaying;
  }

  /**
   * @instance
   * @memberof AdsController
   * @returns {boolean} - Whether we're in an ad break.
   */
  public isAdBreak(): boolean {
    return !!this._adBreak;
  }

  /**
   * @instance
   * @memberof AdsController
   * @returns {Array<number|string>} - The ad breaks layout (cue points).
   */
  public getAdBreaksLayout(): Array<number | string> {
    return this._adBreaksLayout;
  }

  /**
   * @instance
   * @memberof AdsController
   * @returns {?AdBreak} - Gets the current ad break data.
   */
  public getAdBreak(): AdBreak | undefined {
    return this._adBreak;
  }

  /**
   * @instance
   * @memberof AdsController
   * @returns {?Ad} - Gets the current ad data.
   */
  public getAd(): Ad | undefined {
    return this._ad;
  }

  /**
   * Skip on an ad.
   * @instance
   * @memberof AdsController
   * @returns {void}
   */
  public skipAd(): void {
    const activeController = this._adsPluginControllers.find((controller) => controller.active);
    activeController && activeController.skipAd();
  }

  /**
   * Play an ad on demand.
   * @param {KPAdPod} adPod - The ad pod play.
   * @instance
   * @memberof AdsController
   * @returns {void}
   */
  public playAdNow(adPod: KPAdPod): void {
    if (this.isAdBreak()) {
      AdsController._logger.warn('Tried to call playAdNow during an ad break');
    } else {
      const loadPrebidAd = Promise.all(adPod.map((ad) => this._getPrebidAds(ad)));
      this._playAdBreak({
        position: this._player.currentTime || 0,
        ads: adPod,
        played: false,
        loadedPromise: loadPrebidAd
      });
    }
  }

  public getMiddleware(): BaseMiddleware {
    return this._middleware ? this._middleware : (this._middleware = new AdLayoutMiddleware(this));
  }

  private _init(): void {
    this._initMembers();
    this._addBindings();
  }

  private _initMembers(): void {
    this._allAdsCompleted = true;
    this._adBreaksLayout = [];
    this._adBreak = undefined;
    this._ad = undefined;
    this._adPlayed = false;
    this._snapback = 0;
    this._adIsLoading = false;
    this._isAdPlaying = false;
    this._liveSeeking = false;
  }

  private _addBindings(): void {
    this._eventManager.listen(this._player, CustomEventType.SOURCE_SELECTED, () => this._handleConfiguredAdBreaks());
    this._eventManager.listen(this._player, AdEventType.AD_MANIFEST_LOADED, (event) => this._onAdManifestLoaded(event));
    this._eventManager.listen(this._player, AdEventType.AD_BREAK_START, (event) => this._onAdBreakStart(event));
    this._eventManager.listen(this._player, AdEventType.AD_LOADED, () => this._onAdLoaded());
    this._eventManager.listen(this._player, AdEventType.AD_STARTED, (event) => this._onAdStarted(event));
    this._eventManager.listen(this._player, AdEventType.AD_COMPLETED, () => (this._isAdPlaying = false));
    this._eventManager.listen(this._player, AdEventType.AD_BREAK_END, () => this._onAdBreakEnd());
    this._eventManager.listen(this._player, AdEventType.ADS_COMPLETED, () => this._onAdsCompleted());
    this._eventManager.listen(this._player, AdEventType.AD_ERROR, (event) => this._onAdError(event));
    this._eventManager.listen(this._player, CustomEventType.PLAYER_RESET, () => this._reset());
    this._eventManager.listen(this._player, CustomEventType.PLAYER_DESTROY, () => this._destroy());
    this._eventManager.listenOnce(this._player, Html5EventType.ENDED, () => this._onEnded());
    this._eventManager.listenOnce(this._player, CustomEventType.PLAYBACK_ENDED, () => this._onPlaybackEnded());
    this._eventManager.listen(this._player, AdEventType.AD_RESUMED, () => (this._isAdPlaying = true));
    this._eventManager.listen(this._player, AdEventType.AD_PAUSED, () => (this._isAdPlaying = false));
  }

  private _handleConfiguredAdBreaks(): void {
    const playAdsAfterTime = this._player.config.advertising.playAdsAfterTime || this._player.config.sources.startTime;
    // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
    // @ts-ignore
    this._configAdBreaks = this._player.config.advertising.adBreaks
      .filter(
        (adBreak) =>
          (typeof adBreak.every === 'number' || typeof adBreak.position === 'number' || typeof adBreak.percentage === 'number') && adBreak.ads.length
      )
      .map((adBreak) => {
        this._validateOneTimeConfig(adBreak);
        let position = adBreak.position;
        adBreak.percentage === 0 && (position = 0);
        adBreak.percentage === 100 && (position = -1);
        adBreak.every && (position = adBreak.every);
        const played = this._player.isLive() ? position < playAdsAfterTime! : position <= playAdsAfterTime!;
        return {
          position,
          percentage: adBreak.percentage,
          every: adBreak.every,
          ads: adBreak.ads.slice(),
          played: -1 < position && played
        };
      });
    if (this._configAdBreaks.length) {
      this._dispatchAdManifestLoaded();
      this._handlePrebidAdConfig();
      this._handleConfiguredPreroll();
      this._eventManager.listenOnce(this._player, Html5EventType.DURATION_CHANGE, () => {
        this._player.isLive()
          ? this._eventManager.listenOnce(this._player, Html5EventType.SEEKING, () => {
              this._pushNextAdsForLive(this._configAdBreaks, (adBreak) => this._player.currentTime + adBreak.every);
              this._attachLiveSeekedHandler();
            })
          : this._handleEveryAndPercentage();
        this._configAdBreaks.sort((a, b) => a.position - b.position);
        if (this._configAdBreaks.some((adBreak) => adBreak.position > 0)) {
          this._handleConfiguredMidrolls();
        }
      });
    } else {
      this.prerollReady = Promise.resolve();
    }
  }

  private _validateOneTimeConfig(adBreak: KPAdBreakObject): void {
    if (typeof adBreak.position === 'number') {
      if (typeof adBreak.percentage === 'number') {
        AdsController._logger.warn(`Validate ad break - ignore percentage ${adBreak.percentage} as position ${adBreak.position} configured`);
        delete adBreak.percentage;
      }
      if (typeof adBreak.every === 'number') {
        AdsController._logger.warn(`Validate ad break - ignore every ${adBreak.every} as position ${adBreak.position} configured`);
        delete adBreak.every;
      }
    }
    if (typeof adBreak.percentage === 'number' && typeof adBreak.every === 'number') {
      AdsController._logger.warn(`Validate ad break - ignore every ${adBreak.every} as percentage ${adBreak.percentage} configured`);
      delete adBreak.every;
    }
  }

  private _dispatchAdManifestLoaded(): void {
    const adBreaksPosition = Array.from(
      new Set(
        this._configAdBreaks.map(
          (adBreak) =>
            (adBreak.every && adBreak.every + 's') || (typeof adBreak.percentage === 'number' && adBreak.percentage + '%') || adBreak.position
        )
      )
    );
    AdsController._logger.debug(AdEventType.AD_MANIFEST_LOADED, adBreaksPosition);
    // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
    // @ts-ignore
    this._player.dispatchEvent(new FakeEvent(AdEventType.AD_MANIFEST_LOADED, { adBreaksPosition }));
    if (this._player.hasService('timeline') && this._player.config.advertising.showAdBreakCuePoint) {
      adBreaksPosition.forEach((position) => {
        this._player.getService('timeline').addCuePoint({
          time: position !== -1 ? position : Infinity,
          ...this._player.config.advertising.adBreakCuePointStyle
        });
      });
    }
  }

  private _handlePrebidAdConfig(): void {
    this._prebidManager &&
      this._configAdBreaks
        .filter((adBreak) => !adBreak.played)
        .map((adBreak) => {
          const loadPrebidAd = Promise.all(adBreak.ads.map((ad) => this._getPrebidAds(ad)));
          adBreak.loadedPromise = loadPrebidAd;
          loadPrebidAd.then((ads) => (adBreak.ads = ads));
        });
  }

  private _getPrebidAds(ad: KPAdObject): Promise<any> {
    return new Promise((resolve) => {
      if (ad.prebid && this._prebidManager) {
        const prebidConfig = Utils.Object.mergeDeep({}, ad.prebid, this._player.config.advertising.prebid);
        const promiseLoad = this._prebidManager.load(prebidConfig);
        promiseLoad
          .then((bids) => {
            const vastUrls = bids.map((bid) => bid && bid.vastUrl);
            ad.url = vastUrls.concat(ad.url);
            resolve(ad);
          })
          .catch(() => {
            resolve(ad);
          });
      } else {
        resolve(ad);
      }
    });
  }

  private _handleConfiguredPreroll(): void {
    const prerolls = this._configAdBreaks.filter((adBreak) => adBreak.position === 0 && !adBreak.played);
    const mergedPreroll = this._mergeAdBreaks(prerolls);
    this.prerollReady = mergedPreroll && mergedPreroll.loadedPromise ? mergedPreroll.loadedPromise : Promise.resolve();
    mergedPreroll && this._playAdBreak(mergedPreroll);
  }

  private _handleEveryAndPercentage(): void {
    this._configAdBreaks.forEach((adBreak) => {
      if (this._player.duration && adBreak.every) {
        let currentPosition = 2 * adBreak.every;
        while (currentPosition <= this._player.duration) {
          this._configAdBreaks.push({
            position: currentPosition,
            ads: adBreak.ads,
            played: false,
            loadedPromise: Promise.resolve()
          });
          currentPosition += adBreak.every;
        }
      } else {
        if (this._player.duration && adBreak.percentage && !adBreak.position) {
          adBreak.position = Math.floor((this._player.duration * adBreak.percentage) / 100);
        }
      }
    });
  }

  private _attachLiveSeekedHandler(): void {
    this._eventManager.listenOnce(this._player, CustomEventType.FIRST_PLAYING, () => {
      this._eventManager.listen(this._player, Html5EventType.SEEKING, () => {
        this._liveSeeking = true;
      });
      this._eventManager.listen(this._player, Html5EventType.SEEKED, () => {
        this._liveSeeking = false;
        this._pushNextAdsForLive(this._configAdBreaks, (adBreak) => this._player.currentTime + adBreak.every);
      });
    });
  }

  private _pushNextAdsForLive(iterator: Array<RunTimeAdBreakObject>, calcPositionCallback: (params: any) => void): void {
    this._liveEventManager.removeAll();
    const liveConfigAdBreaks = [];
    iterator.forEach((adBreak) => {
      if (![-1, 0].includes(adBreak.position)) {
        const { every, ads } = adBreak;
        const position = calcPositionCallback(adBreak);
        const nextAdBreak = {
          every,
          position,
          ads,
          played: false,
          loadedPromise: Promise.resolve()
        };
        AdsController._logger.debug('Pushing next ad for live', nextAdBreak);
        // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
        // @ts-ignore
        liveConfigAdBreaks.push(nextAdBreak);
      }
    });
    if (liveConfigAdBreaks.length) {
      this._configAdBreaks = [...liveConfigAdBreaks, ...this._configAdBreaks.filter((adBreak) => adBreak.position === -1)];
    }
  }

  private _handleConfiguredMidrolls(): void {
    this._eventManager.listen(this._player, Html5EventType.TIME_UPDATE, () => {
      if (!this._player.paused && !this._liveSeeking) {
        const adBreaks = this._configAdBreaks.filter(
          (adBreak) =>
            !adBreak.played && this._player.currentTime && adBreak.position <= this._player.currentTime && adBreak.position > this._snapback
        );
        if (adBreaks.length) {
          const maxPosition = adBreaks[adBreaks.length - 1].position;
          const lastAdBreaks = adBreaks.filter((adBreak) => adBreak.position === maxPosition);
          if (this._player.isLive()) {
            const returnToLive =
              !this._player.isDvr() ||
              (this._player.isOnLiveEdge() &&
                // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
                // @ts-ignore
                this._player.config.advertising.returnToLive);

            returnToLive
              ? this._handleReturnToLive(lastAdBreaks)
              : this._pushNextAdsForLive(
                  lastAdBreaks,
                  (adBreak) => (this._player.isOnLiveEdge() ? this._player.currentTime : adBreak.position) + adBreak.every
                );
          } else {
            this._snapback = maxPosition;
            AdsController._logger.debug(`Set snapback value ${this._snapback}`);
            this._eventManager.listen(this._player, Html5EventType.SEEKED, () => {
              const nextPlayedAdBreakIndex = this._configAdBreaks.findIndex(
                (adBreak) => adBreak.played && typeof this._player.currentTime === 'number' && this._player.currentTime < adBreak.position
              );
              if (nextPlayedAdBreakIndex > 0 && !this._configAdBreaks[nextPlayedAdBreakIndex - 1].played) {
                this._snapback = 0;
                AdsController._logger.debug('Reset snapback value');
              }
            });
          }
          const mergedAdBreak = this._mergeAdBreaks(lastAdBreaks);
          mergedAdBreak && this._playAdBreak(mergedAdBreak);
        }
      }
    });
  }

  private _handleReturnToLive(adBreaks: Array<RunTimeAdBreakObject>): void {
    this._liveEventManager.listenOnce(this._player, AdEventType.AD_ERROR, () => {
      this._pushNextAdsForLive(adBreaks, (adBreak) => (this._player.isOnLiveEdge() ? this._player.currentTime : adBreak.position) + adBreak.every);
    });
    this._liveEventManager.listenOnce(this._player, AdEventType.AD_BREAK_END, () => {
      this._player.seekToLiveEdge();
    });
  }

  private _playAdBreak(adBreak: RunTimeAdBreakObject): void {
    const adController = this._adsPluginControllers.find((controller) => typeof controller.playAdNow === 'function');
    if (adController) {
      adBreak.played = true;
      this._adIsLoading = true;
      AdsController._logger.debug(`Playing ad break positioned in ${adBreak.position}`);
      // $FlowFixMe
      adBreak.loadedPromise.then(() => adController.playAdNow(adBreak.ads));
    } else {
      AdsController._logger.warn('No ads plugin registered');
    }
  }

  private _onAdManifestLoaded(event: FakeEvent): void {
    this._adBreaksLayout = Array.from(new Set(this._adBreaksLayout.concat(event.payload.adBreaksPosition))).sort();
    this._allAdsCompleted = false;
  }

  private _onAdBreakStart(event: FakeEvent): void {
    this._adBreak = event.payload.adBreak;
  }

  private _onAdLoaded(): void {
    this._adIsLoading = false;
  }

  private _onAdStarted(event: FakeEvent): void {
    this._ad = event.payload.ad;
    this._adPlayed = true;
    this._isAdPlaying = true;
  }

  private _onAdBreakEnd(): void {
    this._adBreak = undefined;
    this._ad = undefined;
  }

  private _onAdsCompleted(): void {
    if (this._adsPluginControllers.every((controller) => controller.done) && this._configAdBreaks.every((adBreak) => adBreak.played)) {
      this._allAdsCompleted = true;
      AdsController._logger.debug(AdEventType.ALL_ADS_COMPLETED);
      // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
      // @ts-ignore
      this.dispatchEvent(new FakeEvent(AdEventType.ALL_ADS_COMPLETED));
    }
  }

  private _onAdError(event: FakeEvent): void {
    this._adIsLoading = false;
    if (event.payload.severity === Error.Severity.CRITICAL) {
      this._isAdPlaying = false;
      if (this._adsPluginControllers.every((controller) => controller.done) && this._configAdBreaks.every((adBreak) => adBreak.played)) {
        this._allAdsCompleted = true;
        if (this._adPlayed) {
          AdsController._logger.debug(AdEventType.ALL_ADS_COMPLETED);
          // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
          // @ts-ignore
          this.dispatchEvent(new FakeEvent(AdEventType.ALL_ADS_COMPLETED));
        }
      }
    }
  }

  private _isBumper(controller: IAdsPluginController): boolean {
    return controller.name === 'bumper';
  }

  private _onEnded(): void {
    if (this._adIsLoading) {
      return;
    }
    const bumperCtrl = this._adsPluginControllers.find((controller) => this._isBumper(controller));
    const adCtrl = this._adsPluginControllers.find((controller) => !this._isBumper(controller) && !controller.done);
    const bumperCompleted =
      bumperCtrl && typeof bumperCtrl.onPlaybackEnded === 'function'
        ? (): Promise<any> => bumperCtrl.onPlaybackEnded()
        : (): Promise<any> => Promise.resolve();
    const adCompleted =
      adCtrl && typeof adCtrl.onPlaybackEnded === 'function' ? (): Promise<any> => adCtrl.onPlaybackEnded() : (): Promise<any> => Promise.resolve();
    if (!(this._adBreaksLayout.includes(-1) || this._adBreaksLayout.includes('100%'))) {
      this._allAdsCompleted = true;
    }
    // $FlowFixMe
    bumperCompleted().finally(() => {
      // $FlowFixMe
      adCompleted().finally(() => this._handleConfiguredPostroll());
    });
  }

  private _onPlaybackEnded(): void {
    this._configAdBreaks.forEach((adBreak) => (adBreak.played = true));
  }

  private _handleConfiguredPostroll(): void {
    const postrolls = this._configAdBreaks.filter((adBreak) => !adBreak.played && adBreak.position === -1);
    if (postrolls.length) {
      const mergedPostroll = this._mergeAdBreaks(postrolls);
      mergedPostroll && this._playAdBreak(mergedPostroll);
    }
    this._configAdBreaks.forEach((adBreak) => (adBreak.played = true));
  }

  private _reset(): void {
    this._eventManager.removeAll();
    this._liveEventManager.removeAll();
    this._init();
  }

  private _destroy(): void {
    this._adsPluginControllers = [];
    this._eventManager.destroy();
    this._liveEventManager.destroy();
  }

  private _mergeAdBreaks(adBreaks: Array<RunTimeAdBreakObject>): RunTimeAdBreakObject | undefined {
    if (adBreaks.length) {
      adBreaks.forEach((adBreak) => (adBreak.played = true));
      return {
        position: adBreaks[0].position,
        ads: adBreaks.reduce(
          // eslint-disable-next-line  @typescript-eslint/ban-ts-comment
          // @ts-ignore
          (result, adBreak) => result.concat(adBreak.ads),
          []
        ),
        played: false,
        loadedPromise: Promise.all(adBreaks.map((adBreak) => adBreak.loadedPromise))
      };
    }
  }
}

export { AdsController };
