export interface CompressionPreview {
  originalSize: number
  compressedSize: number
  savedBytes: number
  savedRatio: number
}

export interface CompressionResult {
  file: File
  usedCompressed: boolean
  preview: CompressionPreview | null
}

export type MediaUploadTargetFormat = 'auto' | 'avif' | 'webp'

interface ProcessedVariant {
  file: File
  preview: CompressionPreview
}

interface ProcessImageOptions {
  quality: number
  maxWidth: number
  maxHeight: number
  preferredFormats: string[]
  coverWidth?: number
  coverHeight?: number
}

function formatBytes(value: number) {
  if (!Number.isFinite(value) || value <= 0) {
    return '0 B'
  }

  const units = ['B', 'KB', 'MB', 'GB']
  let size = value
  let unit = 0

  while (size >= 1024 && unit < units.length - 1) {
    size /= 1024
    unit += 1
  }

  return `${size >= 10 || unit === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unit]}`
}

function canTransformWithCanvas(file: File) {
  return file.type.startsWith('image/') && file.type !== 'image/svg+xml' && file.type !== 'image/gif'
}

async function canvasToBlob(
  canvas: HTMLCanvasElement,
  preferredFormats: string[],
  quality: number,
) {
  for (const format of preferredFormats) {
    const blob = await new Promise<Blob | null>((resolve) => {
      canvas.toBlob(resolve, format, quality)
    })

    if (blob && blob.type === format) {
      return blob
    }
  }

  return new Promise<Blob | null>((resolve) => {
    canvas.toBlob(resolve, 'image/png')
  })
}

function extensionForMimeType(mimeType: string) {
  switch (mimeType) {
    case 'image/avif':
      return '.avif'
    case 'image/webp':
      return '.webp'
    case 'image/png':
      return '.png'
    default:
      return '.jpg'
  }
}

function deriveFileName(file: File, mimeType: string) {
  const extension = extensionForMimeType(mimeType)
  if (/\.[A-Za-z0-9]+$/.test(file.name)) {
    return file.name.replace(/\.[A-Za-z0-9]+$/, extension)
  }

  return `processed${extension}`
}

function buildPreview(originalSize: number, compressedSize: number): CompressionPreview {
  const savedBytes = originalSize - compressedSize
  const savedRatio = originalSize > 0 ? savedBytes / originalSize : 0

  return {
    originalSize,
    compressedSize,
    savedBytes,
    savedRatio,
  }
}

function formatLabelForMimeType(mimeType: string) {
  switch (mimeType) {
    case 'image/avif':
      return 'AVIF'
    case 'image/webp':
      return 'WebP'
    case 'image/png':
      return 'PNG'
    default:
      return 'JPEG'
  }
}

function defaultPreferredFormats(file: File, coverMode = false) {
  if (coverMode) {
    return ['image/avif', 'image/webp', 'image/jpeg']
  }

  if (file.type === 'image/png') {
    return ['image/png', 'image/webp', 'image/jpeg']
  }

  return ['image/webp', 'image/avif', 'image/jpeg']
}

function preferredFormatsForTarget(file: File, targetFormat: MediaUploadTargetFormat, coverMode = false) {
  switch (targetFormat) {
    case 'avif':
      return ['image/avif', 'image/webp', 'image/jpeg']
    case 'webp':
      return ['image/webp', 'image/jpeg']
    default:
      return defaultPreferredFormats(file, coverMode)
  }
}

async function buildProcessedVariants(file: File, options: ProcessImageOptions): Promise<ProcessedVariant[]> {
  const variants: ProcessedVariant[] = []
  const requestedFormats = Array.from(new Set(options.preferredFormats))

  for (const format of requestedFormats) {
    const processed = await processImage(file, {
      ...options,
      preferredFormats: [format],
    })

    if (processed.type !== format) {
      continue
    }

    if (variants.some((item) => item.file.type === processed.type)) {
      continue
    }

    variants.push({
      file: processed,
      preview: buildPreview(file.size, processed.size),
    })
  }

  return variants
}

