<template>
  <div class="input-box" :class="customClasses" @click="focusInput">
    <div class="top-slot">
      <slot name="top"></slot>
    </div>

    <div class="expand-options" v-if="hasOptionsLeft">
      <a-popover
        v-model:open="optionsExpanded"
        placement="bottomLeft"
        trigger="click"
        :overlay-inner-style="{ padding: '4px' }"
      >
        <template #content>
          <slot name="options-left">
            <div class="no-options">没有配置 options</div>
          </slot>
        </template>
        <a-button type="text" class="expand-btn">
          <template #icon>
            <Paperclip :size="16" :class="{ rotated: optionsExpanded }" />
          </template>
        </a-button>
      </a-popover>
      <slot name="actions-left"></slot>
    </div>

    <div
      ref="inputRef"
      class="user-input mention-editor"
      role="textbox"
      aria-multiline="true"
      :aria-label="placeholder"
      :contenteditable="disabled ? 'false' : 'true'"
      :data-placeholder="placeholder"
      @keydown="handleKeyPress"
      @keyup="handleKeyUp"
      @input="handleInput"
      @focus="focusInput"
      @click="handleEditorClick"
      @paste="handlePaste"
      @compositionstart="handleCompositionStart"
      @compositionend="handleCompositionEnd"
    ></div>

    <!-- @ 提及选择弹窗 -->
    <div v-if="mentionPopupVisible" ref="mentionDropdownRef" class="mention-dropdown-wrapper">
      <div class="mention-popup" @mousedown.prevent>
        <!-- 文件列表 -->
        <div v-if="mentionItems.files.length > 0 || showFileSearchPrompt" class="mention-group">
          <div class="mention-group-title">文件</div>
          <div v-if="showFileSearchPrompt" class="mention-search-placeholder">
            输入相关内容以搜索文件
          </div>
          <template v-else>
            <div
              v-for="(item, index) in mentionItems.files"
              :key="'file-' + item.value"
              :class="['mention-item', 'file-item', { active: isItemSelected('file', index) }]"
              @click="insertMention(item)"
            >
              <div class="file-info-left">
                <component
                  :is="item.is_dir ? FolderFilled : getFileIcon(item.label)"
                  :style="{ color: item.is_dir ? '#ffa940' : getFileIconColor(item.label) }"
                  class="file-type-icon"
                />
                <span class="file-name" :title="item.label">
                  <span
                    v-for="(part, pIdx) in splitTextByQuery(item.label, mentionQuery)"
                    :key="pIdx"
                    :class="{ 'query-match': part.isMatch }"
                    >{{ part.text }}</span
                  >
                </span>
              </div>
              <span
                v-if="formatMentionPath(item.description)"
                class="file-parent-dir"
                :title="formatMentionPath(item.description)"
              >
                <span
                  v-for="(part, pIdx) in splitTextByQuery(
                    formatMentionPath(item.description),
                    mentionQuery
                  )"
                  :key="pIdx"
                  :class="{ 'query-match': part.isMatch }"
                  >{{ part.text }}</span
                >
              </span>
            </div>
          </template>
        </div>

        <!-- 知识库列表 -->
        <div v-if="mentionItems.knowledgeBases.length > 0" class="mention-group">
          <div class="mention-group-title">知识库</div>
          <div
            v-for="(item, index) in mentionItems.knowledgeBases"
            :key="'kb-' + item.value"
            :class="[
              'mention-item',
              'resource-item',
              { active: isItemSelected('knowledge', index) }
            ]"
            @click="insertMention(item)"
          >
            <div class="resource-name">
              <span
                v-for="(part, pIdx) in splitTextByQuery(item.label, mentionQuery)"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
            <div
              v-if="getMentionDescription(item.description)"
              class="resource-description"
              :title="getMentionDescription(item.description)"
            >
              <span
                v-for="(part, pIdx) in splitTextByQuery(
                  getMentionDescription(item.description),
                  mentionQuery
                )"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
          </div>
        </div>

        <!-- MCP 列表 -->
        <div v-if="mentionItems.mcps.length > 0" class="mention-group">
          <div class="mention-group-title">MCP</div>
          <div
            v-for="(item, index) in mentionItems.mcps"
            :key="'mcp-' + item.value"
            :class="['mention-item', 'resource-item', { active: isItemSelected('mcp', index) }]"
            @click="insertMention(item)"
          >
            <div class="resource-name">
              <span
                v-for="(part, pIdx) in splitTextByQuery(item.label, mentionQuery)"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
            <div
              v-if="getMentionDescription(item.description)"
              class="resource-description"
              :title="getMentionDescription(item.description)"
            >
              <span
                v-for="(part, pIdx) in splitTextByQuery(
                  getMentionDescription(item.description),
                  mentionQuery
                )"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
          </div>
        </div>

        <!-- Skills 列表 -->
        <div v-if="mentionItems.skills.length > 0" class="mention-group">
          <div class="mention-group-title">Skills</div>
          <div
            v-for="(item, index) in mentionItems.skills"
            :key="'skill-' + item.value"
            :class="['mention-item', 'resource-item', { active: isItemSelected('skill', index) }]"
            @click="insertMention(item)"
          >
            <div class="resource-name">
              <span
                v-for="(part, pIdx) in splitTextByQuery(item.label, mentionQuery)"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
            <div
              v-if="getMentionDescription(item.description)"
              class="resource-description"
              :title="getMentionDescription(item.description)"
            >
              <span
                v-for="(part, pIdx) in splitTextByQuery(
                  getMentionDescription(item.description),
                  mentionQuery
                )"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
          </div>
        </div>

        <!-- Subagents 列表 -->
        <div v-if="mentionItems.subagents.length > 0" class="mention-group">
          <div class="mention-group-title">Subagents</div>
          <div
            v-for="(item, index) in mentionItems.subagents"
            :key="'subagent-' + item.value"
            :class="[
              'mention-item',
              'resource-item',
              { active: isItemSelected('subagent', index) }
            ]"
            @click="insertMention(item)"
          >
            <div class="resource-name">
              <span
                v-for="(part, pIdx) in splitTextByQuery(item.label, mentionQuery)"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
            <div
              v-if="getMentionDescription(item.description)"
              class="resource-description"
              :title="getMentionDescription(item.description)"
            >
              <span
                v-for="(part, pIdx) in splitTextByQuery(
                  getMentionDescription(item.description),
                  mentionQuery
                )"
                :key="pIdx"
                :class="{ 'query-match': part.isMatch }"
                >{{ part.text }}</span
              >
            </div>
          </div>
        </div>

        <!-- 无结果 -->
        <div v-if="!hasAnyItems" class="mention-empty">暂无可引用的项</div>
      </div>
    </div>

    <div class="send-button-container">
      <slot name="actions-right"></slot>
      <a-tooltip :title="isLoading ? '停止回答' : ''">
        <a-button
          @click="handleSendOrStop"
          :disabled="sendButtonDisabled"
          type="link"
          class="send-button"
        >
          <template #icon>
            <component :is="getIcon" class="send-btn" />
          </template>
        </a-button>
      </a-tooltip>
    </div>

    <div class="bottom-slot">
      <slot name="bottom"></slot>
    </div>
  </div>
