'use client';

import clsx from 'clsx';
import React, {
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import { twMerge } from 'tailwind-merge';

import HookActions from '@/components/hooksPlayer/HookActions';
import HookMetadataWithSongBar from '@/components/hooksPlayer/HookMetadataWithSongBar';
import HooksLyricOverlay, {
  HooksLyricsOverlayCenter,
} from '@/components/hooksPlayer/HooksLyricOverlay';
import HooksPlayer, {
  HooksPlayerProps,
} from '@/components/hooksPlayer/HooksPlayer';
import {
  HookActionHandler,
  HookLyricDisplay,
} from '@/components/hooksPlayer/constants';
import { VideoHookEntity } from '@/components/hooksPlayer/useVideoHooks';
import ImageWithFallback from '@/components/image/ImageWithFallback';
import { VideoPlayerInterface } from '@/components/video/SimpleVideoPlayer';
import { useCommentCount } from '@/hooks/useComments';
import { HookPlaybackEventTypeContext } from '@/logging/eventTypes/HookPlaybackEventType';
import { logHookWebPlaybackEvent } from '@/logging/logWebUserEvent';

export enum VideoPlaybackState {
  /**
   * Normal playback, play/pause logged as normal
   */
  Normal = 'normal',
  /**
   * Seeking playback, play/pause logged as seeking events
   */
  Seeking = 'seeking',
  /**
   * Restarting playback for video loops
   */
  Restarting = 'restarting',
  /**
   * Transitioning between videos
   *
   * Play or pause will cancel this state
   */
  Transitioning = 'transitioning',
  /**
   * Playback suspended by tab becoming inactive
   *
   * Play will cancel this state, but pause will not
   */
  Suspended = 'suspended',
  /**
   * Playback suspended by navigation
   */
  Navigation = 'navigation',
  /**
   * Playback suspended by unloading
   */
  Unloading = 'unloading',
}

export function getPlayActionName(playbackState: VideoPlaybackState) {
  switch (playbackState) {
    // Events that are manually logged
    case VideoPlaybackState.Restarting:
    case VideoPlaybackState.Transitioning:
      return null;
    case VideoPlaybackState.Seeking:
      return 'SeekHookPlayHook';
    case VideoPlaybackState.Suspended:
      return 'ForegroundPlayHook';
    default:
      return 'PlayHook';
  }
}

export function getPauseActionName(playbackState: VideoPlaybackState) {
  switch (playbackState) {
    // These events are logged manually
    case VideoPlaybackState.Restarting:
    case VideoPlaybackState.Transitioning:
      return null;
    case VideoPlaybackState.Seeking:
      return 'SeekHookPauseHook';
    case VideoPlaybackState.Suspended:
      return 'BackgroundPauseHook';
    case VideoPlaybackState.Navigation:
      return 'NavigatePauseHook';
    case VideoPlaybackState.Unloading:
      return 'ClosePauseHook';
    default:
      return 'PauseHook';
  }
}

export type HooksFeedItemInterface = {
  getContainer: () => HTMLDivElement | null;
  getPlaybackInterface: () => VideoPlayerInterface | null;
  getPlaybackInfo: () => {
    startTime: number;
    endTime: number;
    shouldPlayAfterSeek: boolean;
  };
  getPlaybackState: () => VideoPlaybackState;
  getPlaying: () => boolean;
  getCurrentTime: () => number;
  play: (throwsError?: boolean) => Promise<void>;
  pause: () => void;
  toggle: () => Promise<void>;
  setMuted: (muted?: boolean) => void;
  setPlaybackState: (playbackState: VideoPlaybackState) => void;
};

export type HooksFeedItemProps = {
  ref?: React.Ref<HooksFeedItemInterface>;
  index?: number;
  hook: VideoHookEntity;
  videoElement: HTMLVideoElement | null;
  videoUrls: string[];
  hasPrevious?: boolean;
  hasNext?: boolean;
  isPlaybackAllowed?: boolean;
  isPlaceholder?: boolean;
  isCurrent?: boolean;
  isInactive?: boolean;
  currentUserHandle?: string;
  hookUpdatedAt?: number;
  getPlaybackEventContext: (
    hook: Pick<VideoHookEntity, 'id' | 'recommendationItemId'>,
    context?: Partial<HookPlaybackEventTypeContext>
  ) => HookPlaybackEventTypeContext;
  onPreviousClick?: HookActionHandler;
  onNextClick?: HookActionHandler;
  onCreatorClick?: HookActionHandler<{ handle: string }>;
  onProfileClick?: HookActionHandler<{ handle: string }>;
  onAddToPlaylistClick?: HookActionHandler<{ clipId: string }>;
  onSongArtistClick?: HookActionHandler<{ clipId: string; handle: string }>;
  onSongThumbnailClick?: HookActionHandler<{ clipId: string }>;
  onSongTitleClick?: HookActionHandler<{ clipId: string }>;
  onRemixClick?: HookActionHandler<{ clipId: string }>;
  onFollowClick?: HookActionHandler<{ handle: string }>;
  onLikeClick?: HookActionHandler<{ isLike?: boolean }>;
  onCommentClick?: HookActionHandler;
  onShareClick?: HookActionHandler<{ countShare?: boolean }>;
  onHideCreatorClick?: HookActionHandler<{ handle: string }>;
  onReportClick?: HookActionHandler;
  onDownloadClick?: HookActionHandler;
  onNotInterestedClick?: HookActionHandler;
  onFlagClick?: HookActionHandler;
  onToggleTestClick?: HookActionHandler<{ isTest?: boolean }>;
  onQualityLabelClick?: HookActionHandler<{ newLabel: string | null }>;
  onResetModerationClick?: HookActionHandler;
  onReprocessClick?: HookActionHandler;
} & Pick<
  HooksPlayerProps,
  | 'playing'
  | 'muted'
  | 'onVideoClick'
  | 'onPlaybackError'
  | 'onPlay'
  | 'onPause'
  | 'onSeeked'
  | 'onEnded'
>;

type Props = HooksFeedItemProps &
  Omit<React.HTMLAttributes<HTMLDivElement>, keyof HooksFeedItemProps>;

/**
 * This component renders a single hook in the feed and is responsible for
 * triggering the logging events with context passed in from the parent.
 */
const HooksFeedItem: React.FC<Props> = React.memo((props) => {
  const {
    ref,
    className,
    index,
    hook,
    videoElement,
    videoUrls,
    isPlaybackAllowed = true,
    isPlaceholder = false,
    isCurrent = false,
    isInactive = !isCurrent,
    playing,
    muted = true,
    hasPrevious = false,
    hasNext = false,
    currentUserHandle,
    hookUpdatedAt,
    getPlaybackEventContext,
    // Video player events
    onPlaybackError,
    onPlay,
    onPause,
    onSeeked,
    onEnded,
    // User events
    onVideoClick,
    onPreviousClick,
    onNextClick,
    onCreatorClick,
    onProfileClick,
    onAddToPlaylistClick,
    onSongArtistClick,
    onSongThumbnailClick,
    onSongTitleClick,
    onRemixClick,
    onFollowClick,
    onLikeClick,
    onCommentClick,
    onShareClick,
    onHideCreatorClick,
    onReportClick,
    onDownloadClick,
    onNotInterestedClick,
    onFlagClick,
    onToggleTestClick,
    onQualityLabelClick,
    onResetModerationClick,
    onReprocessClick,
    ...restProps
  } = props;

  const [isPlaying, setIsPlaying] = useState(false);

  const containerRef = useRef<HTMLDivElement>(null);
  const playerRef = useRef<VideoPlayerInterface>(null);

  /**
   * Used to keep track of start/end times and additional state properties for
   * handling logging events properly
   */
  const playbackInfoRef = useRef({
    startTime: 0,
    endTime: 0,
    shouldPlayAfterSeek: false,
    playbackState: VideoPlaybackState.Normal,
  });

  useImperativeHandle(ref, () => ({
    getContainer: () => containerRef.current,
    getPlaybackInterface: () => playerRef.current,
    getPlaybackInfo: () => playbackInfoRef.current,
    getPlaying: () => playerRef.current?.getPlaying() ?? false,
    getCurrentTime: () => playerRef.current?.getCurrentTime() ?? 0,
    getPlaybackState: () => playbackInfoRef.current.playbackState,
    play: async (throwsError = false) =>
      throwsError
        ? playerRef.current?.getInternalPlayer()?.play()
        : playerRef.current?.play(),
    pause: () => {
      playerRef.current?.pause();
    },
    toggle: async () => {
      playerRef.current?.toggle();
    },
    setMuted: (muted?: boolean) => {
      playerRef.current?.setMuted(muted);
    },
    setPlaybackState: (playbackState: VideoPlaybackState) => {
      playbackInfoRef.current.playbackState = playbackState;
    },
  }));

  const lyricsInMetadata = hook.lyricDisplay === HookLyricDisplay.BottomLeft;
  const lyricsOverlayContent = !hook.showLyrics
    ? undefined
    : lyricsInMetadata
      ? HooksLyricOverlay
      : HooksLyricsOverlayCenter;

  const { numComments: commentCount } = useCommentCount({
    entityId: hook.id,
    entityType: 'hook',
    enabled: isCurrent,
    initialCount: hook.commentCount,
    initialDataUpdatedAt: hookUpdatedAt,
  });

  const handleVideoClick = useCallback<
    NonNullable<HooksPlayerProps['onVideoClick']>
  >(
    (e) => {
      onVideoClick?.(e);
      playerRef?.current?.toggle();
    },
    [onVideoClick]
  );

  /**
   * We don't want to use the `loop` prop because it prevents the `ended` event
   * from being fired, and then we can't log when a hook ends/restarts.
   *
   * This handles the manual looping and logging
   */
  const handleVideoEnded = useCallback(
    (e: Event) => {
      async function restartVideo() {
        const videoPlayer = playerRef?.current;

        // Ignore if there's no video player or we're seeking
        if (
          !videoPlayer ||
          playbackInfoRef.current.playbackState === VideoPlaybackState.Seeking
        ) {
          return;
        }

        onEnded?.(e);

        playbackInfoRef.current.playbackState = VideoPlaybackState.Restarting;

        // First event is for the pause (hitting the end of the video)
        playbackInfoRef.current.endTime = videoPlayer.getCurrentTime();
        logHookWebPlaybackEvent({
          actionName: 'AutoRepeatPauseHook',
          context: getPlaybackEventContext(hook, {
            startTime: playbackInfoRef.current.startTime,
            endTime: playbackInfoRef.current.endTime,
          }),
        });

        // Second event is from playing at the beginning
        playbackInfoRef.current.startTime = 0;
        playbackInfoRef.current.endTime = 0;
        logHookWebPlaybackEvent({
          actionName: 'AutoRepeatPlayHook',
          context: getPlaybackEventContext(hook, {
            startTime: playbackInfoRef.current.startTime,
            endTime: playbackInfoRef.current.endTime,
          }),
        });

        // Now actually loop the video to restart playback
        videoPlayer.seek(0);
        await videoPlayer.play();

        playbackInfoRef.current.playbackState = VideoPlaybackState.Normal;
      }
      restartVideo();
    },
    [onEnded, getPlaybackEventContext, hook]
  );

  const handlePlay = useCallback(
    (e: Event) => {
      onPlay?.(e);
      setIsPlaying(true);
      const videoPlayer = playerRef?.current;
      if (!videoPlayer) return;
      const actionName = getPlayActionName(
        playbackInfoRef.current.playbackState
      );
      // Events to ignore
      switch (playbackInfoRef.current.playbackState) {
        case VideoPlaybackState.Transitioning:
        case VideoPlaybackState.Suspended:
          playbackInfoRef.current.playbackState = VideoPlaybackState.Normal;
          break;
        case VideoPlaybackState.Restarting:
          return;
        default:
          break;
      }
      // Log a play event if the video was paused
      if (actionName) {
        logHookWebPlaybackEvent({
          actionName,
          context: getPlaybackEventContext(hook, {
            startTime: playbackInfoRef.current.startTime,
            endTime: playbackInfoRef.current.endTime,
          }),
        });
      }
    },
    [onPlay, getPlaybackEventContext, hook]
  );

  const handlePause = useCallback(
    (e: Event) => {
      onPause?.(e);
      setIsPlaying(false);
      const videoPlayer = playerRef?.current;
      if (!videoPlayer) return;
      const actionName = getPauseActionName(
        playbackInfoRef.current.playbackState
      );
      switch (playbackInfoRef.current.playbackState) {
        case VideoPlaybackState.Transitioning:
          playbackInfoRef.current.playbackState = VideoPlaybackState.Normal;
          break;
        case VideoPlaybackState.Restarting:
          return;
        // Remain in suspended state until next playback
        case VideoPlaybackState.Suspended:
        default:
          break;
      }
      // If this pause event happened at the end of the video, ignore it and
      // let the `ended` event handle the logging/looping instead
      const currentTime = videoPlayer.getCurrentTime();
      const duration = videoPlayer.getDuration();
      if (currentTime >= duration - 0.01) {
        return;
      }
      // Mark the end time using the current playback time
      playbackInfoRef.current.endTime = currentTime;
      // Log the event if we have an action name
      if (actionName) {
        logHookWebPlaybackEvent({
          actionName,
          context: getPlaybackEventContext(hook, {
            startTime: playbackInfoRef.current.startTime,
            endTime: playbackInfoRef.current.endTime,
          }),
        });
      }
      // Mark the start time for the next event using the current playback time
      playbackInfoRef.current.startTime = playbackInfoRef.current.endTime;
    },
    [onPause, getPlaybackEventContext, hook]
  );

  const handleVideoSeekStart = useCallback(() => {
    const shouldPlayAfterSeek = playerRef?.current?.getPlaying() ?? false;
    playbackInfoRef.current.playbackState = VideoPlaybackState.Seeking;
    playbackInfoRef.current.shouldPlayAfterSeek = shouldPlayAfterSeek;
    if (shouldPlayAfterSeek) {
      playerRef?.current?.pause();
    }
  }, []);

  const handleVideoSeekEnd = useCallback(() => {
    async function seekEnd() {
      const shouldPlayAfterSeek = playbackInfoRef.current.shouldPlayAfterSeek;
      playbackInfoRef.current.shouldPlayAfterSeek = false;
      if (shouldPlayAfterSeek) {
        await playerRef?.current?.play();
      }
      playbackInfoRef.current.playbackState = VideoPlaybackState.Normal;
    }
    seekEnd();
  }, []);

  /**
   * @TODO we don't have standalone seek events, just ones that are related to
   * pause/play if the user seeks during playback.
   *
   * We can infer seeking that happens while the video is paused by differences
   * in start/end times of adjacent events, but since we mostly care about
   * overall playback duration, we aren't logging seek events exhaustively.
   */
  const handleVideoSeeked = useCallback(
    (e: Event) => {
      const videoPlayer = playerRef?.current;
      if (!videoPlayer) return;

      onSeeked?.(e);

      // Set the startTime and endTime to the current time of the video element.

      // Note: because we manually handle looping by resetting videoElement.currentTime, the `seeked` event
      // is fired. See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentTime.
      // Setting the startTime and endTime in this way should be safe because if the video is looping, we're setting startTime and endTime
      // to 0 whether the seeked event fires first or the videoEnded event fires first.

      const currentTime = videoPlayer.getCurrentTime();
      playbackInfoRef.current.startTime = currentTime;
      playbackInfoRef.current.endTime = currentTime;
    },
    [onSeeked]
  );

  /**
   * Previous/next handlers wrapped here to supply the index to the parent
   */
  const handlePreviousClick = useCallback<HookActionHandler>(
    (payload, e) => {
      onPreviousClick?.({ index, ...payload }, e);
    },
    [onPreviousClick, index]
  );
  const handleNextClick = useCallback<HookActionHandler>(
    (payload, e) => {
      onNextClick?.({ index, ...payload }, e);
    },
    [onNextClick, index]
  );

  // Make it slightly harder to spam the endpoint
  const hasSharedRef = useRef(false);
  const handleShareClick = useCallback<
    HookActionHandler<{ countShare?: boolean }>
  >(
    (payload, e) => {
      onShareClick?.({ countShare: !hasSharedRef.current, ...payload }, e);
      hasSharedRef.current = true;
    },
    [onShareClick]
  );

  /**
   * If playback is not allowed, force the video to pause
   */
  useEffect(() => {
    if (!isPlaybackAllowed || !isCurrent) {
      playerRef.current?.pause();
      if (!isCurrent) {
        playerRef.current?.seek(0);
        // Cancel the current seek operation
        playbackInfoRef.current.shouldPlayAfterSeek = false;
      }
    }
  }, [isPlaybackAllowed, isCurrent]);

  return (
    <div
      data-index={index}
      ref={containerRef}
      className={twMerge(
        clsx(
          'relative flex h-[calc(100svh-2rem)] min-h-full w-full snap-start snap-always flex-col',
          'max-md:h-[calc(100svh)] max-md:scroll-mt-0',
          'md:scroll-mt-4 xl:scroll-mt-0',
          'transition-opacity duration-300',
          { 'opacity-50': isInactive }
        ),
        className
      )}
      {...restProps}
    >
      {isPlaceholder ? (
        <ImageWithFallback
          className='relative h-full w-full overflow-clip bg-background-primary-dark object-contain md:rounded-xl'
          src={hook.thumbnailImageUrl}
          alt={hook.title}
        />
      ) : (
        <HooksPlayer
          key={hook.id}
          ref={playerRef}
          data-hook-id={hook.id}
          className='h-full w-full overflow-clip bg-background-primary-dark md:rounded-xl'
          playerClassName='md:bottom-26 md:h-auto'
          videoElement={videoElement}
          hookId={hook.id}
          clipId={hook.originalClipId}
          videoUrl={videoUrls?.length ? videoUrls : null}
          imageUrl={hook.thumbnailImageUrl}
          playing={playing}
          muted={muted}
          onVideoClick={handleVideoClick}
          onVideoSeekStart={handleVideoSeekStart}
          onVideoSeekEnd={handleVideoSeekEnd}
          onPlaybackError={onPlaybackError}
          onPlay={handlePlay}
          onPause={handlePause}
          onSeeked={handleVideoSeeked}
          onEnded={handleVideoEnded}
          inert={isInactive}
          duration={hook.videoDuration}
          // While we want Hooks to loop, we don't want to use the loop prop because it
          // prevents the `ended` event from being fired, and then we can't log when
          // a hook ends/restarts. We handle looping manually in HooksFeedClient.tsx.
          loop={false}
          metadataContent={
            <HookMetadataWithSongBar
              className='flex flex-col gap-4'
              hookId={hook.id}
              clipId={hook.originalClipId}
              recommendationItemId={hook.recommendationItemId || undefined}
              handle={hook.user?.handle || undefined}
              displayName={hook.user?.displayName || undefined}
              avatarImageUrl={hook.user?.avatarImageUrl || undefined}
              title={hook.title}
              caption={hook.caption}
              currentUserHandle={currentUserHandle}
              isRemix={hook.clip?.metadata?.isRemix ?? undefined}
              isPlaying={isPlaying}
              clipTitle={hook.clip?.title}
              clipHandle={hook.clip?.handle || undefined}
              clipDisplayName={hook.clip?.displayName || undefined}
              clipImageUrl={hook.clip?.imageUrl || undefined}
              clipPlayCount={hook.clip?.playCount || undefined}
              isFollowingCreator={hook.currentUserFollowsCreator || false}
              isRemixDisabled={!hook.clip?.metadata?.canRemix}
              onFollow={onFollowClick}
              onCreatorClick={onCreatorClick}
              onAddToPlaylist={onAddToPlaylistClick}
              onSongArtistClick={onSongArtistClick}
              onSongThumbnailClick={onSongThumbnailClick}
              onSongTitleClick={onSongTitleClick}
              // onRemix={onRemixClick}
            />
          }
          actionsContent={
            <HookActions
              hookId={hook.id}
              clipId={hook.originalClipId}
              recommendationItemId={hook.recommendationItemId || undefined}
              creatorImageUrl={hook.clip?.avatarImageUrl || undefined}
              creatorHandle={hook.clip?.handle || undefined}
              creatorDisplayName={hook.clip?.displayName || undefined}
              disabledPrevious={!hasPrevious}
              disabledNext={!hasNext}
              currentUserHandle={currentUserHandle}
              isFollowingCreator={hook.currentUserFollowsCreator || false}
              isRemixDisabled={!hook.clip?.metadata?.canRemix}
              isLiked={hook.currentUserLiked}
              isNotInterested={hook.currentUserDisliked}
              isPlaying={isPlaying}
              // viewCount={hook.viewCount}
              commentCount={commentCount || undefined} // Hide when zero
              likeCount={hook.likeCount || undefined} // Hide when zero
              onPrevious={onPreviousClick && handlePreviousClick}
              onNext={onNextClick && handleNextClick}
              onProfileClick={onProfileClick}
              onFollow={onFollowClick}
              onRemix={onRemixClick}
              onLike={onLikeClick}
              onNotInterested={onNotInterestedClick}
              onComment={onCommentClick}
              onShare={handleShareClick}
              onHideCreator={onHideCreatorClick}
              onReport={onReportClick}
              onDownload={
                // Only allow downloads of own hooks
                currentUserHandle === hook.user?.handle
                  ? onDownloadClick
                  : undefined
              }
              onFlag={onFlagClick}
              onToggleTest={onToggleTestClick}
              isTestHook={hook.isTest}
              onQualityLabelClick={onQualityLabelClick}
              onResetModeration={onResetModerationClick}
              onReprocess={onReprocessClick}
              humanRating={hook.humanRating}
            />
          }
          lyricsInMetadata={lyricsInMetadata}
          lyricsOverlayContent={lyricsOverlayContent}
          lyricDisplayStyle={hook.lyricDisplay}
          clipStartTime={hook.startClipTimestamp}
          clipEndTime={hook.endClipTimestamp}
        />
      )}
    </div>
  );
});
HooksFeedItem.displayName = 'HooksFeedItem';

export default HooksFeedItem;
