<template>
  <div class="assistant-text">
    <ThinkingBlock
      v-if="message.thinking"
      :content="message.thinking"
      :is-streaming="isLoading && !message.content"
    />

    <div v-if="toolCalls.length > 0" class="assistant-text__tools">
      <ToolCallCard
        v-for="tc in toolCalls"
        :key="tc.id"
        :tool-call="tc"
      />
    </div>

    <div v-if="message.content" ref="markdownRef" class="assistant-text__markdown" v-html="renderedContent"></div>

    <div v-else-if="isLoading" class="assistant-text__loading">
      <a-spin size="small" />
    </div>

    <div v-if="showRouting" class="assistant-text__routing">
      <a-tag color="purple">
        <ThunderboltOutlined /> {{ message.matched_skill }}
      </a-tag>
      <a-tag v-if="message.confidence !== undefined" color="green">
        置信度: {{ (message.confidence * 100).toFixed(1) }}%
      </a-tag>
      <a-tag v-if="message.routing_method" color="default">
        {{ message.routing_method }}
      </a-tag>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, watch, nextTick, onBeforeUnmount } from 'vue'
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
import { Tag as ATag, Spin as ASpin, message as antMessage } from 'ant-design-vue'
import { ThunderboltOutlined } from '@ant-design/icons-vue'
import hljs from 'highlight.js/lib/core'
import python from 'highlight.js/lib/languages/python'
import javascript from 'highlight.js/lib/languages/javascript'
import typescript from 'highlight.js/lib/languages/typescript'
import json from 'highlight.js/lib/languages/json'
import bash from 'highlight.js/lib/languages/bash'
import yaml from 'highlight.js/lib/languages/yaml'
import sql from 'highlight.js/lib/languages/sql'
import xml from 'highlight.js/lib/languages/xml'
import css from 'highlight.js/lib/languages/css'
import markdown from 'highlight.js/lib/languages/markdown'
import type { IChatMessage } from '@/api/types'
import ToolCallCard from '@/components/chat/ToolCallCard.vue'
import ThinkingBlock from '@/components/chat/ThinkingBlock.vue'

hljs.registerLanguage('python', python)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('json', json)
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('yaml', yaml)
hljs.registerLanguage('sql', sql)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('css', css)
hljs.registerLanguage('markdown', markdown)

interface Props {
  message: IChatMessage
}

const props = defineProps<Props>()
const markdownRef = ref<HTMLElement | null>(null)

const md = new MarkdownIt({
  html: false,
  linkify: true,
  breaks: true,
  highlight(str: string, lang: string): string {
    let highlighted: string
    if (lang && hljs.getLanguage(lang)) {
      highlighted = hljs.highlight(str, { language: lang, ignoreIllegals: true }).value
    } else {
      highlighted = MarkdownIt.prototype.utils.escapeHtml(str)
    }
    return `<pre class="hljs" data-language="${lang || ''}"><code>${highlighted}</code></pre>`
  },
})

md.renderer.rules.image = (tokens, idx) => {
  const token = tokens[idx]
  const rawSrc = token.attrGet('src') || ''
  const rawAlt = token.content || ''
  if (!/^https?:\/\//i.test(rawSrc)) return ''
  const src = rawSrc.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
  const alt = rawAlt.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')
  return `<img src="${src}" alt="${alt}" class="assistant-text__inline-image" loading="lazy" />`
}

function sanitize(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: [
      'p', 'br', 'strong', 'em', 'del', 'code', 'pre', 'a',
      'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'blockquote',
      'table', 'thead', 'tbody', 'tr', 'th', 'td', 'span',
      'div', 'img', 'sup', 'sub',
    ],
    ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'data-language', 'src', 'alt', 'loading'],
    ALLOW_DATA_ATTR: false,
    ALLOWED_URI_REGEXP: /^(?:(?:https?):\/\/[^\s]*)$/i,
  })
}

const isLoading = computed(() => {
  return props.message.role === 'assistant' && props.message.status === 'pending' && !props.message.content
})

const showRouting = computed(() => {
  return props.message.role === 'assistant' && props.message.matched_skill
})

