import {createHash} from 'node:crypto'

import sharp from 'sharp'

const MAX_INPUT_PIXELS = 24_000_000 // ~24mp guards decompression bombs
const WEBP_QUALITY = 80

// validate against magic bytes, not the client content-type
const isJpeg = (b: Uint8Array) =>
  b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff
const isPng = (b: Uint8Array) =>
  b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47
const isWebp = (b: Uint8Array) =>
  b[0] === 0x52 &&
  b[1] === 0x49 &&
  b[2] === 0x46 &&
  b[3] === 0x46 &&
  b[8] === 0x57 &&
  b[9] === 0x45 &&
  b[10] === 0x42 &&
  b[11] === 0x50

const looksLikeImage = (b: Uint8Array) =>
  isJpeg(b) || isPng(b) || isWebp(b)

export class InvalidImageError extends Error {}

export type ProcessedImage = {
  bytes: Uint8Array
  contentType: string
  hash: string
}

type ReencodeOpts = {
  width: number
  height?: number
  fit: 'cover' | 'inside'
  maxInputBytes: number
  withoutEnlargement?: boolean
}

// re-encode to webp — neutralizes crafted payloads, strips exif (sharp drops
// metadata unless withMetadata is called), normalizes dimensions
export const reencodeWebp = async (
  input: Uint8Array,
  {
    width,
    height,
    fit,
    maxInputBytes,
    withoutEnlargement,
  }: ReencodeOpts,
): Promise<ProcessedImage> => {
  if (input.byteLength === 0)
    throw new InvalidImageError('empty_body')
  if (input.byteLength > maxInputBytes)
    throw new InvalidImageError('too_large')
  if (input.byteLength < 12 || !looksLikeImage(input))
    throw new InvalidImageError('unsupported_type')

  let output: Buffer
  try {
    output = await sharp(input, {limitInputPixels: MAX_INPUT_PIXELS})
      .rotate()
      .resize(width, height, {fit, withoutEnlargement})
      .webp({quality: WEBP_QUALITY})
      .toBuffer()
  } catch {
    throw new InvalidImageError('decode_failed')
  }

  const hash = createHash('sha256').update(output).digest('hex')
  return {bytes: output, contentType: 'image/webp', hash}
}
