<template>
  <div class="agent-chat-preview">
    <div class="chat-header">
      <div class="agent-info">
        <el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
        <span class="agent-name">{{ agentName || 'Agent' }}</span>
      </div>
      <div class="header-actions">
        <el-button text size="small" title="仅清空当前界面，刷新页面后会重新加载历史记录" @click="handleClearChat">
          <el-icon><Delete /></el-icon>
          清空对话
        </el-button>
      </div>
    </div>
    
    <div class="chat-messages" ref="messagesContainer">
      <!-- 开场白 -->
      <div v-if="openingMessage" class="message agent-message">
        <el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
        <div class="message-content">
          <div class="message-bubble" v-html="formatMessage(openingMessage)"></div>
        </div>
      </div>
      
      <!-- 预设问题（无历史且无对话时显示） -->
      <div v-if="presetQuestions.length > 0 && messages.length === 0 && !historyLoading" class="preset-questions">
        <div
          v-for="(question, index) in presetQuestions"
          :key="index"
          class="preset-question"
          @click="handlePresetQuestion(question)"
        >
          {{ question }}
        </div>
      </div>
      
      <!-- 对话消息 -->
      <div
        v-for="(message, index) in messages"
        :key="index"
        :class="['message', message.role === 'user' ? 'user-message' : 'agent-message']"
      >
        <el-avatar
          v-if="message.role === 'agent'"
          :size="32"
          :src="agentAvatar"
          :icon="UserFilled"
        />
        <div class="message-content">
          <div class="message-bubble" v-html="formatMessage(message.content)"></div>
          <div
            v-if="message.attachments?.length"
            :class="['message-attachments', message.role === 'user' ? 'is-user' : 'is-agent']"
          >
            <div
              v-for="(a, ai) in message.attachments"
              :key="`msg-att-${index}-${ai}`"
              class="message-attachment-item"
            >
              <img
                v-if="a.isImage && a.thumbUrl"
                :src="a.thumbUrl"
                :alt="a.filename"
                class="message-attachment-image"
                @click="openImagePreview(a)"
              />
              <div v-else class="message-attachment-file">
                <el-icon :size="14"><Document /></el-icon>
                <span class="message-attachment-name">{{ a.filename }}</span>
              </div>
            </div>
          </div>
          <div class="message-time">{{ formatTime(message.timestamp) }}</div>
        </div>
        <el-avatar
          v-if="message.role === 'user'"
          :size="32"
          :icon="UserFilled"
          style="background-color: #409eff;"
        />
      </div>
      
      <!-- 加载中 -->
      <div v-if="loading" class="message agent-message">
        <el-avatar :size="32" :src="agentAvatar" :icon="UserFilled" />
        <div class="message-content">
          <div class="message-bubble loading">
            <el-icon class="is-loading"><Loading /></el-icon>
            <span>正在思考...</span>
          </div>
        </div>
      </div>
      
      <!-- 节点测试结果 -->
      <div v-if="props.nodeTestResult" class="node-test-result">
        <el-alert
          :type="props.nodeTestResult.result.status === 'success' ? 'success' : 'error'"
          :closable="true"
          show-icon
          @close="handleCloseNodeTest"
        >
          <template #title>
            <div class="node-test-header">
              <strong>节点测试: {{ props.nodeTestResult.node.data?.label || props.nodeTestResult.node.type }}</strong>
              <span class="node-test-time">{{ props.nodeTestResult.result.execution_time }}ms</span>
            </div>
          </template>
          <div class="node-test-content">
            <div v-if="props.nodeTestResult.result.status === 'success'">
              <div class="test-section">
                <strong>输入:</strong>
                <pre>{{ JSON.stringify(props.nodeTestResult.input, null, 2) }}</pre>
              </div>
              <div class="test-section">
                <strong>输出:</strong>
                <pre>{{ JSON.stringify(props.nodeTestResult.result.output, null, 2) }}</pre>
              </div>
            </div>
            <div v-else>
              <div class="test-section">
                <strong>错误:</strong>
                <pre>{{ props.nodeTestResult.result.error_message || '未知错误' }}</pre>
              </div>
            </div>
          </div>
        </el-alert>
      </div>
    </div>
    <el-dialog
      v-model="imagePreviewVisible"
      title="图片预览"
      width="min(86vw, 980px)"
      append-to-body
      destroy-on-close
    >
      <div class="image-preview-dialog-body">
        <img
          v-if="imagePreviewSrc"
          :src="imagePreviewSrc"
          :alt="imagePreviewTitle || '图片预览'"
          class="image-preview-dialog-img"
        />
      </div>
    </el-dialog>

    <div
      class="chat-input-area"
      :class="{ 'is-drag-over': inputDragOver }"
      @dragenter.prevent="onInputDragEnter"
      @dragleave.prevent="onInputDragLeave"
      @dragover.prevent="onInputDragOver"
      @drop.prevent="onInputDrop"
    >
      <input
        ref="fileInputRef"
        type="file"
        class="hidden-file-input"
        multiple
        accept=".txt,.md,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.csv,.json,.py,.ts,.js,.vue,.html,.xml,.zip,.png,.jpg,.jpeg,.gif,.webp,.bmp,.tif,.tiff"
        @change="onFileInputChange"
      />
      <div class="input-toolbar">
        <el-button text size="small" :disabled="loading || !agentId" @click="handleAttachFile">
          <el-icon><Paperclip /></el-icon>
        </el-button>
      </div>
      <div v-if="pendingAttachments.length > 0" class="attachment-thumb-strip">
        <div
          v-for="(a, i) in pendingAttachments"
          :key="`th-${a.relative_path}-${i}`"
          class="attachment-thumb-item"
        >
          <button
            type="button"
            class="attachment-thumb-remove"
            title="移除"
            @click="removeAttachment(i)"
          >
            ×
          </button>
          <div v-if="a.thumbUrl" class="attachment-thumb-img-wrap">
            <img :src="a.thumbUrl" class="attachment-thumb-img" :alt="a.filename" />
          </div>
          <div v-else class="attachment-thumb-file-placeholder">
            <el-icon :size="28"><Document /></el-icon>
          </div>
          <div class="attachment-thumb-caption" :title="a.filename">{{ a.filename }}</div>
        </div>
      </div>
      <div v-if="pendingAttachments.some((x) => x.previewText || x.previewNote)" class="attachment-preview-stack">
        <div
          v-for="(a, i) in pendingAttachments"
          v-show="a.previewText || a.previewNote"
          :key="`pv-${a.relative_path}-${i}`"
          class="attachment-preview-card"
        >
          <div class="attachment-preview-title">
            {{ a.previewText ? '内容预览' : '已上传' }} · {{ a.filename }}
          </div>
          <pre v-if="a.previewText" class="attachment-preview-body">{{ a.previewText }}</pre>
          <div v-else-if="a.previewNote" class="attachment-preview-note">{{ a.previewNote }}</div>
        </div>
      </div>
      <el-input
        v-model="inputMessage"
        type="textarea"
        :rows="2"
        placeholder="发送消息…（可将图片拖入此区域）"
        @keydown.enter.exact.prevent="handleSendMessage"
        @keydown.enter.shift.exact="handleNewLine"
        :disabled="loading || !agentId"
      />
      <div class="input-footer">
        <div class="disclaimer">
          内容由AI生成，无法确保真实准确，仅供参考。
        </div>
        <div class="input-actions">
          <el-button text size="small" @click="handleVoiceInput">
            <el-icon><Microphone /></el-icon>
          </el-button>
          <el-button
            type="primary"
            @click="handleSendMessage"
            :disabled="(!inputMessage.trim() && pendingAttachments.length === 0) || loading || !agentId"
            :loading="loading"
          >
            <el-icon><Promotion /></el-icon>
          </el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, nextTick, onMounted, onActivated, onUnmounted } from 'vue'
