<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { api, generatedUrl, thumbUrl } from '../api'
import { fmtTs, fmtSize } from '../utils/format'
import { copyText } from '../utils/clipboard'
import { zipSync } from 'fflate'
import Icon from '../components/Icon.vue'
import MediaLightbox from '../components/MediaLightbox.vue'

const items = ref([])
const total = ref(0)
const stats = ref({ total: 0, image: 0, video: 0, size_bytes: 0 })
const loading = ref(false)
const kind = ref('')          // '' | 'image' | 'video'
const selected = ref(null)
// Videos whose first-frame thumbnail is missing (old videos) — fall back to
// the muted <video> preview for those cards.
const thumbFail = reactive({})
const toast = ref('')

const page = ref(1)
// 20 per page so a 4-col (lg) or 5-col (xl) grid lays out as clean rows of
// 5×4 or 4×5 instead of a half-empty trailing row.
const pageSize = ref(20)


async function load() {
  loading.value = true
  const qs = new URLSearchParams({
    limit: String(pageSize.value),
    offset: String((page.value - 1) * pageSize.value),
  })
  if (kind.value) qs.set('kind', kind.value)
  const r = await api('/images?' + qs.toString())
  items.value = r.data?.data || []
  total.value = Number(r.data?.total ?? items.value.length)
  // Stats arrive with the same payload so the KPI strip stays cheap.
  stats.value = r.data?.stats || { total: 0, image: 0, video: 0, size_bytes: 0 }
  loading.value = false
}

// Admin delete: remove the file (+derived stills) from storage, then reload
// so pagination and the KPI strip stay accurate.
async function deleteFile(f) {
  if (!f || !f.name) return
  if (!confirm('确定删除这个文件？删除后不可恢复')) return
  const r = await api('/images?name=' + encodeURIComponent(f.name), { method: 'DELETE' })
  flash(r.ok ? '已删除' : (r.data?.detail || '删除失败'))
  if (r.ok) load()
}

// multi-select (keyed by file name) — bulk delete/download from the toolbar.
const picked = ref(new Set())
function togglePick(f) {
  const s = new Set(picked.value)
  s.has(f.name) ? s.delete(f.name) : s.add(f.name)
  picked.value = s
}
const pageAllPicked = computed(() =>
  items.value.length > 0 && items.value.every((f) => picked.value.has(f.name)))
function togglePickAll() {
  const s = new Set(picked.value)
  if (pageAllPicked.value) items.value.forEach((f) => s.delete(f.name))
  else items.value.forEach((f) => s.add(f.name))
  picked.value = s
}
async function deletePicked() {
  const names = [...picked.value]
  if (!names.length) return
  if (!confirm(`确定删除选中的 ${names.length} 个文件？删除后不可恢复`)) return
  let ok = 0
  for (const n of names) {
    const r = await api('/images?name=' + encodeURIComponent(n), { method: 'DELETE' })
    if (r.ok) ok++
  }
  picked.value = new Set()
  flash(`已删除 ${ok} 个`)
  load()
}
// Single pick → direct file download; multiple → bundle into one zip.
const zipping = ref(false)
async function downloadPicked() {
  const names = [...picked.value]
  if (!names.length) return
  if (names.length === 1) {
    const a = document.createElement('a')
    a.href = generatedUrl(names[0])
    a.download = names[0].split('/').pop()
    document.body.appendChild(a)
    a.click()
    a.remove()
    return
  }
  zipping.value = true
  flash('打包中…')
  try {
    // Fetch concurrently (10 at a time) so large batches pack fast.
    const bufs = []
    let next = 0
    await Promise.all(Array.from({ length: Math.min(10, names.length) }, async () => {
      while (next < names.length) {
        const i = next++
        bufs[i] = await (await fetch(generatedUrl(names[i]))).arrayBuffer()
      }
    }))
    const entries = {}
    names.forEach((n, i) => {
      let name = n.split('/').pop()
      while (entries[name]) name = '_' + name
      entries[name] = [new Uint8Array(bufs[i]), { level: 0 }]
    })
    const zipped = zipSync(entries)
    const url = URL.createObjectURL(new Blob([zipped], { type: 'application/zip' }))
    const a = document.createElement('a')
    a.href = url
    a.download = `图片-${names.length}个-${Date.now()}.zip`
    document.body.appendChild(a)
    a.click()
    a.remove()
    setTimeout(() => URL.revokeObjectURL(url), 30000)
    flash('已打包下载')
  } catch {
    flash('打包失败')
  }
  zipping.value = false
}

function absUrl(name) {
  const u = generatedUrl(name)
  return u.startsWith('http') ? u : location.origin + u
}

