import axios, { type AxiosRequestHeaders } from 'axios';
import { toByteArray } from 'base64-js';
import secondsToMilliseconds from 'date-fns/secondsToMilliseconds';
import {
  type FileAsset,
  type FileAssetType,
  type Attachment as HuAttachment,
  type UploadFile,
} from '@material-hu/types/attachments';

import api from 'src/config/api';
import giphyAPI from 'src/config/giphy';
import { generateFilename } from 'src/pages/dashboard/Conversations/utils';
import {
  type Attachment,
  FileTypes,
  type FormFile,
  type SignedUrlResponse,
} from 'src/types/attachments';
import { type AudioFile } from 'src/types/chats';
import { getFileType } from 'src/utils/attachments';
import { bytesToSize } from 'src/utils/bytes';
import {
  getArrayBuffer,
  getAttachment,
  getBlob,
  getBlobUrl,
  getContentType,
  getSimplifiedFileName,
  parseVTTFile,
} from 'src/utils/files';

export const fileToBase64 = (
  file: File,
): Promise<string | ArrayBuffer | null> =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
  });

export const getVideoMetadata = (
  file: File,
): Promise<{ width: number; height: number; durationInMs: number }> => {
  return new Promise((resolve, reject) => {
    try {
      const url = URL.createObjectURL(file);
      const video = document.createElement('video');

      video.preload = 'metadata';
      video.src = url;

      video.onloadedmetadata = () => {
        URL.revokeObjectURL(url);
        resolve({
          width: video.videoWidth,
          height: video.videoHeight,
          durationInMs: secondsToMilliseconds(video.duration),
        });
      };

      video.onerror = err => {
        reject(err);
      };
    } catch (err) {
      reject(err);
    }
  });
};

export const getDimensions = (
  file: File | Blob | null,
): Promise<{ width: number; height: number }> =>
  new Promise((resolve, reject) => {
    if (!file) {
      reject(new Error('File is null'));
      return;
    }

    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onerror = error => reject(error);
    reader.onload = theFile => {
      const image = new Image();
      image.src = theFile.target?.result as string;
      image.onload = () => {
        resolve({
          width: image.width,
          height: image.height,
        });
      };
    };
  });

export const getBinaryDataFromUri = async (uri: string) => {
  const base64 = uri.split(',')[1];
  return toByteArray(base64);
};

export const getGif = async (gifId: string) => {
  try {
    return await giphyAPI.gif(gifId);
  } catch (error) {
    return null;
  }
};

export const getTrendingGifs = (params: any) => giphyAPI.trending(params);

export const searchGifs = (searchTerm: string, params: any) =>
  giphyAPI.search(searchTerm, params);

export const uploadFileToS3 = async (
  signedUrl: string,
  payload: ArrayBuffer | Blob | Uint8Array,
  contentType: string,
  signal?: AbortSignal,
) => {
  const config = {
    headers: {
      'Content-Type': contentType,
      'Content-Encoding': 'base64',
    },
    signal,
    transformRequest: (data: any, headers: AxiosRequestHeaders) => {
      headers.delete('Authorization');
      return data;
    },
  };
  const response = await axios.put(signedUrl, payload, config);

  return response;
};

export const uploadVideoToS3 = async (
  signedUrl: string,
  contentType: string,
  signal?: AbortSignal,
  videoFile?: File,
) => {
  const config = {
    headers: {
      'Content-Type': contentType,
    },
    signal,
    transformRequest: (data: any, headers: AxiosRequestHeaders) => {
      headers.delete('Authorization');
      return data;
    },
  };

  const response = await axios.put(signedUrl, videoFile, config);

  return response;
};

export const getSignedUrl = (
  fileName: string,
  type: string,
  contentType: string,
) =>
  api.get<SignedUrlResponse>('/signed-url', {
    params: {
      fileName,
      type,
      contentType,
    },
  });

export const signUrl = async (
  name: string,
  type: string,
  mime: string,
): Promise<string> => {
  const response = await getSignedUrl(
    getSimplifiedFileName(name, type),
    type,
    mime,
  );

  return response?.data?.url;
};

export const uploadAudio = async (audio: AudioFile) => {
  const signedUrl = await signUrl(audio.name, FileTypes.AUDIO, audio.mime);
  const data = await getBinaryDataFromUri(audio.uri);
  await uploadFileToS3(signedUrl, data, audio.mime);

  return {
    url: signedUrl.split('?')[0],
    type: FileTypes.AUDIO,
    name: audio.name,
    size: audio.size,
    width: audio.width,
  };
};

export const uploadSignature = async (signature: string) => {
  const signedUrl = await signUrl('image.png', FileTypes.IMAGE, 'image/png');
  const binaryData = toByteArray(signature);

  await axios.put(signedUrl, binaryData, {
    headers: {
      'Content-Type': 'image/png',
      'Content-Encoding': 'base64',
    },
  });

  return signedUrl.split('?')[0];
};

