import {
  getWarpBeatsFromSeconds,
  getWarpEnabledAndPopulated,
  getWarpSecondsFromBeats,
} from '@suno/studiokit/warpUtils';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo } from 'react';
import { useSessionStorage } from 'usehooks-ts';

import { useStores } from '@/app/(root)/AppProviders';
import {
  createContext,
  useContext,
  useContextSelector,
} from '@/hooks/useContextSelector';
import useDebounceCallback from '@/hooks/useDebounceCallback';
import { ContextType } from '@/logging/contextTypes';
import { Clip } from '@/state/clipStore';
import { autoAssignPlayContextIndex } from '@/state/queueStore';
import { sleep } from '@/utils/utils';

import { useRemasterPlayCount } from '../modal/RemasterPlayCountContext';
import StudioContext from '../studio/StudioContext';
import getAbsolutePoint from '../studio/getAbsolutePoint';
import getClipRelativePoint from '../studio/getClipRelativePoint';
import { getDerivedTiming, getFocusedStudioClip } from '../studio/selectors';
import { getMaximumKnownClipDuration } from '../studio/useFfmpegBufferCache';
import { ClipBrowserContext } from './useClipBrowser';
import useClipChanges from './useClipChanges';
import useHasBeenPlayed from './useHasBeenPlayed';

export default function useClipPlayback(
  clip: Clip | null,
  playContextFromProps?: any
) {
  const { incrementClipPlayCount } = useClipChanges();
  const remasterPlayCount = useRemasterPlayCount();
  const { playbar: playbarStore, queue: queueStore } = useStores();
  const playContext = useContextSelector(ClipBrowserContext, (ctx) =>
    ctx ? ctx.playContext : null
  );
  const derivedPlayContext = playContextFromProps || playContext;

  const [willPlay, setWillPlay] = useSessionStorage<boolean>(
    `${clip?.id ?? 'NO_CLIP_ID'}-will-play`,
    false
  );

  const isLoadedIntoPlayer =
    playbarStore.clip?.id !== undefined && playbarStore.clip?.id === clip?.id;

  const storeIsPlayingClip = playbarStore.isPlaying && isLoadedIntoPlayer;

  const getCurrentProgress = useCallback(() => {
    if (isLoadedIntoPlayer) {
      return playbarStore.getCurrentTime() / (clip?.metadata?.duration ?? 0);
    }
    return 0;
  }, [isLoadedIntoPlayer, clip?.metadata?.duration]);

  const debouncedSeek = useDebounceCallback(
    playbarStore.userSetCurrentProgressWithTime,
    50
  );

  const setCurrentProgress = useCallback(
    (progress: number) => {
      if (isLoadedIntoPlayer) {
        debouncedSeek(progress * (clip?.metadata?.duration ?? 0));
      }
    },
    [isLoadedIntoPlayer, clip?.metadata?.duration, debouncedSeek]
  );

  useEffect(() => {
    if (!clip || isLoadedIntoPlayer) {
      setWillPlay(false);
    }
  }, [clip, isLoadedIntoPlayer]);

  useEffect(() => {
    if (!storeIsPlayingClip) {
      setWillPlay(false);
    }
  }, [storeIsPlayingClip]);

  const [, setHasBeenPlayed] = useHasBeenPlayed(clip);

  const onPlayClick = useCallback(async () => {
    if (!clip || !['streaming', 'complete'].includes(clip.status || '')) return;
    setHasBeenPlayed(true);
    const wasPlaying = storeIsPlayingClip;
    if (!wasPlaying) {
      setWillPlay(true);
    } else {
      setWillPlay(false);
    }
    await sleep(1);
    // Check if we should skip play count increment
    let skipPlayCount = false;

    if (!wasPlaying && remasterPlayCount) {
      const isFirstPlayInSession =
        remasterPlayCount.markClipPlayedAndCheckIfFirst(clip.id);
      skipPlayCount = !isFirstPlayInSession;
    }

    if (isLoadedIntoPlayer) {
      playbarStore.togglePlay();
    } else {
      playbarStore.playClip(
        clip,
        undefined,
        undefined,
        false,
        0,
        skipPlayCount
      );
      const effectivePlayContext = derivedPlayContext || {
        currentIndex: 0,
        clips: queueStore.getClipsForContext(ContextType.Song, clip.id),
        contextType: ContextType.Song,
        contextId: clip.id,
      };

      queueStore.setPlayContext(
        autoAssignPlayContextIndex(effectivePlayContext, clip.id)
      );
    }
    if (!wasPlaying && !skipPlayCount) {
      incrementClipPlayCount(clip.id);
    }
  }, [
    setHasBeenPlayed,
    clip,
    incrementClipPlayCount,
    remasterPlayCount,
    playbarStore.togglePlay,
    playbarStore.playClip,
    playContext,
    queueStore.getClipsForContext,
    queueStore.setPlayContext,
    storeIsPlayingClip,
    isLoadedIntoPlayer,
  ]);

  useEffect(() => {
    if (storeIsPlayingClip) {
      setHasBeenPlayed(true);
    }
  }, [storeIsPlayingClip, setHasBeenPlayed]);

  return useMemo(
    () => ({
      getCurrentProgress,
      setCurrentProgress,
      onPlayClick,
      isPlaying: storeIsPlayingClip,
      willPlay,
      isLoadedIntoPlayer,
    }),
    [
      onPlayClick,
      storeIsPlayingClip,
      willPlay,
      isLoadedIntoPlayer,
      getCurrentProgress,
      setCurrentProgress,
    ]
  );
}

