import {
  ATTACHMENT_ASSOCIATION_CONFIRM_HREF,
  buildAttachmentAssociationConfirmationMessage,
  buildUnsavedDraftAttachmentConfirmationMessage
} from './travelReimbursementAttachmentModel.js'
import { resolveAssistantScopeGuard } from '../../utils/assistantSessionScope.js'
import {
  applyApplicationPolicyEstimateError,
  applyApplicationPolicyEstimateResult,
  buildApplicationPolicyEstimateRequest,
  buildLocalApplicationPreview,
  buildLocalApplicationPreviewMessage,
  buildModelRefinedApplicationPreview,
  shouldUseLocalApplicationPreview
} from '../../utils/expenseApplicationPreview.js'
import { fetchOntologyParse } from '../../services/ontology.js'
import { buildExpenseApplicationOntologyContext } from '../../utils/expenseApplicationOntology.js'
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
import {
  handleBudgetCompileReportSubmit,
  shouldUseBudgetCompileReport
} from './budgetAssistantReportModel.js'

export function useTravelReimbursementSubmitComposer(ctx) {
  const {
    MAX_ATTACHMENTS,
    activeReviewPayload,
    activeSessionType,
    adjustComposerTextareaHeight,
    attachedFiles,
    buildAgentInsight,
    buildClientTimeContext,
    buildComposerBusinessTimeContext,
    buildComposerFilePreviews,
    buildDraftAssociationQueryPayload,
    buildErrorInsight,
    buildExpenseIntentConfirmationActions,
    buildExpenseIntentConfirmationMessage,
    buildExpenseSceneSelectionActions,
    buildExpenseSceneSelectionMessage,
    buildMessageMeta,
    buildOcrDocumentsFromReviewPayload,
    buildOcrFilePreviews,
    buildOcrSummary,
    buildOcrSummaryFromDocuments,
    buildReviewFormContextFromPayload,
    clearAttachedFiles,
    clearFlowSimulationTimers,
    completeFlowResult,
    completeFlowStep,
    composerBusinessTimeDraftTouched,
    composerBusinessTimeTags,
    composerDraft,
    composerUploadIntent,
    conversationId,
    createMessage,
    currentInsight,
    currentUser,
    draftClaimId,
    extractReviewAttachmentNames,
    failCurrentFlowStep,
    fetchExpenseClaims,
    fileInputRef,
    flowRunId,
    insightPanelCollapsed,
    isKnowledgeSession,
    linkedRequest,
    mergeBusinessTimeIntoExtraContext,
    mergeFilePreviews,
    mergeFilesWithLimit,
    mergeUploadAttachmentNames,
    mergeUploadOcrDocuments,
    messages,
    nextTick,
    normalizeExpenseQueryPayload,
    normalizeOcrDocuments,
    persistSessionState,
    props,
    recognizeOcrFiles,
    refreshFlowRunDetail,
    rememberFilePreviews,
    replaceMessage,
    resolveComposerDisplaySubmitText,
    resetFlowRun,
    resolveComposerSubmitText,
    reviewInlineForm,
    runOrchestrator,
    scrollToBottom,
    sessionSwitchBusy,
    shouldRequestExpenseIntentConfirmation,
    shouldRequestExpenseSceneSelection,
    startExpenseClaimDraftFlowStep,
    startExpenseIntentConfirmationFlowPreview,
    startExpenseSceneSelectionFlowPreview,
    startFlowStep,
    startSemanticFlowPreview,
    submitting,
    syncComposerFilesToDraft,
    toast
  } = ctx

    const pendingAttachmentAssociations = new Map()

    function createPendingAttachmentAssociationId() {
      return `attachment-association-${Date.now()}-${Math.random().toString(16).slice(2)}`
    }

    function normalizeRecognizedAttachmentData(data) {
      if (!data || typeof data !== 'object') {
        return null
      }
      const documents = Array.isArray(data.ocrDocuments) ? data.ocrDocuments : []
      if (!documents.length) {
        return null
      }
      return {
        ocrPayload: data.ocrPayload || null,
        ocrSummary: String(data.ocrSummary || '').trim(),
        ocrDocuments: documents,
        ocrFilePreviews: Array.isArray(data.ocrFilePreviews) ? data.ocrFilePreviews : []
      }
    }

    function buildConfirmedAssociationText(message) {
      return String(message?.text || '')
        .replace(`[确认](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确认')
        .replace(`[确定](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确定')
    }

    function resolveReviewPanelScope({
      reviewPayload = null,
      reviewAction = '',
      fileCount = 0,
      rawText = ''
    } = {}) {
      if (!reviewPayload || typeof reviewPayload !== 'object') {
        return ''
      }

      const normalizedAction = String(reviewAction || '').trim()
      const documentCount = Array.isArray(reviewPayload.document_cards) ? reviewPayload.document_cards.length : 0
      const riskCount = Array.isArray(reviewPayload.risk_briefs) ? reviewPayload.risk_briefs.length : 0
      const asksRisk = /风险|隐患|超标|异常|重复|待整改|风险项|高风险|中风险|低风险/.test(String(rawText || ''))

      if (fileCount > 0 && documentCount > 0) {
        return 'documents'
      }
      if (riskCount > 0 && (asksRisk || ['next_step', 'submit', 'submit_claim'].includes(normalizedAction))) {
        return 'risk'
      }
      if (!normalizedAction && fileCount === 0) {
        return 'overview'
      }
      return ''
    }

    async function confirmPendingAttachmentAssociation(message) {
      if (submitting.value || sessionSwitchBusy.value) return null

      const pending = message?.pendingAttachmentAssociation && typeof message.pendingAttachmentAssociation === 'object'
        ? message.pendingAttachmentAssociation
        : null
      const associationId = String(pending?.id || '').trim()
      if (!associationId || pending?.status === 'confirmed') {
        return null
      }

      const runtime = pendingAttachmentAssociations.get(associationId)
      if (!runtime || !Array.isArray(runtime.files) || !runtime.files.length) {
        toast('当前会话里没有可归集的附件原件，请重新上传票据后再确认。')
        return null
      }

      pending.status = 'confirmed'
      message.pendingAttachmentAssociation = pending
      message.text = buildConfirmedAssociationText(message)
      message.meta = ['已确认归集']
      persistSessionState()

      if (pending.mode === 'save_then_associate') {
        const inheritedReviewContext = buildReviewFormContextFromPayload(
          activeReviewPayload.value,
          reviewInlineForm.value
        )
        const savePayload = await submitComposer({
          rawText: '请先把当前已识别的报销信息保存为草稿，随后继续归集本次上传的附件。',
          userText: '',
          files: [],
          skipUserMessage: true,
          pendingText: '正在先保存未保存单据...',
          systemGenerated: true,
          extraContext: {
            ...runtime.extraContext,
            ...inheritedReviewContext,
            review_action: 'save_draft'
          }
        })
        const savedClaimId = String(savePayload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
        const savedClaimNo = String(savePayload?.result?.draft_payload?.claim_no || '').trim()
        if (!savedClaimId) {
          toast('当前单据还没有保存成功，请稍后重试。')
          return savePayload
        }

        return submitComposer({
          rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${savedClaimNo || '当前草稿'}`,
          userText: `保存草稿并归集 ${runtime.fileNames.length} 份票据`,
          files: runtime.files,
          uploadDisposition: 'continue_existing',
          skipDraftAssociationPrompt: true,
          skipUserMessage: true,
          appendToCurrentFlow: true,
          systemGenerated: true,
          pendingText: savedClaimNo
            ? `草稿 ${savedClaimNo} 已保存，正在识别并归集附件...`
            : '草稿已保存，正在识别并归集附件...',
          associationConfirmed: true,
          extraContext: {
            ...runtime.extraContext,
            review_action: 'link_to_existing_draft',
            draft_claim_id: savedClaimId,
            selected_claim_id: savedClaimId,
            selected_claim_no: savedClaimNo,
            attachment_association_confirmed: true
          }
        })
      }

      return submitComposer({
        rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${runtime.claimNo || '当前草稿'}`,
        userText: `确认归集到草稿 ${runtime.claimNo || '当前草稿'}`,
        files: runtime.files,
        uploadDisposition: 'continue_existing',
        skipDraftAssociationPrompt: true,
        pendingText: runtime.claimNo
          ? `正在将票据归集到草稿 ${runtime.claimNo}...`
          : '正在将票据归集到当前草稿...',
        associationConfirmed: true,
        recognizedAttachmentData: {
          ocrPayload: runtime.ocrPayload,
          ocrSummary: runtime.ocrSummary,
          ocrDocuments: runtime.ocrDocuments,
          ocrFilePreviews: runtime.ocrFilePreviews
        },
        extraContext: {
          ...runtime.extraContext,
          review_action: 'link_to_existing_draft',
          draft_claim_id: runtime.claimId,
          selected_claim_id: runtime.claimId,
          selected_claim_no: runtime.claimNo,
          attachment_association_confirmed: true
        }
      })
    }

    function buildBackendMessage(rawText, fileNames, ocrSummary = '') {
      const parts = []
      const normalizedText = String(rawText || '').trim()
      const sessionType = String(activeSessionType.value || '').trim()

      if (normalizedText) {
        parts.push(normalizedText)
      } else if (fileNames.length) {
        parts.push(
          isKnowledgeSession.value
            ? `我上传了 ${fileNames.length} 份附件，请结合附件名称回答财务相关问题。`
            : sessionType === 'application'
              ? `我上传了 ${fileNames.length} 份附件，请结合附件名称整理费用申请建议和待核对信息。`
              : sessionType === 'approval'
                ? `我上传了 ${fileNames.length} 份附件，请结合附件名称整理审核风险和处理建议。`
                : `我上传了 ${fileNames.length} 份票据，请结合附件名称给出报销建议并整理待核对信息。`
        )
      }

      if (fileNames.length) {
        parts.push(`附件名称：${fileNames.join('、')}`)
      }

      if (ocrSummary) {
        parts.push(`OCR摘要：${ocrSummary}`)
      }

      if (props.entrySource === 'detail' && linkedRequest.value?.id) {
        parts.push(`关联单号：${linkedRequest.value.id}`)
      }

      return parts.join('\n')
    }

    function resolveDetailScopedClaimId() {
      if (props.entrySource !== 'detail' || isKnowledgeSession.value) {
        return ''
      }
      return String(
        linkedRequest.value?.claimId ||
          linkedRequest.value?.claim_id ||
          ''
      ).trim()
    }

    function buildApplicationPreviewReviewMeta(ontology) {
      return [
        '申请核对预览',
        String(ontology?.parse_strategy || '').trim() === 'llm_primary'
          ? '模型复核完成'
          : '规则兜底复核'
      ]
    }

    async function buildApplicationPreviewWithModelReview(rawText) {
      const user = currentUser.value || {}
      const localPreview = buildLocalApplicationPreview(rawText, user)

      const enrichWithPolicyEstimate = async (preview) => {
        const estimateRequest = buildApplicationPolicyEstimateRequest(preview, user)
        if (!estimateRequest.canCalculate) {
          return preview
        }
        try {
          const result = await calculateTravelReimbursement(estimateRequest.payload)
          return applyApplicationPolicyEstimateResult(preview, result, user)
        } catch (error) {
          console.warn('Application policy estimate failed:', error)
          return applyApplicationPolicyEstimateError(preview, error, user)
        }
      }

      try {
        const ontology = await fetchOntologyParse(
          {
            query: rawText,
            user_id: user.username || user.name || 'anonymous',
            context_json: {
              ...buildExpenseApplicationOntologyContext(user),
              session_type: activeSessionType.value,
              entry_source: props.entrySource,
              user_input_text: rawText
            }
          },
          {
            timeoutMs: 45000,
            timeoutMessage: '模型抽取申请字段超时，已保留当前本地预览。'
          }
        )

        const refinedPreview = buildModelRefinedApplicationPreview(
          localPreview,
          ontology,
          rawText,
          user
        )
        return {
          applicationPreview: await enrichWithPolicyEstimate(refinedPreview),
          meta: buildApplicationPreviewReviewMeta(ontology)
        }
      } catch (error) {
        console.warn('Application preview model refinement failed:', error)
        return {
          applicationPreview: await enrichWithPolicyEstimate({
            ...localPreview,
            modelReviewStatus: 'failed'
          }),
          meta: ['申请核对预览', '模型复核失败']
        }
      }
    }

    async function submitComposer(options = {}) {
      if (submitting.value || sessionSwitchBusy.value) return null

      const rawText = resolveComposerSubmitText(options.rawText).trim()
      const systemGenerated = Boolean(options.systemGenerated)
      const appendToCurrentFlow = Boolean(options.appendToCurrentFlow)
      const normalizedFiles = isKnowledgeSession.value ? [] : Array.from(options.files ?? attachedFiles.value)
      const fileMergeResult = mergeFilesWithLimit([], normalizedFiles, MAX_ATTACHMENTS)
      const files = fileMergeResult.files
      const detailScopedClaimId = resolveDetailScopedClaimId()
      const detailScopedUpload = Boolean(detailScopedClaimId && files.length)
      if (detailScopedClaimId) {
        draftClaimId.value = detailScopedClaimId
      }
      const resolvedUploadDisposition =
        String(options.uploadDisposition || '').trim() ||
        (composerUploadIntent.value === 'continue_existing' ? 'continue_existing' : '') ||
        (detailScopedUpload ? 'continue_existing' : '')
      if (fileMergeResult.overflowCount > 0) {
        toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件，已保留前 ${MAX_ATTACHMENTS} 份。`)
      }
      if (!rawText && !files.length) return
      const fileNames = files.map((file) => file.name)

      const optionExtraContext = options.extraContext && typeof options.extraContext === 'object'
        ? { ...options.extraContext }
        : {}
      const detailScopedClaimNo = String(
        linkedRequest.value?.documentNo ||
          linkedRequest.value?.id ||
          ''
      ).trim()
      const initialExtraContext = detailScopedClaimId
        ? {
            ...optionExtraContext,
            draft_claim_id: detailScopedClaimId,
            selected_claim_id: detailScopedClaimId,
            selected_claim_no: detailScopedClaimNo,
            detail_scope_claim_id: detailScopedClaimId
          }
        : optionExtraContext
      const selectedBusinessTimeContext = isKnowledgeSession.value ? null : buildComposerBusinessTimeContext()
      const extraContext = isKnowledgeSession.value
        ? initialExtraContext
        : mergeBusinessTimeIntoExtraContext(initialExtraContext, selectedBusinessTimeContext)
      const reviewAction = String(extraContext.review_action || '').trim()
      const attachmentAssociationConfirmed = Boolean(
        options.associationConfirmed ||
          extraContext.attachment_association_confirmed ||
          reviewAction === 'link_to_existing_draft' ||
          detailScopedUpload
      )
      const hasSelectedExpenseType = Boolean(
        extraContext.expense_scene_selection ||
          String(extraContext.review_form_values?.expense_type || extraContext.review_form_values?.reimbursement_type || '').trim()
      )
      const hasConfirmedExpenseIntent = Boolean(extraContext.expense_intent_confirmed)
      const waitForExpenseIntentConfirmation = shouldRequestExpenseIntentConfirmation(rawText, {
        sessionType: activeSessionType.value,
        attachmentCount: files.length,
        reviewAction,
        hasSelectedExpenseType,
        hasConfirmedExpenseIntent
      })
      const waitForExpenseSceneSelection = !waitForExpenseIntentConfirmation && shouldRequestExpenseSceneSelection(rawText, {
        sessionType: activeSessionType.value,
        attachmentCount: files.length,
        reviewAction,
        hasSelectedExpenseType
      })
      const reviewAttachmentNames = extractReviewAttachmentNames(activeReviewPayload.value)
      const userText =
        String(options.userText || '').trim() ||
        resolveComposerDisplaySubmitText(rawText) ||
        rawText ||
        (isKnowledgeSession.value
          ? `我上传了 ${fileNames.length} 份附件，请帮我回答相关财务问题。`
          : resolvedUploadDisposition === 'continue_existing'
            ? `继续上传 ${fileNames.length} 份票据，并归集到当前单据。`
            : resolvedUploadDisposition === 'new_document'
              ? `新上传 ${fileNames.length} 份票据，请单独建立报销单。`
              : `我上传了 ${fileNames.length} 份票据，请帮我识别并整理报销建议。`)

      if (shouldUseBudgetCompileReport(rawText, { sessionType: activeSessionType.value }) && !reviewAction) {
        return handleBudgetCompileReportSubmit({
          adjustComposerTextareaHeight,
          clearAttachedFiles,
          completeFlowStep,
          composerBusinessTimeDraftTouched,
          composerBusinessTimeTags,
          composerDraft,
          createMessage,
          currentUser,
          fileInputRef,
          fileNames,
          messages,
          nextTick,
          options,
          persistSessionState,
          rawText,
          replaceMessage,
          resetFlowRun,
          scrollToBottom,
          startFlowStep,
          submitting,
          userText
        })
      }

      const scopeGuard = resolveAssistantScopeGuard(rawText, activeSessionType.value, {
        attachmentCount: files.length,
        hasActiveReviewPayload: Boolean(activeReviewPayload.value),
        reviewAction
      })
      if (scopeGuard && !systemGenerated && !reviewAction && !options.skipScopeGuard) {
        if (!options.skipUserMessage) {
          messages.value.push(createMessage('user', userText, fileNames))
        }
        messages.value.push(createMessage('assistant', scopeGuard.text, [], {
          meta: scopeGuard.meta,
          suggestedActions: scopeGuard.suggestedActions
        }))
        composerDraft.value = ''
        composerBusinessTimeTags.value = []
        composerBusinessTimeDraftTouched.value = false
        nextTick(() => {
          adjustComposerTextareaHeight()
          scrollToBottom()
        })
        persistSessionState()
        return null
      }

      if (shouldUseLocalApplicationPreview(rawText, {
        sessionType: activeSessionType.value,
        attachmentCount: files.length,
        reviewAction,
        systemGenerated
      })) {
        const intentStartedAt = Date.now()
        const reviewStartedAt = intentStartedAt
        resetFlowRun()
        startFlowStep('intent', {
          title: '业务意图识别',
          tool: 'ontology.intent_detection',
          detail: '正在识别是否为费用申请事项...'
        })
        startFlowStep('application-review-preview', {
          title: '申请信息核对',
          tool: 'ontology.application_review',
          detail: '正在进行申请信息模型复核...'
        })
        if (!options.skipUserMessage) {
          messages.value.push(createMessage('user', userText, fileNames))
        }
        const pendingMessage = createMessage(
          'assistant',
          '正在进行申请信息模型复核。本步骤只识别意图和抽取字段，不会创建、更新或保存草稿。',
          [],
          {
            meta: ['模型复核中']
          }
        )
        messages.value.push(pendingMessage)
        composerDraft.value = ''
        composerBusinessTimeTags.value = []
        composerBusinessTimeDraftTouched.value = false
        clearAttachedFiles()
        if (fileInputRef.value) {
          fileInputRef.value.value = ''
        }
        nextTick(() => {
          adjustComposerTextareaHeight()
          scrollToBottom()
        })
        persistSessionState()

        submitting.value = true
        try {
          const { applicationPreview, meta } = await buildApplicationPreviewWithModelReview(rawText)
          const reviewStatus = String(meta?.[1] || '').trim()
          completeFlowStep('intent', '已识别为费用申请事项', Date.now() - intentStartedAt)
          completeFlowStep(
            'application-review-preview',
            reviewStatus === '模型复核完成'
              ? '模型复核完成，已生成申请核对表'
              : reviewStatus === '模型复核失败'
                ? '模型复核失败，已生成临时核对表'
                : '模型未返回稳定结果，已完成规则兜底核对',
            Date.now() - reviewStartedAt
          )
          replaceMessage(pendingMessage.id, createMessage(
            'assistant',
            buildLocalApplicationPreviewMessage(applicationPreview),
            [],
            {
              meta,
              applicationPreview
            }
          ))
          persistSessionState()
          nextTick(scrollToBottom)
        } finally {
          submitting.value = false
        }
        return null
      }

      const hasUnsavedReviewDraft = Boolean(
        !isKnowledgeSession.value &&
        files.length &&
        activeReviewPayload.value &&
        !String(draftClaimId.value || '').trim() &&
        !detailScopedClaimId &&
        !resolvedUploadDisposition &&
        !options.skipDraftAssociationPrompt &&
        !reviewAction
      )
      if (hasUnsavedReviewDraft) {
        const associationId = createPendingAttachmentAssociationId()
        pendingAttachmentAssociations.set(associationId, {
          files,
          fileNames,
          filePreviews: buildComposerFilePreviews(files),
          extraContext
        })
        resetFlowRun()
        if (!options.skipUserMessage) {
          messages.value.push(createMessage('user', userText, fileNames))
        }
        messages.value.push(createMessage(
          'assistant',
          buildUnsavedDraftAttachmentConfirmationMessage({ fileNames }),
          [],
          {
            meta: ['等待确认保存并归集'],
            pendingAttachmentAssociation: {
              id: associationId,
              mode: 'save_then_associate',
              status: 'pending',
              fileNames
            }
          }
        ))
        nextTick(scrollToBottom)
        persistSessionState()
        return null
      }

      if (
        !isKnowledgeSession.value &&
        files.length &&
        !resolvedUploadDisposition &&
        !options.skipDraftAssociationPrompt &&
        !reviewAction
      ) {
        try {
          const claims = await fetchExpenseClaims()
          const queryPayload = buildDraftAssociationQueryPayload(claims)
          if (queryPayload?.records?.length) {
            resetFlowRun()
            if (!options.skipUserMessage) {
              messages.value.push(createMessage('user', userText, fileNames))
            }
            messages.value.push(createMessage(
              'assistant',
              `我找到 ${queryPayload.records.length} 张可关联的草稿/待补单据。请先选择这批附件要归集到哪张单据，我再开始识别附件。`,
              [],
              {
                meta: ['等待选择关联单据'],
                queryPayload
              }
            ))
            composerDraft.value = ''
            composerBusinessTimeTags.value = []
            composerBusinessTimeDraftTouched.value = false
            nextTick(() => {
              adjustComposerTextareaHeight()
              scrollToBottom()
            })
            persistSessionState()
            return null
          }
        } catch (error) {
          console.warn('Failed to load draft claims before attachment recognition:', error)
          resetFlowRun()
          if (!options.skipUserMessage) {
            messages.value.push(createMessage('user', userText, fileNames))
          }
          messages.value.push(createMessage(
            'assistant',
            '我暂时没能查询到可关联的草稿/待补单据，所以先不识别这批附件。请稍后重试，或从对应草稿进入后继续上传票据。',
            [],
            {
              meta: ['单据查询失败']
            }
          ))
          nextTick(scrollToBottom)
          persistSessionState()
          toast(error?.message || '查询可关联草稿失败，请稍后重试。')
          return null
        }
      }

      if (!appendToCurrentFlow) {
        resetFlowRun()
      } else {
        clearFlowSimulationTimers()
      }
      if (rawText && !reviewAction) {
        startFlowStep('intent', '正在识别业务意图...')
        if (waitForExpenseIntentConfirmation) {
          startExpenseIntentConfirmationFlowPreview(rawText)
        } else if (waitForExpenseSceneSelection) {
          startExpenseSceneSelectionFlowPreview(rawText)
        } else {
          startSemanticFlowPreview(rawText, { attachmentCount: files.length })
        }
      }

      const filePreviews = buildComposerFilePreviews(files)
      rememberFilePreviews(filePreviews)

      // 只有在非静默模式下才添加用户消息
      if (!options.skipUserMessage) {
        messages.value.push(createMessage('user', userText, fileNames))
      }

      if (waitForExpenseIntentConfirmation) {
        messages.value.push(createMessage('assistant', buildExpenseIntentConfirmationMessage(rawText), [], {
          meta: ['等待确认意图'],
          suggestedActions: buildExpenseIntentConfirmationActions(rawText)
        }))
        composerDraft.value = ''
        composerBusinessTimeTags.value = []
        composerBusinessTimeDraftTouched.value = false
        clearAttachedFiles()
        if (fileInputRef.value) {
          fileInputRef.value.value = ''
        }
        nextTick(() => {
          adjustComposerTextareaHeight()
          scrollToBottom()
        })
        return null
      }

      if (waitForExpenseSceneSelection) {
        messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(rawText), [], {
          meta: ['等待选择场景'],
          suggestedActions: buildExpenseSceneSelectionActions(rawText)
        }))
        composerDraft.value = ''
        composerBusinessTimeTags.value = []
        composerBusinessTimeDraftTouched.value = false
        clearAttachedFiles()
        if (fileInputRef.value) {
          fileInputRef.value.value = ''
        }
        nextTick(() => {
          adjustComposerTextareaHeight()
          scrollToBottom()
        })
        return null
      }

      const pendingMessage = createMessage(
        'assistant',
        options.pendingText || (
          isKnowledgeSession.value
            ? '正在整理财务知识答案...'
            : activeSessionType.value === 'application'
              ? '正在识别并整理申请核对信息...'
              : activeSessionType.value === 'approval'
                ? '正在查询审核上下文并整理风险提示...'
                : '正在识别并整理右侧核对信息...'
        ),
        [],
        {
          meta: ['处理中']
        }
      )
      messages.value.push(pendingMessage)

      composerDraft.value = ''
      composerBusinessTimeTags.value = []
      composerBusinessTimeDraftTouched.value = false
      clearAttachedFiles()
      if (fileInputRef.value) {
        fileInputRef.value.value = ''
      }
      nextTick(adjustComposerTextareaHeight)

      submitting.value = true
      nextTick(scrollToBottom)

      let responsePayload = null

      try {
        const user = currentUser.value || {}
        let ocrPayload = null
        let ocrSummary = ''
        let ocrDocuments = []
        let ocrFilePreviews = []
        const recognizedAttachmentData = normalizeRecognizedAttachmentData(options.recognizedAttachmentData)

        if (files.length) {
          const ocrStartedAt = Date.now()
          startFlowStep('ocr', { detail: `正在识别 ${files.length} 份附件...`, startedAt: ocrStartedAt })
          if (recognizedAttachmentData) {
            ocrPayload = recognizedAttachmentData.ocrPayload
            ocrSummary = recognizedAttachmentData.ocrSummary || buildOcrSummaryFromDocuments(recognizedAttachmentData.ocrDocuments)
            ocrDocuments = [...recognizedAttachmentData.ocrDocuments]
            ocrFilePreviews = [...recognizedAttachmentData.ocrFilePreviews]
            rememberFilePreviews(ocrFilePreviews)
            completeFlowStep('ocr', `复用已确认的 ${ocrDocuments.length || files.length} 张票据识别结果`, Date.now() - ocrStartedAt)
          } else {
            try {
              ocrPayload = await recognizeOcrFiles(files, {
                timeoutMs: 90000,
                timeoutMessage: '票据 OCR 识别超时，已继续使用附件名称处理。'
              })
              ocrSummary = buildOcrSummary(ocrPayload)
              ocrDocuments = normalizeOcrDocuments(ocrPayload)
              ocrFilePreviews = buildOcrFilePreviews(ocrPayload)
              rememberFilePreviews(ocrFilePreviews)
              completeFlowStep('ocr', `识别到 ${ocrDocuments.length || files.length} 张票据`, Date.now() - ocrStartedAt)
            } catch (error) {
              console.warn('OCR request failed:', error)
              completeFlowStep('ocr', 'OCR识别失败，已继续使用附件名称', Date.now() - ocrStartedAt)
            }
          }

          if (resolvedUploadDisposition === 'continue_existing') {
            replaceMessage(pendingMessage.id, {
              ...pendingMessage,
              text: attachmentAssociationConfirmed
                ? '票据识别已完成，正在把本次附件归集到已选择的草稿...'
                : '票据识别已完成，正在整理归集前确认信息...',
              meta: attachmentAssociationConfirmed ? ['正在归集'] : ['等待确认归集']
            })
            persistSessionState()
          }
        }

        const associationTargetClaimId = String(extraContext.draft_claim_id || draftClaimId.value || '').trim()
        const associationTargetClaimNo = String(
          extraContext.selected_claim_no ||
            extraContext.draft_claim_no ||
            ''
        ).trim()
        if (
          files.length &&
          resolvedUploadDisposition === 'continue_existing' &&
          associationTargetClaimId &&
          !attachmentAssociationConfirmed
        ) {
          const associationId = createPendingAttachmentAssociationId()
          const pendingAssociation = {
            id: associationId,
            status: 'pending',
            claimId: associationTargetClaimId,
            claimNo: associationTargetClaimNo,
            fileNames
          }
          pendingAttachmentAssociations.set(associationId, {
            files,
            fileNames,
            ocrPayload,
            ocrSummary,
            ocrDocuments,
            ocrFilePreviews,
            filePreviews,
            claimId: associationTargetClaimId,
            claimNo: associationTargetClaimNo,
            extraContext: {
              ...extraContext,
              draft_claim_id: associationTargetClaimId,
              selected_claim_id: associationTargetClaimId,
              selected_claim_no: associationTargetClaimNo
            }
          })
          replaceMessage(pendingMessage.id, createMessage(
            'assistant',
            buildAttachmentAssociationConfirmationMessage({
              claimNo: associationTargetClaimNo,
              fileNames,
              ocrDocuments
            }),
            [],
            {
              meta: ['等待确认归集'],
              pendingAttachmentAssociation: pendingAssociation
            }
          ))
          persistSessionState()
          nextTick(scrollToBottom)
          return null
        }

        let effectiveFileNames = [...fileNames]
        let effectiveOcrDocuments = [...ocrDocuments]
        let effectiveOcrSummary = ocrSummary

        if (resolvedUploadDisposition === 'continue_existing') {
          extraContext.review_action = 'link_to_existing_draft'
          const inheritedReviewContext = buildReviewFormContextFromPayload(
            activeReviewPayload.value,
            reviewInlineForm.value
          )
          if (inheritedReviewContext.review_form_values) {
            extraContext.review_form_values = {
              ...inheritedReviewContext.review_form_values,
              ...(extraContext.review_form_values && typeof extraContext.review_form_values === 'object'
                ? extraContext.review_form_values
                : {})
            }
          }
          if (inheritedReviewContext.business_time_context && !extraContext.business_time_context) {
            extraContext.business_time_context = inheritedReviewContext.business_time_context
          }
          effectiveFileNames = mergeUploadAttachmentNames(reviewAttachmentNames, fileNames)
          effectiveOcrDocuments = mergeUploadOcrDocuments(
            buildOcrDocumentsFromReviewPayload(activeReviewPayload.value),
            ocrDocuments
          )
          effectiveOcrSummary = buildOcrSummaryFromDocuments(effectiveOcrDocuments)
        } else if (resolvedUploadDisposition === 'new_document') {
          extraContext.review_action = 'create_new_claim_from_documents'
        }

        startExpenseClaimDraftFlowStep(String(extraContext.review_action || '').trim(), {
          attachmentCount: effectiveFileNames.length,
          waitForSceneSelection: waitForExpenseSceneSelection
        })

        const backendMessage = buildBackendMessage(rawText, effectiveFileNames, effectiveOcrSummary)
        const orchestratorOptions = isKnowledgeSession.value
          ? {
              timeoutMs: 75000,
              timeoutMessage: '知识问答仍在检索整理，已停止等待。请稍后重试，或补充制度名称、费用类型等限定条件。'
            }
          : {
              timeoutMs: 120000,
              timeoutMessage: '票据归集处理超时，当前仍停留在原草稿，请稍后重试或重新选择附件。'
            }

        const payload = await runOrchestrator(
          {
            source: 'user_message',
            user_id: user.username || user.name || 'anonymous',
            conversation_id: conversationId.value || null,
            message: backendMessage,
            context_json: {
              role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
              is_admin: Boolean(user.isAdmin),
              name: user.name || '',
              role: user.role || '',
              department: user.department || user.departmentName || '',
              department_name: user.department || user.departmentName || '',
              position: user.position || '',
              grade: user.grade || '',
              employee_no: user.employeeNo || user.employee_no || '',
              manager_name: user.managerName || user.manager_name || '',
              employee_location: user.location || '',
              cost_center: user.costCenter || user.cost_center || '',
              finance_owner_name: user.financeOwnerName || user.finance_owner_name || '',
              employee_risk_profile: user.riskProfile && typeof user.riskProfile === 'object' ? user.riskProfile : {},
              ...buildClientTimeContext(),
              session_type: activeSessionType.value,
              entry_source: props.entrySource,
              user_input_text: systemGenerated ? '' : rawText,
              attachment_names: effectiveFileNames,
              attachment_count: effectiveFileNames.length,
              draft_claim_id: isKnowledgeSession.value ? undefined : draftClaimId.value || undefined,
              ocr_summary: effectiveOcrSummary,
              ocr_documents: effectiveOcrDocuments,
              ...(linkedRequest.value && !isKnowledgeSession.value ? { request_context: linkedRequest.value } : {}),
              ...extraContext
            }
          },
          orchestratorOptions
        )
        responsePayload = payload
        flowRunId.value = String(payload?.run_id || '').trim()
        let flowRunDetail = null
        if (flowRunId.value) {
          flowRunDetail = await refreshFlowRunDetail()
        }

        conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value
        draftClaimId.value =
          isKnowledgeSession.value
            ? ''
            : String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value

        const reviewActionResult = String(extraContext.review_action || '').trim()
        const resultClaimNo = String(payload?.result?.draft_payload?.claim_no || '').trim()
        const fallbackAnswer = reviewActionResult === 'link_to_existing_draft'
          ? (resultClaimNo ? `已将本次上传的票据关联到草稿 ${resultClaimNo}。` : '已将本次上传的票据关联到现有草稿。')
          : '智能体已完成处理。'
        const assistantMessage = createMessage('assistant', payload?.result?.answer || payload?.result?.message || fallbackAnswer, [], {
          meta: buildMessageMeta(payload, effectiveFileNames),
          citations: Array.isArray(payload?.result?.citations) ? payload.result.citations : [],
          suggestedActions: Array.isArray(payload?.result?.suggested_actions)
            ? payload.result.suggested_actions
            : [],
          queryPayload: normalizeExpenseQueryPayload(payload?.result?.query_payload),
          draftPayload: payload?.result?.draft_payload || null,
          reviewPayload: payload?.result?.review_payload || null,
          reviewPanelScope: resolveReviewPanelScope({
            reviewPayload: payload?.result?.review_payload || null,
            reviewAction: reviewActionResult,
            fileCount: files.length,
            rawText
          }),
          riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
        })
        replaceMessage(pendingMessage.id, assistantMessage)
        const nextInsight = buildAgentInsight(
          payload,
          effectiveFileNames,
          mergeFilePreviews(filePreviews, ocrFilePreviews)
        )
        if (nextInsight.agent) {
          nextInsight.agent.reviewPanelScope = assistantMessage.reviewPanelScope
        }
        currentInsight.value = nextInsight
        completeFlowResult(payload, flowRunDetail)
        persistSessionState()
        nextTick(scrollToBottom)

        const resolvedDraftClaimId = String(payload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
        if (!isKnowledgeSession.value && resolvedDraftClaimId && files.length) {
          void syncComposerFilesToDraft(resolvedDraftClaimId, files)
            .then(() => {
              persistSessionState()
            })
            .catch((error) => {
              console.warn('Failed to persist composer attachments to draft claim:', error)
              toast(error?.message || '票据已归集到草稿，但附件原件保存失败，请在单据详情中重新上传。')
            })
        }
      } catch (error) {
        clearFlowSimulationTimers()
        failCurrentFlowStep(error)
        replaceMessage(
          pendingMessage.id,
          createMessage(
            'assistant',
            error?.message || '无法连接后端 Orchestrator，请稍后重试。',
            [],
            {
              meta: ['调用失败']
            }
          )
        )
        currentInsight.value = buildErrorInsight(error, fileNames)
        persistSessionState()
      } finally {
        submitting.value = false
        composerUploadIntent.value = ''
        nextTick(scrollToBottom)
      }

      return responsePayload
    }


  return {
    confirmPendingAttachmentAssociationInternal: confirmPendingAttachmentAssociation,
    submitComposerInternal: submitComposer
  }
}
