import JSZip from 'jszip'

import {
  type BatchIssue,
  type CertificateTemplateConfig,
  type GeneratedAsset,
  type IdPhotoConfig,
  type IdPhotoCrop,
  type Rect,
  type StudentRow,
  DEFAULT_ID_PHOTO_CROP,
  denormalizeRect,
  idPhotoBackgroundColor,
  normalizeMatchName,
  resolveIdPhotoCrop,
  sanitizeFileName
} from '@/lib/certificate-types'

export type ImageSource = {
  fileName: string
  blob?: Blob
  dataUrl: string
  width: number
  height: number
  extension: 'jpg' | 'jpeg' | 'png'
  contentType: 'image/jpeg' | 'image/png'
}

type ParsedWorkbook = {
  rows: StudentRow[]
  error?: string
}

function decodeXml(value: string) {
  return value
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&')
}

function canvasBlob(canvas: HTMLCanvasElement, type: string, quality = 0.94) {
  return new Promise<Blob>((resolve, reject) => {
    canvas.toBlob((blob) => {
      if (blob) {
        resolve(blob)
      } else {
        reject(new Error('图片导出失败'))
      }
    }, type, quality)
  })
}

function readAsDataUrl(blob: Blob) {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(String(reader.result))
    reader.onerror = () => reject(new Error('图片读取失败'))
    reader.readAsDataURL(blob)
  })
}

async function blobFromImageSource(source: ImageSource) {
  if (source.blob) {
    return source.blob.type
      ? source.blob
      : new Blob([source.blob], { type: source.contentType })
  }

  const response = await fetch(source.dataUrl)

  if (!response.ok) {
    throw new Error('照片读取失败')
  }

  const blob = await response.blob()

  return blob.type
    ? blob
    : new Blob([blob], { type: source.contentType })
}

function readImage(dataUrl: string) {
  return new Promise<HTMLImageElement>((resolve, reject) => {
    const image = new Image()
    image.onload = () => resolve(image)
    image.onerror = () => reject(new Error('图片加载失败'))
    image.src = dataUrl
  })
}

function foregroundBounds(image: HTMLImageElement) {
  const canvas = document.createElement('canvas')
  canvas.width = image.naturalWidth
  canvas.height = image.naturalHeight
  const context = canvas.getContext('2d', { willReadFrequently: true })

  if (!context) {
    throw new Error('浏览器不支持 Canvas')
  }

  context.drawImage(image, 0, 0)
  const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data
  let minX = canvas.width
  let minY = canvas.height
  let maxX = 0
  let maxY = 0

  for (let y = 0; y < canvas.height; y += 1) {
    for (let x = 0; x < canvas.width; x += 1) {
      if (pixels[(y * canvas.width + x) * 4 + 3] <= 8) {
        continue
      }

      minX = Math.min(minX, x)
      minY = Math.min(minY, y)
      maxX = Math.max(maxX, x)
      maxY = Math.max(maxY, y)
    }
  }

  if (minX > maxX || minY > maxY) {
    return {
      x: 0,
      y: 0,
      width: image.naturalWidth,
      height: image.naturalHeight
    }
  }

  return {
    x: minX,
    y: minY,
    width: maxX - minX + 1,
    height: maxY - minY + 1
  }
}

async function cropImageSource(source: ImageSource, rect: Rect) {
  const image = await readImage(source.dataUrl)
  const sourceX = rect.x * image.naturalWidth
  const sourceY = rect.y * image.naturalHeight
  const sourceWidth = rect.width * image.naturalWidth
  const sourceHeight = rect.height * image.naturalHeight
  const canvas = document.createElement('canvas')
  canvas.width = Math.max(1, Math.round(sourceWidth))
  canvas.height = Math.max(1, Math.round(sourceHeight))

  const context = canvas.getContext('2d')

  if (!context) {
    throw new Error('浏览器不支持 Canvas')
  }

  context.imageSmoothingEnabled = true
  context.imageSmoothingQuality = 'high'
  context.drawImage(
    image,
    sourceX,
    sourceY,
    sourceWidth,
    sourceHeight,
    0,
    0,
    canvas.width,
    canvas.height
  )

  return loadImageSource(
    await canvasBlob(canvas, source.contentType, 0.94),
    `${sanitizeFileName(source.fileName.replace(/\.[^.]+$/, ''))}-裁剪.${source.extension}`
  )
}

function imageMetaFromFileName(fileName: string) {
  const extension = fileName.toLowerCase().endsWith('.png') ? 'png' : 'jpg'

  return {
    extension,
    contentType: extension === 'png' ? 'image/png' : 'image/jpeg'
  } as const
}