const ClipPlaybackContext = createContext<ReturnType<typeof useClipPlayback>>({
  isPlaying: false,
  isLoadedIntoPlayer: false,
  willPlay: false,
  onPlayClick: async () => {},
  getCurrentProgress: () => 0,
  setCurrentProgress: () => {},
});

export function ClipPlaybackProvider({
  value,
  children,
}: {
  value: ReturnType<typeof useClipPlayback>;
  children: React.ReactNode;
}) {
  return (
    <ClipPlaybackContext.Provider value={value}>
      {children}
    </ClipPlaybackContext.Provider>
  );
}

export const useStudioClipPlayback = (
  clip: Clip | null,
  studioPreviewsWithContextWindow?: boolean
) => {
  const hasStudioContext = useContextSelector(StudioContext, (ctx) => !!ctx);
  const { incrementClipPlayCount } = useClipChanges();
  const remasterPlayCount = useRemasterPlayCount();

  const previewOffTimeline = useContextSelector(StudioContext, (ctx) =>
    ctx ? ctx.previewController.previewOffTimeline : null
  );
  const derivedTiming = useContextSelector(StudioContext, (ctx) =>
    ctx ? getDerivedTiming(ctx.heardState) : null
  );
  const play = useContextSelector(StudioContext, (ctx) =>
    ctx ? ctx.playbackController.play : null
  );
  const stop = useContextSelector(StudioContext, (ctx) =>
    ctx ? ctx.playbackController.stop : null
  );
  const playing = useContextSelector(StudioContext, (ctx) =>
    ctx ? ctx.playbackController.playing : null
  );
  const timing = useContextSelector(StudioContext, (ctx) =>
    ctx ? getDerivedTiming(ctx.heardState) : null
  );
  const isLoadedIntoPlayer = useContextSelector(StudioContext, (ctx) =>
    Boolean(
      ctx && clip ? ctx.previewController.previewPackage?.id === clip.id : null
    )
  );

  const asPreviewedStudioClip = useContextSelector(StudioContext, (ctx) => {
    if (!ctx) return null;
    const focusedStudioClip = getFocusedStudioClip(ctx.heardState);
    if (!focusedStudioClip) return null;
    if (focusedStudioClip.clipId === clip?.id) return focusedStudioClip;
    return null;
  });

  const [willPlay, setWillPlay] = useSessionStorage<boolean>(
    `${clip?.id ?? 'NO_CLIP_ID'}-will-play-studio`,
    false
  );

  const isPlaying = useMemo(() => {
    return Boolean(isLoadedIntoPlayer && playing);
  }, [isLoadedIntoPlayer, playing]);

  const getCurrentBeats = useContextSelector(StudioContext, (ctx) =>
    ctx ? ctx.playbackController.getCurrentBeats : () => 0
  );

  const getCurrentProgress = useCallback(() => {
    if (isLoadedIntoPlayer && derivedTiming && asPreviewedStudioClip) {
      const duration = clip ? getMaximumKnownClipDuration(clip) : 0;
      if (!duration) {
        return 0;
      }
      const currentTimeBeats =
        getWarpEnabledAndPopulated(asPreviewedStudioClip.warp) ||
        asPreviewedStudioClip.readStartBeats !== 0 ||
        !!asPreviewedStudioClip.warp.markers[0]
          ? getClipRelativePoint(
              derivedTiming,
              asPreviewedStudioClip,
              getCurrentBeats(),
              true
            )[0]
          : getCurrentBeats();

      const currentTimeSeconds = getWarpSecondsFromBeats(
        asPreviewedStudioClip.warp,
        currentTimeBeats
      );
      if (!Number.isFinite(currentTimeSeconds)) {
        return 0;
      }
      const result = Math.max(0, Math.min(1, currentTimeSeconds / duration));
      if (Number.isNaN(result)) {
        return 0;
      }
      return result;
    }
    return 0;
  }, [
    isLoadedIntoPlayer,
    getCurrentBeats,
    derivedTiming,
    asPreviewedStudioClip,
    clip?.metadata?.duration,
  ]);

  const seek = useContextSelector(StudioContext, (ctx) =>
    ctx ? ctx.playbackController.seek : () => void 0
  );

  const setCurrentProgress = useCallback(
    (progress: number) => {
      if (seek && isLoadedIntoPlayer && asPreviewedStudioClip && timing) {
        const duration = clip ? getMaximumKnownClipDuration(clip) : 0;

        const contentBeats = getWarpBeatsFromSeconds(
          asPreviewedStudioClip.warp,
          progress * duration
        );

        seek(
          Math.max(
            asPreviewedStudioClip.startBeats,
            Math.min(
              asPreviewedStudioClip.endBeats,
              getAbsolutePoint(
                timing,
                asPreviewedStudioClip,
                contentBeats,
                true
              )
            )
          )
        );
      }
    },
    [
      timing,
      isLoadedIntoPlayer,
      seek,
      asPreviewedStudioClip,
      clip?.metadata?.duration,
    ]
  );

  const [, setHasBeenPlayed] = useHasBeenPlayed(clip);
  const [bufferIsLoading, setBufferIsLoading] = useSessionStorage<boolean>(
    `${clip?.id ?? 'NO_CLIP_ID'}-buffer-is-loading`,
    false
  );

  const onPlayClick = useCallback(async () => {
    if (!clip) return;
    const wasPlaying = isPlaying;
    setHasBeenPlayed(true);
    if (isPlaying) {
      setWillPlay(false);
      stop?.(true);
    } else if (isLoadedIntoPlayer) {
      setWillPlay(true);
      play?.();
    } else {
      setWillPlay(true);
      setBufferIsLoading(true);
      await previewOffTimeline?.(clip, studioPreviewsWithContextWindow);
      setBufferIsLoading(false);
    }
    if (!wasPlaying) {
      // Check if we're in a remaster modal context
      if (remasterPlayCount) {
        const isFirstPlayInSession =
          remasterPlayCount.markClipPlayedAndCheckIfFirst(clip.id);
        if (isFirstPlayInSession) {
          incrementClipPlayCount(clip.id);
        }
      } else {
        // Normal behavior outside remaster modal
        incrementClipPlayCount(clip.id);
      }
    }
  }, [
    setHasBeenPlayed,
    getCurrentProgress,
    setCurrentProgress,
    incrementClipPlayCount,
    remasterPlayCount,
    previewOffTimeline,
    isPlaying,
    play,
    stop,
    clip,
  ]);

  useEffect(() => {
    if (!isLoadedIntoPlayer && !bufferIsLoading) {
      setWillPlay(false);
    } else {
      setWillPlay(true);
    }
  }, [isLoadedIntoPlayer, bufferIsLoading]);

  useEffect(() => {
    if (isPlaying) {
      setHasBeenPlayed(true);
    }
  }, [isPlaying, setHasBeenPlayed]);

  return useMemo(
    () => ({
      onPlayClick,
      isLoadedIntoPlayer,
      isPlaying,
      willPlay,
      getCurrentProgress,
      setCurrentProgress,
    }),
    [
      hasStudioContext,
      onPlayClick,
      isPlaying,
      isLoadedIntoPlayer,
      willPlay,
      getCurrentProgress,
      setCurrentProgress,
    ]
  );
};

