import { noop } from 'lodash-es';
import { observer } from 'mobx-react-lite';
import {
  createContext,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import storageAvailable from 'storage-available';

import { useStores } from '@/app/(root)/AppProviders';
import { invalidateWorkspaceQueries } from '@/components/clipBrowser/clipBrowserQueryClient';
import { ClipBrowserRegistryContext } from '@/components/clipBrowser/useClipBrowserRegistry';
import { toast } from '@/components/toast/Toast';
import { useBreakpointMd } from '@/hooks/useBreakpoint';
import { useContextSelector } from '@/hooks/useContextSelector';
import useUploadFile, { UploadFileOptions } from '@/hooks/useUploadFile';
import { useApiClient } from '@/lib/apiClient';
import { AudioUploadStatus } from '@/state/createV2Store';
import {
  AUDIO_UPLOAD_TOS_STORAGE_KEY,
  FREE_MAX_UPLOAD_SECONDS,
  PRO_MAX_UPLOAD_SECONDS,
  TRUE_MAX_UPLOAD_SECONDS,
} from '@/utils/constants';
import { isDev, isStaging } from '@/utils/environment';
import { eventLogger } from '@/utils/event-logger';
import { ActionName } from '@/utils/event-names';
import { RemixHandlerProps } from '@/utils/remixUtils';

import { AudioActions } from '../v2/types';

export enum UploaderUIStage {
  TermsOfService = 'TermsOfService',
  UploadOrRecord = 'UploadOrRecord',
  Trim = 'Trim',
  Finalize = 'Finalize',
}

export type UploadFileConfig = {
  clientSelectedFile: File | null;
  audioBuffer: AudioBuffer | null;
  title: string;
};

export type PendingClipContext = {
  clipId?: string;
  projectId?: string;
  title?: string;
};

export const useUploadState = ({
  onClose,
  autoSaveOnFinalize = false,
  isVoxPersonaUpload = false,
}: {
  onClose?: (initializationCondition: AudioActions | null) => void;
  skipFinalize?: boolean;
  autoSaveOnFinalize?: boolean;
  isVoxPersonaUpload?: boolean;
}) => {
  const { session, createV2, project, genForm, library, clips } = useStores();

  // Controls upload UI open state
  const [isUploadInProgress, setIsUploadInProgress] = useState(false);

  const [uploadFileConfig, setUploadFileConfig] =
    useState<UploadFileConfig | null>(null);
  const [pendingClipContext, setPendingClipContext] =
    useState<PendingClipContext | null>(null);
  const pendingClipContextRef = useRef<PendingClipContext | null>(null);

  const [initializationCondition, setInitializationCondition] =
    useState<AudioActions | null>(null);
  const initializationConditionRef = useRef<AudioActions | null>(null);
  const [onClipInitializedCallback, setOnClipInitializedCallback] = useState<
    | ((clipId: string, initializationCondition: AudioActions | null) => void)
    | null
  >(null);
  const [onModalClosedCallback, setOnModalClosedCallback] = useState<
    | ((
        pendingClipContext: PendingClipContext | null,
        initializationCondition: AudioActions | null
      ) => void)
    | null
  >(null);
  const [addClipConditionCallback, setAddClipConditionCallback] = useState<
    ((props: RemixHandlerProps) => void) | null
  >(null);
  const addClipConditionCallbackRef = useRef<
    ((props: RemixHandlerProps) => void) | null
  >(null);

  const [isRecordingMode, setIsRecordingMode] = useState(false);

  const [isConfirmingClose, setIsConfirmingClose] = useState(false);
  const [shouldConfirmClose /*setShouldConfirmClose*/] = useState(false);
  const needsUploadedOrRecordedBuffer = useMemo(
    () => !uploadFileConfig?.audioBuffer,
    [uploadFileConfig?.audioBuffer]
  );

  const [trimmedFile, setTrimmedFile] = useState<File | null>(null);
  const [trimRange, setTrimRange] = useState<[number, number]>([0, 0]);
  const [finishedUploadId, setFinishedUploadId] = useState<null | string>(null);
  const [submittedFinal, setSubmittedFinal] = useState(false);

  const [generatingImage, setGeneratingImage] = useState(true);
  const [imageUrl, setImageUrl] = useState('');

  const [hasVocalsDetected, setHasVocalsDetected] = useState(false);
  const [hasShownVocalsAlert, setHasShownVocalsAlert] = useState(false);

  const [uploadStatus, setUploadStatus] = useState<AudioUploadStatus | null>(
    null
  );
  const uploadStatusRef = useRef<AudioUploadStatus | null>(null);
  const [uploadError, setUploadError] = useState<string | null>(null);

  const lastTrimmedFile = useRef<File | null>(null);

  const uploadFile = useUploadFile();
  const apiClient = useApiClient();
  const isMobile = !useBreakpointMd();

  useEffect(() => {
    initializationConditionRef.current = initializationCondition;
    uploadStatusRef.current = uploadStatus;
    addClipConditionCallbackRef.current = addClipConditionCallback;
  }, [initializationCondition, uploadStatus, addClipConditionCallback]);

  const clipCreated = useContextSelector(
    ClipBrowserRegistryContext,
    (context) => context?.clipCreated || noop
  );

  const [minSeconds, maxSeconds] = useMemo(
    () => [
      session.audioUploadLimits?.min ?? 6,
      uploadFileConfig?.clientSelectedFile?.name?.startsWith('Mikey') ||
      uploadFileConfig?.clientSelectedFile?.name?.startsWith('SunoTimes') ||
      isDev ||
      isStaging
        ? TRUE_MAX_UPLOAD_SECONDS
        : (session.audioUploadLimits?.max ??
          (session.isStaff || session.flags?.['skip-paywall'] // allow 480 seconds for VIP users
            ? TRUE_MAX_UPLOAD_SECONDS
            : session.roles['pro']
              ? PRO_MAX_UPLOAD_SECONDS
              : FREE_MAX_UPLOAD_SECONDS)),
    ],
    [session.roles, uploadFileConfig?.clientSelectedFile?.name]
  );

  const [confirmedTOS, setConfirmedTOS] = useState<boolean>(() => {
    if (
      storageAvailable('localStorage') &&
      localStorage.getItem(AUDIO_UPLOAD_TOS_STORAGE_KEY)
    ) {
      return true;
    } else {
      return false;
    }
  });

  const handleOnFinish = useCallback(
    async (
      uploadId: string | null,
      title: string,
      imageUrl: string,
      uploadType: string | null
    ) => {
      if (uploadStatusRef.current === AudioUploadStatus.CANCELLED) {
        return;
      }
      const { data, error } = await apiClient.POST(
        '/api/uploads/audio/{upload_id}/initialize-clip/',
        {
          params: {
            path: {
              upload_id: uploadId || '',
            },
          },
          body: {},
        }
      );
      if (error || !data) {
        setUploadStatus(AudioUploadStatus.INVALID_UPLOAD);
        toast({
          title: 'Invalid upload. Please try again.',
          status: 'error',
          duration: null,
          isClosable: true,
        });
        setIsUploadInProgress(false);
        onModalClosedCallback?.(
          pendingClipContextRef.current,
          initializationConditionRef.current
        );
        resetState();
        return;
      }
      setUploadStatus(AudioUploadStatus.COMPLETE);
      const clipId = (data as any).clip_id;
      await library.loadClips();
      await clips.setMetadata({
        clipId,
        title,
        imageUrl,
        isAudioUploadTOSAccepted: true,
      });
      const clip = clips.clipById[clipId];
      // Upload completion status is handled by UploadStateContext
      setPendingClipContext({
        ...(pendingClipContextRef.current as PendingClipContext),
        clipId: clip.id,
      });
      onClipInitializedCallback?.(clipId, initializationConditionRef.current);
      if (pendingClipContextRef.current?.projectId && clip) {
        project?.addClip(clip);
        await project?.addClipsToProject(
          [clip],
          pendingClipContextRef.current?.projectId ?? ''
        );
      }
      // Need to re-fetch the clip because it wouldn't have had a "project" before
      const clipWithProject = await clips.loadClipById(clip.id);
      if (clipWithProject) {
        clips.updateClips([clipWithProject]);
        if (
          pendingClipContextRef.current?.projectId === project?.currentProjectId
        ) {
          clipCreated(clipWithProject);
        }
      } else {
        clipCreated(clip);
      }
      invalidateWorkspaceQueries(project.currentProjectId);
      setIsUploadInProgress(false);
      setUploadStatus(null);
      resetState();
      eventLogger.logAudioCreationEvent(
        isMobile,
        ActionName.uploadSong,
        genForm,
        session,
        {
          hasVocal: clip.metadata.has_vocal,
          newClipIds: [clipId],
          isUploaded: true,
          uploadType: uploadType,
        }
      );
    },
    [
      apiClient,
      toast,
      uploadStatus,
      setUploadStatus,
      setIsUploadInProgress,
      onModalClosedCallback,
      pendingClipContext,
      initializationCondition,
      onClipInitializedCallback,
      clipCreated,
      isMobile,
      genForm,
      session,
      library,
      clips,
      project,
    ]
  );

  const notifyStatus = (status: AudioUploadStatus, errorMessage?: string) => {
    setUploadStatus(status);
    if (errorMessage) {
      setUploadError(errorMessage);
    } else {
      setUploadError(null);
    }
  };

  useEffect(() => {
    if (trimmedFile && trimmedFile !== lastTrimmedFile.current) {
      lastTrimmedFile.current = trimmedFile;
      setGeneratingImage(true);
      notifyStatus(AudioUploadStatus.UPLOADING);
      setPendingClipContext({
        ...pendingClipContext,
        projectId: project.currentProjectId,
      });
      pendingClipContextRef.current = {
        ...pendingClipContext,
        projectId: project.currentProjectId,
      };

      const options: UploadFileOptions = {
        file: trimmedFile,
        type: isRecordingMode ? 'audio_recording' : 'file_upload',
      };

      if (autoSaveOnFinalize) {
        options.onGetClipId = (clipId: string) => {
          // Start tracking upload with real uploadId
          createV2.updateStudioUploadStatusForClip({
            clipId,
            fileName: trimmedFile.name,
            status: 'uploading',
            originalFile: trimmedFile,
          });
        };
        options.getUploadCancelled = (id: string) =>
          createV2.isStudioUploadCancelled(id);
      }

      uploadFile(options)
        .then(async (result) => {
          setUploadFileConfig((current) => ({
            ...(current as UploadFileConfig),
            title: current?.title || result.title,
          }));
          setImageUrl((current) => current || result.imageUrl);
          setFinishedUploadId(result.uploadId);

          if (result.errorMessage) {
            notifyStatus(AudioUploadStatus.INVALID_UPLOAD, result.errorMessage);
            // Mark upload as failed
            if (autoSaveOnFinalize && result.uploadId) {
              createV2.updateStudioUploadStatusForClip({
                clipId: result.uploadId,
                fileName: trimmedFile.name,
                status: 'failed',
                error: result.errorMessage,
                originalFile: trimmedFile,
              });
            }
            resetState();
            setIsUploadInProgress(false);
            setUploadStatus(null);
            onClose?.(initializationCondition);
          } else {
            if (uploadStatusRef.current === AudioUploadStatus.CANCELLED) {
              return;
            }

            // Handle vocals detection but still initialize the clip
            if (result.hasVocal) {
              notifyStatus(AudioUploadStatus.VOCALS_DETECTED);
              setHasVocalsDetected(true);
            }

            // Call onFinish for all successful uploads (with or without vocals)
            try {
              setSubmittedFinal(true);
              notifyStatus(AudioUploadStatus.INITIALIZING);
              await handleOnFinish?.(
                result.uploadId,
                uploadFileConfig?.title || result.title,
                imageUrl || result.imageUrl,
                isRecordingMode ? 'audio_recording' : 'file_upload'
              );

              // Mark upload as completed
              if (autoSaveOnFinalize) {
                createV2.updateStudioUploadStatusForClip({
                  clipId: result.uploadId,
                  fileName: trimmedFile.name,
                  status: 'completed',
                });
              }
            } catch (finishError) {
              // Mark upload as failed
              if (autoSaveOnFinalize) {
                createV2.updateStudioUploadStatusForClip({
                  clipId: result.uploadId,
                  fileName: trimmedFile.name,
                  status: 'failed',
                  error: `Initialization failed: ${finishError}`,
                  originalFile: trimmedFile,
                });
              }
            }
          }
        })
        .catch((error) => {
          console.log(`Error occurred: ${error}`);
          setIsUploadInProgress(false);
          setUploadStatus(null);
          resetState();
          // Note: We can't mark specific upload as failed here since we don't have uploadId
          // The onGetClipId callback handles the early failure case
        });
    }
  }, [
    trimmedFile,
    uploadFileConfig,
    uploadFile,
    isRecordingMode,
    project.currentProjectId,
    handleOnFinish,
    imageUrl,
    onClose,
    initializationCondition,
    autoSaveOnFinalize,
  ]);

  const resetState = useCallback(() => {
    setIsRecordingMode(false);
    setUploadFileConfig(null);
    setTrimmedFile(null);
    setFinishedUploadId(null);
    setSubmittedFinal(false);
    setImageUrl('');
    setGeneratingImage(true);
    setTrimRange([0, 0]);
    setPendingClipContext(null);
  }, []);

  useEffect(() => {
    if (imageUrl) setGeneratingImage(false);
  }, [imageUrl]);

  const uiStage = useMemo(() => {
    const result = (() => {
      if (!confirmedTOS) {
        return UploaderUIStage.TermsOfService;
      } else if (needsUploadedOrRecordedBuffer) {
        return UploaderUIStage.UploadOrRecord;
      } else if (!trimmedFile) {
        return UploaderUIStage.Trim;
      } else {
        return UploaderUIStage.Finalize;
      }
    })();

    return result;
  }, [
    confirmedTOS,
    needsUploadedOrRecordedBuffer,
    trimmedFile,
    autoSaveOnFinalize,
  ]) as UploaderUIStage;

  const closeAndReset = useCallback(() => {
    setIsUploadInProgress(false);
    onClose?.(initializationCondition);
    resetState();
  }, []);

  const cancelClose = useCallback(() => {
    setIsConfirmingClose(false);
  }, []);

  const attemptClose = useCallback(() => {
    if (isConfirmingClose) {
      closeAndReset();
      setIsConfirmingClose(false);
    } else if (shouldConfirmClose) {
      setIsConfirmingClose(true);
    } else {
      closeAndReset();
    }
  }, [isConfirmingClose, closeAndReset, shouldConfirmClose]);

  const handleGoBack = useCallback(() => {
    if (uiStage === UploaderUIStage.UploadOrRecord) {
      attemptClose();
    } else if (uiStage === UploaderUIStage.TermsOfService) {
      attemptClose();
    } else if (uiStage === UploaderUIStage.Trim) {
      setIsRecordingMode(false);
      setUploadFileConfig({
        ...(uploadFileConfig as UploadFileConfig),
        audioBuffer: null,
      });
    } else {
      console.error('Invalid UI stage');
    }
  }, [uiStage, attemptClose]);

  useEffect(() => {
    if (!uploadFileConfig?.audioBuffer) return;
    const bufferDuration = uploadFileConfig?.audioBuffer.duration;
    setTrimRange([0, Math.min(bufferDuration, maxSeconds)]);
  }, [uploadFileConfig?.audioBuffer, maxSeconds]);

  return useMemo(
    () => ({
      setConfirmedTOS,
      setIsRecordingMode,
      setTrimmedFile,
      setUploadFileConfig,

      uploadFileConfig,
      isUploadInProgress,
      setIsUploadInProgress,
      isRecordingMode,

      finishedUploadId,
      setFinishedUploadId,

      uploadStatus,
      setUploadStatus,

      uploadError,
      setUploadError,

      submittedFinal,

      uiStage,
      handleGoBack,

      minSeconds,
      maxSeconds,

      imageUrl,
      setImageUrl,

      generatingImage,
      setGeneratingImage,

      trimRange,
      setTrimRange,
      trimmedFile,

      isConfirmingClose,
      cancelClose,
      attemptClose,

      hasVocalsDetected,
      setHasVocalsDetected,
      hasShownVocalsAlert,
      setHasShownVocalsAlert,

      initializationCondition,
      setInitializationCondition,

      setOnClipInitializedCallback,
      onClipInitializedCallback,
      setOnModalClosedCallback,
      onModalClosedCallback,
      setAddClipConditionCallback,
      addClipConditionCallback,

      pendingClipContext,
      setPendingClipContext,

      onClose,
      isVoxPersonaUpload,
    }),
    [
      uiStage,
      uploadFileConfig,
      isRecordingMode,
      isUploadInProgress,
      handleGoBack,
      trimRange,
      trimmedFile,
      submittedFinal,
      imageUrl,
      generatingImage,
      minSeconds,
      maxSeconds,
      finishedUploadId,
      isConfirmingClose,
      uploadStatus,
      setUploadStatus,
      cancelClose,
      attemptClose,
      hasVocalsDetected,
      hasShownVocalsAlert,
      initializationCondition,
      setInitializationCondition,
      setOnModalClosedCallback,
      setOnClipInitializedCallback,
      onClipInitializedCallback,
      setAddClipConditionCallback,
      addClipConditionCallback,
      addClipConditionCallbackRef,
      pendingClipContext,
      setPendingClipContext,
      onClose,
      isVoxPersonaUpload,
    ]
  );
};

const UploadStateContext = createContext<ReturnType<typeof useUploadState>>(
  undefined as never
);

export const UploadStateProvider: React.FC<
  React.PropsWithChildren<{
    autoSaveOnFinalize?: boolean;
    isVoxPersonaUpload?: boolean;
  }>
> = observer(
  ({ autoSaveOnFinalize = false, isVoxPersonaUpload = false, ...props }) => {
    const uploadState = useUploadState({
      autoSaveOnFinalize,
      isVoxPersonaUpload,
    });
    return (
      <UploadStateContext.Provider value={uploadState}>
        {props.children}
      </UploadStateContext.Provider>
    );
  }
);

export default UploadStateContext;