export const uploadImage = async (
  image: FormFile,
  config?: {
    signal?: AbortSignal;
  },
): Promise<Attachment> => {
  if (!image.fileObject) {
    throw new Error('File is null');
  }

  if (!image.name) {
    throw new Error('Image name is null');
  }

  const arrayBuffer = await getArrayBuffer(image.fileObject);
  const signedUrl = await signUrl(image.name, FileTypes.IMAGE, image.mime);
  await uploadFileToS3(signedUrl, arrayBuffer, image.mime, config?.signal);

  return {
    url: signedUrl.split('?')[0],
    type: FileTypes.IMAGE,
    name: image.name,
    width: image?.width,
    height: image?.height,
    position: image?.position,
    size: image.fileObject && bytesToSize(image.fileObject.size),
  };
};

export const uploadFile = async (
  file: FormFile,
  config?: {
    signal?: AbortSignal;
  },
): Promise<Attachment> => {
  if (!file.fileObject) {
    throw new Error('File is null');
  }

  if (!file.name) {
    throw new Error('File name is null');
  }

  const arrayBuffer = await getArrayBuffer(file.fileObject);
  const signedUrl = await signUrl(file.name, FileTypes.FILE, file.mime);
  await uploadFileToS3(signedUrl, arrayBuffer, file.mime, config?.signal);

  return {
    url: signedUrl.split('?')[0],
    type: FileTypes.FILE,
    name: file.name,
    width: file?.width || null,
    height: file?.height || null,
    position: null,
    size: file.fileObject && bytesToSize(file.fileObject.size),
  };
};

export const uploadVideo = async (
  video: FormFile,
  config?: {
    signal?: AbortSignal;
  },
): Promise<Attachment> => {
  if (!video.fileObject) {
    throw new Error('File is null');
  }

  if (!video.name) {
    throw new Error('Video name is null');
  }

  const signedUrl = await signUrl(video.name, FileTypes.VIDEO, video.mime);
  await uploadVideoToS3(
    signedUrl,
    video.mime,
    config?.signal,
    video.fileObject,
  );

  return {
    url: signedUrl.split('?')[0],
    type: FileTypes.VIDEO,
    name: video.name,
    width: video.width || null,
    height: video.height || null,
    position: video?.position,
    size: video.fileObject && bytesToSize(video.fileObject.size),
  };
};

export const uploadAllFiles = (
  files: FormFile[],
  config?: { signal?: AbortSignal },
): Promise<Attachment[]> =>
  uploadFormFiles(files, file => uploadFile(file, config));

export const uploadAllImages = (
  images: FormFile[],
  config?: { signal?: AbortSignal },
): Promise<Attachment[]> =>
  uploadFormFiles(images, image => uploadImage(image, config));

export const uploadAllVideos = (
  videos: FormFile[],
  config?: { signal?: AbortSignal },
): Promise<Attachment[]> =>
  uploadFormFiles(videos, video => uploadVideo(video, config));

export const uploadAudioFile = async (
  audio: FormFile,
  config?: { signal?: AbortSignal },
): Promise<Attachment> => {
  if (!audio.fileObject) {
    throw new Error('File is null');
  }

  if (!audio.name) {
    throw new Error('Audio name is null');
  }

  const arrayBuffer = await getArrayBuffer(audio.fileObject);
  const signedUrl = await signUrl(audio.name, FileTypes.AUDIO, audio.mime);
  await uploadFileToS3(signedUrl, arrayBuffer, audio.mime, config?.signal);

  return {
    url: signedUrl.split('?')[0],
    type: FileTypes.AUDIO,
    name: audio.name,
    // KNOWN ISSUE: backend `Attachment` model has no dedicated `durationInMs`
    // field for audios, so we persist the duration in `width`. When reading it
    // back, consumers must go through `getAudioDurationMs` (src/utils/attachments.ts).
    // TODO: remove this workaround once backend exposes a proper duration field.
    width: audio.metadata?.durationInMs ?? null,
    height: null,
    position: null,
    size: audio.fileObject && bytesToSize(audio.fileObject.size),
  };
};

export const uploadAllAudios = (
  audioFiles: FormFile[],
  config?: { signal?: AbortSignal },
): Promise<Attachment[]> =>
  uploadFormFiles(audioFiles, audio => uploadAudioFile(audio, config));

export const uploadAllAttachments = (
  files: FormFile[],
  images: FormFile[],
  videos: FormFile[],
  audioFiles: FormFile[] = [],
  config?: { signal?: AbortSignal },
) =>
  Promise.all([
    uploadAllFiles(files, config),
    uploadAllImages(images, config),
    uploadAllVideos(videos, config),
    uploadAllAudios(audioFiles, config),
  ]);

