<script setup lang="ts">
import { onMounted, ref } from 'vue'

type KnowledgeBuilderStatus = {
  ok: boolean
  extraction_ready: boolean
  indexing_ready: boolean
  helper_root: string
  mineru_version: string
  python_version: string
  model_dir: string
  license_notice: string
  checks: Record<string, boolean>
}

type KnowledgeBuilderMatch = {
  id: string
  job_name: string
  title: string
  score: number
  matched_terms: string[]
  excerpt: string
  source: {
    document_path?: string
    markdown_path?: string
    page_start?: number
    page_end?: number
    images?: string[]
  }
}

type KnowledgeBuilderQueryResult = {
  ok: boolean
  index_count: number
  entry_count: number
  matched_count: number
  matches: KnowledgeBuilderMatch[]
}

const knowledgeApiBase = String(
  import.meta.env.VITE_KNOWLEDGE_API_BASE || import.meta.env.VITE_API_BASE || 'http://127.0.0.1:7200',
).replace(/\/$/, '')
const status = ref<KnowledgeBuilderStatus | null>(null)
const busy = ref(false)
const message = ref('')
const pdfSource = ref('')
const ingestJobName = ref('')
const chunkSize = ref(50)
const inputRoot = ref('')
const indexJobName = ref('')
const queryText = ref('')
const queryLimit = ref(8)
const queryResult = ref<KnowledgeBuilderQueryResult | null>(null)

async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
  const response = await fetch(`${knowledgeApiBase}${path}`, init)
  if (!response.ok) {
    const text = await response.text()
    try {
      const payload = JSON.parse(text) as { detail?: string; message?: string; title?: string }
      throw new Error(payload.detail || payload.message || payload.title || text || `HTTP ${response.status}`)
    } catch (error) {
      if (error instanceof SyntaxError) throw new Error(text || `HTTP ${response.status}`)
      throw error
    }
  }
  return (await response.json()) as T
}

async function loadStatus(showResult = false) {
  busy.value = true
  if (showResult) message.value = '正在检查文档解析与知识索引工具...'
  try {
    status.value = await requestJson<KnowledgeBuilderStatus>('/api/knowledge-builder/status')
    if (showResult) {
      message.value = status.value.extraction_ready
        ? 'MinerU 文档解析与本地知识索引均可用。'
        : '本地知识索引可用，MinerU OCR 模型尚未就绪。'
    }
  } catch (error) {
    status.value = null
    message.value = error instanceof Error ? error.message : '知识库工具检查失败。'
  } finally {
    busy.value = false
  }
}

async function ingestPdf() {
  if (!pdfSource.value.trim()) return
  busy.value = true
  message.value = '正在解析 PDF 并建立知识索引...'
  try {
    const result = await requestJson<{ ok?: boolean; entry_count?: number; index_path?: string; message?: string; stage?: string }>('/api/knowledge-builder/ingest', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        source: pdfSource.value.trim(),
        job_name: ingestJobName.value.trim(),
        chunk_size: Number(chunkSize.value),
      }),
    })
    message.value = result.ok === false
      ? result.message || `PDF 导入未完成：${result.stage || '运行时检查未通过'}`
      : `PDF 导入完成：${result.entry_count ?? 0} 条知识，保存到 ${result.index_path || 'runtime/knowledge-base'}`
  } catch (error) {
    message.value = error instanceof Error ? error.message : 'PDF 文档导入失败。'
  } finally {
    busy.value = false
  }
}

async function buildIndex() {
  if (!inputRoot.value.trim()) return
  busy.value = true
  message.value = '正在读取 Markdown/JSON 并构建知识索引...'
  try {
    const result = await requestJson<{ entry_count: number; index_path: string }>('/api/knowledge-builder/index', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ input_root: inputRoot.value.trim(), job_name: indexJobName.value.trim() }),
    })
    message.value = `索引构建完成：${result.entry_count} 条知识，保存到 ${result.index_path}`
  } catch (error) {
    message.value = error instanceof Error ? error.message : '知识索引构建失败。'
  } finally {
    busy.value = false
  }
}

