<template>
  <div
    ref="rootRef"
    class="user-bubble"
    :class="{
      'user-bubble--focusable': msgId,
      'user-bubble--text': isPlainText,
    }"
    :tabindex="msgId ? 0 : undefined"
    @mouseenter="onBubbleMouseEnter"
    @mouseleave="onBubbleMouseLeave"
    @focus="focused = true"
    @blur="focused = false"
    @pointerdown="onPointerDown"
    @keydown.enter.prevent="onCopy"
  >
    <FileAttachment
      v-if="fileAttachment"
      :filename="fileAttachment.filename"
      :url="fileAttachment.url"
    />
    <div v-else-if="commandBubble" class="user-bubble__command" :class="`user-bubble__command--${commandBubble.kind}`">
      <div class="user-bubble__command-header">
        <span class="user-bubble__command-icon"><component :is="commandBubble.icon" /></span>
        <span class="user-bubble__command-label">{{ commandBubble.label }}</span>
        <span class="user-bubble__command-sep">·</span>
        <span class="user-bubble__command-topic">{{ commandBubble.topic }}</span>
        <span class="user-bubble__command-sep">·</span>
        <span class="user-bubble__command-count" :title="`${commandBubble.expertCount} 位专家`">
          {{ commandBubble.expertCount }} 位专家
        </span>
      </div>
      <ul v-if="commandBubble.experts.length > 0" class="user-bubble__command-experts">
        <li
          v-for="expert in commandBubble.experts"
          :key="expert"
          class="user-bubble__command-expert"
        >
          <span class="user-bubble__command-expert-dot" />
          <span class="user-bubble__command-expert-name">{{ expert }}</span>
        </li>
      </ul>
    </div>
    <span v-else class="user-bubble__text">{{ content }}</span>

    <div
      v-if="actionsVisible"
      class="user-bubble__actions"
      @click.stop
      @pointerdown.stop
      @mouseenter="onActionsMouseEnter"
      @mouseleave="onActionsMouseLeave"
    >
      <button
        class="ub-action"
        :class="{ 'ub-action--active': copied }"
        type="button"
        tabindex="-1"
        title="复制"
        aria-label="复制"
        @click="onCopy"
      >
        <CopyOutlined />
      </button>

      <a-tooltip v-if="hasReply" title="该消息已有回复，无法删除">
        <span class="ub-action ub-action--disabled" aria-disabled="true">
          <DeleteOutlined />
        </span>
      </a-tooltip>
      <a-popconfirm
        v-else
        title="确定删除该消息？"
        ok-text="删除"
        cancel-text="取消"
        @confirm="onDelete"
      >
        <button class="ub-action" type="button" tabindex="-1" title="删除" aria-label="删除">
          <DeleteOutlined />
        </button>
      </a-popconfirm>

      <button
        class="ub-action"
        type="button"
        tabindex="-1"
        title="回填到输入框"
        aria-label="回填到输入框"
        @click="onRefill"
      >
        <EditOutlined />
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, type Component } from 'vue'
import { CopyOutlined, DeleteOutlined, EditOutlined, BankOutlined, TeamOutlined } from '@ant-design/icons-vue'
import FileAttachment from './FileAttachment.vue'
import { useChatStore, nextMessageIsAssistant } from '@/stores/chatStore'

interface Props {
  content: string
  /** Message id. When absent (preview scenes), no action toolbar is shown. */
  msgId?: string
}

const props = defineProps<Props>()
const chatStore = useChatStore()

const FILE_MARKDOWN_RE = /^\[文件\]\s*\[(.+?)\]\((.+?)\)$/s

// Recognize @board / @team prefixes and surface a structured bubble showing
// the command label, the (bold) topic, an expert-count chip, and a
// per-expert list. Topics can contain anything (CJK, punctuation), so anchor
// on the prefix and only strip known param tokens (`rounds=N`,
// `template=...`) at the head of the remainder. An optional `:experts`
// segment immediately after the prefix is split off and rendered as the
// expert list — the count chip uses the same list.
const COMMAND_RE = /^@(board|team)(?::([^\s]+))?\s+([\s\S]+)$/

interface CommandBubble {
  kind: 'board' | 'team'
  icon: Component
  label: string
  topic: string
  experts: string[]
  expertCount: number
}

