'use client';

import { useMutation, useQuery } from '@tanstack/react-query';
import S3 from '@uppy/aws-s3';
import Uppy, { FileProgress, UppyFile } from '@uppy/core';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import {
  MAX_VIDEO_DURATION_SECONDS,
  MAX_VIDEO_UPLOAD_FILESIZE_MB,
} from '@/app/(root)/hooks/create/useHookForm';
import { toast } from '@/components/toast/Toast';
import { ApiClient, useApiClient } from '@/lib/apiClient';
import { components } from '@/lib/gen';
import logWebUserEvent from '@/logging/logWebUserEvent';

type VideoUploadType = components['schemas']['VideoUploadType'];
type UploadRequestStatusSchema =
  components['schemas']['UploadRequestStatusSchema'];

const DEFAULT_UPLOAD_TIMEOUT = 10 * 60 * 1000; // 10 minutes in milliseconds
interface UseVideoUploadOptions {
  isVideoCover?: boolean;
  requireVideoSprite?: boolean;
  requireModeration?: boolean;
  videoUploadType: VideoUploadType;
  maxFileSizeMb?: number;
  maxFileSize?: number;
  minDuration?: number;
  maxDuration?: number;
  pollingTimeout?: number;
  onVideoSelected?: (file: File | null) => void;
  uploadTimeout?: number;
  clipId?: string;
}

const FILE_UPLOAD_TYPE = 'file_upload';
const VIDEO_PROCESSING_STATUS_TIMEOUT = 3 * 60 * 1000; // 3 minutes in milliseconds

export const videoUploadKeys = {
  all: [{ scope: 'videoUpload' }] as const,
  uploadStatus: ({ uploadId }: { uploadId: string }) =>
    [{ ...videoUploadKeys.all[0], entity: 'uploadStatus', uploadId }] as const,
};

type UploadStatus =
  | 'uploading'
  | 'uploading-complete'
  | 'processing'
  | 'complete'
  | 'error';

/**
 * Creates a mutation to start video processing after S3 upload completes.
 * Calls the upload-finish endpoint with processing options and initiates polling.
 *
 * @param options - Configuration for video processing
 * @param options.isVideoCover - Whether this video is a cover/thumbnail
 * @param options.requireVideoSprite - Whether to generate video sprite during processing
 * @param options.requireModeration - Whether to fail processing on moderation issues
 * @param options.videoUploadType - Type of video upload for processing pipeline
 * @param options.setUploadStatus - Function to set the upload status
 * @param options.pollingStartTime - Ref object to track polling start time
 * @returns TanStack Query mutation for starting video processing
 */
const useStartProcessingVideoMutation = ({
  isVideoCover,
  requireVideoSprite,
  requireModeration,
  videoUploadType,
  setUploadStatus,
  pollingStartTime,
  clipId,
}: {
  isVideoCover: boolean;
  requireVideoSprite: boolean;
  requireModeration: boolean;
  videoUploadType: VideoUploadType;
  setUploadStatus: (status: UploadStatus) => void;
  pollingStartTime: React.RefObject<number | null>;
  clipId?: string;
}) => {
  const apiClient: ApiClient = useApiClient();

  const onSuccess = useCallback(() => {
    setUploadStatus('processing');
    pollingStartTime.current = Date.now();
  }, [setUploadStatus, pollingStartTime]);

  const onError = useCallback(
    (error: Error) => {
      setUploadStatus('error');
      toast({
        title: 'Failed to process upload',
        description:
          error instanceof Error ? error.message : 'Please try again.',
        status: 'error',
        duration: 4000,
        isClosable: true,
      });
    },
    [setUploadStatus]
  );

  const startProcessingVideoMutation = useMutation({
    mutationFn: async ({
      uploadId,
      uploadedFilename,
    }: {
      uploadId: string;
      uploadedFilename: string;
    }) => {
      const { error } = await apiClient.POST(
        '/api/uploads/video/{upload_id}/upload-finish/',
        {
          params: { path: { upload_id: uploadId } },
          body: {
            upload_type: FILE_UPLOAD_TYPE,
            upload_filename: uploadedFilename || 'unknown',
            is_video_cover: isVideoCover,
            require_video_sprite: requireVideoSprite,
            fail_task_on_moderation: requireModeration,
            video_upload_type: videoUploadType,
            clip_id: clipId,
          },
        }
      );

      if (error) {
        throw error;
      }
    },
    onSuccess: onSuccess,
    onError: onError,
  });

  return startProcessingVideoMutation;
};