export async function loadImageSource(fileOrBlob: File | Blob, fileName = 'image.jpg') {
  const dataUrl = await readAsDataUrl(fileOrBlob)
  const image = await readImage(dataUrl)
  const meta = imageMetaFromFileName(fileName)

  return {
    fileName,
    blob: fileOrBlob,
    dataUrl,
    width: image.naturalWidth,
    height: image.naturalHeight,
    ...meta
  } satisfies ImageSource
}

export async function loadImageSourceFromUrl(
  url: string,
  fileName = 'image.jpg',
  width?: number,
  height?: number,
  contentType?: 'image/jpeg' | 'image/png'
) {
  const image =
    width && height
      ? null
      : await readImage(url)
  const meta = contentType
    ? {
      extension: contentType === 'image/png' ? 'png' : 'jpg',
      contentType
    } as const
    : imageMetaFromFileName(fileName)

  return {
    fileName,
    dataUrl: url,
    width: width ?? image?.naturalWidth ?? 0,
    height: height ?? image?.naturalHeight ?? 0,
    ...meta
  } satisfies ImageSource
}

export async function loadImageSourceFromDataUrl(dataUrl: string, fileName = 'image.jpg') {
  if (!dataUrl.startsWith('data:')) {
    return loadImageSourceFromUrl(dataUrl, fileName)
  }

  const mimeType = dataUrl.match(/^data:([^;]+);base64,/)?.[1] ?? 'image/jpeg'
  const base64 = dataUrl.split(',')[1] ?? ''
  const bytes = Uint8Array.from(atob(base64), (character) => character.charCodeAt(0))

  return loadImageSource(new Blob([bytes], { type: mimeType }), fileName)
}

async function removeBackgroundOnServer(image: Blob) {
  const response = await fetch('/api/background-removal', {
    method: 'POST',
    headers: {
      'Content-Type': image.type || 'application/octet-stream'
    },
    body: image
  })

  if (!response.ok) {
    const message = await response.text()
    let error = '背景移除失败'

    try {
      error = JSON.parse(message).error ?? error
    } catch {
      error = message || error
    }

    throw new Error(error)
  }

  return response.blob()
}

export async function generateIdPhoto(
  source: ImageSource,
  config: IdPhotoConfig,
  crop: IdPhotoCrop = DEFAULT_ID_PHOTO_CROP
) {
  const resolvedCrop = resolveIdPhotoCrop(crop)
  const workingSource = resolvedCrop.sourceRect
    ? await cropImageSource(source, resolvedCrop.sourceRect)
    : source
  const foregroundBlob = await removeBackgroundOnServer(await blobFromImageSource(workingSource))
  const foregroundSource = await loadImageSource(foregroundBlob, `${sanitizeFileName(source.fileName)}-foreground.png`)
  const foregroundImage = await readImage(foregroundSource.dataUrl)
  const bounds = resolvedCrop.sourceRect
    ? {
      x: 0,
      y: 0,
      width: foregroundImage.naturalWidth,
      height: foregroundImage.naturalHeight
    }
    : foregroundBounds(foregroundImage)
  const canvas = document.createElement('canvas')
  canvas.width = config.widthPx
  canvas.height = config.heightPx

  const context = canvas.getContext('2d')

  if (!context) {
    throw new Error('浏览器不支持 Canvas')
  }

  context.fillStyle = idPhotoBackgroundColor(config)
  context.fillRect(0, 0, canvas.width, canvas.height)

  const targetWidth = resolvedCrop.sourceRect ? canvas.width : canvas.width * 0.9
  const targetHeight = resolvedCrop.sourceRect ? canvas.height : canvas.height * 1.03
  const scale = Math.max(targetWidth / bounds.width, targetHeight / bounds.height) *
    (resolvedCrop.sourceRect ? 1 : resolvedCrop.scale)
  const width = bounds.width * scale
  const height = bounds.height * scale
  const x = (canvas.width - width) / 2 + (resolvedCrop.sourceRect ? 0 : canvas.width * resolvedCrop.offsetX)
  const y = resolvedCrop.sourceRect
    ? (canvas.height - height) / 2
    : canvas.height - height + canvas.height * 0.03 + canvas.height * resolvedCrop.offsetY

  context.imageSmoothingEnabled = true
  context.imageSmoothingQuality = 'high'
  context.drawImage(
    foregroundImage,
    bounds.x,
    bounds.y,
    bounds.width,
    bounds.height,
    x,
    y,
    width,
    height
  )

  const blob = await canvasBlob(canvas, 'image/jpeg', 0.94)
  return loadImageSource(blob, `${sanitizeFileName(source.fileName.replace(/\.[^.]+$/, ''))}-证件照.jpg`)
}

