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

import DOMPurify from 'dompurify';
import { Page, VbenIcon } from '@vben/common-ui';
import { Button, Card, Tag } from 'ant-design-vue';
import { marked } from 'marked';
import { useAccessStore } from '@vben/stores';

interface Source {
  chunk_id: string;
  score: number;
  summary?: string;
}

interface Msg {
  role: 'bot' | 'err' | 'user';
  text: string;
  sources?: Source[];
  streaming?: boolean;
}

const accessStore = useAccessStore();
const messages = ref<Msg[]>([]);
const input = ref('');
const sending = ref(false);
const logEl = ref<HTMLDivElement>();
const logBottomEl = ref<HTMLDivElement>();
let logObserver: MutationObserver | null = null;
const quickQuestions = [
  'AdGiftData 表里有哪些字段？',
  'UserBaseData 里和等级相关的字段有哪些？',
  'OrderData 表常用索引字段是什么？',
];

marked.setOptions({
  breaks: true,
  gfm: true,
});

function scrollBottom() {
  nextTick(() => {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        logBottomEl.value?.scrollIntoView({
          block: 'end',
        });
        logEl.value?.scrollTo({ top: logEl.value.scrollHeight });
      });
    });
  });
}

onMounted(() => {
  if (!logEl.value) {
    return;
  }

  logObserver = new MutationObserver(() => {
    if (sending.value) {
      scrollBottom();
    }
  });

  logObserver.observe(logEl.value, {
    characterData: true,
    childList: true,
    subtree: true,
  });
});

onBeforeUnmount(() => {
  logObserver?.disconnect();
  logObserver = null;
});

function extractStreamText(data: string) {
  if (!data || data === '[DONE]') {
    return '';
  }

  try {
    const parsed = JSON.parse(data);
    if (typeof parsed === 'string') {
      return parsed;
    }
    if (typeof parsed === 'number' || typeof parsed === 'boolean') {
      return String(parsed);
    }
    if (typeof parsed?.token === 'string') {
      return parsed.token;
    }
    if (typeof parsed?.token === 'number' || typeof parsed?.token === 'boolean') {
      return String(parsed.token);
    }
    if (typeof parsed?.content === 'string') {
      return parsed.content;
    }
    if (typeof parsed?.content === 'number' || typeof parsed?.content === 'boolean') {
      return String(parsed.content);
    }
    if (typeof parsed?.text === 'string') {
      return parsed.text;
    }
    if (typeof parsed?.text === 'number' || typeof parsed?.text === 'boolean') {
      return String(parsed.text);
    }
    if (typeof parsed?.answer === 'string') {
      return parsed.answer;
    }
    if (typeof parsed?.answer === 'number' || typeof parsed?.answer === 'boolean') {
      return String(parsed.answer);
    }
    if (typeof parsed?.delta === 'string') {
      return parsed.delta;
    }
    if (typeof parsed?.delta === 'number' || typeof parsed?.delta === 'boolean') {
      return String(parsed.delta);
    }
    if (typeof parsed?.choices?.[0]?.delta?.content === 'string') {
      return parsed.choices[0].delta.content;
    }
    if (
      typeof parsed?.choices?.[0]?.delta?.content === 'number'
      || typeof parsed?.choices?.[0]?.delta?.content === 'boolean'
    ) {
      return String(parsed.choices[0].delta.content);
    }
    return '';
  } catch {
    return data;
  }
}

function renderMarkdown(text: string) {
  const html = marked.parse(text, { async: false }) as string;
  return DOMPurify.sanitize(html);
}

function applyStreamChunk(raw: string, botMsg: Msg) {
  let evt = 'message';
  let data = '';

  for (const line of raw.split('\n')) {
    if (line.startsWith('event:')) evt = line.slice(6).trim();
    else if (line.startsWith('data:')) {
      data += (data ? '\n' : '') + line.slice(5).replace(/^ /, '');
    }
  }

  if (evt === 'sources') {
    botMsg.sources = JSON.parse(data);
    scrollBottom();
  } else if (evt === 'token' || evt === 'message') {
    const chunk = extractStreamText(data);
    if (chunk) {
      botMsg.text += chunk;
    }
    scrollBottom();
  } else if (evt === 'error') {
    botMsg.role = 'err';
    botMsg.text = `流式失败: ${data}`;
  } else if (evt === 'done') {
    botMsg.streaming = false;
    scrollBottom();
  }
}

