import {useMemo, useRef} from 'react';
import {useTranslation} from 'react-i18next';
import {Paths, File} from 'expo-file-system';
import HumandMediaHandlerModule from '@native-modules/expo-humand-media-handler/src/HumandMediaHandlerModule';
import {useSyncState} from '@hooks/useSyncState';
import {
  CHAT_ATTACHMENT_VIDEO_BITRATE,
  CHAT_MAX_ATTACHMENTS,
} from '@modules/chats/constants';
import {fileCompressionHandler} from '@modules/chats/instances/FileCompressionHandler';
import {
  MediaStatus,
  MediaEntity,
  MediaType,
  FileAsset,
} from '@modules/chats/interfaces';
import {
  generateAsset,
  uploadMedia,
  GenerateAssetParams,
} from '@modules/chats/services';
import {
  compressVideo,
  replaceFilenameSpaces,
  generateThumbnailUrl,
  generateThumbnailUrlBatch,
} from '@modules/chats/utils';
import {showSnackbar} from '@redux/dispatchers';
import {asyncMap, getUuid} from '@shared/utils';
import {useFeatureFlag} from '@stores/featureFlags';

import {
  SelectImageProps,
  SelectMediaResult,
  useSelectMedia,
} from './useSelectMedia';

const ERRORS = {
  FAILED_TO_UPLOAD_MEDIA: 'FAILED_TO_UPLOAD_MEDIA',
  MAX_SIZE_ERROR: 'MAX_SIZE_ERROR',
  MAX_ATTACHMENTS_ERROR: 'MAX_ATTACHMENTS_ERROR',
};

function computeTotalFilesBytes(files: MediaEntity[]) {
  return files.reduce((acc, file) => {
    // prevent NaN values
    return acc + (Number.isFinite(file.sizeInBytes) ? file.sizeInBytes : 0);
  }, 0);
}

/**
 * This hook is used to upload an image to the server and create an asset
 * It returns the asset and the temporary image
 * The temporary image is used to display the image in the UI
 * The asset is used to create the image
 * The idea is try to start the uploading process right away to win some time until the user press the confirm button
 * The images array will have a register of all images handled by the hook
 */
