<template>
  <template v-if="!embedded">
    <div v-if="src && showThumb" class="vm-preview-viewer" :class="{ 'is-video': isVideoKind }">
      <button
        type="button"
        class="vm-preview-viewer__trigger"
        :style="thumbStyle"
        :disabled="!preview"
        :title="preview ? (isVideoKind ? crudLabel('playVideo') : crudLabel('viewImage')) : undefined"
        @click="openViewer"
      >
        <img
          v-if="!isVideoKind"
          class="vm-preview-viewer__media"
          :src="src"
          alt=""
          :style="mediaStyle"
        />
        <template v-else>
          <img
            v-if="videoFrameSrc"
            class="vm-preview-viewer__media"
            :src="videoFrameSrc"
            alt=""
            :style="mediaStyle"
          />
          <video
            v-else-if="frameFailed"
            class="vm-preview-viewer__media"
            :src="src"
            muted
            playsinline
            preload="metadata"
            :style="mediaStyle"
          />
          <span v-else class="vm-preview-viewer__loading" aria-hidden="true" />
          <span class="vm-preview-viewer__play" aria-hidden="true">
            <i class="ri-play-fill" />
          </span>
        </template>
      </button>
    </div>
    <div v-else-if="src && showButton" class="vm-preview-viewer">
      <button
        type="button"
        class="vm-preview-viewer__btn"
        :disabled="!preview"
        :title="preview ? triggerTitle : undefined"
        @click="openViewer"
      >
        <i :class="triggerIcon" aria-hidden="true" />
        <span>{{ displayLabel }}</span>
      </button>
    </div>
    <span v-else-if="!src" class="vm-preview-viewer is-empty">—</span>
  </template>

  <Teleport to="body">
    <div
      v-if="viewerOpen"
      class="vm-file-preview-viewer vm-dialog-float"
      role="dialog"
      aria-modal="true"
      @click.self="closeViewer"
    >
      <header class="vm-file-preview-viewer__head">
        <div class="vm-file-preview-viewer__meta">
          <span class="vm-file-preview-viewer__name">{{ displayName }}</span>
          <span class="vm-file-preview-viewer__tag">{{ kindLabel }}</span>
        </div>
        <div class="vm-file-preview-viewer__actions">
          <button
            type="button"
            class="vm-file-preview-viewer__link"
            :disabled="opening"
            @click="openInNewWindow"
          >
            {{ opening ? crudLabel('opening') : crudLabel('openNewWindow') }}
          </button>
          <button type="button" class="vm-file-preview-viewer__close" :title="crudLabel('dismiss')" @click="closeViewer">
            <i class="ri-close-line" />
          </button>
        </div>
      </header>

      <div
        class="vm-file-preview-viewer__body vm-scroll"
        :class="{ 'is-media': kind === 'image' || kind === 'video' }"
      >
        <p v-if="loading" class="vm-file-preview-viewer__hint">{{ crudLabel('loading') }}</p>
        <p v-else-if="error" class="vm-file-preview-viewer__hint is-error">{{ error }}</p>

        <template v-else-if="kind === 'image'">
          <div class="vm-file-preview-viewer__image-stage" @wheel.prevent="onWheel">
            <img
              class="vm-file-preview-viewer__image"
              :src="src"
              :alt="displayName"
              :style="viewerImgStyle"
              draggable="false"
              @load="resetView"
            />
          </div>
          <div class="vm-file-preview-viewer__toolbar" @click.stop>
            <button type="button" :title="crudLabel('zoomOut')" @click="zoomOut">
              <i class="ri-subtract-line" />
            </button>
            <button type="button" :title="crudLabel('zoomIn')" @click="zoomIn">
              <i class="ri-add-line" />
            </button>
            <button type="button" :title="crudLabel('actualSize')" @click="resetView">
              <i class="ri-aspect-ratio-line" />
            </button>
            <button type="button" :title="crudLabel('rotateCCW')" @click="rotateLeft">
              <i class="ri-anticlockwise-line" />
            </button>
            <button type="button" :title="crudLabel('rotateCW')" @click="rotateRight">
              <i class="ri-clockwise-line" />
            </button>
          </div>
        </template>

        <div v-else-if="kind === 'video'" class="vm-file-preview-viewer__video-wrap">
          <video
            ref="videoRef"
            class="vm-file-preview-viewer__video"
            :src="src"
            controls
            autoplay
            playsinline
          />
        </div>

        <audio
          v-else-if="kind === 'audio'"
          ref="audioRef"
          class="vm-file-preview-viewer__audio"
          :src="src"
          controls
          autoplay
          playsinline
        />

        <iframe
          v-else-if="kind === 'pdf'"
          class="vm-file-preview-viewer__iframe"
          :src="src"
          :title="crudLabel('pdfPreview')"
        />

        <div
          v-else-if="kind === 'excel' && excelHtml"
          class="vm-file-preview-viewer__excel"
          v-html="excelHtml"
        />

        <pre v-else-if="kind === 'text' && textContent" class="vm-file-preview-viewer__text">{{
          textContent
        }}</pre>

        <div v-else-if="kind === 'office'" class="vm-file-preview-viewer__fallback">
          <i class="ri-file-3-line" aria-hidden="true" />
          <p>{{ crudLabel('officeNoPreview') }}</p>
          <p>{{ crudLabel('officeDownloadHint') }}</p>
          <button
            type="button"
            class="vm-file-preview-viewer__dl-btn"
            :disabled="downloading"
            @click="downloadFile"
          >
            {{ downloading ? crudLabel('downloadingFile') : crudLabel('downloadFile') }}
          </button>
        </div>

        <div v-else class="vm-file-preview-viewer__fallback">
          <i class="ri-file-3-line" aria-hidden="true" />
          <p>{{ crudLabel('typeNoPreview') }}</p>
          <p>{{ crudLabel('downloadHint') }}</p>
          <button
            type="button"
            class="vm-file-preview-viewer__dl-btn"
            :disabled="downloading"
            @click="downloadFile"
          >
            {{ downloading ? crudLabel('downloadingFile') : crudLabel('downloadFile') }}
          </button>
        </div>
      </div>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import { crudLabel } from '../style'
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import DOMPurify from 'dompurify'
import { useVideoFrame } from '../../lib/video-frame'
import {
  fileDisplayName,
  getMediaPreviewKind,
  loadExcelPreviewHtml,
  fetchFileText,
  type MediaPreviewKind,
} from '../../lib/file-preview'

