<!-- eslint-disable no-console -->
<script setup lang="ts"
  generic="K extends string | string[] | Record<string, unknown> | Record<string, unknown>[] | null | undefined"
>
import type OSS from 'ali-oss'
import type { UploadFileInfo } from 'naive-ui'
import type { PropType, Ref } from 'vue'
import { useAutoAnimate } from '@formkit/auto-animate/vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { getAliOssTokenAxios } from '@/service/api/upload'
import { getOnlyId } from '@/utils/data'
import { browserPathJoin } from '@/utils/date'
import { fileToBase64, getFirstFrameOfVideo } from '@/utils/file'
import { initOSSClient, uploadFileToOSS } from '@/utils/oss'
import { FileUploadService } from '@/utils/upload-util'
import { isObject } from '@/utils/verify'
import { verifyAddIntRoZeroNumb } from '@/utils/verify-number'

export interface FileItem {
  /** 唯一 ID */
  __id: number | string
  /** 浏览器中的文件对象 */
  file?: File
  /** 进度 */
  percentage: number
  /** 当前 index */
  index: number
  /** 上传状态; 0:正在上传; 1:上传成功; -1:上传失败;  */
  uploadStatus: -1 | 0 | 1
  /** 错误原因  */
  errMsg?: string
  /** url */
  url: string
  /** 海报 */
  poster: string
}

export interface CustomKeys {
  /** 绑定上传文件链接的 key（即modelValue为对象或对象数组时绑定的上传文件链接的key） */
  bindFileUrlKey?: string
  /** 绑定返回海报链接的 key（即modelValue为对象或对象数组时绑定的海报链接的key） */
  bindPosterUrlKey?: string
  /** 上传成功后服务器返回链接中文件链接的 key */
  upFileUrlKey?: string
  /** 上传成功后服务器返回海报字段的的 key */
  upPosterUrlKey?: string
}

const props = defineProps({
  /** 上传图片的URL */
  path: { type: String, default: undefined },
  /** 自定义 key */
  customKeys: { type: Object as PropType<CustomKeys>, default: (): CustomKeys => ({}) },
  /** 提示文字 */
  hint: { type: String, default: '点击上传' },
  /** 图片对象数组 */
  modelValue: { type: [Array, String, Object, null, undefined] as PropType<K>, default: () => [] },
  /** 宽度 */
  width: { type: [String, Number], default: 100 },
  /** 高度 */
  height: { type: [String, Number], default: 100 },
  /** 最多上传几张图片 */
  limit: { type: Number, default: 1 },
  /** 限制可以选择的文件类型(根据后缀限制) 如：['.png', 'jpg']  */
  accept: { type: Array as PropType<`.${string}`[]>, default: () => ['.png', '.jpg'] },
  /** 加载状态 */
  loading: { type: Boolean, default: false },
  /** 选择图片完成后的回调 */
  afterSelectFile: { type: Function as PropType<(data: File[]) => File[] | Promise<File[]>>, default: undefined },
  /** 上传成功后的回调 */
  afterUpload: { type: Function as PropType<(data: OSS.MultipartUploadResult) => Record<string, unknown> | string>, default: undefined },
  /** 限制视频最大允许上传多少 kb，   */
  maxVideoSize: { type: Number, default: undefined },
  /** 限制图片最小允许上传多少 kb，默认  1  */
  minImageSize: { type: Number, default: 1 },
  /** 限制图片最大允许上传多少 kb，默认 800  */
  maxImageSize: { type: Number, default: undefined },
})
const emit = defineEmits<{
  (e: 'change', list: FileItem[]): void
  (e: 'update:loading', state: boolean): void
  (e: 'update:modelValue', list: any): void
  (e: 'error', myFile: FileItem): void
}>()

const [parent] = useAutoAnimate()
const upLoading = ref(props.loading)

// 视频预览状态
const showVideoPreview = ref(false)
const videoPreviewSrc = ref('')
const videoPreviewPoster = ref('')

const bindFileUrlKey = computed(() => props.customKeys.bindFileUrlKey || 'url')
const bindPosterUrlKey = computed(() => props.customKeys.bindPosterUrlKey || 'frameUrl')
const upFileUrlKey = computed(() => props.customKeys.upFileUrlKey || 'url')
const upPosterUrlKey = computed(() => props.customKeys.upPosterUrlKey || 'frameUrl')

const newWidth = computed(() => (verifyAddIntRoZeroNumb(`${props.width}`) || typeof props.width === 'number' ? `${props.width}px` : props.width))
const newHeight = computed(() => (verifyAddIntRoZeroNumb(`${props.height}`) || typeof props.height === 'number' ? `${props.height}px` : props.height))

