import styled from '@emotion/styled';
import { useMutation, useQuery } from '@tanstack/react-query';
import clsx from 'clsx';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useIntersectionObserver } from 'usehooks-ts';

import { useStores } from '@/app/(root)/AppProviders';
import { toast } from '@/components/toast/Toast';
import useClip from '@/hooks/useClip';
import { useContextSelector } from '@/hooks/useContextSelector';
import {
  ArrowRightIcon,
  InfoIcon,
  PauseIcon,
  PlayIcon,
  ThumbsDownIcon,
  ThumbsUpIcon,
  WaveformIcon,
} from '@/icons';
import { Clip, isDisliked, isLiked } from '@/state/clipStore';
import { getOpusFileURL, getOrGenerateOpusFileUrl } from '@/utils/download';
import { snapWithEvent } from '@/utils/snap';

import Button, {
  ButtonShape,
  ButtonSize,
  ButtonVariant,
} from '../button/Button';
import ImageWithFallback from '../image/ImageWithFallback';
import SpinnerSVG from '../svg/SpinnerSVG';
import { Tooltip } from '../tooltip/Tooltip';
import StudioContext from './StudioContext';
import { useClipArrangementPackageQuery } from './fetchClipArrangementPackage';
import {
  ClipInsertionSpec,
  applyInsertionSpec,
  getClosestInsertionSpec,
} from './insertion';
import { getFocusedStudioClip } from './selectors';
import { ClipArrangementPackage } from './types';
import useFfmpegBufferCache from './useFfmpegBufferCache';
import useGetInsertionSpecs from './useGetInsertionSpecs';

const GenerateOpusOverlay = styled.div<{ processing: boolean }>`
  position: absolute;
  z-index: 4;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: ${(props) => (props.processing ? 1 : 0)};
  background-color: var(--color-background-smoke-thick);
  backdrop-filter: blur(10px);
  transition: opacity 0.1s ease-in-out;
  pointer-events: auto;
  cursor: default;
  &:hover {
    opacity: 1;
  }
`;

const SongWrapper = styled.div<{
  isLoading: boolean;
  isDragging: boolean;
  preventDrag?: boolean;
  short?: boolean;
}>`
  pointer-events: ${(props) => (props.isLoading ? 'none' : 'auto')};
  cursor: ${(props) => (props.preventDrag ? 'pointer' : 'grab')};
  user-select: none;
  position: relative;
  opacity: ${(props) => (props.isDragging ? 0.3 : 1)};
  min-height: ${(props) => (props.short ? '52px' : '60px')};
  border-radius: 10px;
  &:active {
    cursor: ${(props) => (props.preventDrag ? 'default' : 'grabbing')};
  }
  overflow: hidden;
  &:after {
    content: '';
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 3;
    pointer-events: none;
    background-color: ${(props) =>
      props.isLoading ? 'var(--color-background-smoke-dense)' : 'transparent'};
    opacity: ${(props) => (props.isLoading ? 1 : 0)};
  }
`;

const ClipDisplayWrapper = styled.div<{ active: boolean; isLoading: boolean }>`
  position: relative;
  z-index: 4;
  border-radius: 10px;
  height: 60px;
  padding: 5px;
  display: grid;
  grid-template-columns: 40px 1fr;
  gap: 10px;
  width: 100%;
  overflow: hidden;
  background-color: ${({ active }) =>
    active ? 'var(--color-background-glass-thick)' : 'transparent'};
  opacity: ${({ isLoading }) => (isLoading ? 0.5 : 1)};
  > * {
    min-width: 0;
  }
`;

const ClipDragHighlight = styled.div`
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 2;
  transition: background-color 0.1s ease-in-out;
  &:hover {
    background-color: var(--color-background-glass-thin);
    cursor: grab;
  }
`;