async function queryKnowledge() {
  if (!queryText.value.trim()) return
  busy.value = true
  queryResult.value = null
  message.value = '正在检索工程文档知识...'
  try {
    queryResult.value = await requestJson<KnowledgeBuilderQueryResult>('/api/knowledge-builder/query', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: queryText.value.trim(), limit: Number(queryLimit.value) }),
    })
    message.value = `检索完成：从 ${queryResult.value.entry_count} 条知识中命中 ${queryResult.value.matched_count} 条。`
  } catch (error) {
    message.value = error instanceof Error ? error.message : '文档知识检索失败。'
  } finally {
    busy.value = false
  }
}

function scoreText(score: number) {
  if (!Number.isFinite(Number(score))) return '--'
  return Number(score) <= 1 ? `${Math.round(Number(score) * 100)}%` : Number(score).toFixed(3)
}

onMounted(() => void loadStatus(false))
</script>

<template>
  <section class="knowledge-workspace">
    <header class="knowledge-header">
      <div>
        <p>Document Knowledge</p>
        <h2>知识库构建与检索</h2>
      </div>
      <button type="button" class="secondary" :disabled="busy" @click="loadStatus(true)">重新检查</button>
    </header>

    <section class="status-band">
      <article>
        <span>文档解析</span>
        <strong>{{ status?.extraction_ready ? '可用' : '模型未就绪' }}</strong>
        <small>MinerU {{ status?.mineru_version || '未连接' }}</small>
      </article>
      <article>
        <span>本地索引</span>
        <strong>{{ status?.indexing_ready ? '可用' : '不可用' }}</strong>
        <small>{{ status?.python_version || '未连接' }}</small>
      </article>
      <article>
        <span>工具目录</span>
        <strong>{{ status?.ok ? '已连接' : '等待连接' }}</strong>
        <small>{{ status?.helper_root || knowledgeApiBase }}</small>
      </article>
    </section>

    <div class="knowledge-grid">
      <section class="knowledge-panel ingest-panel">
        <div class="panel-heading"><span>01</span><div><h3>解析 PDF 并入库</h3><p>MinerU ingestion</p></div></div>
        <label><span>PDF 文件路径</span><input v-model="pdfSource" placeholder="D:\books\mechanical-design.pdf" /></label>
        <div class="inline-fields">
          <label><span>知识库名称</span><input v-model="ingestJobName" placeholder="mechanical-design" /></label>
          <label><span>每批页数</span><input v-model.number="chunkSize" type="number" min="1" max="200" /></label>
        </div>
        <button type="button" :disabled="busy || !pdfSource.trim()" @click="ingestPdf">解析并建立索引</button>
      </section>

      <section class="knowledge-panel">
        <div class="panel-heading"><span>02</span><div><h3>导入已有解析结果</h3><p>Markdown / JSON index</p></div></div>
        <label><span>解析产物目录</span><input v-model="inputRoot" placeholder="D:\knowledge-output\chunks" /></label>
        <label><span>知识库名称</span><input v-model="indexJobName" placeholder="bearing-design-handbook" /></label>
        <button type="button" :disabled="busy || !inputRoot.trim()" @click="buildIndex">构建知识索引</button>
      </section>

      <section class="knowledge-panel">
        <div class="panel-heading"><span>03</span><div><h3>验证知识检索</h3><p>Retrieval test</p></div></div>
        <label><span>工程问题</span><textarea v-model="queryText" rows="4" placeholder="支架底座固定后，螺栓孔附近如何避免应力集中？"></textarea></label>
        <label class="limit-field"><span>返回数量</span><input v-model.number="queryLimit" type="number" min="1" max="50" /></label>
        <button type="button" :disabled="busy || !queryText.trim()" @click="queryKnowledge">检索知识</button>
      </section>
    </div>

    <p class="knowledge-message">{{ message || `知识库 API：${knowledgeApiBase}` }}</p>

    <section v-if="queryResult" class="query-results">
      <header><div><p>Retrieval Results</p><h3>检索结果</h3></div><strong>{{ queryResult.matched_count }} / {{ queryResult.entry_count }}</strong></header>
      <article v-for="match in queryResult.matches" :key="match.id">
        <div class="match-heading"><strong>{{ match.title }}</strong><span>{{ scoreText(match.score) }}</span></div>
        <p>{{ match.excerpt }}</p>
        <div v-if="match.matched_terms?.length" class="matched-terms"><span v-for="term in match.matched_terms" :key="term">{{ term }}</span></div>
        <small>{{ match.job_name }} · 第 {{ match.source.page_start ?? '?' }}-{{ match.source.page_end ?? '?' }} 页 · {{ match.source.markdown_path || match.source.document_path }}</small>
      </article>
    </section>
  </section>
