import { RcFile, UploadFile } from "antd/lib/upload/interface";
import Axios from "axios";
import { Video } from "@komi-app/profiles-sdk";
import { awsService, photoService, videoService } from "services";
import { PresignedPostData } from "services/externals/AWSService";
import notification from "utils/notification";
import {
  getExtension,
  isDocumentFile,
  isImage,
  isSoundFile,
  isVideo,
} from "./file";
import { Area } from "react-easy-crop/types";
import config from "config";
import { FLAGS, isOn } from "@komi-app/flags-sdk";

export const getVideoDuration = (file: any): Promise<number> =>
  new Promise((resolve, reject) => {
    try {
      const video = document.createElement("video");
      video.preload = "metadata";
      video.onloadedmetadata = function () {
        window.URL.revokeObjectURL(video.src);
        resolve(Math.floor(video.duration));
      };
      video.onerror = function () {
        reject("Invalid video.");
      };
      video.src = window.URL.createObjectURL(file);
    } catch (e) {
      console.log("getVideoDuration error:", e);
      reject(e);
    }
  });

export const getVideoDurationWithUrl = (url: string): Promise<number> =>
  new Promise((resolve, reject) => {
    try {
      const video = document.createElement("video");
      video.preload = "metadata";
      video.onloadedmetadata = function () {
        window.URL.revokeObjectURL(video.src);
        resolve(Math.floor(video.duration));
      };
      video.onerror = function () {
        reject("Invalid video.");
      };
      video.src = url;
    } catch (e) {
      console.log("getVideoDuration error:", e);
      reject(e);
    }
  });

export const onBeforeUpload = (file: RcFile, checkAvatar?: boolean) => {
  const limitSize = isImage(file.name)
    ? file.size / 1024 / 1024 < 6
    : file.size / 1024 / 1024 < 1024 * 15;

  if (!limitSize && isImage(file.name)) {
    notification.error({ message: "Your image must be smaller than 6MB" });
  }
  if (!limitSize && isVideo(file.name)) {
    notification.error({ message: "Your Video must smaller than 15GB!" });
  }
  if (checkAvatar && isVideo(file.name)) {
    notification.error({ message: "Cannot use Video for avatar" });
  }

  if (checkAvatar) return limitSize && !isVideo(file.name);
  return limitSize;
};

export const handleCreatePresignedUrl = async (
  file: UploadFile<any> | File
): Promise<PresignedPostData | any> => {
  if (!file.name) return;

  const fileName = file.name;
  const fileExtension = getExtension(fileName)?.toLowerCase() as string;
  const contentType = file.type || "";

  let result: any;

  if (isDocumentFile(fileName) || isSoundFile(fileName)) {
    result = await photoService.getPresignedUrl(
      fileExtension,
      file.name.split(".")[0],
      contentType
    );
  } else if (isVideo(fileName)) {
    result = await photoService.getPresignedVideoUrl(fileExtension);
  } else {
    result = await photoService.getPresignedPhotoUrl(
      fileExtension,
      contentType
    );
  }

  if (result && !result.ok) {
    throw new Error(result.message);
  } else return result.response;
};