const commandBubble = computed<CommandBubble | null>(() => {
  const text = (props.content || '').trim()
  const m = text.match(COMMAND_RE)
  if (!m) return null
  const kind = m[1] as 'board' | 'team'
  const rawExperts = (m[2] || '').trim()
  const rest = m[3].trim()
  // strip optional "rounds=N" or "template=..." param tokens at the front
  const topic = rest.replace(/^[a-z_]+=\S+\s*/i, '').trim()
  const experts = rawExperts
    ? rawExperts.split(',').map((s) => s.trim()).filter(Boolean)
    : []
  return {
    kind,
    icon: kind === 'board' ? BankOutlined : TeamOutlined,
    label: kind === 'board' ? '私董会' : '专家团',
    topic: topic || rest,
    experts,
    expertCount: experts.length,
  }
})

const fileAttachment = computed(() => {
  const match = props.content.match(FILE_MARKDOWN_RE)
  if (!match) return null
  return {
    filename: match[1],
    url: match[2],
  }
})

/**
 * U4: 普通文本消息判定 — 非 file attachment 且非 @board/@team command card。
 * 仅普通文本应用深色气泡，command card / file attachment 保持浅色 --bg-tertiary。
 * 核心正则逻辑提取到 bubbleUtils.isPlainUserText 以支持单元测试；
 * 此处保留对 fileAttachment/commandBubble computed 的引用以维持响应性。
 */
const isPlainText = computed(() => !fileAttachment.value && !commandBubble.value)

// --- Action toolbar visibility ---
// Three independent reasons the toolbar can stay open; the toolbar shows
// while ANY is active. This avoids the classic hover-toolbar bug where
// clicking a button blurs the bubble and hides the toolbar before the click
// lands: on click, `focused` flips false but `hovered` stays true (mouse
// hasn't left the bubble), so the toolbar survives. Action buttons carry
// tabindex="-1" so the keyboard model is Tab→bubble (Enter copies), not a
// focus trap into the buttons.
const hovered = ref(false)
const focused = ref(false)
const touched = ref(false)
const showActions = computed(
  () => hovered.value || focused.value || touched.value,
)
const actionsVisible = computed(() => !!props.msgId && showActions.value)

// --- Copy feedback ---
const copied = ref(false)
let copiedTimer: ReturnType<typeof setTimeout> | null = null

function scheduleCopiedReset(): void {
  if (copiedTimer) clearTimeout(copiedTimer)
  copiedTimer = setTimeout(() => {
    copied.value = false
    copiedTimer = null
  }, 1200)
}

async function onCopy(): Promise<void> {
  try {
    await navigator.clipboard.writeText(props.content)
    copied.value = true
    scheduleCopiedReset()
  } catch {
    // clipboard unavailable (e.g. insecure context) — fail silently
  }
}

// --- Delete (frontend-only hide) ---
const hasReply = computed(() =>
  props.msgId ? nextMessageIsAssistant(chatStore.currentMessages, props.msgId) : false,
)

function onDelete(): void {
  const convId = chatStore.currentConversationId
  if (!convId || !props.msgId) return
  chatStore.deleteMessage(convId, props.msgId)
}

// --- Refill input ---
function onRefill(): void {
  chatStore.setRefillText(props.content)
}

// --- Touch parity ---
// Touch has no hover; tap on the bubble toggles the toolbar, tap elsewhere
// hides it. ponytail: a single passive document touchstart listener covers
// the "tap elsewhere" case without per-show add/remove churn.
const rootRef = ref<HTMLElement | null>(null)

// --- Delayed hide for hover toolbar ---
// 操作按钮位于气泡左侧外部（right: calc(100% + var(--space-2))），
// 鼠标从气泡穿过 8px 间隙移到按钮时触发 mouseleave，导致按钮消失。
// 延迟 150ms 隐藏给鼠标时间穿过间隙；按钮的 mouseenter 会取消延迟。
let hideTimer: ReturnType<typeof setTimeout> | null = null

function clearHideTimer(): void {
  if (hideTimer !== null) {
    clearTimeout(hideTimer)
    hideTimer = null
  }
}

