<script setup lang="ts">
import { computed, defineAsyncComponent, onBeforeUnmount, ref, watch } from 'vue'
import { ElImageViewer } from 'element-plus'
import { CircleCheck, Document, MagicStick } from '@element-plus/icons-vue'
import { Bot, Brain, Images, Wrench } from '@lucide/vue'
import type { AgentEvent } from '../api'

const MarkdownRender = defineAsyncComponent(() => import('markstream-vue'))

const props = defineProps<{ events: AgentEvent[]; running: boolean; projectId: string }>()
const visibleLimit = ref(180)
const previewIndex = ref<number | null>(null)
const showFinalizing = ref(false)
const timeFormatter = new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' })
let finalizingTimer: number | undefined

type ActivityStatus = 'running' | 'done' | 'failed' | 'stopped'
type ViewImage = { index: number; path: string; sourcePath: string; label: string }
type FlowItem =
  | { key: string; runId: string | null; kind: 'message'; text: string; time: string; showIdentity: boolean; final: boolean }
  | { key: string; runId: string | null; kind: 'reasoning'; text: string; status: ActivityStatus; time: string }
  | { key: string; runId: string | null; kind: 'tool'; name: string; args: string; detail: string; images: ViewImage[]; status: ActivityStatus; time: string }
  | { key: string; kind: 'notice'; text: string; tone: 'success' | 'error' | 'info'; time: string }