async function uploadParts(
  files: any,
  results: {
    parts: number;
    objectKey: string;
    urls: Record<number, string>;
    uploadId: string;
    partSize: number;
  },
  callback?: (value: number) => void,
  threshHold = 3
): Promise<Array<{ ETag: string; PartNumber: number }>> {
  const { parts, objectKey, uploadId, urls, partSize } = results;
  const axios = Axios.create();
  delete axios.defaults.headers.put["Content-Type"];

  const keys = Object.keys(urls);

  let percentageUpload = 0;
  // eslint-disable-next-line no-var
  var runCallbackInterval = setInterval(() => {
    if (percentageUpload < Math.ceil((1 / parts) * 100)) {
      callback && callback(percentageUpload);
      percentageUpload = percentageUpload + 1;
    }
  }, 1000);

  const resParts: any = [];
  let promises = [];
  const MAXIMUM_CONCURRENT = 5;

  // eslint-disable-next-line no-var
  var numberUploaded = 0;
  // eslint-disable-next-line no-var
  var stopUpload = false;

  const uploadPart = async (val: number, blob: Buffer) => {
    let tryTimes = 0;
    let success = false;

    while (!success && tryTimes <= threshHold) {
      const request = async () => {
        const result = await axios.put(urls[val + 1], blob);

        if (numberUploaded === 0) clearInterval(runCallbackInterval);

        callback &&
          callback(Math.ceil((Number(numberUploaded + 1) / parts) * 100));

        numberUploaded = numberUploaded + 1;

        success = true;
        return result;
      };
      try {
        return await request();
      } catch (err) {
        tryTimes++;
      }
    }
    if (!success && !stopUpload) {
      await photoService.stopMultipartUpload(objectKey, uploadId);
      stopUpload = true;
      clearInterval(runCallbackInterval);
      throw new Error("");
    }
  };

  let count = 0;

  while (count < keys.length) {
    const start = count * partSize;
    const end = (count + 1) * partSize;

    const partFile =
      count !== keys.length - 1 ? files.slice(start, end) : files.slice(start);

    const blob: Buffer = await fileToBuffer(partFile);
    console.log(blob);

    if (promises.length < MAXIMUM_CONCURRENT) {
      promises.push(uploadPart(count, blob));
    }
    const canCount =
      promises.length !== MAXIMUM_CONCURRENT && count !== keys.length - 1;

    if (canCount) count++;

    if (promises.length === MAXIMUM_CONCURRENT || count === keys.length - 1) {
      const result = await Promise.all(promises);
      resParts.push(...result);
      promises = [];
      if (!canCount) count++;
    }
  }

  return resParts.map((part: any, index: number) => ({
    ETag: (part as any)?.headers?.etag,
    PartNumber: index + 1,
  }));
}

const handleCreateInitiateMultipartUpload = async (
  file: UploadFile<any> | File
): Promise<{
  urls: Array<string>;
  partSize: number;
  objectKey: string;
  uploadId: string;
  parts: number;
}> => {
  let result: any;

  const fileName = file.name;
  const fileExtension = getExtension(fileName)?.toLowerCase() as string;
  const fileSize = file.size || 0;

  try {
    result = await photoService.createInitiateMultipartUpload(
      fileExtension,
      fileName,
      fileSize
    );
  } catch (err) {
    // To do later
  }

  if (result && !result.ok) {
    throw new Error(result.message);
  } else return result.response;
};

async function fileToBuffer(file: any): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = (event: any) => {
      if (event.target.readyState !== FileReader.DONE) {
        return reject(event?.error);
      }
      return resolve(event.target.result);
    };
  });
}

export const pushImageToS3 = async (
  file: UploadFile<any>,
  callbackFn?: (value: number, xhr?: any) => void,
  onFinish?: () => void
): Promise<string | undefined> => {
  /*const MAXIMUM_FILE_SUPPORTED_WEBVIEW = 250;

  if (
    file.size > MAXIMUM_FILE_SUPPORTED_WEBVIEW * 1024 * 1024 &&
    webviewMessage.isWebView()
  ) {
    webviewMessage.alert({
      subTitle: `Sorry, we cannot support file sizes larger than ${MAXIMUM_FILE_SUPPORTED_WEBVIEW}MB on mobile, please try again on desktop`,
      button1: "Close",
      type: ALERT_TYPE_WEBVIEW.UPLOAD_VIDEO_SIZE_BIG,
    });

    return;
  }*/

  if ((file.size || 0) > 50 * 1024 * 1024 && isVideo(file.name)) {
    try {
      const result = await handleCreateInitiateMultipartUpload(file);

      const uploadResult = await uploadParts(
        file?.originFileObj,
        result,
        callbackFn
      );

      if (uploadResult.filter((value) => value.ETag).length === result.parts) {
        const data: any = await photoService.getCompleteMultipartUpload(
          result?.objectKey,
          result.uploadId,
          uploadResult
        );

        return data.ok && data?.response && data.response?.url;
      }
    } catch {
      notification.error({
        message:
          "Highly unstable network connection. Please check your network connect again.",
        duration: 0,
        isOffDuration: true,
      });
      return;
    }
  } else {
    try {
      const presignedData = await handleCreatePresignedUrl(file);

      if (isVideo(file.name)) {
        return await awsService.uploadVideoToS3(
          presignedData,
          file.originFileObj,
          callbackFn,
          onFinish
        );
      } else {
        return await awsService.uploadPhotoToS3(
          presignedData,
          file.originFileObj,
          callbackFn,
          onFinish
        );
      }
    } catch (e: any) {
      notification.error({
        message: e.message,
        description: "",
      });
    }
  }
};