interface PollingConfig {
  pollingStartTime: React.RefObject<number | null>;
  pollingTimeout: number;
}

/**
 * Polls the video processing status endpoint until completion or timeout.
 * Only active when upload status is 'processing' and within polling interval.
 *
 * @param uploadId - The upload ID to poll status for
 * @param uploadStatus - Current upload status to determine if polling should be active
 * @param pollingConfig - Polling configuration with start time and interval
 * @param onSuccess - Callback when processing completes successfully
 * @param onError - Callback when processing fails
 * @param onPollingTimedOut - Callback when polling exceeds timeout interval
 * @returns TanStack Query response for upload status
 */
const useQueryVideoProcessingStatus = ({
  uploadId,
  uploadStatus,
  pollingConfig,
  onSuccess,
  onError,
  onPollingTimedOut,
}: {
  uploadId: string | null;
  uploadStatus: UploadStatus | null;
  pollingConfig: PollingConfig;
  onSuccess: (data: UploadRequestStatusSchema) => void;
  onError: ({ errorMessage }: { errorMessage: string }) => void;
  onPollingTimedOut: () => void;
}) => {
  const apiClient: ApiClient = useApiClient();
  const { pollingStartTime, pollingTimeout } = pollingConfig;

  const hasPollingTimedOut = useCallback(() => {
    if (!pollingStartTime.current) return false;
    return Date.now() - pollingStartTime.current > pollingTimeout;
  }, [pollingTimeout]);

  const response = useQuery({
    queryKey: videoUploadKeys.uploadStatus({
      uploadId: uploadId || '',
    }),
    queryFn: async () => {
      if (!uploadId) return;
      if (hasPollingTimedOut()) {
        onPollingTimedOut();
        return;
      }
      const { data } = await apiClient.GET('/api/uploads/video/{upload_id}/', {
        params: { path: { upload_id: uploadId } },
      });

      if (data?.status === 'error') {
        onError({ errorMessage: data?.error_message || 'Unknown error' });
      }
      if (data?.status === 'complete') {
        onSuccess(data);
      }
      return data;
    },
    enabled:
      !!uploadId && uploadStatus === 'processing' && !hasPollingTimedOut(),
    refetchInterval:
      uploadStatus === 'processing' && !hasPollingTimedOut() ? 2000 : false, // Poll every 2 seconds while processing
    staleTime: 0, // Always fetch fresh data when polling
  });

  return response;
};

export function getVideoDuration(file: File): Promise<number> {
  return new Promise((resolve, reject) => {
    const video = document.createElement('video');
    const url = URL.createObjectURL(file);
    video.preload = 'metadata';
    video.onloadedmetadata = () => {
      URL.revokeObjectURL(url);
      resolve(video.duration);
    };
    video.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error('Failed to load video metadata'));
    };

    video.src = url;
  });
}

