const SUPPORTED_IMAGE_TYPES = new Set([
  "image/jpeg",
  "image/png",
  "image/webp",
]);

export const FEEDBACK_IMAGE_ACCEPT = "image/jpeg,image/png,image/webp";
export const FEEDBACK_IMAGE_LIMIT = 3;
export const FEEDBACK_IMAGE_SOURCE_MAX_BYTES = 5 * 1024 * 1024;
export const FEEDBACK_IMAGE_TARGET_MAX_BYTES = 2 * 1024 * 1024;
export const FEEDBACK_IMAGE_MAX_DIMENSION = 1600;

const OUTPUT_QUALITIES = [0.86, 0.74, 0.62, 0.5] as const;
const DIMENSION_CAPS = [1600, 1400, 1200, 1000, 800] as const;

interface DecodedImage {
  width: number;
  height: number;
  source: CanvasImageSource;
  dispose: () => void;
}

export function validateFeedbackImage(file: File): string | null {
  if (!SUPPORTED_IMAGE_TYPES.has(file.type)) {
    return "Please choose a JPEG, PNG, or WebP image.";
  }
  if (file.size > FEEDBACK_IMAGE_SOURCE_MAX_BYTES) {
    return "Each image must be 5 MB or smaller.";
  }
  return null;
}

export async function prepareFeedbackImage(file: File): Promise<File> {
  const validationError = validateFeedbackImage(file);
  if (validationError) throw new Error(validationError);

  const decoded = await decodeImage(file);
  try {
    const longestEdge = Math.max(decoded.width, decoded.height);
    if (
      longestEdge <= FEEDBACK_IMAGE_MAX_DIMENSION &&
      file.size <= FEEDBACK_IMAGE_TARGET_MAX_BYTES
    ) {
      return file;
    }

    const mimeType = file.type === "image/jpeg" ? "image/jpeg" : "image/webp";
    let smallestBlob: Blob | null = null;
    const renderedSizes = new Set<string>();

    for (const dimensionCap of DIMENSION_CAPS) {
      const size = fitWithinDimension(
        decoded.width,
        decoded.height,
        dimensionCap,
      );
      const sizeKey = `${size.width}x${size.height}`;
      if (renderedSizes.has(sizeKey)) continue;
      renderedSizes.add(sizeKey);

      const canvas = document.createElement("canvas");
      canvas.width = size.width;
      canvas.height = size.height;
      const context = canvas.getContext("2d");
      if (!context) throw new Error("This browser cannot process images.");
      context.drawImage(decoded.source, 0, 0, size.width, size.height);

      for (const quality of OUTPUT_QUALITIES) {
        const blob = await canvasToBlob(canvas, mimeType, quality);
        if (!smallestBlob || blob.size < smallestBlob.size) smallestBlob = blob;
        if (blob.size <= FEEDBACK_IMAGE_TARGET_MAX_BYTES) {
          return toFeedbackFile(blob, file.name);
        }
      }
    }

    if (smallestBlob && smallestBlob.size <= FEEDBACK_IMAGE_TARGET_MAX_BYTES) {
      return toFeedbackFile(smallestBlob, file.name);
    }
    throw new Error("This image is still too large after compression.");
  } finally {
    decoded.dispose();
  }
}

export function fitWithinDimension(
  width: number,
  height: number,
  maxDimension: number,
): { width: number; height: number } {
  const scale = Math.min(1, maxDimension / Math.max(width, height));
  return {
    width: Math.max(1, Math.round(width * scale)),
    height: Math.max(1, Math.round(height * scale)),
  };
}

async function decodeImage(file: File): Promise<DecodedImage> {
  if (typeof createImageBitmap === "function") {
    try {
      const bitmap = await createImageBitmap(file, {
        imageOrientation: "from-image",
      });
      return {
        width: bitmap.width,
        height: bitmap.height,
        source: bitmap,
        dispose: () => bitmap.close(),
      };
    } catch {
      // Some WebViews expose createImageBitmap but cannot decode every format.
    }
  }

  const objectUrl = URL.createObjectURL(file);
  try {
    const image = await new Promise<HTMLImageElement>((resolve, reject) => {
      const element = new Image();
      element.onload = () => resolve(element);
      element.onerror = () => reject(new Error("This image could not be read."));
      element.src = objectUrl;
    });
    return {
      width: image.naturalWidth,
      height: image.naturalHeight,
      source: image,
      dispose: () => URL.revokeObjectURL(objectUrl),
    };
  } catch (error) {
    URL.revokeObjectURL(objectUrl);
    throw error;
  }
}

function canvasToBlob(
  canvas: HTMLCanvasElement,
  mimeType: string,
  quality: number,
): Promise<Blob> {
  return new Promise((resolve, reject) => {
    canvas.toBlob(
      (blob) => {
        if (blob) resolve(blob);
        else reject(new Error("This image could not be compressed."));
      },
      mimeType,
      quality,
    );
  });
}

function toFeedbackFile(blob: Blob, originalName: string): File {
  const extension = blob.type === "image/jpeg" ? "jpg" : "webp";
  const baseName = originalName.replace(/\.[^.]+$/, "") || "feedback-image";
  return new File([blob], `${baseName}.${extension}`, {
    type: blob.type,
    lastModified: Date.now(),
  });
}