/**
 * When the assistant is still streaming, defer re-parsing the markdown by a
 * short window so we don't re-render on every token delta (which causes
 * visible flicker on tables and code fences). When streaming ends, render
 * immediately.
 */
const isStreaming = computed(
  () => props.message.role === 'assistant' && props.message.status === 'pending',
)

const renderedContent = ref<string>('')
let renderTimer: ReturnType<typeof setTimeout> | null = null

function doRender(): void {
  renderTimer = null
  if (!props.message.content) {
    renderedContent.value = ''
    return
  }
  renderedContent.value = sanitize(md.render(props.message.content))
}

watch(
  () => props.message.content,
  () => {
    if (renderTimer !== null) {
      clearTimeout(renderTimer)
      renderTimer = null
    }
    if (isStreaming.value) {
      renderTimer = setTimeout(doRender, 80)
    } else {
      doRender()
    }
  },
  { immediate: true },
)

watch(isStreaming, (now) => {
  if (!now && renderTimer !== null) {
    clearTimeout(renderTimer)
    renderTimer = null
    doRender()
  }
})

onBeforeUnmount(() => {
  if (renderTimer !== null) {
    clearTimeout(renderTimer)
    renderTimer = null
  }
})

const toolCalls = computed(() => props.message.tool_calls ?? [])

watch(renderedContent, () => {
  nextTick(() => {
    if (!markdownRef.value) return
    const pres = markdownRef.value.querySelectorAll('pre.hljs')
    pres.forEach((pre) => {
      if (pre.querySelector('.assistant-text__code-header')) return
      const lang = pre.getAttribute('data-language') || ''
      const code = pre.querySelector('code')
      if (!code) return

      const header = document.createElement('div')
      header.className = 'assistant-text__code-header'
      if (lang) {
        const langLabel = document.createElement('span')
        langLabel.className = 'assistant-text__code-lang'
        langLabel.textContent = lang
        header.appendChild(langLabel)
      }
      const copyBtn = document.createElement('button')
      copyBtn.className = 'assistant-text__copy-btn'
      copyBtn.innerHTML = '<span class="anticon"><svg viewBox="0 0 1024 1024" width="14" height="14" fill="currentColor"><path d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v608c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V112c0-26.5-21.5-48-48-48zM704 256H192c-26.5 0-48 21.5-48 48v608c0 26.5 21.5 48 48 48h512c26.5 0 48-21.5 48-48V304c0-26.5-21.5-48-48-48zm-8 608H200V312h496v552z"/></svg></span>'
      copyBtn.title = '复制代码'
      copyBtn.addEventListener('click', () => {
        navigator.clipboard.writeText(code.textContent || '').then(() => {
          antMessage.success('已复制到剪贴板')
        }).catch(() => {
          antMessage.error('复制失败')
        })
      })
      header.appendChild(copyBtn)
      pre.insertBefore(header, pre.firstChild)
    })
  })
})
</script>

<style scoped>
.assistant-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  line-height: var(--leading-message);
  font-size: var(--font-base);
  color: var(--text-primary);
  word-break: break-word;
}

.assistant-text__markdown {
  overflow-x: auto;
}

.assistant-text__markdown :deep(p) {
  margin-bottom: var(--space-3);
}

.assistant-text__markdown :deep(p:last-child) {
  margin-bottom: 0;
}

.assistant-text__markdown :deep(pre) {
  background: var(--code-bg);
  color: var(--code-fg);
  padding: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  margin: var(--space-3) 0;
  font-size: var(--font-sm);
  position: relative;
}

.assistant-text__markdown :deep(pre.hljs) {
  background: var(--code-bg);
}

.assistant-text__markdown :deep(pre code) {
  display: block;
  padding: var(--space-3);
  overflow-x: auto;
  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
}

.assistant-text__markdown :deep(.assistant-text__code-header) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-1) var(--space-3);
  background: rgba(0, 0, 0, 0.2);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