/** 全部的文件（包含上传和未上传的） */
const fileList: Ref<FileItem[]> = ref(getDefFiles(props.modelValue))
/** 得到上传成功的文件（包含本次上传成功和之前上传成功的） */
const uploadedList = shallowRef<Record<string, string>[] | string[]>(getUploadedList())
/** 最终需要得到文件（根据是多选还是单选整理成需要的文件） */
const uploadedFiles = computed(() => (props.limit > 1 ? (uploadedList.value as K) : (uploadedList.value[0] as K)))
/** 现在还可以添加多少个 */
const efficientNumb = computed(() => props.limit - uploadedList.value.length - fileList.value.length)

// NUpload 文件列表，用于选择后清除
const nUploadFileList = ref<UploadFileInfo[]>([])

watch(
  () => props.modelValue,
  (val) => {
    fileList.value = getDefFiles(val)
  },
)
watch(
  () => upLoading.value,
  (val) => {
    emit('update:loading', val)
  },
)

watch(
  () => uploadedFiles.value,
  (val) => {
    emit('update:modelValue', val)
  },
)

/**
 * 视频预览
 */
function previewVideoClick(src: string, poster: string) {
  if (src) {
    videoPreviewSrc.value = src
    videoPreviewPoster.value = poster
    showVideoPreview.value = true
  }
}

/**
 * 删除 (已经上传的)
 */
function deleteClick(index: number) {
  fileList.value.splice(index, 1)
  emit('change', fileList.value)
  uploadedList.value = getUploadedList()
}
/**
 * 重新上传
 */
async function reUploadClick(index: number) {
  try {
    upLoading.value = true
    await uploadFile(fileList.value[index]!)
    upLoading.value = false
    emit('change', fileList.value)
    uploadedList.value = getUploadedList()
  }
  catch (error) {
    upLoading.value = false
    uploadedList.value = getUploadedList()
    // eslint-disable-next-line no-console
    console.log('重新上传失败====', error)
  }
}

/**
 * 重置
 */
function reset() {
  fileList.value = []
  uploadedList.value = []
}

/**
 *  上传文件
 */
function handleUploadChange(options: { fileList: UploadFileInfo[] }) {
  // 获取新添加的文件
  const newFiles = options.fileList.map(item => item.file).filter(Boolean) as File[]

  // 清除 NUpload 列表以避免重复并允许重新选择
  nUploadFileList.value = []

  if (newFiles.length > 0) {
    uploadFileList(newFiles)
  }
}

/**
 * 处理上传所有图片
 */
async function uploadFileList(files: File[] | null) {
  try {
    if (files && files.length > 0) {
      let processFiles = files
      if (props.afterSelectFile) {
        processFiles = await props.afterSelectFile(files)
      }

      const promiseArr: FileItem[] = []
      // 使用临时列表以避免循环期间 fileList 发生变化的问题（尽管 push 是安全的）
      // 但我们需要将其推入 fileList 以立即显示它们
      for (let i = 0, len = processFiles.length; i < len; i++) {
        if (efficientNumb.value > 0 && processFiles[i]) {
          const newItem: FileItem = {
            file: processFiles[i]!,
            percentage: 0,
            uploadStatus: 0,
            index: fileList.value.length,
            __id: getOnlyId(),
            url: '',
            poster: '',
          }
          fileList.value.push(newItem)
          // 必须获取响应式对象，否则进度条不会更新
          promiseArr.push(fileList.value[fileList.value.length - 1])
        }
      }

      if (promiseArr.length > 0) {
        upLoading.value = true
        const uploadService = new FileUploadService(uploadFile, 5)
        await uploadService.upload(promiseArr)
        upLoading.value = false
        emit('change', fileList.value)
        uploadedList.value = getUploadedList()
      }
      else {
        window.$message?.warning('暂无可上传文件或已达上限')
      }
    }
  }
  catch (error) {
    uploadedList.value = getUploadedList()
    window.$message?.error('上传失败')
    console.error(error)
  }
}

/**
 * 上传文件
 */
