import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Jimp, JimpMime } from "jimp";
import decodeWebp, { init as initWebpDecode } from "@jsquash/webp/decode.js";

export interface WechatUploadAsset {
  buffer: Buffer;
  filename: string;
  contentType: string;
  fileExt: string;
  fileSize: number;
}

export interface PreparedWechatUploadAsset {
  buffer: Buffer;
  filename: string;
  contentType: string;
  wasProcessed: boolean;
  processingNotes: string[];
}

export const WECHAT_BODY_IMAGE_MAX_SIZE = 1024 * 1024; // 1MB
export const WECHAT_BODY_IMAGE_UNSUPPORTED_FORMATS = new Set([
  ".gif",
  ".webp",
  ".bmp",
  ".tiff",
  ".tif",
  ".svg",
  ".ico",
]);

const BODY_UPLOAD_ALLOWED_MIME_TYPES = new Set([
  JimpMime.jpeg,
  JimpMime.png,
]);

const MIME_TO_EXT: Record<string, string> = {
  "image/jpeg": ".jpg",
  "image/png": ".png",
  "image/gif": ".gif",
  "image/webp": ".webp",
  "image/bmp": ".bmp",
  "image/x-ms-bmp": ".bmp",
  "image/tiff": ".tiff",
  "image/svg+xml": ".svg",
  "image/x-icon": ".ico",
  "image/vnd.microsoft.icon": ".ico",
};

const JPEG_QUALITY_STEPS = [82, 74, 66, 58, 50, 42, 34];
const MAX_WIDTH_STEPS = [2560, 2048, 1600, 1280, 1024, 800, 640, 480];

let webpDecoderReady: Promise<void> | undefined;

type JimpImage = Awaited<ReturnType<typeof Jimp.read>>;

function normalizeMimeType(contentType: string): string {
  return contentType.split(";")[0]!.trim().toLowerCase();
}

function extFromMimeType(contentType: string): string {
  return MIME_TO_EXT[normalizeMimeType(contentType)] || "";
}

function ensureFileExt(asset: WechatUploadAsset): string {
  return asset.fileExt || extFromMimeType(asset.contentType);
}

function basenameWithoutExt(filename: string): string {
  const base = path.basename(filename, path.extname(filename));
  return base || "image";
}

function renameWithExt(filename: string, ext: string): string {
  return `${basenameWithoutExt(filename)}${ext}`;
}

export function needsWechatBodyImageProcessing(asset: WechatUploadAsset): boolean {
  if (asset.fileSize > WECHAT_BODY_IMAGE_MAX_SIZE) {
    return true;
  }

  const normalizedMimeType = normalizeMimeType(asset.contentType);
  if (BODY_UPLOAD_ALLOWED_MIME_TYPES.has(normalizedMimeType)) {
    return false;
  }

  const fileExt = ensureFileExt(asset);
  return WECHAT_BODY_IMAGE_UNSUPPORTED_FORMATS.has(fileExt) || !fileExt;
}

async function ensureWebpDecoder(): Promise<void> {
  if (!webpDecoderReady) {
    webpDecoderReady = (async () => {
      const __filename = fileURLToPath(import.meta.url);
      const __dirname = path.dirname(__filename);
      const wasmPath = path.resolve(__dirname, "node_modules/@jsquash/webp/codec/dec/webp_dec.wasm");
      const wasmModule = await WebAssembly.compile(await fs.readFile(wasmPath));
      await initWebpDecode(wasmModule, {});
    })();
  }

  await webpDecoderReady;
}

async function loadImageForProcessing(asset: WechatUploadAsset): Promise<JimpImage> {
  const fileExt = ensureFileExt(asset);
  const normalizedMimeType = normalizeMimeType(asset.contentType);

  if (fileExt === ".webp" || normalizedMimeType === "image/webp") {
    await ensureWebpDecoder();
    const decoded = await decodeWebp(asset.buffer);
    return new Jimp({
      data: Buffer.from(decoded.data.buffer, decoded.data.byteOffset, decoded.data.byteLength),
      width: decoded.width,
      height: decoded.height,
    });
  }

  if (fileExt === ".svg" || fileExt === ".ico") {
    throw new Error(`Cannot convert ${fileExt} image for WeChat body upload; provide a PNG or JPG instead.`);
  }

  return Jimp.read(asset.buffer);
}

