<template>
  <div class="ai-input-area">
    <!-- 已选技能 chip（含参数格式提示 argument_hint） -->
    <div v-if="pendingSkill" class="ai-skill-chip">
      <span class="ai-skill-chip-main">
        <span class="ai-skill-chip-name">/{{ pendingSkill.name }}</span>
        <span v-if="pendingSkill.description" class="ai-skill-chip-desc">{{ pendingSkill.description }}</span>
        <code v-if="pendingSkill.argument_hint" class="ai-skill-chip-hint">{{ pendingSkill.argument_hint }}</code>
      </span>
      <button class="ai-skill-chip-x" @click="clearSkill" :title="$t('aiChat.clearSkill')" :aria-label="$t('aiChat.clearSkill')">×</button>
    </div>
    <div class="ai-input-wrap">
      <!-- F-260614-05 Phase 2b: 待发送图片预览(粘贴/拖拽加入,发送前可移除) -->
      <div v-if="pendingImages.length" class="ai-img-preview-row">
        <div v-for="img in pendingImages" :key="img.id" class="ai-img-preview">
          <img :src="img.dataUrl" :alt="$t('aiChat.imagePreviewAlt')" class="ai-img-preview-thumb" />
          <button
            type="button"
            class="ai-img-preview-x"
            :title="$t('aiChat.removeImage')"
            :aria-label="$t('aiChat.removeImage')"
            @click="removePendingImage(img.id)"
          >×</button>
        </div>
      </div>
      <textarea
        ref="inputEl"
        v-model="inputText"
        class="ai-input"
        :class="{ 'ai-input--editing': !!editingMsgId }"
        :placeholder="editingMsgId
          ? $t('aiChat.placeholderEditing')
          : (pendingSkill ? (pendingSkill.argument_hint || $t('aiChat.placeholderWithSkill')) : $t('aiChat.placeholderDefault'))"
        rows="1"
        @keydown="handleKeydown"
        @input="autoResize"
        @paste="onPaste"
        @dragover="onDragOver"
        @drop="onDrop"
      ></textarea>
      <!-- 技能联想浮层 -->
      <div v-if="skillOpen" class="ai-skill-popover">
        <div v-if="!filteredSkills.length" class="ai-skill-empty">
          {{ store.state.skills.length ? $t('aiChat.skillNoMatch') : $t('aiChat.skillNotLoaded') }}
        </div>
        <div
          v-for="(s, i) in filteredSkills"
          :key="s.path"
          class="ai-skill-item"
          :class="{ 'ai-skill-item--active': i === skillIndex }"
          @click="selectSkill(s)"
          @mouseenter="skillIndex = i"
        >
          <div class="ai-skill-item-row">
            <span class="ai-skill-item-name">/{{ s.name }}</span>
            <span class="ai-skill-item-desc">{{ s.description }}</span>
            <span class="ai-skill-item-src">{{ s.source }}</span>
          </div>
          <code v-if="s.argument_hint" class="ai-skill-item-hint">{{ s.argument_hint }}</code>
        </div>
      </div>
      <!-- UX-10 §1.4: @ 实体引用浮层(@ 触发,选中插入 [类型:名] 标记,架构复用技能 popover) -->
      <div v-if="mentionOpen" class="ai-skill-popover ai-mention-popover">
        <div v-if="!mentionItems.length" class="ai-skill-empty">
          {{ mentionTotal ? $t('aiChat.mentionNoMatch') : $t('aiChat.mentionEmpty') }}
        </div>
        <div v-else>
          <template v-for="g in mentionGroups" :key="g.key">
            <div v-if="g.items.length" class="ai-mention-group">{{ g.label }}</div>
            <div
              v-for="item in g.items"
              :key="item.uid"
              class="ai-skill-item"
              :class="{ 'ai-skill-item--active': mentionFlatIndex(item.uid) === mentionIndex }"
              @click="selectMention(item)"
              @mouseenter="mentionIndex = mentionFlatIndex(item.uid)"
            >
              <div class="ai-skill-item-row">
                <span class="ai-mention-item-type" :class="'ai-mention-item-type--' + item.type">{{ item.typeLabel }}</span>
                <span class="ai-mention-item-name">{{ item.name }}</span>
                <span class="ai-mention-item-desc">{{ item.desc }}</span>
              </div>
            </div>
          </template>
        </div>
      </div>
      <!-- L2 状态机停止按钮三态(批2 1c):conv_state 派生 generating/stopping/error 三态,
           idle(或 conv_state 未收到回退旧 streaming=false)显发送按钮。双轨过渡不强制全替旧 bool。 -->
      <button
        v-if="stopBtnState === 'stopping'"
        class="ai-send-btn ai-send-btn--stop ai-send-btn--stopping"
        :title="$t('aiChat.stopping')"
        disabled
      >
        <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
      </button>
      <button
        v-else-if="stopBtnState === 'retry'"
        class="ai-send-btn ai-send-btn--stop ai-send-btn--retry"
        :title="$t('aiChat.stopFailedRetry')"
        @click="store.stopChat"
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
      </button>
      <button
        v-else-if="stopBtnState === 'stop'"
        class="ai-send-btn ai-send-btn--stop"
        :title="$t('aiChat.stopGenerating')"
        @click="store.stopChat"
      >
        <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
      </button>
      <button
        v-else
        class="ai-send-btn"
        :class="{ 'ai-send-btn--active': canSend }"
        :disabled="!canSend"
        @click="handleSend"
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
      </button>
    </div>
    <!-- ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) -->
    <div v-if="hasEnrichment" class="ai-enrichment-bar">
      <button
        class="ai-enrichment-toggle"
        :class="{ 'ai-enrichment-toggle--expanded': enrichmentExpanded }"
        @click="enrichmentExpanded = !enrichmentExpanded"
        :title="enrichmentExpanded ? $t('aiChat.enrichmentCollapse') : $t('aiChat.enrichmentExpand')"
        :aria-label="enrichmentExpanded ? $t('aiChat.enrichmentCollapse') : $t('aiChat.enrichmentExpand')"
      >
        <svg class="ai-enrichment-chevron" width="10" height="10" viewBox="0 0 10 10" fill="currentColor">
          <path d="M2 3l3 4 3-4" />
        </svg>
        <span class="ai-enrichment-badge">{{ enrichmentBadgeText }}</span>
      </button>
      <button class="ai-enrichment-dismiss" @click="dismissEnrichment" title="取消关联">×</button>
      <div v-if="enrichmentExpanded" class="ai-enrichment-panel">
        <div class="ai-enrichment-label">{{ $t('aiChat.enrichmentLabel') }}</div>
        <pre class="ai-enrichment-body"><code>{{ enrichmentDetailText }}</code></pre>
      </div>
    </div>
    <div class="ai-input-hint">{{ $t('aiChat.inputHint') }}</div>
  </div>