/**
 * Manages video upload to S3 and server-side processing workflow.
 * Handles file selection, S3 upload via Uppy, processing initiation, and status polling.
 *
 * @param options - Configuration options for video upload behavior
 * @param options.isVideoCover - Whether this video is a cover/thumbnail
 * @param options.requireVideoSprite - Whether to generate video sprite during processing
 * @param options.requireModeration - Whether to fail processing on moderation issues
 * @param options.videoUploadType - Type of video upload for processing pipeline
 * @param options.maxFileSizeMb - Maximum file size in MB (default: 100)
 * @param options.maxFileSize - Maximum file size in bytes (overrides maxFileSizeMb)
 * @param options.minDuration - Minimum duration in seconds
 * @param options.maxDuration - Maximum duration in seconds
 * @param options.pollingTimeout - Timeout for processing status polling (default: 3min)
 * @param options.onVideoSelected - Callback when video file is selected/deselected
 * @param options.uploadTimeout - Timeout for upload in milliseconds
 *
 * @returns Object containing upload state and control functions
 * @returns uploadId - Server-generated upload identifier
 * @returns uploadS3Id - S3 object ID after successful processing
 * @returns uploadImageUrl - Generated thumbnail/sprite URL after processing
 * @returns uploadStatus - Current upload/processing status
 * @returns uploadProgress - Upload progress percentage (0-100)
 * @returns updateSelectedVideo - Function to start upload with selected file
 * @returns removeFiles - Function to clear uploaded files from Uppy
 * @returns uploadTimeout - Timeout for upload in milliseconds
 */