const ClipDisplay = observer(
  ({
    clip,
    loading,
    onPlayClick,
    onLikeClick,
    onDislikeClick,
    onCommitPreviewClick,
    onAutoInsertClick,

    canAutoInsert,
    canCommitPreview,
    isPreviewing,
    isFocused,
    audioLoading,
    timelinePlaying,
  }: {
    clip: Clip;
    loading: boolean;
    onPlayClick: (forcePreview: boolean) => void;
    onLikeClick: () => void;
    onDislikeClick: () => void;
    onCommitPreviewClick: () => void;
    onAutoInsertClick: (forcePreview: boolean) => void;

    canAutoInsert: boolean;
    canCommitPreview: boolean;
    isPreviewing: boolean;
    isFocused: boolean;
    audioLoading: boolean;
    timelinePlaying: boolean;
  }) => {
    return (
      <ClipDisplayWrapper isLoading={loading} active={isFocused}>
        {!loading && <ClipDragHighlight />}
        <div className='relative flex h-[50px] w-[40px] items-center justify-center'>
          {clip.status === 'complete' ? (
            <>
              <ImageWithFallback
                className='h-[50px] w-[40px] rounded-md object-cover'
                src={clip.image_url}
                alt={clip.title ?? 'Song Image'}
              />
              <Tooltip label={isPreviewing ? '' : 'Preview'} placement='left'>
                <Button
                  className={`absolute inset-0 z-10 flex items-center justify-center rounded-sm hover:bg-[rgba(0,0,0,0.5)] ${isPreviewing ? 'bg-[rgba(0,0,0,0.5)]' : ''} ${isPreviewing ? 'border-2 border-white' : ''}`}
                  variant={ButtonVariant.Tertiary}
                  size={ButtonSize.Large}
                  disabled={!clip.metadata?.duration}
                  icon={
                    audioLoading ? (
                      <SpinnerSVG />
                    ) : isPreviewing && timelinePlaying ? (
                      <PauseIcon />
                    ) : (
                      <PlayIcon />
                    )
                  }
                  onClick={(e) => {
                    e.stopPropagation();
                    onPlayClick(true);
                  }}
                />
              </Tooltip>
            </>
          ) : (
            <SpinnerSVG />
          )}
        </div>
        {loading ? (
          <div className='flex items-center justify-between gap-2'>
            <div>Loading...</div>
            <SpinnerSVG />
          </div>
        ) : (
          <div>
            <div className='flex items-center justify-between gap-2'>
              <Tooltip label='Open in new tab' placement='right'>
                <a
                  className='z-10 overflow-hidden text-sm text-ellipsis whitespace-nowrap hover:underline'
                  href={`/song/${clip.id}`}
                  target='_blank'
                >
                  {clip.title}
                </a>
              </Tooltip>
              <div className='flex items-center gap-1'>
                {
                  <Button
                    className={clsx('relative z-10 text-foreground-inactive', {
                      ['text-foreground-priamry']: isLiked(clip),
                    })}
                    variant={ButtonVariant.Tertiary}
                    shape={ButtonShape.Pill}
                    icon={<ThumbsUpIcon className='h-5 w-5' />}
                    size={ButtonSize.Mini}
                    onClick={onLikeClick}
                  />
                }
                {
                  <Button
                    className={clsx('relative z-10 text-foreground-inactive', {
                      ['text-foreground-priamry']: isDisliked(clip),
                    })}
                    variant={ButtonVariant.Tertiary}
                    shape={ButtonShape.Pill}
                    icon={<ThumbsDownIcon className='h-5 w-5' />}
                    size={ButtonSize.Mini}
                    onClick={onDislikeClick}
                  />
                }
                {canCommitPreview ? (
                  <Button
                    className='relative z-10'
                    shape={ButtonShape.Pill}
                    variant={ButtonVariant.Primary}
                    icon={<ArrowRightIcon />}
                    size={ButtonSize.Mini}
                    onClick={onCommitPreviewClick}
                    disabled={!clip.metadata?.duration}
                  >
                    Commit
                  </Button>
                ) : canAutoInsert ? (
                  <Button
                    className='relative z-10'
                    shape={ButtonShape.Pill}
                    variant={ButtonVariant.Secondary}
                    icon={<ArrowRightIcon />}
                    size={ButtonSize.Mini}
                    disabled={!clip.metadata?.duration}
                    onClick={() => onAutoInsertClick(false)}
                  >
                    Insert
                  </Button>
                ) : (
                  <Tooltip
                    label='Auto-insert is not available for this clip. Drag to the timeline to insert at a location of your choice.'
                    placement='right'
                  >
                    <Button
                      className='relative z-10'
                      disabled
                      shape={ButtonShape.Pill}
                      variant={ButtonVariant.Tertiary}
                      size={ButtonSize.Mini}
                      icon={<InfoIcon />}
                    />
                  </Tooltip>
                )}
              </div>
            </div>
            <div className='overflow-hidden text-sm text-ellipsis whitespace-nowrap opacity-50'>
              {clip.metadata.tags || '[no tags]'}
            </div>
          </div>
        )}
      </ClipDisplayWrapper>
    );
  }
);