</template>

<script setup lang="ts">
// ChatInput: AI 输入框子组件(第二批抽离自 AiChat.vue God 组件)
// 职责:输入框 textarea + 附件(图片粘贴/拖拽)+ 技能 `/` 联想 + `@` 实体引用 + 发送/停止按钮
// 边界(交父处理):
//   - editingMsgId 由父持有(prop),startEdit/cancelEdit 由父操作 prop + 调本组件 expose
//   - 发送/编辑完成 emit('sent') → 父 scrollToBottom
//   - 出错 emit('error', { msg, type }) → 父 showToast(单一 toast 源)
//   - 示例问题卡片在父(空状态区),父调 expose sendExamplePrompt 触发本组件发送
// store 单例共享:useAiStore(sendMessage/editMessage/stopChat)、useProjectStore(@ 联想源)
import { ref, computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { useProjectStore } from '../../stores/project'
import { getConvState } from '../../composables/ai/useAiEvents'
import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, IdeaRecord, MentionSpan } from '../../api/types'

const props = defineProps<{
  /** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
  editingMsgId: string | null
}>()

const emit = defineEmits<{
  /** 发送或编辑完成(父 scrollToBottom) */
  (e: 'sent'): void
  /** 出错提示(父 showToast,保持单一 toast 源) */
  (e: 'error', payload: { msg: string; type: 'error' | 'warning' | 'info' }): void
  /** UX-09 编辑态取消(Esc / 编辑完成清 editingMsgId) */
  (e: 'cancel-edit'): void
}>()

const store = useAiStore()
const projectsStore = useProjectStore()
const { t } = useI18n()

const inputText = ref('')
const inputEl = ref<HTMLTextAreaElement>()

// ── F-260614-05 Phase 2b: 图片输入(粘贴/拖拽) ──
// 待发送图片列表(base64 data URI + media_type),handleSend 时构造 ContentPart[]。
// 仅本组件级 ref,不入 store;发送后清空。预览缩略图渲染在输入框上方。
//
// 约束:
// - 限制单次最多 6 张(防超长上下文/超限请求);超过 toast 提示。
// - 限制单张 10MB(纯文本 base64 估算 ≥ 13M 字符;超限 toast 提示跳过)。
// - 仅接受 image/* MIME;非图片(文本粘贴/文件拖入)走原生行为不拦。
const MAX_PENDING_IMAGES = 6
const MAX_IMAGE_BYTES = 10 * 1024 * 1024 // 10MB(解码后字节上限)
const pendingImages = ref<{ id: string; dataUrl: string; mediaType: string }[]>([])

/** 读 File → base64 data URL(供 <img> 预览 + ContentPart.image base64 字段) */
function readImageFile(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(reader.result as string)
    reader.onerror = () => reject(reader.error ?? new Error('read failed'))
    reader.readAsDataURL(file)
  })
}