/** 多根（缩略图 + Teleport）：禁止 fallthrough，避免表格传入的 scope/column/options 告警 */
defineOptions({ name: 'vm-preview-viewer', inheritAttrs: false })

const VIDEO_EXT = /\.(mp4|webm|mov|m4v|ogv|ogg)(\?|#|$)/i

const KIND_LABEL: Record<MediaPreviewKind, string> = {
  image: 'typeImage',
  video: 'typeVideo',
  audio: 'typeAudio',
  pdf: 'PDF',
  excel: 'Excel',
  text: 'typeText',
  office: 'Office',
  unsupported: 'typeFile',
}

const props = withDefaults(
  defineProps<{
    /** CRUD 列绑定的 URL */
    modelValue?: string | null
    /** 表格列透传（vm-table），本组件不使用 */
    scope?: Record<string, unknown>
    column?: { property?: string; prop?: string }
    options?: unknown
    /** 仅渲染浮层（vm-upload-item 内部） */
    embedded?: boolean
    /** embedded 时外部控制开关 */
    open?: boolean
    /** embedded 时可显式传 src */
    src?: string
    fileName?: string
    /** 按钮触发文案（PDF / Excel 等非缩略图类型） */
    label?: string
    /** 覆盖 URL 推断的媒体类型 */
    mediaKind?: MediaPreviewKind
    /** thumb | button | auto：触发方式 */
    trigger?: 'thumb' | 'button' | 'auto'
    /** image | video | audio | auto（缩略图列或类型覆盖） */
    type?: 'image' | 'video' | 'audio' | 'auto'
    size?: number | string
    radius?: number | string
    preview?: boolean
    fit?: 'cover' | 'contain'
    /** 私有文件：点「下载」时先解析可访问短链 */
    resolveDownloadUrl?: (url: string) => Promise<string | undefined>
  }>(),
  {
    embedded: false,
    label: undefined,
    trigger: 'auto',
    type: 'auto',
    size: 40,
    radius: 8,
    preview: true,
    fit: 'cover',
  },
)

const emit = defineEmits<{ 'update:open': [v: boolean] }>()

const internalOpen = ref(false)
const loading = ref(false)
const downloading = ref(false)
const opening = ref(false)
const error = ref('')
const textContent = ref('')
const excelHtml = ref('')
const scale = ref(1)
const rotate = ref(0)
const videoRef = ref<HTMLVideoElement | null>(null)
const audioRef = ref<HTMLAudioElement | null>(null)

const src = computed(() => {
  const raw = props.embedded ? props.src : props.modelValue
  if (raw == null) return ''
  return String(raw).trim()
})

const isVideoKind = computed(() => {
  if (props.mediaKind === 'video') return true
  if (props.mediaKind === 'image' || props.mediaKind === 'audio') return false
  if (props.type === 'video') return true
  if (props.type === 'image' || props.type === 'audio') return false
  return VIDEO_EXT.test(src.value)
})

const isAudioKind = computed(() => {
  if (props.mediaKind === 'audio') return true
  if (props.type === 'audio') return true
  if (props.mediaKind || props.type === 'image' || props.type === 'video') return false
  return getMediaPreviewKind(props.fileName || src.value) === 'audio'
})

const showThumb = computed(() => {
  if (props.embedded) return false
  if (props.trigger === 'button') return false
  if (props.trigger === 'thumb') return !!src.value
  if (props.mediaKind === 'image' || props.mediaKind === 'video') return true
  if (props.mediaKind) return false
  if (props.type === 'image' || props.type === 'video') return true
  if (isVideoKind.value) return true
  const k = getMediaPreviewKind(props.fileName || src.value)
  return k === 'image' || k === 'video'
})

const showButton = computed(() => {
  if (props.embedded || !src.value) return false
  if (props.trigger === 'button') return true
  if (props.trigger === 'thumb') return false
  return !showThumb.value
})

const kind = computed((): MediaPreviewKind => {
  if (props.mediaKind) return props.mediaKind
  if (props.type === 'audio' || isAudioKind.value) return 'audio'
  if (props.type === 'video' || isVideoKind.value) return 'video'
  if (props.type === 'image') return 'image'
  return getMediaPreviewKind(props.fileName || src.value)
})

const displayLabel = computed(() => {
  if (props.label != null && props.label !== '') return props.label
  if (kind.value === 'audio') return crudLabel('play')
  return crudLabel('view')
})

const triggerIcon = computed(() =>
  kind.value === 'audio' ? 'ri-play-circle-line' : 'ri-file-search-line',
)

const triggerTitle = computed(() =>
  kind.value === 'audio' ? crudLabel('playAudio') : crudLabel('viewFile'),
)

const kindLabel = computed(() => {
  const key = KIND_LABEL[kind.value]
  return key.startsWith('type') ? crudLabel(key) : key
})

const displayName = computed(() =>
  fileDisplayName(props.fileName || src.value, crudLabel('unnamedFile')),
)

/** 无法内嵌预览的类型在正文区提供「下载文件」；顶栏统一「新窗口打开」 */

async function downloadFile() {
  if (downloading.value) return
  const raw = src.value
  if (!raw) return
  downloading.value = true
  try {
    let url = raw
    if (props.resolveDownloadUrl) {
      const signed = await props.resolveDownloadUrl(raw)
      if (!signed) return
      url = signed
    }
    const a = document.createElement('a')
    a.href = url
    a.download = displayName.value || 'download'
    a.rel = 'noopener'
    document.body.appendChild(a)
    a.click()
    a.remove()
  } finally {
    downloading.value = false
  }
}

async function openInNewWindow() {
  if (opening.value) return
  const raw = src.value
  if (!raw) return
  opening.value = true
  try {
    let url = raw
    if (props.resolveDownloadUrl) {
      const signed = await props.resolveDownloadUrl(raw)
      if (!signed) return
      url = signed
    }
    window.open(url, '_blank', 'noopener,noreferrer')
  } finally {
    opening.value = false
  }
}

const viewerOpen = computed({
  get: () => (props.embedded ? !!props.open : internalOpen.value),
  set: (v: boolean) => {
    if (props.embedded) emit('update:open', v)
    else internalOpen.value = v
  },
})

const videoThumbSource = computed(() => (isVideoKind.value ? src.value : ''))
const { videoFrameSrc, frameFailed } = useVideoFrame(videoThumbSource)

const viewerImgStyle = computed(() => ({
  transform: `scale(${scale.value}) rotate(${rotate.value}deg)`,
}))

function toCssUnit(v: number | string) {
  return typeof v === 'number' ? `${v}px` : v
}

const thumbStyle = computed(() => ({
  width: toCssUnit(props.size),
  height: toCssUnit(props.size),
  borderRadius: toCssUnit(props.radius),
}))

const mediaStyle = computed(() => ({
  objectFit: props.fit,
}))

function resetView() {
  scale.value = 1
  rotate.value = 0
}

function zoomIn() {
  scale.value = Math.min(5, +(scale.value + 0.2).toFixed(2))
}

function zoomOut() {
  scale.value = Math.max(0.2, +(scale.value - 0.2).toFixed(2))
}

function rotateLeft() {
  rotate.value -= 90
}

function rotateRight() {
  rotate.value += 90
}

function onWheel(e: WheelEvent) {
  if (kind.value !== 'image') return
  if (e.deltaY < 0) zoomIn()
  else zoomOut()
}

function lockBody(lock: boolean) {
  if (typeof document === 'undefined') return
  document.body.style.overflow = lock ? 'hidden' : ''
}

async function loadPreview() {
  if (!src.value) return
  loading.value = true
  error.value = ''
  textContent.value = ''
  excelHtml.value = ''

  try {
    if (kind.value === 'text') {
      textContent.value = await fetchFileText(src.value)
    } else if (kind.value === 'excel') {
      excelHtml.value = DOMPurify.sanitize(await loadExcelPreviewHtml(src.value), {
        USE_PROFILES: { html: true },
      })
    }
  } catch (e) {
    error.value = e instanceof Error ? e.message : crudLabel('previewFail')
  } finally {
    loading.value = false
  }
}

function openViewer() {
  if (!props.preview || !src.value) return
  viewerOpen.value = true
}

function closeViewer() {
  viewerOpen.value = false
}

function onKeydown(e: KeyboardEvent) {
  if (!viewerOpen.value) return
  if (e.key !== 'Escape') return
  e.preventDefault()
  e.stopPropagation()
  closeViewer()
}

watch(viewerOpen, (open) => {
  lockBody(open)
  if (typeof window === 'undefined') return
  if (open) {
    resetView()
    // 捕获阶段拦截 Escape，避免同时关掉宿主 Dialog
    window.addEventListener('keydown', onKeydown, true)
    if (kind.value === 'text' || kind.value === 'excel') void loadPreview()
    if (kind.value === 'video') {
      void nextTick(() => {
        videoRef.value?.play?.().catch(() => undefined)
      })
    }
    if (kind.value === 'audio') {
      void nextTick(() => {
        audioRef.value?.play?.().catch(() => undefined)
      })
    }
  } else {
    window.removeEventListener('keydown', onKeydown, true)
    loading.value = false
    error.value = ''
    textContent.value = ''
    excelHtml.value = ''
    videoRef.value?.pause?.()
    audioRef.value?.pause?.()
  }
})

onUnmounted(() => {
  lockBody(false)
  if (typeof window !== 'undefined') {
    window.removeEventListener('keydown', onKeydown, true)
  }
})
</script>

<style lang="scss" scoped>
.vm-preview-viewer {
  display: inline-flex;
  vertical-align: middle;
}

.vm-preview-viewer.is-empty {
  color: var(--muted-foreground);
  font-size: 12px;
}

.vm-preview-viewer__trigger {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
  background: var(--muted);
  cursor: pointer;
  overflow: hidden;
  transition:
    border-color 0.15s ease,
    box-shadow 0.15s ease;

  &:hover:not(:disabled) {
    border-color: var(--focus-border);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 12%, transparent);
  }

  &:disabled {
    cursor: default;
  }
}