async function copyLink(name) {
  flash(await copyText(absUrl(name)) ? '链接已复制' : '复制失败')
}

async function copyImage(url) {
  try {
    const blob = await (await fetch(url)).blob()
    const pngBlob = blob.type === 'image/png'
      ? blob
      : await new Promise((resolve, reject) => {
          createImageBitmap(blob).then(async (bitmap) => {
            const canvas = document.createElement('canvas')
            canvas.width = bitmap.width
            canvas.height = bitmap.height
            const ctx = canvas.getContext('2d')
            if (!ctx) { reject(new Error('no canvas ctx')); return }
            ctx.drawImage(bitmap, 0, 0)
            canvas.toBlob((out) => {
              if (out) resolve(out)
              else reject(new Error('png convert failed'))
            }, 'image/png')
          }).catch(reject)
        })
    await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })])
    flash('图片已复制')
  } catch {
    flash('复制失败')
  }
}

async function copyPrompt(f) {
  if (!f.prompt) return
  flash(await copyText(f.prompt) ? '指令已复制' : '复制失败')
}

let toastTimer = null
function flash(msg) {
  toast.value = msg
  clearTimeout(toastTimer)
  toastTimer = setTimeout(() => (toast.value = ''), 1800)
}

function setKind(v) { kind.value = v; page.value = 1; load() }

const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)))
function goPage(n) {
  const target = Math.max(1, Math.min(totalPages.value, n))
  if (target === page.value) return
  page.value = target
  load()
}

const pageNumbers = computed(() => {
  const n = totalPages.value
  const cur = page.value
  if (n <= 7) return Array.from({ length: n }, (_, i) => i + 1)
  const want = new Set([1, n, cur - 1, cur, cur + 1])
  if (cur <= 3) { want.add(2); want.add(3); want.add(4) }
  if (cur >= n - 2) { want.add(n - 1); want.add(n - 2); want.add(n - 3) }
  const list = [...want].filter((x) => x >= 1 && x <= n).sort((a, b) => a - b)
  const out = []
  for (let i = 0; i < list.length; i++) {
    if (i > 0 && list[i] - list[i - 1] > 1) out.push(null)
    out.push(list[i])
  }
  return out
})

function onKey(e) {
  if (e.key === 'Escape') selected.value = null
}
onMounted(() => { load(); window.addEventListener('keydown', onKey) })
onUnmounted(() => window.removeEventListener('keydown', onKey))
</script>