</template>

<script setup>
import {
  ref,
  computed,
  onMounted,
  nextTick,
  watch,
  onBeforeUnmount,
  useSlots,
  h,
  render
} from 'vue'
import { SendOutlined, ArrowUpOutlined, PauseOutlined, FolderFilled } from '@ant-design/icons-vue'
import { Paperclip } from 'lucide-vue-next'
import { searchMentionFiles } from '@/apis/mention_api'
import { getFileIcon, getFileIconColor } from '@/utils/file_utils'
import {
  getMentionIconComponent,
  getMentionIconStyle,
  MENTION_ICON_SIZE,
  MENTION_ICON_STROKE_WIDTH
} from '@/utils/mention_icon_utils'
import {
  buildMentionDisplayLabels,
  expandMentionDeletionRange,
  findActiveMentionQuery,
  formatMentionToken,
  getMentionDisplayLabel,
  mentionTypePrefixMap,
  parseMentionText,
  replaceRawRange
} from '@/utils/mention_utils'

// 点击外部关闭下拉框
const mentionDropdownRef = ref(null)
const closeMentionPopup = (e) => {
  if (!mentionPopupVisible.value) return
  if (inputRef.value?.contains(e.target)) return
  if (mentionDropdownRef.value?.contains(e.target)) return
  mentionPopupVisible.value = false
}

const inputRef = ref(null)
const optionsExpanded = ref(false)
// 用于防抖的定时器
const debounceTimer = ref(null)
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  },
  placeholder: {
    type: String,
    default: '输入问题...'
  },
  isLoading: {
    type: Boolean,
    default: false
  },
  disabled: {
    type: Boolean,
    default: false
  },
  sendButtonDisabled: {
    type: Boolean,
    default: false
  },
  autoSize: {
    type: Object,
    default: () => ({ minRows: 2, maxRows: 6 })
  },
  sendIcon: {
    type: String,
    default: 'ArrowUpOutlined'
  },
  customClasses: {
    type: Object,
    default: () => ({})
  },
  mention: {
    type: Object,
    default: () => null
  },
  threadId: {
    type: String,
    default: ''
  }
})

const emit = defineEmits(['update:modelValue', 'send', 'keydown'])
const slots = useSlots()

// @ 提及功能是否启用
const mentionEnabled = computed(() => {
  return !!props.mention
})

const mentionDisplayLabels = computed(() => buildMentionDisplayLabels(props.mention || {}))

let lastRawSelectionRange = null
let lastSyncedEditorValue = props.modelValue || ''

const getStoredRawSelectionRange = () => {
  const length = getEditorRawValue().length
  if (!lastRawSelectionRange) {
    return { start: length, end: length, collapsed: true }
  }

  const start = Math.max(0, Math.min(lastRawSelectionRange.start, length))
  const end = Math.max(start, Math.min(lastRawSelectionRange.end, length))
  return { start, end, collapsed: start === end }
}

const rememberRawSelectionRange = (range) => {
  lastRawSelectionRange = { ...range }
  return range
}

