import { EditTiming } from '@/components/edit2025/types';
import { fetchClip } from '@/hooks/useClip';
import { AlignmentSpec } from '@/hooks/useGenerate';
import { Clip, ClipsStore } from '@/state/clipStore';

import DEFAULT_STATE from '../defaultState';
import getClipsInRange from '../getClipsInRange';
import getFullVolumeSingleTrackState from '../getFullVolumeSingleTrackState';
import {
  ExactBeatRange,
  ExactSecondsRange,
  StudioClip,
  StudioProjectState,
  StudioTrack,
} from '../types';
import { getAlignmentSpec } from '../useAlignedClips';
import getClipRelativeSecondsRange from './getClipRelativeSecondsRange';
import renderAuxiliaryClip from './renderAuxiliaryClip';
import { shiftExactSecondsRange } from './shiftTimeRange';

// Some of these task types cause bad extension/infill behavior.
// This helper lets us know we should render a project state to extend into instead.
export const isClipWithSpecialTimingBehavior = (clip: Clip) => {
  return [
    'extend',
    'upload_extend',
    'infill',
    'fixed_infill',
    'infill_intro',
    'infill_outro',
    'cover_infill',
    'cover_extend',
    'artist_infill',
    'artist_extend',
    'artist_cover_extend',
  ].includes(clip.metadata.task ?? '');
};

export default async function resolveTargetClipAndRelativeTimings(
  clipsStore: ClipsStore,
  payload: {
    title?: string;
    timing: EditTiming;
    tracks: StudioTrack[];
    projectId?: string;
    studioProjectId: string;
    originalLyrics: string;
    projectRelativeContext: {
      beats: ExactBeatRange;
      seconds: ExactSecondsRange;
    };
    projectRelativeSelection: {
      beats: ExactBeatRange;
      seconds: ExactSecondsRange;
    };
  },
  padContextEnd: boolean = false,
  alwaysRender: boolean = false,
  offsetDownbeatsSeconds?: number
): Promise<{
  clip: Clip;
  clipId: string;
  clipRelativeContextRangeSeconds: ExactSecondsRange;
  clipRelativeSelectionRangeSeconds: ExactSecondsRange;
  alignmentSpec: AlignmentSpec | undefined;
}> {
  const affectedNonEmptyTracks = payload.tracks
    .map((t) => ({
      ...t,
      clips: getClipsInRange(
        payload.projectRelativeContext.beats.startBeats,
        payload.projectRelativeContext.beats.endBeats,
        t.clips
      ),
    }))
    .filter((t) => t.clips.length > 0 || t.solo);

  const allAffectedUploadingAndFinalizedClips = affectedNonEmptyTracks.flatMap(
    (t) => t.clips
  );

  if (allAffectedUploadingAndFinalizedClips.length === 0) {
    throw new Error('No clips in context');
  }

  if (allAffectedUploadingAndFinalizedClips.find((c) => !c.clipId)) {
    throw new Error('Please wait for all clips to finish uploading');
  }

  const allAffectedClips =
    allAffectedUploadingAndFinalizedClips as (StudioClip & {
      clipId: string;
    })[];

  // Check if context window extends beyond song bounds
  const songStartBeats = Math.min(...allAffectedClips.map((c) => c.startBeats));
  const songEndBeats = Math.max(...allAffectedClips.map((c) => c.endBeats));
  const contextExtendsBeyondBounds =
    payload.projectRelativeContext.beats.startBeats < songStartBeats ||
    payload.projectRelativeContext.beats.endBeats > songEndBeats;

  if (
    allAffectedClips.length === 1 &&
    !contextExtendsBeyondBounds &&
    !alwaysRender
  ) {
    const clip = await fetchClip(
      clipsStore,
      allAffectedClips[0].clipId!,
      false
    );
    if (!isClipWithSpecialTimingBehavior(clip)) {
      return {
        clip,
        clipId: allAffectedClips[0].clipId!,
        clipRelativeContextRangeSeconds: getClipRelativeSecondsRange(
          allAffectedClips[0],
          payload.timing,
          payload.projectRelativeContext.beats
        ),
        clipRelativeSelectionRangeSeconds: getClipRelativeSecondsRange(
          allAffectedClips[0],
          payload.timing,
          payload.projectRelativeSelection.beats
        ),
        alignmentSpec: undefined,
      };
    }
  }
  const stateToRender: StudioProjectState =
    affectedNonEmptyTracks.length === 1
      ? getFullVolumeSingleTrackState(
          payload.timing,
          allAffectedClips,
          payload.studioProjectId
        )
      : {
          ...DEFAULT_STATE,
          timing: {
            type: 'manual',
            ...payload.timing,
          },
          tracks: affectedNonEmptyTracks,
        };
  const startBeats = payload.projectRelativeContext.beats.startBeats;
  const endBeats =
    payload.projectRelativeContext.beats.endBeats + (padContextEnd ? 32 : 0);

  const targetClip = await renderAuxiliaryClip({
    apiClient: clipsStore.apiClient,
    state: stateToRender,
    projectId: payload.projectId,
    fromStudioProjectId: payload.studioProjectId,
    startBeats,
    endBeats,
    lyrics: payload.originalLyrics,
    offsetDownbeatsSeconds,
  });

  await fetchClip(clipsStore, targetClip.id, true);

  const durationTrimmedFromStart =
    payload.projectRelativeContext.seconds.startSeconds;

  const alignmentSpec = getAlignmentSpec(
    payload.timing,
    affectedNonEmptyTracks.length === 1 ? allAffectedClips : [],
    payload.projectRelativeSelection.beats.startBeats,
    payload.projectRelativeSelection.beats.endBeats
  );

  return {
    clip: targetClip,
    clipId: targetClip.id,
    clipRelativeContextRangeSeconds: shiftExactSecondsRange(
      payload.projectRelativeContext.seconds,
      -durationTrimmedFromStart
    ),
    clipRelativeSelectionRangeSeconds: shiftExactSecondsRange(
      payload.projectRelativeSelection.seconds,
      -durationTrimmedFromStart
    ),
    alignmentSpec,
  };
}
