<template>
  <div
    class="vm-upload"
    :class="[
      `vm-upload--${type}`,
      {
        'is-disabled': disabled,
        'is-multiple': multiple,
        'is-drag': drag,
      },
    ]"
  >
    <div
      v-if="type === 'file' && isAdd"
      class="vm-upload__file-btn"
      :class="{ 'is-dragging': dragging }"
      @dragenter="onDragEnter"
      @dragover="onDragOver"
      @dragleave="onDragLeave"
      @drop="onDrop"
    >
      <label class="vm-upload__pick">
        <input
          ref="inputRef"
          type="file"
          class="vm-upload__input"
          :accept="acceptValue"
          :multiple="multiple"
          :disabled="disabled"
          @change="onInputChange"
        />
        <Button type="button" variant="softSuccess" :disabled="disabled">
          {{ text }}
        </Button>
      </label>
    </div>

    <div class="vm-upload__list">
      <div
        v-for="(item, index) in list"
        :key="item.uid"
        class="vm-upload__item"
        :style="itemStyle"
      >
        <vm-upload-item
          :item="item"
          :disabled="disabled"
          :show-tag="showTag"
          :resolve-download-url="resolveDownloadUrl"
          @remove="remove(index)"
        />
      </div>

      <!-- 拖拽只绑在触发格上：默认即 size 正方形，不是表单 span 整宽 -->
      <label
        v-if="isAdd && (type === 'image' || drag)"
        class="vm-upload__trigger"
        :class="{ 'is-drag': drag, 'is-dragging': dragging }"
        :style="drag ? undefined : itemStyle"
        @dragenter="onDragEnter"
        @dragover="onDragOver"
        @dragleave="onDragLeave"
        @drop="onDrop"
      >
        <input
          ref="inputRef"
          type="file"
          class="vm-upload__input"
          :accept="acceptValue"
          :multiple="multiple"
          :disabled="disabled"
          @change="onInputChange"
        />
        <slot>
          <div v-if="drag" class="vm-upload__drag">
            <i class="ri-upload-cloud-2-line" />
            <span>{{ crudFormat(crudLabel('uploadHint'), limitSize) }}</span>
          </div>
          <div v-else class="vm-upload__demo">
            <i class="ri-image-add-line" />
            <span v-if="text">{{ text }}</span>
          </div>
        </slot>
      </label>
    </div>
  </div>
</template>

<script setup lang="ts">
import { crudFormat, crudLabel } from '../style'
import { ref, computed, watch, reactive } from 'vue'
import { toast } from 'vue-sonner'
import { useUpload } from '../../hooks/useUpload'
import {
  compressImage,
  isCompressibleImage,
} from '../../lib/compress-image'
import { getFileType, getUrls, uploadUid } from '../../lib/upload'
import type { UploadItem } from '../../../../typings/admin/comm/upload'

defineOptions({ name: 'vm-upload' })

const props = withDefaults(
  defineProps<{
    modelValue?: string | string[]
    type?: 'image' | 'file'
    accept?: string
    multiple?: boolean
    limit?: number
    limitSize?: number
    disabled?: boolean
    text?: string
    size?: number | string | Array<number | string>
    showTag?: boolean
    drag?: boolean
    prefixPath?: string
    /** 上传前压缩图片；默认 true，传 false 关闭 */
    compress?: boolean
    beforeUpload?: (file: File) => boolean | Promise<boolean>
    /** 私有文件：预览内点「下载」时先解析可访问短链 */
    resolveDownloadUrl?: (url: string) => Promise<string | undefined>
  }>(),
  {
    modelValue: '',
    type: 'image',
    multiple: false,
    limit: 9,
    limitSize: 100,
    disabled: false,
    text: '',
    size: 120,
    showTag: true,
    drag: false,
    prefixPath: 'app/public',
    compress: true,
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: string | string[]]
  change: [v: string | string[]]
  success: [item: UploadItem]
  error: [item: UploadItem]
}>()

const { toUpload } = useUpload()
const inputRef = ref<HTMLInputElement | null>(null)
const list = ref<UploadItem[]>([])
/** 拖入高亮；用计数避免子元素 leave 误关 */
const dragging = ref(false)
let dragDepth = 0

const acceptValue = computed(() => props.accept ?? '')

const text = computed(() => {
  if (props.text) return props.text
  return props.type === 'file' ? crudLabel('selectFile') : crudLabel('selectImage')
})

const itemStyle = computed(() => {
  const raw = Array.isArray(props.size) ? props.size[0] : props.size
  const n = typeof raw === 'number' ? `${raw}px` : raw
  return { width: n, height: n }
})

const isAdd = computed(() => {
  if (props.disabled) return false
  if (props.multiple) return list.value.length < props.limit
  return list.value.length === 0
})

function normalizeValue(val: string | string[] | undefined) {
  if (Array.isArray(val)) return val.filter(Boolean)
  if (val) return [val]
  return []
}

function syncFromModel(val: string | string[] | undefined) {
  const urls = normalizeValue(val)
  if (list.value.some((item) => item.progress < 100)) return
  list.value = urls.map((url, index) => {
    const old = list.value[index]
    return {
      uid: old?.uid || uploadUid(),
      name: old?.name || url.split('/').pop() || url,
      url,
      preload: old?.url === url ? old.preload : url,
      type: getFileType(url),
      progress: 100,
      error: '',
    }
  })
}

watch(
  () => props.modelValue,
  (val) => syncFromModel(val),
  { immediate: true },
)