export const useUploadMedia = () => {
  const {t} = useTranslation();
  const [getFiles, setFiles] = useSyncState<MediaEntity[]>([]);
  const {selectFile, selectMedia} = useSelectMedia();
  const limitInMb = useFeatureFlag<number>('CHATS_V2_MAX_MEDIA_SIZE_MB') ?? 150;
  const limitInBytes = limitInMb * 1024 * 1024;
  const cancelledIds = useRef<Set<string>>(new Set()).current;
  const files = getFiles();
  const totalFilesBytes = useMemo(() => computeTotalFilesBytes(files), [files]);
  const isUploadingFiles = files.some(
    file => file.status === 'uploading' || file.status === 'creatingAsset',
  );

  const setStatusForMedia = (targetId: string, newStatus: MediaStatus) => {
    setFiles(prev =>
      prev.map(file =>
        file.id === targetId ? {...file, status: newStatus} : file,
      ),
    );
  };

  const areAllFilesUploaded = useMemo(() => {
    return files.every(file => file.status === 'uploaded');
  }, [files]);

  const generateMediaAssetAndUpload = async (
    media: MediaEntity & Partial<Pick<FileAsset, 'metadata' | 'thumbnailUrl'>>,
  ) => {
    try {
      if (!media.localUri) {
        throw new Error('Local URI is required');
      }

      let mediaToUpload: MediaEntity = {...media};

      if (media.status !== 'creatingAsset') {
        // Update media status to creatingAsset
        setStatusForMedia(media.id, 'creatingAsset');
      }

      if (media.type === MediaType.IMAGE) {
        const [entry] = await fileCompressionHandler.compressImagesWithCache([
          {source: media.localUri, name: media.name, hash: media.hash},
        ]);
        mediaToUpload.localUri = entry.uri;
        mediaToUpload.height = entry.height;
        mediaToUpload.width = entry.width;
        mediaToUpload.sizeInBytes = entry.size;
        mediaToUpload.contentType = 'image/jpeg';

        // Update state with compressed data so UI shows compressed image
        setFiles(prev =>
          prev.map(file =>
            file.id === media.id
              ? {
                  ...file,
                  contentType: mediaToUpload.contentType,
                  localUri: mediaToUpload.localUri,
                  height: mediaToUpload.height,
                  width: mediaToUpload.width,
                  sizeInBytes: mediaToUpload.sizeInBytes,
                }
              : file,
          ),
        );
      } else if (media.type === MediaType.VIDEO) {
        // Generate thumbnail if not already present (e.g. camera already provides one)
        let thumbnailUrl: string | null = null;
        if (!media.fileAsset?.thumbnailUrl) {
          thumbnailUrl = await generateThumbnailUrl(media.localUri);

          if (thumbnailUrl) {
            setFiles(prev =>
              prev.map(file =>
                file.id === media.id
                  ? {
                      ...file,
                      fileAsset: {thumbnailUrl} as FileAsset,
                    }
                  : file,
              ),
            );
          }
        } else {
          thumbnailUrl = media.fileAsset.thumbnailUrl;
        }

        if (media.hash) {
          const entry = await fileCompressionHandler.compressVideoWithCache(
            mediaToUpload,
            media.hash,
          );
          mediaToUpload = {
            ...mediaToUpload,
            contentType: 'video/mp4',
            localUri: entry.compressedUri,
            height: entry.height,
            width: entry.width,
            sizeInBytes: entry.sizeInBytes,
          };
        } else {
          mediaToUpload = {
            ...(await compressVideo(mediaToUpload)),
            contentType: 'video/mp4',
          };
        }

        // Update state with compressed video size
        setFiles(prev =>
          prev.map(file =>
            file.id === media.id
              ? {
                  ...file,
                  localUri: mediaToUpload.localUri,
                  height: mediaToUpload.height,
                  width: mediaToUpload.width,
                  sizeInBytes: mediaToUpload.sizeInBytes,
                }
              : file,
          ),
        );
      }

      await createAssetAndUpload(mediaToUpload, media.fileAsset?.thumbnailUrl);
    } catch (error) {
      // At this point the media was not uploaded, so update media status to failed
      setStatusForMedia(media.id, 'failed');
      throw new Error(ERRORS.FAILED_TO_UPLOAD_MEDIA);
    }
  };

  const createAssetAndUpload = async (
    media: MediaEntity,
    existingThumbnailUrl?: string | null,
  ) => {
    const body: GenerateAssetParams = {
      contentType: media.contentType,
      height: media.height,
      width: media.width,
      name: replaceFilenameSpaces(media.name),
      size: `${media.sizeInBytes} Bytes`,
      type: media.type,
      metadata: media.metadata || undefined,
    };

    const newAsset = await generateAsset(body);

    const thumbnailUrl =
      media.fileAsset?.thumbnailUrl ?? existingThumbnailUrl ?? null;

    setFiles(prev =>
      prev.map(file => ({
        ...file,
        fileAsset:
          file.id === media.id
            ? {
                ...newAsset,
                thumbnailUrl: newAsset.thumbnailUrl ?? thumbnailUrl,
              }
            : file.fileAsset,
      })),
    );

    setStatusForMedia(media.id, 'uploading');
    await uploadMedia(media.localUri, newAsset.uploadUrl, media.contentType);
    setStatusForMedia(media.id, 'uploaded');
  };

  /**
   * Background half of the upload pipeline: image compression, then video
   * compression + asset generation + upload per item. Runs detached from
   * uploadMediaItems so the caller (camera screen / chat footer) can move
   * on as soon as the entities are in state. Cancellation is honored at
   * every async boundary via cancelledIds.
   */
  const runBackgroundUpload = async (
    liveFiles: MediaEntity[],
    imagesToCompress: {source: string; name: string; hash?: string}[],
  ) => {
    // Compress images
    const compressedImages =
      imagesToCompress.length > 0
        ? await fileCompressionHandler.compressImagesWithCache(imagesToCompress)
        : [];

    // apply compressed image urls to live files
    for (const file of liveFiles) {
      if (file.type === MediaType.IMAGE) {
        const index = imagesToCompress.findIndex(
          item => item.source === file.localUri,
        );

        if (index > -1) {
          const compressed = compressedImages[index];

          if (compressed) {
            Object.assign(file, {
              localUri: compressed.uri,
              height: compressed.height,
              width: compressed.width,
              sizeInBytes: compressed.size,
              contentType: 'image/jpeg',
            });
          }
        }
      }
    }

    // Build nextFiles from compressed images and video thumbnails, then
    // push the compressed image data into state.
    const nextFilesMap = new Map(liveFiles.map(f => [f.id, f]));
    setFiles(prev => prev.map(f => nextFilesMap.get(f.id) ?? f));

    // Generate assets and upload; FileHandler handles video compression caching.
    asyncMap(liveFiles, async media => {
      // Bail if the user removed this entity mid-compression so we don't
      // waste video compression / network bandwidth on a discarded item.
      if (cancelledIds.has(media.id)) {
        return;
      }
      try {
        if (media.type === MediaType.VIDEO) {
          if (media.hash) {
            const compressed =
              await fileCompressionHandler.compressVideoWithCache(
                media,
                media.hash,
              );
            media = {
              ...media,
              contentType: 'video/mp4',
              localUri: compressed.compressedUri,
              height: compressed.height,
              width: compressed.width,
              sizeInBytes: compressed.sizeInBytes,
            };
          } else {
            media = {
              ...media,
              ...(await compressVideo(media)),
              contentType: 'video/mp4',
            };
          }

          if (cancelledIds.has(media.id)) {
            return;
          }

          // Update state with compressed video size
          setFiles(prev =>
            prev.map(file =>
              file.id === media.id
                ? {
                    ...file,
                    contentType: media.contentType,
                    localUri: media.localUri,
                    height: media.height,
                    width: media.width,
                    sizeInBytes: media.sizeInBytes,
                  }
                : file,
            ),
          );
        }

        if (cancelledIds.has(media.id)) {
          return;
        }

        await createAssetAndUpload(media, media.fileAsset?.thumbnailUrl);
      } catch {
        if (!cancelledIds.has(media.id)) {
          setStatusForMedia(media.id, 'failed');
        }
      }
    });
  };

  const uploadFile = async (): Promise<SelectMediaResult> => {
    try {
      const result = await selectFile();

      if (!result.ok || !result.data) {
        return result;
      }

      const totalBytes = result.data.reduce((acc, file) => {
        return acc + file.sizeInBytes;
      }, computeTotalFilesBytes(getFiles()));

      if (totalBytes > limitInBytes) {
        throw new Error(ERRORS.MAX_SIZE_ERROR);
      }

      if (getFiles().length + result.data!.length > CHAT_MAX_ATTACHMENTS) {
        throw new Error(ERRORS.MAX_ATTACHMENTS_ERROR);
      }

      setFiles(prev => [
        ...prev,
        ...result.data!.map(f => ({...f, status: 'creatingAsset' as const})),
      ]);

      // Files don't need compression, go straight to asset generation + upload
      asyncMap(result.data!, async file => {
        try {
          await createAssetAndUpload(file);
        } catch {
          setStatusForMedia(file.id, 'failed');
        }
      });

      return result;
    } catch (error) {
      if (
        typeof error === 'string' &&
        error.includes('canceled directory picker')
      ) {
        return {
          ok: false,
        };
      }
      const errorMessage =
        typeof error === 'object' && error !== null && 'message' in error
          ? (error.message as string)
          : undefined;

      if (errorMessage === ERRORS.MAX_SIZE_ERROR) {
        showSnackbar({
          title: t('chat.files.max_limit_files', {max: `${limitInMb} MB`}),
          variant: 'error',
        });
      } else if (errorMessage === ERRORS.MAX_ATTACHMENTS_ERROR) {
        showSnackbar({
          title: t('chat.files.limit_count_files', {
            count: CHAT_MAX_ATTACHMENTS,
          }),
          variant: 'error',
        });
      } else {
        showSnackbar({
          title: t('errors.api.500', {info: ''}),
          variant: 'error',
        });
      }

      return {ok: false};
    }
  };

  const uploadMediaItems = async (
    items: MediaEntity[],
    options?: {
      /**
       * When false, the size + attachment-count limits are not enforced
       * and the items are accepted regardless. Used by the camera capture
       * path: those items are gated upstream (the chat screen blocks
       * camera entry once the attachment count is at the cap, and the
       * camera screen caps recording duration so size can't blow up).
       * Rejecting a freshly captured photo/video here would discard work
       * the user already invested in. Default: true.
       */
      enforceLimits?: boolean;
    },
  ): Promise<{ok: boolean}> => {
    const enforceLimits = options?.enforceLimits ?? true;
    try {
      // Step 1: Hash all selected files for deduplication
      const hashes = await HumandMediaHandlerModule.hashFileBatch(
        items.map(file => file.localUri),
        256000, // 250KB threshold - smaller files fully hashed, larger files partially hashed
      );

      // Step 2: Deduplicate and compute sizes in a single pass
      const deduplicatedFiles: MediaEntity[] = [];
      const uncachedVideoUris: string[] = [];
      const videosToGenerateThumbnails: string[] = [];
      const imagesToCompress: {
        source: string;
        name: string;
        hash?: string;
      }[] = [];
      const seenHashes = new Set<string>();
      let knownBytes = 0;

      const existingFiles = getFiles();
      for (let i = 0; i < hashes.length; i++) {
        const {hash, error} = hashes[i];
        if (error || !hash) {
          continue;
        }
        if (existingFiles.some(f => f.hash === hash) || seenHashes.has(hash)) {
          continue;
        }
        seenHashes.add(hash);

        const item = {...items[i], hash};
        const cached = fileCompressionHandler.getCachedEntry(hash);

        if (item.type === MediaType.VIDEO) {
          // always regenerate thumbnails
          videosToGenerateThumbnails.push(item.localUri);
        }

        if (item.type === MediaType.IMAGE) {
          // Always add images to the compress list — compressImagesWithCache
          // handles cache hits internally and validates that the cached file
          // still exists on disk.
          imagesToCompress.push({
            source: item.localUri,
            name: item.name,
            hash: item.hash ?? '',
          });

          if (cached?.type === 'image') {
            knownBytes += cached.size;
            item.sizeInBytes = cached.size;
          } else {
            knownBytes += item.sizeInBytes;
          }
        } else if (item.type === MediaType.VIDEO) {
          if (cached?.type === 'video') {
            item.sizeInBytes = cached.sizeInBytes;
            knownBytes += item.sizeInBytes;
          } else {
            uncachedVideoUris.push(item.localUri);
          }
        }

        deduplicatedFiles.push(item);
      }

      // Predict compressed sizes for uncached videos
      const predictedVideoSizes =
        uncachedVideoUris.length > 0
          ? await HumandMediaHandlerModule.predictCompressedSizes(
              uncachedVideoUris,
              {bitrate: CHAT_ATTACHMENT_VIDEO_BITRATE, maxSize: 1280},
            )
          : [];
      const predictedVideosSize = predictedVideoSizes.reduce(
        (acc, item) => (item.error ? acc : acc + item.size),
        0,
      );

      // Step 3: Check limits against deduped data (skipped for camera
      // captures so the user never loses a freshly captured photo/video
      // — those are gated upstream by the chat screen + recording cap).
      if (enforceLimits) {
        const totalBytes =
          knownBytes + predictedVideosSize + computeTotalFilesBytes(getFiles());

        if (totalBytes > limitInBytes) {
          throw new Error(ERRORS.MAX_SIZE_ERROR);
        }

        if (
          getFiles().length + deduplicatedFiles.length >
          CHAT_MAX_ATTACHMENTS
        ) {
          throw new Error(ERRORS.MAX_ATTACHMENTS_ERROR);
        }
      }

      const videoThumbnails =
        videosToGenerateThumbnails.length > 0
          ? await generateThumbnailUrlBatch(videosToGenerateThumbnails)
          : [];

      // apply predicted sizes to deduplicatedFiles
      for (const file of deduplicatedFiles) {
        if (file.type === MediaType.VIDEO) {
          const predictedSize = predictedVideoSizes.find(
            item => item.path === file.localUri,
          );
          if (predictedSize) {
            file.sizeInBytes = predictedSize.size;
          }

          const thumbnailIndex = videosToGenerateThumbnails.findIndex(
            item => item === file.localUri,
          );

          if (thumbnailIndex > -1) {
            const thumbnail = videoThumbnails[thumbnailIndex];
            file.fileAsset = {thumbnailUrl: thumbnail.path} as FileAsset;
          }
        }
      }

      // display files and video thumbnails — drop anything the user already
      // cancelled while we were still hashing/predicting (e.g. tapped back
      // on the camera preview before the pipeline got here).
      const liveFiles = deduplicatedFiles.filter(f => !cancelledIds.has(f.id));
      if (liveFiles.length === 0) {
        return {ok: true};
      }
      setFiles(prev => [...prev, ...liveFiles]);

      // Validation + add are done. Resolve to the caller now so the
      // camera screen can show its preview (or chat footer can render the
      // attachment) without waiting on image compression and the asset
      // upload — those run in the background below.
      void runBackgroundUpload(liveFiles, imagesToCompress);

      return {ok: true};
    } catch (error) {
      const errorMessage =
        typeof error === 'object' && error !== null && 'message' in error
          ? (error.message as string)
          : undefined;

      if (errorMessage === ERRORS.MAX_SIZE_ERROR) {
        showSnackbar({
          title: t('chat.files.max_limit_files', {max: `${limitInMb} MB`}),
          variant: 'error',
        });
      } else if (errorMessage === ERRORS.MAX_ATTACHMENTS_ERROR) {
        showSnackbar({
          title: t('chat.files.limit_count_files', {
            count: CHAT_MAX_ATTACHMENTS,
          }),
          variant: 'error',
        });
      } else {
        showSnackbar({
          title: t('errors.api.500', {info: ''}),
          variant: 'error',
        });
      }

      return {ok: false};
    }
  };

  const uploadImageOrVideo = async ({
    multiple,
    withVideo,
  }: SelectImageProps): Promise<SelectMediaResult> => {
    try {
      const result = await selectMedia({
        multiple,
        withVideo,
        selectionLimit: CHAT_MAX_ATTACHMENTS - getFiles().length,
      });

      if (!result.ok || !result.data) {
        return result;
      }

      await uploadMediaItems(result.data);

      return result;
    } catch (error) {
      if (error?.toString().includes('cancelled image selection')) {
        return {ok: false};
      }
      showSnackbar({
        title: t('errors.api.500', {info: ''}),
        variant: 'error',
      });
      return {ok: false};
    }
  };

  const uploadImageBase64 = async (base64: string) => {
    try {
      const base64Data = base64.includes(',') ? base64.split(',')[1] : base64;

      const id = getUuid();
      const filename = `image_${id}.jpg`;

      const file = new File(Paths.cache, filename);
      file.write(base64Data, {
        encoding: 'base64',
      });

      const fileInfo = file.info();
      const size = fileInfo.size || 0;
      const localUri = file.uri;

      // Deduplication via hashing
      const [{hash, error: hashError}] =
        await HumandMediaHandlerModule.hashFileBatch([localUri], 256000);
      if (!hashError && hash && getFiles().some(f => f.hash === hash)) {
        return {ok: false}; // silently skip duplicate
      }

      const cached = hash
        ? fileCompressionHandler.getCachedEntry(hash)
        : undefined;
      const sizeForCheck = cached?.type === 'image' ? cached.size : size;
      const totalBytes = computeTotalFilesBytes(getFiles()) + sizeForCheck;
      if (totalBytes > limitInBytes) {
        throw new Error(ERRORS.MAX_SIZE_ERROR);
      }

      if (getFiles().length + 1 > CHAT_MAX_ATTACHMENTS) {
        throw new Error(ERRORS.MAX_ATTACHMENTS_ERROR);
      }

      const mediaEntity: MediaEntity = {
        id,
        type: MediaType.IMAGE,
        fileAsset: null,
        status: 'idle',
        contentType: 'image/jpeg',
        localUri,
        name: filename,
        sizeInBytes: size,
        hash: !hashError ? hash : undefined,
      };
      setFiles(prev => [...prev, mediaEntity]);
      generateMediaAssetAndUpload(mediaEntity);

      return {ok: true, data: [mediaEntity]};
    } catch (error) {
      const errorMessage =
        typeof error === 'object' && error !== null && 'message' in error
          ? (error.message as string)
          : undefined;

      if (errorMessage === ERRORS.MAX_SIZE_ERROR) {
        showSnackbar({
          title: t('chat.files.max_limit_files', {max: `${limitInMb} MB`}),
          variant: 'error',
        });
      } else if (errorMessage === ERRORS.MAX_ATTACHMENTS_ERROR) {
        showSnackbar({
          title: t('chat.files.limit_count_files', {
            count: CHAT_MAX_ATTACHMENTS,
          }),
          variant: 'error',
        });
      } else {
        showSnackbar({
          title: t('errors.api.500', {info: ''}),
          variant: 'error',
        });
      }

      return {ok: false};
    }
  };

  const uploadImagePaste = async (uri: string, bytes: number) => {
    try {
      const id = getUuid();

      // Deduplication via hashing
      const [{hash, error: hashError}] =
        await HumandMediaHandlerModule.hashFileBatch([uri], 256000);
      if (!hashError && hash && getFiles().some(f => f.hash === hash)) {
        return {ok: false}; // silently skip duplicate
      }

      const cached = hash
        ? fileCompressionHandler.getCachedEntry(hash)
        : undefined;
      const sizeForCheck = cached?.type === 'image' ? cached.size : bytes;
      const totalBytes = computeTotalFilesBytes(getFiles()) + sizeForCheck;
      if (totalBytes > limitInBytes) {
        throw new Error(ERRORS.MAX_SIZE_ERROR);
      }

      if (getFiles().length + 1 > CHAT_MAX_ATTACHMENTS) {
        throw new Error(ERRORS.MAX_ATTACHMENTS_ERROR);
      }

      const mediaEntity: MediaEntity = {
        id,
        type: MediaType.IMAGE,
        fileAsset: null,
        status: 'idle',
        contentType: 'image/jpeg',
        localUri: uri,
        name: `image_${id}.jpg`,
        sizeInBytes: bytes,
        hash: !hashError ? hash : undefined,
      };
      setFiles(prev => [...prev, mediaEntity]);
      generateMediaAssetAndUpload(mediaEntity);

      return {ok: true, data: [mediaEntity]};
    } catch (error) {
      const errorMessage =
        typeof error === 'object' && error !== null && 'message' in error
          ? (error.message as string)
          : undefined;

      if (errorMessage === ERRORS.MAX_SIZE_ERROR) {
        showSnackbar({
          title: t('chats2.messages.max_size_error'),
          variant: 'error',
        });
      } else if (errorMessage === ERRORS.MAX_ATTACHMENTS_ERROR) {
        showSnackbar({
          title: t('chats2.messages.max_attachments_error', {
            count: CHAT_MAX_ATTACHMENTS,
          }),
          variant: 'error',
        });
      } else {
        showSnackbar({
          title: t('errors.api.500', {info: ''}),
          variant: 'error',
        });
      }

      return {ok: false};
    }
  };

  const removeMedia = (targetId: string) => {
    cancelledIds.add(targetId);
    setFiles(prev => prev.filter(file => file.id !== targetId));
  };

  const clearMedia = () => {
    // Don't clear cancelledIds — that would erase markers for items
    // already removed via removeMedia whose background tasks may be
    // mid-compression and would otherwise resume past their next
    // cancelledIds.has() checkpoint, producing orphaned server-side
    // assets. Defensively add current file IDs too in case anything
    // is still in flight for them.
    for (const file of getFiles()) {
      cancelledIds.add(file.id);
    }
    setFiles([]);
  };

  const retryMedia = async (targetId: string) => {
    try {
      const media = getFiles().find(file => file.id === targetId);
      if (media) {
        await generateMediaAssetAndUpload(media);
      }
    } catch (error) {
      showSnackbar({
        title: t('errors.api.500', {info: ''}),
        variant: 'error',
      });
    }
  };

  const uploadCameraMedia = async ({
    localUri,
    mediaType,
    width,
    height,
    duration,
  }: {
    localUri: string;
    mediaType: 'photo' | 'video';
    width: number;
    height: number;
    duration?: number;
  }) => {
    try {
      const id = getUuid();
      const isVideo = mediaType === 'video';
      const extension = isVideo ? 'mp4' : 'jpg';
      const filename = `camera_${id}.${extension}`;

      const file = new File(localUri);
      const fileInfo = file.info();
      const size = fileInfo.size || 0;

      // Even though camera media should be unique, we leave it here for
      // future use of hashes in the app for better caching
      const [{hash, error: hashError}] =
        await HumandMediaHandlerModule.hashFileBatch([localUri], 256000);
      if (!hashError && hash && getFiles().some(f => f.hash === hash)) {
        return; // silently skip duplicate
      }

      // For video use predicted compressed size; fall back to raw size on error
      let sizeForCheck = size;
      if (isVideo) {
        try {
          const [predicted] =
            await HumandMediaHandlerModule.predictCompressedSizes([localUri], {
              bitrate: CHAT_ATTACHMENT_VIDEO_BITRATE,
              maxSize: 1280,
            });
          if (!predicted.error && predicted.size > 0) {
            sizeForCheck = predicted.size;
          }
        } catch {
          // fall back to raw size
        }
      }

      const totalBytes = computeTotalFilesBytes(getFiles()) + sizeForCheck;
      if (totalBytes > limitInBytes) {
        throw new Error(ERRORS.MAX_SIZE_ERROR);
      }

      if (getFiles().length + 1 > CHAT_MAX_ATTACHMENTS) {
        throw new Error(ERRORS.MAX_ATTACHMENTS_ERROR);
      }

      let thumbnailUrl: string | null = null;
      if (isVideo) {
        try {
          thumbnailUrl = await generateThumbnailUrl(localUri);
        } catch (error) {
          thumbnailUrl = null;
        }
      }

      const mediaEntity: MediaEntity = {
        id,
        type: isVideo ? MediaType.VIDEO : MediaType.IMAGE,
        fileAsset:
          isVideo && thumbnailUrl ? ({thumbnailUrl} as FileAsset) : null,
        status: 'idle',
        contentType: isVideo ? 'video/mp4' : 'image/jpeg',
        localUri,
        height,
        width,
        sizeInBytes: isVideo ? sizeForCheck : size,
        name: filename,
        metadata:
          isVideo && duration ? {durationInMs: duration * 1000} : undefined,
        hash: !hashError ? hash : undefined,
      };

      setFiles(prev => [...prev, mediaEntity]);
      await generateMediaAssetAndUpload(mediaEntity);
    } catch (error) {
      if (error instanceof Error) {
        if (error.message === ERRORS.MAX_SIZE_ERROR) {
          showSnackbar({
            title: t('chats.max_size_error', {maxSize: `${limitInMb}MB`}),
            variant: 'error',
          });
        } else if (error.message === ERRORS.MAX_ATTACHMENTS_ERROR) {
          showSnackbar({
            title: t('chats.max_attachments_error', {
              count: CHAT_MAX_ATTACHMENTS,
            }),
            variant: 'error',
          });
        } else {
          showSnackbar({
            title: t('errors.api.500', {info: ''}),
            variant: 'error',
          });
        }
      }
    }
  };

  return {
    uploadImageOrVideo,
    uploadMediaItems,
    uploadFile,
    files,
    removeMedia,
    clearMedia,
    isUploadingFiles,
    uploadImageBase64,
    uploadImagePaste,
    uploadCameraMedia,
    retryMedia,
    totalFilesBytes,
    areAllFilesUploaded,
  };
};