export const uploadImageToS3 = async (
  file: File
): Promise<string | undefined> => {
  if (isVideo(file.name)) {
    // only supports image
    return;
  }

  if ((file.size || 0) > 50 * 1024 * 1024) {
    // above maximum size allowed
    return;
  }

  const presignedData = await handleCreatePresignedUrl(file);

  return await awsService.uploadPhotoToS3(presignedData, file);
};

export const uploadImageWithCrop = async (
  imageFile: File,
  cropParams: Area
) => {
  const imageUrl = await uploadImage(imageFile);

  const urlObj = new URL(imageUrl);

  // set crop params for imagekit api
  urlObj.searchParams.set(
    "tr",
    `w-${cropParams.width},h-${cropParams.height},cm-extract,x-${cropParams.x},y-${cropParams.y}`
  );

  urlObj.searchParams.set("crp", JSON.stringify(cropParams));

  return {
    fileName: imageFile.name,
    url: urlObj.toString(),
  };
};

/**
 * Uploads an image to S3
 * This function will throw an error if the file is not an image or if the file size is greater than 50MB.
 * This means `undefined` will never be returned, compared to the previous implementation.
 * @param file
 */
export const uploadImage = async (file: File): Promise<string> => {
  if (!isImage(file.name)) {
    throw new UnsupportedFileTypeError("Only image files are supported");
  }

  if ((file.size || 0) > 50 * 1024 * 1024) {
    throw new FileTooLargeError("File size must be less than 50MB");
  }

  const presignedUrl = await handleCreatePresignedUrl(file);

  return awsService.uploadPhotoToS3(presignedUrl, file);
};

/**
 * This functions uploads a video directly to the target, based on a returned signed upload url.
 *
 * @param file
 * @returns Video object with thumbnail, and playback urls
 */
export const uploadVideo = async (file: File): Promise<Video> => {
  if (!isVideo(file.name)) {
    throw new UnsupportedFileTypeError("Only video files are supported");
  }

  if ((file.size || 0) > 200 * 1000 * 1000) {
    // Cannot support file size larger than 200MB until we support `tus` uploads
    // See: https://developers.cloudflare.com/stream/uploading-videos/direct-creator-uploads/
    throw new FileTooLargeError("File size must be less than 200MB");
  }

  const { uploadURL, videoId } = await videoService.createVideoDirectUpload();

  // upload file
  const formData = new FormData();
  formData.append("file", file);
  const response = await fetch(uploadURL, {
    method: "POST",
    body: formData,
  });

  if (!response.ok) {
    throw new FailedUploadError("Failed to upload video");
  }

  // poll every 5 seconds until the video is ready or an error
  let video: Video;
  do {
    video = await videoService.getVideo(videoId);
    await new Promise((resolve) => setTimeout(resolve, 5000));
  } while (video.status === "in-progress");

  if (video.status === "error") {
    throw new Error("Failed to upload video");
  }

  return video;
};