.assistant-text__markdown :deep(.assistant-text__code-lang) {
  font-size: var(--font-xs);
  color: var(--code-comment);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.assistant-text__markdown :deep(.assistant-text__copy-btn) {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: none;
  background: transparent;
  color: var(--code-comment);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: all var(--transition-fast);
  padding: 0;
}

.assistant-text__markdown :deep(.assistant-text__copy-btn:hover) {
  color: var(--code-fg);
  background: rgba(255, 255, 255, 0.1);
}

.assistant-text__markdown :deep(.hljs-keyword) { color: var(--code-keyword); }
.assistant-text__markdown :deep(.hljs-string) { color: var(--code-string); }
.assistant-text__markdown :deep(.hljs-number) { color: var(--code-number); }
.assistant-text__markdown :deep(.hljs-comment) { color: var(--code-comment); font-style: italic; }
.assistant-text__markdown :deep(.hljs-function) { color: var(--code-function); }
.assistant-text__markdown :deep(.hljs-variable) { color: var(--code-variable); }
.assistant-text__markdown :deep(.hljs-type) { color: var(--code-type); }
.assistant-text__markdown :deep(.hljs-built_in) { color: var(--code-type); }
.assistant-text__markdown :deep(.hljs-attr) { color: var(--code-function); }
.assistant-text__markdown :deep(.hljs-selector-tag) { color: var(--code-keyword); }
.assistant-text__markdown :deep(.hljs-selector-class) { color: var(--code-type); }
.assistant-text__markdown :deep(.hljs-selector-id) { color: var(--code-type); }
.assistant-text__markdown :deep(.hljs-literal) { color: var(--code-number); }
.assistant-text__markdown :deep(.hljs-meta) { color: var(--code-comment); }
.assistant-text__markdown :deep(.hljs-title) { color: var(--code-function); }
.assistant-text__markdown :deep(.hljs-params) { color: var(--code-fg); }
.assistant-text__markdown :deep(.hljs-section) { color: var(--code-function); }
.assistant-text__markdown :deep(.hljs-addition) { background: var(--code-added-bg); }
.assistant-text__markdown :deep(.hljs-deletion) { background: var(--code-removed-bg); }

.assistant-text__markdown :deep(.assistant-text__inline-image) {
  max-width: 100%;
  max-height: 200px;
  object-fit: contain;
  border-radius: var(--radius-md);
  cursor: pointer;
  margin: var(--space-2) 0;
  transition: opacity var(--transition-fast);
}

.assistant-text__markdown :deep(.assistant-text__inline-image:hover) {
  opacity: 0.9;
}

.assistant-text__markdown :deep(code) {
  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
  font-size: var(--font-sm);
}

.assistant-text__markdown :deep(:not(pre) > code) {
  background: var(--bg-secondary);
  color: var(--text-primary);
  padding: 2px var(--space-1);
  border-radius: var(--radius-sm);
  font-size: var(--font-xs);
}

.assistant-text__markdown :deep(ul),
.assistant-text__markdown :deep(ol) {
  padding-left: var(--space-5);
  margin-bottom: var(--space-3);
}

.assistant-text__markdown :deep(table) {
  width: 100%;
  border-collapse: collapse;
  margin: var(--space-3) 0;
  font-size: var(--font-sm);
  background: var(--bg-secondary);
  border-radius: var(--radius-md);
  overflow: hidden;
}

.assistant-text__markdown :deep(thead) {
  background: rgba(0, 0, 0, 0.18);
}

.assistant-text__markdown :deep(th),
.assistant-text__markdown :deep(td) {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: 1px solid var(--border-color);
  vertical-align: top;
}

.assistant-text__markdown :deep(th) {
  font-weight: var(--font-weight-semibold, 600);
  color: var(--text-primary);
}

.assistant-text__markdown :deep(tbody tr:last-child td) {
  border-bottom: none;
}

.assistant-text__markdown :deep(tbody tr:hover) {
  background: rgba(255, 255, 255, 0.03);
}

.assistant-text__markdown :deep(h1),
.assistant-text__markdown :deep(h2),
.assistant-text__markdown :deep(h3) {
  margin-top: var(--space-4);
  margin-bottom: var(--space-2);
  font-weight: var(--font-weight-semibold);
}

.assistant-text__tools {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  max-width: 100%;
}

.assistant-text__loading {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-tertiary);
}

.assistant-text__routing {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.assistant-text__routing :deep(.ant-tag) {
  border: none;
  background: var(--bg-secondary);
  color: var(--text-tertiary);
  font-size: var(--font-xs);
}
</style>
