<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { Activity, Database, LoaderCircle, Moon, Search, Server, Sun, X } from '@lucide/vue'

import SearchResultCard from '@/components/SearchResultCard.vue'
import TorrentDetailDialog from '@/components/TorrentDetailDialog.vue'
import { Button } from '@/components/ui/button'
import { AppPagination } from '@/components/ui/pagination'
import { AppSelect } from '@/components/ui/select'
import { getStats, getTorrent, getVariants, search } from '@/lib/api'
import type { SearchHit, SearchPage, SearchSort, ServiceStats, TorrentDetail, ContentVariants } from '@/types/api'

const query = ref('')
const submittedQuery = ref('')
const regexMode = ref(false)
const sort = ref<SearchSort>('latest')
const page = ref<SearchPage | null>(null)
const offset = ref(0)
const supportedResultPageSizes = [10, 20, 50, 100]
const savedResultPageSize = Number(localStorage.getItem('dht-search-result-page-size'))
const limit = ref(supportedResultPageSizes.includes(savedResultPageSize) ? savedResultPageSize : 20)
const loading = ref(true)
const error = ref('')
const stats = ref<ServiceStats | null>(null)
const statsOpen = ref(false)
const darkMode = ref(false)
const detailOpen = ref(false)
const detailLoading = ref(false)
const detailFilesLoading = ref(false)
const detailFilesError = ref('')
const detailError = ref('')
const selectedHash = ref('')
const detail = ref<TorrentDetail | null>(null)
const variants = ref<ContentVariants | null>(null)
const supportedFilePageSizes = [25, 50, 100, 200]
const savedFilePageSize = Number(localStorage.getItem('dht-search-file-page-size'))
const detailFilePageSize = ref(supportedFilePageSizes.includes(savedFilePageSize) ? savedFilePageSize : 100)
let searchController: AbortController | null = null
let detailController: AbortController | null = null
let filesController: AbortController | null = null
let statsTimer: number | null = null
let statsRequestActive = false

const pageNumber = computed(() => Math.floor((page.value?.offset ?? 0) / limit.value) + 1)
const paginationTotal = computed(() => Math.min(page.value?.total ?? 0, 10_000 + limit.value))
const resultPageSizeOptions = [
  { value: '10', label: '10 条/页' },
  { value: '20', label: '20 条/页' },
  { value: '50', label: '50 条/页' },
  { value: '100', label: '100 条/页' },
]
const sortOptions: ReadonlyArray<{ value: SearchSort; label: string }> = [
  { value: 'relevance', label: '相关度' },
  { value: 'latest', label: '最近收录' },
  { value: 'oldest', label: '最早收录' },
  { value: 'heat', label: '热度' },
  { value: 'discoveries', label: '发现次数' },
  { value: 'size_desc', label: '大小降序' },
  { value: 'size_asc', label: '大小升序' },
]

function updateBrowserUrl() {
  const params = new URLSearchParams()
  if (submittedQuery.value) params.set('q', submittedQuery.value)
  if (sort.value !== 'latest') params.set('sort', sort.value)
  if (regexMode.value) params.set('regex', 'true')
  if (offset.value) params.set('offset', String(offset.value))
  history.replaceState(null, '', params.size ? `?${params}` : location.pathname)
}

async function loadSearch() {
  searchController?.abort()
  searchController = new AbortController()
  loading.value = true
  error.value = ''
  updateBrowserUrl()
  try {
    page.value = await search({ query: submittedQuery.value, offset: offset.value, limit: limit.value, sort: sort.value, regex: regexMode.value }, searchController.signal)
  } catch (cause) {
    if ((cause as Error).name !== 'AbortError') error.value = cause instanceof Error ? cause.message : '搜索失败'
  } finally {
    if (!searchController.signal.aborted) loading.value = false
  }
}

function submitSearch() {
  const hadQuery = submittedQuery.value.trim() !== ''
  submittedQuery.value = query.value
  if (!hadQuery && submittedQuery.value.trim() && sort.value === 'latest') sort.value = 'relevance'
  offset.value = 0
  void loadSearch()
}
function changePage(pageNumber: number) { offset.value = Math.max(0, (pageNumber - 1) * limit.value); void loadSearch(); window.scrollTo({ top: 0, behavior: 'smooth' }) }
function changeResultPageSize(value: string) {
  const pageSize = Number(value)
  if (!supportedResultPageSizes.includes(pageSize) || pageSize === limit.value) return
  limit.value = pageSize
  localStorage.setItem('dht-search-result-page-size', String(pageSize))
  offset.value = 0
  void loadSearch()
}
function changeSort(value: string) { sort.value = value as SearchSort; offset.value = 0; void loadSearch() }
function toggleRegex() { regexMode.value = !regexMode.value; offset.value = 0 }