.vm-preview-viewer__media {
  display: block;
  width: 100%;
  height: 100%;
}

.vm-preview-viewer__loading {
  display: block;
  width: 100%;
  height: 100%;
  background: color-mix(in srgb, var(--muted) 70%, var(--foreground) 8%);
}

.vm-preview-viewer__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: rgb(20 24 40 / 28%);
  color: #fff;
  pointer-events: none;

  i {
    font-size: 18px;
    line-height: 1;
  }
}

.vm-preview-viewer__btn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--brand);
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;

  i {
    font-size: 16px;
    line-height: 1;
  }

  &:hover:not(:disabled) {
    color: var(--brand-deep);
  }

  &:disabled {
    opacity: 0.55;
    cursor: default;
  }
}
</style>

<style lang="scss">
.vm-file-preview-viewer {
  position: fixed;
  inset: 0;
  z-index: 2200;
  display: flex;
  flex-direction: column;
  background: rgb(10 12 20 / 88%);
}

.vm-file-preview-viewer__head {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 18px;
  border-bottom: 1px solid rgb(255 255 255 / 10%);
  background: rgb(24 28 40 / 92%);
}

.vm-file-preview-viewer__meta {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 10px;
}

.vm-file-preview-viewer__name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 14px;
  font-weight: 650;
  color: #fff;
}