async function send() {
  const query = input.value.trim();
  if (!query || sending.value) return;

  messages.value.push({ role: 'user', text: query });
  input.value = '';
  sending.value = true;

  const botMsg = reactive<Msg>({ role: 'bot', text: '', streaming: true });
  messages.value.push(botMsg);
  scrollBottom();

  const token = accessStore.accessToken;

  try {
    const resp = await fetch('/api/knowledge/chat', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: token ? `Bearer ${token}` : '',
      },
      body: JSON.stringify({ query, top_k: 30 }),
    });
    if (!resp.ok || !resp.body) {
      throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
    }

    // SSE 行解析：按 \n\n 分块；每块取 event: 与 data:
    const reader = resp.body.pipeThrough(new TextDecoderStream()).getReader();
    let buf = '';
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        const tail = buf.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
        if (tail) {
          applyStreamChunk(tail, botMsg);
        }
        break;
      }
      buf += value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
      let idx;
      while ((idx = buf.indexOf('\n\n')) >= 0) {
        const raw = buf.slice(0, idx);
        buf = buf.slice(idx + 2);
        applyStreamChunk(raw, botMsg);
      }
    }
    botMsg.streaming = false;
  } catch (err: any) {
    botMsg.role = 'err';
    botMsg.text = `请求失败: ${err.message || err}`;
    botMsg.streaming = false;
  } finally {
    sending.value = false;
    scrollBottom();
  }
}

function onKeydown(e: KeyboardEvent) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    send();
  }
}

function fillQuestion(question: string) {
  input.value = question;
}
</script>