function emitValue() {
  const urls = getUrls(list.value.filter((item) => item.url))
  const val = props.multiple ? urls : urls[0] || ''
  emit('update:modelValue', val)
  emit('change', val)
}

function remove(index: number) {
  const item = list.value[index]
  if (item?.preload?.startsWith('blob:')) {
    URL.revokeObjectURL(item.preload)
  }
  list.value.splice(index, 1)
  emitValue()
}

async function onInputChange(e: Event) {
  const input = e.target as HTMLInputElement
  const files = Array.from(input.files ?? [])
  input.value = ''
  await ingestFiles(files)
}

function hasFilePayload(dt: DataTransfer | null) {
  return !!dt && Array.from(dt.types || []).includes('Files')
}

function onDragEnter(e: DragEvent) {
  if (props.disabled || !hasFilePayload(e.dataTransfer)) return
  e.preventDefault()
  e.stopPropagation()
  dragDepth += 1
  dragging.value = true
}

function onDragOver(e: DragEvent) {
  if (props.disabled || !hasFilePayload(e.dataTransfer)) return
  e.preventDefault()
  e.stopPropagation()
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}

function onDragLeave(e: DragEvent) {
  if (props.disabled || !hasFilePayload(e.dataTransfer)) return
  e.preventDefault()
  e.stopPropagation()
  dragDepth = Math.max(0, dragDepth - 1)
  if (dragDepth === 0) dragging.value = false
}

async function onDrop(e: DragEvent) {
  if (props.disabled) return
  e.preventDefault()
  e.stopPropagation()
  dragDepth = 0
  dragging.value = false
  const files = Array.from(e.dataTransfer?.files ?? [])
  await ingestFiles(files)
}

async function ingestFiles(files: File[]) {
  for (const file of files) {
    if (!isAdd.value) {
      toast.warning(crudFormat(crudLabel('maxUpload'), props.limit))
      break
    }
    await appendFile(file)
  }
}

async function appendFile(raw: File) {
  if (raw.size / 1024 / 1024 >= props.limitSize) {
    toast.error(crudFormat(crudLabel('fileTooLarge'), props.limitSize))
    return
  }
  if (props.beforeUpload) {
    const ok = await props.beforeUpload(raw)
    if (!ok) return
  }

  // 先入列表 + 原图预览，再压缩 / 上传（避免压缩期间空白）
  // 必须 reactive：否则改 progress 绕过 Proxy，UI 会 0% 直接跳 100%
  const item = reactive<UploadItem>({
    uid: uploadUid(),
    name: raw.name,
    url: '',
    type: getFileType(raw.name),
    size: raw.size,
    progress: 0,
    error: '',
    preload: URL.createObjectURL(raw),
  })
  if (props.multiple) list.value.push(item)
  else list.value = [item]

  const doCompress = props.compress && isCompressibleImage(raw.type)

  const setProgress = (n: number) => {
    item.progress = Math.max(0, Math.min(100, Math.floor(n)))
  }

  try {
    let file = raw
    if (doCompress) {
      file = await compressImage(raw, {
        onProgress: (p) => {
          // 压缩占整体 0–30%
          setProgress((p / 100) * 30)
        },
      })
      setProgress(30)
      item.size = file.size
      item.name = file.name
    }

    const res = await toUpload(file, {
      prefixPath: props.prefixPath,
      onProgress: (n) => {
        // 直传 OSS：有压缩则 30–100，否则 0–100（xhr.upload.onprogress）
        if (doCompress) {
          setProgress(30 + (n / 100) * 70)
        } else {
          setProgress(n)
        }
      },
    })
    item.url = res.url
    setProgress(100)
    item.error = ''
    emit('success', item)
    emitValue()
  } catch (e) {
    item.progress = -1
    item.error = e instanceof Error ? e.message : crudLabel('uploadFailed')
    emit('error', item)
  }
}

defineExpose({ list, remove })
</script>

<style lang="scss" scoped>
.vm-upload {
  width: 100%;
}

.vm-upload__input {
  display: none;
}

.vm-upload__list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.vm-upload__item,
.vm-upload__trigger {
  flex-shrink: 0;
  box-sizing: border-box;
}

.vm-upload__trigger {
  display: grid;
  place-items: center;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--muted);
  color: var(--muted-foreground);
  cursor: pointer;
  transition:
    border-color 0.15s ease,
    color 0.15s ease,
    background 0.15s ease,
    outline-color 0.15s ease;

  &:hover {
    border-color: var(--brand);
    color: var(--brand);
    background: var(--brand-soft);
  }

  /* 仅 drag 大区铺满字段宽；默认靠 itemStyle 固定正方形 */
  &.is-drag {
    width: 100%;
    min-height: 120px;
  }

  &.is-dragging {
    border-color: var(--brand);
    color: var(--brand);
    background: var(--brand-soft);
    outline: 2px dashed var(--brand);
    outline-offset: 2px;
  }
}

.vm-upload__demo,
.vm-upload__drag {
  display: grid;
  gap: 6px;
  place-items: center;
  padding: 10px;
  font-size: 12px;
  text-align: center;

  i {
    font-size: 28px;
  }
}

.vm-upload__file-btn {
  display: inline-block;
  margin-bottom: 8px;
  border-radius: 8px;
  transition:
    outline-color 0.15s ease,
    background 0.15s ease;

  &.is-dragging {
    outline: 2px dashed var(--brand);
    outline-offset: 4px;
    background: var(--brand-soft);
  }
}

.vm-upload__pick {
  display: inline-block;
  cursor: pointer;
}

.vm-upload.is-disabled {
  opacity: 0.6;
  pointer-events: none;
}
</style>