<template>
  <section class="space-y-4">
    <!-- KPI strip — same shape as the LogsView so the admin shell stays
         consistent. /images returns all four numbers in one payload. -->
    <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
      <div class="card p-4">
        <div class="text-[11px] uppercase tracking-wider text-white/45">总计</div>
        <div class="text-2xl font-semibold mt-1 tabular-nums">{{ stats.total }}</div>
      </div>
      <div class="card p-4">
        <div class="text-[11px] uppercase tracking-wider text-indigo-300/80">图像</div>
        <div class="text-2xl font-semibold mt-1 tabular-nums text-indigo-300">{{ stats.image }}</div>
      </div>
      <div class="card p-4">
        <div class="text-[11px] uppercase tracking-wider text-fuchsia-300/80">视频</div>
        <div class="text-2xl font-semibold mt-1 tabular-nums text-fuchsia-300">{{ stats.video }}</div>
      </div>
      <div class="card p-4">
        <div class="text-[11px] uppercase tracking-wider text-amber-300/80">存储</div>
        <div class="text-2xl font-semibold mt-1 tabular-nums text-amber-300">{{ fmtSize(stats.size_bytes) }}</div>
      </div>
    </div>

    <!-- toolbar -->
    <div class="card p-3 flex items-center justify-between gap-3 flex-wrap">
      <div class="flex items-center gap-1">
        <button @click="setKind('')" class="fp" :class="kind === '' && 'fp-on'">全部</button>
        <button @click="setKind('image')" class="fp" :class="kind === 'image' && 'fp-on'">图像</button>
        <button @click="setKind('video')" class="fp" :class="kind === 'video' && 'fp-on'">视频</button>
      </div>
      <div class="flex items-center gap-2">
        <button @click="togglePickAll" class="btn-soft" :class="pageAllPicked && '!bg-white/90 !text-slate-900'">
          <Icon name="check" class="w-3.5 h-3.5" /> 全选本页
        </button>
        <template v-if="picked.size">
          <button @click="downloadPicked" :disabled="zipping" class="btn-soft disabled:opacity-50">
            <Icon name="download" class="w-3.5 h-3.5" /> {{ zipping ? '打包中…' : `下载选中 (${picked.size})` }}
          </button>
          <button @click="deletePicked" class="btn-soft danger">
            <Icon name="trash" class="w-3.5 h-3.5" /> 删除选中 ({{ picked.size }})
          </button>
        </template>
        <button @click="load" class="btn-soft">
          <Icon name="refresh" class="w-3.5 h-3.5" /> 刷新
        </button>
      </div>
    </div>

    <!-- grid -->
    <div v-if="loading && !items.length" class="text-center text-sm text-white/40 py-20">加载中…</div>
    <div v-else-if="!items.length" class="card flex flex-col items-center gap-3 text-white/40 py-20">
      <span class="w-14 h-14 rounded-2xl bg-white/[0.04] grid place-items-center">
        <Icon name="files" class="w-6 h-6" />
      </span>
      <span class="text-sm">还没有生成过任何图片</span>
    </div>

    <div v-else class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
      <div v-for="f in items" :key="f.name"
           class="media-card group relative rounded-xl overflow-hidden ring-1 ring-white/[0.06] bg-white/[0.03] aspect-[4/5] cursor-zoom-in"
           @click="selected = f">
        <!-- media -->
        <template v-if="f.kind === 'video'">
          <!-- first-frame still as background-image (same as image cards); the
               hidden probe img flips to the <video> fallback for old videos. -->
          <div v-if="!thumbFail[f.name]" :style="{ backgroundImage: `url(${thumbUrl(f.name)})` }"
               class="absolute inset-0 w-full h-full bg-cover bg-center transition-transform duration-300 group-hover:scale-105">
            <img :src="thumbUrl(f.name)" class="hidden" @error="thumbFail[f.name] = true" />
          </div>
          <video v-else :src="generatedUrl(f.name)" muted loop preload="metadata"
                 class="absolute inset-0 w-full h-full object-cover"
                 @mouseenter="$event.target.play && $event.target.play()"
                 @mouseleave="$event.target.pause && $event.target.pause()" />
        </template>
        <!-- background-image (not <img>) so Edge shows no 视觉搜索 overlay icon. -->
        <div v-else :style="{ backgroundImage: `url(${thumbUrl(f.name)})` }"
             class="absolute inset-0 w-full h-full bg-cover bg-center transition-transform duration-300 group-hover:scale-105"></div>

        <!-- gradient veil (always visible so the prompt overlay reads) -->
        <div class="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-black/85 via-black/40 to-transparent pointer-events-none"></div>

        <!-- select + kind chip -->
        <div class="absolute top-3 left-3 flex items-center gap-1.5">
          <button @click.stop.prevent="togglePick(f)" :title="picked.has(f.name) ? '取消选择' : '选择'"
                  class="pick" :class="picked.has(f.name) && 'pick-on'">
            <Icon name="check" class="w-3 h-3" />
          </button>
          <span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider ring-1"
                :class="f.kind === 'video' ? 'bg-fuchsia-500/20 text-fuchsia-200 ring-fuchsia-400/30' : 'bg-indigo-500/20 text-indigo-200 ring-indigo-400/30'">
            {{ f.kind === 'video' ? '视频' : '图像' }}
          </span>
        </div>

        <!-- quick actions, hover-revealed; same style as 首页内容 -->
        <div class="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
          <button v-if="f.kind !== 'video'" @click.stop.prevent="copyImage(generatedUrl(f.name))" title="复制图片"
                  class="w-7 h-7 rounded-lg bg-black/50 ring-1 ring-white/10 hover:bg-black/70 text-white grid place-items-center">
            <Icon name="copy" class="w-3.5 h-3.5" />
          </button>
          <a :href="generatedUrl(f.name)" :download="f.name.split('/').pop()" @click.stop title="下载"
             class="w-7 h-7 rounded-lg bg-black/50 ring-1 ring-white/10 hover:bg-black/70 text-white grid place-items-center">
            <Icon name="download" class="w-3.5 h-3.5" />
          </a>
          <button @click.stop.prevent="deleteFile(f)" title="删除"
                  class="w-7 h-7 rounded-lg bg-black/50 ring-1 ring-white/10 hover:bg-rose-600/80 text-white grid place-items-center">
            <Icon name="trash" class="w-3.5 h-3.5" />
          </button>
        </div>

        <!-- caption: prompt (truncated 2 lines) + meta line -->
        <div class="absolute inset-x-0 bottom-0 p-3 pointer-events-none">
          <div class="text-[12px] leading-tight text-white font-medium line-clamp-2 mb-1 transition-colors"
               :class="f.prompt ? 'pointer-events-auto cursor-pointer hover:text-white/75' : ''"
               :title="f.prompt ? '点击复制提示词' : f.name"
               @click.stop="copyPrompt(f)">
            {{ f.prompt || f.name.split('/').pop() }}
          </div>
          <div class="text-[10px] text-white/55 flex items-center justify-between gap-2 tabular-nums">
            <span class="truncate" :title="f.model || ''">{{ f.model || '—' }}</span>
            <span class="shrink-0 flex items-center gap-1">
              <span v-if="f.resolution" class="text-emerald-300/90">{{ f.resolution }}</span>
              <span v-if="f.ratio" class="text-white/40">{{ f.ratio }}</span>
              <span v-if="f.kind === 'video' && f.duration" class="text-fuchsia-300/80">{{ f.duration }}</span>
            </span>
          </div>
          <div class="text-[10px] text-white/35 mt-0.5 tabular-nums">{{ fmtSize(f.size) }} · {{ fmtTs(f.mtime) }}</div>
        </div>
      </div>
    </div>

    <!-- pagination — hidden when everything fits on one page -->
    <div v-if="!loading && totalPages > 1" class="card !p-3 flex items-center justify-between gap-3">
      <div class="text-xs text-white/55 tabular-nums px-2">
        <span class="text-white/85">{{ (page - 1) * pageSize + 1 }}–{{ Math.min(total, page * pageSize) }}</span>
        / {{ total }} 张
      </div>
      <div class="flex items-center gap-1">
        <template v-for="(n, i) in pageNumbers" :key="i">
          <span v-if="n === null" class="px-1 text-white/35">…</span>
          <button v-else @click="goPage(n)" class="pg" :class="page === n && 'pg-on'">{{ n }}</button>
        </template>
      </div>
    </div>

    <!-- Lightbox (shared component) -->
    <MediaLightbox
      v-if="selected"
      :src="generatedUrl(selected.name)"
      :kind="selected.kind"
      :prompt="selected.prompt"
      :meta="[selected.model, selected.name].filter(Boolean).join(' · ')"
      :meta-sub="[selected.resolution, selected.ratio, (selected.kind === 'video' ? selected.duration : ''), fmtSize(selected.size), fmtTs(selected.mtime)].filter(Boolean).join(' · ')"
      :download-name="selected.name.split('/').pop()"
      @close="selected = null" />

    <!-- toast -->
    <transition name="fade">
      <div v-if="toast"
           class="fixed bottom-6 left-1/2 -translate-x-1/2 z-[60] bg-slate-900 text-white text-xs px-4 py-2 rounded-lg shadow-lg">
        {{ toast }}
      </div>
    </transition>
  </section>