<template>
  <Page auto-content-height>
    <div class="kb-chat-page">
      <Card :bordered="false" class="kb-hero-card mb-5">
        <div class="kb-hero">
          <div class="kb-hero__content">
            <div class="kb-hero__title">
              <div class="kb-hero__icon">
                <VbenIcon icon="solar:book-bookmark-bold" />
              </div>
              <div>
                <div class="kb-hero__heading">配置表知识库问答</div>
                <div class="kb-hero__subheading">
                  基于 BM25 检索与 DeepSeek 流式回答，适合快速查询配置字段、含义和上下游关系。
                </div>
              </div>
            </div>
            <div class="kb-hero__tags">
              <Tag color="blue" class="kb-hero__tag">
                <VbenIcon icon="solar:database-bold" class="mr-1" />
                配置检索
              </Tag>
              <Tag color="gold" class="kb-hero__tag">
                <VbenIcon icon="solar:chat-round-dots-bold" class="mr-1" />
                流式回答
              </Tag>
              <Tag color="green" class="kb-hero__tag">
                <VbenIcon icon="solar:document-text-bold" class="mr-1" />
                来源追踪
              </Tag>
            </div>
          </div>
          <div class="kb-hero__examples">
            <div class="kb-hero__examples-title">试试这些问题</div>
            <div class="kb-hero__examples-list">
              <Button
                v-for="question in quickQuestions"
                :key="question"
                class="kb-example-btn"
                type="default"
                @click="fillQuestion(question)"
              >
                {{ question }}
              </Button>
            </div>
          </div>
        </div>
      </Card>

      <Card :bordered="false" class="kb-chat-card">
        <template #title>
          <div class="kb-card-title">
            <span class="kb-card-title__text">知识库对话</span>
            <Tag color="processing" class="kb-card-title__tag">
              {{ sending ? '回答中' : '就绪' }}
            </Tag>
          </div>
        </template>

        <div class="kb-chat-shell">
          <div ref="logEl" class="kb-log">
            <div v-if="messages.length === 0" class="kb-empty-state">
              <div class="kb-empty-state__icon">
                <VbenIcon icon="solar:chat-round-line-line-duotone" />
              </div>
              <div class="kb-empty-state__title">开始一次配置检索问答</div>
              <div class="kb-empty-state__hint">
                例如：AdGiftData 表里有哪些字段？
              </div>
            </div>
            <div
              v-for="(m, i) in messages"
              :key="i"
              :class="['kb-msg', `kb-msg-${m.role}`]"
            >
              <div class="kb-msg__meta">
                <div :class="['kb-msg__avatar', `kb-msg__avatar-${m.role}`]">
                  <VbenIcon
                    :icon="
                      m.role === 'user'
                        ? 'solar:user-bold'
                        : m.role === 'err'
                          ? 'solar:danger-circle-bold'
                          : 'solar:stars-bold'
                    "
                  />
                </div>
                <div class="kb-msg__role">
                  {{ m.role === 'user' ? '你' : m.role === 'err' ? '错误' : '助手' }}
                </div>
              </div>
              <div :class="['kb-body', `kb-body-${m.role}`, { 'kb-cursor': m.streaming }]">
                <template v-if="m.role === 'user'">
                  {{ m.text }}
                </template>
                <div v-else class="kb-markdown" v-html="renderMarkdown(m.text)"></div>
              </div>
              <details v-if="m.sources && m.sources.length" class="kb-sources">
                <summary>
                  <span>来源片段</span>
                  <Tag color="default">{{ m.sources.length }}</Tag>
                </summary>
                <ol>
                  <li v-for="(s, j) in m.sources" :key="j" class="kb-sources__item">
                    <span class="kb-score">{{ s.score.toFixed(2) }}</span>
                    <span class="kb-cid">{{ s.chunk_id }}</span>
                    <span v-if="s.summary" class="kb-summary">
                      {{ s.summary.slice(0, 60) }}
                    </span>
                  </li>
                </ol>
              </details>
            </div>
            <div ref="logBottomEl" class="kb-log-bottom"></div>
          </div>

          <form class="kb-form" @submit.prevent="send">
            <div class="kb-form__editor">
              <textarea
                v-model="input"
                placeholder="输入问题，Enter 发送，Shift+Enter 换行"
                :disabled="sending"
                @keydown="onKeydown"
              />
            </div>
            <div class="kb-form__actions">
              <Button
                class="kb-send-btn"
                type="primary"
                :loading="sending"
                :disabled="!input.trim()"
                @click="send"
              >
                <template #icon>
                  <VbenIcon icon="solar:arrow-up-linear" />
                </template>
                发送
              </Button>
            </div>
          </form>
        </div>
      </Card>
    </div>
  </Page>
</template>

<style scoped>
.kb-chat-page {
  min-height: 100%;
}

.kb-hero-card,
.kb-chat-card {
  overflow: hidden;
  border-radius: 24px;
  background: rgb(255 255 255 / 96%);
  box-shadow: 0 16px 34px rgb(15 23 42 / 8%);
}

.kb-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.9fr);
  gap: 24px;
  align-items: stretch;
}

.kb-hero__content {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.kb-hero__title {
  display: flex;
  gap: 16px;
  align-items: flex-start;
}

.kb-hero__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 18px;
  background: linear-gradient(135deg, #fde68a 0%, #f59e0b 100%);
  color: #78350f;
  font-size: 24px;
  box-shadow: 0 14px 28px rgb(245 158 11 / 22%);
}

.kb-hero__heading {
  font-size: 24px;
  font-weight: 700;
  color: #0f172a;
  line-height: 1.2;
}

.kb-hero__subheading {
  margin-top: 8px;
  color: #64748b;
  line-height: 1.7;
}

.kb-hero__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.kb-hero__tag {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding-inline: 10px;
  font-weight: 700;
}

.kb-hero__examples {
  border: 1px solid rgb(191 219 254 / 70%);
  border-radius: 22px;
  background: linear-gradient(180deg, rgb(255 255 255 / 98%) 0%, rgb(239 246 255 / 96%) 100%);
  padding: 20px;
}

.kb-hero__examples-title {
  margin-bottom: 12px;
  font-size: 14px;
  font-weight: 700;
  color: #1e3a8a;
}

.kb-hero__examples-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.kb-example-btn {
  justify-content: flex-start;
  min-height: 42px;
  border-radius: 14px;
  border-color: rgb(191 219 254 / 80%);
  background: rgb(255 255 255 / 92%);
  color: #1e293b;
  font-weight: 600;
}

.kb-card-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.kb-card-title__text {
  font-size: 16px;
  font-weight: 700;
  color: #0f172a;
}

.kb-card-title__tag {
  border-radius: 999px;
  font-weight: 700;
}

.kb-chat-shell {
  display: flex;
  flex-direction: column;
  min-height: 680px;
}

.kb-log {
  flex: 1;
  overflow-y: auto;
  padding: 8px 4px 16px;
}

.kb-empty-state {
  display: flex;
  min-height: 420px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 40px 20px;
}

.kb-empty-state__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 68px;
  height: 68px;
  border-radius: 22px;
  background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
  color: #2563eb;
  font-size: 30px;
  box-shadow: 0 16px 28px rgb(59 130 246 / 16%);
}