async function uploadFile(myFile: FileItem): Promise<FileItem> {
  try {
    const fileSize = myFile.file!.size / 1024
    if (myFile.file!.type.includes('image')) {
      if (typeof props.maxImageSize === 'number' && fileSize > props.maxImageSize) {
        const msg = getRestrictSizeMsg(props.maxImageSize, 'max')
        myFile.uploadStatus = -1
        myFile.errMsg = msg
        return Promise.reject(new Error(msg))
      }
      else if (fileSize < props.minImageSize) {
        const msg = getRestrictSizeMsg(props.minImageSize, 'min')
        myFile.uploadStatus = -1
        myFile.errMsg = msg
        return Promise.reject(new Error(msg))
      }
    }
    if (typeof props.maxVideoSize === 'number' && myFile.file!.type.includes('video') && fileSize > props.maxVideoSize) {
      const msg = getRestrictSizeMsg(props.maxVideoSize, 'max')
      myFile.uploadStatus = -1
      myFile.errMsg = msg
      return Promise.reject(new Error(msg))
    }
    const { data: tokenData, error: tokenError } = await getAliOssTokenAxios()
    if (tokenError || !tokenData) {
      throw new Error('获取上传凭证失败')
    }
    // 初始化OSS客户端
    const client = initOSSClient(tokenData?.data || {})
    let path = typeof props.path === 'string' ? '' : `temp/${Date.now()}`
    path = browserPathJoin(path, myFile.file!.name)
    // 上传文件
    const res: OSS.MultipartUploadResult = await uploadFileToOSS(client, myFile.file!, path, (progress) => {
      myFile.percentage = Math.floor((progress || 0) * 10000) / 100
    })
    const url = browserPathJoin(import.meta.env.VITE_BASE_OSS_URL, path)
    const data = typeof props.afterUpload === 'function' ? props.afterUpload(res) : res
    if (isObject(data)) {
      const newData = data as Record<string, string>
      myFile.url = upFileUrlKey.value in newData ? String(newData[upFileUrlKey.value] || url) : url
      myFile.poster = upPosterUrlKey.value in newData ? String(newData[upPosterUrlKey.value] || '') : ''
      if (myFile.file!.type.includes('video') && !myFile.poster) {
        try {
          const info = await getFirstFrameOfVideo(myFile.file!)
          myFile.poster = (await fileToBase64(info.firstFrame)).img || ''
        }
        catch (error) {
          // eslint-disable-next-line no-console
          console.log('error====', error)
        }
      }
    }
    else {
      myFile.url = url
    }
    myFile.uploadStatus = 1
    return Promise.resolve(myFile)
  }
  catch (error: any) {
    console.log('上传错误====', error)
    myFile.uploadStatus = -1
    myFile.errMsg = typeof error.msg === 'string' ? error.msg : '上传失败'
    emit('error', error)
    return Promise.reject(new Error(myFile.errMsg))
  }
}

/** 得到初始已上传的文件 */
function getDefFiles(data: unknown): FileItem[] {
  const val: unknown[] = Array.isArray(data) ? data : data ? [data] : []
  const newVal: FileItem[] = val.map((item, index: number) => {
    const _item: FileItem = isObject(item)
      ? {
          url: String(item[bindFileUrlKey.value] || ''),
          poster: String(item[bindPosterUrlKey.value] || ''),
          uploadStatus: 1,
          index,
          __id: getOnlyId(),
          percentage: 0,
        }
      : { url: String(item), poster: '', uploadStatus: 1, index, __id: getOnlyId(), percentage: 0 }
    return _item
  })
  return newVal
}
/** 得到已上传成功的文件 */
function getUploadedList() {
  const list = fileList.value.filter(item => item.uploadStatus === 1 && item.url)
  // 不能用 bindFileUrlKey 代替
  if (props.customKeys.bindFileUrlKey) {
    return list.map((item) => {
      const obj: Record<string, string> = {}
      obj[bindFileUrlKey.value] = item.url
      obj[bindPosterUrlKey.value] = item.poster
      return obj
    })
  }
  else {
    return list.map(item => item.url)
  }
}
/** 得到不符合大小限制的提示语 */
function getRestrictSizeMsg(maxSize: number, type: 'max' | 'min') {
  const maxSizeText = maxSize >= 1024 ? `${Number((maxSize / 1024).toFixed(2))}M` : `${maxSize}KB`
  return type === 'max' ? `文件大小不能超过${maxSizeText}` : `文件大小不能小于${maxSizeText}`
}

defineExpose({ reset, getLoadState: () => upLoading.value })
</script>

