import cleanState from '@suno/studiokit/projectState/cleanState';
import { getSecondsBetween } from '@suno/studiokit/timeMapping';
import { RefObject, useEffect, useRef } from 'react';

import { useStores } from '@/app/(root)/AppProviders';
import { toast } from '@/components/toast/Toast';
import {
  getLowestObservedDeltaFromServerTimeMs,
  getMsSinceServerReportedTime,
} from '@/lib/apiClient';
import { Clip, ClipsStore, isTimedOut } from '@/state/clipStore';

import { hasExtendTask } from './StudioContext';
import { combineActions } from './actions/combineActions';
import updateClipCreationIntent from './actions/updateClipCreationIntent';
import { updateTrackOrTakeLane } from './actions/updateTracksAndTakeLanes';
import createUnknownTimingStudioClipSync from './createUnknownTimingStudioClipSync';
import { fetchClipArrangementPackage } from './fetchClipArrangementPackage';
import { useClips } from './hooks/useClips';
import {
  ClipInsertionSpec,
  applyInsertionSpec,
  getClosestInsertionSpec,
  getTimelineInsertionSpecs,
} from './insertion';
import {
  getAllUsedClipIds,
  getDerivedTiming,
  getFallbackBPS,
  getFocusedClipCreationIntent,
  getFocusedStudioClip,
  getTracksAndTakeLanes,
  getTracksAndTakeLanesById,
  getTracksById,
} from './selectors';
import { ClipArrangementPackage, StudioProjectState } from './types';
import useStudioClipArrangementPackage from './useStudioClipArrangementPackage';

const erroredClipIds = new Set<string>();