const formatMentionPath = (path) => {
  if (!path) return ''
  let cleanPath = path.replace(/^\/?home\/gem\/user-data\/?/, '')
  if (cleanPath.startsWith('/')) {
    cleanPath = cleanPath.substring(1)
  }
  // 如果以 / 结尾，说明它是一个目录，我们先去掉末尾的 / 之后再算父目录
  let isDir = cleanPath.endsWith('/')
  let pathForParent = isDir ? cleanPath.substring(0, cleanPath.length - 1) : cleanPath

  const lastSlashIndex = pathForParent.lastIndexOf('/')
  if (lastSlashIndex === -1) {
    return ''
  }
  return pathForParent.substring(0, lastSlashIndex + 1)
}

const getMentionDescription = (description) => {
  const value = String(description || '').trim()
  if (!value || value === '暂无描述') return ''
  return value
}

// 高性能且安全的关键字切片高亮解析函数 (100% 防御 XSS，避开危险的 v-html)
const splitTextByQuery = (text, query) => {
  if (!text) return []
  if (!query) return [{ text, isMatch: false }]

  const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  const regex = new RegExp(`(${escapedQuery})`, 'gi')
  const parts = text.split(regex)

  return parts.map((part) => ({
    text: part,
    isMatch: part.toLowerCase() === query.toLowerCase()
  }))
}

const isTextNode = (node) => node?.nodeType === Node.TEXT_NODE
const isElementNode = (node) => node?.nodeType === Node.ELEMENT_NODE
const isMentionNode = (node) => isElementNode(node) && node.dataset?.mentionRaw !== undefined
const isLineBreakNode = (node) => isElementNode(node) && node.tagName === 'BR'
const childIndex = (node) => Array.prototype.indexOf.call(node.parentNode?.childNodes || [], node)

const getRawNodeLength = (node) => {
  if (!node) return 0
  if (isTextNode(node)) return node.textContent?.length || 0
  if (isMentionNode(node)) return node.dataset.mentionRaw?.length || 0
  if (isLineBreakNode(node)) return 1
  return Array.from(node.childNodes || []).reduce(
    (total, child) => total + getRawNodeLength(child),
    0
  )
}

const serializeEditorNode = (node) => {
  if (!node) return ''
  if (isTextNode(node)) return node.textContent || ''
  if (isMentionNode(node)) return node.dataset.mentionRaw || ''
  if (isLineBreakNode(node)) return '\n'
  return Array.from(node.childNodes || [])
    .map((child) => serializeEditorNode(child))
    .join('')
}

const serializeEditorContent = () => serializeEditorNode(inputRef.value)
const getEditorRawValue = () => (inputRef.value ? serializeEditorContent() : inputValue.value)

const unmountEditorMentionIcons = () => {
  inputRef.value
    ?.querySelectorAll('.mention-ref-icon[data-vue-icon]')
    .forEach((container) => render(null, container))
}

const createEditorMentionElement = (segment) => {
  const token = document.createElement('span')
  token.className = `mention-ref-token mention-ref-${segment.type} mention-ref-editable`
  token.contentEditable = 'false'
  token.dataset.mentionRaw = segment.raw
  token.dataset.mentionType = segment.type
  token.dataset.mentionValue = segment.value
  token.title = segment.raw

  const icon = document.createElement('span')
  icon.className = 'mention-ref-icon'
  icon.dataset.vueIcon = 'true'
  Object.assign(icon.style, getMentionIconStyle(segment.type, segment.value) || {})
  render(
    h(getMentionIconComponent(segment.type, segment.value), {
      size: MENTION_ICON_SIZE,
      strokeWidth: MENTION_ICON_STROKE_WIDTH
    }),
    icon
  )
  token.appendChild(icon)

  const label = document.createElement('span')
  label.className = 'mention-ref-label'
  label.textContent = getMentionDisplayLabel(
    segment.type,
    segment.value,
    mentionDisplayLabels.value
  )
  token.appendChild(label)

  return token
}

const renderEditorContent = (raw = '') => {
  const editor = inputRef.value
  if (!editor) return

  unmountEditorMentionIcons()
  editor.replaceChildren()
  parseMentionText(raw).forEach((segment) => {
    editor.appendChild(
      segment.kind === 'text'
        ? document.createTextNode(segment.text)
        : createEditorMentionElement(segment)
    )
  })
  lastSyncedEditorValue = String(raw || '')
}

const isNodeInEditor = (node) => {
  const editor = inputRef.value
  if (!editor || !node) return false
  const element = isElementNode(node) ? node : node.parentNode
  return element === editor || editor.contains(element)
}

const getRawOffsetFromDomPoint = (container, offset) => {
  const editor = inputRef.value
  if (!editor || !isNodeInEditor(container)) return getEditorRawValue().length

  let rawOffset = 0
  let found = false

  const visit = (node) => {
    if (!node || found) return

    if (node === container) {
      if (isTextNode(node)) {
        rawOffset += Math.min(offset, node.textContent?.length || 0)
      } else if (isMentionNode(node)) {
        rawOffset += offset > 0 ? getRawNodeLength(node) : 0
      } else {
        const children = Array.from(node.childNodes || [])
        for (let index = 0; index < Math.min(offset, children.length); index++) {
          rawOffset += getRawNodeLength(children[index])
        }
      }
      found = true
      return
    }

    if (isTextNode(node) || isMentionNode(node) || isLineBreakNode(node)) {
      rawOffset += getRawNodeLength(node)
      return
    }

    for (const child of Array.from(node.childNodes || [])) {
      visit(child)
      if (found) return
    }
  }

  visit(editor)
  return rawOffset
}