.kb-empty-state__title {
  margin-top: 18px;
  font-size: 20px;
  font-weight: 700;
  color: #0f172a;
}

.kb-empty-state__hint {
  margin-top: 8px;
  color: #64748b;
  font-size: 14px;
}

.kb-msg {
  margin-bottom: 20px;
}

.kb-msg__meta {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
}

.kb-msg__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 12px;
  font-size: 16px;
}

.kb-msg__avatar-user {
  background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
  color: #1d4ed8;
}

.kb-msg__avatar-bot {
  background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
  color: #b45309;
}

.kb-msg__avatar-err {
  background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
  color: #dc2626;
}

.kb-msg__role {
  font-size: 13px;
  font-weight: 700;
  color: #64748b;
}

.kb-body {
  padding: 14px 16px;
  border-radius: 18px;
  word-break: break-word;
  border: 1px solid rgb(226 232 240 / 90%);
  line-height: 1.75;
  box-shadow: 0 10px 20px rgb(15 23 42 / 4%);
}

.kb-body-user {
  background: linear-gradient(180deg, rgb(239 246 255 / 95%) 0%, rgb(219 234 254 / 96%) 100%);
  border-color: rgb(147 197 253 / 80%);
  color: #0f172a;
  white-space: pre-wrap;
}

.kb-body-bot {
  background: linear-gradient(180deg, rgb(255 255 255 / 98%) 0%, rgb(248 250 252 / 98%) 100%);
  color: #1e293b;
}

.kb-body-err {
  background: linear-gradient(180deg, rgb(254 242 242 / 98%) 0%, rgb(254 226 226 / 98%) 100%);
  color: #b91c1c;
  border-color: rgb(252 165 165 / 70%);
}

.kb-markdown :deep(*) {
  max-width: 100%;
}

.kb-markdown :deep(> :first-child) {
  margin-top: 0;
}

.kb-markdown :deep(> :last-child) {
  margin-bottom: 0;
}

.kb-markdown :deep(p),
.kb-markdown :deep(ul),
.kb-markdown :deep(ol),
.kb-markdown :deep(pre),
.kb-markdown :deep(blockquote),
.kb-markdown :deep(table) {
  margin: 0 0 12px;
}

.kb-markdown :deep(h1),
.kb-markdown :deep(h2),
.kb-markdown :deep(h3),
.kb-markdown :deep(h4) {
  margin: 0 0 10px;
  color: #0f172a;
  font-weight: 700;
  line-height: 1.4;
}

.kb-markdown :deep(h1) {
  font-size: 22px;
}

.kb-markdown :deep(h2) {
  font-size: 18px;
}

.kb-markdown :deep(h3) {
  font-size: 16px;
}

.kb-markdown :deep(ul),
.kb-markdown :deep(ol) {
  padding-left: 20px;
}

.kb-markdown :deep(li + li) {
  margin-top: 4px;
}

.kb-markdown :deep(code) {
  border-radius: 6px;
  background: rgb(226 232 240 / 75%);
  padding: 2px 6px;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-size: 12px;
}

.kb-markdown :deep(pre) {
  overflow-x: auto;
  border: 1px solid rgb(226 232 240 / 90%);
  border-radius: 14px;
  background: #0f172a;
  padding: 14px 16px;
  color: #e2e8f0;
}

