<template>
  <div class="task-detail">
    <!-- 页面头部(吸顶):标题 + 状态 + 优先级 在左,推进按钮在右(滚动始终可见)。
         Problem4 ① 推进按钮从面板内移到 header 右侧,与 refresh 同区。-->
    <header class="page-header task-detail-header">
      <div class="header-left">
        <router-link to="/tasks" class="back-link">{{ $t('taskDetail.backToList') }}</router-link>
        <h1>{{ task?.title ?? '...' }}</h1>
        <span v-if="task" class="status-tag" :class="taskStatusClass(task.status)">{{ $t(taskStatusLabel(task.status)) }}</span>
        <!-- F-04 review 轮次显示(>0 才显示,in_review→in_progress / testing→in_review 退回时后端 +1) -->
        <span v-if="task && task.review_rounds > 0" class="review-rounds-badge">
          {{ $t('taskDetail.reviewRounds', { n: task.review_rounds }) }}
        </span>
        <span v-if="task" class="priority-badge" :class="priorityClass(task.priority)">{{ priorityLabel(task.priority) }}</span>
      </div>
      <div class="header-actions">
        <button class="btn btn-ghost btn-sm" type="button" @click="refresh">{{ $t('taskDetail.refresh') }}</button>
        <!-- F-260616-06 ①-1 / B-41 工作流推进按钮(联动任务,按 target_status 后端自动选推进链模板)
             与手动 advance 并存:仅当当前态的 primary 前向推进目标 ∈ {in_progress, testing, done}
             (即 template_for 有对应模板)时显示。手动 advance 仍走 advance_task 直 IPC;工作流推进
             走 run_workflow,经 AiNode/HumanNode 执行后再由后端回调推进任务。
             独立 loading 标志 wfAdvancing 与 advancing 互不干扰。 -->
        <button
          v-if="wfAdvanceAction"
          type="button"
          class="btn btn-sm btn-primary"
          :disabled="wfAdvancing || advancing"
          @click="handleWorkflowAdvance(wfAdvanceAction.target)"
        >{{ wfAdvancing ? $t('taskDetail.workflowAdvancing') : $t('taskDetail.workflowAdvance') }}</button>
        <!-- F-05 推进按钮:按当前 status 显示状态机合法下一态(todo/done/cancelled 终态无按钮) -->
        <button
          v-for="act in advanceActions"
          :key="act.target"
          type="button"
          class="btn btn-sm"
          :class="act.variant"
          :disabled="advancing"
          @click="handleAdvance(act.target)"
        >{{ advancing ? $t('taskDetail.advancing') : $t(act.label) }}</button>
      </div>
    </header>

    <!-- 加载/错误态 -->
    <div v-if="loading" class="empty-hint">{{ $t('taskDetail.loading') }}</div>
    <div v-else-if="errorMsg" class="empty-hint error-hint">⚠ {{ errorMsg }}</div>

    <!-- 主体:两栏布局(Problem4 ⑤)。左栏=描述+关联信息+时间戳,右栏=任务产出+工作流。
         窄屏自动单列。-->
    <div v-else-if="task" class="detail-grid">
      <!-- ============ 左栏 ============ -->
      <div class="left-column">
        <!-- 描述(可折叠:Problem4 ② 超 400px 显展开按钮) -->
        <section v-if="task.description" class="panel">
          <div class="panel-header"><h2>{{ $t('taskDetail.description') }}</h2></div>
          <div class="description-wrap" :style="descWrapStyle">
            <!-- B-24:任务描述 Markdown 渲染(## 标题 / - 列表 等),v-html 经 useMarkdown DOMPurify sanitize -->
            <span ref="descEl" class="value description ai-md" v-html="renderedDesc"></span>
          </div>
          <button
            v-if="descCollapsible"
            class="btn btn-ghost btn-sm desc-toggle"
            type="button"
            @click="descExpanded = !descExpanded"
          >{{ descExpanded ? $t('taskDetail.collapse') : $t('taskDetail.expand') }}</button>
          <!-- 折叠态淡出渐变蒙层(展开态不显) -->
          <div v-if="descCollapsible && !descExpanded" class="desc-fade"></div>
        </section>

        <!-- 关联信息(Problem4 ③④ 去重 + 空值不显行)。
             标题/状态/优先级已在 header 显示,这里不重复。-->
        <section class="panel">
          <div class="panel-header"><h2>{{ $t('taskDetail.relatedTitle') }}</h2></div>
          <div class="task-info">
            <div class="info-item">
              <span class="label">{{ $t('taskDetail.project') }}</span>
              <span class="value">
                <router-link v-if="task.project_id" :to="`/projects/${task.project_id}`" class="project-link">
                  {{ projectName }}
                </router-link>
                <span v-else>—</span>
              </span>
            </div>
            <!-- F-260619-01:关联灵感(1对1 单向,idea_id 解析为友好 title,非裸 id) -->
            <div v-if="task.idea_id" class="info-item">
              <span class="label">{{ $t('taskDetail.relatedIdea') }}</span>
              <span class="value">
                <router-link :to="`/ideas/${task.idea_id}`" class="project-link">{{ ideaTitle }}</router-link>
              </span>
            </div>
            <!-- 分支(有值才显) -->
            <div v-if="task.branch_name" class="info-item">
              <span class="label">{{ $t('taskDetail.branch') }}</span>
              <span class="value">
                <span class="branch-tag"><span class="branch-icon">⑂</span>{{ task.branch_name }}</span>
              </span>
            </div>
            <!-- 负责人(空值不显行,Problem4 ③) -->
            <div v-if="task.assignee" class="info-item">
              <span class="label">{{ $t('taskDetail.assignee') }}</span>
              <span class="value">{{ task.assignee }}</span>
            </div>
            <!-- 基础分支(空值不显行) -->
            <div v-if="task.base_branch" class="info-item">
              <span class="label">{{ $t('taskDetail.baseBranch') }}</span>
              <span class="value mono">{{ task.base_branch }}</span>
            </div>
            <!-- 工作流定义(空值不显行) -->
            <div v-if="task.workflow_def_id" class="info-item">
              <span class="label">{{ $t('taskDetail.workflowDef') }}</span>
              <span class="value mono">{{ task.workflow_def_id }}</span>
            </div>
            <!-- 创建/更新时间:最底小字(Problem4 ⑥) -->
            <div class="info-item timestamps">
              <span class="timestamp">{{ $t('taskDetail.createdAt') }}: {{ formatDate(task.created_at) }}</span>
              <span class="timestamp">{{ $t('taskDetail.updatedAt') }}: {{ formatDate(task.updated_at) }}</span>
            </div>
          </div>
        </section>
      </div>

      <!-- ============ 右栏:任务产出 + 工作流进度 ============ -->
      <div class="right-column">
        <!-- F-AiNodeSelfReview: 任务产出 + AI 自审结果展示(抽至 TaskOutputCard 子组件,零行为变更) -->
        <section class="panel">
          <div class="panel-header"><h2>{{ $t('taskDetail.output') }}</h2></div>
          <!-- 无产出时占位(任务未跑过 AiNode 产出),右栏保留产出栏框架便于后续产出出现即填入 -->
          <div v-if="!task.output_json" class="empty-hint output-empty">—</div>
          <TaskOutputCard v-else :output-json="task.output_json" />
        </section>

        <!-- B-41 工作流推进轻量进度(从面板内 advance-row 提取,推进按钮已移 header) -->
        <section v-if="wfAdvancing || wfProgressHint" class="panel">
          <div class="panel-header"><h2>{{ $t('taskDetail.workflowAdvanceTitle') }}</h2></div>
          <div class="wf-progress">
            <span v-if="wfRunningNode">{{ $t('taskDetail.workflowStepRunning', { node: wfRunningNode }) }}</span>
            <span v-else-if="wfDoneTotal > 0" class="wf-progress-count">
              {{ $t('taskDetail.workflowStepsProgress', { done: wfDoneCount, total: wfDoneTotal }) }}
            </span>
            <span v-if="wfCompletedHint" class="wf-progress-hint">{{ $t('taskDetail.workflowCompletedHint') }}</span>
            <span v-if="wfFailedHint" class="wf-progress-hint wf-progress-hint-fail">{{ $t('taskDetail.workflowFailedHint') }}</span>
          </div>
          <!-- 工作流 DAG 结构 -->
          <WorkflowDagDisplay v-if="wfDagJson" :dag-json="wfDagJson" :node-statuses="wfNodeStatuses" />
        </section>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { listen } from '@tauri-apps/api/event'
import { taskApi, projectApi, ideaApi } from '@/api'
import { workflowApi } from '@/api'
import { formatDate } from '@/utils/time'
import { useRendered } from '@/composables/useMarkdown'
import {
  taskStatusLabel,
  taskStatusClass,
  priorityLabel,
  priorityClass,
} from '../constants/project'
import type { TaskRecord, ProjectRecord, IdeaRecord, DfDataChangedPayload, WorkflowEventPayload } from '@/api/types'
import TaskOutputCard from '@/components/task/TaskOutputCard.vue'
import WorkflowDagDisplay from '@/components/workflow/WorkflowDagDisplay.vue'
import type { NodeStatus } from '@/components/workflow/WorkflowDagDisplay.vue'

const { t } = useI18n()
const route = useRoute()

const loading = ref(true)
const errorMsg = ref('')
const task = ref<TaskRecord | null>(null)
const projects = ref<ProjectRecord[]>([])
// F-260619-01:灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式)
const ideas = ref<IdeaRecord[]>([])
const advancing = ref(false)

// ============================================================
// F-260616-06 ①-1 / B-41 工作流推进状态(与手动 advance 的 advancing 独立,互不干扰)
// ------------------------------------------------------------
// wfAdvancing:推进中态;wfExecId:当前监听的工作流执行 ID(过滤 workflow-event);
// wfTotalNodes/wfDoneCount/wfRunningNode:轻量进度(听 NodeStarted/NodeCompleted);
// wfResult: 'completed' | 'failed' | null —— 终态提示,完成/失败后自动清空(由新一次推进重置)。
const wfAdvancing = ref(false)
const wfExecId = ref<string | null>(null)
const wfTotalNodes = ref(0)
const wfDoneCount = ref(0)
const wfRunningNode = ref<string>('')
const wfResult = ref<'completed' | 'failed' | null>(null)
// SW-260618-21: 终态提示 timer 引用,卸载时清理防写已销毁 ref(对齐 Projects.vue _toastTimer 模式)
let _wfResultTimer: ReturnType<typeof setTimeout> | null = null

const wfProgressHint = computed(() => wfResult.value !== null)
const wfCompletedHint = computed(() => wfResult.value === 'completed')
const wfFailedHint = computed(() => wfResult.value === 'failed')

// 工作流 DAG 结构展示
const wfDagJson = ref('')
const wfNodeStatuses = ref<Record<string, NodeStatus>>({})
async function refreshWorkflowDag(execId: string) {
  wfNodeStatuses.value = {}
  try {
    const record = await workflowApi.getExecution(execId)
    if (record?.dag_json) wfDagJson.value = record.dag_json
  } catch { /* 静默 */ }
}
const wfDoneTotal = computed(() => wfTotalNodes.value)

const taskId = computed(() => route.params.id as string)

// B-24:任务描述 Markdown 渲染(复用 AiChat 同款渲染器,模块级单例),useRendered 封装
// computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
const { rendered: renderedDesc, ensureLoaded } = useRendered(
  () => task.value?.description ?? '',
)

// ============================================================
// Problem4 ②:描述可折叠 — 超 400px 显展开按钮(测真实 DOM 高度)
// ------------------------------------------------------------
// 不用字符数近似(渲染后 Markdown 列表/标题行高不一,字符数与 px 无线性关系),
// 改用 descEl 实测 scrollHeight:首次渲染 + task 切换后 nextTick 测量。
// descCollapsible=true 时才显按钮;默认折叠(!descExpanded)即首次进入截 400px。
// 折叠态 max-height 由 descWrapStyle 内联注入(展开态不设上限,自适应)。
// .description-wrap 的 overflow:hidden + transition 让折叠/展开有过渡。
// 蒙层 .desc-fade 暗示折叠态下方还有内容。
const DESC_COLLAPSE_PX = 400
const descEl = ref<HTMLElement | null>(null)
const descExpanded = ref(false)
const descOverflow = ref(false) // 真实高度是否超阈值(控制按钮显隐)

const descCollapsible = computed(() => descOverflow.value)

// 折叠态 max-height 内联(过渡友好):展开态不设上限(自适应)
const descWrapStyle = computed(() =>
  descExpanded.value ? {} : { maxHeight: DESC_COLLAPSE_PX + 'px' },
)

async function measureDescHeight() {
  // descEl 是 <span>(描述内容),自身无 max-height 限制 → scrollHeight 即其真实渲染高度,
  // 不受父 .description-wrap 的 max-height 裁剪影响(span 在父内若被裁,其 scrollHeight
  // 仍反映完整内容高)。直接读无需临时改父样式。
  await nextTick()
  const el = descEl.value
  if (!el) { descOverflow.value = false; return }
  descOverflow.value = el.scrollHeight > DESC_COLLAPSE_PX
}

// F-AiNodeSelfReview: output_json 解析 + AI 产出/自审渲染已抽至 TaskOutputCard 子组件
// (components/task/TaskOutputCard.vue),父级只传 output-json prop,零行为变更。

const projectName = computed(() => {
  const p = projects.value.find(p => p.id === task.value?.project_id)
  return p?.name ?? task.value?.project_id ?? '—'
})

// F-260619-01:解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击)
const ideaTitle = computed(() => {
  const ideaId = task.value?.idea_id
  if (!ideaId) return '—'
  const idea = ideas.value.find(i => i.id === ideaId)
  return idea?.title ?? ideaId
})

// ============================================================
// F-05 推进按钮 — 状态机合法下一态映射
// ------------------------------------------------------------
// 真相源:后端 df-nodes task_state_machine.rs can_transition(详见该文件注释矩阵)。
// 前端只做「按当前 status 显示哪些合法目标」的渲染决策,不做合法性判定;
// 点击调 advance_task,后端二次校验(can_transition)+ 原子写 + review_rounds 累加。
//
// 矩阵(行=from):
//   todo        → in_progress(开始), cancelled(取消)
//   in_progress → in_review(提交审查), blocked(阻塞), cancelled
//   in_review   → testing(通过进测试), in_progress(退回修改), blocked, cancelled
//   testing     → done(完成), in_review(退回重审), blocked, cancelled
//   blocked     → in_progress(恢复), cancelled
//   done/cancelled → 终态,无按钮
// variant: primary=前向推进主操作(每态至多一个),danger=取消,ghost=其余(阻塞/退回/恢复)
interface AdvanceAction {
  target: string
  label: string   // i18n key(走 taskDetail.advance.*)
  variant: string // btn-primary | btn-danger | btn-ghost
}
const ADVANCE_MAP: Record<string, AdvanceAction[]> = {
  todo: [
    { target: 'in_progress', label: 'taskDetail.advance.toInProgress', variant: 'btn-primary' },
    { target: 'cancelled', label: 'taskDetail.advance.cancel', variant: 'btn-danger' },
  ],
  in_progress: [
    { target: 'in_review', label: 'taskDetail.advance.toInReview', variant: 'btn-primary' },
    { target: 'blocked', label: 'taskDetail.advance.block', variant: 'btn-ghost' },
    { target: 'cancelled', label: 'taskDetail.advance.cancel', variant: 'btn-danger' },
  ],
  in_review: [
    { target: 'testing', label: 'taskDetail.advance.toTesting', variant: 'btn-primary' },
    { target: 'in_progress', label: 'taskDetail.advance.sendBack', variant: 'btn-ghost' },
    { target: 'blocked', label: 'taskDetail.advance.block', variant: 'btn-ghost' },
    { target: 'cancelled', label: 'taskDetail.advance.cancel', variant: 'btn-danger' },
  ],
  testing: [
    { target: 'done', label: 'taskDetail.advance.toDone', variant: 'btn-primary' },
    { target: 'in_review', label: 'taskDetail.advance.sendBackToReview', variant: 'btn-ghost' },
    { target: 'blocked', label: 'taskDetail.advance.block', variant: 'btn-ghost' },
    { target: 'cancelled', label: 'taskDetail.advance.cancel', variant: 'btn-danger' },
  ],
  blocked: [
    { target: 'in_progress', label: 'taskDetail.advance.resume', variant: 'btn-primary' },
    { target: 'cancelled', label: 'taskDetail.advance.cancel', variant: 'btn-danger' },
  ],
  // done / cancelled:终态,无推进按钮
}

const advanceActions = computed<AdvanceAction[]>(() => {
  const s = task.value?.status
  if (!s) return []
  return ADVANCE_MAP[s] ?? []
})

// F-260616-06 ①-1: 工作流推进按钮的目标态。
// 仅当当前态的 primary(前向推进)目标 ∈ template_for 支持的三态 {in_progress, testing, done} 时显示。
// 非前向(退回/阻塞/取消)不走工作流(无对应模板,且语义上工作流只做前向推进)。
// ADVANCE_MAP 各行的首个 primary 项:todo→in_progress, in_review→testing, testing→done, blocked→in_progress
// in_progress 的 primary 是 in_review(无模板,不显示工作流推进按钮)。
const WF_SUPPORTED_TARGETS = new Set(['in_progress', 'testing', 'done'])
// CR-08-O1: blocked 是阻塞态(任务被阻塞),其 primary=in_progress 虽 ∈ WF_SUPPORTED_TARGETS,
// 但 template_for('in_progress') 是「todo→in_progress 从头执行」拓扑(AiNode 等重新跑一遍),
// blocked 恢复语义不该重跑 AiNode(任务已有上下文/进度,只需恢复非从头执行)。
// 最小修法:blocked 不显示工作流推进按钮,用户手动 advance(blocked→in_progress 走后端状态机)或先解除阻塞。
// (引入 blocked 恢复模板需后端 template_for 扩展,复杂度高,非阻断 med 项留待 UX 决策。)
const WF_EXCLUDED_FROM = new Set(['blocked'])
const wfAdvanceAction = computed<AdvanceAction | null>(() => {
  const s = task.value?.status
  if (!s) return null
  if (WF_EXCLUDED_FROM.has(s)) return null
  const actions = ADVANCE_MAP[s] ?? []
  const primary = actions.find(a => a.variant === 'btn-primary')
  if (!primary) return null
  return WF_SUPPORTED_TARGETS.has(primary.target) ? primary : null
})

async function handleAdvance(target: string) {
  if (!task.value || advancing.value) return
  advancing.value = true
  try {
    // advance_task 返回更新后的 TaskRecord(含新 status / review_rounds)
    const updated = await taskApi.advance(task.value.id, target)
    task.value = updated
    errorMsg.value = ''
  } catch (e: any) {
    errorMsg.value = t('taskDetail.advanceFailed', { msg: e?.toString() ?? t('common.unknownError') })
  } finally {
    advancing.value = false
  }
}

// F-260616-06 ①-1: 工作流推进 — 传空 dag + target_status,后端自动选推进链模板。
// B-41: 监听 workflow-event 显示轻量进度;完成/失败后任务态由后端回调推进(②-3/②-4),
//       df-data-changed 自动刷新 task(AR-11 已通),此处不直接改 task.value。
// name 用推进链标识(模板内未硬编码 task_id,运行时由后端 config 注入,前端只传 topology hint)。
async function handleWorkflowAdvance(target: string) {
  if (!task.value || wfAdvancing.value || advancing.value) return
  wfAdvancing.value = true
  wfResult.value = null
  wfRunningNode.value = ''
  wfDoneCount.value = 0
  wfTotalNodes.value = 0
  try {
    const execId = await workflowApi.run(
      `task-advance:${target}`,
      {}, // 空 dag → 后端 template_for(target) 选模板
      {}, // 全局 config 留空(节点级 config 由模板/运行时注入)
      task.value.id,
      target,
    )
    wfExecId.value = execId
    errorMsg.value = ''
    await refreshWorkflowDag(execId)
  } catch (e: any) {
    wfAdvancing.value = false
    wfExecId.value = null
    errorMsg.value = t('taskDetail.workflowAdvanceFailed', { msg: e?.toString() ?? t('common.unknownError') })
  }
}

// B-41: 处理单个工作流事件(由 onEvent 回调 dispatch)。按 execution_id 过滤当前推进。
function handleWorkflowEvent(payload: WorkflowEventPayload) {
  if (!wfExecId.value || payload.execution_id !== wfExecId.value) return
  const evt = payload.event
  switch (evt?.type) {
    case 'node_started': {
      // 第一次 NodeStarted 时初始化 total(后端模板节点数,无法前端预知,
      // 用「已启动 + 已完成」近似 total,显示已完成/已启动进度)
      const node = String(evt.node_id ?? '')
      wfRunningNode.value = node
      wfNodeStatuses.value = { ...wfNodeStatuses.value, [node]: 'running' }
      if (wfTotalNodes.value === 0) wfTotalNodes.value = 1
      else wfTotalNodes.value = Math.max(wfTotalNodes.value, wfDoneCount.value + 1)
      break
    }
    case 'node_completed': {
      wfDoneCount.value += 1
      if (wfRunningNode.value) {
        wfNodeStatuses.value = { ...wfNodeStatuses.value, [wfRunningNode.value]: 'completed' }
      }
      wfRunningNode.value = ''
      break
    }
    case 'workflow_completed': {
      wfAdvancing.value = false
      wfRunningNode.value = ''
      wfResult.value = 'completed'
      // 终态提示保留 3s 后清空(任务本体由 df-data-changed 刷新,提示不影响数据)
      if (_wfResultTimer) clearTimeout(_wfResultTimer)
      _wfResultTimer = setTimeout(() => {
        if (wfResult.value === 'completed') wfResult.value = null
        _wfResultTimer = null
      }, 3000)
      break
    }
    case 'workflow_failed':
    case 'node_failed': {
      if (evt?.node_id) {
        const node = String(evt.node_id)
        wfNodeStatuses.value = { ...wfNodeStatuses.value, [node]: 'failed' }
      }
      if (evt?.type === 'workflow_failed') {
        wfAdvancing.value = false
        wfRunningNode.value = ''
        wfResult.value = 'failed'
        if (_wfResultTimer) clearTimeout(_wfResultTimer)
        _wfResultTimer = setTimeout(() => {
          if (wfResult.value === 'failed') wfResult.value = null
          _wfResultTimer = null
        }, 3000)
      }
      // node_failed: 单节点失败,工作流后续会发 workflow_failed,此处不提前改终态
      break
    }
    default:
      break
  }
}

async function load() {
  loading.value = true
  errorMsg.value = ''
  try {
    const [t, ps, ideasList] = await Promise.all([
      taskApi.get(taskId.value),
      // 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store)
      projectApi.list(),
      // F-260619-01:灵感列表用于解析 idea_id → 灵感 title
      ideaApi.list(),
    ])
    task.value = t
    projects.value = ps
    ideas.value = ideasList
  } catch (e: any) {
    task.value = null
    errorMsg.value = t('taskDetail.loadFailed', { msg: e?.toString() ?? t('common.unknownError') })
  } finally {
    loading.value = false
  }
}

function refresh() {
  load()
}

// 路由参数变化(id 变化)时重新加载
watch(taskId, () => { load() })

// Problem4 ②:切换 task / 描述渲染完成 → 重置折叠态 + 重测高度。
// renderedDesc 异步(Markdown 经 marked 渲染),内容变化后 nextTick 测真实 px。
watch(() => task.value?.id, () => { descExpanded.value = false })
watch(renderedDesc, () => { measureDescHeight() })

// B-260616-18: 数据变更联动刷新 unlistener(onMounted 注册,onBeforeUnmount 释放)
// 对齐 AiChat _unlistenToolSlow 生命周期模式。本视图绕 store 直调 taskApi.get/projectApi.list,
// 不享受 store 全局 df-data-changed 监听(该监听只刷 store.tasks 列表,不含本视图的当前 task 单体),
// 故此本地监听 entity=task/project 时重载当前 task。
let _unlistenDataChanged: (() => void) | null = null
// B-41: 工作流推进事件 unlistener(听 NodeStarted/NodeCompleted/WorkflowFailed 显示轻量进度)
let _unlistenWorkflowEvent: (() => void) | null = null

onMounted(async () => {
  ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat 共享),不阻塞 load
  load()
  // B-260616-18: 后端 AI 工具(create/update/delete 等)emit df-data-changed → 本视图重载当前 task
  // entity=task:任务本体字段(标题/状态/描述/分支…)被改时刷新;entity=project:项目名变更影响 projectName 解析时刷新
  try {
    _unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
      const { entity } = event.payload
      if (entity === 'task' || entity === 'project' || entity === 'idea') {
        load()
      }
    })
  } catch (e) {
    console.error('[TaskDetail] 启动 df-data-changed 监听失败:', e)
  }
  // B-41: 工作流推进事件监听(复用 workflowApi.onEvent, 与 workflow store 的全局监听并存无冲突 —
  // 多个 listen 各自接收全量事件,本视图按 execution_id 过滤只处理自己发起的推进)
  try {
    _unlistenWorkflowEvent = await workflowApi.onEvent(handleWorkflowEvent)
  } catch (e) {
    console.error('[TaskDetail] 启动 workflow-event 监听失败:', e)
  }
})