const getRawSelectionRange = () => {
  const selection = window.getSelection()
  if (!selection || selection.rangeCount === 0 || !isNodeInEditor(selection.anchorNode)) {
    return getStoredRawSelectionRange()
  }

  const anchor = getRawOffsetFromDomPoint(selection.anchorNode, selection.anchorOffset)
  const focus = getRawOffsetFromDomPoint(selection.focusNode, selection.focusOffset)
  return rememberRawSelectionRange({
    start: Math.min(anchor, focus),
    end: Math.max(anchor, focus),
    collapsed: anchor === focus
  })
}

const getDomPointForRawOffset = (rawOffset) => {
  const editor = inputRef.value
  const offset = Math.max(0, Math.min(rawOffset, getEditorRawValue().length))
  let remaining = offset

  const pointBeforeNode = (node) => ({ node: node.parentNode || editor, offset: childIndex(node) })
  const pointAfterNode = (node) => ({
    node: node.parentNode || editor,
    offset: childIndex(node) + 1
  })

  const visit = (node) => {
    if (!node) return null

    if (isTextNode(node)) {
      const length = node.textContent?.length || 0
      if (remaining <= length) {
        return { node, offset: remaining }
      }
      remaining -= length
      return null
    }

    if (isMentionNode(node) || isLineBreakNode(node)) {
      const length = getRawNodeLength(node)
      if (remaining === 0) return pointBeforeNode(node)
      if (remaining <= length) return pointAfterNode(node)
      remaining -= length
      return null
    }

    for (const child of Array.from(node.childNodes || [])) {
      const point = visit(child)
      if (point) return point
    }

    return node === editor ? { node: editor, offset: editor.childNodes.length } : null
  }

  return visit(editor) || { node: editor, offset: editor?.childNodes.length || 0 }
}

const restoreEditorSelection = (start, end = start) => {
  const editor = inputRef.value
  const selection = window.getSelection()
  if (!editor || !selection) return

  const startPoint = getDomPointForRawOffset(start)
  const endPoint = getDomPointForRawOffset(end)
  const range = document.createRange()
  range.setStart(startPoint.node, startPoint.offset)
  range.setEnd(endPoint.node, endPoint.offset)
  selection.removeAllRanges()
  selection.addRange(range)
  rememberRawSelectionRange({ start, end, collapsed: start === end })
  editor.focus()
}

const updateRawValue = (value, caretStart, caretEnd = caretStart) => {
  renderEditorContent(value)
  emit('update:modelValue', value)
  nextTick(() => {
    restoreEditorSelection(caretStart, caretEnd)
    adjustTextareaHeight()
    if (mentionEnabled.value) {
      checkMentionTrigger()
    }
  })
}

const replaceCurrentRawSelection = (replacement) => {
  const currentValue = getEditorRawValue()
  const range = getRawSelectionRange()
  const nextValue = replaceRawRange(currentValue, range.start, range.end, replacement)
  const nextOffset = range.start + replacement.length
  updateRawValue(nextValue, nextOffset)
}

// 检测是否在 @ 触发位置
const checkMentionTrigger = () => {
  if (!inputRef.value || !mentionEnabled.value) return false

  const selectionRange = getRawSelectionRange()
  if (!selectionRange.collapsed) {
    mentionPopupVisible.value = false
    return false
  }

  const activeMention = findActiveMentionQuery(getEditorRawValue(), selectionRange.end)
  if (activeMention) {
    const sameQuery = mentionPopupVisible.value && mentionQuery.value === activeMention.query
    mentionQuery.value = activeMention.query
    mentionPopupVisible.value = true
    if (!sameQuery) {
      mentionSelectedIndex.value = 0
      updateMentionItems(mentionQuery.value)
    }
    return true
  }

  mentionPopupVisible.value = false
  return false
}