function imageHasTransparency(image: JimpImage): boolean {
  const { data } = image.bitmap;
  for (let i = 3; i < data.length; i += 4) {
    if (data[i] !== 255) {
      return true;
    }
  }
  return false;
}

function buildCandidateWidths(width: number): number[] {
  const candidates = new Set<number>([width]);

  for (const maxWidth of MAX_WIDTH_STEPS) {
    if (width > maxWidth) {
      candidates.add(maxWidth);
    }
  }

  return [...candidates].sort((a, b) => b - a);
}

function resizeToWidth(image: JimpImage, width: number): JimpImage {
  const cloned = image.clone();
  if (width < image.bitmap.width) {
    cloned.resize({ w: width });
  }
  return cloned;
}

function flattenOnWhite(image: JimpImage): JimpImage {
  const flattened = new Jimp({
    width: image.bitmap.width,
    height: image.bitmap.height,
    color: 0xffffffff,
  });
  flattened.composite(image, 0, 0);
  return flattened;
}

async function encodePng(image: JimpImage): Promise<Buffer> {
  return image.getBuffer(JimpMime.png);
}

async function encodeJpeg(image: JimpImage, quality: number): Promise<Buffer> {
  const jpegSource = imageHasTransparency(image) ? flattenOnWhite(image) : image;
  return jpegSource.getBuffer(JimpMime.jpeg, { quality });
}

function buildProcessingNotes(asset: WechatUploadAsset): string[] {
  const notes: string[] = [];
  const fileExt = ensureFileExt(asset);

  if (fileExt && WECHAT_BODY_IMAGE_UNSUPPORTED_FORMATS.has(fileExt)) {
    notes.push(`converted unsupported ${fileExt} source`);
  }

  if (asset.fileSize > WECHAT_BODY_IMAGE_MAX_SIZE) {
    notes.push(`compressed ${(asset.fileSize / 1024 / 1024).toFixed(2)}MB source below 1MB`);
  }

  if (notes.length === 0) {
    notes.push("re-encoded for WeChat body upload");
  }

  return notes;
}

export async function prepareWechatBodyImageUpload(
  asset: WechatUploadAsset,
): Promise<PreparedWechatUploadAsset> {
  if (!needsWechatBodyImageProcessing(asset)) {
    return {
      buffer: asset.buffer,
      filename: asset.filename,
      contentType: asset.contentType,
      wasProcessed: false,
      processingNotes: [],
    };
  }

  const image = await loadImageForProcessing(asset);
  const widths = buildCandidateWidths(image.bitmap.width);
  const preferPng = imageHasTransparency(image) || ensureFileExt(asset) === ".png";
  const processingNotes = buildProcessingNotes(asset);

  for (const width of widths) {
    const resized = resizeToWidth(image, width);

    if (preferPng) {
      const pngBuffer = await encodePng(resized);
      if (pngBuffer.length <= WECHAT_BODY_IMAGE_MAX_SIZE) {
        return {
          buffer: pngBuffer,
          filename: renameWithExt(asset.filename, ".png"),
          contentType: JimpMime.png,
          wasProcessed: true,
          processingNotes: width < image.bitmap.width
            ? [...processingNotes, `resized to ${width}px wide`]
            : processingNotes,
        };
      }
    }

    for (const quality of JPEG_QUALITY_STEPS) {
      const jpegBuffer = await encodeJpeg(resized, quality);
      if (jpegBuffer.length <= WECHAT_BODY_IMAGE_MAX_SIZE) {
        const notes = [...processingNotes, `encoded as JPEG (${quality} quality)`];
        if (width < image.bitmap.width) {
          notes.push(`resized to ${width}px wide`);
        }
        return {
          buffer: jpegBuffer,
          filename: renameWithExt(asset.filename, ".jpg"),
          contentType: JimpMime.jpeg,
          wasProcessed: true,
          processingNotes: notes,
        };
      }
    }
  }

  throw new Error(`Unable to reduce ${asset.filename} below 1MB for WeChat body upload.`);
}