const flow = computed<FlowItem[]>(() => {
  const items: FlowItem[] = []
  const messages = new Map<string, FlowItem & { kind: 'message' }>()
  const reasoning = new Map<string, FlowItem & { kind: 'reasoning' }>()
  const tools = new Map<string, FlowItem & { kind: 'tool' }>()
  const asks = new Map<string | null, Record<string, any>>()
  const terminalRuns = new Map<string | null, Exclude<ActivityStatus, 'running'>>()

  for (const event of props.events) {
    const payload = event.payload || {}
    const eventKey = (id: unknown) => `${event.runId || 'none'}:${String(id)}`
    if (event.type === 'TEXT_MESSAGE_START') {
      const id = eventKey(payload.messageId || event.id)
      const item: FlowItem & { kind: 'message' } = {
        key: `m-${id}`, runId: event.runId, kind: 'message', text: '', time: formatTime(event.createdAt),
        showIdentity: false, final: false
      }
      messages.set(id, item)
      items.push(item)
    } else if (event.type === 'TEXT_MESSAGE_CONTENT' || event.type === 'TEXT_MESSAGE_CHUNK') {
      const id = eventKey(payload.messageId || 'current')
      let item = messages.get(id)
      if (!item) {
        item = {
          key: `m-${id}`, runId: event.runId, kind: 'message', text: '',
          time: formatTime(event.createdAt), showIdentity: false, final: false
        }
        messages.set(id, item)
        items.push(item)
      }
      item.text += String(payload.delta || payload.content || '')
    } else if (event.type === 'TEXT_MESSAGE_END') {
      const item = messages.get(eventKey(payload.messageId || 'current'))
      if (item) item.final = true
    } else if (event.type === 'REASONING_MESSAGE_START' || event.type === 'REASONING_START') {
      const id = eventKey(payload.messageId || payload.reasoningId || event.id)
      const item: FlowItem & { kind: 'reasoning' } = {
        key: `r-${id}`, runId: event.runId, kind: 'reasoning', text: '', status: 'running', time: formatTime(event.createdAt)
      }
      reasoning.set(id, item)
      items.push(item)
    } else if (event.type === 'REASONING_MESSAGE_CONTENT' || event.type === 'REASONING_MESSAGE_CHUNK') {
      const id = eventKey(payload.messageId || payload.reasoningId || event.id)
      let item = reasoning.get(id)
      if (!item) {
        item = {
          key: `r-${id}`, runId: event.runId, kind: 'reasoning', text: '', status: 'running', time: formatTime(event.createdAt)
        }
        reasoning.set(id, item)
        items.push(item)
      }
      item.text += String(payload.delta || payload.content || '')
    } else if (event.type === 'REASONING_MESSAGE_END' || event.type === 'REASONING_END') {
      const item = reasoning.get(eventKey(payload.messageId || payload.reasoningId || event.id))
      if (item) item.status = 'done'
    } else if (event.type === 'TOOL_CALL_START') {
      const id = eventKey(payload.toolCallId || event.id)
      const item: FlowItem & { kind: 'tool' } = {
        key: `t-${id}`, runId: event.runId, kind: 'tool',
        name: String(payload.toolCallName || payload.name || '工具'), args: '', detail: '', images: [],
        status: 'running', time: formatTime(event.createdAt)
      }
      tools.set(id, item)
      items.push(item)
    } else if (event.type === 'TOOL_CALL_ARGS' || event.type === 'TOOL_CALL_CHUNK') {
      const item = tools.get(eventKey(payload.toolCallId || payload.id || ''))
      if (item) item.args += String(payload.delta || payload.args || '')
    } else if (event.type === 'TOOL_CALL_RESULT' || event.type === 'TOOL_CALL_END') {
      const item = tools.get(eventKey(payload.toolCallId || payload.id || ''))
      if (item) {
        const result = String(payload.content || payload.result || '')
        item.status = /(?:执行失败|(?:^|\n)"?(?:error:|failed\b|exit code:\s*[1-9]))/i.test(result.trim()) ? 'failed' : 'done'
        if (result) {
          item.detail = summarize(result, 360)
          if (item.name === 'document_view') item.images = parseViewImages(result)
        }
      }
    } else if (event.type === 'ASK_REQUESTED') {
      asks.set(event.runId, payload)
    } else if (event.type === 'ASK_RESPONDED') {
      const ask = asks.get(event.runId)
      items.push({
        key: `ask-${event.id}`, kind: 'notice', text: askSummary(ask, payload),
        tone: 'success', time: formatTime(event.createdAt)
      })
    } else if (event.type === 'RUN_ERROR') {
      terminalRuns.set(event.runId, 'failed')
      closeActive(event.runId, 'failed', reasoning, tools)
      items.push({
        key: `e-${event.id}`, kind: 'notice', text: '执行遇到问题，已保留现有进度',
        tone: 'error', time: formatTime(event.createdAt)
      })
    } else if (event.type === 'MODEL_RETRY') {
      items.push({
        key: `retry-${event.id}`, kind: 'notice',
        text: `模型连接中断，正在重连（${payload.attempt}/${payload.maxAttempts || 5}）`,
        tone: 'info', time: formatTime(event.createdAt)
      })
    } else if (event.type === 'RUN_FINISHED' && payload.outcome === 'CANCELLED') {
      terminalRuns.set(event.runId, 'stopped')
      closeActive(event.runId, 'stopped', reasoning, tools)
      items.push({
        key: `cancel-${event.id}`, kind: 'notice', text: 'Agent 已停止，当前上下文已保留',
        tone: 'info', time: formatTime(event.createdAt)
      })
    } else if (event.type === 'ARTIFACT_PUBLISHED') {
      items.push({
        key: `a-${event.id}`, kind: 'notice', text: '申报书审阅稿已生成',
        tone: 'success', time: formatTime(event.createdAt)
      })
    } else if (event.type === 'RUN_FINISHED') {
      terminalRuns.set(event.runId, 'done')
      closeActive(event.runId, 'done', reasoning, tools)
    }
  }
  for (const [runId, status] of terminalRuns) closeActive(runId, status, reasoning, tools)
  for (const item of messages.values()) if (terminalRuns.has(item.runId)) item.final = true
  const filtered = items.filter(item => (item.kind !== 'message' && item.kind !== 'reasoning') || cleanText(item.text))
  const identifiedRuns = new Set<string | null>()
  for (const item of filtered) {
    if (item.kind === 'message' && !identifiedRuns.has(item.runId)) {
      item.showIdentity = true
      identifiedRuns.add(item.runId)
    }
  }
  return filtered
})

const visibleFlow = computed(() => flow.value.slice(-visibleLimit.value))
const hiddenCount = computed(() => Math.max(0, flow.value.length - visibleFlow.value.length))
const galleryImages = computed(() => flow.value.flatMap(item => item.kind === 'tool'
  ? item.images.map(image => ({ key: `${item.key}:${image.path}`, image, url: previewUrl(image.path) }))
  : []))
const galleryUrls = computed(() => galleryImages.value.map(item => item.url))

watch(
  () => [props.running, props.events.at(-1)?.id, props.events.at(-1)?.type] as const,
  ([running, , type]) => {
    window.clearTimeout(finalizingTimer)
    showFinalizing.value = false
    if (running && type === 'TEXT_MESSAGE_END') {
      finalizingTimer = window.setTimeout(() => { showFinalizing.value = true }, 500)
    }
  },
  { immediate: true }
)

onBeforeUnmount(() => window.clearTimeout(finalizingTimer))

function closeActive(
  runId: string | null,
  status: Exclude<ActivityStatus, 'running'>,
  reasoning: Map<string, FlowItem & { kind: 'reasoning' }>,
  tools: Map<string, FlowItem & { kind: 'tool' }>
) {
  for (const item of reasoning.values()) if (item.runId === runId && item.status === 'running') item.status = status
  for (const item of tools.values()) if (item.runId === runId && item.status === 'running') item.status = status
}

function formatTime(value: string) {
  return timeFormatter.format(new Date(value))
}

function askSummary(ask: Record<string, any> | undefined, response: Record<string, any>) {
  if (ask?.kind !== 'material_check') return '已确认建设规划'
  const decisions = Array.isArray(response.decisions) ? response.decisions : []
  const assumptions = decisions.filter((item: any) => item?.action === 'ASSUMPTION').length
  const pending = decisions.filter((item: any) => item?.action === 'PENDING_COMMENT').length
  const uploaded = decisions.filter((item: any) => item?.action === 'UPLOADED').length
  const parts = ['已确认材料检验']
  if (assumptions) parts.push(`${assumptions} 项按规划假设继续`)
  if (pending) parts.push(`${pending} 项待确认`)
  if (uploaded) parts.push(`${uploaded} 项已补充`)
  return parts.join(' · ')
}

function summarize(value: unknown, maxLength = 160) {
  const text = cleanText(typeof value === 'string' ? value : JSON.stringify(value))
  return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text
}

function cleanText(value: string) {
  return redactText(value)
    .replace(/^\s{0,3}#{1,6}\s+/gm, '')
    .replace(/\*\*|__|`{1,3}/g, '')
    .replace(/^\s*\|?\s*:?-{3,}.*$/gm, '')
    .trim()
}

function redactText(value: string) {
  return value
    .replace(/\/Users\/[^\s"')]+/g, '工作区路径')
    .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi, '内部任务')
    .replace(/sk-[A-Za-z0-9_-]{8,}/g, '已隐藏凭证')
}

function parseArgs(item: FlowItem & { kind: 'tool' }) {
  try { return JSON.parse(item.args) as Record<string, any> } catch { return {} }
}

function skillName(item: FlowItem & { kind: 'tool' }) {
  return String(parseArgs(item).skillId || '').replace(/_(?:builtin|imported)$/, '') || 'Skill'
}

function baseToolName(item: FlowItem & { kind: 'tool' }) {
  const labels: Record<string, string> = {
    list_files: '查看文件', read_file: '读取文件', write_file: '写入文件', edit_file: '修改文件',
    glob_files: '查找文件', grep_files: '检索文件', execute: '工作区',
    memory_search: '项目记忆检索', memory_save: '项目记忆保存'
  }
  return labels[item.name] || item.name
}

function activityLabel(item: FlowItem & { kind: 'tool' }) {
  if (item.name === 'document_view') {
    const count = item.images.length || requestedViewCount(item)
    if (item.status === 'running') return '正在查看图片'
    if (item.status === 'stopped') return '已停止查看图片'
    if (item.status === 'failed') return '查看图片失败'
    return `已查看图片${count ? ` · ${count} 张` : ''}`
  }
  if (item.name === 'read_file') return fileActivityLabel(item, '读取')
  if (item.name === 'write_file' || item.name === 'edit_file') return fileActivityLabel(item, '编辑')
  const skill = isSkill(item)
  const name = skill ? skillName(item) : baseToolName(item)
  if (item.status === 'running') return skill ? `正在调用 ${name} Skill` : `正在调用${name}工具`
  if (item.status === 'stopped') return skill ? `已停止调用 ${name} Skill` : `已停止调用${name}工具`
  if (item.status === 'failed') return skill ? `${name} Skill 调用失败` : `${name}工具调用失败`
  return skill ? `已调用 ${name} Skill` : `已调用${name}工具`
}

function fileActivityLabel(item: FlowItem & { kind: 'tool' }, action: '读取' | '编辑') {
  const path = String(parseArgs(item).path || '').replace(/\\/g, '/')
  const name = path.split('/').filter(Boolean).pop()
  const target = name ? `${name} 文件` : '文件'
  if (item.status === 'running') return `正在${action} ${target}`
  if (item.status === 'stopped') return `已停止${action} ${target}`
  if (item.status === 'failed') return `${action} ${target}失败`
  return `已${action} ${target}`
}

function isSkill(item: FlowItem & { kind: 'tool' }) {
  return item.name === 'load_skill_through_path' || item.name.toLowerCase().includes('skill')
}

function requestedViewCount(item: FlowItem & { kind: 'tool' }) {
  const views = parseArgs(item).views
  return Array.isArray(views) ? views.length : 0
}

function parseViewImages(content: string): ViewImage[] {
  const marker = 'document_view_result='
  const start = content.indexOf(marker)
  if (start < 0) return []
  try {
    const line = content.slice(start + marker.length).split('\n', 1)[0]
    const images = JSON.parse(line).images
    return Array.isArray(images) ? images : []
  } catch { return [] }
}

function previewUrl(path: string) {
  return `/api/projects/${props.projectId}/view-images?path=${encodeURIComponent(path)}`
}

function openPreview(itemKey: string, path: string) {
  const index = galleryImages.value.findIndex(item => item.key === `${itemKey}:${path}`)
  if (index >= 0) previewIndex.value = index
}

function reasoningLabel(item: FlowItem & { kind: 'reasoning' }) {
  if (item.status === 'running') return '正在思考'
  if (item.status === 'stopped') return '已停止思考'
  if (item.status === 'failed') return '思考中断'
  return '已思考'
}
</script>

<template>
  <div class="timeline">
    <span v-if="running" class="sr-only" aria-live="polite">Agent 正在执行</span>
    <button v-if="hiddenCount" class="load-earlier" @click="visibleLimit += 180">查看更早记录（{{ hiddenCount }}）</button>
    <article v-for="item in visibleFlow" :key="item.key" class="flow-row" :class="`flow-${item.kind}`">
      <div v-if="item.kind === 'message' && item.showIdentity" class="agent-avatar" aria-label="Agent"><Bot /></div>
      <div class="flow-content">
        <div v-if="item.kind === 'message' && item.showIdentity" class="flow-meta"><strong>Agent</strong><time>{{ item.time }}</time></div>
        <MarkdownRender
          v-if="item.kind === 'message'"
          class="agent-markdown"
          mode="chat"
          :content="redactText(item.text).trim()"
          :final="item.final"
          smooth-streaming="auto"
          :fade="false"
          html-policy="escape"
        />
        <details v-else-if="item.kind === 'reasoning'" class="activity-row reasoning-row" :class="{ active: item.status === 'running', failed: item.status === 'failed' }">
          <summary><Brain /><span>{{ reasoningLabel(item) }}</span></summary>
          <p>{{ cleanText(item.text) }}</p>
        </details>
        <details
          v-else-if="item.kind === 'tool'"
          class="activity-row tool-row"
          :class="{ active: item.status === 'running', failed: item.status === 'failed', visual: item.name === 'document_view' }"
        >
          <summary>
            <Images v-if="item.name === 'document_view'" />
            <MagicStick v-else-if="isSkill(item)" />
            <Wrench v-else />
            <span>{{ activityLabel(item) }}</span>
          </summary>
          <div v-if="item.images.length" class="view-image-strip">
            <button
              v-for="image in item.images"
              :key="image.path"
              type="button"
              class="view-image-item"
              :aria-label="`查看渲染图片 ${image.index}`"
              @click="openPreview(item.key, image.path)"
            >
              <img :src="previewUrl(image.path)" :alt="`渲染图片 ${image.index}`" loading="lazy" />
              <span>渲染图片 {{ image.index }} · {{ image.label }}</span>
            </button>
          </div>
          <p v-else-if="item.detail">{{ item.detail }}</p>
        </details>
        <div v-else class="notice-row" :class="item.tone">
          <CircleCheck v-if="item.tone === 'success'" /><Document v-else />
          <span>{{ item.text }}</span>
        </div>
      </div>
    </article>
    <div v-if="showFinalizing" class="finalizing-row" aria-live="polite">
      <span>正在整理结果</span>
    </div>
    <ElImageViewer
      v-if="previewIndex !== null && galleryUrls.length"
      :url-list="galleryUrls"
      :initial-index="previewIndex"
      :infinite="true"
      :show-progress="true"
      :close-on-press-escape="true"
      :teleported="true"
      @close="previewIndex = null"
    />
  </div>
</template>