const resolveClipCreationIntents = (
  clips: ClipsStore,
  clipsById: Record<string, Clip>,
  state: StudioProjectState,
  focusedClipArrangementPackage: ClipArrangementPackage | null,
  rewriteHistory: (
    transformState: (state: StudioProjectState) => StudioProjectState
  ) => void,
  setState: (
    state:
      | StudioProjectState
      | ((state: StudioProjectState) => StudioProjectState)
  ) => void,
  canMoveOtherClips: boolean,
  snapInsertsToGrid: boolean,
  preventStreaming: boolean,
  previewOrSetState: (
    id: string,
    previewState: (state: StudioProjectState) => StudioProjectState,
    forcePreview: boolean
  ) => void,
  attemptCountRef: RefObject<number>
) => {
  const tracks = getTracksAndTakeLanes(state);
  const tracksById = getTracksById(state);
  const isTakeLane = (track: { id: string }) => !tracksById[track.id];
  const clipCreationIntentTrackPairs = tracks.flatMap((track) =>
    track.clipCreationIntents.map(
      (ci) => [ci, track, isTakeLane(track)] as const
    )
  );

  attemptCountRef.current++;

  const resolutionAttempt = attemptCountRef.current;

  clipCreationIntentTrackPairs.forEach(
    ([clipCreationIntent, track, isTakeLane]) => {
      clipCreationIntent.possibleClipIds.forEach(async (possibleClipId) => {
        try {
          const clip = clipsById[possibleClipId];

          if (!clip) {
            return;
          }

          if (clip.status === 'error') {
            throw new Error(
              clip.metadata.error_message?.toString() ??
                'Clip generation failed, please try again'
            );
          }

          if (isTimedOut(clip)) {
            console.error(
              [
                `Clip timed out.`,
                `clip.status: ${clip.status}`,
                `now: ${new Date().toISOString()}`,
                `clip.created_at: ${clip.created_at}.`,
                `msSinceServerReportedTime: ${getMsSinceServerReportedTime(clip.created_at)}.`,
                `lowestObservedDeltaFromServerTimeMs: ${getLowestObservedDeltaFromServerTimeMs()}`,
              ].join('\n')
            );
            throw new Error('Clip generation timed out, please try again');
          }

          if (!['complete', 'streaming'].includes(clip.status ?? '')) {
            return;
          }

          if (
            preventStreaming &&
            clip.status !== 'complete' &&
            !clip.metadata.duration
          ) {
            return;
          }

          // if the attempt count has changed, let the later resolution attempt handle it.
          if (resolutionAttempt < attemptCountRef.current) {
            return;
          }

          let arrangementPackage = preventStreaming
            ? await fetchClipArrangementPackage(
                clips,
                possibleClipId,
                false,
                clipCreationIntent.startBeats !== undefined &&
                  clipCreationIntent.endBeats !== undefined
                  ? getSecondsBetween(
                      clipCreationIntent.startBeats,
                      clipCreationIntent.endBeats,
                      getDerivedTiming(state)
                    )
                  : undefined
              )
            : null;

          // if the attempt count has changed, let the later resolution attempt handle it.
          if (resolutionAttempt < attemptCountRef.current) {
            return;
          }

          const getUpdateFunction = (selectResult: boolean) => {
            return (state: StudioProjectState): StudioProjectState => {
              if (
                !getTracksAndTakeLanesById(state)[
                  track.id
                ]?.clipCreationIntents.find(
                  (ci) => ci.id === clipCreationIntent.id
                )
              ) {
                return state;
              }
              const removeClipCreationIntent = updateTrackOrTakeLane(
                track.id,
                (track) => ({
                  ...track,
                  clipCreationIntents: track.clipCreationIntents.filter(
                    (ci) => ci.id !== clipCreationIntent.id
                  ),
                })
              );

              let insertionSpec: ClipInsertionSpec | null;

              if (arrangementPackage) {
                const insertionSpecs = getTimelineInsertionSpecs(
                  arrangementPackage,
                  focusedClipArrangementPackage,
                  state,
                  !hasExtendTask(arrangementPackage.clip) && snapInsertsToGrid,
                  canMoveOtherClips
                );

                let centerBeats = 0;
                if (
                  clipCreationIntent.startBeats !== undefined &&
                  clipCreationIntent.endBeats !== undefined
                ) {
                  centerBeats =
                    (clipCreationIntent.startBeats +
                      clipCreationIntent.endBeats) /
                    2;
                } else if (clipCreationIntent.startBeats !== undefined) {
                  centerBeats = clipCreationIntent.startBeats;
                } else if (clipCreationIntent.endBeats !== undefined) {
                  centerBeats = clipCreationIntent.endBeats;
                }

                const closestInsertionSpec = getClosestInsertionSpec(
                  insertionSpecs,
                  track.id,
                  centerBeats
                );

                insertionSpec = closestInsertionSpec;

                if (!insertionSpec || !insertionSpec.studioClip) {
                  if (
                    clipCreationIntent.startBeats !== undefined &&
                    clipCreationIntent.endBeats !== undefined
                  ) {
                    insertionSpec = {
                      replacementStartBeats: clipCreationIntent.startBeats,
                      replacementEndBeats: clipCreationIntent.endBeats,
                      trackId: track.id,
                      studioClip: {
                        ...arrangementPackage.studioClip,
                        readStartBeats:
                          arrangementPackage.studioClip.readStartBeats +
                          clipCreationIntent.startTrimmedBeats,
                        startBeats: clipCreationIntent.startBeats,
                        endBeats: clipCreationIntent.endBeats,
                      },
                      clip: clip,
                    };
                  } else {
                    return removeClipCreationIntent(state);
                  }
                }
              } else {
                let studioClip = createUnknownTimingStudioClipSync(
                  clip,
                  getFallbackBPS(state)
                );
                if (clipCreationIntent.startBeats !== undefined) {
                  const movement =
                    clipCreationIntent.startBeats - studioClip.startBeats;
                  studioClip.startBeats += movement;
                  studioClip.endBeats += movement;
                }
                if (clipCreationIntent.endBeats !== undefined) {
                  studioClip.endBeats = clipCreationIntent.endBeats;
                }
                if (clipCreationIntent.startTrimmedBeats) {
                  studioClip.readStartBeats +=
                    clipCreationIntent.startTrimmedBeats;
                }

                insertionSpec = {
                  replacementStartBeats: studioClip.startBeats,
                  replacementEndBeats: studioClip.endBeats,
                  trackId: track.id,
                  studioClip: studioClip,
                  clip: clip,
                };
              }

              return combineActions(
                applyInsertionSpec(insertionSpec!),
                removeClipCreationIntent,
                selectResult
                  ? (intermediateState) => ({
                      ...intermediateState,
                      selection: {
                        ...intermediateState.selection,
                        anchorBeats: insertionSpec.studioClip!.startBeats,
                        focusBeats: insertionSpec.studioClip!.endBeats,
                        // override applyInsertionSpec's (usually good) effect of moving selection to another track
                        focusedTrackId: state.selection.focusedTrackId,
                        trackIds: state.selection.trackIds,
                      },
                    })
                  : (intermediateState) => ({
                      ...intermediateState,
                      selection: {
                        ...intermediateState.selection,
                        // override applyInsertionSpec's (usually good) effect of moving selection to another track
                        focusedTrackId: state.selection.focusedTrackId,
                        trackIds: state.selection.trackIds,
                      },
                    }),
                cleanState
              )(state);
            };
          };

          const focusedClipCreationIntent = getFocusedClipCreationIntent(state);

          if (isTakeLane) {
            rewriteHistory(getUpdateFunction(false));
          } else if (focusedClipCreationIntent?.id === clipCreationIntent.id) {
            // this update function will remove the CCI from the previewed state.
            // the following rewriteHistory will remove it from the true underlying state without adding anything new to the undo stack.
            previewOrSetState(clip.id, getUpdateFunction(true), false);
          } else {
            setState(getUpdateFunction(false));
          }

          // whether we immediately commit the change or not, this clip creation intent needs to be removed from all known states.
          rewriteHistory(
            updateClipCreationIntent(clipCreationIntent.id, () => null)
          );
        } catch (e) {
          console.error(
            'Error in clip creation intent resolver: ',
            (e as Error).message
          );
          if (!erroredClipIds.has(possibleClipId)) {
            toast({
              title: 'Error generating clip',
              description: (e as Error).message,
              status: 'error',
              duration: 5000,
              isClosable: true,
            });
            erroredClipIds.add(possibleClipId);
          }
          rewriteHistory(
            updateClipCreationIntent(
              clipCreationIntent.id,
              (clipCreationIntent) => ({
                ...clipCreationIntent,
                possibleClipIds: clipCreationIntent.possibleClipIds.filter(
                  (id) => id !== possibleClipId
                ),
              })
            )
          );
        }
      });
    }
  );
};