function drawCoverImage(
  context: CanvasRenderingContext2D,
  image: HTMLImageElement,
  rect: Rect
) {
  const sourceRatio = image.naturalWidth / image.naturalHeight
  const targetRatio = rect.width / rect.height
  let sourceWidth = image.naturalWidth
  let sourceHeight = image.naturalHeight
  let sourceX = 0
  let sourceY = 0

  if (sourceRatio > targetRatio) {
    sourceWidth = image.naturalHeight * targetRatio
    sourceX = (image.naturalWidth - sourceWidth) / 2
  } else {
    sourceHeight = image.naturalWidth / targetRatio
    sourceY = (image.naturalHeight - sourceHeight) / 2
  }

  context.drawImage(
    image,
    sourceX,
    sourceY,
    sourceWidth,
    sourceHeight,
    rect.x,
    rect.y,
    rect.width,
    rect.height
  )
}

function drawName(
  context: CanvasRenderingContext2D,
  name: string,
  rect: Rect,
  config: CertificateTemplateConfig
) {
  const style = config.textStyle
  let fontSize = style.fontSize
  context.fillStyle = style.color
  context.textAlign = style.align
  context.textBaseline = 'middle'

  do {
    context.font = `${style.fontWeight} ${fontSize}px ${style.fontFamily}`
    if (context.measureText(name).width <= rect.width * 0.96 || fontSize <= 12) {
      break
    }
    fontSize -= 2
  } while (fontSize > 12)

  const x =
    style.align === 'left'
      ? rect.x
      : style.align === 'right'
        ? rect.x + rect.width
        : rect.x + rect.width / 2

  context.fillText(name, x, rect.y + rect.height / 2)
}

export async function renderCertificateJpg(
  template: ImageSource,
  photo: ImageSource,
  name: string,
  config: CertificateTemplateConfig
) {
  const canvas = document.createElement('canvas')
  canvas.width = template.width
  canvas.height = template.height

  const context = canvas.getContext('2d')

  if (!context) {
    throw new Error('浏览器不支持 Canvas')
  }

  const templateImage = await readImage(template.dataUrl)
  const photoImage = await readImage(photo.dataUrl)
  const imageRect = denormalizeRect(config.imageRect, template.width, template.height)
  const nameRect = denormalizeRect(config.nameRect, template.width, template.height)

  context.fillStyle = '#ffffff'
  context.fillRect(0, 0, canvas.width, canvas.height)
  context.drawImage(templateImage, 0, 0, canvas.width, canvas.height)
  drawCoverImage(context, photoImage, imageRect)
  drawName(context, name, nameRect, config)

  return canvasBlob(canvas, 'image/jpeg')
}

export async function generateCertificateAsset(
  template: ImageSource,
  photo: ImageSource,
  name: string,
  config: CertificateTemplateConfig
) {
  const safeName = sanitizeFileName(name)
  const jpg = await renderCertificateJpg(template, photo, name, config)

  return {
    fileName: safeName,
    jpg
  } satisfies GeneratedAsset
}

function parseCsv(text: string): ParsedWorkbook {
  const lines = text
    .replace(/^\uFEFF/, '')
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)

  if (lines.length === 0) {
    return { rows: [], error: '表格为空' }
  }

  const headers = lines[0].split(',').map((cell) => cell.trim())
  const nameIndex = headers.indexOf('姓名')

  if (nameIndex < 0) {
    return { rows: [], error: 'Excel/CSV 必须包含“姓名”列' }
  }

  return {
    rows: lines
      .slice(1)
      .map((line, index) => ({
        name: line.split(',')[nameIndex]?.trim() ?? '',
        rowNumber: index + 2
      }))
      .filter((row) => row.name)
  }
}

function columnIndex(cellRef: string) {
  const letters = cellRef.replace(/\d+/g, '')
  return [...letters].reduce((total, letter) => total * 26 + letter.charCodeAt(0) - 64, 0) - 1
}