onBeforeUnmount(() => {
  if (_unlistenDataChanged) { _unlistenDataChanged(); _unlistenDataChanged = null }
  if (_unlistenWorkflowEvent) { _unlistenWorkflowEvent(); _unlistenWorkflowEvent = null }
  // SW-260618-21: 清终态提示 timer 防卸载后写已销毁 ref
  if (_wfResultTimer) { clearTimeout(_wfResultTimer); _wfResultTimer = null }
})
</script>

<style scoped>
.task-detail { padding: 16px 20px 20px; }

/* page-header / btn / back-link 等已提取到 global.css 全局 */
.header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.header-left h1 {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  min-width: 0;
}

/* Problem4 ① 吸顶 header(推进按钮始终可见) */
.task-detail-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--df-bg);
  /* 留底部细分割线,与全局 page-header margin-bottom 协同(0.5px 边框规范) */
  padding-bottom: 8px;
  border-bottom: 0.5px solid var(--df-border);
  /* 顶负 padding 抵消 .task-detail 的 16px 顶 padding,吸顶贴顶 */
  margin: -16px -20px 0;
  padding-left: 20px;
  padding-right: 20px;
  padding-top: 16px;
  align-items: center;
}
/* header-actions 内多个推进按钮 + refresh 排列(对齐全局 .header-actions gap:10px) */
.task-detail-header .header-actions { flex-wrap: wrap; justify-content: flex-end; }

