<template>
  <div
    ref="rootRef"
    class="voucher-upload"
    tabindex="0"
    @click="focusUploadArea"
    @paste="handlePaste"
  >
    <ElUpload
      ref="uploadRef"
      class="voucher-upload__control"
      drag
      multiple
      :auto-upload="false"
      :accept="accept"
      :on-change="handleFileChange"
      :on-remove="handleRemoveFile"
      list-type="picture"
    >
      <ElIcon class="voucher-upload__icon"><UploadFilled /></ElIcon>
      <div class="voucher-upload__text">将{{ voucherName }}拖到此处，或<em>点击上传</em></div>
      <template #tip>
        <div class="el-upload__tip">
          {{ tip || `支持多张图片或附件，最多上传 ${maxCount} 个文件；可粘贴文件。` }}
        </div>
      </template>
      <template #file="{ file }">
        <div class="voucher-upload__file-item">
          <img
            v-if="isUploadImage(file)"
            class="voucher-upload__file-cover"
            :src="getUploadFileUrl(file)"
            alt=""
          />
          <div v-else class="voucher-upload__file-cover voucher-upload__file-cover--default">
            <ElIcon><Document /></ElIcon>
          </div>
          <span class="voucher-upload__file-name" :title="file.name">{{ file.name }}</span>
          <button
            class="voucher-upload__file-remove"
            type="button"
            aria-label="移除文件"
            @click.stop="removeUploadFile(file)"
          >
            <ElIcon><Close /></ElIcon>
          </button>
        </div>
      </template>
    </ElUpload>
  </div>
</template>

