import { noop } from 'lodash-es';
import { useCallback, useMemo } from 'react';

import { toast } from '@/components/toast/Toast';
import { useContext, useContextSelector } from '@/hooks/useContextSelector';
import useGenerate, { CustomPrompt, GenerateParams } from '@/hooks/useGenerate';
import { createTransactionLogger } from '@/logging/logWebUserEvent';

import { AlignedLyric } from '../edit2025/types';
import StudioContext, { StudioContextType } from './StudioContext';
import {
  prepareExtendFromPointRequest,
  prepareExtendSelectionRequest,
} from './apiWrappers/extendHelpers';
import {
  prepareFixedInfillSelectionRequest,
  prepareInfillSelectionRequest,
} from './apiWrappers/infillHelpers';
import getUpdatedRegenerateParams from './getUpdatedRegenerateParams';
import {
  getAllUsedClipIds,
  getSelectionDurationSeconds,
  getSelectionEndBeats,
  getSelectionStartBeats,
} from './selectors';
import sortStudioClips from './sortStudioClips';
import { ClipCreationIntent } from './types';
import { useAlignmentSpec } from './useAlignedClips';
import useAlignedLyricsByClipId from './useAlignedLyricsByClipId';
import {
  AlternateControllerContext,
  getSelectionHash,
} from './useAlternateController';

const prepareGeneratePayload = async (
  method: 'infill' | 'fixed_infill' | 'extend',
  studioContext: StudioContextType,
  alignedLyricsByClipId: Record<string, AlignedLyric[]>
): Promise<{ params: GenerateParams; isRegeneration: boolean }> => {
  const regenerateParams = studioContext.regenerateParams;

  if (regenerateParams) {
    return {
      params: await getUpdatedRegenerateParams(
        regenerateParams,
        studioContext,
        alignedLyricsByClipId
      ),
      isRegeneration: true,
    };
  }

  let params: GenerateParams;
  if (method === 'infill') {
    params = await prepareInfillSelectionRequest(studioContext);
  } else if (method === 'fixed_infill') {
    params = await prepareFixedInfillSelectionRequest(studioContext);
  } else if (method === 'extend') {
    params = await prepareExtendSelectionRequest(studioContext);
  } else {
    throw new Error(`Invalid method: ${method}`);
  }

  return { params, isRegeneration: false };
};

const generateAndArrange = async (
  generate: ReturnType<typeof useGenerate>,
  payload: GenerateParams,
  promoteClipCreationIntents: (
    updateFn: (
      clipCreationIntent: ClipCreationIntent
    ) => ClipCreationIntent | null
  ) => void
) => {
  console.log('prepared payload', payload);

  console.log('about to generate');
  const clips = await generate(createTransactionLogger(), payload);

  console.log('started generating clips', clips);

  try {
    promoteClipCreationIntents((c) => ({
      ...c,
      possibleClipIds: [
        ...c.possibleClipIds,
        ...clips.sort(sortStudioClips).map((c) => c.id),
      ],
      name: (payload.prompt as CustomPrompt).title || 'Clip',
      namePersists: false,
    }));
  } catch (error) {
    console.error(error);
    toast({
      title: 'Error generating clip',
      description: `If this error persists, please contact support.`,
      status: 'error',
      duration: 5000,
      isClosable: true,
    });
  }

  return clips;
};

// should stems be a separate callback returned from this thing?
// stems work pretty differently, given that they insert several clips at once on new tracks
export default function useGenerateAndInsertV2() {
  const studioContext = useContext(StudioContext);
  const nonPreviewSelection = useContextSelector(
    StudioContext,
    (context) => context.nonPreviewState.selection
  );
  const focusedTrackId = useContextSelector(
    StudioContext,
    (context) => context.state.selection.focusedTrackId
  );
  const selectionStartBeats = useContextSelector(StudioContext, (context) =>
    getSelectionStartBeats(context.state)
  );
  const selectionEndBeats = useContextSelector(StudioContext, (context) =>
    getSelectionEndBeats(context.state)
  );
  const selectionDurationSeconds = useContextSelector(
    StudioContext,
    (context) => getSelectionDurationSeconds(context.state)
  );
  const generate = useGenerate();
  const alignmentSpec = useAlignmentSpec();

  const allUsedClipIds = useContextSelector(StudioContext, (context) =>
    getAllUsedClipIds(context.state)
  );
  const lyricsCorrectionsByClipId = useContextSelector(
    StudioContext,
    (context) => context.state.lyricsCorrectionsByClipId
  );
  const alignedLyricsByClipId = useAlignedLyricsByClipId(
    allUsedClipIds,
    lyricsCorrectionsByClipId
  );
  const withQueuedGeneration = useContextSelector(
    StudioContext,
    (context) => context.withQueuedGeneration
  );
  const expectAlternates = useContextSelector(
    AlternateControllerContext,
    (context) => context?.expect || noop
  );

  const generateInPlace = useCallback(
    async (method: 'infill' | 'fixed_infill' | 'smart_infill' | 'extend') => {
      if (!focusedTrackId) {
        throw new Error('No focused track');
      }

      const startBeats = selectionStartBeats;
      const endBeats = selectionEndBeats;

      const durationSeconds = selectionDurationSeconds;

      if (method === 'smart_infill') {
        if (durationSeconds < 2) {
          method = 'fixed_infill';
        } else {
          method = 'infill';
        }
      }

      if (durationSeconds <= 0) {
        console.error('not generating - selection duration <= 0');
        return;
      }

      withQueuedGeneration({
        startBeats,
        endBeats,
        targetTrackId: focusedTrackId,
        fn: async (promoteClipCreationIntents) => {
          const { params: generateParams, isRegeneration } =
            await prepareGeneratePayload(
              method,
              studioContext,
              alignedLyricsByClipId
            );

          const clips = await generateAndArrange(
            generate,
            generateParams,
            (updateFn) => promoteClipCreationIntents(updateFn, isRegeneration)
          );

          clips.forEach((c) => {
            expectAlternates(getSelectionHash(nonPreviewSelection), c.id);
          });
        },
        newTrackName: undefined,
        splitToTakeLanes: true,
      });
    },
    [
      studioContext,
      alignmentSpec,
      nonPreviewSelection,
      selectionStartBeats,
      selectionEndBeats,
      selectionDurationSeconds,
      expectAlternates,
    ]
  );

  const generateExtension = useCallback(
    async (extendFromBeats: number) => {
      if (!focusedTrackId) {
        throw new Error('No focused track');
      }

      withQueuedGeneration({
        startBeats: extendFromBeats,
        endBeats: undefined,
        targetTrackId: focusedTrackId,
        fn: async (promoteClipCreationIntents) => {
          const generateParams = await prepareExtendFromPointRequest(
            studioContext,
            extendFromBeats
          );

          const clips = await generateAndArrange(
            generate,
            generateParams,
            promoteClipCreationIntents
          );

          clips.forEach((c) => {
            expectAlternates(getSelectionHash(nonPreviewSelection), c.id);
          });
        },
      });
    },
    [
      studioContext,
      alignmentSpec,
      focusedTrackId,
      nonPreviewSelection,
      expectAlternates,
    ]
  );

  return useMemo(
    () => ({
      generateInPlace,
      generateExtension,
    }),
    [generateInPlace, generateExtension]
  );
}