import { ElMessage, ElNotification } from 'element-plus'
import {
  UserFilled,
  Delete,
  Loading,
  Paperclip,
  Microphone,
  Promotion,
  Document
} from '@element-plus/icons-vue'
import api from '@/api'
import { useUserStore } from '@/stores/user'

interface Message {
  role: 'user' | 'agent'
  content: string
  timestamp: number
  attachments?: MessageAttachment[]
}

interface PendingAttachment {
  relative_path: string
  filename: string
  size?: number
  content_type?: string
  /** 图片：本地 blob URL，用于缩略图（移除或发送后 revoke） */
  thumbUrl?: string
  /** 浏览器本地读取的纯文本预览（发送后也会写入气泡） */
  previewText?: string
  /** 非文本类：简短说明 */
  previewNote?: string
}

interface MessageAttachment {
  relative_path: string
  filename: string
  thumbUrl?: string
  isImage?: boolean
  content_type?: string
}

const props = defineProps<{
  agentId?: string
  agentName?: string
  agentAvatar?: string
  openingMessage?: string
  presetQuestions?: string[]
  nodeTestResult?: any
}>()

const emit = defineEmits<{
  'execution-status': [status: any]
}>()

const userStore = useUserStore()

const messages = ref<Message[]>([])
const inputMessage = ref('')
const loading = ref(false)
const historyLoading = ref(false)
const messagesContainer = ref<HTMLElement>()
const fileInputRef = ref<HTMLInputElement | null>(null)
const pendingAttachments = ref<PendingAttachment[]>([])
/** 已发送、待轮询结束后再 revoke 的附件（含 thumbUrl） */
const blobsPendingRevokeAfterRun = ref<PendingAttachment[] | null>(null)
/** 拖放图片到输入区时高亮 */
const inputDragOver = ref(false)
const imagePreviewVisible = ref(false)
const imagePreviewSrc = ref('')
const imagePreviewTitle = ref('')
let pollingInterval: any = null
let replyAdded = false // 标志位：防止重复添加回复

/** 设计器预览对话本地镜像（同浏览器同 Agent 在接口不可用或尚未落库时仍可恢复） */
const DESIGN_CHAT_STORAGE_PREFIX = 'agent_design_chat_v1'
const MAX_PERSIST_MESSAGES = 200
let persistTimer: ReturnType<typeof setTimeout> | null = null
let loadHistoryGeneration = 0

/** 本地缓存按「登录用户 + Agent」分桶，跨设备以服务端为准时仍避免同机多账号串缓存 */
function designChatStorageKey(agentId: string) {
  return `${DESIGN_CHAT_STORAGE_PREFIX}_${agentId}_${getPreviewContextUserId(agentId)}`
}