function ensureImageChoiceDialogStyles() {
  const styleId = 'termi-image-choice-dialog-style'
  if (document.getElementById(styleId)) {
    return
  }

  const style = document.createElement('style')
  style.id = styleId
  style.textContent = `
    .termi-image-choice-overlay {
      position: fixed;
      inset: 0;
      z-index: 9999;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 24px;
      background: rgba(15, 23, 42, 0.42);
      backdrop-filter: blur(6px);
    }

    .termi-image-choice-dialog {
      width: min(680px, 100%);
      border-radius: 24px;
      border: 1px solid rgba(148, 163, 184, 0.28);
      background: rgba(255, 255, 255, 0.96);
      color: #0f172a;
      box-shadow: 0 24px 80px rgba(15, 23, 42, 0.24);
      overflow: hidden;
    }

    .termi-image-choice-header {
      padding: 20px 22px 12px;
    }

    .termi-image-choice-title {
      margin: 0;
      font-size: 18px;
      font-weight: 700;
      line-height: 1.45;
    }

    .termi-image-choice-description {
      margin: 8px 0 0;
      color: #475569;
      font-size: 14px;
      line-height: 1.7;
    }

    .termi-image-choice-body {
      padding: 0 22px 22px;
      display: grid;
      gap: 12px;
    }

    .termi-image-choice-note {
      border-radius: 16px;
      border: 1px solid rgba(59, 130, 246, 0.18);
      background: rgba(239, 246, 255, 0.92);
      color: #1d4ed8;
      padding: 12px 14px;
      font-size: 13px;
      line-height: 1.7;
    }

    .termi-image-choice-option {
      display: grid;
      gap: 8px;
      border-radius: 18px;
      border: 1px solid rgba(148, 163, 184, 0.24);
      background: #f8fafc;
      padding: 14px 16px;
      cursor: pointer;
      transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
    }

    .termi-image-choice-option:hover {
      border-color: rgba(37, 99, 235, 0.35);
      transform: translateY(-1px);
    }

    .termi-image-choice-option.is-selected {
      border-color: rgba(37, 99, 235, 0.52);
      box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
      background: rgba(239, 246, 255, 0.92);
    }

    .termi-image-choice-option-top {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
    }

    .termi-image-choice-option-label {
      display: flex;
      align-items: center;
      gap: 10px;
      font-weight: 600;
      line-height: 1.5;
    }

    .termi-image-choice-option-label input {
      margin: 0;
    }

    .termi-image-choice-badges {
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }

    .termi-image-choice-badge {
      display: inline-flex;
      align-items: center;
      border-radius: 999px;
      padding: 4px 10px;
      font-size: 12px;
      font-weight: 600;
      line-height: 1;
    }

    .termi-image-choice-badge--recommended {
      background: rgba(37, 99, 235, 0.1);
      color: #2563eb;
    }

    .termi-image-choice-badge--neutral {
      background: rgba(148, 163, 184, 0.14);
      color: #475569;
    }

    .termi-image-choice-meta {
      display: grid;
      gap: 4px;
      color: #475569;
      font-size: 13px;
      line-height: 1.65;
    }

    .termi-image-choice-actions {
      display: flex;
      justify-content: flex-end;
      gap: 10px;
      padding: 0 22px 22px;
    }

    .termi-image-choice-button {
      border: 0;
      border-radius: 999px;
      padding: 11px 18px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.18s ease, opacity 0.18s ease;
    }

    .termi-image-choice-button:hover {
      transform: translateY(-1px);
    }

    .termi-image-choice-button--ghost {
      background: rgba(148, 163, 184, 0.18);
      color: #334155;
    }

    .termi-image-choice-button--primary {
      background: linear-gradient(135deg, #2563eb, #4f46e5);
      color: #fff;
      box-shadow: 0 12px 32px rgba(37, 99, 235, 0.26);
    }
  `
  document.head.appendChild(style)
}