.vm-file-preview-viewer__tag {
  flex-shrink: 0;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgb(255 255 255 / 12%);
  color: rgb(255 255 255 / 88%);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
}

.vm-file-preview-viewer__actions {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 10px;
}

.vm-file-preview-viewer__link {
  appearance: none;
  border: 0;
  padding: 0;
  background: transparent;
  color: rgb(147 197 253);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;

  &:hover {
    text-decoration: underline;
  }
}

.vm-file-preview-viewer__dl-btn {
  margin-top: 12px;
  padding: 8px 16px;
  border: 0;
  border-radius: 8px;
  background: var(--brand, #4e5dff);
  color: #fff;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

.vm-file-preview-viewer__close {
  display: inline-flex;
  width: 34px;
  height: 34px;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 10px;
  background: rgb(255 255 255 / 12%);
  color: #fff;
  font-size: 18px;
  cursor: pointer;

  &:hover {
    background: rgb(255 255 255 / 22%);
  }
}

.vm-file-preview-viewer__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 16px;
  overflow: auto;

  &.is-media {
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
  }
}

.vm-file-preview-viewer__hint {
  margin: 24px auto;
  color: rgb(255 255 255 / 80%);
  text-align: center;

  &.is-error {
    color: #fca5a5;
  }
}

.vm-file-preview-viewer__image-stage {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.vm-file-preview-viewer__image {
  display: block;
  width: auto;
  height: auto;
  max-width: min(1200px, calc(100vw - 48px));
  max-height: calc(100vh - 140px);
  object-fit: contain;
  transition: transform 0.15s ease;
  user-select: none;
}

.vm-file-preview-viewer__toolbar {
  position: relative;
  z-index: 2;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  gap: 4px;
  margin: 0 auto 20px;
  padding: 6px 8px;
  border-radius: 12px;
  background: rgb(24 28 40 / 92%);

  button {
    display: inline-flex;
    width: 36px;
    height: 36px;
    align-items: center;
    justify-content: center;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: rgb(255 255 255 / 88%);
    font-size: 18px;
    cursor: pointer;

    &:hover {
      background: rgb(255 255 255 / 12%);
      color: #fff;
    }
  }
}

.vm-file-preview-viewer__video-wrap {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  align-items: center;
  justify-content: center;
  padding: 16px;
}

.vm-file-preview-viewer__video {
  display: block;
  width: min(960px, calc(100vw - 48px));
  max-height: calc(100vh - 120px);
  border-radius: 12px;
  background: #000;
  outline: none;
}

.vm-file-preview-viewer__audio {
  display: block;
  width: min(480px, 100%);
  margin: 48px auto 0;
}

.vm-file-preview-viewer__iframe {
  display: block;
  width: min(1100px, 100%);
  height: calc(100vh - 88px);
  margin: 0 auto;
  border: none;
  border-radius: 12px;
  background: #fff;
}

.vm-file-preview-viewer__excel {
  max-width: min(1100px, 100%);
  margin: 0 auto;
  padding: 12px;
  border-radius: 12px;
  background: #fff;
  overflow: auto;

  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
  }

  td,
  th {
    padding: 6px 8px;
    border: 1px solid #e5e7eb;
    white-space: nowrap;
  }
}

.vm-file-preview-viewer__text {
  max-width: min(960px, 100%);
  margin: 0 auto;
  padding: 16px 18px;
  border-radius: 12px;
  background: rgb(15 23 42 / 92%);
  color: #e2e8f0;
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-wrap;
  word-break: break-word;
}

.vm-file-preview-viewer__fallback {
  display: grid;
  gap: 8px;
  max-width: 420px;
  margin: 48px auto;
  padding: 28px 24px;
  border-radius: 16px;
  background: rgb(255 255 255 / 8%);
  color: rgb(255 255 255 / 88%);
  text-align: center;

  i {
    font-size: 42px;
    line-height: 1;
    color: rgb(255 255 255 / 72%);
  }

  p {
    margin: 0;
    font-size: 14px;
    line-height: 1.6;
  }
}
</style>