// 更新提及候选项
const updateMentionItems = (query = '') => {
  const normalizedQuery = String(query || '')
  if (!normalizedQuery) {
    clearTimeout(mentionSearchTimer)
    if (activeAbortController) {
      activeAbortController.abort()
      activeAbortController = null
    }
    searchRequestId.value++
  }

  if (!props.mention) {
    mentionItems.value = { files: [], knowledgeBases: [], mcps: [], skills: [], subagents: [] }
    return
  }

  const lowerQuery = normalizedQuery.toLowerCase()
  const { files = [], knowledgeBases = [], mcps = [], skills = [], subagents = [] } = props.mention

  const filterItems = (list) =>
    list.filter((item) => {
      const searchTexts = [
        item.label,
        item.value,
        item.description,
        item.resourceId,
        item.tokenLabel,
        item.type,
        mentionTypePrefixMap[item.type]
      ]
      return searchTexts.some((text) =>
        String(text || '')
          .toLowerCase()
          .includes(lowerQuery)
      )
    })

  // 本地临时文件/附件候选项过滤
  const localFileItems = files.map((f) => {
    const path = f.path || ''
    const fileName = path.split('/').pop() || path
    return {
      value: path,
      label: fileName,
      type: 'file',
      insertValue: path || fileName,
      tokenLabel: formatMentionToken('file', fileName),
      description: path
    }
  })

  const filteredLocalFiles = normalizedQuery ? filterItems(localFileItems) : []

  const knowledgeItems = knowledgeBases.map((kb) => {
    const kbName = kb.name || ''
    return {
      value: kbName,
      label: kbName,
      type: 'knowledge',
      insertValue: kbName,
      tokenLabel: formatMentionToken('knowledge', kbName),
      description: kb.description || '',
      resourceId: kb.kb_id
    }
  })

  const mcpItems = mcps.map((m) => {
    const mcpValue = m.slug || m.value || m.id || m.name || ''
    const mcpLabel = m.name || m.label || mcpValue
    return {
      value: mcpValue,
      label: mcpLabel,
      type: 'mcp',
      insertValue: mcpValue,
      tokenLabel: formatMentionToken('mcp', mcpLabel),
      description: m.description || ''
    }
  })

  const skillItems = skills.map((skill) => {
    const skillValue = skill.slug || skill.value || skill.id || skill.name || ''
    const skillLabel = skill.name || skill.label || skillValue
    return {
      value: skillValue,
      label: skillLabel,
      type: 'skill',
      insertValue: skillValue,
      tokenLabel: formatMentionToken('skill', skillLabel),
      description: skill.description || ''
    }
  })

  const subagentItems = subagents.map((subagent) => {
    const subagentValue = subagent.id || subagent.value || subagent.slug || subagent.name || ''
    const subagentLabel = subagent.name || subagent.label || subagentValue
    return {
      value: subagentValue,
      label: subagentLabel,
      type: 'subagent',
      insertValue: subagentValue,
      tokenLabel: formatMentionToken('subagent', subagentLabel),
      description: subagent.description || ''
    }
  })

  // 初始化设置 mentionItems 状态（使用前端已有的本地过滤结果，瞬间更新，达到零卡顿）
  mentionItems.value = {
    files: filteredLocalFiles,
    knowledgeBases: filterItems(knowledgeItems),
    mcps: filterItems(mcpItems),
    skills: filterItems(skillItems),
    subagents: filterItems(subagentItems)
  }

  if (normalizedQuery) {
    const activeThreadId = props.threadId || ''
    clearTimeout(mentionSearchTimer)
    if (activeAbortController) {
      activeAbortController.abort()
      activeAbortController = null
    }
    searchRequestId.value++
    const currentId = searchRequestId.value

    mentionSearchTimer = setTimeout(async () => {
      activeAbortController = new AbortController()

      try {
        const responseData = await searchMentionFiles(
          activeThreadId,
          normalizedQuery,
          activeAbortController.signal
        )

        // 竞态校验锁，确保是当前最新响应
        if (currentId === searchRequestId.value && Array.isArray(responseData)) {
          const remoteFileItems = responseData.map((f) => {
            const path = f.path || ''
            const fileName = f.name || path.split('/').pop() || path
            return {
              value: path,
              label: fileName,
              type: 'file',
              insertValue: path || fileName,
              tokenLabel: formatMentionToken('file', fileName),
              description: path,
              is_dir: f.is_dir,
              source: f.source
            }
          })

          // 合并本地临时文件与后端高匹配度文件（使用 Set 进行去重，防止重复展示）
          const seenValues = new Set(filteredLocalFiles.map((x) => x.value))
          const mergedFiles = [...filteredLocalFiles]

          remoteFileItems.forEach((item) => {
            if (!seenValues.has(item.value)) {
              seenValues.add(item.value)
              mergedFiles.push(item)
            }
          })

          mentionItems.value.files = mergedFiles
        }
      } catch (error) {
        // 主动取消的请求我们不作为错误抛出
        if (error.name !== 'AbortError') {
          console.error('Mention search error:', error)
        }
      } finally {
        if (currentId === searchRequestId.value) {
          activeAbortController = null
        }
      }
    }, 250) // 250ms 经典防抖时间
  }
}

// 检查项是否被选中
const isItemSelected = (type, index) => {
  if (mentionSelectedIndex.value < 0) return false

  const filesLen = mentionItems.value.files.length
  const kbLen = mentionItems.value.knowledgeBases.length
  const mcpLen = mentionItems.value.mcps.length
  const skillsLen = mentionItems.value.skills.length

  if (type === 'file') {
    return mentionSelectedIndex.value === index
  } else if (type === 'knowledge') {
    return mentionSelectedIndex.value === filesLen + index
  } else if (type === 'mcp') {
    return mentionSelectedIndex.value === filesLen + kbLen + index
  } else if (type === 'skill') {
    return mentionSelectedIndex.value === filesLen + kbLen + mcpLen + index
  } else {
    return mentionSelectedIndex.value === filesLen + kbLen + mcpLen + skillsLen + index
  }
}

// 是否有任何候选项
const showFileSearchPrompt = computed(() => {
  return Boolean(props.mention?.files?.length) && !mentionQuery.value
})

const hasAnyItems = computed(() => {
  const items = mentionItems.value
  return (
    showFileSearchPrompt.value ||
    items.files.length > 0 ||
    items.knowledgeBases.length > 0 ||
    items.mcps.length > 0 ||
    items.skills.length > 0 ||
    items.subagents.length > 0
  )
})

