import { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
import {
  PlayerLifecycleListener,
  playerManager,
} from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
import { setUserCellPlayerMutedPreference } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/userCellPlayerMutedPreference";
import { playerFactory } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/conts";
import { loggerLiveImageManager } from "./loggerHelper";
import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
import {
  executePreloadHooks,
  PreloadHookConfig,
} from "../MasterCell/DefaultComponents/LiveImage/executePreloadHooks";

const TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION = 500; // ms

const { log_debug, log_info, log_error } = loggerLiveImageManager;

export type LiveImageManagerEvent =
  | "onEnd"
  | "onStart"
  | "onError"
  | "onDestroy";

export type LiveImageManagerListenerData = {
  id: string;
  listener: Record<
    LiveImageManagerEvent,
    (event?: Record<string, any>) => void
  >;
};

export enum LiveImageType {
  Video = "video",
  Image = "image",
}

type Position = {
  centerX: number;
  centerY: number;
  top: number;
  bottom: number;
  left: number;
  right: number;
};

type PlayerFactoryConfig = {
  player: any; // React ref for native view
  playerId: string;
  playerPluginId: string;
  screenConfig: Record<string, any>;
  entry: ZappEntry; // original entry, used as fallback if no hooks
};

type LiveImageProps = {
  playerId: string;
  setMode?: (type: LiveImageType) => void;
  preloadHooks?: PreloadHookConfig[];
  factoryConfig: PlayerFactoryConfig;
  tag: string;
};

// Disabled because we have only unmute button but no play/pause state anymore
const IS_ALLOWED_REPLAY = false;

const playerInfo = (player: Player | null): string =>
  player
    ? `playerId: ${player.playerId}, title: ${player.getEntry()?.title}`
    : "null";

export class LiveImageManager implements PlayerLifecycleListener {
  protected items: LiveImage[] = [];

  // Only allow one player to play at a time for now
  protected currentlyPlaying: LiveImage | null = null;
  protected primaryPlayer: Player | null = null;

  private checkPlayerPositionTimeout: ReturnType<typeof setTimeout> | null =
    null;

  protected listeners: Record<
    string,
    Record<LiveImageManagerEvent, (event?: Record<string, any>) => void>
  > = {};

  constructor() {
    playerManager.addLifecycleListener(this);
    this.listeners = {};
  }

  private static _instance: LiveImageManager;

  public static get instance() {
    return this._instance || (this._instance = new this());
  }

  public register = (item: LiveImage): (() => void) => {
    this.items.push(item);
    log_debug(`register: live image ${item.playerId} - ${item.tag}`);

    // TV only Start playing video once registered
    if (isTV()) {
      this.playLiveImage(item);
    }

    return () => this.unregister(item);
  };

  public unregister = (item: LiveImage) => {
    log_debug(`unregister: live-image ${item.playerId} - ${item.tag}`);

    if (this.currentlyPlaying === item) {
      this.currentlyPlaying = null;

      log_debug(
        `unregister: currently playing live-image was destroyed, ${item.playerId} - ${item.tag}`
      );

      // TODO: Maybe start another one
    }

    this.items = this.items.filter((i) => i !== item);

    this.invokeListenersUpdate({
      callbackName: "onDestroy",
      event: {
        item,
        playerId: this.currentlyPlaying?.playerId,
        primaryPlayerId: this.primaryPlayer?.playerId,
        entry: item.getPlayer()?.getEntry(),
      },
    });
  };

  private cancelCheckPlayerPositionTimeout = () => {
    clearTimeout(this.checkPlayerPositionTimeout);
    this.checkPlayerPositionTimeout = null;
  };

  public onViewportEnter = (item: LiveImage) => {
    log_debug(
      `onViewportEnter: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
    );

    if (!isTV()) {
      // mobile only
      // we have to delay running checkPlayerPosition, because sometimes on fast scrolling we get wrong order onEnter, then onLeave.
      // which could cause select wrong item to play

      this.cancelCheckPlayerPositionTimeout();

      this.checkPlayerPositionTimeout = setTimeout(() => {
        this.cancelCheckPlayerPositionTimeout();

        this.checkPlayerPosition(item);
      }, TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION);
    } else {
      this.checkPlayerPosition(item);
    }
  };

  public onViewportLeave = (item: LiveImage) => {
    log_debug(
      `onViewportLeave: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
    );

    this.pauseItem(item);
  };

  public getCurrentlyPlaying = () => {
    return this.currentlyPlaying;
  };

  private findNextPlayableItem = () => {
    if (isTV()) {
      return this.items[this.items.length - 1];
    }

    return (
      this.items
        .filter(({ isFullyVisible }) => isFullyVisible)
        .sort((a: LiveImage, b: LiveImage) => {
          return a.position.centerX - b.position.centerX;
        })
        .sort((a: LiveImage, b: LiveImage) => {
          const a1 = Math.abs(a.position.centerY - 0.5);
          const b1 = Math.abs(b.position.centerY - 0.5);

          return a1 - b1;
        })?.[0] || this.currentlyPlaying
    );
  };

  private findItem = (playerId: string): LiveImage | null =>
    this.items.find((i) => i.playerId === playerId) || null;

  private pauseItem = (item: LiveImage) => {
    log_debug(`pauseItem: live-image ${item.playerId} - ${item.tag}`);

    if (!item.player) {
      // Player not yet created (e.g. hooks still running) — just reset mode
      item.setMode?.(LiveImageType.Image);

      if (item === this.currentlyPlaying) {
        this.currentlyPlaying = null;
      }

      return;
    }

    if (!item.player.playerState.isReadyToPlay) {
      log_debug(
        `pauseItem: live-image not ready, ${item.playerId} - ${item.tag}`
      );
    } else {
      item.player.pause();
    }

    // Fake close event, because we unmount native view
    item.player.onPlayerClose();
    item.setMode?.(LiveImageType.Image);

    if (item === this.currentlyPlaying) {
      this.currentlyPlaying = null;
    }
  };

  public playLiveImage = (item: LiveImage) => {
    log_debug(
      `playLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
    );

    if (this.primaryPlayer) {
      return;
    }

    if (this.currentlyPlaying) {
      if (this.currentlyPlaying.playerId === item.playerId) {
        return;
      } else {
        this.pauseItem(this.currentlyPlaying);
      }
    }

    this.currentlyPlaying = item;

    item
      .prepareForPlayback()
      .then((result) => {
        if (!result) {
          log_error(
            `Failed to prepare live image ${item.playerId} - ${item.tag} for playback: prepareForPlayback returned false`
          );

          this.currentlyPlaying = null;
          item.setMode?.(LiveImageType.Image);

          return;
        }

        // Guard: item might have been replaced while hooks were running
        if (this.currentlyPlaying !== item) return;

        item.setMode?.(LiveImageType.Video);

        if (item.player?.playerState.isReadyToPlay) {
          item.player.play();
        }
      })
      .catch((error) => {
        log_error(
          `Failed to prepare live image ${item.playerId} - ${item.tag} for playback: ${error?.message}`
        );

        this.onLiveImageError(item, error);
      });
  };

  public pauseLiveImage = (item: LiveImage) => {
    log_debug(
      `pauseLiveImage: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
    );

    this.pauseItem(item);
  };

  public onLiveImageCompleted = (_item: LiveImage) => {
    // TODO: Notify listeners that player has died
    // Do not try look for new playable, otherwise you will restart video finish to play
  };

  public muteAll = () => {
    log_debug("muteAll");

    setUserCellPlayerMutedPreference(true);

    this.items.forEach((liveImage) => liveImage.mute());
  };

  public unmuteAll = () => {
    log_debug("unmuteAll");

    setUserCellPlayerMutedPreference(false);

    this.items.forEach((liveImage) => liveImage.unmute());
  };

  public onViewPositionChanged = (item: LiveImage) => {
    log_debug(
      `onViewPositionChanged: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)} - position:${item.positionToString()}`
    );

    if (!isTV()) {
      // mobile only
      // we have to delay running checkPlayerPosition, because sometimes on fast scrolling we get wrong order onEnter, then onLeave.
      // which could cause select wrong item to play

      this.cancelCheckPlayerPositionTimeout();

      this.checkPlayerPositionTimeout = setTimeout(() => {
        this.cancelCheckPlayerPositionTimeout();

        this.checkPlayerPosition(item);
      }, TIMEOUT_FOR_DELAY_CHECK_PLAYER_POSITION);
    } else {
      this.checkPlayerPosition(item);
    }
  };

  private checkPlayerPosition = (item: LiveImage) => {
    this.cancelCheckPlayerPositionTimeout();

    log_debug(
      `checkPlayerPosition: live-image ${item.playerId} - ${item.tag}, primary ${playerInfo(this.primaryPlayer)}`
    );

    const playerItem = this.findNextPlayableItem();

    if (playerItem) {
      if (!playerItem.isFullyVisible) {
        log_error(
          `checkPlayerPosition: trying to start playback currently invisible item: ${playerInfo(
            playerItem.player
          )}`
        );
      }

      // Will also check if it's item that already playing
      this.playLiveImage(playerItem);
    }
  };

  onPrimaryPlayerClosed = () => {
    const playerItem = this.findNextPlayableItem();
    log_info("onPrimaryPlayerClosed: starting to play visible live-image");

    this.currentlyPlaying = null;

    if (playerItem) {
      this.playLiveImage(playerItem);
    }
  };

  onPrimaryPlayerCreated = () => {
    log_info("onPrimaryPlayerCreated: pausing visible live-images");

    if (this.currentlyPlaying) {
      this.pauseItem(this.currentlyPlaying);
    }
  };

  private liveImageManagerListenerId = "live-image-manager";

  onRegistered = (player: Player) => {
    const item = this.findItem(player.playerId);

    if (item) {
      return;
    }

    player.addListener({
      id: this.liveImageManagerListenerId,
      listener: {
        onVideoEnd: (event) => () => this.onPrimaryPlayerEnded(player, event),
        onError: (event) => () => this.onPrimaryPlayerError(player, event),
        onLoad: (event) => () => this.onPrimaryPlayerLoad(player, event),
      },
    });

    // Should not happen with current architecture
    if (!this.primaryPlayer) {
      this.primaryPlayer = player;

      this.onPrimaryPlayerCreated();
    } else {
      log_error(
        `onRegistered: multiple primary players not allowed, primary ${playerInfo(
          this.primaryPlayer
        )}`
      );
    }
  };

  onUnRegistered = (player: Player) => {
    const item = this.findItem(player.playerId);
    if (item) return;

    player.removeListener(this.liveImageManagerListenerId);

    if (player === this.primaryPlayer) {
      this.primaryPlayer = null;
      this.onPrimaryPlayerClosed();
    }
  };

  // Primary player callbacks
  onPrimaryPlayerEnded = (_player: Player, _event) => {
    // Not used now, primary player has been destroyed
  };

  onPrimaryPlayerError = (_player: Player, _event) => {
    // Not used now, primary player has been destroyed
  };

  onPrimaryPlayerLoad = (_player: Player, _event) => {
    // Not used now, we stop playing when primary player is registered
  };

  // Live Image player callbacks

  onLiveImageVideoLoaded = (item: LiveImage) => {
    log_debug(
      `onLiveImageVideoLoaded: live-image ${playerInfo(
        item.player
      )}, currentPlayingId: ${
        this.currentlyPlaying?.playerId
      }, primaryPlayerId: ${this.primaryPlayer?.playerId}`
    );

    if (this.currentlyPlaying === item) {
      item.player?.play();
    }

    this.invokeListenersUpdate({
      callbackName: "onStart",
      event: {
        item,
        playerId: this.currentlyPlaying?.playerId,
        primaryPlayerId: this.primaryPlayer?.playerId,
        entry: item.getPlayer()?.getEntry(),
      },
    });
  };

  onLiveImageEnded = (item: LiveImage) => {
    log_debug(
      `onLiveImageEnded: live-image ${playerInfo(
        item.player
      )}, currentPlayingId: ${
        this.currentlyPlaying?.playerId
      }, primaryPlayerId: ${this.primaryPlayer?.playerId}`
    );

    const isCurrentItemEnded = this.currentlyPlaying === item;

    // TODO: This branch was used when we had play button to toggle replay.
    //  Now we mute button instead but we keep it just in case.
    if (IS_ALLOWED_REPLAY && !isTV() && isCurrentItemEnded) {
      this.currentlyPlaying?.player.seekTo(0);
      this.currentlyPlaying?.player.pause();

      // TODO: ...Maybe player some other item
    } else {
      item.setMode?.(LiveImageType.Image);
    }

    // Prevent receiving onEnd event from both `close` and `end` player events
    isCurrentItemEnded &&
      this.invokeListenersUpdate({
        callbackName: "onEnd",
        event: {
          item,
          playerId: this.currentlyPlaying?.playerId,
          primaryPlayerId: this.primaryPlayer?.playerId,
          entry: item.getPlayer()?.getEntry(),
        },
      });
  };

  onLiveImageError = (item: LiveImage, error: Error) => {
    item.setMode?.(LiveImageType.Image);

    const currentItem = this.currentlyPlaying;

    log_debug(
      `onLiveImageError: error: ${error.message}, live-image ${playerInfo(
        item.player
      )}, currentPlayingId: ${
        this.currentlyPlaying?.playerId
      }, primaryPlayerId: ${this.primaryPlayer?.playerId}`
    );

    if (currentItem === item) {
      this.currentlyPlaying = null;

      log_debug(
        `onLiveImageError: currentItem: ${currentItem.playerId} - ${currentItem.tag} was removed`
      );

      // TODO: ...Maybe player some other item
    }

    this.invokeListenersUpdate({
      callbackName: "onError",
      event: {
        item,
        error,
        playerId: currentItem?.playerId,
        primaryPlayerId: this.primaryPlayer?.playerId,
        entry: item.getPlayer()?.getEntry(),
      },
    });
  };

  addListener = ({ id, listener }: LiveImageManagerListenerData) => {
    this.listeners[id] = listener;

    return () => this.removeListener(id);
  };

  removeListener = (id: string) => {
    delete this.listeners[id];
  };

  public invokeListenersUpdate = ({
    callbackName,
    event = {},
  }: {
    callbackName: LiveImageManagerEvent;
    event?: Record<string, any>;
  }) => {
    for (const [key, listener] of Object.entries(this.listeners)) {
      try {
        listener[callbackName]?.(event);
      } catch (error) {
        log_error(
          `invokeListenersUpdate: Error: listenerId: ${key}, invoking callback: ${callbackName}, message: ${error?.message}`,
          { error }
        );
      }
    }
  };
}

// TODO: now we can check primary player exist and we can remove this hack
// then primary player was created and LiveImageManager doesn't know that primary player exist
// Happens when home screen has LiveImage and deep link open a player
// https://applicaster.monday.com/boards/1615228456/pulses/5979392200?notification=4136716156
LiveImageManager.instance;

export class LiveImage implements QuickBrickPlayer.SharedPlayerCallBacks {
  public player: Player | null = null;
  public setMode?: (type: LiveImageType) => void;
  public isFullyVisible: boolean = false;
  public tag: string;
  public position: Position = {
    centerX: 0,
    centerY: 0,
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
  };

  // Keeps muted state in sync: updates the initial value for deferred
  // player creation and forwards to the player if it already exists.
  public initiallyMuted: boolean = true;

  mute = () => {
    this.initiallyMuted = true;
    this.player?.mute();
  };

  unmute = () => {
    this.initiallyMuted = false;
    this.player?.unmute();
  };

  positionToString() {
    if (!this.position) {
      return "position not set";
    }

    const { centerX, centerY, top, bottom, left, right } = this.position;

    return `centerX: ${centerX.toFixed(2)}, centerY: ${centerY.toFixed(2)}, top: ${top.toFixed(2)}, bottom: ${bottom.toFixed(2)}, left: ${left.toFixed(2)}, right: ${right.toFixed(2)}`;
  }

  readonly playerId: string;
  public component: any = null;

  private factoryConfig: PlayerFactoryConfig;
  public preloadHooks?: PreloadHookConfig[];
  public processedEntry: ZappEntry | null = null;
  private _preparePromise: Promise<boolean> | null = null;

  constructor(props: LiveImageProps) {
    this.setMode = props.setMode;
    this.playerId = props.playerId;
    this.preloadHooks = props.preloadHooks;
    this.factoryConfig = props.factoryConfig;
    this.tag = props.tag || "untagged";
  }

  async prepareForPlayback(): Promise<boolean> {
    // Already prepared — player exists
    if (this.player) {
      return true;
    }

    // Deduplicate: if preparation is already in flight, await the same promise
    if (this._preparePromise) {
      return this._preparePromise;
    }

    this._preparePromise = this.createPreparedPlayer()
      .then((result) => {
        this._preparePromise = null;

        return result;
      })
      .catch((error) => {
        this._preparePromise = null;

        log_error(
          `prepareForPlayback: live-image ${this.playerId}, error preparing for playback: ${error?.message}`,
          { error }
        );

        throw error;
      });

    return this._preparePromise;
  }

  private async createPreparedPlayer(): Promise<boolean> {
    let entry = this.factoryConfig.entry;

    if (this.preloadHooks?.length) {
      const result = await executePreloadHooks({
        preloadHooks: this.preloadHooks,
        entry,
      });

      if (!result) return false;

      this.processedEntry = result;
      entry = result;
    }

    const factoryItem = await playerFactory({
      player: this.factoryConfig.player,
      playerId: this.factoryConfig.playerId,
      autoplay: false,
      entry,
      muted: this.initiallyMuted,
      playerPluginId: this.factoryConfig.playerPluginId,
      screenConfig: this.factoryConfig.screenConfig,
      playerRole: PlayerRole.Cell,
    });

    if (!factoryItem) {
      throw new Error(
        `Player factory returned null (playerId: ${this.factoryConfig.playerId}, playerPluginId: ${this.factoryConfig.playerPluginId})`
      );
    }

    this.player = factoryItem.controller;
    this.component = factoryItem.Component;
    this.player.addListener({ id: "live-image", listener: this });

    return true;
  }

  public getPlayer = (): Player | null => {
    return this.player;
  };

  onLoad = (_event) => {
    log_info(
      `onLoad: live-image, video loaded, switching to video, ${playerInfo(
        this.player
      )}`
    );

    LiveImageManager.instance.onLiveImageVideoLoaded(this);
  };

  onError = (error: Error) => {
    log_error(
      `onError: live-image, fail to play, switching back to image. error: ${
        error?.message
      }, ${playerInfo(this.player)}`
    );

    LiveImageManager.instance.onLiveImageError(this, error);
  };

  onVideoError = (error: Error) => {
    log_error(
      `onVideoError: live-image, fail to play, switching back to image. error: ${
        error?.message
      }, ${playerInfo(this.player)}`
    );

    LiveImageManager.instance.onLiveImageError(this, error);
  };

  onVideoEnd = (_event) => {
    log_info(
      `onVideoEnd: live-image, video completed, switching back to image, ${playerInfo(
        this.player
      )}`
    );

    LiveImageManager.instance.onLiveImageEnded(this);
  };

  onPlayerClose = () => {
    log_info(
      `onPlayerClose: live-image, player closed, switching back to image, ${playerInfo(
        this.player
      )}`
    );

    // TODO: we maybe want to add onPlayerClose to separate completion behavior
    LiveImageManager.instance.onLiveImageEnded(this);
  };
}
