import { promises as fs } from 'fs'
import { createHash } from 'node:crypto'
import path from 'path'
import { ImgHTMLAttributes } from 'react'
import sharp from 'sharp'

const outputDir = path.join(process.cwd(), '.next/static/images')
const serverPath = '/_next/static/images/'

interface ReqProps {
  src: string,
  alt: string,
}

type FixedSingleDimension = {width: number} | {height: number}
type FixedBothDimension = {width: number, height: number}
type MultiSize = {width: number[]} | {height: number[]}
type SizeProps = FixedSingleDimension | FixedBothDimension | MultiSize
type ImageAttrs = Omit<ImgHTMLAttributes<HTMLImageElement>, "width" | "height" | "src"> 

type ImageProps = ImageAttrs
  & SizeProps
  & ReqProps 

interface ProcessedImage {
  hostPath: string,
  randPrefix: string,
  origSrc: string,
  aspectRatio: number,
}


async function ImageServer({src,...imgAttrs}: ImageProps) {
  const width = 'width' in imgAttrs ? imgAttrs.width : null
  const height = 'height' in imgAttrs ? imgAttrs.height : null

  // Handle where we have an array of widths
  if (Array.isArray(width)) {
    let _src : string = src
    const srcSet = (await Promise.all(width.map(async width => {
      const {outputPath, origSrc} = await processImage(src, {width})
      _src = origSrc
      return `${outputPath} ${width}w`
    }))).join(',')

    return (
      <img
        src={_src}
        srcSet={srcSet}
        {...imgAttrs}
        width={undefined}
        height={undefined}
      />
    )
  }else if (Array.isArray(height)) {
    let _src : string = src
    const srcSet = (await Promise.all(height.map(async height => {
      const {outputPath, origSrc} = await processImage(src, {height})
      _src = origSrc
      return `${outputPath} ${width}w`
    }))).join(',')

    return (
      <img
        src={_src}
        srcSet={srcSet}
        {...imgAttrs}
        width={undefined}
        height={undefined}
      />
    )
  } else {
    const {outputPath, resolvedWidth} = await processImage(src, {width, height})
    return <img src={outputPath} {...imgAttrs} height={undefined} width={resolvedWidth} />
  }
}

async function processImage(sourcePath: string, {width, height} : {width?: number, height?: number}) : Promise<{outputPath: string, resolvedWidth: number, origSrc: string}> {
  await fs.mkdir(outputDir, {recursive: true})

  const hostPath = path.join(process.cwd(), sourcePath)
  const srcImg = sharp(hostPath)
  const metadata = await srcImg.metadata()
  const extension = path.extname(hostPath)
  const filename = path.basename(hostPath, extension)
  const hashPrefix = createHash('sha256').update(hostPath).digest('hex').substr(0, 7)
  const origSrc = `${hashPrefix}-${filename}${extension}`
  const aspectRatio = metadata.width / metadata.height
  const resolvedWidth = width ? width : aspectRatio * height
  const outputName = `${hashPrefix}-${filename}-${resolvedWidth}w${extension}`

  const origCopyStat = await fs.stat(path.join(outputDir, origSrc)).catch(() => undefined)
  if (!origCopyStat?.isFile()) {
    console.log(`copying file ${hostPath}`)
    await fs.copyFile(hostPath, path.join(outputDir, origSrc))
  }

  const outputStat = await fs.stat(path.join(outputDir, outputName)).catch(() => undefined)
  if (!outputStat?.isFile()) {
    console.log(`resizing file ${hostPath} for width ${resolvedWidth}`)

    await srcImg
      .resize({width: resolvedWidth})
      .toFile(path.join(outputDir, outputName))
  }

  return {
    outputPath: serverPath + outputName,
    resolvedWidth,
    origSrc,
  }
}

export default function ImageServerWrap(props: ImageProps) {
  return (
    <>
      {/* @ts-expect-error Async Server Component */}
      <ImageServer {...props} />
    </>
  )
}