function readDesignChatFromStorage(agentId: string): Message[] {
  try {
    const raw = localStorage.getItem(designChatStorageKey(agentId))
    if (!raw) return []
    const parsed = JSON.parse(raw) as unknown
    if (!Array.isArray(parsed)) return []
    return parsed
      .filter(
        (m: any) =>
          m &&
          (m.role === 'user' || m.role === 'agent') &&
          typeof m.content === 'string' &&
          typeof m.timestamp === 'number'
      )
      .map((m: any) => ({
        role: m.role,
        content: m.content,
        timestamp: m.timestamp,
        attachments: Array.isArray(m.attachments)
          ? m.attachments
              .filter((a: any) => a && typeof a.relative_path === 'string' && typeof a.filename === 'string')
              .map((a: any) => ({
                relative_path: String(a.relative_path),
                filename: String(a.filename),
                isImage: !!a.isImage,
                content_type: a.content_type ? String(a.content_type) : undefined
              }))
          : undefined
      }))
  } catch {
    return []
  }
}

function writeDesignChatToStorage(agentId: string, list: Message[]) {
  try {
    const slice = list.slice(-MAX_PERSIST_MESSAGES)
    const compact = slice.map((m) => ({
      role: m.role,
      content: m.content,
      timestamp: m.timestamp,
      attachments: m.attachments?.map((a) => ({
        relative_path: a.relative_path,
        filename: a.filename,
        isImage: !!a.isImage,
        content_type: a.content_type
      }))
    }))
    let raw = JSON.stringify(compact)
    if (raw.length > 450_000) {
      raw = JSON.stringify(slice.slice(-80))
    }
    localStorage.setItem(designChatStorageKey(agentId), raw)
  } catch {
    // 存储满或禁用
  }
}

function schedulePersistDesignChat() {
  if (!props.agentId) return
  if (persistTimer) clearTimeout(persistTimer)
  persistTimer = setTimeout(() => {
    persistTimer = null
    if (props.agentId) writeDesignChatToStorage(props.agentId, messages.value)
  }, 400)
}

function clearDesignChatStorage(agentId: string) {
  try {
    localStorage.removeItem(designChatStorageKey(agentId))
  } catch {
    /* ignore */
  }
}

/**
 * 预览/执行写入 input_data.user_id：
 * - 已登录：用账号 id，换设备同一账号可拉同一套历史
 * - 未登录：退回浏览器级 preview_ 会话（仅本机）
 */
function getPreviewContextUserId(agentId: string): string {
  const uid = userStore.user?.id
  if (uid) return String(uid)
  return getPreviewSessionUserId(agentId)
}

/** 未登录时的浏览器会话 id（见 agent记忆实现方案.md） */
function getPreviewSessionUserId(agentId: string): string {
  const key = `agent_preview_uid_${agentId}`
  const newId = () =>
    typeof crypto !== 'undefined' && crypto.randomUUID
      ? `preview_${crypto.randomUUID().replace(/-/g, '').slice(0, 16)}`
      : `preview_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`
  try {
    let id = localStorage.getItem(key)
    if (!id) {
      id = newId()
      localStorage.setItem(key, id)
    }
    return id
  } catch {
    // 勿用 Date.now() 作唯一后缀：每次刷新会换 ID，历史接口永远对不上
    try {
      let id = sessionStorage.getItem(key)
      if (!id) {
        id = newId()
        sessionStorage.setItem(key, id)
      }
      return id
    } catch {
      return `preview_${agentId}_browser`
    }
  }
}