const uploadFormFiles = async (
  files: FormFile[],
  upload: (file: FormFile) => Promise<Attachment>,
): Promise<Attachment[]> => {
  const toUpload = files.filter(file => file.toUpload);
  const toNotUpload = files.filter(file => !file.toUpload);

  const uploadedFiles = await Promise.all(toUpload.map(upload));
  const notUploadedFiles = toNotUpload.map(getAttachment).map((file: any) => ({
    ...file,
    name: file.name,
    url: file.url && file.url.split('?')[0],
  }));

  return [...notUploadedFiles, ...uploadedFiles];
};

export const fetchBlob = async (attachment: Attachment) => {
  if (!attachment?.url) {
    throw new Error('Attachment URL is null');
  }

  const blob = await getBlob(attachment?.url);
  const blobUrl = getBlobUrl(blob);

  return {
    blob,
    blobUrl,
  };
};

export const fetchCaptions = async (url: string) => {
  const response = await fetch(url);
  const text = await response.text();
  const parsedSubtitles = parseVTTFile(text);

  return parsedSubtitles;
};

export const uploadGenericFile = (file: File, type?: FileTypes) => {
  const fileObject = {
    fileObject: file,
    name: file.name,
    size: file.size,
    mime: file.type || getContentType(file.name),
    type: type || getFileType(file),
  };
  let uploadFunction = uploadFile;
  switch (fileObject.type) {
    case FileTypes.IMAGE:
      uploadFunction = uploadImage;
      break;
    case FileTypes.VIDEO:
      uploadFunction = uploadVideo;
      break;
    default:
      uploadFunction = uploadFile;
      break;
  }
  return uploadFunction(fileObject);
};

export const uploadAttachment: UploadFile = async (
  file: File,
): Promise<HuAttachment> => {
  const attachment = await uploadGenericFile(file);
  if (!attachment.url) {
    throw new Error('[uploadAttachment] Upload failed: missing url');
  }
  return {
    url: attachment.url,
    name: attachment.name ?? file.name,
    type: attachment.type,
    size: attachment.size ?? '',
    bytes: attachment.bytes ?? file.size,
  };
};

export const uploadFiles = (
  files: FormFile[],
  signals?: Record<string, AbortController>,
): Promise<FileAsset[]> =>
  uploadFormFilesFileAssets(files, file =>
    uploadOneFile(
      {
        ...file,
        fileObject: file.fileObject as Blob,
        contentType: file.fileObject?.type || '',
      },
      signals,
    ),
  );

const uploadFormFilesFileAssets = async (
  files: FormFile[],
  upload: (file: FormFile) => Promise<FileAsset>,
): Promise<FileAsset[]> => {
  if (!files.length) return [];

  const results = await Promise.allSettled(files?.map(upload));

  return results
    .filter(
      (result): result is PromiseFulfilledResult<FileAsset> =>
        result.status === 'fulfilled',
    )
    .map(result => result.value);
};

export const uploadOneFile = async (
  file: Omit<FormFile, 'mime' | 'fileObject'> & {
    contentType: string;
    duration?: string;
    fileObject: Blob;
  },
  signals?: Record<string, AbortController>,
): Promise<
  FileAsset & {
    duration?: string;
    isLoading?: boolean;
    metadata?: { durationInMs?: number };
  }
> => {
  if (!file.size) {
    throw new Error('File size is null');
  }
  if (!file.fileObject) {
    throw new Error('File is null');
  }

  const filename = generateFilename(file.name);
  const abortController = filename ? signals?.[filename] : undefined;

  const fileAsset = await api.post<FileAsset>(
    '/file-assets',
    {
      name: file.name,
      type: file.type,
      contentType: file.contentType,
      size: bytesToSize(file.size),
      width: file.width,
      height: file.height,
    },
    {
      signal: abortController?.signal,
    },
  );
  const fileData = fileAsset?.data;

  const arrayBuffer = await getArrayBuffer(file.fileObject as File); // TODO: check getArrayBuffer. File extends Blob so it could be changed to Blob

  await uploadFileToS3(
    decodeURI(fileData?.uploadUrl!),
    arrayBuffer,
    file.contentType,
    abortController?.signal,
  );

  return {
    id: fileData?.id!,
    key: fileData?.key!,
    url: fileData?.url!,
    type: file.type as FileAssetType,
    name: fileData?.name!,
    width: fileData?.width,
    height: fileData?.height,
    size: fileData?.size!,
    contentType: fileData?.contentType!,
    duration: file?.duration,
    metadata: file?.metadata,
    isLoading: true,
    createdAt: fileData?.createdAt,
    updatedAt: fileData?.updatedAt,
    externalReference: fileData?.externalReference,
    thumbnailUrl: fileData?.thumbnailUrl,
  };
};