async function showImageChoiceDialog(options: {
  title: string
  description: string
  note?: string
  choices: Array<{
    id: string
    title: string
    meta: string[]
    badge?: string
    recommended?: boolean
  }>
  defaultChoiceId: string
  confirmLabel?: string
  cancelLabel?: string
}) {
  ensureImageChoiceDialogStyles()

  return new Promise<string>((resolve) => {
    const overlay = document.createElement('div')
    overlay.className = 'termi-image-choice-overlay'

    const dialog = document.createElement('div')
    dialog.className = 'termi-image-choice-dialog'
    dialog.setAttribute('role', 'dialog')
    dialog.setAttribute('aria-modal', 'true')
    dialog.setAttribute('aria-label', options.title)

    const header = document.createElement('div')
    header.className = 'termi-image-choice-header'
    header.innerHTML = `
      <h3 class="termi-image-choice-title"></h3>
      <p class="termi-image-choice-description"></p>
    `
    const titleEl = header.querySelector('.termi-image-choice-title')
    const descriptionEl = header.querySelector('.termi-image-choice-description')
    if (titleEl) titleEl.textContent = options.title
    if (descriptionEl) descriptionEl.textContent = options.description

    const body = document.createElement('div')
    body.className = 'termi-image-choice-body'

    if (options.note) {
      const note = document.createElement('div')
      note.className = 'termi-image-choice-note'
      note.textContent = options.note
      body.appendChild(note)
    }

    let selectedChoiceId = options.defaultChoiceId
    const optionElements: HTMLElement[] = []

    for (const choice of options.choices) {
      const option = document.createElement('label')
      option.className = 'termi-image-choice-option'
      option.dataset.choiceId = choice.id

      const top = document.createElement('div')
      top.className = 'termi-image-choice-option-top'

      const label = document.createElement('div')
      label.className = 'termi-image-choice-option-label'

      const input = document.createElement('input')
      input.type = 'radio'
      input.name = 'termi-image-choice'
      input.value = choice.id
      input.checked = choice.id === selectedChoiceId

      const text = document.createElement('span')
      text.textContent = choice.title

      label.append(input, text)

      const badges = document.createElement('div')
      badges.className = 'termi-image-choice-badges'
      if (choice.recommended) {
        const recommended = document.createElement('span')
        recommended.className = 'termi-image-choice-badge termi-image-choice-badge--recommended'
        recommended.textContent = '推荐'
        badges.appendChild(recommended)
      }
      if (choice.badge) {
        const badge = document.createElement('span')
        badge.className = 'termi-image-choice-badge termi-image-choice-badge--neutral'
        badge.textContent = choice.badge
        badges.appendChild(badge)
      }

      top.append(label, badges)

      const meta = document.createElement('div')
      meta.className = 'termi-image-choice-meta'
      for (const line of choice.meta) {
        const item = document.createElement('div')
        item.textContent = line
        meta.appendChild(item)
      }

      option.append(top, meta)
      option.addEventListener('click', () => {
        selectedChoiceId = choice.id
        optionElements.forEach((element) => {
          const checked = element.dataset.choiceId === selectedChoiceId
          element.classList.toggle('is-selected', checked)
          const radio = element.querySelector('input[type="radio"]') as HTMLInputElement | null
          if (radio) {
            radio.checked = checked
          }
        })
      })

      option.classList.toggle('is-selected', choice.id === selectedChoiceId)
      optionElements.push(option)
      body.appendChild(option)
    }

    const actions = document.createElement('div')
    actions.className = 'termi-image-choice-actions'

    const cancelButton = document.createElement('button')
    cancelButton.type = 'button'
    cancelButton.className = 'termi-image-choice-button termi-image-choice-button--ghost'
    cancelButton.textContent = options.cancelLabel ?? '保留原图'

    const confirmButton = document.createElement('button')
    confirmButton.type = 'button'
    confirmButton.className = 'termi-image-choice-button termi-image-choice-button--primary'
    confirmButton.textContent = options.confirmLabel ?? '使用所选版本'

    const cleanup = () => {
      overlay.remove()
      document.removeEventListener('keydown', handleKeyDown)
    }

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        cleanup()
        resolve('original')
      }
    }

    cancelButton.addEventListener('click', () => {
      cleanup()
      resolve('original')
    })

    confirmButton.addEventListener('click', () => {
      cleanup()
      resolve(selectedChoiceId)
    })

    overlay.addEventListener('click', (event) => {
      if (event.target === overlay) {
        cleanup()
        resolve('original')
      }
    })

    actions.append(cancelButton, confirmButton)
    dialog.append(header, body, actions)
    overlay.appendChild(dialog)
    document.body.appendChild(overlay)
    document.addEventListener('keydown', handleKeyDown)

    const defaultInput = overlay.querySelector(
      `input[value="${CSS.escape(options.defaultChoiceId)}"]`,
    ) as HTMLInputElement | null
    defaultInput?.focus()
  })
}