/** 从服务端恢复本会话（preview_user_id 与 input_data.user_id 一致）的已完成对话 */
async function loadChatHistory() {
  if (!props.agentId) return
  const gen = ++loadHistoryGeneration
  const aid = props.agentId
  historyLoading.value = true

  // 先读本地镜像，刷新/再次进入页面时立刻有内容（不闪空）
  const cached = readDesignChatFromStorage(aid)
  if (cached.length > 0) {
    messages.value = cached
    await hydrateMessageAttachmentThumbs(messages.value)
    nextTick(() => scrollToBottom())
  }

  const applyServerTurns = (
    turns: Array<{
      user_text: string
      agent_text: string
      created_at: string
      execution_id: string
      attachments?: Array<{
        relative_path: string
        filename: string
        content_type?: string
      }>
    }>
  ) => {
    const next: Message[] = []
    for (const t of turns) {
      const base = new Date(t.created_at).getTime()
      const msgAttachments: MessageAttachment[] = (t.attachments || []).map((a) => ({
        relative_path: a.relative_path,
        filename: a.filename,
        content_type: a.content_type,
        isImage:
          !!a.content_type?.startsWith('image/') ||
          /\.(png|jpe?g|gif|webp|bmp|tiff?)$/i.test(a.filename || a.relative_path)
      }))
      next.push({
        role: 'user',
        content: t.user_text || ' ',
        timestamp: Number.isFinite(base) ? base - 400 : Date.now() - 400,
        attachments: msgAttachments.length ? msgAttachments : undefined
      })
      next.push({
        role: 'agent',
        content: t.agent_text || '',
        timestamp: Number.isFinite(base) ? base : Date.now()
      })
    }
    return next
  }

  type HistTurn = {
    user_text: string
    agent_text: string
    created_at: string
    execution_id: string
    attachments?: Array<{
      relative_path: string
      filename: string
      content_type?: string
    }>
  }

  const fetchHist = async (previewUserId: string | undefined): Promise<HistTurn[]> => {
    const url = `/api/v1/agents/${aid}/preview-chat-history`
    const params: Record<string, string | number> = { limit: 80 }
    if (previewUserId) params.preview_user_id = previewUserId
    const res = await api.get(url, { params, skipErrorHandler: true })
    return Array.isArray(res.data) ? (res.data as HistTurn[]) : []
  }

  try {
    const contextId = getPreviewContextUserId(aid)
    const browserPreviewId = getPreviewSessionUserId(aid)
    const byExe = new Map<string, HistTurn>()
    const merge = (list: HistTurn[]) => {
      for (const t of list) {
        if (t?.execution_id) byExe.set(t.execution_id, t)
      }
    }

    merge(await fetchHist(contextId))
    // 登录前产生的记录多为 preview_xxx；登录后 context 变为账号 id，必须合并本机仍保存的 preview 会话，否则会只剩极少数条
    if (userStore.user?.id && browserPreviewId !== contextId) {
      merge(await fetchHist(browserPreviewId))
    }
    // 两次仍为空（例如新浏览器只有库里旧 preview、且本地无该 preview id）：再拉本 Agent 最近完成记录（设计页需 read 权限）
    if (byExe.size === 0) {
      merge(await fetchHist(undefined))
    }

    const turns = Array.from(byExe.values()).sort(
      (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
    )

    if (gen !== loadHistoryGeneration) return

    // 用户已在发消息/轮询中：不要用历史接口覆盖当前界面，避免吞掉刚发的内容
    if (loading.value) {
      return
    }

    if (Array.isArray(turns) && turns.length > 0) {
      const next = applyServerTurns(turns)
      revokeMessageAttachmentBlobs(messages.value)
      messages.value = next
      await hydrateMessageAttachmentThumbs(messages.value)
      writeDesignChatToStorage(aid, next)
    }
    // 服务端无记录时保留上面已展示的本地镜像（若有）
  } catch (e) {
    console.warn('加载对话历史失败', e)
    // 网络/404/401：保留本地镜像
  } finally {
    if (gen === loadHistoryGeneration) {
      historyLoading.value = false
      nextTick(() => scrollToBottom())
    }
  }
}

function handleAttachFile() {
  fileInputRef.value?.click()
}

function isImageFile(file: File): boolean {
  if (file.type && file.type.startsWith('image/')) return true
  const ext = file.name.split('.').pop()?.toLowerCase() || ''
  return ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tif', 'tiff'].includes(ext)
}

function revokeAttachmentBlobs(items: PendingAttachment[]) {
  for (const a of items) {
    if (a.thumbUrl) {
      try {
        URL.revokeObjectURL(a.thumbUrl)
      } catch {
        /* ignore */
      }
    }
  }
}

function revokeMessageAttachmentBlobs(items: Message[]) {
  for (const m of items) {
    for (const a of m.attachments || []) {
      if (a.thumbUrl) {
        try {
          URL.revokeObjectURL(a.thumbUrl)
        } catch {
          /* ignore */
        }
      }
    }
  }
}

function isThumbInMessages(url: string): boolean {
  if (!url) return false
  return messages.value.some((m) => (m.attachments || []).some((a) => a.thumbUrl === url))
}

async function fetchAttachmentThumb(relativePath: string): Promise<string | undefined> {
  try {
    const res = await api.get('/api/v1/uploads/preview/file', {
      params: { file_path: relativePath },
      responseType: 'blob',
      skipErrorHandler: true
    })
    return URL.createObjectURL(res.data as Blob)
  } catch {
    return undefined
  }
}

async function hydrateMessageAttachmentThumbs(target: Message[]) {
  for (const m of target) {
    for (const a of m.attachments || []) {
      if (!a.isImage || a.thumbUrl || !a.relative_path) continue
      const url = await fetchAttachmentThumb(a.relative_path)
      if (url) a.thumbUrl = url
    }
  }
}

function openImagePreview(a: MessageAttachment) {
  if (!a.thumbUrl) return
  imagePreviewSrc.value = a.thumbUrl
  imagePreviewTitle.value = a.filename
  imagePreviewVisible.value = true
}

function removeAttachment(index: number) {
  const cur = pendingAttachments.value[index]
  if (cur?.thumbUrl) {
    try {
      URL.revokeObjectURL(cur.thumbUrl)
    } catch {
      /* ignore */
    }
  }
  pendingAttachments.value.splice(index, 1)
}

const PREVIEW_MAX_FILE_BYTES = 512 * 1024
const PREVIEW_MAX_CHARS = 12000
const TEXT_PREVIEW_EXTS = new Set([
  'txt',
  'md',
  'markdown',
  'csv',
  'json',
  'jsonl',
  'log',
  'yaml',
  'yml',
  'py',
  'ts',
  'js',
  'mjs',
  'cjs',
  'vue',
  'html',
  'htm',
  'xml',
  'css',
  'sql',
  'sh',
  'bat',
  'ps1',
  'ini',
  'cfg',
  'properties',
  'rst',
  'tex',
  'gitignore',
  'env'
])

function escapeHtml(s: string) {
  return s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

function readLocalTextPreview(file: File): Promise<string | null> {
  const m = file.name.match(/\.([^.]+)$/)
  const ext = (m?.[1] || '').toLowerCase()
  if (!TEXT_PREVIEW_EXTS.has(ext)) return Promise.resolve(null)
  if (file.size > PREVIEW_MAX_FILE_BYTES) return Promise.resolve(null)
  return new Promise((resolve) => {
    const r = new FileReader()
    r.onload = () => {
      let t = String(r.result ?? '')
      if (t.length > PREVIEW_MAX_CHARS) {
        t =
          t.slice(0, PREVIEW_MAX_CHARS) +
          `\n\n…（仅展示前 ${PREVIEW_MAX_CHARS} 字，完整内容请发送后由助手读取文件）`
      }
      resolve(t)
    }
    r.onerror = () => resolve(null)
    r.readAsText(file, 'UTF-8')
  })
}

/** 与回形针选择、拖放共用：上传到预览区并加入待发送列表 */
async function uploadPreviewFiles(fileList: File[]) {
  if (!fileList.length) return
  if (!props.agentId) {
    ElMessage.warning('请先选中要预览的智能体后再上传附件')
    return
  }
  const uploadedNames: string[] = []
  for (const file of fileList) {
    const fd = new FormData()
    fd.append('file', file)
    try {
      const res = await api.post<PendingAttachment & { content_type?: string }>(
        '/api/v1/uploads/preview',
        fd
      )
      const d = res.data as PendingAttachment
      if (d?.relative_path) {
        const previewText = (await readLocalTextPreview(file)) || undefined
        let thumbUrl: string | undefined
        if (isImageFile(file)) {
          thumbUrl = URL.createObjectURL(file)
        }
        const entry: PendingAttachment = {
          relative_path: d.relative_path,
          filename: d.filename || file.name,
          size: d.size,
          content_type: (res.data as any)?.content_type,
          thumbUrl,
          previewText,
          previewNote: previewText
            ? undefined
            : '发送后由智能体通过 file_read 读取（支持 PDF、Word、Excel、图片 OCR 等）'
        }
        pendingAttachments.value.push(entry)
        uploadedNames.push(d.filename || file.name)
      } else {
        ElMessage.warning(`「${file.name}」上传未返回有效路径，请重试`)
      }
    } catch {
      /* 错误提示由 api 拦截器统一处理 */
    }
  }
  if (uploadedNames.length === 1) {
    ElNotification.success({
      title: '附件上传成功',
      message: `「${uploadedNames[0]}」已加入待发送区，发送后智能体可通过 file_read 读取。`,
      duration: 5000,
      offset: 72
    })
  } else if (uploadedNames.length > 1) {
    ElNotification.success({
      title: '附件上传成功',
      message: `共 ${uploadedNames.length} 个文件已加入待发送区：${uploadedNames.join('、')}`,
      duration: 5500,
      offset: 72
    })
  }
}

async function onFileInputChange(ev: Event) {
  const input = ev.target as HTMLInputElement
  const files = input.files
  input.value = ''
  if (!files?.length) return
  await uploadPreviewFiles(Array.from(files))
}

function onInputDragEnter(e: DragEvent) {
  if (!props.agentId || loading.value) return
  e.preventDefault()
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
  inputDragOver.value = true
}

function onInputDragLeave(e: DragEvent) {
  e.preventDefault()
  const el = e.currentTarget as HTMLElement
  const rel = e.relatedTarget as Node | null
  if (rel && el.contains(rel)) return
  inputDragOver.value = false
}

function onInputDragOver(e: DragEvent) {
  if (!props.agentId || loading.value) return
  e.preventDefault()
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}

async function onInputDrop(e: DragEvent) {
  e.preventDefault()
  inputDragOver.value = false
  if (!props.agentId || loading.value) {
    if (!props.agentId) ElMessage.warning('请先选中要预览的智能体后再拖入图片')
    return
  }
  const dt = e.dataTransfer
  if (!dt?.files?.length) return
  const list = Array.from(dt.files)
  const images = list.filter((f) => isImageFile(f))
  if (images.length === 0) {
    ElMessage.warning('请拖入图片文件（如 png、jpg、jpeg、webp、gif 等）')
    return
  }
  if (images.length < list.length) {
    ElMessage.info('已忽略非图片文件，仅添加图片附件')
  }
  await uploadPreviewFiles(images)
}

// 发送消息
const handleSendMessage = async () => {
  if ((!inputMessage.value.trim() && pendingAttachments.value.length === 0) || loading.value || !props.agentId)
    return

  const userMessage = inputMessage.value.trim()
  const attachSnap = pendingAttachments.value.slice()
  pendingAttachments.value = []
  inputMessage.value = ''

  const attachHint =
    attachSnap.length > 0
      ? `\n\n[用户上传的附件（相对工作区根路径，可用 file_read 读取）:\n${attachSnap.map((a) => `- ${a.relative_path}（${a.filename}）`).join('\n')}]`
      : ''
  const mergedForModel = (userMessage || '（用户上传了附件，请阅读并回答。）') + attachHint

  let userBubble = userMessage
  if (attachSnap.length) {
    const lines = attachSnap.map((a) => `📎 ${a.filename}`)
    let head = userBubble ? `${userBubble}\n${lines.join('\n')}` : lines.join('\n')
    const textPreviews = attachSnap
      .filter((a) => a.previewText?.trim())
      .map(
        (a) =>
          `\n—— ${a.filename} 原文预览 ——\n${escapeHtml(a.previewText!.trim())}`
      )
    if (textPreviews.length) {
      head += `\n${textPreviews.join('\n')}`
    }
    userBubble = head
  }

  // 添加用户消息
  const userAttachments: MessageAttachment[] = attachSnap.map((a) => ({
    relative_path: a.relative_path,
    filename: a.filename,
    thumbUrl: a.thumbUrl,
    content_type: a.content_type,
    isImage:
      !!a.thumbUrl ||
      Boolean(a.content_type?.startsWith('image/')) ||
      /\.(png|jpe?g|gif|webp|bmp|tiff?)$/i.test(a.filename)
  }))
  messages.value.push({
    role: 'user',
    content: userBubble || '（附件）',
    timestamp: Date.now(),
    attachments: userAttachments.length ? userAttachments : undefined
  })
  
  // 滚动到底部
  scrollToBottom()
  
  // 发送到Agent
  loading.value = true
  try {
    const response = await api.post('/api/v1/executions', {
      agent_id: props.agentId,
      input_data: {
        USER_INPUT: mergedForModel,
        query: mergedForModel,
        user_id: getPreviewContextUserId(props.agentId),
        attachments: attachSnap
      }
    })
    
    const execution = response.data
    blobsPendingRevokeAfterRun.value = attachSnap.length ? attachSnap : null
    
    // 重置标志位
    replyAdded = false
    
    // 轮询执行状态
    const checkStatus = async () => {
      const finishBlobs = () => {
        if (blobsPendingRevokeAfterRun.value) {
          const revokable = blobsPendingRevokeAfterRun.value.filter(
            (x) => !x.thumbUrl || !isThumbInMessages(x.thumbUrl)
          )
          revokeAttachmentBlobs(revokable)
          blobsPendingRevokeAfterRun.value = null
        }
      }
      try {
        // 如果已经添加过回复，直接返回，避免重复添加
        if (replyAdded) {
          return
        }
        
        // 获取详细执行状态（包含节点执行信息）
        const statusResponse = await api.get(`/api/v1/executions/${execution.id}/status`)
        const status = statusResponse.data
        
        // 将执行状态传递给父组件，用于显示工作流动画
        emit('execution-status', status)
        
        // 获取执行详情（用于提取输出结果）
        const execResponse = await api.get(`/api/v1/executions/${execution.id}`)
        const exec = execResponse.data
        
        if (exec.status === 'completed') {
          // 防止重复添加：如果已经添加过回复，直接返回
          if (replyAdded) {
            return
          }
          
          // 标记已添加回复
          replyAdded = true
          
          // 提取Agent回复
          let agentReply = ''
          if (exec.output_data) {
            // 优先从 result 字段获取（工作流执行结果）
            if (exec.output_data.result) {
              // result 字段应该是纯文本字符串（End节点的输出）
              if (typeof exec.output_data.result === 'string') {
                agentReply = exec.output_data.result
              } else {
                agentReply = String(exec.output_data.result)
              }
            } else if (typeof exec.output_data === 'string') {
              agentReply = exec.output_data
            } else if (exec.output_data.output) {
              agentReply = exec.output_data.output
            } else if (exec.output_data.response) {
              agentReply = exec.output_data.response
            } else if (exec.output_data.text) {
              agentReply = exec.output_data.text
            } else {
              agentReply = JSON.stringify(exec.output_data, null, 2)
            }
          }
          
          messages.value.push({
            role: 'agent',
            content: agentReply || '执行完成',
            timestamp: Date.now()
          })
          
          loading.value = false
          scrollToBottom()
          
          // 延迟清除执行状态，让用户能看到最终的执行结果
          setTimeout(() => {
            emit('execution-status', null)
          }, 3000) // 3秒后清除
          
          if (pollingInterval) {
            clearInterval(pollingInterval)
            pollingInterval = null
          }
          finishBlobs()
        } else if (exec.status === 'failed') {
          // 防止重复添加：如果已经添加过回复，直接返回
          if (replyAdded) {
            return
          }
          
          // 标记已添加回复
          replyAdded = true
          
          messages.value.push({
            role: 'agent',
            content: `执行失败: ${exec.error_message || '未知错误'}`,
            timestamp: Date.now()
          })
          loading.value = false
          scrollToBottom()
          
          // 延迟清除执行状态，让用户能看到失败节点的状态
          setTimeout(() => {
            emit('execution-status', null)
          }, 5000) // 5秒后清除
          
          if (pollingInterval) {
            clearInterval(pollingInterval)
            pollingInterval = null
          }
          finishBlobs()
        } else {
          // 继续轮询（pending 或 running 状态）
          // 不需要做任何操作，等待下次轮询
        }
      } catch (error: any) {
        // 防止重复添加：如果已经添加过回复，直接返回
        if (replyAdded) {
          return
        }
        
        // 标记已添加回复
        replyAdded = true
        finishBlobs()
        
        const st = error.response?.status
        const msg401 =
          st === 401
            ? '登录已过期或未登录（401）。请重新登录后再试预览对话。'
            : error.response?.data?.detail || error.message
        messages.value.push({
          role: 'agent',
          content: `获取执行结果失败：${msg401}`,
          timestamp: Date.now()
        })
        loading.value = false
        scrollToBottom()
        
        // 清除执行状态
        emit('execution-status', null)
        
        if (pollingInterval) {
          clearInterval(pollingInterval)
          pollingInterval = null
        }
      }
    }
    
    // 使用 setInterval 进行轮询，每500毫秒检查一次（更频繁，能捕获快速执行的节点）
    pollingInterval = setInterval(checkStatus, 500)
    // 立即执行一次
    checkStatus()
    
  } catch (error: any) {
    console.error('发送消息失败:', error)
    if (attachSnap.length) {
      pendingAttachments.value = [...attachSnap, ...pendingAttachments.value]
    }
    inputMessage.value = userMessage
    const last = messages.value[messages.value.length - 1]
    if (last?.role === 'user') messages.value.pop()
    messages.value.push({
      role: 'agent',
      content: `发送失败: ${error.response?.data?.detail || error.message}`,
      timestamp: Date.now()
    })
    loading.value = false
    scrollToBottom()
    ElMessage.error(error.response?.data?.detail || '发送失败')
  }
}

// 预设问题点击
const handlePresetQuestion = (question: string) => {
  inputMessage.value = question
  handleSendMessage()
}

// 清空对话
const handleClearChat = () => {
  revokeAttachmentBlobs([...pendingAttachments.value])
  revokeMessageAttachmentBlobs(messages.value)
  pendingAttachments.value = []
  if (blobsPendingRevokeAfterRun.value) {
    revokeAttachmentBlobs(blobsPendingRevokeAfterRun.value)
    blobsPendingRevokeAfterRun.value = null
  }
  messages.value = []
  if (props.agentId) clearDesignChatStorage(props.agentId)
  ElMessage.info('已清空预览对话（含本机缓存）；刷新后不再恢复本条记录')
  // 清除执行状态
  emit('execution-status', null)
  // 清除轮询
  if (pollingInterval) {
    clearInterval(pollingInterval)
    pollingInterval = null
  }
}

// 滚动到底部
const scrollToBottom = () => {
  nextTick(() => {
    if (messagesContainer.value) {
      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
  })
}

/**
 * 知你等工作流约定「自然语言 + 末尾单行 JSON（intent/reply/user_profile）」。
 * 聊天区若原样展示会重复；展示前去掉末尾 JSON 行（可连续多行）；若整段只有 JSON 则用 reply 作为正文。
 */
const stripOneTrailingWorkflowJsonLine = (raw: string): string => {
  if (!raw || typeof raw !== 'string') return ''
  const t = raw.trimEnd()
  const lastNl = t.lastIndexOf('\n')
  const lastLine = (lastNl >= 0 ? t.slice(lastNl + 1) : t).trim()
  if (!lastLine.startsWith('{')) return raw
  try {
    const j = JSON.parse(lastLine) as Record<string, unknown>
    if (!j || typeof j !== 'object') return raw
    const reply = j.reply
    if (typeof reply !== 'string') return raw
    const head = lastNl >= 0 ? t.slice(0, lastNl).trimEnd() : ''
    if (head) return head
    return reply
  } catch {
    return raw
  }
}

const stripTrailingWorkflowJsonLine = (raw: string): string => {
  let cur = raw
  for (let i = 0; i < 6; i++) {
    const next = stripOneTrailingWorkflowJsonLine(cur)
    if (next === cur) break
    cur = next
  }
  return cur
}

const stripDsmlLines = (raw: string): string => {
  if (!raw || typeof raw !== 'string') return ''
  // 过滤协议泄露行：<｜DSML｜...> / <|DSML|...>
  const lines = raw.split(/\r?\n/)
  const filtered = lines.filter((line) => !/[<]\s*[|｜]DSML[|｜]/i.test(line))
  return filtered.join('\n').trim()
}

// 格式化消息（支持简单的Markdown）
const formatMessage = (content: string) => {
  if (!content) return ''
  const noProtocol = stripDsmlLines(content)
  const display = stripTrailingWorkflowJsonLine(noProtocol)
  return display.replace(/\n/g, '<br>')
}

// 格式化时间
const formatTime = (timestamp: number) => {
  const date = new Date(timestamp)
  return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}


// 语音输入
const handleVoiceInput = () => {
  ElMessage.info('语音输入功能开发中')
}

// 换行
const handleNewLine = () => {
  // Shift+Enter 换行，不需要特殊处理
}

// 关闭节点测试结果
const handleCloseNodeTest = () => {
  // 通过事件通知父组件清除测试结果
  // 这里暂时不做处理，由父组件自动清除
}

// 监听消息变化，自动滚动 + 持久化到本机（设计器刷新可恢复）
watch(
  messages,
  () => {
    scrollToBottom()
    schedulePersistDesignChat()
  },
  { deep: true }
)

watch(
  () => props.agentId,
  (id, prev) => {
    if (id && id !== prev) {
      loadChatHistory()
    }
  },
  { immediate: true }
)

// 登录/登出后 user_id 语义变化，需重拉历史与本机缓存键
watch(
  () => userStore.user?.id,
  (id, prev) => {
    if (!props.agentId) return
    if (id !== prev) loadChatHistory()
  }
)

// 从 bfcache 返回时补拉历史（部分浏览器前进/后退不重建组件）
function onPageShow(ev: PageTransitionEvent) {
  if (ev.persisted && props.agentId) {
    loadChatHistory()
  }
}
onMounted(() => window.addEventListener('pageshow', onPageShow as EventListener))

onActivated(() => {
  if (props.agentId) loadChatHistory()
})

// 组件卸载时清理轮询
onUnmounted(() => {
  window.removeEventListener('pageshow', onPageShow as EventListener)
  if (pollingInterval) {
    clearInterval(pollingInterval)
    pollingInterval = null
  }
  revokeAttachmentBlobs([...pendingAttachments.value])
  revokeMessageAttachmentBlobs(messages.value)
  if (blobsPendingRevokeAfterRun.value) {
    revokeAttachmentBlobs(blobsPendingRevokeAfterRun.value)
    blobsPendingRevokeAfterRun.value = null
  }
  // 清除执行状态
  emit('execution-status', null)
})
</script>

<style scoped>
.agent-chat-preview {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: #f5f5f5;
  border-left: 1px solid #e4e7ed;
}

.chat-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px;
  background: white;
  border-bottom: 1px solid #e4e7ed;
}

.agent-info {
  display: flex;
  align-items: center;
  gap: 8px;
}

.agent-name {
  font-weight: 500;
  font-size: 14px;
}

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.message {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}

.user-message {
  flex-direction: row-reverse;
}

.message-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.user-message .message-content {
  align-items: flex-end;
}

.message-bubble {
  padding: 10px 14px;
  border-radius: 12px;
  max-width: 70%;
  word-wrap: break-word;
  line-height: 1.5;
}

.agent-message .message-bubble {
  background: white;
  color: #303133;
  border: 1px solid #e4e7ed;
}

.user-message .message-bubble {
  background: #409eff;
  color: white;
}

.message-bubble.loading {
  display: flex;
  align-items: center;
  gap: 8px;
  color: #909399;
}

.message-time {
  font-size: 12px;
  color: #909399;
  padding: 0 4px;
}

.message-attachments {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  max-width: min(70%, 420px);
  gap: 8px;
  margin-top: 6px;
  padding-bottom: 2px;
}

.message-attachments.is-user {
  justify-content: flex-end;
}

.message-attachments.is-agent {
  justify-content: flex-start;
}

.message-attachment-item {
  max-width: 220px;
  flex: 0 0 auto;
}

.message-attachment-image {
  width: 120px;
  height: 120px;
  object-fit: cover;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  background: #fff;
  display: block;
  cursor: zoom-in;
}

.message-attachment-file {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  font-size: 12px;
  background: #fff;
  color: #606266;
  max-width: 220px;
}

.message-attachment-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.image-preview-dialog-body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 240px;
}

.image-preview-dialog-img {
  max-width: 100%;
  max-height: 72vh;
  object-fit: contain;
  border-radius: 10px;
  border: 1px solid #ebeef5;
  background: #fff;
}

.preset-questions {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
}

.preset-question {
  padding: 10px 14px;
  background: white;
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
  font-size: 14px;
  color: #409eff;
}

.preset-question:hover {
  background: #ecf5ff;
  border-color: #409eff;
}

.node-test-result {
  margin-top: 16px;
  margin-bottom: 16px;
}

.node-test-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.node-test-time {
  font-size: 12px;
  color: #909399;
  font-weight: normal;
}

.node-test-content {
  margin-top: 8px;
}

.test-section {
  margin-bottom: 12px;
}

.test-section strong {
  display: block;
  margin-bottom: 4px;
  font-size: 13px;
  color: #606266;
}

.test-section pre {
  background: #f5f7fa;
  padding: 8px;
  border-radius: 4px;
  font-size: 12px;
  max-height: 200px;
  overflow: auto;
  margin: 0;
  border: 1px solid #e4e7ed;
}

.chat-input-area {
  background: white;
  border-top: 1px solid #e4e7ed;
  padding: 12px;
  border-radius: 0;
  transition: background-color 0.15s ease, box-shadow 0.15s ease;
}

.chat-input-area.is-drag-over {
  background: #ecf5ff;
  box-shadow: inset 0 0 0 2px #409eff;
}

.input-toolbar {
  margin-bottom: 8px;
}

.input-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 8px;
}