<script setup lang="ts">
  import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
  import { ElMessage } from 'element-plus'
  import { genFileId } from 'element-plus'
  import { Close, Document, UploadFilled } from '@element-plus/icons-vue'
  import type { UploadFile, UploadInstance, UploadRawFile } from 'element-plus'
  import { StorageService } from '@/api/modules'
  import type { RefundAttachment } from '@/types/api/refund'

  interface Props {
    modelValue?: string[] | string
    voucherName?: string
    maxCount?: number
    accept?: string
    tip?: string
  }

  const props = withDefaults(defineProps<Props>(), {
    voucherName: '凭证',
    maxCount: 5,
    accept: '',
    tip: ''
  })

  const emit = defineEmits<{
    'update:modelValue': [value: string[]]
    'uploading-change': [value: boolean]
    change: [value: string[]]
    'files-change': [value: RefundAttachment[]]
  }>()

  const rootRef = ref<HTMLElement>()
  const uploadRef = ref<UploadInstance>()
  const uploadingCount = ref(0)
  const voucherFileKeyMap = new Map<number, string>()
  const voucherFileMetadataMap = new Map<number, RefundAttachment>()
  const removedUploadUids = new Set<number>()
  const fileObjectUrlMap = new Map<number, string>()
  const selectedUploadUids = new Set<number>()
  let activeUploadBatch = 0

  const getMaxCount = () => Math.max(1, props.maxCount)

  const showMaxCountWarning = () => {
    ElMessage.warning(`最多只能上传 ${getMaxCount()} 个文件`)
  }

  const emitVoucherKeys = () => {
    const keys = Array.from(voucherFileKeyMap.values())
    emit('update:modelValue', keys)
    emit('change', keys)
  }

  const emitFileMetadata = () => {
    emit('files-change', Array.from(voucherFileMetadataMap.values()))
  }

  const setUploadingCount = (count: number) => {
    uploadingCount.value = Math.max(0, count)
    emit('uploading-change', uploadingCount.value > 0)
  }

  const clearFiles = (emitValue = true) => {
    activeUploadBatch += 1
    setUploadingCount(0)
    voucherFileKeyMap.clear()
    voucherFileMetadataMap.clear()
    removedUploadUids.clear()
    selectedUploadUids.clear()
    revokeAllFileObjectUrls()
    uploadRef.value?.clearFiles()
    if (emitValue) {
      emitVoucherKeys()
      emitFileMetadata()
    }
  }

  watch(
    () => props.modelValue,
    (value) => {
      const keys = Array.isArray(value)
        ? value
        : value
          ? value
              .split(',')
              .map((key) => key.trim())
              .filter(Boolean)
          : []

      voucherFileKeyMap.clear()
      keys.slice(0, getMaxCount()).forEach((key, index) => {
        voucherFileKeyMap.set(index + 1, key)
      })
    },
    { immediate: true }
  )

  const revokeFileObjectUrl = (uid: number) => {
    const url = fileObjectUrlMap.get(uid)
    if (!url) return

    URL.revokeObjectURL(url)
    fileObjectUrlMap.delete(uid)
  }

  const revokeAllFileObjectUrls = () => {
    fileObjectUrlMap.forEach((url) => URL.revokeObjectURL(url))
    fileObjectUrlMap.clear()
  }

  const isUploadImage = (file: UploadFile) => {
    if (file.raw?.type?.startsWith('image/')) return true
    return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name)
  }

  const getUploadFileUrl = (file: UploadFile) => {
    if (file.url) return file.url
    if (!file.raw) return ''

    const cachedUrl = fileObjectUrlMap.get(file.uid)
    if (cachedUrl) return cachedUrl

    const url = URL.createObjectURL(file.raw)
    fileObjectUrlMap.set(file.uid, url)
    return url
  }

  const removeUploadFile = (uploadFile: UploadFile) => {
    uploadRef.value?.handleRemove(uploadFile)
  }

  const handleFileChange = async (uploadFile: UploadFile) => {
    const file = uploadFile.raw
    if (!file) return

    if (props.accept) {
      const accepted = props.accept.split(',').some((type) => {
        const value = type.trim().toLowerCase()
        return value.startsWith('.')
          ? file.name.toLowerCase().endsWith(value)
          : file.type.toLowerCase() === value
      })
      if (!accepted) {
        ElMessage.warning(`只能上传 ${props.accept} 格式的文件`)
        removeUploadFile(uploadFile)
        return
      }
    }

    if (!selectedUploadUids.has(uploadFile.uid)) {
      if (selectedUploadUids.size >= getMaxCount()) {
        showMaxCountWarning()
        removeUploadFile(uploadFile)
        return
      }

      selectedUploadUids.add(uploadFile.uid)
    }

    const uploadBatch = activeUploadBatch
    removedUploadUids.delete(uploadFile.uid)
    setUploadingCount(uploadingCount.value + 1)

    try {
      ElMessage.info(`正在上传${props.voucherName}...`)

      const contentType = file.type || 'application/octet-stream'
      const uploadUrlRes = await StorageService.getUploadUrl({
        file_name: file.name,
        content_type: contentType,
        purpose: 'attachment'
      })

      if (uploadUrlRes.code !== 0) {
        ElMessage.error(uploadUrlRes.msg || '获取上传地址失败')
        removeUploadFile(uploadFile)
        return
      }

      const { upload_url, file_key } = uploadUrlRes.data
      await StorageService.uploadFile(upload_url, file, contentType)

      if (uploadBatch !== activeUploadBatch || removedUploadUids.has(uploadFile.uid)) {
        return
      }

      voucherFileKeyMap.set(uploadFile.uid, file_key)
      voucherFileMetadataMap.set(uploadFile.uid, {
        file_key,
        file_name: file.name,
        file_size: file.size
      })
      emitVoucherKeys()
      emitFileMetadata()
      ElMessage.success('上传成功')
    } catch (error: any) {
      if (uploadBatch !== activeUploadBatch) return

      console.error(`上传${props.voucherName}失败:`, error)
      ElMessage.error(error?.message || '上传失败，请重试')
      voucherFileKeyMap.delete(uploadFile.uid)
      voucherFileMetadataMap.delete(uploadFile.uid)
      emitVoucherKeys()
      emitFileMetadata()
      removeUploadFile(uploadFile)
    } finally {
      if (uploadBatch === activeUploadBatch) {
        setUploadingCount(uploadingCount.value - 1)
      }
    }
  }

  const handleRemoveFile = (uploadFile: UploadFile) => {
    removedUploadUids.add(uploadFile.uid)
    selectedUploadUids.delete(uploadFile.uid)
    revokeFileObjectUrl(uploadFile.uid)
    voucherFileKeyMap.delete(uploadFile.uid)
    voucherFileMetadataMap.delete(uploadFile.uid)
    emitVoucherKeys()
    emitFileMetadata()
  }

  const focusUploadArea = () => {
    nextTick(() => rootRef.value?.focus())
  }

  const getExtensionByMimeType = (mimeType: string) => {
    const extensionMap: Record<string, string> = {
      'image/png': 'png',
      'image/jpeg': 'jpg',
      'image/gif': 'gif',
      'image/webp': 'webp',
      'application/pdf': 'pdf',
      'text/plain': 'txt'
    }
    return extensionMap[mimeType] || 'bin'
  }

  const normalizeClipboardFile = (file: File, index: number) => {
    if (file.name) return file

    const extension = getExtensionByMimeType(file.type)
    return new File([file], `clipboard-${Date.now()}-${index + 1}.${extension}`, {
      type: file.type || 'application/octet-stream'
    })
  }

  const startUploadFile = (file: File) => {
    const uploadRawFile = file as UploadRawFile
    uploadRawFile.uid = genFileId()
    uploadRef.value?.handleStart(uploadRawFile)
  }

  const getClipboardFiles = (event: ClipboardEvent) => {
    const clipboardData = event.clipboardData
    if (!clipboardData) return []

    const files = Array.from(clipboardData.files || [])
    if (files.length) return files

    return Array.from(clipboardData.items || [])
      .filter((item) => item.kind === 'file')
      .map((item) => item.getAsFile())
      .filter((file): file is File => !!file)
  }

  const handlePaste = (event: ClipboardEvent) => {
    if (event.defaultPrevented) return
    if (!isUploadVisible()) return

    const files = getClipboardFiles(event)
    if (!files.length) return

    event.preventDefault()
    const remainingCount = getMaxCount() - selectedUploadUids.size
    if (remainingCount <= 0) {
      showMaxCountWarning()
      return
    }

    if (files.length > remainingCount) {
      showMaxCountWarning()
    }

    files.slice(0, remainingCount).map(normalizeClipboardFile).forEach(startUploadFile)
  }

  const isUploadVisible = () => {
    const element = rootRef.value
    return (
      !!element &&
      !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length)
    )
  }

  onMounted(() => {
    document.addEventListener('paste', handlePaste)
  })

  onBeforeUnmount(() => {
    document.removeEventListener('paste', handlePaste)
    revokeAllFileObjectUrls()
  })

  defineExpose({
    clearFiles
  })