async function processImage(file: File, options: ProcessImageOptions): Promise<File> {
  if (!canTransformWithCanvas(file)) {
    return file
  }

  const bitmap = await createImageBitmap(file)
  const canvas = document.createElement('canvas')

  if (options.coverWidth && options.coverHeight) {
    canvas.width = options.coverWidth
    canvas.height = options.coverHeight
  } else {
    const scale = Math.min(
      options.maxWidth / bitmap.width,
      options.maxHeight / bitmap.height,
      1,
    )

    canvas.width = Math.max(1, Math.round(bitmap.width * scale))
    canvas.height = Math.max(1, Math.round(bitmap.height * scale))
  }

  const ctx = canvas.getContext('2d')
  if (!ctx) {
    return file
  }

  if (options.coverWidth && options.coverHeight) {
    const scale = Math.max(
      options.coverWidth / bitmap.width,
      options.coverHeight / bitmap.height,
    )
    const drawWidth = bitmap.width * scale
    const drawHeight = bitmap.height * scale
    const offsetX = (options.coverWidth - drawWidth) / 2
    const offsetY = (options.coverHeight - drawHeight) / 2

    ctx.drawImage(bitmap, offsetX, offsetY, drawWidth, drawHeight)
  } else {
    ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
  }

  const blob = await canvasToBlob(canvas, options.preferredFormats, options.quality)
  if (!blob) {
    return file
  }

  return new File([blob], deriveFileName(file, blob.type), {
    type: blob.type,
    lastModified: Date.now(),
  })
}

async function maybeProcessImageWithPrompt(
  file: File,
  options?: {
    quality?: number
    ask?: boolean
    minSavingsRatio?: number
    contextLabel?: string
    maxWidth?: number
    maxHeight?: number
    preferredFormats?: string[]
    coverWidth?: number
    coverHeight?: number
    forceProcessed?: boolean
  },
): Promise<CompressionResult> {
  if (!canTransformWithCanvas(file)) {
    return { file, usedCompressed: false, preview: null }
  }

  const quality = Math.min(Math.max(options?.quality ?? 0.82, 0.45), 0.95)
  const minSavingsRatio = Math.min(Math.max(options?.minSavingsRatio ?? 0.03, 0), 0.9)
  const ask = options?.ask ?? true
  const contextLabel = options?.contextLabel ?? '图片上传'
  const forceProcessed = options?.forceProcessed ?? false

  const processOptions: ProcessImageOptions = {
    quality,
    maxWidth: Math.max(options?.maxWidth ?? 2200, 320),
    maxHeight: Math.max(options?.maxHeight ?? 2200, 320),
    preferredFormats:
      options?.preferredFormats && options.preferredFormats.length
        ? options.preferredFormats
        : file.type === 'image/png'
          ? ['image/png', 'image/webp', 'image/jpeg']
          : ['image/webp', 'image/avif', 'image/jpeg'],
    coverWidth: options?.coverWidth,
    coverHeight: options?.coverHeight,
  }

  let processed: File
  try {
    processed = await processImage(file, processOptions)
  } catch {
    return { file, usedCompressed: false, preview: null }
  }

  const preview = buildPreview(file.size, processed.size)
  const { savedRatio } = preview

  if (!forceProcessed && processed.size >= file.size) {
    return { file, usedCompressed: false, preview }
  }

  if (!forceProcessed && savedRatio < minSavingsRatio) {
    return { file, usedCompressed: false, preview }
  }

  if (!ask) {
    return { file: processed, usedCompressed: true, preview }
  }

  let variants: ProcessedVariant[]
  try {
    variants = await buildProcessedVariants(file, processOptions)
  } catch {
    variants = [
      {
        file: processed,
        preview,
      },
    ]
  }

  const selectableVariants = forceProcessed
    ? variants
    : variants.filter((item) => item.file.size < file.size && item.preview.savedRatio >= minSavingsRatio)

  if (!selectableVariants.length) {
    return { file, usedCompressed: false, preview }
  }

  const recommendedVariant = selectableVariants[0]
  const missingPreferredFormats = processOptions.preferredFormats.filter(
    (format) => !variants.some((item) => item.file.type === format),
  )
  const note =
    missingPreferredFormats.length > 0
      ? `当前环境未提供 ${missingPreferredFormats.map(formatLabelForMimeType).join(' / ')} 编码能力，因此这里只展示可实际生成的格式。`
      : undefined

  const choice = await showImageChoiceDialog({
    title: forceProcessed ? `${contextLabel}：已生成规范化版本` : `${contextLabel}：检测到可压缩空间`,
    description: forceProcessed
      ? '可以直接保留原图，也可以选择更适合上传的规范化版本。'
      : '可以直接保留原图，也可以选择体积更合适的版本再上传。',
    note,
    choices: [
      {
        id: 'original',
        title: `保留原图（${file.name}）`,
        meta: [
          `当前文件: ${formatBytes(file.size)}`,
          `格式: ${formatLabelForMimeType(file.type || 'image/jpeg')}`,
        ],
        badge: '原图',
      },
      ...selectableVariants.map((item, index) => {
        const variantSavedBytes = item.preview.savedBytes
        const variantSavedRatio = item.preview.savedRatio
        return {
          id: item.file.type,
          title: `${formatLabelForMimeType(item.file.type)} 版本`,
          meta: [
            `处理后: ${formatBytes(item.file.size)}`,
            variantSavedBytes >= 0
              ? `节省: ${formatBytes(variantSavedBytes)} (${(variantSavedRatio * 100).toFixed(1)}%)`
              : `体积增加: ${formatBytes(Math.abs(variantSavedBytes))} (${Math.abs(variantSavedRatio * 100).toFixed(1)}%)`,
          ],
          badge: item.file.name.replace(/^.*(\.[A-Za-z0-9]+)$/, '$1').toLowerCase(),
          recommended: index === 0,
        }
      }),
    ],
    defaultChoiceId: recommendedVariant.file.type,
    confirmLabel: '使用所选版本',
    cancelLabel: '保留原图',
  })

  const selectedVariant = selectableVariants.find((item) => item.file.type === choice)
  const useProcessed = Boolean(selectedVariant)

  return {
    file: selectedVariant?.file ?? file,
    usedCompressed: useProcessed,
    preview: selectedVariant?.preview ?? preview,
  }
}