const insertMention = (item) => {
  if (!inputRef.value) return

  const currentValue = getEditorRawValue()
  const selectionRange = getRawSelectionRange()
  const activeMention = findActiveMentionQuery(currentValue, selectionRange.end)
  if (!activeMention) return

  const mentionValue = item.insertValue || item.value
  const mentionText = `${formatMentionToken(item.type, mentionValue)} `
  const newValue = replaceRawRange(
    currentValue,
    activeMention.start,
    activeMention.end,
    mentionText
  )
  const newCursorPos = activeMention.start + mentionText.length

  mentionPopupVisible.value = false
  mentionQuery.value = ''
  updateRawValue(newValue, newCursorPos)
}

// 滚动到选中项
const scrollToItem = (index) => {
  nextTick(() => {
    const popup = mentionDropdownRef.value?.querySelector('.mention-popup')
    if (!popup) return

    // 查找所有 mention-item 元素
    const items = popup.querySelectorAll('.mention-item')
    const selectedItem = items[index]

    if (selectedItem) {
      // 检查元素是否在可视区域内
      const popupRect = popup.getBoundingClientRect()
      const itemRect = selectedItem.getBoundingClientRect()

      if (itemRect.bottom > popupRect.bottom) {
        selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
      } else if (itemRect.top < popupRect.top) {
        selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
      }
    }
  })
}

// 处理键盘导航
const handleMentionNavigation = (e) => {
  if (!mentionPopupVisible.value) return

  const allItems = [
    ...mentionItems.value.files,
    ...mentionItems.value.knowledgeBases,
    ...mentionItems.value.mcps,
    ...mentionItems.value.skills,
    ...mentionItems.value.subagents
  ]

  const total = allItems.length
  if (total === 0) return

  if (e.key === 'ArrowDown') {
    e.preventDefault()
    mentionSelectedIndex.value = (mentionSelectedIndex.value + 1) % total
    scrollToItem(mentionSelectedIndex.value)
  } else if (e.key === 'ArrowUp') {
    e.preventDefault()
    mentionSelectedIndex.value = (mentionSelectedIndex.value - 1 + total) % total
    scrollToItem(mentionSelectedIndex.value)
  } else if (e.key === 'Enter' || e.key === 'Tab') {
    if (mentionSelectedIndex.value >= 0 && mentionSelectedIndex.value < total) {
      e.preventDefault()
      insertMention(allItems[mentionSelectedIndex.value])
    }
  } else if (e.key === 'Escape') {
    e.preventDefault()
    mentionPopupVisible.value = false
  }
}

const hasOptionsLeft = computed(() => {
  const slot = slots['options-left']
  if (!slot) {
    return false
  }
  const renderedNodes = slot()
  return Boolean(renderedNodes && renderedNodes.length)
})

// 图标映射
const iconComponents = {
  SendOutlined: SendOutlined,
  ArrowUpOutlined: ArrowUpOutlined,
  PauseOutlined: PauseOutlined
}

// 根据传入的图标名动态获取组件
const getIcon = computed(() => {
  if (props.isLoading) {
    return PauseOutlined
  }
  return iconComponents[props.sendIcon] || ArrowUpOutlined
})

// 创建本地引用以进行双向绑定
const inputValue = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})

const handleMentionDeletion = (e) => {
  if (e.key !== 'Backspace' && e.key !== 'Delete') return false

  const currentValue = getEditorRawValue()
  const selectionRange = getRawSelectionRange()
  const expandedRange = expandMentionDeletionRange(
    currentValue,
    selectionRange.start,
    selectionRange.end,
    e.key === 'Delete' ? 'forward' : 'backward'
  )

  if (!expandedRange) return false

  e.preventDefault()
  const nextValue = replaceRawRange(currentValue, expandedRange.start, expandedRange.end, '')
  mentionPopupVisible.value = false
  updateRawValue(nextValue, expandedRange.start)
  return true
}

// 处理键盘事件
const handleKeyPress = (e) => {
  // @ 提及键盘导航
  if (mentionPopupVisible.value) {
    if (['ArrowDown', 'ArrowUp', 'Enter', 'Tab', 'Escape'].includes(e.key)) {
      handleMentionNavigation(e)
      return
    }
  }

  if (handleMentionDeletion(e)) {
    return
  }

  if (e.key === 'Enter' && e.shiftKey) {
    e.preventDefault()
    replaceCurrentRawSelection('\n')
    return
  }

  emit('keydown', e)
}

const shouldCheckMentionOnKeyUp = (e) => {
  if (!e) return false
  if (e.key.length === 1) return true
  return e.key === 'Backspace' || e.key === 'Delete'
}

// 检测 @ 触发
const handleKeyUp = (e) => {
  if (!mentionEnabled.value || isComposing.value || !shouldCheckMentionOnKeyUp(e)) return
  nextTick(() => {
    checkMentionTrigger()
  })
}

// 处理输入事件
const handleInput = () => {
  if (isComposing.value) return

  if (inputRef.value && !inputRef.value.querySelector('.mention-ref-token')) {
    const text = inputRef.value.textContent || ''
    if (!text.trim()) {
      inputRef.value.replaceChildren()
    }
  }

  const value = serializeEditorContent()
  lastSyncedEditorValue = value
  emit('update:modelValue', value)
  adjustTextareaHeight()

  if (mentionEnabled.value) {
    nextTick(() => {
      checkMentionTrigger()
    })
  }
}

const handlePaste = (e) => {
  e.preventDefault()
  const text = e.clipboardData?.getData('text/plain') || ''
  replaceCurrentRawSelection(text)
}