function toggleTheme() {
  darkMode.value = !darkMode.value
  document.documentElement.classList.toggle('dark', darkMode.value)
  localStorage.setItem('dht-search-theme', darkMode.value ? 'dark' : 'light')
}

function closeStatsOnOutsideClick(event: PointerEvent) {
  if (statsOpen.value && event.target instanceof Element && !event.target.closest('[data-stats-panel]')) {
    statsOpen.value = false
  }
}

async function openDetail(hit: SearchHit) {
  selectedHash.value = hit.info_hash
  detailOpen.value = true
  detailLoading.value = true
  detailError.value = ''
  detailFilesError.value = ''
  detail.value = null
  variants.value = null
  detailController?.abort()
  detailController = new AbortController()
  try {
    const [torrent, contentVariants] = await Promise.all([getTorrent(hit.info_hash, 0, detailFilePageSize.value, detailController.signal), getVariants(hit.content_key, detailController.signal)])
    detail.value = torrent
    variants.value = contentVariants
  } catch (cause) {
    if ((cause as Error).name !== 'AbortError') detailError.value = cause instanceof Error ? cause.message : '详情加载失败'
  } finally {
    if (!detailController.signal.aborted) detailLoading.value = false
  }
}

async function loadFilePage(fileOffset: number) {
  if (!detail.value || detailFilesLoading.value) return
  filesController?.abort()
  filesController = new AbortController()
  detailFilesLoading.value = true
  detailFilesError.value = ''
  const infoHash = detail.value.info_hash
  try {
    const next = await getTorrent(infoHash, fileOffset, detailFilePageSize.value, filesController.signal)
    if (selectedHash.value === infoHash) detail.value = next
  } catch (cause) {
    if ((cause as Error).name !== 'AbortError') detailFilesError.value = cause instanceof Error ? cause.message : '文件列表加载失败'
  } finally {
    if (!filesController.signal.aborted) detailFilesLoading.value = false
  }
}

function changeFilePageSize(fileLimit: number) {
  if (!supportedFilePageSizes.includes(fileLimit) || fileLimit === detailFilePageSize.value) return
  detailFilePageSize.value = fileLimit
  localStorage.setItem('dht-search-file-page-size', String(fileLimit))
  void loadFilePage(0)
}

function closeDetail() { detailController?.abort(); filesController?.abort(); detailOpen.value = false }
function retryDetail() { const hit = page.value?.hits.find((item) => item.info_hash === selectedHash.value); if (hit) void openDetail(hit) }

async function loadStats() {
  if (statsRequestActive) return
  statsRequestActive = true
  try { stats.value = await getStats() } catch { stats.value = null } finally { statsRequestActive = false }
}

watch(statsOpen, (open) => {
  if (statsTimer !== null) {
    window.clearInterval(statsTimer)
    statsTimer = null
  }
  if (open) {
    void loadStats()
    statsTimer = window.setInterval(() => void loadStats(), 1_000)
  }
})

onMounted(() => {
  document.addEventListener('pointerdown', closeStatsOnOutsideClick)
  darkMode.value = document.documentElement.classList.contains('dark')
  const params = new URLSearchParams(location.search)
  query.value = params.get('q') ?? ''
  submittedQuery.value = query.value
  regexMode.value = params.get('regex') === 'true'
  const initialSort = params.get('sort') as SearchSort | null
  if (initialSort && ['relevance', 'latest', 'oldest', 'heat', 'size_desc', 'size_asc', 'discoveries'].includes(initialSort)) sort.value = initialSort
  else if (query.value.trim()) sort.value = 'relevance'
  offset.value = Math.min(10_000, Math.max(0, Number(params.get('offset')) || 0))
  void loadSearch()
})
onBeforeUnmount(() => {
  document.removeEventListener('pointerdown', closeStatsOnOutsideClick)
  searchController?.abort()
  detailController?.abort()
  filesController?.abort()
  if (statsTimer !== null) window.clearInterval(statsTimer)
})
</script>