.kb-markdown :deep(pre code) {
  background: transparent;
  padding: 0;
  color: inherit;
  font-size: 12px;
}

.kb-markdown :deep(blockquote) {
  border-left: 4px solid #93c5fd;
  background: rgb(239 246 255 / 90%);
  border-radius: 0 12px 12px 0;
  padding: 10px 12px;
  color: #334155;
}

.kb-markdown :deep(a) {
  color: #2563eb;
  text-decoration: underline;
}

.kb-markdown :deep(table) {
  width: 100%;
  border-collapse: collapse;
  overflow: hidden;
  border-radius: 12px;
}

.kb-markdown :deep(th),
.kb-markdown :deep(td) {
  border: 1px solid rgb(226 232 240 / 90%);
  padding: 8px 10px;
  text-align: left;
  vertical-align: top;
}

.kb-markdown :deep(th) {
  background: rgb(241 245 249 / 98%);
  color: #0f172a;
  font-weight: 700;
}

.kb-cursor::after {
  content: '▍';
  color: #2563eb;
  animation: kb-blink 1s steps(2) infinite;
}

@keyframes kb-blink {
  50% {
    opacity: 0;
  }
}

.kb-sources {
  margin-top: 8px;
  font-size: 13px;
  background: rgb(248 250 252 / 98%);
  border: 1px solid rgb(226 232 240 / 90%);
  border-radius: 14px;
}

.kb-sources summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 14px;
  color: #64748b;
  font-weight: 600;
  user-select: none;
}

.kb-sources[open] summary {
  border-bottom: 1px solid rgb(226 232 240 / 90%);
}

.kb-sources ol {
  margin: 0;
  padding: 10px 14px 14px 30px;
}

.kb-sources__item {
  margin: 4px 0;
}

.kb-score {
  display: inline-block;
  min-width: 42px;
  color: #2563eb;
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  font-weight: 700;
}

.kb-cid {
  font-family: ui-monospace, 'SF Mono', Menlo, monospace;
  color: #334155;
}

.kb-summary {
  color: #64748b;
  margin-left: 6px;
}

.kb-form {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 16px;
  align-items: end;
  margin-top: auto;
  padding-top: 18px;
  border-top: 1px solid rgb(226 232 240 / 90%);
}

.kb-form__editor {
  border: 1px solid rgb(226 232 240 / 90%);
  border-radius: 20px;
  background: linear-gradient(180deg, rgb(255 255 255 / 100%) 0%, rgb(248 250 252 / 100%) 100%);
  padding: 12px 14px;
  box-shadow: inset 0 1px 0 rgb(255 255 255 / 60%);
}

.kb-form textarea {
  flex: 1;
  resize: none;
  width: 100%;
  min-height: 86px;
  max-height: 160px;
  padding: 0;
  border: none;
  background: transparent;
  color: #0f172a;
  font: inherit;
  outline: none;
  line-height: 1.7;
}

.kb-form textarea:focus {
  border-color: transparent;
}

.kb-form__actions {
  display: flex;
  min-width: 148px;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

:deep(.kb-send-btn) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 120px;
  height: 42px;
  padding-inline: 18px;
  border-radius: 14px;
  font-weight: 700;
  white-space: nowrap;
  box-shadow: 0 12px 24px rgb(37 99 235 / 18%);
}

:deep(.kb-send-btn .ant-btn-icon) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-inline-end: 6px;
  line-height: 1;
}

:deep(.kb-send-btn .ant-btn-loading-icon) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-inline-end: 6px;
}

:deep(.kb-send-btn > span) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

@media (width <= 960px) {
  .kb-hero {
    grid-template-columns: 1fr;
  }

  .kb-chat-shell {
    min-height: 600px;
  }

  .kb-form {
    grid-template-columns: 1fr;
  }

  .kb-form__actions {
    min-width: 0;
    align-items: stretch;
  }

  .kb-card-title {
    align-items: flex-start;
    flex-direction: column;
  }
}

@media (width <= 640px) {
  .kb-hero__title {
    flex-direction: column;
  }

  .kb-log {
    padding-inline: 0;
  }

  .kb-body {
    border-radius: 16px;
  }
}
</style>