const handleCompositionStart = () => {
  isComposing.value = true
}

const handleCompositionEnd = () => {
  isComposing.value = false
  handleInput()
}

// 处理发送按钮点击
const handleSendOrStop = () => {
  emit('send')
}

// @ 提及功能状态
const mentionPopupVisible = ref(false)
const mentionQuery = ref('')
const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [], skills: [], subagents: [] })
const mentionSelectedIndex = ref(0)
const searchRequestId = ref(0)
const isComposing = ref(false)
let activeAbortController = null
let mentionSearchTimer = null

const adjustTextareaHeight = () => {
  if (!inputRef.value) {
    return
  }

  const textarea = inputRef.value
  textarea.style.height = 'auto'
  textarea.style.height = `${textarea.scrollHeight}px`
}

// 聚焦输入框
const focusInput = () => {
  if (inputRef.value && !props.disabled) {
    inputRef.value.focus()
    // 聚焦回来时，如果开启了提及，自动检测当前光标位置是否处于 @提及 范围，是则重新升起弹框
    if (mentionEnabled.value) {
      nextTick(() => {
        checkMentionTrigger()
      })
    }
  }
}

// 处理输入框点击事件，自适应检测光标是否落入 @提及 范围内以唤醒或更新弹窗
const handleEditorClick = () => {
  if (mentionEnabled.value) {
    nextTick(() => {
      checkMentionTrigger()
    })
  }
}

// 监听输入值变化
watch(inputValue, (value) => {
  if (value !== lastSyncedEditorValue) {
    renderEditorContent(value || '')
  }

  if (debounceTimer.value) {
    clearTimeout(debounceTimer.value)
  }
  debounceTimer.value = setTimeout(() => {
    nextTick(() => {
      adjustTextareaHeight()
    })
  }, 100)
})

onMounted(() => {
  document.addEventListener('click', closeMentionPopup)
  nextTick(() => {
    if (inputRef.value) {
      renderEditorContent(inputValue.value || '')
      adjustTextareaHeight()
      inputRef.value.focus()
    }
  })
})

// 组件卸载时清除定时器和事件监听器
onBeforeUnmount(() => {
  unmountEditorMentionIcons()
  if (debounceTimer.value) {
    clearTimeout(debounceTimer.value)
  }
  if (mentionSearchTimer) {
    clearTimeout(mentionSearchTimer)
  }
  if (activeAbortController) {
    activeAbortController.abort()
  }
  document.removeEventListener('click', closeMentionPopup)
})

// 公开方法供父组件调用
defineExpose({
  focus: () => inputRef.value?.focus(),
  closeOptions: () => {
    optionsExpanded.value = false
  }
})
</script>

<style lang="less" scoped>
.input-box {
  display: grid;
  width: 100%;
  margin: 0 auto;
  border: 1px solid var(--gray-150);
  border-radius: 0.8rem;
  box-shadow: 0 2px 8px var(--shadow-1);
  transition: all 0.3s ease;
  background: var(--gray-0);
  gap: 0px;
  position: relative;

  /* Default: Multi-line layout with top/bottom slots */
  padding: 0.8rem 0.75rem 0.6rem 0.75rem;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto auto auto;
  grid-template-areas:
    'top top'
    'input input'
    'options send';

  .top-slot {
    display: flex;
    grid-area: top;
  }

  .expand-options {
    grid-area: options;
    justify-self: start;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .user-input {
    grid-area: input;
  }

  .send-button-container {
    grid-area: send;
    justify-self: end;
  }

  .bottom-slot {
    grid-column: 1 / -1;
  }

  // &:focus-within {
  //   border-color: var(--main-500);
  //   background: var(--gray-0);
  //   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
  // }
}

.expand-options {
  grid-area: options;
  display: flex;
  align-items: center;
}

.user-input {
  grid-area: input;
  width: 100%;
  padding: 0;
  background-color: transparent;
  border: none;
  margin: 0;
  margin-bottom: 0.5rem;
  color: var(--gray-1000);
  font-size: 15px;
  outline: none;
  resize: none;
  line-height: 1.5;
  font-family: inherit;
  min-height: 44px; /* Default min-height for multi-line */
  max-height: 200px;
  overflow-y: auto;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  cursor: text;

  &:focus {
    outline: none;
    box-shadow: none;
  }

  &::placeholder {
    color: var(--gray-400);
  }

  &.mention-editor {
    position: relative;

    &:empty::before {
      content: attr(data-placeholder);
      position: absolute;
      left: 0;
      top: 0;
      color: var(--gray-400);
      pointer-events: none;
    }
  }

  &[contenteditable='false'] {
    cursor: not-allowed;
  }

  :deep(.mention-ref-token) {
    display: inline-flex;
    align-items: baseline;
    gap: 2px;
    max-width: min(100%, 360px);
    color: var(--main-700);
    line-height: normal;
    vertical-align: baseline;
    white-space: nowrap;
    user-select: all;
  }

  :deep(.mention-ref-icon) {
    position: relative;
    top: 2px;
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    font-size: 13px;
    line-height: 1;
    margin-left: 4px;
  }

  :deep(.mention-ref-icon svg) {
    display: block;
  }

  :deep(.mention-ref-label) {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: normal;
    font-weight: 500;
  }
}

.send-button-container {
  grid-area: send;
  display: flex;
  align-items: center;
  justify-content: center;
}

.expand-btn {
  width: 28px;
  height: 28px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--gray-600);
  transition: all 0.2s ease;
  border: 1px solid transparent;
  background-color: transparent;

  &:hover {
    color: var(--main-color);
  }

  &:active {
    color: var(--main-color);
    // 移除点击缩小效果
  }

  .anticon {
    font-size: 14px;
    transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);

    &.rotated {
      transform: rotate(45deg);
    }
  }
}