async function parseXlsx(file: File): Promise<ParsedWorkbook> {
  const zip = await JSZip.loadAsync(await file.arrayBuffer())
  const sharedStringsXml = await zip.file('xl/sharedStrings.xml')?.async('text')
  const sharedStrings = sharedStringsXml
    ? [...sharedStringsXml.matchAll(/<si>([\s\S]*?)<\/si>/g)].map((match) =>
        decodeXml([...match[1].matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((text) => text[1]).join(''))
      )
    : []
  const workbookRels = await zip.file('xl/_rels/workbook.xml.rels')?.async('text')
  const firstSheetRel = workbookRels?.match(
    /<Relationship\b(?=[^>]*Type="[^"]*\/worksheet")[^>]*Target="([^"]+)"/
  )?.[1]
  const sheetPath = firstSheetRel
    ? `xl/${firstSheetRel.replace(/^\//, '').replace(/^xl\//, '')}`
    : 'xl/worksheets/sheet1.xml'
  const sheetXml = await zip.file(sheetPath)?.async('text')

  if (!sheetXml) {
    return { rows: [], error: '未找到 Excel 第一张工作表' }
  }

  const rows = [...sheetXml.matchAll(/<row[^>]*r="(\d+)"[^>]*>([\s\S]*?)<\/row>/g)].map((rowMatch) => {
    const values: string[] = []

    for (const cellMatch of rowMatch[2].matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>/g)) {
      const attrs = cellMatch[1]
      const body = cellMatch[2]
      const ref = attrs.match(/\br="([^"]+)"/)?.[1] ?? ''
      const type = attrs.match(/\bt="([^"]+)"/)?.[1]
      const rawValue = body.match(/<v>([\s\S]*?)<\/v>/)?.[1] ?? ''
      const inlineValue = body.match(/<t[^>]*>([\s\S]*?)<\/t>/)?.[1]
      const value = type === 's' ? sharedStrings[Number(rawValue)] : inlineValue ?? rawValue

      values[columnIndex(ref)] = decodeXml(String(value ?? '')).trim()
    }

    return {
      rowNumber: Number(rowMatch[1]),
      values
    }
  })

  const headerRow = rows[0]
  const nameIndex = headerRow?.values.indexOf('姓名') ?? -1

  if (nameIndex < 0) {
    return { rows: [], error: 'Excel/CSV 必须包含“姓名”列' }
  }

  return {
    rows: rows
      .slice(1)
      .map((row) => ({
        name: row.values[nameIndex] ?? '',
        rowNumber: row.rowNumber
      }))
      .filter((row) => row.name)
  }
}

export async function parseStudentFile(file: File) {
  if (/\.csv$/i.test(file.name)) {
    return parseCsv(await file.text())
  }

  return parseXlsx(file)
}

export async function collectPhotos(files: File[], zipFiles: File[]) {
  const photos = new Map<string, ImageSource>()
  const issues: BatchIssue[] = []

  async function addPhoto(fileName: string, blob: Blob) {
    if (!/\.(jpe?g|png)$/i.test(fileName)) {
      return
    }

    const key = normalizeMatchName(fileName)

    if (photos.has(key)) {
      issues.push({
        name: fileName,
        status: 'skipped',
        message: '照片文件名重复，已保留第一张'
      })
      return
    }

    photos.set(key, await loadImageSource(blob, fileName))
  }

  for (const file of files) {
    await addPhoto(file.name, file)
  }

  for (const zipFile of zipFiles) {
    const zip = await JSZip.loadAsync(await zipFile.arrayBuffer())
    for (const [zipPath, entry] of Object.entries(zip.files)) {
      if (entry.dir) {
        continue
      }

      await addPhoto(zipPath.split(/[\\/]/).pop() ?? zipPath, await entry.async('blob'))
    }
  }

  return { photos, issues }
}

export async function buildBatchZip(
  template: ImageSource,
  students: StudentRow[],
  photos: Map<string, ImageSource>,
  config: CertificateTemplateConfig,
  initialIssues: BatchIssue[]
) {
  const zip = new JSZip()
  const issues: BatchIssue[] = [...initialIssues]
  const usedNames = new Map<string, number>()

  for (const student of students) {
    const key = normalizeMatchName(student.name)
    const photo = photos.get(key)

    if (!photo) {
      issues.push({
        name: student.name,
        status: 'skipped',
        message: `第 ${student.rowNumber} 行缺少匹配照片`
      })
      continue
    }

    const asset = await generateCertificateAsset(template, photo, student.name, config)
    const usedCount = usedNames.get(asset.fileName) ?? 0
    usedNames.set(asset.fileName, usedCount + 1)
    const suffix = usedCount > 0 ? `-${usedCount + 1}` : ''
    const outputName = `${asset.fileName}${suffix}`

    zip.file(`jpg/${outputName}.jpg`, asset.jpg)
    issues.push({
      name: student.name,
      status: 'success',
      message: `已生成 ${outputName}`
    })
  }

  const report = ['姓名,状态,说明']
    .concat(issues.map((issue) => `${issue.name},${issue.status},${issue.message}`))
    .join('\n')

  zip.file('生成结果.csv', `\uFEFF${report}`)

  return zip.generateAsync({
    type: 'blob',
    compression: 'DEFLATE'
  })
}