/* advance 操作区已移入 header-actions,旧 .advance-row 样式删除 */

/* B-41 工作流推进轻量进度提示 */
.wf-progress {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 6px;
  font-size: 12px;
  color: var(--df-text-secondary);
}
.wf-progress-count { color: var(--df-text-dim); }
.wf-progress-hint { color: var(--df-success); }
.wf-progress-hint-fail { color: var(--df-danger); }

/* F-04 review 轮次徽章 */
.review-rounds-badge {
  font-size: 11px;
  color: var(--df-warning);
  background: rgba(255,217,61,0.12);
  padding: 3px 8px;
  border-radius: var(--df-radius-xs);
  margin-left: 8px;
  white-space: nowrap;
}

/* ===== 状态/优先级标签 ===== */
.status-tag {
  font-size: 11px; font-weight: 500;
  padding: 4px 10px;
  border-radius: var(--df-radius-sm);
  white-space: nowrap;
}
.status-todo { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
.status-progress { background: rgba(100,181,246,0.2); color: var(--df-info); }
.status-review { background: rgba(255,217,61,0.2); color: var(--df-warning); }
.status-done { background: rgba(100,255,218,0.15); color: var(--df-success); }
.status-abandoned { background: rgba(255,107,107,0.2); color: var(--df-danger); }
/* D-260616-01:7 态新增 — testing(橙警告,近 review 区分)/blocked(红 danger,与 cancelled 区分用实心边框)。配色与 Tasks.vue 一致保持跨视图统一 */
.status-testing { background: rgba(255,152,0,0.2); color: #ff9800; }
.status-blocked { background: rgba(255,107,107,0.12); color: var(--df-danger); border: 0.5px solid var(--df-danger); }

.priority-badge {
  font-size: 10px; font-weight: 500;
  padding: 1px 6px;
  border-radius: var(--df-radius-xs);
}
.priority-critical { background: rgba(255,107,107,0.2); color: var(--df-danger); }
.priority-high { background: rgba(255,152,0,0.2); color: #ff9800; }
.priority-medium { background: rgba(100,181,246,0.2); color: var(--df-info); }
.priority-low { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }

/* ===== 面板 ===== */
/* Problem4 ⑤ 宽屏两栏:左栏(描述+关联信息+时间戳) / 右栏(任务产出+工作流进度)。
   窄屏(<960px)单列。对齐 ProjectDetail 两栏模式但阈值更宽(产出栏更窄)。
   不设 margin-top —— 全局 .page-header 已有 margin-bottom: var(--df-gap-page) 提供分隔。*/
.detail-grid {
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: var(--df-gap-page);
  align-items: start; /* 两栏顶对齐,长栏不撑高短栏 */
}
.left-column, .right-column {
  display: flex;
  flex-direction: column;
  gap: var(--df-gap-grid);
  min-width: 0; /* 网格项防溢出(长 url/代码块) */
}

/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619)。 */

/* ===== 信息列表 ===== */
.task-info {
  display: flex;
  flex-direction: column;
  gap: var(--df-gap-grid);
}
/* B-260615-31:字段同行布局(label 固定宽 + value 占余),描述字段 info-block 保持块状)
   基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
   此处仅保留本组件特有覆盖。 */
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li)。
   Problem4 重设计:描述区从 .info-item 内移到独立 .description-wrap(可折叠容器),
   故选择器改 .description-wrap。display:block 让 <span> 有块盒模型,scrollHeight 可读。*/
.description-wrap .value.description { line-height: 1.5; display: block; }

/* Problem4 ② 描述可折叠:折叠态 max-height(内联注入 400px) + overflow hidden + 过渡。
   展开态 max-height none(内联不设),自然撑开。蒙层 .desc-fade 暗示下方有内容。*/
.description-wrap {
  position: relative;
  overflow: hidden;
  transition: max-height 0.25s var(--df-ease);
}
.desc-toggle {
  margin-top: 8px;
}
.desc-fade {
  /* 折叠态底部渐变蒙层,提示"下方还有内容"(展开态不渲染此元素) */
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 32px;
  background: linear-gradient(to bottom, transparent, var(--df-bg-card));
  pointer-events: none;
}

/* Problem4 ⑥ 创建/更新时间最底小字(独立行,非 label/value 对) */
.info-item.timestamps {
  flex-direction: row;
  gap: 16px;
  padding-top: var(--df-gap-grid);
  border-top: 0.5px solid var(--df-border);
}
.timestamp {
  font-size: 11px;
  color: var(--df-text-dim);
}

/* 右栏空产出占位提示 */
.output-empty { padding: 24px 12px; }

/* F-AiNodeSelfReview output_json 区块样式已随子组件移至 TaskOutputCard.vue */

/* ===== 任务描述 Markdown 渲染(B-24,基础样式收敛至全局 ai-md.css) ===== */

.project-link {
  color: var(--df-accent);
  text-decoration: none;
  font-weight: 500;
}
.project-link:hover { text-decoration: underline; }

.branch-tag {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 12px;
  color: var(--df-text-secondary);
  background: rgba(108, 99, 255, 0.06);
  padding: 2px 8px;
  border-radius: var(--df-radius-xs);
  font-family: monospace;
}
.branch-icon { color: var(--df-accent); }

.mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }

.empty-hint {
  text-align: center; padding: 24px 12px;
  font-size: 13px; color: var(--df-text-dim);
}
.error-hint { color: var(--df-danger); }

/* Problem4 ⑤ 响应式:窄屏(<960px)两栏退单列,产出栏移到下方 */
@media (max-width: 960px) {
  .detail-grid { grid-template-columns: 1fr; }
}

/* 窄屏 header-actions 内推进按钮可能换行,允许并压缩间距 */
@media (max-width: 720px) {
  .task-detail-header { flex-wrap: wrap; }
  .task-detail-header .header-left { flex: 1 1 100%; margin-bottom: 8px; }
}
</style>