// Popover 选项样式
.popover-options {
  min-width: 160px;
  max-width: 200px;
  padding: 4px;

  .no-options {
    color: var(--gray-500);
    font-size: 12px;
    text-align: center;
    padding: 12px 8px;
  }

  :deep(.opt-item) {
    border-radius: 8px;
    padding: 6px 10px;
    cursor: pointer;
    font-size: 12px;
    color: var(--gray-700);
    transition: all 0.2s ease;
    margin: 2px;
    display: inline-block;

    &:hover {
      background-color: var(--main-10);
      color: var(--main-600);
    }

    &.active {
      color: var(--main-600);
      background-color: var(--main-10);
    }
  }
}

.send-button.ant-btn-icon-only {
  height: 32px;
  width: 32px;
  cursor: pointer;
  background-color: var(--main-500);
  border-radius: 50%;
  border: none;
  transition: all 0.2s ease;
  box-shadow: 0 2px 6px var(--shadow-2);
  color: var(--gray-0);
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;

  &:hover {
    background-color: var(--main-color);
    box-shadow: 0 4px 8px var(--shadow-3);
    color: var(--gray-0);
  }

  &:active {
    box-shadow: 0 2px 4px var(--shadow-2);
    // 移除点击动画效果
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
  }
}

@media (max-width: 520px) {
  .input-box {
    border-radius: 15px;
    padding: 0.625rem 0.875rem;
  }
}

// @ 提及弹窗样式
.mention-dropdown-wrapper {
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  margin-bottom: 8px;
  z-index: 1000;
}

.mention-popup {
  width: 100%;
  max-height: 280px;
  overflow-y: auto;
  background: var(--gray-0);
  border-radius: 8px;
  box-shadow:
    0 -4px 16px rgba(0, 0, 0, 0.08),
    0 4px 16px rgba(0, 0, 0, 0.12);
  border: 1px solid var(--gray-200);
  padding: 8px 0;

  .mention-group {
    margin-bottom: 4px;

    &:last-child {
      margin-bottom: 0;
    }
  }

  .mention-group-title {
    font-size: 12px;
    color: var(--gray-500);
    padding: 4px 8px;
    display: flex;
    align-items: center;
    gap: 4px;
    border-bottom: 1px solid var(--gray-100);
    margin-bottom: 2px;
  }

  .mention-item {
    padding: 4px 8px;
    cursor: pointer;
    font-size: 13px;
    color: var(--gray-700);
    transition: all 0.15s ease;
    margin: 1px 4px;
    border-radius: 4px;

    &.resource-item {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
      padding: 6px 10px;

      .resource-name {
        color: var(--gray-800);
        font-weight: 500;
        flex: 0 1 auto;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      .resource-description {
        color: var(--gray-500);
        font-size: 12px;
        line-height: 1.35;
        flex: 1 1 auto;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }
    }

    &.file-item {
      display: flex;
      flex-direction: row;
      align-items: center;
      justify-content: flex-start;
      gap: 0;
      padding: 6px 10px;

      .file-info-left {
        display: flex;
        align-items: center;
        gap: 8px;
        flex-shrink: 1;
        min-width: 0;

        .file-type-icon {
          font-size: 15px;
          flex-shrink: 0;
          display: flex;
          align-items: center;
        }

        .file-name {
          font-weight: 500;
          color: var(--gray-800);
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          font-size: 13px;
        }
      }

      .file-parent-dir {
        font-size: 11px;
        color: var(--gray-400);
        margin-left: 8px;
        flex-shrink: 1;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        transition: color 0.15s ease;
      }
    }

    &:hover {
      background-color: var(--main-10);
      color: var(--main-600);

      &.resource-item {
        .resource-name {
          color: var(--main-600);
        }
        .resource-description {
          color: var(--main-400);
        }
      }

      &.file-item {
        .file-info-left .file-name {
          color: var(--main-600);
        }
        .file-parent-dir {
          color: var(--main-400);
        }
      }
    }

    &.active {
      background-color: var(--gray-50);
      color: var(--main-600);

      &.resource-item {
        .resource-name {
          color: var(--main-600);
        }
        .resource-description {
          color: var(--main-400);
        }
      }

      &.file-item {
        .file-info-left .file-name {
          color: var(--main-600);
        }
        .file-parent-dir {
          color: var(--main-400);
        }
      }
    }
  }

  .query-match {
    color: #fa8c16; /* 明亮温润的金橘色 */
    font-weight: 700;
  }

  .mention-empty {
    text-align: center;
    padding: 12px 8px;
    color: var(--gray-400);
    font-size: 13px;
  }

  .mention-search-placeholder {
    padding: 4px 8px;
    color: var(--gray-400);
    font-size: 13px;
  }
}
</style>