<template>
  <div class="rest-upload-container">
    <NImageGroup>
      <div ref="parent" class="flex flex-wrap gap-4">
        <!-- 文件列表 -->
        <div
          v-for="(item, index) in fileList" :key="item.__id"
          class="group relative overflow-hidden border border-gray-200 rounded-lg transition-all duration-300 hover:border-primary"
          :style="{ width: newWidth, height: newHeight }"
        >
          <!-- 图片预览 -->
          <div
            v-if="!item.url.includes('.mp4') && !item.url.includes('.webm') && (item.file?.type?.includes('image') || !item.file)"
            class="h-full w-full flex items-center justify-center bg-gray-50"
          >
            <NImage
              :src="item.url" class="h-full w-full flex items-center justify-center"
              :img-props="{ style: { width: '100%', height: '100%', objectFit: 'contain' } }" object-fit="cover"
            >
              <template #error>
                <div class="h-full w-full flex items-center justify-center bg-gray-50 text-gray-400">
                  <SvgIcon icon="mdi:image-broken-variant" class="text-2xl" />
                </div>
              </template>
            </NImage>
          </div>

          <!-- 视频预览 -->
          <div
            v-else class="h-full w-full flex items-center justify-center bg-black bg-cover bg-center bg-no-repeat"
            :style="{ backgroundImage: item.poster ? `url(${item.poster})` : undefined }"
          >
            <SvgIcon icon="mdi:play-circle-outline" class="text-4xl text-white opacity-80" />
          </div>

          <!-- 进度覆盖层 -->
          <div
            v-if="item.uploadStatus === 0"
            class="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/50 p-1"
          >
            <NProgress
              type="circle" :percentage="item.percentage" :color="{ stops: ['#E3F2FD', '#2080f0'] }"
              style="width: 60px; height: 60px"
            >
              <template #default>
                <span class="text-xs text-white">{{ item.percentage }}%</span>
              </template>
            </NProgress>
          </div>

          <!-- 错误覆盖层 -->
          <div
            v-if="item.uploadStatus === -1"
            class="absolute inset-0 z-20 flex flex-col items-center justify-center bg-red-50/90 p-2 text-center"
          >
            <span class="mb-1 text-xs text-red-500">{{ item.errMsg || '上传失败' }}</span>
            <NButton size="tiny" type="error" ghost @click="reUploadClick(index)">
              重试
            </NButton>
            <div class="absolute right-1 top-1 cursor-pointer" @click="deleteClick(index)">
              <SvgIcon icon="mdi:close" class="text-red-500" />
            </div>
          </div>

          <!-- 操作覆盖层（悬停） -->
          <div
            v-if="item.uploadStatus === 1"
            class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center gap-2 bg-black/0 opacity-0 transition-all duration-300 group-hover:bg-black/50 group-hover:opacity-100"
          >
            <NButton
              v-if="item.url.includes('.mp4') || item.url.includes('.webm') || item.file?.type?.includes('video')"
              circle secondary type="info" class="pointer-events-auto"
              @click="previewVideoClick(item.url, item.poster)"
            >
              <template #icon>
                <SvgIcon icon="mdi:eye" />
              </template>
            </NButton>
            <NButton v-else circle secondary type="info" class="pointer-events-none">
              <template #icon>
                <SvgIcon icon="mdi:eye" />
              </template>
            </NButton>

            <NButton circle secondary type="error" class="pointer-events-auto" @click="deleteClick(index)">
              <template #icon>
                <SvgIcon icon="mdi:delete" />
              </template>
            </NButton>
          </div>
        </div>

        <!-- 上传触发器 -->
        <div v-if="efficientNumb > 0" :style="{ width: newWidth, height: newHeight }">
          <NUpload
            :accept="accept.join(',')" :multiple="limit > 1" :max="limit" :show-file-list="false"
            :file-list="nUploadFileList" :default-upload="false" class="h-full w-full" @change="handleUploadChange"
          >
            <template #default>
              <div
                class="h-full w-full flex flex-col cursor-pointer items-center justify-center border border-gray-300 rounded-lg border-dashed bg-gray-50 transition-colors hover:border-primary hover:bg-primary-50"
                :style="{ width: newWidth, height: newHeight }"
              >
                <SvgIcon icon="mdi:plus" class="text-3xl text-gray-400" />
                <span class="mt-1 text-xs text-gray-400">{{ hint }}</span>
              </div>
            </template>
          </NUpload>
        </div>
      </div>
    </NImageGroup>

    <!-- 视频预览弹窗 -->
    <NModal v-model:show="showVideoPreview" preset="card" title="视频预览" class="max-w-full w-[800px]">
      <div class="flex items-center justify-center rounded-lg bg-black/5 p-4">
        <video
          v-if="videoPreviewSrc" :src="videoPreviewSrc" :poster="videoPreviewPoster" controls
          class="max-h-[600px] max-w-full w-auto"
        />
      </div>
    </NModal>
  </div>
</template>

<style scoped>
.rest-upload-container {
  /* 确保容器不会塌陷 */
  min-height: v-bind(newHeight);
}
</style>