/** 把 image File 加入 pendingImages;超限/超大小跳过并 toast */
async function addImageFile(file: File): Promise<void> {
  if (!file.type.startsWith('image/')) return
  if (file.size > MAX_IMAGE_BYTES) {
    emit('error', { msg: t('aiChat.imageTooLarge'), type: 'warning' })
    return
  }
  if (pendingImages.value.length >= MAX_PENDING_IMAGES) {
    emit('error', { msg: t('aiChat.imageLimit', { n: MAX_PENDING_IMAGES }), type: 'warning' })
    return
  }
  try {
    const dataUrl = await readImageFile(file)
    pendingImages.value.push({
      // id 用 timestamp+random,DOM key 唯一,移除按钮定位
      id: `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      dataUrl,
      mediaType: file.type,
    })
  } catch (e) {
    emit('error', { msg: t('aiChat.imageReadFailed'), type: 'error' })
    console.error('[AI] 图片读取失败:', e)
  }
}

/** 移除待发送图片 */
function removePendingImage(id: string): void {
  pendingImages.value = pendingImages.value.filter(p => p.id !== id)
}

/** 粘贴:ClipboardEvent 检测 image 类型条目,加入待发送列表(不插入文本占位) */
async function onPaste(e: ClipboardEvent): Promise<void> {
  const items = e.clipboardData?.items
  if (!items) return
  const files: File[] = []
  for (let i = 0; i < items.length; i++) {
    const it = items[i]
    if (it.kind === 'file' && it.type.startsWith('image/')) {
      const f = it.getAsFile()
      if (f) files.push(f)
    }
  }
  if (files.length === 0) return
  // 含图片:阻止默认粘贴(避免把图片的二进制当文本塞进 textarea),逐个加入
  e.preventDefault()
  for (const f of files) await addImageFile(f)
}

/** 拖拽进入:仅允许图片,preventDefault 让 drop 事件触发 */
function onDragOver(e: DragEvent): void {
  if (!e.dataTransfer) return
  // 判断当前是否含 image/* 类型(copy/move 光标提示)
  const hasImage = Array.from(e.dataTransfer.types).includes('Files')
  if (hasImage) {
    e.preventDefault()
    e.dataTransfer.dropEffect = 'copy'
  }
}

/** 拖拽放下:取出图片 File,加入待发送 */
async function onDrop(e: DragEvent): Promise<void> {
  const files = e.dataTransfer?.files
  if (!files || files.length === 0) return
  // 仅处理图片文件(非图片拖入走原生不拦,但这里 preventDefault 已无副作用)
  const images = Array.from(files).filter(f => f.type.startsWith('image/'))
  if (images.length === 0) return
  e.preventDefault()
  for (const f of images) await addImageFile(f)
}

/** 发送可用:有文本或图片或已选技能(纯技能调用允许空文本) */
const canSend = computed(() => inputText.value.trim().length > 0 || pendingImages.value.length > 0 || !!pendingSkill.value)

// L2 状态机停止按钮三态(批2 1c):从 conv_state 派生,旧 streaming/generating bool 双轨过渡兜底。
//
// 三态映射(对齐后端 ConvState 语义):
// - 'stopping':ConvState=stopping,停止中 → 显"停止中" disabled(防重复点);
// - 'stop':    ConvState=generating(含审批挂起/压缩派生等活跃态)→ 显停止按钮可点;
// - 'retry':   ConvState=error,停止失败可重试 → 显重试按钮(再发 stop 信号);
// - 'idle':    空闲(或 conv_state 未收到回退旧 bool=false)→ 显发送按钮。
//
// 兜底:conv_state 未追踪过(getConvState 返回 null,CONV_STATE_ENABLED=off 或老后端)时,
// 回退旧 store.state.streaming 判断(generating→显停止,否则 idle)。不强制全替旧 bool,防回归。
// 注:旧 streaming 是全局单值(非 per-conv),仅当前会话生成时为 true;F-09 多会话并发下 conv_state
// 更精确,但回退路径用 streaming 对单会话场景语义等价。
const stopBtnState = computed<'idle' | 'stop' | 'stopping' | 'retry'>(() => {
  const convId = store.state.activeConversationId
  const cs = getConvState(convId)
  if (cs === 'stopping') return 'stopping'
  if (cs === 'error') return 'retry'
  if (cs === 'generating' || cs === 'compressed') return 'stop'
  // conv_state 未收到(idle 或 null)→ 回退旧 streaming bool
  return store.state.streaming ? 'stop' : 'idle'
})

// ── 技能 `/` 联想 ──
const skillOpen = ref(false)
const skillIndex = ref(0)
const pendingSkill = ref<SkillInfo | null>(null)
const skillQuery = computed(() => (inputText.value.startsWith('/') ? inputText.value.slice(1) : ''))
const filteredSkills = computed(() => {
  const q = skillQuery.value.trim().toLowerCase()
  const list = store.state.skills
  if (!q) return list
  return list.filter(
    (s) => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q),
  )
})

watch(inputText, (v) => {
  // Input Augmentation: skill chip + @ 标记共存(两者正交 user intent)——删原 pendingSkill
  // 非空时强制关 mention 的分支。skill chip 是独立 UI 态(已选技能),@ 是输入态(光标触发),
  // 用户可同时持有已选技能并在文本里 @ 引用实体。
  // / 技能联想(仅当输入以 / 开头且光标在首字符区)
  skillOpen.value = v.startsWith('/')
  if (skillOpen.value) skillIndex.value = 0
  // UX-10: @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开;视觉打架防护)
  if (skillOpen.value) {
    mentionOpen.value = false
    mentionStart.value = -1
  } else {
    detectMentionTrigger()
  }
})

/**
 * UX-10: 检测光标前最近的 @ 触发符。
 * 规则:从光标位置向前找最近的 @,期间无空白/换行则激活(支持 "abc @dev" 但不支持 "a @b c @d" 中第二个被空格阻断)。
 * 找到 → mentionStart=该 @ 下标, mentionOpen=true; 否则关闭。
 */
function detectMentionTrigger() {
  const el = inputEl.value
  const text = inputText.value
  const cursor = el?.selectionStart ?? text.length
  // 从光标向前扫,遇空白/换行即停(说明 @ 与光标间有分隔,非同一 token)
  let i = cursor - 1
  while (i >= 0) {
    const ch = text[i]
    if (ch === '@') {
      // @ 必须在行首或前一字符为空白/换行/起始(避免邮箱 a@b 误触发)
      const prev = i > 0 ? text[i - 1] : ''
      if (prev === '' || /\s/.test(prev)) {
        mentionStart.value = i
        mentionOpen.value = true
        mentionIndex.value = 0
        return
      }
      // 形如 a@b:前字符非空白 → 不是 mention 触发符,继续向前扫也只会找到更早的 @,
      // 但中间已有非空白阻断,直接关闭
      break
    }
    if (/\s/.test(ch)) break
    i--
  }
  mentionOpen.value = false
  mentionStart.value = -1
}

function selectSkill(s: SkillInfo) {
  pendingSkill.value = s
  inputText.value = ''
  // selectSkill 清空 inputText,pendingMentionSpans 指向的区间随之失效,一并清空防孤儿 span
  pendingMentionSpans.value = []
  skillOpen.value = false
  mentionOpen.value = false
  mentionStart.value = -1
  nextTick(() => inputEl.value?.focus())
}

function clearSkill() {
  pendingSkill.value = null
  nextTick(() => inputEl.value?.focus())
}

// ── UX-10 §1.4: @ 实体引用(@ 触发 popover,选中插入 [类型:名] 标记,后端 system prompt 注入摘要) ──
// 架构复用 / 技能联想:独立状态(mentionOpen/mentionIndex/mentionQuery),不与 skill 混。
// 联想源只读 projectsStore.projects / projectsStore.tasks(前端 store 现有数据,不新增 IPC)。
// 触发:输入框光标前最近的 @ 后无空白/换行 → 激活;Esc/光标移开/选中插入 → 关闭。
// Enter 不冲突:popover 开时 Enter 插入选中(覆盖发送),关时 Enter 发送(原逻辑)。
interface MentionItem {
  type: 'project' | 'task' | 'idea'
  /** 扁平后唯一 key(用于 key/导航扁平索引定位) */
  uid: string
  /** 实体原始 id(供后端解析,目前仅插入 [类型:名] 文本,id 未透传) */
  id: string
  name: string
  desc: string
  typeLabel: string
}

const mentionOpen = ref(false)
const mentionIndex = ref(0)
// @ 触发符在 inputText 中的起始下标(用于选中后精确替换 @query 段)
const mentionStart = ref(-1)
// Input Augmentation: pending mention 区间元数据(selectMention 记录,handleSend 透传后清空)。
// 每个 span 描述用户气泡内一段 [类型: 名] 文本在 inputText 中的 {start,length} + kind/refId/label,
// 后端据 mentionSpans resolve 投影成 Augmentation 注入;MessageList 据此精确切区间渲染 chip。
const pendingMentionSpans = ref<MentionSpan[]>([])

// ── ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) ──
// 当用户 @[项目:xxx] 后,从 projectsStore 反查项目+关联任务/灵感,
// 在输入区下方显式 badge + 可展开的上下文参考面板。
interface ProjectEnrichment {
  id: string
  name: string
  status: string
  path: string | null
  taskCount: number
  ideaCount: number
  inProgressTasks: { title: string; status: string }[]
  pendingIdeas: { title: string; status: string }[]
}

const enrichmentExpanded = ref(false)

/** @项目 mentions 经 stores 反查后的 enrichment 摘要列表 */
const projectEnrichments = computed<ProjectEnrichment[]>(() => {
  const projectSpans = pendingMentionSpans.value.filter(sp => sp.kind === 'project')
  if (projectSpans.length === 0) return []
  const projects = (projectsStore.projects || []) as ProjectRecord[]
  const tasks = (projectsStore.tasks || []) as TaskRecord[]
  const ideas = (projectsStore.ideas || []) as IdeaRecord[]
  const results: ProjectEnrichment[] = []
  for (const span of projectSpans) {
    const project = projects.find(p => p.id === span.refId)
    if (!project) continue
    const relTasks = tasks.filter(t => t.project_id === project.id)
    // ideas linking to this project via promoted_to (project name matching)
    const relIdeas = ideas.filter(i => i.promoted_to === project.name || i.promoted_to === project.id)
    const running = relTasks.filter(t => t.status === 'in_progress').slice(0, 20)
    const pending = relIdeas.filter(i => i.status === 'pending_review').slice(0, 20)
    results.push({
      id: project.id,
      name: project.name,
      status: project.status as string,
      path: project.path,
      taskCount: relTasks.length,
      ideaCount: relIdeas.length,
      inProgressTasks: running.map(t => ({ title: t.title, status: t.status as string })),
      pendingIdeas: pending.map(i => ({ title: i.title, status: i.status as string })),
    })
  }
  return results
})

/** 是否有 enrichment 可展示 */
const hasEnrichment = computed(() => projectEnrichments.value.length > 0)

/** 概要 badge 文本(汇总所有 @项目 entrichment) */
const enrichmentBadgeText = computed(() => {
  const list = projectEnrichments.value
  if (!list.length) return ''
  const totalTasks = list.reduce((s, p) => s + p.taskCount, 0)
  const totalIdeas = list.reduce((s, p) => s + p.ideaCount, 0)
  const names = list.map(p => p.name).join('、')
  return t('aiChat.enrichmentBadge', { project: names, tasks: totalTasks, ideas: totalIdeas })
})

/** 展开后的 enrichment 正文(mimic 后端 build_augmentation_segment 格式) */
const enrichmentDetailText = computed(() => {
  const list = projectEnrichments.value
  if (!list.length) return ''
  const lines: string[] = []
  for (const p of list) {
    lines.push(`【项目】${p.name}（状态: ${p.status}）`)
    if (p.path) lines.push(`目录: ${p.path}`)
    if (p.inProgressTasks.length > 0) {
      lines.push(`进行中任务(${p.taskCount}):`)
      for (const t of p.inProgressTasks) {
        lines.push(`  - ${t.title} (${t.status})`)
      }
    }
    if (p.pendingIdeas.length > 0) {
      lines.push(`待评估灵感(${p.ideaCount}):`)
      for (const i of p.pendingIdeas) {
        lines.push(`  - ${i.title} (${i.status})`)
      }
    }
    lines.push('')
  }
  return lines.join('\n')
})

/** 取消 @项目 关联:清除 mentionSpan + 移除输入框中的 [@项目: xxx] 标记 */
function dismissEnrichment() {
  const projectSpans = pendingMentionSpans.value.filter(sp => sp.kind === 'project')
  if (projectSpans.length === 0) return
  // 从 inputText 中移除所有 [@项目: xxx] 标记
  let text = inputText.value
  for (const span of projectSpans) {
    const label = text.slice(span.start, span.start + span.length)
    // 替换标记 + 前后可能的空格为单个空格
    const pattern = new RegExp(`\\s*${escapeRegex(label)}\s*`)
    text = text.replace(pattern, ' ').trim()
  }
  inputText.value = text
  // 清除 project 类型的 mentionSpan
  pendingMentionSpans.value = pendingMentionSpans.value.filter(sp => sp.kind !== 'project')
  enrichmentExpanded.value = false
}

function escapeRegex(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

const mentionGroupLabel = computed(() => ({
  project: t('aiChat.mentionGroupProject'),
  task: t('aiChat.mentionGroupTask'),
  idea: t('aiChat.mentionGroupIdea'),
}))

/** 当前 @ 查询文本(@ 之后的串,不含 @) */
const mentionQuery = computed(() => {
  if (mentionStart.value < 0) return ''
  return inputText.value.slice(mentionStart.value + 1)
})

/** 联想候选(项目 + 任务,按查询过滤;各限 20 防列表膨胀) */
const mentionItems = computed<MentionItem[]>(() => {
  const q = mentionQuery.value.trim().toLowerCase()
  const projects = (projectsStore.projects || []) as ProjectRecord[]
  const tasks = (projectsStore.tasks || []) as TaskRecord[]
  const match = (s: string) => !q || s.toLowerCase().includes(q)
  const projectItems: MentionItem[] = projects
    .filter(p => match(p.name) || match(p.description || ''))
    .slice(0, 20)
    .map(p => ({
      type: 'project' as const,
      uid: 'p-' + p.id,
      id: p.id,
      name: p.name,
      desc: p.description || '',
      typeLabel: t('aiChat.mentionGroupProject'),
    }))
  const taskItems: MentionItem[] = tasks
    .filter(tk => match(tk.title) || match(tk.description || ''))
    .slice(0, 20)
    .map(tk => ({
      type: 'task' as const,
      uid: 't-' + tk.id,
      id: tk.id,
      name: tk.title,
      desc: tk.description || '',
      typeLabel: t('aiChat.mentionGroupTask'),
    }))
  const ideas = (projectsStore.ideas || []) as IdeaRecord[]
  const ideaItems: MentionItem[] = ideas
    .filter(it => match(it.title) || match(it.description || ''))
    .slice(0, 20)
    .map(it => ({
      type: 'idea' as const,
      uid: 'i-' + it.id,
      id: it.id,
      name: it.title,
      desc: it.description || '',
      typeLabel: t('aiChat.mentionGroupIdea'),
    }))
  return [...projectItems, ...taskItems, ...ideaItems]
})

/** 联想候选总数(未过滤,用于区分"无匹配" vs "空") */
const mentionTotal = computed(() =>
  ((projectsStore.projects || []) as ProjectRecord[]).length
  + ((projectsStore.tasks || []) as TaskRecord[]).length
  + ((projectsStore.ideas || []) as IdeaRecord[]).length,
)

/** 按类型分组(模板按组渲染组标题) */
const mentionGroups = computed(() => [
  { key: 'project', label: mentionGroupLabel.value.project, items: mentionItems.value.filter(i => i.type === 'project') },
  { key: 'task', label: mentionGroupLabel.value.task, items: mentionItems.value.filter(i => i.type === 'task') },
  { key: 'idea', label: mentionGroupLabel.value.idea, items: mentionItems.value.filter(i => i.type === 'idea') },
])

/** 扁平下标(组内 item 在扁平列表中的位置,供键盘导航 ↑↓/高亮) */
function mentionFlatIndex(uid: string): number {
  return mentionItems.value.findIndex(i => i.uid === uid)
}

/** 选中实体 → 替换 @query 段为 [类型: 名] 标记 + 关闭浮层 */
function selectMention(item: MentionItem) {
  const before = inputText.value.slice(0, mentionStart.value)
  const cursorAfter = mentionStart.value + 1 + mentionQuery.value.length
  const after = inputText.value.slice(cursorAfter)
  const label = item.type === 'project'
    ? `[${t('aiChat.mentionGroupProject')}: ${item.name}]`
    : item.type === 'task'
      ? `[${t('aiChat.mentionGroupTask')}: ${item.name}]`
      : `[${t('aiChat.mentionGroupIdea')}: ${item.name}]`
  inputText.value = before + label + ' ' + after
  // Input Augmentation: 记 pending mention 区间(start=before.length,label 区间长度=label.length)。
  // before 已包含此前所有插入的 [类型: 名] 标记,故 before.length 恰为本 label 在最终文本中的起点。
  // refId 用实体 id(后端 resolve 用),label 即 chip 展示文本(== 后段校验用 == 文本)。
  pendingMentionSpans.value.push({
    start: before.length,
    length: label.length,
    kind: item.type,
    refId: item.id,
    label,
  })
  mentionOpen.value = false
  mentionStart.value = -1
  mentionIndex.value = 0
  nextTick(() => {
    const el = inputEl.value
    if (el) {
      // 光标置插入标记末尾
      const pos = (before + label + ' ').length
      el.focus()
      el.setSelectionRange(pos, pos)
      el.style.height = 'auto'
      el.style.height = Math.min(el.scrollHeight, 120) + 'px'
    }
  })
}

function handleKeydown(e: KeyboardEvent) {
  // UX-09:编辑态 Escape 取消编辑(最高优先级)
  if (props.editingMsgId && e.key === 'Escape') {
    e.preventDefault()
    emit('cancel-edit')
    return
  }
  // Escape 关闭技能联想浮层（无论是否有匹配项）
  if (skillOpen.value && e.key === 'Escape') {
    e.preventDefault()
    skillOpen.value = false
    return
  }
  // 已选技能 chip 态下 Escape 取消选中(浮层已关闭,这里兜底 chip 可取消路径)
  if (pendingSkill.value && e.key === 'Escape') {
    e.preventDefault()
    pendingSkill.value = null
    return
  }
  // 技能联想浮层导航（仅有匹配项时拦截方向/回车）
  if (skillOpen.value && filteredSkills.value.length) {
    if (e.key === 'ArrowDown') {
      e.preventDefault()
      skillIndex.value = (skillIndex.value + 1) % filteredSkills.value.length
      return
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault()
      skillIndex.value = (skillIndex.value - 1 + filteredSkills.value.length) % filteredSkills.value.length
      return
    }
    if ((e.key === 'Enter' || e.key === 'Tab') && !e.shiftKey) {
      e.preventDefault()
      selectSkill(filteredSkills.value[skillIndex.value])
      return
    }
    if (e.key === 'Escape') {
      e.preventDefault()
      skillOpen.value = false
      return
    }
  }
  // UX-10: @ 实体引用浮层导航(仅有匹配项时拦截方向/回车;与技能联想互斥)
  if (mentionOpen.value && mentionItems.value.length) {
    if (e.key === 'ArrowDown') {
      e.preventDefault()
      mentionIndex.value = (mentionIndex.value + 1) % mentionItems.value.length
      return
    }
    if (e.key === 'ArrowUp') {
      e.preventDefault()
      mentionIndex.value = (mentionIndex.value - 1 + mentionItems.value.length) % mentionItems.value.length
      return
    }
    if ((e.key === 'Enter' || e.key === 'Tab') && !e.shiftKey) {
      e.preventDefault()
      selectMention(mentionItems.value[mentionIndex.value])
      return
    }
    if (e.key === 'Escape') {
      e.preventDefault()
      mentionOpen.value = false
      mentionStart.value = -1
      return
    }
  }
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    handleSend()
  }
}

function autoResize() {
  const el = inputEl.value
  if (el) {
    el.style.height = 'auto'
    el.style.height = Math.min(el.scrollHeight, 120) + 'px'
  }
}

async function handleSend() {
  const text = inputText.value.trim()
  // UX-09 编辑态:走 editMessage(不 push 新 user,编辑末条 user 后重生成)
  if (props.editingMsgId) {
    if (!text) return
    inputText.value = ''
    if (inputEl.value) inputEl.value.style.height = 'auto'
    try {
      await store.editMessage(text)
      emit('cancel-edit')
    } catch (e) {
      console.error('[AI] 编辑失败:', e)
      // 失败回填输入框供重试,保持编辑态(editMessage 内已回滚占位)
      inputText.value = text
      const msg = e instanceof Error ? e.message : String(e)
      emit('error', { msg: t('aiChat.editMessageFailed', { msg }), type: 'error' })
    }
    await nextTick()
    emit('sent')
    return
  }
  const skill = pendingSkill.value
  // 无文本、无图片、未选技能时忽略；选了技能允许空文本（纯技能调用）
  if (!text && !skill && pendingImages.value.length === 0) return
  // F-260614-05 Phase 2b: 快照待发送图片,构造 ContentPart[]([Text?, Image...])。
  // 后端 ai_chat_send IPC 当前不接 parts(Phase 2c 接入);本轮 parts 仅写本地 push 的
  // user 消息渲染多模态图,后端请求仍走 content 文本路径。
  const snapshotImgs = pendingImages.value.slice()
  // Input Augmentation: 快照 pending mention 区间(同 parts 快照,失败时回填)。
  // 关键:span.start 是相对原始 inputText(可能含前导空格)的偏移;text = inputText.trim()
  // 已剥前导空白。若不修正偏移,trim 掉的前导空白会让所有 chip start 错位 → 渲染降级纯文本。
  // 此处按剥掉的前导空白长度整体左移 start(尾部 trim 不影响已记录的 chip start;chip 不在尾部空白)。
  const rawLeadingWs = inputText.value.length - inputText.value.replace(/^\s+/, '').length
  const snapshotSpans = pendingMentionSpans.value.slice().map(sp => ({
    ...sp,
    start: Math.max(0, sp.start - rawLeadingWs),
  }))
  const parts: ContentPart[] | undefined = snapshotImgs.length > 0
    ? [
        // 文本片(非空时作首片,便于后端 2c 接入后 content 与 parts 文本一致;
        //  空文本则只发图片片)
        ...(text ? [{ type: 'text' as const, text }] : []),
        ...snapshotImgs.map(img => {
          // dataUrl 形如 data:image/png;base64,xxxx —— 拆出纯 base64 + media_type
          const m = /^data:([^;]+);base64,(.*)$/s.exec(img.dataUrl)
          const mediaType = m?.[1] || img.mediaType
          const base64 = m?.[2] || ''
          return {
            type: 'image' as const,
            url: null,
            base64,
            media_type: mediaType,
            alt: null,
          }
        }),
      ]
    : undefined
  // 生成中不再拦截：sendMessage 会把消息排入待发送队列，完成后自动续发
  // 先清输入（无论立即发还是入队）；队列满/IPC 失败时 catch 回填
  inputText.value = ''
  pendingSkill.value = null
  pendingImages.value = []
  // Input Augmentation: 清空 mention 区间(与 inputText 清空同处,保证下条消息从空白态起步)
  pendingMentionSpans.value = []
  skillOpen.value = false
  mentionOpen.value = false
  mentionStart.value = -1
  if (inputEl.value) inputEl.value.style.height = 'auto'
  // 同步清空输入框后立即发送：sendMessage 内部同步 push 用户消息，
  // 清空与 push 合并到同一渲染周期 flush，消除"输入框已空但消息未显示"的间隙
  try {
    // Input Augmentation: 透传 mentionSpans(L0 直接 doSend 注入 + 本地 user 消息挂 mentionSpans 渲染 chip;
    // L1 入队续发当前不挂 spans,见 useAiSend.sendMessage 设计取舍注释)
    await store.sendMessage(text, skill?.name, false, parts, snapshotSpans.length > 0 ? snapshotSpans : undefined)
  } catch (e) {
    console.error('[AI] 发送失败:', e)
    inputText.value = text
    pendingSkill.value = skill
    // 发送失败:回填图片供重试(快照原样还原)
    if (parts) {
      pendingImages.value = snapshotImgs
    }
    // 发送失败:回填 mention 区间供重试(快照原样还原,区间仍与回填文本对齐)
    if (snapshotSpans.length) {
      pendingMentionSpans.value = snapshotSpans
    }
    // 用户可见提示(原仅 console.error,用户无感);Tauri IPC 错误是字符串非 Error 对象
    const msg = e instanceof Error ? e.message : String(e)
    emit('error', { msg: t('aiChat.toastSendFail', { msg }), type: 'error' })
  }
  await nextTick()
  emit('sent')
}

/** 点击示例问题卡片:填入输入框并发送(复用 handleSend 同款发送路径,错误回退一致) */
async function sendExamplePrompt(i18nKey: string) {
  inputText.value = t(i18nKey)
  await handleSend()
}

// ── 父调用 expose(父 startEdit/cancelEdit/beforeunload 调用) ──

/** UX-09 进入编辑态:回填末条 user 内容到输入框,聚焦,光标置末 */
function startEditFocus(msg: AiMessage): void {
  inputText.value = msg.content
  nextTick(() => {
    inputEl.value?.focus()
    const el = inputEl.value
    if (el) {
      const len = el.value.length
      el.setSelectionRange(len, len)
      el.style.height = 'auto'
      el.style.height = Math.min(el.scrollHeight, 120) + 'px'
    }
  })
}

/** UX-09 取消编辑态:清空输入框 + 重置高度(父置 editingMsgId=null 后调) */
function clearInput(): void {
  inputText.value = ''
  pendingSkill.value = null
  pendingImages.value = []
  pendingMentionSpans.value = []
  skillOpen.value = false
  mentionOpen.value = false
  mentionStart.value = -1
  if (inputEl.value) inputEl.value.style.height = 'auto'
}

/** 聚焦输入框 */
function focus(): void {
  inputEl.value?.focus()
}

/**
 * UX-260617-16:未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
 * 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
 */
function hasUnsentInput(): boolean {
  return (
    inputText.value.trim().length > 0 ||
    pendingImages.value.length > 0 ||
    !!pendingSkill.value
  )
}

defineExpose({
  focus,
  startEditFocus,
  clearInput,
  hasUnsentInput,
  sendExamplePrompt,
})
</script>

<!-- ═══ ChatInput scoped 样式(第二批抽离遗漏修复: 从 AiChat.vue <style scoped> 迁移,
     零视觉变更。原 AiChat scoped 不渗透子组件根,输入框/技能联想/发送按钮元素失配 → 本组件自持。
     CSS 变量(--df-*)由根级 global.css 提供,共享同一份。.ai-input-area 容器留父级 AiChat scoped。) ═══ -->
<style scoped>
.ai-input-wrap {
  position: relative;
  display: flex;
  /* F-260614-05 Phase 2b: flex-wrap 让全宽的图片预览行强制 textarea+按钮换行到下一行 */
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  background: var(--df-bg-card);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-lg);
  padding: 6px 8px;
  transition: border-color 0.15s;
}

/* F-260614-05 Phase 2b: 待发送图片预览行(粘贴/拖拽加入,发送前可移除) */
.ai-img-preview-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  width: 100%; /* 全宽强制后续 textarea+按钮换行 */
  padding-bottom: 4px;
  border-bottom: 0.5px solid var(--df-border);
}
.ai-img-preview {
  position: relative;
  width: 56px;
  height: 56px;
  border-radius: var(--df-radius);
  overflow: hidden;
  border: 0.5px solid var(--df-border);
  background: var(--df-bg);
}
.ai-img-preview-thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.ai-img-preview-x {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: color-mix(in srgb, var(--df-bg) 80%, transparent);
  color: var(--df-text-dim);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  transition: background 0.15s, color 0.15s;
}
.ai-img-preview-x:hover {
  background: var(--df-danger);
  color: #fff;
}

.ai-input-wrap:focus-within {
  border-color: color-mix(in srgb, var(--df-accent) 40%, transparent);
}

/* ── 技能 chip / 联想浮层 ── */
.ai-skill-chip {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  padding: 3px 8px;
  background: color-mix(in srgb, var(--df-accent) 16%, transparent);
  border: 0.5px solid color-mix(in srgb, var(--df-accent) 40%, transparent);
  border-radius: var(--df-radius);
  font-size: 12px;
}
.ai-skill-chip-name { color: var(--df-accent); font-weight: 600; flex-shrink: 0; }
.ai-skill-chip-desc { color: var(--df-text-dim); flex-shrink: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 参数格式提示(技能自带 argument_hint 数据,不进 i18n) */
.ai-skill-chip-main { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; flex-wrap: wrap; }
.ai-skill-chip-hint {
  color: var(--df-text-dim);
  font-family: var(--df-font-mono, ui-monospace, monospace);
  font-size: 11px;
  background: color-mix(in srgb, var(--df-text-dim) 14%, transparent);
  padding: 1px 5px;
  border-radius: 3px;
}
.ai-skill-chip-x { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0; }
.ai-skill-chip-x:hover { color: var(--df-text); }

.ai-skill-popover {
  position: absolute;
  bottom: calc(100% + 4px);
  left: 0;
  right: 0;
  max-height: 240px;
  overflow-y: auto;
  background: var(--df-bg-card);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  z-index: 20;
}
.ai-skill-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px 10px;
  cursor: pointer;
  font-size: 12px;
}
.ai-skill-item-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
.ai-skill-item--active,
.ai-skill-item:hover {
  background: color-mix(in srgb, var(--df-accent) 14%, transparent);
}
.ai-skill-item-name { color: var(--df-accent); font-weight: 600; min-width: 90px; }
.ai-skill-item-desc { color: var(--df-text-dim); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ai-skill-item-src { color: var(--df-text-dim); opacity: 0.6; font-size: 10px; text-transform: uppercase; }
/* 浮层内参数格式提示(等宽 dim 小字,仅在 skill.argument_hint 有值时显) */
.ai-skill-item-hint {
  color: var(--df-text-dim);
  font-family: var(--df-font-mono, ui-monospace, monospace);
  font-size: 11px;
  opacity: 0.85;
  word-break: break-all;
}
.ai-skill-empty { padding: 8px 12px; color: var(--df-text-dim); opacity: 0.7; font-size: 12px; }

/* ── UX-10 §1.4: @ 实体引用浮层(复用 .ai-skill-popover 容器,仅新增组标题/类型标签) ── */
.ai-mention-popover { /* 同 .ai-skill-popover 定位,这里仅为语义占位,样式继承 */ }
.ai-mention-group {
  padding: 4px 10px 2px;
  font-size: 10px;
  font-weight: 600;
  color: var(--df-text-dim);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  user-select: none;
}
.ai-mention-item-name { color: var(--df-text); font-weight: 500; flex-shrink: 0; }
.ai-mention-item-desc {
  color: var(--df-text-dim);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 类型标签小色块(项目/任务区分) */
.ai-mention-item-type {
  flex-shrink: 0;
  padding: 1px 5px;
  border-radius: var(--df-radius-sm);
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.3px;
  background: var(--df-bg-hover);
  color: var(--df-text-dim);
}
.ai-mention-item-type--project {
  background: color-mix(in srgb, var(--df-accent) 16%, transparent);
  color: var(--df-accent);
}
.ai-mention-item-type--task {
  background: color-mix(in srgb, var(--df-success) 16%, transparent);
  color: var(--df-success);
}

.ai-input {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--df-text);
  font-size: 13px;
  font-family: var(--df-font-sans);
  line-height: 1.5;
  resize: vertical;
  min-height: 20px;
  height: 20px;
  max-height: 200px;
  overflow-y: auto;
}
.ai-input::placeholder {
  color: var(--df-text-dim);
}
.ai-send-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: var(--df-radius);
  background: var(--df-bg-raised);
  color: var(--df-text-dim);
  cursor: pointer;
  transition: all 0.15s;
  flex-shrink: 0;
}
.ai-send-btn--active {
  background: var(--df-accent);
  color: #fff;
}
.ai-send-btn--active:hover {
  background: var(--df-accent-hover);
}
.ai-send-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.ai-send-btn--stop {
  background: #e5484d;
  color: #fff;
}
.ai-send-btn--stop:hover {
  background: #d63a3f;
}
/* L2 停止中态:disabled 灰显(防重复点),复用 :disabled 通用规则(opacity 0.4) */
.ai-send-btn--stopping {
  cursor: not-allowed;
}
/* L2 停止失败可重试态:橙色边框提示(区别于红色停止),hover 加深 */
.ai-send-btn--retry {
  background: #e5484d;
  color: #fff;
}
.ai-send-btn--retry:hover {
  background: #d63a3f;
}
.ai-input-hint {
  font-size: 10px;
  color: var(--df-text-dim);
  text-align: right;
  margin-top: 4px;
  opacity: 0.6;
}

/* ── ⑥.4 Phase 4: @项目展开摘要(输入区 enrichment 预览) ── */
.ai-enrichment-bar {
  display: flex;
  align-items: center;
  margin-top: 6px;
  border: 0.5px solid color-mix(in srgb, var(--df-accent) 30%, transparent);
  border-radius: var(--df-radius);
  overflow: hidden;
}
.ai-enrichment-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 4px 8px;
  border: none;
  background: color-mix(in srgb, var(--df-accent) 10%, transparent);
  color: var(--df-text);
  cursor: pointer;
  font-size: 11px;
  text-align: left;
  transition: background 0.15s;
}
.ai-enrichment-toggle:hover {
  background: color-mix(in srgb, var(--df-accent) 18%, transparent);
}
.ai-enrichment-toggle--expanded {
  border-bottom: 0.5px solid color-mix(in srgb, var(--df-accent) 20%, transparent);
}
.ai-enrichment-chevron {
  flex-shrink: 0;
  color: var(--df-text-dim);
  transition: transform 0.15s;
}
.ai-enrichment-toggle--expanded .ai-enrichment-chevron {
  transform: rotate(180deg);
}
.ai-enrichment-dismiss {
  flex-shrink: 0;
  border: none;
  background: transparent;
  color: var(--df-text-dim);
  cursor: pointer;
  font-size: 14px;
  padding: 0 4px;
  line-height: 1;
  transition: color 0.15s;
}
.ai-enrichment-dismiss:hover {
  color: var(--df-danger);
}
.ai-enrichment-badge {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--df-accent);
  font-weight: 500;
}
.ai-enrichment-panel {
  padding: 6px 8px;
  background: var(--df-bg);
  max-height: 200px;
  overflow-y: auto;
}
.ai-enrichment-label {
  font-size: 10px;
  color: var(--df-text-dim);
  margin-bottom: 4px;
  font-weight: 600;
}
.ai-enrichment-body {
  margin: 0;
  font-size: 11px;
  line-height: 1.6;
  color: var(--df-text);
  white-space: pre-wrap;
  word-break: break-all;
  font-family: var(--df-font-mono, ui-monospace, monospace);
}
</style>