async function multiPartUploadFile(file: File): Promise<string> {
  try {
    const result = await handleCreateInitiateMultipartUpload(file);

    const uploadResult = await uploadParts(file, result);

    if (uploadResult.filter((value) => value.ETag).length !== result.parts) {
      throw new Error("Failed to upload all parts");
    }

    const data: any = await photoService.getCompleteMultipartUpload(
      result?.objectKey,
      result.uploadId,
      uploadResult
    );

    return data.ok && data?.response && data.response?.url;
  } catch (e) {
    throw new FailedUploadError(e.message);
  }
}

export class FailedUploadError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FailedUploadError";
  }
}

export class UnsupportedFileTypeError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "UnsupportedFileTypeError";
  }
}

export class ImageTooSmallError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ImageTooSmallError";
  }
}

export class FileTooLargeError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "FileTooLargeError";
  }
}

export type TransformationParams = {
  // Resize the image
  width?: number;
  height?: number; // if not provided, the aspect ratio will be preserved

  // Crop the image before resizing
  crop?: {
    width: number;
    height: number;
    x: number;
    y: number;
  };

  // Add a blur effect
  blur?: number;
};

function parseTransformationParams(
  url: string
): TransformationParams | undefined {
  try {
    const urlObj = new URL(url);
    const trParams = urlObj.searchParams.get("tr");

    if (!trParams) {
      return undefined;
    }

    const {
      w: width,
      h: height,

      x: cropX,
      y: cropY,
    } = trParams.split(",").reduce(
      (acc, curr) => {
        const [key, value] = curr.split("-");
        return { ...acc, [key]: parseInt(value || "0") };
      },
      {
        w: 0,
        h: 0,
        x: 0,
        y: 0,
      }
    );

    return {
      crop: {
        width,
        height,
        x: cropX,
        y: cropY,
      },
    };
  } catch (e) {
    console.log(e);
  }

  return undefined;
}

/**
 * Add cache buster parameter to image URL. This is forces the browser to fetch the image from the server
 * with the latest CORS headers. Image can be cached after the first request so using a static cache buster.
 * @param imageUrl
 */
export function addCacheBusterParameter(imageUrl: string): string {
  if (imageUrl.startsWith("data")) return imageUrl;

  try {
    const imageUrlWithCacheBuster = new URL(imageUrl);
    imageUrlWithCacheBuster.searchParams.set("cacheBuster", "1"); // cache bust once & then allow caching
    return imageUrlWithCacheBuster.toString();
  } catch (e) {
    console.log(`Error adding cache bust param to "${imageUrl}"`, e);
  }

  return imageUrl;
}

/**
 * Converts the requested image URL from our S3 bucket to a CDN URL.
 * @param imageUrl - Either a URL to an image hosted on our S3 bucket or a relative URL (/icons/apple.png)
 * @returns A CDN URL if the image is hosted on our S3 bucket, otherwise the original URL
 */
export function getImageUrlWithCDN(imageUrl: string): string {
  if (!imageUrl) {
    return imageUrl;
  }

  // If the image URL is a relative URL, return the CDN URL
  if (imageUrl.startsWith("/")) {
    return `${config.images.cdn}${imageUrl}`;
  }

  if (!imageUrl.includes(config.images.bucket)) {
    return imageUrl;
  }

  const startAt = imageUrl.split("/", 3).join("/").length;
  const match =
    /https:\/\/.+\.(s3\.amazonaws|s3-accelerate\.amazonaws)\.com\//gim.test(
      imageUrl
    );
  if (!match) {
    return imageUrl;
  }

  return `${config.images.cdn}${imageUrl.substring(startAt)}`;
}

