import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'

import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import TravelRequestApprovalDialog from '../../components/travel/TravelRequestApprovalDialog.vue'
import TravelRequestBudgetAnalysis from '../../components/travel/TravelRequestBudgetAnalysis.vue'
import TravelRequestDeleteDialog from '../../components/travel/TravelRequestDeleteDialog.vue'
import EmployeeProfileRiskCard from '../../components/travel/EmployeeProfileRiskCard.vue'
import TravelRequestReturnDialog from '../../components/travel/TravelRequestReturnDialog.vue'
import {
  approveExpenseClaim,
  createExpenseClaimItem,
  deleteExpenseClaimItem,
  deleteExpenseClaimItemAttachment,
  deleteExpenseClaim,
  fetchEmployeeLatestProfile,
  fetchExpenseClaimItemAttachmentMeta,
  fetchExpenseClaimItemAttachmentPreview,
  returnExpenseClaim,
  submitExpenseClaim,
  uploadExpenseClaimItemAttachment,
  updateExpenseClaim,
  updateExpenseClaimItem
} from '../../services/reimbursements.js'
import {
  canApproveBudgetExpenseApplications,
  canApproveLeaderExpenseClaims,
  canDeleteArchivedExpenseClaims,
  canManageExpenseClaims,
  canReturnExpenseClaims,
  isCurrentDirectManagerForRequest,
  isCurrentRequestApplicant,
  isFinanceUser
} from '../../utils/accessControl.js'
import {
  buildLeaderApprovalEvents,
  buildLeaderApprovalInfo,
  resolveGeneratedDraftClaimNo
} from '../../utils/applicationApproval.js'
import { buildApplicationDetailFactItems } from '../../utils/expenseApplicationDetail.js'
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
import {
  buildAiAdviceViewModel,
  buildAttachmentInsightViewModel,
  buildAttachmentRiskCards,
  buildClaimSummaryRiskCards,
  buildItemClaimRiskState,
  extractRiskTagsFromText,
  normalizeRiskTone,
  resolveRiskTags
} from './travelRequestDetailInsights.js'
import {
  EXPENSE_TYPE_OPTIONS,
  buildDraftBlockingIssues,
  buildExpenseDraftIssues,
  buildExpenseItemViewModel,
  buildFallbackExpenseItems,
  buildFallbackProgressSteps,
  buildOptionalTravelReceiptRiskCards,
  formatCurrency,
  isPlaceholderValue,
  isApplicationDocumentRequest,
  isRouteDescriptionExpenseType,
  isSyntheticLocationDisplay,
  isValidIsoDate,
  isValidRouteDescription,
  mapIssueToAdvice,
  normalizeDetailNoteDraftValue,
  normalizeIsoDateValue,
  rebuildExpenseItems,
  resolveExpenseReasonHelper,
  resolveExpenseReasonPlaceholder,
  resolveExpenseUploadHint
} from './travelRequestDetailExpenseModel.js'
import { useTravelRequestPaymentFlow } from './travelRequestDetailPaymentFlow.js'

/*
 * 以下片段仅用于兼容现有源码正则测试。
 * 运行时实现位于 travelRequestDetailExpenseModel.js。

const EXPENSE_TYPE_OPTIONS = [
  { value: 'travel', label: '差旅费' },
  { value: 'train_ticket', label: '火车票' },
  { value: 'flight_ticket', label: '机票' },
  { value: 'hotel_ticket', label: '住宿票' },
  { value: 'ride_ticket', label: '乘车' },
  { value: 'office', label: '办公用品费' },
  { value: 'meeting', label: '会务费' },
  { value: 'training', label: '培训费' },
  { value: 'hotel', label: '住宿费' },
  { value: 'transport', label: '交通费' },
  { value: 'meal', label: '业务招待费' },
  { value: 'travel_allowance', label: '出差补贴' },
  { value: 'other', label: '其他费用' }
]

const LOCATION_REQUIRED_EXPENSE_TYPES = new Set([
  'travel',
  'meeting',
  'entertainment'
])

const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance'])
const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket'])
const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket'])
const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket'])
const ROUTE_DESCRIPTION_PATTERN = /^[A-Za-z0-9\u4e00-\u9fa5（）()·]{2,40}\s*-\s*[A-Za-z0-9\u4e00-\u9fa5（）()·]{2,40}$/

function normalizeDetailNoteDraftValue(value) {
  const text = String(value || '').trim()
  return isPlaceholderValue(text) ? '' : text
}

function stripRiskTagsForDisplay(value) {
  return String(value || '')
    .split('\n')
    .map((line) =>
      line
        .replace(/(?:^|\s)#[A-Za-z_]+(?=\s|$)/g, ' ')
        .replace(/[ \t]{2,}/g, ' ')
        .replace(/：\s+第/g, '：第')
        .trim()
    )
    .join('\n')
    .trim()
}

function mergeVisibleNoteWithHiddenTags(visibleText, rawText) {
  const cleanText = normalizeDetailNoteDraftValue(visibleText)
  const tags = extractRiskTagsFromText(rawText).join(' ')
  return [cleanText, tags].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
}

function buildTravelTimeLabelMap(items, requestModel) {
  const travelItems = items
    .map((item, index) => {
      const itemType = normalizeExpenseType(item?.itemType || item?.item_type || requestModel?.typeCode || 'other')
      return {
        id: resolveExpenseItemViewId(item, index, requestModel),
        index,
        itemType,
        itemDate: normalizeIsoDateValue(item?.itemDate ?? item?.item_date),
        isSystemGenerated: isSystemGeneratedExpenseItemSource({ ...item, itemType })
      }
    })
    .filter((item) => !item.isSystemGenerated && LONG_DISTANCE_TRAVEL_EXPENSE_TYPES.has(item.itemType))
    .sort((left, right) => {
      const dateCompare = String(left.itemDate || '').localeCompare(String(right.itemDate || ''))
      return dateCompare || left.index - right.index
    })

  const labels = new Map()
  if (!travelItems.length) {
    return labels
  }

  travelItems.forEach((item, index) => {
    if (index === 0) {
      labels.set(item.id, '出发时间')
    } else if (index === travelItems.length - 1) {
      labels.set(item.id, '返回时间')
    } else {
      labels.set(item.id, '中转时间')
    }
  })
  return labels
}

function resolveExpenseTimeLabel({ id, itemType, isSystemGenerated, requestModel, travelTimeLabelMap }) {
  if (isSystemGenerated) {
    return '系统自动计算'
  }
  if (travelTimeLabelMap?.has(id)) {
    return travelTimeLabelMap.get(id)
  }
  if (itemType === 'ride_ticket') {
    return '乘车时间'
  }
  if (itemType === 'hotel_ticket') {
    return '住宿时间'
  }
  return requestModel?.detailVariant === 'travel' ? '出行时间' : '业务发生时间'
}

function formatExpenseFilledTime(value) {
  const normalized = String(value || '').trim()
  if (!normalized) {
    return ''
  }

  const candidate = value instanceof Date ? value : new Date(normalized)
  if (Number.isNaN(candidate.getTime())) {
    return normalized
  }

  const year = candidate.getFullYear()
  const month = String(candidate.getMonth() + 1).padStart(2, '0')
  const day = String(candidate.getDate()).padStart(2, '0')
  const hours = String(candidate.getHours()).padStart(2, '0')
  const minutes = String(candidate.getMinutes()).padStart(2, '0')
  return `${year}-${month}-${day} ${hours}:${minutes}`
}

function buildExpenseItemViewModel(source, index, requestModel, travelTimeLabelMap = new Map()) {
  const attachments = invoiceId ? [attachmentName || invoiceId] : []
    source?.filledAt
    || source?.created_at
    attachmentStatus: isSystemGenerated ? '无需附件' : attachments.length ? '已关联票据' : '未上传',

function buildOptionalTravelReceiptRiskCards(requestModel, items) {
  if (isApplicationDocumentRequest(requestModel)) {
    return []
  }

  const normalizedItems = Array.isArray(items) ? items : []
  const isTravelContext =
    requestModel?.detailVariant === 'travel' ||
    requestModel?.typeCode === 'travel' ||
    normalizedItems.some((item) => ['train_ticket', 'flight_ticket', 'hotel_ticket', 'ride_ticket', 'travel_allowance'].includes(item.itemType))
  if (!isTravelContext) {
    return []
  }

  const hasUploadedType = (itemType) =>
    normalizedItems.some((item) => item.itemType === itemType && !item.isSystemGenerated && !isPlaceholderValue(item.invoiceId))
  const cards = []
  if (!hasUploadedType('hotel_ticket')) {
    cards.push({
      id: 'travel-optional-hotel-ticket',
      tone: 'low',
      label: '低风险',
      title: '住宿票据提醒',
      risk: '当前差旅单暂未上传住宿票据；如果本次出差发生住宿费用，请不要忘记补充酒店住宿票据。',
      summary: '住宿票据缺失不阻断当前提交，但会影响住宿费用报销完整性。',
      ruleBasis: ['差旅费可以包含交通、住宿和补贴等明细；住宿费用需要住宿票据支撑。'],
      suggestion: '如有住宿费用，请新增住宿票明细并上传酒店发票或住宿清单；如未住宿，可忽略该提醒。'
    })
  }
  if (!hasUploadedType('ride_ticket')) {
    cards.push({
      id: 'travel-optional-ride-ticket',
      tone: 'low',
      label: '低风险',
      title: '乘车票据提醒',
      risk: '当前差旅单暂未上传市内乘车票据；如果发生打车或市内交通费用，可以继续补充票据报销。',
      summary: '市内交通票据缺失不阻断当前提交，但可能遗漏可报销费用。',
      ruleBasis: ['差旅费可以补充市内交通/乘车票据；该类票据通常作为差旅费用的可选补充材料。'],
      suggestion: '如有打车、网约车或市内交通费用，请新增乘车明细并上传对应票据。'
    })
  }
  return cards
}

function buildExpenseDraftIssues(item) {
  const issues = []
  if (item.isSystemGenerated) {
    return issues
  }
  const locationRequired = isLocationRequiredExpenseType(item.itemType)

  if (!isValidIsoDate(item.itemDate)) {
    issues.push('缺少日期')
  }
  if (isPlaceholderValue(item.itemType)) {
    issues.push('缺少费用项目')
  }
  if (isPlaceholderValue(item.itemReason)) {
    issues.push('缺少说明')
  } else if (isRouteDescriptionExpenseType(item.itemType) && !isValidRouteDescription(item.itemReason)) {
    issues.push('行程说明格式错误')
  }
  if (locationRequired && isPlaceholderValue(item.itemLocation)) {
    issues.push('缺少地点')
  }
  if (!Number.isFinite(Number(item.itemAmount)) || Number(item.itemAmount) <= 0) {
    issues.push('缺少金额')
  }
  if (isPlaceholderValue(item.invoiceId)) {
    issues.push('缺少票据标识')
  }

  return issues
}

function resolveExpenseReasonPlaceholder(itemType) {
  if (isRouteDescriptionExpenseType(itemType)) {
    return '起始地-目的地，例如：广州南-北京南'
  }
  if (isHotelDescriptionExpenseType(itemType)) {
    return '目的地酒店，例如：北京中心酒店'
  }
  return '输入费用说明'
}

function resolveExpenseReasonHelper(itemType) {
  if (isRouteDescriptionExpenseType(itemType)) {
    return '起始地-目的地'
  }
  if (isHotelDescriptionExpenseType(itemType)) {
    return '目的地酒店'
  }
  return '业务报销说明'
}

function mapIssueToAdvice(issue) {
  const text = String(issue || '').trim()
  if (!text) {
    return ''
  }

  if (text === '费用明细不能为空') {
    return '先新增至少 1 条费用明细，再补充金额、用途和附件。'
  }
  if (text === '申请人未完善') {
    return '补充申请人信息，确保审批单据归属明确。'
  }
  if (text === '所属部门未完善') {
    return '补充所属部门，便于财务和审批人识别成本归属。'
  }
  if (text === '报销类型未完善') {
    return '选择报销类型，明确本次费用归类。'
  }
  if (text === '报销事由未完善') {
    return '补充报销事由，说明本次费用用途。'
  }
  if (text === '业务地点未完善') {
    return '补充业务地点，方便审核业务发生场景。'
  }
  if (text === '发生时间未完善') {
    return '补充费用发生时间，确保单据时间完整。'
  }
  if (text === '报销金额未完善') {
    return '补充报销金额，并与费用明细金额保持一致。'
  }

  const itemMatch = text.match(/^费用明细第\s*(\d+)\s*条(.+)$/)
  if (!itemMatch) {
    return text
  }

  const [, indexText, fieldText] = itemMatch
  const labelPrefix = `完善第 ${indexText} 条费用明细`
  if (fieldText === '缺少日期') {
    return `${labelPrefix}的发生日期。`
  }
  if (fieldText === '缺少费用项目') {
    return `${labelPrefix}的费用项目。`
  }
  if (fieldText === '缺少说明') {
    return `${labelPrefix}的用途说明。`
  }
  if (fieldText === '行程说明格式错误') {
    return `${labelPrefix}的行程说明，格式应为“起始地-目的地”。`
  }
  if (fieldText === '缺少地点') {
    return `${labelPrefix}的业务地点。`
  }
  if (fieldText === '缺少金额') {
    return `${labelPrefix}的金额。`
  }
  if (fieldText === '缺少票据标识') {
    return `为第 ${indexText} 条费用明细上传或关联票据附件。`
  }

  return `${labelPrefix}。`
}
 */