interface EditV3LibrarySongWrapperProps {
  clipId: string;
  isDragging: boolean;
  onDragStart: () => void;
  onDragEnd: () => void;
  onLikeChange?: () => void;
  short?: boolean;
  preventStreamingPlayback?: boolean;
}

const EditV3LibrarySong = observer(
  ({
    clipId,
    clip,
    clipArrangementPackage,
    onDragStart,
    onDragEnd,
    onLikeChange,
    isPreviewing,
  }: {
    clipId: string;
    clip: Clip | null;
    clipArrangementPackage: ClipArrangementPackage | null;
    onDragStart: () => void;
    onDragEnd: () => void;
    onLikeChange?: () => void;
    isPreviewing: boolean;
  }) => {
    const { project, clips: clipsStore } = useStores();
    const beatsToCanvasX = useContextSelector(
      StudioContext,
      (context) => context.beatsToCanvasX
    );
    const timelineWrapperRef = useContextSelector(
      StudioContext,
      (context) => context.timelineController.wrapperRef
    );
    const frameCountRef = useContextSelector(
      StudioContext,
      (context) => context.timelineController.frameCountRef
    );
    const firstTrackId = useContextSelector(
      StudioContext,
      (context) => context.state.tracks[0]?.id ?? null
    );
    const oneTrackMode = useContextSelector(
      StudioContext,
      (context) => context.state.tracks.length === 1
    );
    const magnetMode = useContextSelector(
      StudioContext,
      (context) => context.magnetMode
    );
    const dragStateRef = useContextSelector(
      StudioContext,
      (context) => context.dragStateRef
    );
    const gridSizeRef = useContextSelector(
      StudioContext,
      (context) => context.gridSizeRef
    );
    const focusedTrackId = useContextSelector(
      StudioContext,
      (context) => context.state.selection.focusedTrackId
    );
    const anchorBeats = useContextSelector(
      StudioContext,
      (context) => context.state.selection.anchorBeats
    );
    const focusBeats = useContextSelector(
      StudioContext,
      (context) => context.state.selection.focusBeats
    );
    const focusedClipId = useContextSelector(
      StudioContext,
      (context) => getFocusedStudioClip(context.state)?.clipId
    );
    const previewPackage = useContextSelector(
      StudioContext,
      (context) => context.previewController.previewPackage
    );
    const previewOrSetState = useContextSelector(
      StudioContext,
      (context) => context.previewOrSetState
    );
    const previewOffTimeline = useContextSelector(
      StudioContext,
      (context) => context.previewController.previewOffTimeline
    );
    const stopPreviewing = useContextSelector(
      StudioContext,
      (context) => context.previewController.stopPreviewing
    );
    const stopAt = useContextSelector(
      StudioContext,
      (context) => context.playbackController.stopAt
    );
    const stop = useContextSelector(
      StudioContext,
      (context) => context.playbackController.stop
    );
    const seek = useContextSelector(
      StudioContext,
      (context) => context.playbackController.seek
    );
    const playing = useContextSelector(
      StudioContext,
      (context) => context.playbackController.playing
    );
    const play = useContextSelector(
      StudioContext,
      (context) => context.playbackController.play
    );
    const handleClickDrag = useContextSelector(
      StudioContext,
      (context) => context.handleClickDrag
    );
    const commitPreview = useContextSelector(
      StudioContext,
      (context) => context.commitPreview
    );

    const dragRef = useRef<HTMLDivElement>(null);

    const clipStatus = clip?.status;

    const getInsertionSpecs = useGetInsertionSpecs(clipArrangementPackage);

    const handleDrag = useCallback(() => {
      if (!clipArrangementPackage) {
        return {
          onMouseMove: undefined,
          onMouseUp: undefined,
        };
      }

      const { clip, studioClip } = clipArrangementPackage;

      const createPointInsertionSpec = (
        trackId: string | null,
        beats: number
      ): ClipInsertionSpec => {
        if (oneTrackMode) {
          trackId = firstTrackId ?? null;
        }
        return {
          clip,
          studioClip: {
            ...studioClip,
            startBeats: beats,
            endBeats: beats + (studioClip.endBeats - studioClip.startBeats),
          },
          replacementStartBeats: beats,
          replacementEndBeats: magnetMode
            ? beats
            : beats + (studioClip.endBeats - studioClip.startBeats),
          trackId,
        };
      };

      const wrapper = timelineWrapperRef.current;
      if (!wrapper) return { onMouseMove: undefined, onMouseUp: undefined };

      const handleMouseEnter = () => {
        if (dragStateRef.current) {
          dragStateRef.current.active = true;
        }
      };

      const handleMouseLeave = () => {
        if (dragStateRef.current) {
          dragStateRef.current.active = false;
        }
      };

      wrapper.addEventListener('mouseenter', handleMouseEnter);
      wrapper.addEventListener('mouseleave', handleMouseLeave);

      const wasDrag = false;
      let insertionSpecs: ClipInsertionSpec[] = [];

      return {
        onMouseMove: ({
          moveBeats,
          moveTrackId,
          moveEvent,
          moveCanvasRelativePx,
          isDrag,
        }: {
          moveTrackId: string | null;
          moveEvent: MouseEvent;
          moveSeconds: number;
          moveBeats: number;
          moveCanvasRelativePx: number;
          moveContentRelativePx: number;
          isDrag: boolean;
        }) => {
          if (isDrag) {
            insertionSpecs = getInsertionSpecs();
            const snappedMoveBeats = snapWithEvent(
              moveEvent,
              moveBeats,
              gridSizeRef.current
            );

            let targetInsertionSpec = getClosestInsertionSpec(
              insertionSpecs,
              moveTrackId,
              moveBeats
            );

            const targetStartX = targetInsertionSpec
              ? beatsToCanvasX(targetInsertionSpec.replacementStartBeats)
              : Infinity;

            const targetEndX = targetInsertionSpec
              ? beatsToCanvasX(targetInsertionSpec.replacementEndBeats)
              : -Infinity;

            if (
              targetStartX > moveCanvasRelativePx + 10 ||
              targetEndX < moveCanvasRelativePx - 10
            ) {
              targetInsertionSpec = createPointInsertionSpec(
                moveTrackId,
                snappedMoveBeats
              );
            }

            dragStateRef.current = {
              studioClip,
              trackId: moveTrackId,
              active: true,
              insertionSpecs,
              targetInsertionSpec,
            };

            if (!wasDrag) {
              onDragStart();
            }
          }
        },
        onMouseUp: () => {
          wrapper.removeEventListener('mouseenter', handleMouseEnter);
          wrapper.removeEventListener('mouseleave', handleMouseLeave);

          const dragState = dragStateRef.current;

          if (!dragState || !dragState.active) {
            dragStateRef.current = null;
            frameCountRef.current++;
            onDragEnd();
            return;
          }

          if (dragStateRef.current?.targetInsertionSpec) {
            previewOrSetState(
              clipId,
              applyInsertionSpec(dragStateRef.current?.targetInsertionSpec)
            );
          }

          dragStateRef.current = null;
          frameCountRef.current++;
          onDragEnd();
        },
      };
    }, [
      clipArrangementPackage,
      onDragStart,
      onDragEnd,
      oneTrackMode,
      firstTrackId,
      magnetMode,
      getInsertionSpecs,
      beatsToCanvasX,
      dragStateRef,
      frameCountRef,
      previewOrSetState,
      clipId,
    ]);

    const handleMouseDown = useCallback(
      (e: MouseEvent) => {
        handleClickDrag(handleDrag)(e);
      },
      [handleClickDrag, handleDrag]
    );

    useEffect(() => {
      const element = dragRef.current;
      if (!element) return;

      element.addEventListener('mousedown', handleMouseDown);
      return () => {
        element.removeEventListener('mousedown', handleMouseDown);
      };
    }, [handleMouseDown]);

    const generateOpusFileMutation = useMutation({
      mutationFn: async () => {
        try {
          return await getOrGenerateOpusFileUrl(project.apiClient, clipId);
        } catch (error) {
          toast({
            title: 'Processing failed',
            description: `If this error persists, please contact support.`,
            status: 'error',
            duration: 5000,
            isClosable: true,
          });
          throw error;
        }
      },
    });

    const hasOpusFileQuery = useQuery({
      queryKey: [clipId, 'hasOpusFile', generateOpusFileMutation.data],
      queryFn: async () => {
        if (generateOpusFileMutation.data) return true;
        const url = await getOpusFileURL(project.apiClient, clipId);
        return !!url;
      },
      enabled: clipStatus === 'complete',
    });

    const [audioLoading, setAudioLoading] = useState(false);

    const { getFfmpegBuffer } = useFfmpegBufferCache();

    const loadAudio = useCallback(async () => {
      setAudioLoading(true);
      await (
        await getFfmpegBuffer(clipId)
      ).promise;
      setAudioLoading(false);
    }, [clipId, getFfmpegBuffer]);

    const bestInsertionSpec: ClipInsertionSpec | null = useMemo(() => {
      const result = getClosestInsertionSpec(
        getInsertionSpecs(),
        focusedTrackId,
        (anchorBeats + focusBeats) / 2
      );
      return result;
    }, [getInsertionSpecs, focusedTrackId, anchorBeats, focusBeats]);

    const handlePlayClick = useCallback(
      async (forcePreview: boolean = false) => {
        if (previewPackage?.id === clipId) {
          if (playing) {
            stop();
          } else {
            seek(previewPackage.startBeats);
            if (!bestInsertionSpec) {
              stopAt(previewPackage.endBeats);
            }
            play();
          }
        } else {
          await loadAudio();
          if (bestInsertionSpec) {
            previewOrSetState(
              clipId,
              applyInsertionSpec(bestInsertionSpec),
              forcePreview
            );
          } else if (clipArrangementPackage) {
            previewOffTimeline(clipArrangementPackage.clip);
          } else {
            console.error('Unable to preview');
            return;
          }
        }
      },
      [
        playing,
        stopPreviewing,
        stopAt,
        audioLoading,
        loadAudio,
        bestInsertionSpec,
        clipArrangementPackage,
        previewOffTimeline,
        previewOrSetState,
        previewPackage,
      ]
    );

    return (
      <>
        {hasOpusFileQuery.data === false && clipStatus === 'complete' && (
          <GenerateOpusOverlay processing={generateOpusFileMutation.isPending}>
            <Button
              variant={ButtonVariant.Secondary}
              onClick={() => {
                if (generateOpusFileMutation.isError) {
                  generateOpusFileMutation.reset();
                }
                generateOpusFileMutation.mutate();
              }}
              icon={
                generateOpusFileMutation.isPending ? (
                  <SpinnerSVG className='h-5 w-5' />
                ) : (
                  <WaveformIcon className='h-5 w-5' />
                )
              }
            >
              {generateOpusFileMutation.isError
                ? 'Error occurred. Retry?'
                : 'Preprocess for Studio'}
            </Button>
          </GenerateOpusOverlay>
        )}
        {clip ? (
          <div
            ref={dragRef}
            draggable='false'
            onDragStart={(e) => {
              e.preventDefault();
            }}
          >
            <ClipDisplay
              clip={clip}
              loading={!clipArrangementPackage || clipStatus !== 'complete'}
              onLikeClick={async () => {
                await clipsStore.likeClip(clip.id, !isLiked(clip));
                onLikeChange?.();
              }}
              onDislikeClick={async () => {
                await clipsStore.dislikeClip(clip.id, !isDisliked(clip));
                onLikeChange?.();
              }}
              onCommitPreviewClick={() => commitPreview()}
              onAutoInsertClick={handlePlayClick}
              canAutoInsert={!!bestInsertionSpec}
              canCommitPreview={
                previewPackage?.id === clipId && !previewPackage.offTimeline
              }
              isPreviewing={isPreviewing}
              isFocused={focusedClipId === clipId}
              audioLoading={audioLoading}
              timelinePlaying={playing}
              onPlayClick={handlePlayClick}
            />
          </div>
        ) : (
          <div className='flex h-full items-center justify-center'>
            <SpinnerSVG />
          </div>
        )}
      </>
    );
  }
);

