<template>
  <div class="docs-page">
    <aside class="docs-sidebar">
      <div class="docs-panel">
        <div class="docs-panel__head">
          <span class="docs-panel__title">异常处理</span>
        </div>
        <el-input
          v-model="keyword"
          class="docs-search"
          clearable
          placeholder="搜索异常、步骤、错误码"
          :prefix-icon="Search"
        />
        <div class="docs-list">
          <div v-for="doc in docs" :key="doc.path" class="docs-list__entry">
            <button
              type="button"
              :class="['docs-list__item', { 'is-active': activeDocPath === doc.path }]"
              @click="selectDoc(doc.path)"
            >
              <el-icon><Document /></el-icon>
              <span>{{ doc.title }}</span>
            </button>
            <div v-if="activeDocPath === doc.path && level3Toc.length" class="docs-heading-list">
              <button
                v-for="item in level3Toc"
                :key="item.id"
                type="button"
                class="docs-heading-list__item"
                @click="jumpTo(item.id)"
              >
                {{ item.text }}
              </button>
            </div>
          </div>
        </div>
      </div>

      <div v-if="keyword.trim()" class="docs-panel">
        <div class="docs-panel__head">
          <span class="docs-panel__title">搜索结果</span>
          <span class="docs-panel__count">{{ searchResults.length }}</span>
        </div>
        <div v-if="searchResults.length" class="search-results">
          <button
            v-for="item in searchResults"
            :key="item.id"
            type="button"
            class="search-result"
            @click="jumpTo(item.anchor)"
          >
            <span class="search-result__title">{{ item.title }}</span>
            <span class="search-result__text">{{ item.preview }}</span>
          </button>
        </div>
        <el-empty v-else description="未找到匹配内容" :image-size="80" />
      </div>
    </aside>

    <main v-loading="loading" class="docs-main" @click="handleArticleClick">
      <div v-if="loadError" class="docs-error">
        <el-empty :description="loadError" />
      </div>
      <article v-else class="docs-article" v-html="renderedHtml" />
    </main>
    <el-image-viewer
      v-if="previewVisible"
      :url-list="previewUrls"
      :initial-index="previewIndex"
      hide-on-click-modal
      @close="previewVisible = false"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { Document, Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getDocContent, getDocManifest, getDocsBase, type DocManifestItem } from '@/api/docs'

defineOptions({ name: 'ExceptionDocsView' })

interface TocItem {
  id: string
  level: number
  text: string
}

interface SearchSection {
  id: string
  anchor: string
  title: string
  content: string
}

const docs = ref<DocManifestItem[]>([])
const activeDocPath = ref('')
const rawMarkdown = ref('')
const loading = ref(false)
const loadError = ref('')
const keyword = ref('')
const toc = ref<TocItem[]>([])
const searchSections = ref<SearchSection[]>([])
const previewVisible = ref(false)
const previewIndex = ref(0)
const previewUrls = computed(() => extractImageUrls(rawMarkdown.value, activeDocPath.value))

const renderedHtml = computed(() => renderMarkdown(rawMarkdown.value, activeDocPath.value))
const level3Toc = computed(() => toc.value.filter((item) => item.level === 3))
const searchResults = computed(() => {
  const q = keyword.value.trim().toLowerCase()
  if (!q) return []
  return searchSections.value
    .filter((item) => `${item.title}\n${item.content}`.toLowerCase().includes(q))
    .map((item) => ({
      id: item.id,
      anchor: item.anchor,
      title: item.title,
      preview: buildPreview(item.content, q),
    }))
})

watch(rawMarkdown, (value) => {
  toc.value = extractToc(value)
  searchSections.value = buildSearchSections(value)
})

onMounted(async () => {
  await loadManifest()
})

async function loadManifest() {
  loading.value = true
  loadError.value = ''
  try {
    const manifest = await getDocManifest()
    docs.value = manifest.docs
    if (docs.value.length) {
      await selectDoc(docs.value[0].path)
    }
  } catch (error) {
    loadError.value = error instanceof Error ? error.message : '文档加载失败'
    ElMessage.error(loadError.value)
  } finally {
    loading.value = false
  }
}

async function selectDoc(path: string) {
  if (activeDocPath.value === path && rawMarkdown.value) return
  loading.value = true
  loadError.value = ''
  try {
    activeDocPath.value = path
    rawMarkdown.value = await getDocContent(path)
    requestAnimationFrame(() => {
      const firstTitle = toc.value[0]?.id
      if (firstTitle) jumpTo(firstTitle)
    })
  } catch (error) {
    loadError.value = error instanceof Error ? error.message : '文档加载失败'
    ElMessage.error(loadError.value)
  } finally {
    loading.value = false
  }
}