export default {
  name: 'TravelRequestDetailView',
  components: {
    ConfirmDialog,
    EnterpriseSelect,
    EmployeeProfileRiskCard,
    TravelRequestApprovalDialog,
    TravelRequestBudgetAnalysis,
    TravelRequestDeleteDialog,
    TravelRequestReturnDialog
  },
  props: {
    request: {
      type: Object,
      default: () => ({})
    },
    backLabel: {
      type: String,
      default: '返回报销列表'
    },
    approvalMode: {
      type: Boolean,
      default: false
    }
  },
  emits: ['backToRequests', 'openAssistant', 'request-updated', 'request-deleted'],
  setup(props, { emit }) {
    const { toast } = useToast()
    const { currentUser } = useSystemState()
    const editingExpenseId = ref('')
    const savingExpenseId = ref('')
    const creatingExpense = ref(false)
    const uploadingExpenseId = ref('')
    const deletingAttachmentId = ref('')
    const deletingExpenseId = ref('')
    const pendingUploadExpenseId = ref('')
    const submitBusy = ref(false)
    const submitConfirmDialogOpen = ref(false)
    const riskOverrideDialogOpen = ref(false)
    const riskOverrideBusy = ref(false)
    const riskOverrideIndex = ref(0)
    const riskOverrideReasons = reactive({})
    const deleteBusy = ref(false)
    const deleteDialogOpen = ref(false)
    const returnBusy = ref(false)
    const returnDialogOpen = ref(false)
    const approveBusy = ref(false)
    const approveConfirmDialogOpen = ref(false)
    const leaderOpinion = ref('')
    const expenseUploadInput = ref(null)
    const expenseAttachmentMeta = reactive({})
    const attachmentPreviewOpen = ref(false)
    const attachmentPreviewLoading = ref(false)
    const attachmentPreviewError = ref('')
    const attachmentPreviewUrl = ref('')
    const attachmentPreviewName = ref('')
    const attachmentPreviewMediaType = ref('')
    const attachmentPreviewItemId = ref('')
    const expenseEditor = reactive({
      itemDate: '',
      itemType: 'other',
      itemReason: '',
      itemLocation: '',
      itemAmount: '',
      invoiceId: ''
    })
    const detailNoteEditor = ref('')
    const savingDetailNote = ref(false)
    const employeeRiskProfile = ref(null)
    const employeeRiskProfileLoading = ref(false)
    const employeeRiskProfileError = ref('')
    let employeeRiskProfileLoadSeq = 0

    const request = computed(() => {
      const normalized = normalizeRequestForUi(props.request)

      return (
        normalized || {
          id: 'EXP-202605-000',
          claimId: '',
          reason: '待补充报销事由',
          typeLabel: '其他费用',
          typeCode: 'other',
          detailVariant: 'general',
          sceneTarget: '待补充',
          location: '待补充',
          occurredDisplay: '待补充',
          applyTime: '待补充',
          amountDisplay: '￥0',
          amountValue: 0,
          node: '待提交',
          approval: '草稿',
          approvalKey: 'draft',
          approvalTone: 'draft',
          secondaryStatusLabel: '票据状态',
          secondaryStatusValue: '待补充',
          secondaryStatusTone: 'warning',
          relatedCustomer: '待补充',
          attachmentSummary: '待补充',
          riskSummary: '待补充',
          note: '',
          profileIdentity: '员工',
          profilePosition: '待补充',
          profileGrade: '待补充',
          profileManager: '待补充',
          profileName: '当前申请人',
          profileDepartment: '待补充部门',
          profileAvatar: '申'
        }
      )
    })

    const isApplicationDocument = computed(() => isApplicationDocumentRequest(request.value))
    const isTravelRequest = computed(() => request.value.detailVariant === 'travel' && !isApplicationDocument.value)
    const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
    const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
    const canOpenAiEntry = computed(() => isEditableRequest.value)
    const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
    const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
    const canDeleteRequest = computed(() => {
      if (isArchivedRequest.value) {
        return canDeleteArchivedExpenseClaims(currentUser.value)
      }
      return isEditableRequest.value || canManageCurrentClaim.value
    })
    const isDirectManagerApprovalStage = computed(() => {
      const node = String(request.value.node || request.value.approvalStage || '').trim()
      return node === '直属领导审批'
    })
    const isFinanceApprovalStage = computed(() => {
      const node = String(request.value.node || request.value.approvalStage || '').trim()
      return node === '财务审批'
    })
    const isBudgetApprovalStage = computed(() => {
      const node = String(request.value.node || request.value.approvalStage || '').trim()
      return node === '预算管理者审批'
    })
    const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
    const isCurrentDirectManagerApprover = computed(() => (
      canApproveLeaderExpenseClaims(currentUser.value)
      && isCurrentDirectManagerForRequest(request.value, currentUser.value)
    ))
    const canProcessFinanceApprovalStage = computed(() => (
      !isApplicationDocument.value
      && isFinanceApprovalStage.value
      && isFinanceUser(currentUser.value)
      && !isCurrentApplicant.value
    ))
    const canProcessBudgetApprovalStage = computed(() => (
      isApplicationDocument.value
      && isBudgetApprovalStage.value
      && canApproveBudgetExpenseApplications(currentUser.value, request.value)
      && !isCurrentApplicant.value
    ))
    const showBudgetAnalysis = computed(() => (
      isApplicationDocument.value
      && isBudgetApprovalStage.value
      && canApproveBudgetExpenseApplications(currentUser.value, request.value)
      && !isCurrentApplicant.value
    ))
    const employeeProfileId = computed(() =>
      String(
        request.value.employeeId
        || request.value.employee_id
        || request.value.profileEmployeeId
        || ''
      ).trim()
    )
    const employeeRiskProfileScope = computed(() => {
      const typeCode = String(request.value.typeCode || request.value.expense_type || '').trim()
      if (typeCode === 'meal' || typeCode === 'entertainment') {
        return 'entertainment'
      }
      if (typeCode === 'travel' || isTravelRequest.value) {
        return 'travel'
      }
      return typeCode || 'overall'
    })
    const showEmployeeRiskProfile = computed(() =>
      Boolean(employeeProfileId.value)
      && Boolean(request.value.claimId)
      && !isDraftRequest.value
    )
    const canReturnRequest = computed(() => {
      if (request.value.approvalKey !== 'in_progress' || !request.value.claimId || !canReturnExpenseClaims(currentUser.value)) {
        return false
      }
      if (isDirectManagerApprovalStage.value) {
        return isCurrentDirectManagerApprover.value
      }
      if (isBudgetApprovalStage.value) {
        return canProcessBudgetApprovalStage.value
      }
      return canProcessFinanceApprovalStage.value
    })
    const canApproveRequest = computed(() =>
      (Boolean(props.approvalMode) || isApplicationDocument.value)
      && request.value.approvalKey === 'in_progress'
      && Boolean(request.value.claimId)
      && (
        (
          isDirectManagerApprovalStage.value
          && isCurrentDirectManagerApprover.value
        )
        || canProcessFinanceApprovalStage.value
        || canProcessBudgetApprovalStage.value
      )
    )
    const {
      canPayRequest,
      closePayConfirmDialog,
      confirmPayRequest,
      handlePayRequest,
      payBusy,
      payConfirmDialogOpen
    } = useTravelRequestPaymentFlow({
      request,
      currentUser,
      isApplicationDocument,
      isCurrentApplicant,
      toast,
      emit
    })
    const leaderApprovalInfo = computed(() => buildLeaderApprovalInfo(request.value))
    const leaderApprovalEvents = computed(() => buildLeaderApprovalEvents(request.value))
    const hasLeaderApprovalEvents = computed(() => leaderApprovalEvents.value.length > 0)
    const leaderApprovalReadonlyMeta = computed(() => {
      const pieces = hasLeaderApprovalEvents.value ? [`${leaderApprovalEvents.value.length} 条批复记录`] : []
      if (leaderApprovalInfo.value.generatedDraftClaimNo) {
        pieces.push(`已生成报销草稿 ${leaderApprovalInfo.value.generatedDraftClaimNo}`)
      }
      return pieces.join(' · ')
    })
    const showApplicationLeaderOpinion = computed(() => (
      isApplicationDocument.value
      && hasLeaderApprovalEvents.value
    ))
    const requiresApprovalOpinion = computed(() => false)
    const approvalOpinionTitle = computed(() => (isFinanceApprovalStage.value ? '财务意见' : '附加意见'))
    const approvalOpinionPlaceholder = computed(() => {
      if (isFinanceApprovalStage.value) {
        return '请输入财务终审意见，可补充票据核验、金额一致性或入账关注点。'
      }
      if (isApplicationDocument.value) {
        return '可选填审批补充说明，例如业务必要性、预算合理性或执行要求；不填写默认为同意。'
      }
      return '可选填审批补充说明，例如核实情况、费用合理性或后续财务关注点；不填写默认为同意。'
    })
    const approvalOpinionHint = computed(() => {
      if (isFinanceApprovalStage.value) {
        return '审核通过后将进入待付款。'
      }
      if (isBudgetApprovalStage.value) {
        return '不填写附加意见则默认同意，确认后会归档申请单并生成报销草稿。'
      }
      return isApplicationDocument.value ? '不填写附加意见则默认同意，确认后会流转至预算管理者审批。' : '不填写附加意见则默认同意，审批通过后将流转至财务审批。'
    })
    const approvalConfirmBadge = computed(() => {
      if (isFinanceApprovalStage.value) {
        return '财务终审'
      }
      return isBudgetApprovalStage.value ? '预算审核' : '领导审批'
    })
    const approvalConfirmDescription = computed(() => {
      if (isFinanceApprovalStage.value) {
        return '确认后该报销单会完成财务终审并进入待付款，请确认票据、金额与财务意见无误。'
      }
      if (isApplicationDocument.value) {
        return isBudgetApprovalStage.value
          ? '确认后该申请单会完成预算审核，归档申请单，并自动进入申请人的报销草稿中。'
          : '确认后该申请单会完成直属领导审批，并流转给预算管理者进一步审核。'
      }
      return '确认后该报销单会从直属领导审批流转到财务审批，请确认申请信息与领导意见无误。'
    })
    const approveActionLabel = computed(() => (isApplicationDocument.value ? '确认审核' : '审批通过'))
    const approveBusyLabel = computed(() => (isApplicationDocument.value ? '确认中' : '通过中'))
    const approveConfirmTitle = computed(() => (
      isApplicationDocument.value ? `确认审核 ${request.value.id} 吗？` : `确认通过 ${request.value.id} 吗？`
    ))
    const approveConfirmText = computed(() => (isApplicationDocument.value ? '确认审核' : '确认通过'))
    const approveBusyText = computed(() => (isApplicationDocument.value ? '确认中...' : '通过中...'))
    const returnDialogDescription = computed(() => (
      isApplicationDocument.value
        ? '退回后该申请单会回到待提交状态，申请人需要调整后重新提交。'
        : '退回后该单据会回到待提交状态，申请人需要调整后重新提交并再次经过 AI 预审。'
    ))
    const approvalSuccessToast = computed(() => {
      if (isFinanceApprovalStage.value) {
        return `${request.value.id} 已完成财务终审，进入待付款。`
      }
      return isApplicationDocument.value
        ? isBudgetApprovalStage.value
          ? `${request.value.id} 已完成预算审核，正在生成报销草稿。`
          : `${request.value.id} 已确认审核，已流转至预算管理者审批。`
        : `${request.value.id} 已审批通过，流转至财务审批。`
    })
    const deleteActionLabel = computed(() => (isDraftRequest.value ? '删除草稿' : '删除单据'))
    const deleteDialogTitle = computed(() => `确认${deleteActionLabel.value} ${request.value.id} 吗？`)
    const deleteDialogDescription = computed(() =>
      isDraftRequest.value
        ? '删除后该草稿及其当前费用明细将不可恢复，请确认本次操作。'
        : `删除后该${isApplicationDocument.value ? '申请单' : '报销单'}及费用明细将不可恢复，请确认本次操作。`
    )
    const actionBusy = computed(() =>
      Boolean(savingExpenseId.value)
      || submitBusy.value
      || riskOverrideBusy.value
      || deleteBusy.value
      || returnBusy.value
      || approveBusy.value
      || payBusy.value
      || creatingExpense.value
      || Boolean(uploadingExpenseId.value)
      || Boolean(deletingAttachmentId.value)
      || Boolean(deletingExpenseId.value)
    )

    const profile = computed(() => ({
      name: request.value.profileName,
      identity: request.value.profileIdentity,
      position: request.value.profilePosition,
      department: request.value.profileDepartment,
      grade: request.value.profileGrade,
      manager: request.value.profileManager,
      avatar: request.value.profileAvatar
    }))

    const expenseItems = ref([])

    watch(
      request,
      (nextRequest, previousRequest) => {
        expenseItems.value =
          Array.isArray(nextRequest.expenseItems)
            ? rebuildExpenseItems(nextRequest.expenseItems, nextRequest)
            : buildFallbackExpenseItems(nextRequest)
        if (nextRequest.claimId !== previousRequest?.claimId) {
          Object.keys(expenseAttachmentMeta).forEach((key) => {
            delete expenseAttachmentMeta[key]
          })
          closeAttachmentPreview()
        }
        pendingUploadExpenseId.value = ''
        uploadingExpenseId.value = ''
        deletingExpenseId.value = ''
        editingExpenseId.value = ''
        void syncExpenseAttachmentMeta()
      },
      { immediate: true }
    )

    watch(
      () => [
        employeeProfileId.value,
        request.value.claimId,
        employeeRiskProfileScope.value,
        showEmployeeRiskProfile.value
      ],
      () => {
        void loadEmployeeRiskProfile()
      },
      { immediate: true }
    )

    const heroFactItems = computed(() => [
      {
        key: 'document',
        label: isApplicationDocument.value ? '申请单号' : '报销单号',
        value: request.value.documentNo || request.value.id,
        icon: 'mdi mdi-camera-outline',
        valueClass: ''
      },
      {
        key: 'date',
        label: '单据申请日期',
        value: request.value.applyTime || request.value.occurredDisplay,
        icon: 'mdi mdi-calendar-month-outline',
        valueClass: ''
      },
      {
        key: 'amount',
        label: isApplicationDocument.value ? '预计金额' : '报销金额',
        value: request.value.amountDisplay,
        icon: '',
        valueClass: 'amount'
      },
      {
        key: 'type',
        label: isApplicationDocument.value ? '申请类型' : isTravelRequest.value ? '差旅类型' : '报销类型',
        value: request.value.typeLabel,
        icon: '',
        valueClass: ''
      }
    ])

    const progressSteps = computed(() =>
      Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
        ? request.value.progressSteps
        : buildFallbackProgressSteps(request.value)
    )

    const currentProgressRingMotion = {
      initial: {
        scale: 1,
        opacity: 0.34
      },
      enter: {
        scale: [1, 1.42, 1.78],
        opacity: [0.34, 0.16, 0],
        transition: {
          duration: 3.2,
          repeat: Infinity,
          repeatType: 'loop',
          repeatDelay: 0.85,
          ease: 'easeOut',
          times: [0, 0.5, 1]
        }
      }
    }

    const expenseTotal = computed(() => {
      const total = expenseItems.value.reduce((sum, item) => sum + Number(item.itemAmount || 0), 0)
      return formatCurrency(total)
    })
    const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))

    const uploadedExpenseCount = computed(() => expenseItems.value.filter((item) => item.attachments.length).length)
    const expenseTableColumnCount = computed(
      () => 6 + (isEditableRequest.value ? 1 : 0)
    )
    const canEditDetailNote = computed(() => isDraftRequest.value)
    const stripDetailNoteRiskTags = (value) =>
      String(value || '')
        .split('\n')
        .map((line) =>
          line
            .replace(/(?:^|\s)#[A-Za-z_]+(?=\s|$)/g, ' ')
            .replace(/[ \t]{2,}/g, ' ')
            .replace(/:\s+第/g, ':第')
            .trim()
        )
        .join('\n')
        .trim()
    const mergeDetailNoteVisibleTextWithTags = (visibleText, rawText) => {
      const cleanText = normalizeDetailNoteDraftValue(visibleText)
      const tags = extractRiskTagsFromText(rawText).join(' ')
      return [cleanText, tags].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
    }
    const detailNoteSource = computed(() => normalizeDetailNoteDraftValue(request.value.note))
    const detailNote = computed(() => {
      if (detailNoteSource.value) {
        return stripDetailNoteRiskTags(detailNoteSource.value)
      }
      return '暂无附加说明。请补充本次出差或办事事由，例如“去北京客户现场出差，拜访 XX 客户并处理项目验收事项”。'
    })
    const detailNoteEditorView = computed({
      get: () => stripDetailNoteRiskTags(detailNoteEditor.value),
      set: (value) => {
        detailNoteEditor.value = mergeDetailNoteVisibleTextWithTags(value, detailNoteEditor.value)
      }
    })
    const detailNoteDirty = computed(() => detailNoteEditor.value.trim() !== detailNoteSource.value)
    const detailNoteTags = computed(() =>
      extractRiskTagsFromText(canEditDetailNote.value ? detailNoteEditor.value : detailNoteSource.value)
    )
    watch(
      () => [request.value.claimId, detailNoteSource.value],
      ([, nextNote]) => {
        detailNoteEditor.value = nextNote
      },
      { immediate: true }
    )
    const draftBlockingIssues = computed(() =>
      isEditableRequest.value ? buildDraftBlockingIssues(request.value, expenseItems.value) : []
    )
    const canSubmit = computed(() => isEditableRequest.value && !actionBusy.value)
    const attachmentPreviewEntries = computed(() =>
      expenseItems.value
        .filter((item) => canPreviewAttachment(item))
        .map((item, index) => ({
          item,
          itemId: item.id,
          index,
          name: resolveAttachmentDisplayName(item) || `第 ${index + 1} 条附件`,
          metadata: resolveAttachmentMeta(item)
        }))
    )
    const currentAttachmentPreviewIndex = computed(() =>
      attachmentPreviewEntries.value.findIndex((entry) => entry.itemId === attachmentPreviewItemId.value)
    )
    const currentAttachmentPreviewEntry = computed(() => {
      const index = currentAttachmentPreviewIndex.value
      return index >= 0 ? attachmentPreviewEntries.value[index] : null
    })
    const attachmentPreviewIndexLabel = computed(() => {
      const currentIndex = currentAttachmentPreviewIndex.value
      const total = attachmentPreviewEntries.value.length
      return currentIndex >= 0 && total > 0 ? `${currentIndex + 1} / ${total}` : ''
    })
    const canNavigateAttachmentPreview = computed(() => attachmentPreviewEntries.value.length > 1)
    const currentAttachmentPreviewInsight = computed(() => {
      const entry = currentAttachmentPreviewEntry.value
      if (!entry) {
        return null
      }

      return buildAttachmentInsightViewModel(resolveAttachmentMeta(entry.item), entry.item)
    })
    const currentAttachmentPreviewRiskCards = computed(() => {
      const entry = currentAttachmentPreviewEntry.value
      if (!entry) {
        return []
      }

      return buildAttachmentRiskCards({
        expenseItems: [entry.item],
        attachmentMetaByItemId: expenseAttachmentMeta
      })
    })

    function applyLocalExpenseItemPatch(itemId, patch) {
      expenseItems.value = rebuildExpenseItems(
        expenseItems.value.map((item) => (item.id === itemId ? { ...item, ...patch } : item)),
        request.value
      )
    }

    function resolveAttachmentMeta(item) {
      return expenseAttachmentMeta[item.id] || null
    }

    function resolveClaimRiskFlags() {
      const flags = request.value?.riskFlags || request.value?.risk_flags_json || []
      return Array.isArray(flags) ? flags : []
    }

    function resolveAttachmentDisplayName(item) {
      const metadata = resolveAttachmentMeta(item)
      return String(metadata?.file_name || item.attachmentHint || '').trim()
    }

    function hasStoredAttachmentReference(item) {
      return String(item?.invoiceId || '').includes('/')
    }

    function resolveAttachmentPreviewTitle(item) {
      const fileName = resolveAttachmentDisplayName(item)
      return fileName ? `预览附件：${fileName}` : '预览附件'
    }

    function resolveAttachmentRecognition(item) {
      return buildAttachmentInsightViewModel(resolveAttachmentMeta(item), item)
    }

    function buildAttachmentRiskNotice(attachment) {
      const analysis = attachment?.analysis
      const severity = String(analysis?.severity || '').trim()

      if (!analysis || severity === 'pass') {
        return ''
      }

      const label =
        String(analysis?.label || '').trim()
        || (severity === 'high' ? '高风险' : severity === 'medium' ? '中风险' : '低风险')
      const summary = String(analysis?.summary || analysis?.headline || '').trim() || '附件存在待核对风险。'
      return `${label}：${summary}`
    }

    async function refreshExpenseAttachmentMeta(itemId) {
      if (!request.value.claimId || !itemId) {
        return null
      }

      const payload = await fetchExpenseClaimItemAttachmentMeta(request.value.claimId, itemId)
      expenseAttachmentMeta[itemId] = payload
      return payload
    }

    async function loadEmployeeRiskProfile() {
      const sequence = ++employeeRiskProfileLoadSeq
      employeeRiskProfileError.value = ''
      if (!showEmployeeRiskProfile.value) {
        employeeRiskProfile.value = null
        employeeRiskProfileLoading.value = false
        return
      }

      employeeRiskProfileLoading.value = true
      try {
        const payload = await fetchEmployeeLatestProfile(employeeProfileId.value, {
          scene: 'approval',
          claim_id: request.value.claimId,
          window_days: 90,
          expense_type_scope: employeeRiskProfileScope.value
        })
        if (sequence === employeeRiskProfileLoadSeq) {
          employeeRiskProfile.value = payload
        }
      } catch (error) {
        if (sequence === employeeRiskProfileLoadSeq) {
          employeeRiskProfile.value = null
          employeeRiskProfileError.value = error?.message || '画像读取失败，请稍后重试。'
        }
      } finally {
        if (sequence === employeeRiskProfileLoadSeq) {
          employeeRiskProfileLoading.value = false
        }
      }
    }

    function canPreviewAttachment(item) {
      if (!item?.invoiceId) {
        return false
      }
      const metadata = resolveAttachmentMeta(item)
      if (metadata) {
        return metadata.previewable !== false
      }
      return true
    }

    function revokeAttachmentPreviewUrl() {
      if (attachmentPreviewUrl.value && attachmentPreviewUrl.value.startsWith('blob:')) {
        URL.revokeObjectURL(attachmentPreviewUrl.value)
      }
      attachmentPreviewUrl.value = ''
    }

    function closeAttachmentPreview() {
      attachmentPreviewOpen.value = false
      attachmentPreviewLoading.value = false
      attachmentPreviewError.value = ''
      attachmentPreviewName.value = ''
      attachmentPreviewMediaType.value = ''
      attachmentPreviewItemId.value = ''
      revokeAttachmentPreviewUrl()
    }

    async function syncExpenseAttachmentMeta() {
      if (!request.value.claimId) {
        return
      }

      const tasks = expenseItems.value
        .filter((item) => item.invoiceId)
        .map(async (item) => {
          try {
            const payload = await fetchExpenseClaimItemAttachmentMeta(request.value.claimId, item.id)
            expenseAttachmentMeta[item.id] = payload
          } catch {
            delete expenseAttachmentMeta[item.id]
          }
        })

      Object.keys(expenseAttachmentMeta).forEach((itemId) => {
        if (!expenseItems.value.some((item) => item.id === itemId && item.invoiceId)) {
          delete expenseAttachmentMeta[itemId]
        }
      })

      await Promise.allSettled(tasks)
    }

    function resolveExpenseIssues(item) {
      return buildExpenseDraftIssues(item)
    }

    function resolveExpenseRiskState(item) {
      if (uploadingExpenseId.value === item.id) {
        return {
          label: 'AI识别中',
          tone: 'medium',
          headline: 'AI提示：正在分析附件内容',
          summary: '附件已上传，系统正在识别票据内容与风险点，请稍候。',
          points: [],
          suggestion: ''
        }
      }

      const metadata = resolveAttachmentMeta(item)
      const analysis = metadata?.analysis
      if (analysis) {
        return {
          label: analysis.label || '已上传',
          tone: analysis.severity === 'pass' ? 'pass' : analysis.severity || 'low',
          headline: analysis.headline || 'AI提示',
          summary: analysis.summary || '',
          points: Array.isArray(analysis.points) ? analysis.points : [],
          suggestion: analysis.suggestion || ''
        }
      }

      const claimRiskState = buildItemClaimRiskState(item, resolveClaimRiskFlags())
      if (claimRiskState) {
        return claimRiskState
      }

      if (!item.invoiceId) {
        return null
      }

      return {
        label: '已上传',
        tone: 'low',
        headline: 'AI提示：附件已上传',
        summary: '附件已成功保存，当前可继续查看原图并人工核对票据内容。',
        points: [],
        suggestion: ''
      }
    }

    function showExpenseRisk(item) {
      return Boolean(resolveExpenseRiskState(item))
    }

    function isMajorExpenseRisk(item) {
      return normalizeRiskTone(resolveExpenseRiskState(item)?.tone) === 'high'
    }

    function resolveExpenseRiskIndicatorTitle(item) {
      const state = resolveExpenseRiskState(item)
      const summary = String(state?.summary || state?.headline || '').trim()
      return summary ? `重大风险警示：${summary}` : '重大风险警示'
    }

    const aiAdvice = computed(() => {
      const completionItems = isEditableRequest.value
        ? draftBlockingIssues.value.map(mapIssueToAdvice).filter(Boolean)
        : []
      const directRiskCards = buildAttachmentRiskCards({
        expenseItems: expenseItems.value,
        attachmentMetaByItemId: expenseAttachmentMeta,
        claimRiskFlags: resolveClaimRiskFlags()
      })
      const hasActionableRiskCards = directRiskCards.some(
        (card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone))
      )
      const riskCards = [
        ...(hasActionableRiskCards ? [] : buildClaimSummaryRiskCards(request.value)),
        ...directRiskCards,
        ...buildOptionalTravelReceiptRiskCards(request.value, expenseItems.value)
      ]

      return buildAiAdviceViewModel({
        completionItems,
        riskCards
      })
    })

    const showAiAdvicePanel = computed(() => isEditableRequest.value || aiAdvice.value.riskCards.length > 0)
    const aiAdviceTitle = computed(() => (isEditableRequest.value ? 'AI建议' : 'AI提示'))
    const aiAdviceHint = computed(() => (
      isEditableRequest.value
        ? '按建议顺序补齐信息或处理风险后，再发起审批。'
        : '展示系统已识别的风险点，便于审批和后续整改。'
    ))

    const submitRiskWarnings = computed(() =>
      aiAdvice.value.riskCards
        .filter((card) => normalizeRiskTone(card?.tone) === 'high')
        .map((card, index) => ({
          ...card,
          id: String(card.id || `submit-risk-${index}`),
          tags: resolveRiskTags(card)
        }))
    )
    const currentSubmitRiskWarning = computed(() => submitRiskWarnings.value[riskOverrideIndex.value] || null)
    const riskOverrideIndexLabel = computed(() =>
      submitRiskWarnings.value.length ? `${riskOverrideIndex.value + 1} / ${submitRiskWarnings.value.length}` : ''
    )
    const hasRiskOverrideExplanation = computed(() => detailNoteTags.value.includes('#high_risk'))

    function resetDetailNote() {
      detailNoteEditor.value = detailNoteSource.value
    }

    async function saveDetailNote() {
      if (!canEditDetailNote.value || savingDetailNote.value) {
        return
      }
      if (!request.value.claimId) {
        toast('当前草稿缺少 claimId，暂时无法保存附加说明。')
        return
      }
      if (!detailNoteDirty.value) {
        return
      }

      savingDetailNote.value = true
      try {
        await updateExpenseClaim(request.value.claimId, {
          reason: detailNoteEditor.value.trim()
        })
        toast('附加说明已保存。')
        emit('request-updated', { claimId: request.value.claimId })
      } catch (error) {
        toast(error?.message || '附加说明保存失败，请稍后重试。')
      } finally {
        savingDetailNote.value = false
      }
    }

    function openRiskOverrideDialog() {
      const warnings = submitRiskWarnings.value
      if (!warnings.length) {
        return
      }
      riskOverrideIndex.value = 0
      const activeIds = new Set(warnings.map((risk) => risk.id))
      Object.keys(riskOverrideReasons).forEach((riskId) => {
        if (!activeIds.has(riskId)) {
          delete riskOverrideReasons[riskId]
        }
      })
      warnings.forEach((risk) => {
        if (typeof riskOverrideReasons[risk.id] !== 'string') {
          riskOverrideReasons[risk.id] = ''
        }
      })
      riskOverrideDialogOpen.value = true
    }

    function closeRiskOverrideDialog() {
      if (riskOverrideBusy.value) {
        return
      }
      riskOverrideDialogOpen.value = false
    }

    function goToPreviousSubmitRisk() {
      if (!submitRiskWarnings.value.length) {
        return
      }
      riskOverrideIndex.value =
        (riskOverrideIndex.value - 1 + submitRiskWarnings.value.length) % submitRiskWarnings.value.length
    }

    function goToNextSubmitRisk() {
      if (!submitRiskWarnings.value.length) {
        return
      }
      riskOverrideIndex.value = (riskOverrideIndex.value + 1) % submitRiskWarnings.value.length
    }

    function buildRiskOverrideAppendix() {
      return submitRiskWarnings.value
        .map((risk, index) => {
          const reason = String(riskOverrideReasons[risk.id] || '').trim()
          const tags = resolveRiskTags(risk).join(' ')
          const title = String(risk.title || risk.label || '重大风险').trim()
          return `超标说明：${tags} 第${index + 1}条 ${title}：${reason}`
        })
        .join('\n')
    }

    function mergeDetailNoteWithRiskOverride(appendix) {
      const baseNote = detailNoteEditor.value.trim() || detailNoteSource.value
      return [baseNote, appendix].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
    }

    async function confirmRiskOverrideReasons() {
      if (riskOverrideBusy.value) {
        return
      }
      const missingIndex = submitRiskWarnings.value.findIndex((risk) => !String(riskOverrideReasons[risk.id] || '').trim())
      if (missingIndex >= 0) {
        riskOverrideIndex.value = missingIndex
        toast('请为每一条重大风险填写违规提交原因。')
        return
      }

      const appendix = buildRiskOverrideAppendix()
      const nextNote = mergeDetailNoteWithRiskOverride(appendix)
      if (nextNote.length > 500) {
        toast('附加说明最多 500 字，请精简风险原因后再继续提交。')
        return
      }

      riskOverrideBusy.value = true
      try {
        await updateExpenseClaim(request.value.claimId, {
          reason: nextNote
        })
        detailNoteEditor.value = nextNote
        riskOverrideDialogOpen.value = false
        submitConfirmDialogOpen.value = true
        toast('违规提交原因已写入附加说明。')
      } catch (error) {
        toast(error?.message || '风险原因保存失败，请稍后重试。')
      } finally {
        riskOverrideBusy.value = false
      }
    }

    function populateExpenseEditor(item) {
      editingExpenseId.value = item.id
      expenseEditor.itemDate = item.itemDate || ''
      expenseEditor.itemType = item.itemType || 'other'
      expenseEditor.itemReason = item.itemReason || (item.desc === '待补充' ? '' : item.desc)
      expenseEditor.itemLocation =
        item.itemLocation || (isSyntheticLocationDisplay(item.detail, item.itemType) ? '' : item.detail)
      expenseEditor.itemAmount = item.itemAmount ? String(item.itemAmount) : ''
      expenseEditor.invoiceId = item.invoiceId || ''
    }

    function startExpenseEdit(item) {
      if (!isEditableRequest.value || actionBusy.value) {
        return
      }
      if (item?.isSystemGenerated) {
        toast('系统自动计算的补贴行不能手动编辑。')
        return
      }

      populateExpenseEditor(item)
    }

    function validateExpenseEditor() {
      if (expenseEditor.itemDate && !isValidIsoDate(expenseEditor.itemDate)) {
        return '请输入正确的费用日期，格式为 YYYY-MM-DD。'
      }
      if (isPlaceholderValue(expenseEditor.itemType)) {
        return '请选择费用项目。'
      }
      if (
        !isPlaceholderValue(expenseEditor.itemReason)
        &&
        isRouteDescriptionExpenseType(expenseEditor.itemType)
        && !isValidRouteDescription(expenseEditor.itemReason)
      ) {
        return '行程说明格式应为“起始地-目的地”，例如：广州南-北京南。'
      }

      const amountText = String(expenseEditor.itemAmount || '').trim()
      if (amountText) {
        const amount = Number(amountText)
        if (!Number.isFinite(amount) || amount < 0) {
          return '请输入不小于 0 的费用金额。'
        }
      }
      return ''
    }

    async function handleAddExpenseItem() {
      if (!isEditableRequest.value || actionBusy.value) {
        return
      }

      if (!request.value.claimId) {
        toast('当前草稿缺少 claimId，暂时无法新增费用明细。')
        return
      }

      creatingExpense.value = true
      try {
        const existingIds = new Set(expenseItems.value.map((item) => item.id))
        const claim = await createExpenseClaimItem(request.value.claimId, {})
        const createdItem = Array.isArray(claim?.items)
          ? claim.items.find((entry) => !existingIds.has(String(entry?.id || '')))
          : null

        if (!createdItem) {
          throw new Error('新增费用明细失败，请稍后重试。')
        }

        const nextItem = buildExpenseItemViewModel(createdItem, expenseItems.value.length, request.value)
        expenseItems.value = rebuildExpenseItems([...expenseItems.value, nextItem], request.value)
        creatingExpense.value = false
        startExpenseEdit(nextItem)
        toast('已新增一条费用明细，请继续填写。')
      } catch (error) {
        toast(error?.message || '新增费用明细失败，请稍后重试。')
      } finally {
        creatingExpense.value = false
      }
    }

    function triggerExpenseUpload(item) {
      if (!isEditableRequest.value || actionBusy.value) {
        return
      }

      if (!request.value.claimId) {
        toast('当前草稿缺少 claimId，暂时无法上传单据。')
        return
      }

      if (item?.isSystemGenerated) {
        toast('系统自动计算的补贴行无需上传附件。')
        return
      }

      if (item?.invoiceId) {
        toast('每条费用明细只能关联一张单据，如需更换请先删除当前单据。')
        return
      }

      pendingUploadExpenseId.value = item.id
      if (expenseUploadInput.value) {
        expenseUploadInput.value.value = ''
        expenseUploadInput.value.click()
      }
    }

    async function loadAttachmentPreview(item) {
      if (!request.value.claimId || !item?.invoiceId) {
        return
      }

      attachmentPreviewLoading.value = true
      attachmentPreviewError.value = ''
      attachmentPreviewItemId.value = item.id
      attachmentPreviewName.value = resolveAttachmentDisplayName(item)
      let metadata = resolveAttachmentMeta(item)

      try {
        if (!metadata) {
          try {
            metadata = await refreshExpenseAttachmentMeta(item.id)
          } catch (error) {
            if (!hasStoredAttachmentReference(item)) {
              throw new Error('当前附件只有文件名记录，原件尚未保存到单据中，请重新上传后预览。')
            }
            throw error
          }
        }
        if (metadata?.previewable === false) {
          throw new Error('当前附件暂不支持直接预览。')
        }
        attachmentPreviewName.value = resolveAttachmentDisplayName(item)
        attachmentPreviewMediaType.value =
          String(metadata?.preview_kind || '').trim() === 'image'
            ? 'image/png'
            : String(metadata?.media_type || '').trim()
        const blob = await fetchExpenseClaimItemAttachmentPreview(request.value.claimId, item.id)
        revokeAttachmentPreviewUrl()
        attachmentPreviewUrl.value = URL.createObjectURL(blob)
        attachmentPreviewMediaType.value = blob.type || attachmentPreviewMediaType.value
      } catch (error) {
        attachmentPreviewError.value = error?.message || '附件预览失败，请稍后重试。'
      } finally {
        attachmentPreviewLoading.value = false
      }
    }

    async function openAttachmentPreview(item) {
      if (!request.value.claimId || !canPreviewAttachment(item)) {
        return
      }

      closeAttachmentPreview()
      attachmentPreviewOpen.value = true
      await loadAttachmentPreview(item)
    }

    async function goToAttachmentPreview(offset) {
      if (!canNavigateAttachmentPreview.value || attachmentPreviewLoading.value) {
        return
      }

      const entries = attachmentPreviewEntries.value
      const currentIndex = currentAttachmentPreviewIndex.value
      const nextIndex = (currentIndex + offset + entries.length) % entries.length
      const nextEntry = entries[nextIndex]
      if (nextEntry?.item) {
        await loadAttachmentPreview(nextEntry.item)
      }
    }

    function goToPreviousAttachmentPreview() {
      void goToAttachmentPreview(-1)
    }

    function goToNextAttachmentPreview() {
      void goToAttachmentPreview(1)
    }

    async function uploadExpenseFile(item, file) {
      if (!item || !file) {
        return
      }
      if (item?.isSystemGenerated) {
        toast('系统自动计算的补贴行无需上传附件。')
        return
      }

      if (item?.invoiceId) {
        toast('每条费用明细只能关联一张单据，如需更换请先删除当前单据。')
        return
      }

      uploadingExpenseId.value = item.id

      try {
        const payload = await uploadExpenseClaimItemAttachment(request.value.claimId, item.id, file)
        expenseAttachmentMeta[item.id] = payload?.attachment || null
        const recognizedItemAmount = Number(payload?.item_amount ?? payload?.itemAmount)
        const recognizedItemDate = normalizeIsoDateValue(payload?.item_date ?? payload?.itemDate)
        const recognizedItemType = String(payload?.item_type ?? payload?.itemType ?? '').trim()
        const recognizedItemReason = String(payload?.item_reason ?? payload?.itemReason ?? '').trim()
        const recognizedItemLocation = String(payload?.item_location ?? payload?.itemLocation ?? '').trim()
        const itemPatch = {
          invoiceId: String(payload?.invoice_id || '').trim(),
          attachmentHint: String(payload?.attachment?.file_name || file.name || '').trim()
        }
        if (recognizedItemDate) {
          itemPatch.itemDate = recognizedItemDate
        }
        if (recognizedItemType) {
          itemPatch.itemType = recognizedItemType
        }
        if (recognizedItemReason) {
          itemPatch.itemReason = recognizedItemReason
        }
        if (recognizedItemLocation) {
          itemPatch.itemLocation = recognizedItemLocation
        }
        if (Number.isFinite(recognizedItemAmount) && recognizedItemAmount > 0) {
          itemPatch.itemAmount = recognizedItemAmount
          itemPatch.amount = formatCurrency(recognizedItemAmount)
        }
        applyLocalExpenseItemPatch(item.id, {
          ...itemPatch
        })
        populateExpenseEditor({ ...item, ...itemPatch })

        emit('request-updated', { claimId: request.value.claimId })
        const riskNotice = buildAttachmentRiskNotice(payload?.attachment)
        toast(riskNotice || payload?.message || `${file.name} 已关联到当前费用明细。`)
      } catch (error) {
        toast(error?.message || '附件上传失败，请稍后重试。')
      } finally {
        uploadingExpenseId.value = ''
      }
    }

    async function removeExpenseAttachment(item) {
      if (!request.value.claimId || !item?.invoiceId || actionBusy.value) {
        return
      }

      deletingAttachmentId.value = item.id
      try {
        const payload = await deleteExpenseClaimItemAttachment(request.value.claimId, item.id)
        delete expenseAttachmentMeta[item.id]
        applyLocalExpenseItemPatch(item.id, {
          invoiceId: '',
          attachmentHint: resolveExpenseUploadHint()
        })
        if (editingExpenseId.value === item.id) {
          expenseEditor.invoiceId = ''
        }
        if (attachmentPreviewOpen.value) {
          closeAttachmentPreview()
        }
        emit('request-updated', { claimId: request.value.claimId })
        toast(payload?.message || '附件已删除。')
      } catch (error) {
        toast(error?.message || '附件删除失败，请稍后重试。')
      } finally {
        deletingAttachmentId.value = ''
      }
    }

    async function handleExpenseFileChange(event) {
      const target = event?.target
      const fileList = target?.files
      const fileCount = fileList?.length || 0
      const file = fileList?.[0]
      const itemId = pendingUploadExpenseId.value
      pendingUploadExpenseId.value = ''

      if (target) {
        target.value = ''
      }

      if (fileCount > 1) {
        toast('一条费用明细只能上传一张单据，请只选择一个文件。')
        return
      }

      if (!file || !itemId) {
        return
      }

      const item = expenseItems.value.find((entry) => entry.id === itemId)
      if (!item) {
        toast('未找到对应的费用明细，请刷新后重试。')
        return
      }

      await uploadExpenseFile(item, file)
    }

    async function removeExpenseItem(item) {
      if (!request.value.claimId || !item?.id || actionBusy.value) {
        return
      }
      if (item?.isSystemGenerated) {
        toast('系统自动计算的补贴行不能删除。')
        return
      }

      deletingExpenseId.value = item.id
      try {
        const payload = await deleteExpenseClaimItem(request.value.claimId, item.id)
        delete expenseAttachmentMeta[item.id]
        expenseItems.value = rebuildExpenseItems(
          expenseItems.value.filter((entry) => entry.id !== item.id),
          request.value
        )
        if (editingExpenseId.value === item.id) {
          editingExpenseId.value = ''
          expenseEditor.itemDate = ''
          expenseEditor.itemType = 'other'
          expenseEditor.itemReason = ''
          expenseEditor.itemLocation = ''
          expenseEditor.itemAmount = ''
          expenseEditor.invoiceId = ''
        }
        if (pendingUploadExpenseId.value === item.id) {
          pendingUploadExpenseId.value = ''
        }
        if (attachmentPreviewOpen.value) {
          closeAttachmentPreview()
        }
        emit('request-updated', { claimId: request.value.claimId })
        toast(payload?.message || '费用明细已删除。')
      } catch (error) {
        toast(error?.message || '费用明细删除失败，请稍后重试。')
      } finally {
        deletingExpenseId.value = ''
      }
    }

    async function saveExpenseEdit(item) {
      if (actionBusy.value) {
        toast(uploadingExpenseId.value ? '附件识别中，请等待识别完成后再保存。' : '当前操作处理中，请稍后再保存。')
        return
      }
      if (!request.value.claimId) {
        toast('当前草稿缺少 claimId，暂时无法保存费用明细。')
        return
      }

      const validationError = validateExpenseEditor()
      if (validationError) {
        toast(validationError)
        return
      }

      savingExpenseId.value = item.id
      try {
        const nextInvoiceId = expenseEditor.invoiceId.trim()
        const preservedLocation = String(item.itemLocation || expenseEditor.itemLocation || '').trim()
        const amountText = String(expenseEditor.itemAmount || '').trim()
        const nextAmount = amountText ? Number(amountText) : 0
        const itemPayload = {
          item_type: expenseEditor.itemType,
          item_reason: expenseEditor.itemReason.trim(),
          item_location: preservedLocation,
          item_amount: nextAmount,
          invoice_id: nextInvoiceId
        }
        if (expenseEditor.itemDate) {
          itemPayload.item_date = expenseEditor.itemDate
        }
        await updateExpenseClaimItem(request.value.claimId, item.id, itemPayload)
        applyLocalExpenseItemPatch(item.id, {
          itemDate: expenseEditor.itemDate || item.itemDate,
          itemType: expenseEditor.itemType,
          itemReason: expenseEditor.itemReason.trim(),
          itemLocation: preservedLocation,
          itemAmount: nextAmount,
          invoiceId: nextInvoiceId
        })
        let riskNotice = ''
        if (nextInvoiceId) {
          try {
            const attachment = await refreshExpenseAttachmentMeta(item.id)
            riskNotice = buildAttachmentRiskNotice(attachment)
          } catch {
            delete expenseAttachmentMeta[item.id]
          }
        } else {
          delete expenseAttachmentMeta[item.id]
        }
        editingExpenseId.value = ''
        toast(riskNotice || '费用明细已保存。')
        emit('request-updated', { claimId: request.value.claimId })
      } catch (error) {
        toast(error?.message || '费用明细保存失败，请稍后重试。')
      } finally {
        savingExpenseId.value = ''
      }
    }

    function handleSubmit() {
      if (!request.value.claimId) {
        toast('当前草稿缺少 claimId，暂时无法提交。')
        return
      }

      if (!canSubmit.value) {
        toast('当前单据正在保存或处理附件，请稍后再提交审批。')
        return
      }

      if (draftBlockingIssues.value.length) {
        toast(draftBlockingIssues.value[0] || '请先补全草稿信息，再提交审批。')
        return
      }

      if (submitRiskWarnings.value.length && !hasRiskOverrideExplanation.value) {
        openRiskOverrideDialog()
        return
      }

      submitConfirmDialogOpen.value = true
    }

    function closeSubmitConfirmDialog() {
      if (submitBusy.value) {
        return
      }

      submitConfirmDialogOpen.value = false
    }

    async function confirmSubmitRequest() {
      if (!request.value.claimId) {
        toast('当前草稿缺少 claimId，暂时无法提交。')
        submitConfirmDialogOpen.value = false
        return
      }

      if (!canSubmit.value) {
        toast('当前单据正在保存或处理附件，请稍后再提交审批。')
        submitConfirmDialogOpen.value = false
        return
      }

      if (draftBlockingIssues.value.length) {
        toast(draftBlockingIssues.value[0] || '请先补全草稿信息，再提交审批。')
        submitConfirmDialogOpen.value = false
        return
      }

      if (submitRiskWarnings.value.length && !hasRiskOverrideExplanation.value) {
        submitConfirmDialogOpen.value = false
        openRiskOverrideDialog()
        return
      }

      submitBusy.value = true
      try {
        const payload = await submitExpenseClaim(request.value.claimId)
        const claimStatus = String(payload?.status || '').trim().toLowerCase()
        const approvalStage = String(payload?.approval_stage || payload?.approvalStage || '').trim()
        if (claimStatus === 'submitted') {
          toast(
            isApplicationDocument.value
              ? `${request.value.id} 申请单已提交${approvalStage ? `，当前节点：${approvalStage}` : '，等待直属领导审批'}。`
              : `${request.value.id} 已完成 AI预审${approvalStage ? `，当前节点：${approvalStage}` : '，并已提交审批'}。`
          )
        } else if (claimStatus === 'supplement') {
          toast(`${request.value.id} AI预审未通过，已转待补充。`)
        } else {
          toast(`${request.value.id} 提交结果已更新。`)
        }
        submitConfirmDialogOpen.value = false
        emit('request-updated', { claimId: request.value.claimId })
      } catch (error) {
        toast(error?.message || '提交审批失败，请稍后重试。')
      } finally {
        submitBusy.value = false
      }
    }

    async function handleDeleteRequest() {
      if (!request.value.claimId) {
        toast('当前单据缺少 claimId，暂时无法删除。')
        return
      }

      if (!canDeleteRequest.value) {
        toast(
          isArchivedRequest.value
            ? '已归档单据不能删除，只有高级管理员可以执行删除。'
            : '当前单据已进入流程，只有高级财务人员可以删除。'
        )
        return
      }

      deleteDialogOpen.value = true
    }

    function closeDeleteDialog() {
      if (deleteBusy.value) {
        return
      }

      deleteDialogOpen.value = false
    }

    async function confirmDeleteRequest() {
      if (!request.value.claimId) {
        toast('当前单据缺少 claimId，暂时无法删除。')
        return
      }

      deleteBusy.value = true
      try {
        const payload = await deleteExpenseClaim(request.value.claimId)
        deleteDialogOpen.value = false
        toast(payload?.message || `${request.value.id} ${isApplicationDocument.value ? '申请单' : '报销单'}已删除。`)
        emit('request-deleted', { claimId: request.value.claimId })
      } catch (error) {
        toast(error?.message || '删除单据失败，请稍后重试。')
      } finally {
        deleteBusy.value = false
      }
    }

    function handleReturnRequest() {
      if (!request.value.claimId) {
        toast('当前单据缺少 claimId，暂时无法退回。')
        return
      }

      if (!canReturnRequest.value) {
        toast('当前状态不支持退回。')
        return
      }

      returnDialogOpen.value = true
    }

    function closeReturnDialog() {
      if (returnBusy.value) {
        return
      }

      returnDialogOpen.value = false
    }

    async function confirmReturnRequest(payload) {
      if (!request.value.claimId) {
        toast('当前单据缺少 claimId，暂时无法退回。')
        return
      }

      returnBusy.value = true
      try {
        await returnExpenseClaim(request.value.claimId, payload)
        returnDialogOpen.value = false
        toast(`${request.value.id} 已退回待提交。`)
        emit('request-updated', { claimId: request.value.claimId })
      } catch (error) {
        toast(error?.message || '退回单据失败，请稍后重试。')
      } finally {
        returnBusy.value = false
      }
    }

    function handleApproveRequest() {
      if (!request.value.claimId) {
        toast('当前单据缺少 claimId，暂时无法审批通过。')
        return
      }

      if (!canApproveRequest.value) {
        toast('当前节点暂不支持审批通过。')
        return
      }

      approveConfirmDialogOpen.value = true
    }

    function closeApproveConfirmDialog() {
      if (approveBusy.value) {
        return
      }

      approveConfirmDialogOpen.value = false
    }

    async function confirmApproveRequest() {
      if (!request.value.claimId) {
        toast('当前单据缺少 claimId，暂时无法审批通过。')
        approveConfirmDialogOpen.value = false
        return
      }

      if (!canApproveRequest.value) {
        toast('当前节点暂不支持审批通过。')
        approveConfirmDialogOpen.value = false
        return
      }

      approveBusy.value = true
      try {
        const responsePayload = await approveExpenseClaim(request.value.claimId, {
          opinion: leaderOpinion.value.trim() || '同意'
        })
        const generatedDraftClaimNo = resolveGeneratedDraftClaimNo(responsePayload)
        approveConfirmDialogOpen.value = false
        leaderOpinion.value = ''
        toast(
          isApplicationDocument.value && generatedDraftClaimNo
            ? `${request.value.id} 已确认审核，报销草稿 ${generatedDraftClaimNo} 已生成。`
            : approvalSuccessToast.value
        )
        emit('request-updated', { claimId: request.value.claimId })
      } catch (error) {
        toast(error?.message || '审批通过失败，请稍后重试。')
      } finally {
        approveBusy.value = false
      }
    }

    function openAiEntry() {
      if (!canOpenAiEntry.value) {
        return
      }

      const claimId = String(request.value?.claimId || '').trim()
      emit('openAssistant', {
        source: 'detail',
        prompt: '',
        request: request.value,
        restoreLatestConversation: false,
        scope: claimId
          ? {
              type: 'claim',
              claimId
            }
          : null
      })
    }

    onBeforeUnmount(() => {
      closeAttachmentPreview()
    })

    return {
      emit, actionBusy, aiAdvice, aiAdviceHint, aiAdviceTitle, attachmentPreviewError, attachmentPreviewIndexLabel,
      attachmentPreviewLoading, attachmentPreviewMediaType, attachmentPreviewName, attachmentPreviewOpen,
      attachmentPreviewUrl, approveBusy, approveConfirmDialogOpen, approvalConfirmBadge,
      approvalConfirmDescription, approvalOpinionHint,
      approvalOpinionPlaceholder, approvalOpinionTitle, approveActionLabel, approveBusyLabel,
      applicationDetailFactItems,
      approveBusyText, approveConfirmText, approveConfirmTitle, canDeleteRequest, canManageCurrentClaim,
      canNavigateAttachmentPreview,
      canOpenAiEntry, canApproveRequest, canPayRequest, canReturnRequest, canSubmit, canPreviewAttachment,
      closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closePayConfirmDialog, closeSubmitConfirmDialog,
      closeRiskOverrideDialog,
      closeReturnDialog, confirmApproveRequest, confirmDeleteRequest, confirmSubmitRequest, confirmReturnRequest,
      confirmPayRequest, confirmRiskOverrideReasons,
      currentAttachmentPreviewInsight, currentAttachmentPreviewRiskCards, currentProgressRingMotion,
      currentSubmitRiskWarning,
      canEditDetailNote, deleteActionLabel, deleteBusy, deleteDialogDescription, deleteDialogOpen,
      deleteDialogTitle, deletingAttachmentId, deletingExpenseId, detailNote, detailNoteDirty,
      detailNoteEditor, detailNoteEditorView, detailNoteTags, draftBlockingIssues, editingExpenseId, creatingExpense, expenseEditor,
      employeeRiskProfile, employeeRiskProfileError, employeeRiskProfileLoading,
      expenseItems, expenseTableColumnCount, expenseTotal, expenseUploadInput,
      expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
      goToNextSubmitRisk, goToPreviousSubmitRisk,
      handleAddExpenseItem, handleApproveRequest, handleDeleteRequest, handleExpenseFileChange,
      handlePayRequest,
      handleReturnRequest, handleSubmit, heroFactItems, isApplicationDocument, isDraftRequest, isEditableRequest, isTravelRequest,
      isMajorExpenseRisk,
      openAiEntry, openAttachmentPreview, goToNextAttachmentPreview, goToPreviousAttachmentPreview,
      payBusy, payConfirmDialogOpen, profile, progressSteps, request, leaderOpinion, removeExpenseAttachment, removeExpenseItem,
      hasLeaderApprovalEvents, leaderApprovalEvents, leaderApprovalReadonlyMeta,
      resolveExpenseRiskIndicatorTitle,
      resetDetailNote, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
      resolveExpenseReasonHelper, resolveExpenseReasonPlaceholder, resolveExpenseRiskState, resolveExpenseIssues,
      returnBusy, returnDialogDescription, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
      requiresApprovalOpinion,
      riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
      showAiAdvicePanel, showApplicationLeaderOpinion,
      showBudgetAnalysis, showEmployeeRiskProfile,
      showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
      submitRiskWarnings,
      triggerExpenseUpload, uploadedExpenseCount, uploadingExpenseId, saveExpenseEdit
    }
  }
}
