import { mkdirSync, statSync, writeFileSync } from "node:fs";
import { basename, extname, join, resolve } from "node:path";
import { normalizeImageWidths, type ImageFormat } from "./index.ts";

export interface ImageProcessorResult {
  data: Uint8Array;
  width: number;
  height: number;
}

export interface ImageProcessor {
  transform(
    input: string,
    options: { width: number; format: Exclude<ImageFormat, "original">; quality: number },
  ): Promise<ImageProcessorResult>;
}

export interface OptimizeImageOptions {
  outputDir: string;
  widths: number[];
  formats?: Array<Exclude<ImageFormat, "original">>;
  quality?: number;
  maxVariants?: number;
  processor?: ImageProcessor;
}

export interface OptimizedImageVariant {
  path: string;
  width: number;
  height: number;
  format: Exclude<ImageFormat, "original">;
  bytes: number;
}

export interface OptimizedImageManifest {
  source: string;
  variants: OptimizedImageVariant[];
}

async function sharpProcessor(): Promise<ImageProcessor> {
  const packageName = "sharp";
  let sharp: any;
  try {
    const module = (await import(packageName)) as { default?: any };
    sharp = module.default ?? module;
  } catch (error) {
    throw new Error(
      "WRN-IMAGE-SHARP-MISSING: install the optional `sharp` peer or provide an ImageProcessor",
      { cause: error },
    );
  }
  return {
    async transform(input, options) {
      const pipeline = sharp(input)
        .rotate()
        .resize({ width: options.width, withoutEnlargement: true });
      const { data, info } = await pipeline
        .toFormat(options.format, { quality: options.quality })
        .toBuffer({ resolveWithObject: true });
      return { data: new Uint8Array(data), width: info.width, height: info.height };
    },
  };
}

export async function optimizeImage(
  input: string,
  options: OptimizeImageOptions,
): Promise<OptimizedImageManifest> {
  const widths = normalizeImageWidths(options.widths);
  const formats = [...new Set(options.formats ?? ["avif", "webp"])] as Array<
    Exclude<ImageFormat, "original">
  >;
  if (!widths.length) throw new RangeError("Image optimization requires at least one valid width");
  if (
    !formats.length ||
    formats.some((format) => !["avif", "webp", "jpeg", "png"].includes(format))
  )
    throw new RangeError("Image optimization has an unsupported output format");
  const maxVariants = options.maxVariants ?? 32;
  if (!Number.isInteger(maxVariants) || maxVariants < 1)
    throw new RangeError("Image maxVariants must be positive");
  if (widths.length * formats.length > maxVariants)
    throw new RangeError(`Image optimization exceeds ${maxVariants} variants`);
  const quality = Math.max(1, Math.min(100, Math.round(options.quality ?? 80)));
  const processor = options.processor ?? (await sharpProcessor());
  const outputDir = resolve(options.outputDir);
  mkdirSync(outputDir, { recursive: true });
  const stem = basename(input, extname(input)).replace(/[^A-Za-z0-9._-]+/g, "-") || "image";
  const variants: OptimizedImageVariant[] = [];
  for (const format of formats) {
    for (const width of widths) {
      const result = await processor.transform(resolve(input), { width, format, quality });
      if (
        !Number.isInteger(result.width) ||
        result.width < 1 ||
        !Number.isInteger(result.height) ||
        result.height < 1
      )
        throw new Error("Image processor returned invalid dimensions");
      const path = join(outputDir, `${stem}-${result.width}.${format === "jpeg" ? "jpg" : format}`);
      writeFileSync(path, result.data);
      variants.push({
        path,
        width: result.width,
        height: result.height,
        format,
        bytes: statSync(path).size,
      });
    }
  }
  return { source: resolve(input), variants };
}