export const AutoClipPlaybackProvider = ({
  children,
  clip,
  studioPreviewsWithContextWindow,
}: {
  children: React.ReactNode;
  clip: Clip | null;
  studioPreviewsWithContextWindow?: boolean;
}) => {
  const hasStudioContext = useContextSelector(StudioContext, (ctx) => !!ctx);

  return hasStudioContext ? (
    <StudioClipPlaybackContextProvider
      clip={clip}
      studioPreviewsWithContextWindow={studioPreviewsWithContextWindow}
    >
      {children}
    </StudioClipPlaybackContextProvider>
  ) : (
    <ClipPlaybackContextProvider clip={clip}>
      {children}
    </ClipPlaybackContextProvider>
  );
};

const ClipPlaybackContextProvider = observer(
  ({ children, clip }: { children: React.ReactNode; clip: Clip | null }) => {
    return (
      <ClipPlaybackProvider value={useClipPlayback(clip)}>
        {children}
      </ClipPlaybackProvider>
    );
  }
);

const StudioClipPlaybackContextProvider = ({
  children,
  clip,
  studioPreviewsWithContextWindow,
}: {
  children: React.ReactNode;
  clip: Clip | null;
  studioPreviewsWithContextWindow?: boolean;
}) => {
  return (
    <ClipPlaybackProvider
      value={useStudioClipPlayback(clip, studioPreviewsWithContextWindow)}
    >
      {children}
    </ClipPlaybackProvider>
  );
};

export const useClipPlaybackContext = () => {
  return useContext(ClipPlaybackContext);
};