<template>
  <div class="min-h-svh bg-background text-foreground">
    <header class="sticky top-0 z-30 border-b bg-background/90 backdrop-blur-xl">
      <div class="mx-auto flex h-15 max-w-6xl items-center gap-3 px-4 sm:px-6">
        <div class="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground"><Database class="size-4" aria-hidden="true" /></div>
        <p class="text-sm font-semibold tracking-tight">DHT Search</p>
        <Button class="ml-auto" size="icon" variant="ghost" :aria-label="darkMode ? '切换到浅色主题' : '切换到深色主题'" :title="darkMode ? '浅色主题' : '深色主题'" @click="toggleTheme"><Sun v-if="darkMode" /><Moon v-else /></Button>
        <div class="relative" data-stats-panel>
          <Button size="icon" variant="ghost" aria-label="运行状态" title="运行状态" @click="statsOpen = !statsOpen"><span class="relative"><Activity class="size-4" /><i class="absolute -right-0.5 -top-0.5 size-1.5 rounded-full" :class="stats ? 'bg-emerald-500' : 'bg-muted-foreground'" /></span></Button>
          <div v-if="statsOpen" class="absolute right-0 top-11 w-72 rounded-xl border bg-popover p-4 text-popover-foreground shadow-xl">
            <div class="mb-3 flex items-center"><p class="text-sm font-semibold">服务运行状态</p><button class="ml-auto" aria-label="关闭状态面板" @click="statsOpen = false"><X class="size-4" /></button></div>
            <div v-if="stats" class="grid grid-cols-2 gap-3 text-xs"><div class="status-cell"><span>DHT 节点</span><b>{{ stats.nodes.toLocaleString() }}</b></div><div class="status-cell"><span>已索引内容</span><b>{{ stats.indexed_documents.toLocaleString() }}</b></div><div class="status-cell"><span>获取成功</span><b>{{ stats.metadata_ok.toLocaleString() }}</b></div><div class="status-cell"><span>下载中</span><b>{{ stats.metadata_in_flight }}</b></div><div class="status-cell"><span>新收录</span><b>{{ stats.persistence_inserted.toLocaleString() }}</b></div><div class="status-cell"><span>验证成功</span><b>{{ stats.verification_succeeded.toLocaleString() }}</b></div></div>
            <p v-else class="py-4 text-center text-xs text-muted-foreground">无法获取服务状态</p>
          </div>
        </div>
      </div>
    </header>

    <main class="mx-auto max-w-6xl px-4 py-7 sm:px-6 sm:py-10">
      <section>
        <form class="overflow-hidden rounded-xl border bg-card shadow-sm focus-within:border-foreground/20" @submit.prevent="submitSearch">
          <div class="flex flex-wrap items-center gap-2 p-2">
            <Search class="ml-2.5 size-4 shrink-0 text-muted-foreground" />
            <label class="sr-only" for="search-query">搜索关键词</label>
            <input id="search-query" v-model="query" class="h-9 min-w-48 flex-1 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground" :maxlength="regexMode ? 256 : 512" :placeholder="regexMode ? '输入正则表达式' : '输入名称、文件名或 40 位 infohash'" type="search" />
            <Button size="sm" type="button" :variant="regexMode ? 'secondary' : 'ghost'" :aria-pressed="regexMode" title="正则搜索" @click="toggleRegex"><span class="font-mono">.*</span><span class="hidden sm:inline">正则</span></Button>
            <AppSelect :model-value="sort" :options="sortOptions" label="结果排序" @update:model-value="changeSort" />
            <Button type="submit">搜索</Button>
          </div>
        </form>

        <div v-if="loading" class="flex min-h-72 items-center justify-center text-sm text-muted-foreground"><LoaderCircle class="mr-2 size-5 animate-spin" />正在搜索</div>
        <div v-else-if="error" class="mt-4 flex min-h-72 flex-col items-center justify-center gap-4 rounded-xl border border-dashed text-center"><Server class="size-8 text-muted-foreground" /><div><p class="font-medium">无法完成搜索</p><p class="mt-1 text-sm text-muted-foreground">{{ error }}</p></div><Button variant="outline" @click="loadSearch">重试</Button></div>
        <div v-else-if="page?.hits.length" class="mt-4 space-y-3"><SearchResultCard v-for="hit in page.hits" :key="hit.content_key" :hit="hit" @select="openDetail" /></div>
        <div v-else class="mt-4 flex min-h-72 flex-col items-center justify-center rounded-xl border border-dashed text-center"><Search class="mb-3 size-8 text-muted-foreground" /><p class="font-medium">没有找到匹配内容</p><p class="mt-1 text-sm text-muted-foreground">尝试其他关键词或正则表达式</p></div>

        <div v-if="page && page.total > 0" class="mt-6 flex flex-wrap items-center justify-center gap-2">
          <AppSelect :model-value="String(limit)" :options="resultPageSizeOptions" label="每页搜索结果数量" @update:model-value="changeResultPageSize" />
          <AppPagination v-if="paginationTotal > limit" :page="pageNumber" :page-size="limit" :total="paginationTotal" @update:page="changePage" />
        </div>
      </section>
    </main>

    <TorrentDetailDialog :detail="detail" :error="detailError" :files-error="detailFilesError" :files-loading="detailFilesLoading" :loading="detailLoading" :open="detailOpen" :variants="variants" @close="closeDetail" @file-page="loadFilePage" @file-page-size="changeFilePageSize" @retry="retryDetail" />
  </div>
</template>