export async function localResize(
  imageUrl: string,
  transformationParams: TransformationParams
): Promise<string> {
  const { crop } = transformationParams;

  if (!crop) {
    return imageUrl;
  }

  return new Promise((resolve, reject) => {
    const { width, height, x: cropX, y: cropY } = crop;

    const img = new Image();
    img.crossOrigin = "anonymous";
    img.src = addCacheBusterParameter(imageUrl);

    img.onload = () => {
      const devicePixelRatio = width < 1000 ? window.devicePixelRatio || 1 : 1;
      const cropWidth = width * devicePixelRatio;
      let cropHeight = height * devicePixelRatio;

      if (isNaN(cropHeight)) {
        cropHeight = cropWidth * (img.height / img.width);
      }

      const canvas = document.createElement("canvas");
      canvas.width = cropWidth;
      canvas.height = cropHeight;
      const ctx = canvas.getContext("2d");

      if (!ctx) {
        reject(new Error("Failed to get 2D context"));
        return;
      }

      if (typeof cropX === "undefined" && typeof cropY === "undefined") {
        // Centre the image and scale it to cover the canvas while preserving its aspect ratio
        const scale = Math.max(
          canvas.width / img.width,
          canvas.height / img.height
        );
        const scaledWidth = img.width * scale;
        const scaledHeight = img.height * scale;
        const dx = (canvas.width - scaledWidth) / 2;
        const dy = (canvas.height - scaledHeight) / 2;
        ctx.drawImage(img, dx, dy, scaledWidth, scaledHeight);
      } else {
        const adjustedCropWidth = cropWidth / devicePixelRatio;
        const adjustedCropHeight = cropHeight / devicePixelRatio;

        ctx.drawImage(
          img,
          cropX ?? 0,
          cropY ?? 0,
          adjustedCropWidth,
          adjustedCropHeight,
          0,
          0,
          canvas.width,
          canvas.height
        );
      }

      const dataUrl = canvas.toDataURL("image/png");
      resolve(dataUrl);
    };

    img.onerror = (err) => {
      reject(err);
    };
  });
}

// Applies the transformation params to the url so cloudflare can transform it
export function getCloudflareTransformedUrl(
  imageUrl: string,
  transformationParams: TransformationParams
): string {
  // We can only transform images that are hosted on Cloudflare
  if (!imageUrl.includes(config.images.cdn)) {
    return imageUrl;
  }

  try {
    const urlObj = new URL(imageUrl);
    const imagePath = urlObj.pathname;
    const hostname = urlObj.hostname;

    let params = Object.entries({
      width: transformationParams.width,
      height: transformationParams.height,
      "trim.left": transformationParams.crop?.x,
      "trim.top": transformationParams.crop?.y,
      "trim.width": transformationParams.crop?.width,
      "trim.height": transformationParams.crop?.height,
      blur: transformationParams.blur,
    })
      .filter(([_, value]) => value !== undefined)
      .map(([key, value]) => `${key}=${value}`)
      .join(",");

    // No need to transform the image if there are no transformation params
    if (!params) {
      return imageUrl;
    }

    if (window.devicePixelRatio > 1) {
      params += `,dpr=${window.devicePixelRatio}`;
    }

    return `https://${hostname}/cdn-cgi/image/${params}${imagePath}`;
  } catch (e) {
    console.error("Failed to transform image to cloudflare url", e);
  }

  // Return the original url if we can't parse it
  return imageUrl;
}

export async function transformImageFromUrl(
  imageUrl: string,
  requestedTransformation?: TransformationParams
): Promise<string> {
  const cdnUrl = getImageUrlWithCDN(imageUrl);
  const paramsFromUrl = parseTransformationParams(imageUrl);

  const transformationParams = {
    ...paramsFromUrl,
    ...requestedTransformation,
  };

  if (!transformationParams) {
    return cdnUrl;
  }

  // Process the transform via Cloudflare Images
  if (isOn(FLAGS.USE_CLOUDFLARE_IMAGE_TRANSFORM)) {
    return getCloudflareTransformedUrl(cdnUrl, transformationParams);
  }

  // Otherwise process it locally via canvas (slow!)
  return localResize(cdnUrl, transformationParams);
}