.disclaimer {
  font-size: 12px;
  color: #909399;
}

.input-actions {
  display: flex;
  gap: 8px;
  align-items: center;
}

:deep(.el-textarea__inner) {
  border: 1px solid #dcdfe6;
  border-radius: 8px;
  resize: none;
}

.hidden-file-input {
  display: none;
}

.attachment-thumb-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
  min-height: 0;
}

.attachment-thumb-item {
  position: relative;
  width: 76px;
  flex-shrink: 0;
  text-align: center;
}

.attachment-thumb-remove {
  position: absolute;
  top: -6px;
  right: -6px;
  z-index: 2;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}

.attachment-thumb-remove:hover {
  background: #f56c6c;
}

.attachment-thumb-img-wrap {
  width: 72px;
  height: 72px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid #e4e7ed;
  background: #fafafa;
  margin: 0 auto;
}

.attachment-thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.attachment-thumb-file-placeholder {
  width: 72px;
  height: 72px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  border: 1px solid #e4e7ed;
  background: linear-gradient(145deg, #f5f7fa, #ebeef5);
  color: #409eff;
}

.attachment-thumb-caption {
  margin-top: 4px;
  font-size: 11px;
  color: #606266;
  line-height: 1.2;
  max-width: 76px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.attachment-preview-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 10px;
  max-height: 220px;
  overflow-y: auto;
}

.attachment-preview-card {
  border: 1px solid #e4e7ed;
  border-radius: 8px;
  background: #fafafa;
  padding: 8px 10px;
}

.attachment-preview-title {
  font-size: 12px;
  color: #606266;
  margin-bottom: 6px;
  font-weight: 600;
}

.attachment-preview-body {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
  color: #303133;
  max-height: 160px;
  overflow-y: auto;
}

.attachment-preview-note {
  font-size: 12px;
  color: #909399;
  line-height: 1.45;
}
</style>