function extractToc(markdown: string) {
  const counters = new Map<string, number>()
  return markdown
    .split('\n')
    .map((line) => {
      const match = /^(#{1,4})\s+(.+)$/.exec(line.trim())
      if (!match) return null
      const text = stripMarkdown(match[2])
      return {
        id: buildHeadingId(text, counters),
        level: match[1].length,
        text,
      }
    })
    .filter((item): item is TocItem => Boolean(item))
}

function buildSearchSections(markdown: string) {
  const counters = new Map<string, number>()
  const sections: SearchSection[] = []
  let current: SearchSection | null = null

  markdown.split('\n').forEach((line) => {
    const trimmed = line.trim()
    const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed)
    if (heading) {
      const title = stripMarkdown(heading[2])
      current = {
        id: buildHeadingId(title, counters),
        anchor: '',
        title,
        content: '',
      }
      current.anchor = current.id
      sections.push(current)
      return
    }
    if (!current || !trimmed) return
    current.content += `${stripMarkdown(trimmed)}\n`
  })

  return sections
}

function renderMarkdown(markdown: string, docPath: string) {
  const counters = new Map<string, number>()
  const html: string[] = []
  markdown.split('\n').forEach((line) => {
    const trimmed = line.trim()
    if (!trimmed) return

    const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed)
    if (heading) {
      const level = heading[1].length
      const text = stripMarkdown(heading[2])
      const id = buildHeadingId(text, counters)
      html.push(`<h${level} id="${id}">${escapeHtml(text)}</h${level}>`)
      return
    }

    const image = /^!\[([^\]]*)]\(([^)]+)\)$/.exec(trimmed)
    if (image) {
      const alt = escapeHtml(image[1])
      const src = escapeHtml(resolveDocAsset(docPath, image[2]))
      html.push(`<figure><img class="doc-preview-image" src="${src}" alt="${alt}" loading="lazy" data-preview-src="${src}" /><figcaption>${alt}</figcaption></figure>`)
      return
    }

    const step = /^(\d+)\.(.+)$/.exec(trimmed)
    if (step) {
      html.push(`<p class="doc-step"><span>${escapeHtml(step[1])}</span>${escapeHtml(step[2].trim())}</p>`)
      return
    }

    html.push(`<p>${escapeHtml(trimmed)}</p>`)
  })
  return html.join('')
}

function resolveDocAsset(docPath: string, assetPath: string) {
  if (/^(https?:)?\/\//.test(assetPath) || assetPath.startsWith('data:')) return assetPath
  const docDir = docPath.includes('/') ? docPath.slice(0, docPath.lastIndexOf('/') + 1) : ''
  const normalized = `${docDir}${assetPath}`.replace(/^\.\//, '')
  return `${getDocsBase()}${encodeURI(normalized)}`
}

function extractImageUrls(markdown: string, docPath: string) {
  return markdown
    .split('\n')
    .map((line) => /^!\[[^\]]*]\(([^)]+)\)$/.exec(line.trim())?.[1])
    .filter((src): src is string => Boolean(src))
    .map((src) => resolveDocAsset(docPath, src))
}

function buildHeadingId(text: string, counters: Map<string, number>) {
  const base = text
    .toLowerCase()
    .replace(/[^\p{L}\p{N}]+/gu, '-')
    .replace(/^-+|-+$/g, '') || 'section'
  const count = counters.get(base) ?? 0
  counters.set(base, count + 1)
  return count ? `${base}-${count + 1}` : base
}

function buildPreview(content: string, q: string) {
  const compact = content.replace(/\s+/g, ' ').trim()
  const index = compact.toLowerCase().indexOf(q)
  if (index < 0) return compact.slice(0, 80)
  return compact.slice(Math.max(0, index - 24), index + q.length + 56)
}