function scheduleHide(): void {
  clearHideTimer()
  hideTimer = setTimeout(() => {
    hovered.value = false
    hideTimer = null
  }, 150)
}

function onBubbleMouseEnter(): void {
  clearHideTimer()
  hovered.value = true
}

function onBubbleMouseLeave(): void {
  scheduleHide()
}

function onActionsMouseEnter(): void {
  clearHideTimer()
  hovered.value = true
}

function onActionsMouseLeave(): void {
  scheduleHide()
}

function onPointerDown(event: PointerEvent): void {
  if (event.pointerType === 'touch') {
    touched.value = !touched.value
  }
}

function onDocTouchStart(event: TouchEvent): void {
  const target = event.target as Node | null
  if (rootRef.value && target && !rootRef.value.contains(target)) {
    touched.value = false
  }
}

onMounted(() => {
  document.addEventListener('touchstart', onDocTouchStart, { passive: true })
})

onUnmounted(() => {
  document.removeEventListener('touchstart', onDocTouchStart)
  if (copiedTimer) {
    clearTimeout(copiedTimer)
    copiedTimer = null
  }
  clearHideTimer()
})
</script>

<style scoped>
.user-bubble {
  position: relative;
  background: var(--bg-tertiary);
  color: var(--text-primary);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-lg);
  font-size: var(--font-base);
  line-height: var(--leading-normal);
  max-width: 60%;
  word-break: break-word;
}

.user-bubble--focusable {
  outline: none;
}

.user-bubble--focusable:focus-visible {
  outline: 2px solid var(--color-primary, #1a1a1a);
  outline-offset: 2px;
}

/* U4: 普通文本消息参考 demo 深色气泡 (--color-primary + --text-inverse)。
   command card / file attachment 保持 .user-bubble 默认 --bg-tertiary 浅色背景。
   --color-primary / --text-inverse 在 light/dark mode 下自动反转。 */
.user-bubble--text {
  background: var(--color-primary);
  color: var(--text-inverse);
  padding: var(--space-3) var(--space-4);
  max-width: 70%;
}

.user-bubble__text {
  white-space: pre-wrap;
}

.user-bubble__command {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 220px;
  max-width: 100%;
}

.user-bubble__command-header {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.user-bubble__command-icon {
  font-size: 14px;
  line-height: 1;
}

.user-bubble__command-label {
  font-size: var(--font-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--text-primary);
}

.user-bubble__command-sep {
  color: var(--text-placeholder);
  font-size: var(--font-sm);
}

.user-bubble__command-topic {
  font-size: var(--font-sm);
  font-weight: var(--font-weight-semibold);
  color: var(--text-primary);
  word-break: break-word;
  flex: 1 1 auto;
  min-width: 0;
}

/* Expert-count chip — high-contrast pill so the number reads as a
   separate piece of information from the topic. */
.user-bubble__command-count {
  display: inline-flex;
  align-items: center;
  font-size: var(--font-xs);
  font-weight: var(--font-weight-semibold);
  color: var(--text-inverse, #ffffff);
  background: var(--color-primary, #1a1a1a);
  padding: 2px 8px;
  border-radius: 999px;
  line-height: 1.4;
  white-space: nowrap;
  flex-shrink: 0;
}

.user-bubble__command-experts {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  border-top: 1px solid var(--border-color);
  padding-top: 8px;
}

.user-bubble__command-expert {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: var(--font-sm);
  color: var(--text-secondary);
}

.user-bubble__command-expert-dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--text-tertiary);
  flex-shrink: 0;
}

.user-bubble__command-expert-name {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: var(--font-xs);
  color: var(--text-secondary);
}

.user-bubble__actions {
  position: absolute;
  top: 50%;
  right: calc(100% + var(--space-2));
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-1);
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
  z-index: 5;
}

.ub-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-tertiary);
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  transition: color 0.15s ease, background-color 0.15s ease;
}

.ub-action:hover {
  color: var(--text-primary);
  background: var(--bg-tertiary);
}

.ub-action--active {
  color: #52c41a;
}

.ub-action--disabled {
  opacity: 0.45;
  cursor: not-allowed;
  color: var(--text-placeholder);
}

.ub-action--disabled:hover {
  color: var(--text-placeholder);
  background: transparent;
}
</style>