export default function useClipCreationIntentResolver(
  state: StudioProjectState,
  rewriteHistory: (
    transformState: (state: StudioProjectState) => StudioProjectState
  ) => void,
  setState: (
    state:
      | StudioProjectState
      | ((state: StudioProjectState) => StudioProjectState)
  ) => void,
  canMoveOtherClips: boolean,
  snapInsertsToGrid: boolean,
  preventStreaming: boolean,
  previewOrSetState: (
    id: string,
    previewState: (state: StudioProjectState) => StudioProjectState,
    forcePreview: boolean
  ) => void
) {
  const attemptCountRef = useRef(0);
  const { clips } = useStores();
  const { clips: clipsById } = useClips(getAllUsedClipIds(state));
  const focusedStudioClip = getFocusedStudioClip(state);
  const focusedClipArrangementPackage =
    useStudioClipArrangementPackage(focusedStudioClip);

  useEffect(() => {
    resolveClipCreationIntents(
      clips,
      clipsById,
      state,
      focusedClipArrangementPackage,
      rewriteHistory,
      setState,
      canMoveOtherClips,
      snapInsertsToGrid,
      preventStreaming,
      previewOrSetState,
      attemptCountRef
    );
  }, [
    clips,
    clipsById,
    state,
    focusedClipArrangementPackage,
    rewriteHistory,
    setState,
    canMoveOtherClips,
    snapInsertsToGrid,
    preventStreaming,
    previewOrSetState,
    attemptCountRef,
  ]);
}