</script>

<style scoped lang="scss">
  .voucher-upload {
    width: 100%;
    outline: none;

    &:focus-within,
    &:focus {
      :deep(.el-upload-dragger) {
        border-color: var(--el-color-primary);
      }
    }

    :deep(.el-upload) {
      width: 100%;
    }

    :deep(.el-upload-dragger) {
      width: 100%;
      padding: 24px 16px;
      border-radius: 8px;
    }

    :deep(.el-upload-list__item) {
      height: auto;
      padding: 8px;
      line-height: normal;
    }

    &__icon {
      margin-bottom: 12px;
      font-size: 28px;
      color: var(--el-color-primary);
    }

    &__text {
      font-size: 14px;
      color: var(--el-text-color-regular);

      em {
        font-style: normal;
        color: var(--el-color-primary);
      }
    }

    &__file-item {
      display: flex;
      gap: 10px;
      align-items: center;
      width: 100%;
      min-width: 0;
    }

    &__file-cover {
      flex: 0 0 auto;
      width: 42px;
      height: 42px;
      object-fit: cover;
      border: 1px solid var(--el-border-color-light);
      border-radius: 6px;
    }

    &__file-cover--default {
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 22px;
      color: var(--el-text-color-secondary);
      background: var(--el-fill-color-light);
    }

    &__file-name {
      flex: 1 1 auto;
      min-width: 0;
      overflow: hidden;
      color: var(--el-text-color-regular);
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    &__file-remove {
      display: inline-flex;
      flex: 0 0 auto;
      align-items: center;
      justify-content: center;
      width: 24px;
      height: 24px;
      padding: 0;
      color: var(--el-text-color-secondary);
      cursor: pointer;
      background: transparent;
      border: 0;
      border-radius: 50%;

      &:hover {
        color: var(--el-color-danger);
        background: var(--el-fill-color-light);
      }
    }
  }
</style>