export async function maybeCompressImageWithPrompt(
  file: File,
  options?: {
    quality?: number
    ask?: boolean
    minSavingsRatio?: number
    contextLabel?: string
    preferredFormats?: string[]
  },
): Promise<CompressionResult> {
  return maybeProcessImageWithPrompt(file, options)
}

export async function normalizeCoverImageWithPrompt(
  file: File,
  options?: {
    quality?: number
    ask?: boolean
    contextLabel?: string
    width?: number
    height?: number
    preferredFormats?: string[]
  },
): Promise<CompressionResult> {
  return maybeProcessImageWithPrompt(file, {
    quality: options?.quality ?? 0.82,
    ask: options?.ask ?? true,
    contextLabel: options?.contextLabel ?? '封面图规范化',
    preferredFormats: options?.preferredFormats ?? ['image/avif', 'image/webp', 'image/jpeg'],
    coverWidth: Math.max(options?.width ?? 1600, 640),
    coverHeight: Math.max(options?.height ?? 900, 360),
    forceProcessed: true,
    minSavingsRatio: 0,
  })
}

export async function prepareImageForUpload(
  file: File,
  options?: {
    compress?: boolean
    quality?: number
    targetFormat?: MediaUploadTargetFormat
    contextLabel?: string
    mode?: 'image' | 'cover'
  },
): Promise<CompressionResult> {
  const compress = options?.compress ?? true
  if (!compress) {
    return { file, usedCompressed: false, preview: null }
  }

  const targetFormat = options?.targetFormat ?? 'auto'
  const mode = options?.mode ?? 'image'
  const preferredFormats = preferredFormatsForTarget(file, targetFormat, mode === 'cover')

  if (mode === 'cover') {
    return normalizeCoverImageWithPrompt(file, {
      quality: options?.quality ?? 0.82,
      ask: false,
      contextLabel: options?.contextLabel ?? '封面图规范化上传',
      preferredFormats,
    })
  }

  return maybeCompressImageWithPrompt(file, {
    quality: options?.quality ?? 0.82,
    ask: false,
    contextLabel: options?.contextLabel ?? '媒体上传',
    preferredFormats,
  })
}

export function formatCompressionPreview(preview: CompressionPreview | null) {
  if (!preview) {
    return ''
  }

  if (preview.savedBytes >= 0) {
    return `原始 ${formatBytes(preview.originalSize)} → 处理后 ${formatBytes(
      preview.compressedSize,
    )}，节省 ${(preview.savedRatio * 100).toFixed(1)}%`
  }

  return `原始 ${formatBytes(preview.originalSize)} → 处理后 ${formatBytes(
    preview.compressedSize,
  )}，体积增加 ${Math.abs(preview.savedRatio * 100).toFixed(1)}%`
}