function stripMarkdown(value: string) {
  return value
    .replace(/^!\[([^\]]*)]\([^)]+\)$/, '$1')
    .replace(/\[([^\]]+)]\([^)]+\)/g, '$1')
    .replace(/[*_`#>]/g, '')
    .trim()
}

function escapeHtml(value: string) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

function jumpTo(id: string) {
  requestAnimationFrame(() => {
    document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
  })
}

function handleArticleClick(event: MouseEvent) {
  const target = event.target
  if (!(target instanceof HTMLImageElement)) return
  const src = target.dataset.previewSrc
  if (!src) return
  previewIndex.value = Math.max(previewUrls.value.indexOf(src), 0)
  previewVisible.value = true
}
</script>

<style scoped>
.docs-page {
  display: grid;
  grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
  gap: 16px;
  min-height: 100%;
}

.docs-sidebar {
  position: sticky;
  top: 20px;
  align-self: start;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}

.docs-panel,
.docs-main {
  background: #fff;
  border: 1px solid #e4e7ed;
  border-radius: 6px;
}

.docs-panel {
  padding: 14px;
}

.docs-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.docs-panel__title {
  font-size: 15px;
  font-weight: 600;
  color: #303133;
}

.docs-panel__count {
  min-width: 22px;
  height: 22px;
  padding: 0 7px;
  border-radius: 11px;
  background: #ecf5ff;
  color: #409eff;
  font-size: 12px;
  line-height: 22px;
  text-align: center;
}

.docs-search {
  margin-bottom: 12px;
}

.docs-list,
.search-results {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.docs-list__item,
.search-result,
.docs-heading-list__item {
  width: 100%;
  border: 0;
  background: transparent;
  color: #606266;
  cursor: pointer;
  text-align: left;
}

.docs-list__item {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 38px;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 14px;
}

.docs-list__item:hover,
.docs-list__item.is-active {
  background: #ecf5ff;
  color: #409eff;
}

.docs-heading-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 4px 0 6px 28px;
  padding-left: 10px;
  border-left: 2px solid #e4e7ed;
}

.docs-heading-list__item {
  padding: 7px 8px;
  border-radius: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
}

.docs-heading-list__item:hover {
  background: #f5f7fa;
  color: #409eff;
}

.search-result {
  padding: 10px;
  border-radius: 6px;
}

.search-result:hover {
  background: #f5f7fa;
}

.search-result__title,
.search-result__text {
  display: block;
}

.search-result__title {
  margin-bottom: 4px;
  color: #303133;
  font-size: 13px;
  font-weight: 600;
}

.search-result__text {
  color: #909399;
  font-size: 12px;
  line-height: 1.5;
}

.docs-main {
  position: relative;
  min-width: 0;
  padding: 0;
  overflow: hidden;
}

.docs-error {
  padding: 48px 16px;
}

.docs-article {
  max-width: 980px;
  padding: 28px 36px 56px;
}

:deep(.docs-article h1),
:deep(.docs-article h2),
:deep(.docs-article h3),
:deep(.docs-article h4) {
  scroll-margin-top: 74px;
  color: #1f2d3d;
  line-height: 1.35;
}

:deep(.docs-article h1) {
  margin-bottom: 28px;
  font-size: 28px;
}

:deep(.docs-article h2) {
  margin: 32px 0 16px;
  font-size: 22px;
}

:deep(.docs-article h3) {
  margin: 28px 0 14px;
  font-size: 18px;
}

:deep(.docs-article h4) {
  margin: 22px 0 12px;
  font-size: 16px;
}

:deep(.docs-article p) {
  margin: 12px 0;
  color: #303133;
  font-size: 15px;
  line-height: 1.8;
}

:deep(.docs-article .doc-step) {
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

:deep(.docs-article .doc-step span) {
  flex: 0 0 auto;
  min-width: 24px;
  height: 24px;
  margin-top: 2px;
  border-radius: 12px;
  background: #409eff;
  color: #fff;
  font-size: 13px;
  line-height: 24px;
  text-align: center;
}

:deep(.docs-article figure) {
  margin: 18px 0 24px;
}

:deep(.docs-article img) {
  display: block;
  max-width: 100%;
  border: 1px solid #dcdfe6;
  border-radius: 6px;
  background: #f5f7fa;
}

:deep(.docs-article .doc-preview-image) {
  cursor: zoom-in;
}

:deep(.docs-article figcaption) {
  margin-top: 8px;
  color: #909399;
  font-size: 12px;
}

@media (max-width: 900px) {
  .docs-page {
    grid-template-columns: 1fr;
  }

  .docs-sidebar {
    position: static;
    order: 1;
    max-height: none;
    overflow: visible;
  }

  .docs-main {
    order: 2;
  }

  .docs-article {
    padding: 22px 18px 44px;
  }
}
</style>