</template>

<style scoped>
.knowledge-workspace{max-width:1220px;margin:0 auto;padding:24px 20px 40px}.knowledge-header,.query-results header,.match-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.knowledge-header{margin-bottom:18px}.knowledge-header p,.query-results header p{margin:0 0 5px;color:#64736f;font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:0}h2,h3{margin:0}.status-band{display:grid;grid-template-columns:.8fr .8fr 1.6fr;border-top:1px solid #d9e2df;border-bottom:1px solid #d9e2df;background:#fff}.status-band article{display:grid;gap:5px;min-width:0;border-right:1px solid #e3ebe8;padding:15px 17px}.status-band article:last-child{border-right:0}.status-band span,.status-band small{color:#64736f}.status-band small{overflow-wrap:anywhere}.knowledge-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:20px}.knowledge-panel{display:grid;align-content:start;gap:13px;border:1px solid #d9e2df;border-top:3px solid #1e675f;border-radius:0 0 6px 6px;background:#fff;padding:18px}.ingest-panel{grid-column:1/-1}.panel-heading{display:flex;align-items:center;gap:12px}.panel-heading>span{display:grid;place-items:center;width:34px;height:34px;border:1px solid #1e675f;border-radius:50%;color:#1e675f;font-weight:800}.panel-heading p{margin:3px 0 0;color:#64736f;font-size:12px;text-transform:uppercase}.inline-fields{display:grid;grid-template-columns:minmax(0,1fr) 160px;gap:12px}label{display:grid;gap:6px;color:#25343a;font-weight:700}input,textarea{width:100%;box-sizing:border-box;border:1px solid #cbd7d3;border-radius:6px;background:#fff;padding:10px;color:#172026;font:inherit}textarea{resize:vertical}.limit-field{max-width:160px}button{min-height:40px;border:1px solid #1e675f;border-radius:6px;background:#1e675f;color:#fff;padding:8px 12px;font-weight:800;cursor:pointer}button.secondary{background:#fff;color:#1e675f}button:disabled{cursor:not-allowed;opacity:.45}.knowledge-panel>button{justify-self:start}.knowledge-message{min-height:24px;margin:18px 0 0;color:#8a3b12;font-weight:700;overflow-wrap:anywhere}.query-results{margin-top:18px;border:1px solid #d9e2df;background:#fff;padding:18px}.query-results header{padding-bottom:10px}.query-results header>strong{border:1px solid #d9e2df;border-radius:999px;background:#f7faf9;padding:5px 10px;color:#1e675f}.query-results article{border-top:1px solid #e3ebe8;padding:15px 0}.match-heading span{color:#1e675f;font-weight:800}.query-results article p{margin:8px 0;color:#25343a;line-height:1.7}.query-results small{display:block;margin-top:9px;color:#64736f;overflow-wrap:anywhere}.matched-terms{display:flex;flex-wrap:wrap;gap:6px}.matched-terms span{border:1px solid #d9e2df;border-radius:999px;background:#f7faf9;padding:3px 8px;color:#3e514b;font-size:12px}@media(max-width:800px){.status-band,.knowledge-grid,.inline-fields{grid-template-columns:1fr}.status-band article{border-right:0;border-bottom:1px solid #e3ebe8}.ingest-panel{grid-column:auto}.knowledge-header,.query-results header,.match-heading{flex-direction:column}}
</style>