export default observer(function EditV3LibrarySongWrapper({
  onDragStart,
  onDragEnd,
  onLikeChange,
  clipId,
  isDragging,
  short,
  preventStreamingPlayback,
}: EditV3LibrarySongWrapperProps) {
  const previewId = useContextSelector(
    StudioContext,
    (context) => context.previewController.previewPackage?.id
  );
  const { isIntersecting, ref } = useIntersectionObserver({
    threshold: 0.1,
  });

  const wrapperRef = useRef<HTMLDivElement>(null);
  const receiveRef = useCallback((el: HTMLDivElement) => {
    wrapperRef.current = el;
    ref(el);
  }, []);

  const { project } = useStores();

  const [queryEnabled, setQueryEnabled] = useState(false);

  const { clip, isLoading: clipPending } = useClip(clipId);

  const {
    data: clipArrangementPackage,
    isPending: clipArrangementPackagePending,
  } = useClipArrangementPackageQuery(
    clipId,
    queryEnabled && (!preventStreamingPlayback || !!clip?.metadata.duration)
  );

  useEffect(() => {
    let timeoutId: NodeJS.Timeout | undefined;
    let isMounted = true;

    if (isIntersecting) {
      timeoutId = setTimeout(() => {
        if (!isMounted) return;
        setQueryEnabled(true);
      }, 300);
    } else {
      // Clear timeout if element becomes non-intersecting
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
    }

    return () => {
      isMounted = false;
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
    };
  }, [isIntersecting, project.apiClient]);

  const isPreviewing = useMemo(() => {
    return previewId === clipId;
  }, [previewId, clipId]);

  const wasPreviewing = useRef(false);
  useEffect(() => {
    if (
      !wasPreviewing.current &&
      isPreviewing &&
      !isIntersecting &&
      wrapperRef.current
    ) {
      wrapperRef.current.scrollIntoView({ behavior: 'smooth' });
    }
    wasPreviewing.current = isPreviewing;
  }, [isPreviewing, isIntersecting]);

  const loading = clipPending || clipArrangementPackagePending;

  return (
    <SongWrapper
      ref={receiveRef}
      isLoading={loading}
      isDragging={isDragging}
      short={short}
    >
      {isIntersecting && (
        <EditV3LibrarySong
          clipId={clipId}
          isPreviewing={isPreviewing}
          clip={clip}
          clipArrangementPackage={clipArrangementPackage ?? null}
          onDragStart={onDragStart}
          onDragEnd={onDragEnd}
          onLikeChange={onLikeChange}
        />
      )}
    </SongWrapper>
  );
});