export const useVideoUpload = (options: UseVideoUploadOptions) => {
  const apiClient: ApiClient = useApiClient();
  const [uploadId, setUploadId] = useState<string | null>(null);
  const [uploadedFilename, setUploadedFilename] = useState<string | null>(null);
  const [uploadStatus, setUploadStatus] = useState<UploadStatus | null>(null);
  const [uploadProgress, setUploadProgress] = useState<number>(0);
  const [uploadS3Id, setUploadS3Id] = useState<string | null>(null);
  const [uploadImageUrl, setUploadImageUrl] = useState<string | null>(null);
  const [audioAlignmentTimestamp, setAudioAlignmentTimestamp] = useState<
    number | null
  >(null);
  const pollingStartTime = useRef<number | null>(null);
  const uploadTimeoutId = useRef<NodeJS.Timeout | null>(null);

  const {
    isVideoCover = false,
    requireVideoSprite = false,
    requireModeration = true,
    videoUploadType,
    maxFileSizeMb = 100,
    maxFileSize = maxFileSizeMb * 1024 * 1024,
    minDuration,
    maxDuration,
    pollingTimeout = VIDEO_PROCESSING_STATUS_TIMEOUT,
    onVideoSelected,
    uploadTimeout = DEFAULT_UPLOAD_TIMEOUT,
    clipId,
  } = options;

  /**
   * Fetches S3 upload parameters from the server for a given file.
   * Called by Uppy before starting the S3 upload.
   *
   * @param file - Uppy file object containing file metadata
   * @returns S3 upload URL and form fields for direct upload
   */
  const getUploadParameters = useCallback(
    async (file: UppyFile) => {
      setUploadStatus('uploading');
      setUploadProgress(0);
      setUploadedFilename(null);

      const { data } = await apiClient.POST('/api/uploads/video/', {
        body: {
          extension: file.extension,
        },
      });

      if (!data) {
        throw new Error('Failed to fetch upload parameters');
      }

      setUploadId(data.id);

      return {
        url: data?.url,
        fields: data.fields as Record<string, never>,
      };
    },
    [apiClient, setUploadId, setUploadProgress, setUploadedFilename]
  );

  const uppy = useMemo(() => {
    return new Uppy({
      id: 'video-upload',
      restrictions: {
        maxNumberOfFiles: 1,
        maxFileSize,
      },
    }).use(S3, {
      getUploadParameters,
    });
  }, [maxFileSize, getUploadParameters]);

  const removeFiles = useCallback(() => {
    uppy.getFiles().forEach((file) => {
      uppy.removeFile(file.id);
    });
  }, [uppy]);

  // Function to handle upload timeout
  const handleUploadTimeout = useCallback(() => {
    // Cancel the upload
    uppy.cancelAll();

    toast({
      title: 'Upload timed out',
      description:
        'The upload took longer than 10 minutes. Please check your internet connection and try again.',
      status: 'error',
      duration: 6000,
      isClosable: true,
    });

    // Reset upload states
    setUploadStatus('error');
    setUploadProgress(0);
    removeFiles();
  }, [uppy, removeFiles]);

  // Function to clear upload timeout
  const clearUploadTimeout = useCallback(() => {
    if (uploadTimeoutId.current) {
      clearTimeout(uploadTimeoutId.current);
      uploadTimeoutId.current = null;
    }
  }, []);

  // Function to start upload timeout
  const startUploadTimeout = useCallback(() => {
    clearUploadTimeout(); // Clear any existing timeout
    uploadTimeoutId.current = setTimeout(handleUploadTimeout, uploadTimeout);
  }, [clearUploadTimeout, handleUploadTimeout, uploadTimeout]);

  // Handle upload to S3 in an effect using uppy
  useEffect(() => {
    const handleUploadProgress = (_: any, progress: FileProgress) => {
      const percentage = Math.round(
        (progress.bytesUploaded / progress.bytesTotal) * 100
      );

      setUploadProgress(percentage);
    };

    const handleUploadError = () => {
      clearUploadTimeout(); // Clear timeout on error

      // Log when video upload fails for hooks
      if (videoUploadType === 'video_hook') {
        logWebUserEvent({
          actionName: 'HookUploadVideoFailed',
          context: {
            uploadId: uploadId || undefined,
            clipId: clipId || undefined,
          },
        });
      }

      toast({
        title: 'Upload failed',
        description: 'Please try again.',
        status: 'error',
        duration: 4000,
        isClosable: true,
      });
      setUploadStatus('error');
      setUploadProgress(0);
      removeFiles();
    };

    const handleUploadSuccess = () => {
      clearUploadTimeout(); // Clear timeout on success
      const filename = uppy.getFiles()?.[0]?.name;
      setUploadedFilename(filename || null);
      setUploadStatus('uploading-complete');

      // Log when video upload succeeds for hooks
      if (videoUploadType === 'video_hook') {
        logWebUserEvent({
          actionName: 'HookUploadVideoSucceeded',
          context: {
            uploadId: uploadId || undefined,
            clipId: clipId || undefined,
          },
        });
      }
    };

    const handleComplete = () => {
      clearUploadTimeout(); // Clear timeout on complete
      removeFiles();
    };

    uppy.on('upload-progress', handleUploadProgress);
    uppy.on('upload-success', handleUploadSuccess);
    uppy.on('upload-error', handleUploadError);
    uppy.on('complete', handleComplete);

    return () => {
      uppy.off('upload-progress', handleUploadProgress);
      uppy.off('upload-success', handleUploadSuccess);
      uppy.off('upload-error', handleUploadError);
      uppy.off('complete', handleComplete);
      clearUploadTimeout();
    };
  }, [uppy, uploadTimeout, clearUploadTimeout, handleUploadTimeout]);

  // Start processing when upload is complete
  const startProcessingVideoMutation = useStartProcessingVideoMutation({
    isVideoCover,
    requireVideoSprite,
    requireModeration,
    videoUploadType,
    setUploadStatus,
    pollingStartTime,
    clipId,
  });
  useEffect(() => {
    if (uploadStatus === 'uploading-complete' && uploadId && uploadedFilename) {
      startProcessingVideoMutation.mutate({
        uploadId,
        uploadedFilename,
      });
    }
  }, [uploadStatus, uploadId, uploadedFilename]);

  // Poll for video processing status
  useQueryVideoProcessingStatus({
    uploadId,
    uploadStatus,
    pollingConfig: {
      pollingTimeout,
      pollingStartTime,
    },
    onSuccess: (data: UploadRequestStatusSchema) => {
      setUploadStatus('complete');
      setUploadS3Id(data?.s3_id || null);
      setUploadImageUrl(data?.image_url || null);
      setAudioAlignmentTimestamp(data?.audio_alignment_timestamp || null);
    },
    onError: ({ errorMessage }: { errorMessage: string }) => {
      setUploadStatus('error');
      toast({
        title: errorMessage,
        description: 'Please upload a different video file.',
        status: 'error',
        duration: 4000,
        isClosable: true,
      });
    },
    onPollingTimedOut: () => {
      setUploadStatus('error');
      toast({
        title: 'Video processing timed out.',
        description:
          'Please check your network connection or try a different video.',
        status: 'error',
        duration: 6000,
        isClosable: true,
      });
    },
  });

  /**
   * Starts the video upload process with the selected file.
   * Adds file to Uppy, initiates S3 upload, and handles errors.
   *
   * @param file - Video file to upload, or null to clear selection
   */
  const updateSelectedVideo = useCallback(
    async (file: File, options?: { onError?: () => void }) => {
      const { onError } = options || {};
      if (file) {
        if (minDuration != null || maxDuration != null) {
          const videoDuration = await getVideoDuration(file);
          if (minDuration != null && videoDuration < minDuration) {
            const duration =
              minDuration < MAX_VIDEO_DURATION_SECONDS
                ? `${minDuration}s`
                : `${Math.round(minDuration / 60)} minutes`;
            toast({
              title: 'Video is too short',
              description: `Video duration must be at least ${duration}.`,
              status: 'error',
              duration: 5000,
              isClosable: true,
              position: 'top',
            });
            onError?.();
            return;
          } else if (maxDuration != null && videoDuration > maxDuration) {
            const duration =
              maxDuration < MAX_VIDEO_DURATION_SECONDS
                ? `${maxDuration}s`
                : `${Math.round(maxDuration / 60)} minutes`;
            toast({
              title: 'Video is too long',
              description: `Video duration must be less than ${duration}.`,
              status: 'error',
              duration: 5000,
              isClosable: true,
              position: 'top',
            });
            onError?.();
            return;
          }
        }

        let fileId: string | null = null;
        try {
          fileId = uppy.addFile({
            name: file.name,
            type: file.type,
            data: file,
          });

          startUploadTimeout();

          // Log when video upload starts for hooks
          if (videoUploadType === 'video_hook') {
            logWebUserEvent({
              actionName: 'HookUploadVideoStarted',
              context: {
                uploadId: uploadId || undefined,
                clipId: clipId || undefined,
              },
            });
          }

          await uppy.upload();
        } catch (e) {
          clearUploadTimeout(); // Clear timeout on error
          let errorMessage =
            'Error trying to upload this video. Try another one?';
          if (e instanceof Error && e.message) {
            if (e.message.includes('exceeds maximum')) {
              errorMessage = `Sorry, but this file is too large. Please try a video that is under ${MAX_VIDEO_UPLOAD_FILESIZE_MB} MB.`;
            }
          }
          toast({
            title: errorMessage,
            status: 'error',
            duration: 5000,
            isClosable: true,
            position: 'top',
          });
          onVideoSelected?.(null);
          if (fileId) uppy.removeFile(fileId);
          onError?.();
          return;
        }
      } else {
        clearUploadTimeout(); // Clear timeout when removing files
        removeFiles();
      }
      onVideoSelected?.(file);
    },
    [uppy, onVideoSelected, removeFiles]
  );

  return useMemo(
    () => ({
      uploadId,
      uploadS3Id,
      uploadImageUrl,
      uploadStatus,
      uploadProgress,
      audioAlignmentTimestamp,
      updateSelectedVideo,
      removeFiles,
    }),
    [
      uploadId,
      uploadS3Id,
      uploadImageUrl,
      uploadStatus,
      uploadProgress,
      audioAlignmentTimestamp,
      updateSelectedVideo,
      removeFiles,
    ]
  );
};

export default useVideoUpload;