</template>

<style scoped>
.fade-enter-active, .fade-leave-active { transition: opacity 0.18s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }

.line-clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* filter pill — same shape as LogsView so the admin shell stays consistent */
.fp {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.7rem;
  font-size: 0.72rem;
  border-radius: 0.55rem;
  color: var(--fg-3);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition: background 0.15s, color 0.15s;
}
.fp:hover { background: var(--hover); color: var(--fg); }
.fp-on { background: rgb(15 23 42); color: white; box-shadow: none; }
html.dark .fp-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); }

/* numbered pagination buttons */
.pg {
  min-width: 1.75rem;
  padding: 0.3rem 0.55rem;
  font-size: 0.72rem;
  font-weight: 500;
  text-align: center;
  border-radius: 0.45rem;
  color: var(--fg-2);
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--hairline);
  transition: background 0.15s, color 0.15s;
}
.pg:hover:not(.pg-on) { background: var(--hover); color: var(--fg); }
.pg-on { background: rgb(15 23 42); color: white; box-shadow: none; }
html.dark .pg-on { background: rgb(255 255 255 / 0.92); color: rgb(15 23 42); }

.btn-soft.danger {
  color: rgb(253 164 175);
  background: rgb(244 63 94 / 0.12);
  box-shadow: inset 0 0 0 1px rgb(244 63 94 / 0.3);
}
.btn-soft.danger:hover {
  color: white;
  background: rgb(244 63 94 / 0.25);
}

/* card select toggle — always visible rounded-square check button */
.pick {
  width: 1.4rem; height: 1.4rem; border-radius: 0.375rem;
  display: inline-flex; align-items: center; justify-content: center;
  color: rgb(255 255 255 / 0.85);
  background: rgb(0 0 0 / 0.45);
  box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.75);
  transition: background 0.15s, box-shadow 0.15s;
}
.pick svg { opacity: 0; transition: opacity 0.15s; }
.pick:hover { background: rgb(0 0 0 / 0.65); }
.pick:hover svg { opacity: 0.6; }
.pick-on { background: rgb(217 70 239); box-shadow: inset 0 0 0 1.5px rgb(255 255 255 / 0.9); }
.pick-on svg { opacity: 1; }
</style>
