import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

import AuditAssetList from '../../components/audit/AuditAssetList.vue'
import AuditJsonRiskRuleDetail from '../../components/audit/AuditJsonRiskRuleDetail.vue'
import AuditRuleDialogs from '../../components/audit/AuditRuleDialogs.vue'
import AuditSpreadsheetChangeDrawer from '../../components/audit/AuditSpreadsheetChangeDrawer.vue'
import AuditSpreadsheetRuleDetail from '../../components/audit/AuditSpreadsheetRuleDetail.vue'
import AuditVersionTimelineDrawer from '../../components/audit/AuditVersionTimelineDrawer.vue'
import { fetchEmployees } from '../../services/employees.js'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
import {
  activateAgentAsset,
  createAgentAssetReview,
  createAgentAssetVersion,
  deleteAgentAsset,
  fetchAgentAssetDetail,
  fetchAgentAssets,
  fetchAgentAssetSpreadsheetBlob,
  fetchAgentAssetSpreadsheetChangeRecords,
  fetchAgentAssetSpreadsheetOnlyOfficeConfig,
  fetchAgentAssetRuleJson,
  fetchAgentAssetVersionTimeline,
  fetchAgentRuns,
  generateRiskRuleAsset,
  publishRiskRuleAsset,
  returnRiskRuleAsset,
  saveAgentAssetRuleJson,
  importAgentAssetSpreadsheetContent,
  restoreAgentAssetVersion,
  setRiskRuleAssetEnabled,
  updateAgentAsset
} from '../../services/agentAssets.js'
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
import { isFinanceUser, isManagerUser, isPlatformAdminUser } from '../../utils/accessControl.js'
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
import {
  buildReviewNote,
  buildRuleConfigPayload,
  buildSpreadsheetChangeRecordKey,
  filterAuditAssets,
  incrementVersion
} from './auditViewRuntimeModel.js'

import {
  TAB_META,
  STATUS_OPTIONS,
  ENABLED_STATE_OPTIONS,
  ONLINE_STATE_OPTIONS,
  RISK_SCENARIO_OPTIONS,
  normalizeText,
  readConfigJson,
  resolveRuleTemplateLabel,
  resolveTimelineEventMeta,
  formatDateTime,
  formatSpreadsheetChangeSummary,
  resolveDiffChangeMeta,
  resolveDomainLabel,
  resolveStatusMeta,
  resolveReviewMeta,
  buildListItem,
  buildDetailViewModel,
  applyRiskRuleJsonState,
  resolveRiskRuleDescription,
  buildPreviewRuleDetail,
  buildDefaultRuntimeRule,
  stringifyRuntimeRule,
  parseRuntimeRuleText,
  buildMarkdownVersionContent
} from './auditViewModel.js'
import {
  createDefaultRiskRuleForm,
  RISK_RULE_BUSINESS_STAGE_OPTIONS,
  RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
  RISK_RULE_LEVEL_OPTIONS
} from './auditViewRiskRuleModel.js'

export default {
  name: 'AuditView',
  components: {
    AuditAssetList,
    AuditJsonRiskRuleDetail,
    AuditRuleDialogs,
    AuditSpreadsheetChangeDrawer,
    AuditSpreadsheetRuleDetail,
    AuditVersionTimelineDrawer,
    TableLoadingState
  },
  emits: ['detail-open-change'],
  setup(_, { emit }) {
    const { toast } = useToast()
    const { currentUser } = useSystemState()

    const tabs = Object.entries(TAB_META).map(([id, meta]) => ({
      id,
      label: meta.label
    }))

    const activeType = ref('financialRules')
    const selectedSkill = ref(null)
    const versionSwitchTarget = ref(null)
    const keyword = ref('')
    const activeFilterPopover = ref('')
    const selectedDomain = ref('')
    const selectedOwner = ref('')
    const selectedRiskLevel = ref('')
    const selectedStatus = ref('')
    const selectedRiskScenario = ref('')
    const selectedOnlineState = ref('')
    const selectedEnabledState = ref('')
    const loading = ref(false)
    const errorMessage = ref('')
    const detailLoading = ref(false)
    const detailError = ref('')
    const actionState = ref('')
    const reviewSubmitOpen = ref(false)
    const reviewSubmitVersion = ref('')
    const reviewSubmitReviewer = ref('')
    const reviewSubmitReviewerLoading = ref(false)
    const reviewSubmitReviewerOptions = ref([])
    const riskRuleAttachmentOptions = [
      { label: '是', value: true },
      { label: '否', value: false }
    ]
    const riskRuleCreateOpen = ref(false)
    const riskRuleCreateForm = ref(createDefaultRiskRuleForm())
    const riskRuleTestOpen = ref(false)
    const riskRuleDeleteOpen = ref(false)
    const riskRuleReturnOpen = ref(false)
    const riskRulePublishOpen = ref(false)
    const riskRuleReturnNote = ref('')
    const runLoading = ref(false)
    const runs = ref([])
    const spreadsheetUploadInput = ref(null)
    const spreadsheetOnlyOfficeLoading = ref(false)
    const spreadsheetOnlyOfficeError = ref('')
    const spreadsheetOnlyOfficeEditor = ref(null)
    const spreadsheetOnlyOfficeReady = ref(false)
    const spreadsheetOnlyOfficeHostId = ref('audit-rule-onlyoffice')
    const versionTimelineOpen = ref(false)
    const versionTimelineLoading = ref(false)
    const versionTimelineError = ref('')
    const versionTimelineItems = ref([])
    const spreadsheetChangeRecordsByAsset = ref({})
    const spreadsheetChangeDetailOpen = ref(false)
    const selectedSpreadsheetChangeRecord = ref(null)
    let spreadsheetOnlyOfficeMountSeq = 0
    let spreadsheetOnlyOfficeLoadTimer = null
    let spreadsheetOnlyOfficeHadLocalEdits = false
    let spreadsheetOnlyOfficeSyncSeq = 0
    let spreadsheetOnlyOfficeChangePollTimer = null
    const riskRuleGenerationPollTimers = new Map()
    const assetBuckets = ref({
      financialRules: [],
      riskRules: [],
      mcp: []
    })

    const isAdmin = computed(() => isPlatformAdminUser(currentUser.value))
    const isRuleManager = computed(() => isManagerUser(currentUser.value))
    const isFinance = computed(() => isFinanceUser(currentUser.value))
    const activeMeta = computed(() => TAB_META[activeType.value])
    const activeTabLabel = computed(() => activeMeta.value.label)
    const currentAssets = computed(() => assetBuckets.value[activeType.value] || [])
    const searchPlaceholder = computed(() => activeMeta.value.searchPlaceholder)
    const createButtonLabel = computed(() => activeMeta.value.createButtonLabel)
    const hintText = computed(() => activeMeta.value.hintText)
    const tableColumns = computed(() => activeMeta.value.tableColumns)
    const showRuntimeColumn = computed(() => activeMeta.value.showRuntimeColumn !== false)
    const showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false)
    const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
    const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
    const showOnlineColumn = computed(() => false)
    const showEnabledColumn = computed(() => activeMeta.value.showEnabledColumn === true)
    const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
    const selectedSkillUsesSpreadsheet = computed(
      () => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
    )
    const selectedSkillUsesJsonRisk = computed(
      () => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule)
    )
    const canManageSelected = computed(
      () => isRuleManager.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
    )
    const canAdminOperateSelected = computed(
      () => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
    )
    const canEditSelected = computed(
      () =>
        Boolean(selectedSkill.value) &&
        !selectedSkill.value?.isPreviewMock &&
        (isAdmin.value || isFinance.value)
    )
    const canCreateRiskRule = computed(
      () => activeType.value === 'riskRules' && isRuleManager.value && !detailBusy.value
    )
    const latestRiskRuleTestSummary = computed(() => selectedSkill.value?.latestTestSummary || null)
    const riskRuleTestPassed = computed(() => Boolean(latestRiskRuleTestSummary.value?.test_passed))
    const riskRuleInReview = computed(
      () => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'review'
    )
    const riskRuleGenerationBusy = computed(
      () => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'generating'
    )
    const riskRuleGenerationFailed = computed(
      () => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'failed'
    )
    const canOpenRiskRuleTest = computed(
      () =>
        selectedSkillUsesJsonRisk.value &&
        canAdminOperateSelected.value &&
        Boolean(selectedSkill.value?.id) &&
        !riskRuleGenerationBusy.value &&
        !riskRuleGenerationFailed.value
    )
    const canDeleteRiskRule = computed(
      () =>
        selectedSkillUsesJsonRisk.value &&
        canAdminOperateSelected.value &&
        Boolean(selectedSkill.value?.id) &&
        !normalizeText(selectedSkill.value?.publishedVersion).replace('-', '')
    )
    const canOpenRiskRuleReviewSubmit = computed(
      () => false
    )
    const canSubmitRiskRuleReview = computed(
      () =>
        canOpenRiskRuleReviewSubmit.value &&
        riskRuleTestPassed.value
    )
    const canReturnRiskRule = computed(
      () => false
    )
    const canPublishRiskRule = computed(
      () =>
        false
    )
    const canToggleRiskRuleEnabled = computed(
      () => selectedSkillUsesJsonRisk.value && canManageSelected.value
    )
    const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
    const canEditMarkdown = computed(() => selectedSkillIsRule.value && canEditSelected.value)
    const isDisplayingWorkingVersion = computed(
      () => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
    )
    const canSubmitReview = computed(
      () =>
        !selectedSkillUsesJsonRisk.value &&
        canEditSelected.value &&
        selectedSkillIsRule.value &&
        isDisplayingWorkingVersion.value
    )
    const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
    const canReviewSelected = computed(
      () => canManageSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
    )
    const canUploadSpreadsheet = computed(
      () =>
        canEditSelected.value &&
        selectedSkillUsesSpreadsheet.value &&
        !detailBusy.value
    )
    const canDownloadSpreadsheet = computed(
      () =>
        selectedSkillUsesSpreadsheet.value &&
        Boolean(selectedSkill.value?.id) &&
        !detailBusy.value
    )
    const canEditSpreadsheetInline = computed(
      () =>
        selectedSkillUsesSpreadsheet.value &&
        (selectedSkill.value?.isPreviewMock || canEditSelected.value)
    )
    const selectedSpreadsheetFileName = computed(
      () =>
        normalizeText(selectedSkill.value?.ruleDocument?.file_name) || '未上传规则表'
    )
    const selectedSpreadsheetModeLabel = computed(() => {
      if (selectedSkill.value?.isPreviewMock) {
        return canEditSpreadsheetInline.value ? '可编辑' : '只读'
      }
      return canEditSpreadsheetInline.value ? '在线可编辑' : '只读'
    })
    const selectedVersionTimelineItems = computed(() =>
      versionTimelineItems.value.map((item) => ({
        ...item,
        meta: resolveTimelineEventMeta(item.event_type),
        timeLabel: formatDateTime(item.event_time)
      }))
    )
    const selectedSpreadsheetChangeRecords = computed(() => {
      if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id) {
        return []
      }
      return (spreadsheetChangeRecordsByAsset.value[selectedSkill.value.id] || [])
        .filter((item) => item?.changed_at)
        .map((item) => {
          const sheetNames = [
            ...(Array.isArray(item.sheet_changes)
              ? item.sheet_changes.map((change) => normalizeText(change.sheet_name))
              : []),
            ...(Array.isArray(item.cell_changes)
              ? item.cell_changes.map((change) => normalizeText(change.sheet_name))
              : [])
          ].filter(Boolean)
          const changedSheetNames = [...new Set(sheetNames)]
          const previewChanges = Array.isArray(item.cell_changes) ? item.cell_changes.slice(0, 3) : []
          return {
            ...item,
            time: formatDateTime(item.changed_at),
            summary: formatSpreadsheetChangeSummary(item.summary),
            changeCountLabel: item.changed_cell_count
              ? `${item.changed_cell_count} 处改动`
              : `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
            changedSheetNames,
            sheetPreview: changedSheetNames.slice(0, 4),
            remainingSheetCount: Math.max(changedSheetNames.length - 4, 0),
            previewChanges,
            remainingChangeCount: Math.max((item.changed_cell_count || 0) - previewChanges.length, 0)
          }
        })
    })
    const selectedSpreadsheetChangeSheetRows = computed(() =>
      Array.isArray(selectedSpreadsheetChangeRecord.value?.sheet_changes)
        ? selectedSpreadsheetChangeRecord.value.sheet_changes.map((item) => ({
            ...item,
            meta: resolveDiffChangeMeta(item.change_type)
          }))
        : []
    )
    const selectedSpreadsheetChangeCellRows = computed(() =>
      Array.isArray(selectedSpreadsheetChangeRecord.value?.cell_changes)
        ? selectedSpreadsheetChangeRecord.value.cell_changes.map((item) => ({
            ...item,
            meta: resolveDiffChangeMeta(item.change_type)
          }))
        : []
    )
    const detailBusy = computed(() => Boolean(actionState.value))
    const showReviewNote = computed(
      () => selectedSkillIsRule.value && (selectedSkill.value?.reviewNote || selectedSkill.value?.reviewTimeLabel)
    )
    const domainOptions = computed(() => {
      const uniqueValues = [...new Set(currentAssets.value.map((item) => item.domainValue).filter(Boolean))]
      return [
        { value: '', label: '全部业务域' },
        ...uniqueValues.map((value) => ({
          value,
          label: resolveDomainLabel(value)
        }))
      ]
    })
    const ownerOptions = computed(() => {
      const uniqueOwners = [...new Set(currentAssets.value.map((item) => item.owner).filter(Boolean))]
      return [
        { value: '', label: '全部负责人' },
        ...uniqueOwners.map((value) => ({
          value,
          label: value
        }))
      ]
    })
    const riskLevelOptions = computed(() => [
      { value: '', label: '全部风险等级' },
      ...RISK_RULE_LEVEL_OPTIONS
    ])
    const selectedDomainLabel = computed(
      () => domainOptions.value.find((item) => item.value === selectedDomain.value)?.label || '业务域'
    )
    const selectedOwnerLabel = computed(
      () =>
        ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label ||
        '负责人'
    )
    const selectedRiskLevelLabel = computed(
      () =>
        riskLevelOptions.value.find((item) => item.value === selectedRiskLevel.value)?.label ||
        '风险等级'
    )
    const selectedStatusLabel = computed(
      () => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
    )
    const showRiskScenarioFilter = computed(() =>
      ['financialRules', 'riskRules'].includes(activeType.value)
    )
    const showOwnerFilter = computed(() => activeType.value !== 'riskRules')
    const showRiskLevelFilter = computed(() => activeType.value === 'riskRules')
    const showStatusFilter = computed(() => true)
    const showOnlineFilter = computed(() => false)
    const showEnabledFilter = computed(() => false)
    const selectedRiskScenarioLabel = computed(
      () =>
        RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
        '使用场景'
    )
    const selectedOnlineStateLabel = computed(
      () =>
        ONLINE_STATE_OPTIONS.find((item) => item.value === selectedOnlineState.value)?.label ||
        '是否上线'
    )
    const selectedEnabledStateLabel = computed(
      () =>
        ENABLED_STATE_OPTIONS.find((item) => item.value === selectedEnabledState.value)?.label ||
        '是否启用'
    )
    const activeFilterTokens = computed(() => {
      const tokens = []

      if (selectedDomain.value) {
        tokens.push(`业务域：${resolveDomainLabel(selectedDomain.value)}`)
      }
      if (showRiskScenarioFilter.value && selectedRiskScenario.value) {
        tokens.push(`使用场景：${selectedRiskScenario.value}`)
      }
      if (showStatusFilter.value && selectedStatus.value) {
        tokens.push(`状态：${resolveStatusMeta(selectedStatus.value).label}`)
      }
      if (showOnlineFilter.value && selectedOnlineState.value) {
        tokens.push(`是否上线：${selectedOnlineStateLabel.value}`)
      }
      if (showEnabledFilter.value && selectedEnabledState.value) {
        tokens.push(`是否启用：${selectedEnabledStateLabel.value}`)
      }
      if (showOwnerFilter.value && selectedOwner.value) {
        tokens.push(`负责人：${selectedOwner.value}`)
      }
      if (showRiskLevelFilter.value && selectedRiskLevel.value) {
        tokens.push(`风险等级：${selectedRiskLevelLabel.value}`)
      }
      if (keyword.value.trim()) {
        tokens.push(`搜索：${keyword.value.trim()}`)
      }

      return tokens
    })
    const auditEmptyState = computed(() => {
      const hasFilters = activeFilterTokens.value.length > 0
      const supportedFilters = [
        '业务域',
        ...(showOwnerFilter.value ? ['负责人'] : []),
        ...(showRiskLevelFilter.value ? ['风险等级'] : []),
        ...(showRiskScenarioFilter.value ? ['使用场景'] : []),
        ...(showStatusFilter.value ? ['状态'] : []),
        ...(showOnlineFilter.value ? ['是否上线'] : []),
        ...(showEnabledFilter.value ? ['是否启用'] : []),
        '关键词'
      ]

      if (!currentAssets.value.length) {
        return {
          eyebrow: `${activeTabLabel.value}资产`,
          title: `${activeTabLabel.value}列表暂时还是空的`,
          desc: `当前环境里还没有可展示的${activeTabLabel.value}资产。完成接入或同步后，会统一展示在这里。`,
          icon: 'mdi mdi-database-search-outline',
          actionLabel: '',
          actionIcon: '',
          tone: 'amber',
          artLabel: 'ASSET',
          tips: [
            '切换页签可查看其他资产类型',
            `支持按${supportedFilters.slice(0, -1).join('、')}和关键词做过滤`
          ]
        }
      }

      return {
        eyebrow: '筛选结果为空',
        title: `没有找到匹配的${activeTabLabel.value}`,
        desc: hasFilters
          ? `试试清空${supportedFilters.join('、')}筛选，再重新查看。`
          : `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
        icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
        actionLabel: hasFilters ? '清空筛选' : '',
        actionIcon: hasFilters ? 'mdi mdi-filter-remove-outline' : '',
        tone: hasFilters ? 'primary' : 'slate',
        artLabel: hasFilters ? 'FILTER' : 'QUEUE',
        tips: hasFilters
          ? [
              `${supportedFilters.join('、')}会叠加过滤`,
              showRiskScenarioFilter.value
                ? '可以换个规则名称或场景分类继续搜索'
                : '可以换个编码、名称或负责人关键词继续搜索'
            ]
          : ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
      }
    })
    const canActivateSelected = computed(() => {
      if (!selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
        return false
      }

      return (
        isDisplayingWorkingVersion.value &&
        selectedSkill.value?.reviewStatusValue === 'approved' &&
        selectedSkill.value?.workingVersion !== selectedSkill.value?.publishedVersion
      )
    })
    const activateBlockedReason = computed(() => {
      if (!selectedSkillIsRule.value) {
        return ''
      }
      if (selectedSkill.value?.isPreviewMock) {
        return '当前为页面预览态，暂不执行真实审核和上线。'
      }
      if (!canManageSelected.value) {
        return '仅高级财务人员可执行审核和上线。'
      }
      if (!isDisplayingWorkingVersion.value) {
        return '请先切回当前工作版本，再执行审核或上线。'
      }
      if (selectedSkill.value?.workingVersion === selectedSkill.value?.publishedVersion) {
        return '当前工作版本已经是线上版本。'
      }
      if (selectedSkill.value?.reviewStatusValue !== 'approved') {
        return '当前规则版本未审核通过，不能上线。'
      }
      return ''
    })
    const visibleSkills = computed(() =>
      filterAuditAssets(currentAssets.value, {
        keyword: keyword.value,
        selectedDomain: selectedDomain.value,
        selectedOwner: selectedOwner.value,
        selectedRiskLevel: selectedRiskLevel.value,
        selectedStatus: selectedStatus.value,
        selectedRiskScenario: selectedRiskScenario.value,
        selectedOnlineState: selectedOnlineState.value,
        selectedEnabledState: selectedEnabledState.value,
        showStatusFilter: showStatusFilter.value,
        showRiskScenarioFilter: showRiskScenarioFilter.value,
        showOnlineFilter: showOnlineFilter.value,
        showEnabledFilter: showEnabledFilter.value
      })
    )

    watch(
      selectedSkill,
      (value) => {
        emit('detail-open-change', Boolean(value))
      },
      { immediate: true }
    )

    watch(
      () => [
        selectedSkill.value?.id || '',
        selectedSkill.value?.loading ? '1' : '0',
        selectedSkill.value?.usesSpreadsheetRule ? '1' : '0'
      ],
      async () => {
        if (!selectedSkillUsesSpreadsheet.value || selectedSkill.value?.loading) {
          destroySpreadsheetOnlyOfficeEditor()
          spreadsheetOnlyOfficeError.value = ''
          spreadsheetOnlyOfficeLoading.value = false
          return
        }
        await mountSpreadsheetOnlyOfficeEditor()
      }
    )

    watch(activeType, () => {
      destroySpreadsheetOnlyOfficeEditor()
      selectedSkill.value = null
      versionSwitchTarget.value = null
      resetFilters()
      loadAssets({ force: true }).catch((error) => {
        errorMessage.value = error?.message || '资产数据加载失败，请稍后重试。'
      })
    })

    function resetFilters() {
      keyword.value = ''
      selectedDomain.value = ''
      selectedOwner.value = ''
      selectedRiskLevel.value = ''
      selectedStatus.value = ''
      selectedRiskScenario.value = ''
      selectedOnlineState.value = ''
      selectedEnabledState.value = ''
      activeFilterPopover.value = ''
    }

    function handleAuditEmptyAction() {
      if (!currentAssets.value.length || !activeFilterTokens.value.length) {
        loadAssets({ force: true }).catch(() => {})
        return
      }

      resetFilters()
    }

    function toggleFilterPopover(name) {
      activeFilterPopover.value = activeFilterPopover.value === name ? '' : name
    }

    function closeFilterPopover() {
      activeFilterPopover.value = ''
    }

    function selectFilter(name, value) {
      if (name === 'domain') {
        selectedDomain.value = value
      }
      if (name === 'owner') {
        selectedOwner.value = value
      }
      if (name === 'riskLevel') {
        selectedRiskLevel.value = value
      }
      if (name === 'status') {
        selectedStatus.value = value
      }
      if (name === 'riskScenario') {
        selectedRiskScenario.value = value
      }
      if (name === 'online') {
        selectedOnlineState.value = value
      }
      if (name === 'enabled') {
        selectedEnabledState.value = value
      }
      closeFilterPopover()
    }

    function handleDocumentClick(event) {
      const target = event.target
      if (!(target instanceof Element)) {
        closeFilterPopover()
        return
      }
      if (!target.closest('.picker-filter')) {
        closeFilterPopover()
      }
    }

    function resolveActor() {
      return currentUser.value?.name || currentUser.value?.username || 'system'
    }

    function openRiskRuleCreateDialog() {
      if (activeType.value !== 'riskRules') {
        return
      }
      riskRuleCreateForm.value = createDefaultRiskRuleForm()
      riskRuleCreateOpen.value = true
    }

    function closeRiskRuleCreateDialog() {
      if (riskRuleCreateBusy.value) {
        return
      }
      riskRuleCreateOpen.value = false
    }

    async function submitRiskRuleCreate() {
      if (!canCreateRiskRule.value || riskRuleCreateBusy.value) {
        return
      }
      const naturalLanguage = String(riskRuleCreateForm.value.natural_language || '').trim()
      const ruleTitle = String(riskRuleCreateForm.value.rule_title || '').trim()
      if (ruleTitle.length < 2) {
        toast('请输入至少 2 个字的规则标题。')
        return
      }
      if (naturalLanguage.length < 8) {
        toast('请至少输入 8 个字的风险规则描述。')
        return
      }

      actionState.value = 'generate-risk-rule'
      try {
        const detail = await generateRiskRuleAsset(
          {
            business_domain: 'expense',
            business_stage: riskRuleCreateForm.value.business_stage,
            expense_category: riskRuleCreateForm.value.expense_category,
            rule_title: ruleTitle,
            requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
            natural_language: naturalLanguage
          },
          { actor: resolveActor() }
        )
        riskRuleCreateOpen.value = false
        await refreshCurrentAssets()
        scheduleRiskRuleGenerationPoll(detail.id)
        toast('风险规则已进入后台生成，列表会先显示生成中。')
      } catch (error) {
        toast(error?.message || '风险规则生成失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    function stopRiskRuleGenerationPoll(assetId) {
      const timer = riskRuleGenerationPollTimers.get(assetId)
      if (timer) {
        window.clearTimeout(timer)
        riskRuleGenerationPollTimers.delete(assetId)
      }
    }

    function scheduleRiskRuleGenerationPoll(assetId, attempt = 0) {
      const normalizedAssetId = normalizeText(assetId)
      if (!normalizedAssetId) {
        return
      }
      stopRiskRuleGenerationPoll(normalizedAssetId)
      const timer = window.setTimeout(async () => {
        try {
          await refreshCurrentAssets()
          const latest = (assetBuckets.value.riskRules || []).find((item) => item.id === normalizedAssetId)
          if (!latest || latest.statusValue !== 'generating' || attempt >= 59) {
            riskRuleGenerationPollTimers.delete(normalizedAssetId)
            return
          }
          scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
        } catch {
          if (attempt < 59) {
            scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
          } else {
            riskRuleGenerationPollTimers.delete(normalizedAssetId)
          }
        }
      }, attempt === 0 ? 1200 : 3000)
      riskRuleGenerationPollTimers.set(normalizedAssetId, timer)
    }

    async function persistRuleRuntimeConfig(asset, runtimeRule) {
      await updateAgentAsset(
        asset.id,
        {
          config_json: buildRuleConfigPayload(asset, runtimeRule)
        },
        { actor: resolveActor() }
      )
    }

    function destroySpreadsheetOnlyOfficeEditor() {
      if (spreadsheetOnlyOfficeLoadTimer) {
        window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
        spreadsheetOnlyOfficeLoadTimer = null
      }
      stopSpreadsheetOnlyOfficeChangeSync()
      spreadsheetOnlyOfficeHadLocalEdits = false
      spreadsheetOnlyOfficeSyncSeq += 1
      if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
        spreadsheetOnlyOfficeEditor.value.destroyEditor()
      }
      spreadsheetOnlyOfficeEditor.value = null
      spreadsheetOnlyOfficeReady.value = false
    }

    function stopSpreadsheetOnlyOfficeChangeSync() {
      if (spreadsheetOnlyOfficeChangePollTimer) {
        window.clearTimeout(spreadsheetOnlyOfficeChangePollTimer)
        spreadsheetOnlyOfficeChangePollTimer = null
      }
    }

    function getLatestSpreadsheetChangeKey(assetId) {
      return buildSpreadsheetChangeRecordKey(spreadsheetChangeRecordsByAsset.value[assetId] || [])
    }

    async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
      const normalizedAssetId = normalizeText(assetId)
      if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
        return false
      }

      await loadSpreadsheetChangeRecords(normalizedAssetId)
      const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
      if (nextLatestKey && nextLatestKey !== previousLatestKey) {
        return true
      }
      if (attempt >= 9) {
        return false
      }
      await new Promise((resolve) => window.setTimeout(resolve, 800))
      return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
    }

    function scheduleSpreadsheetOnlyOfficeChangeSync(assetId, attempt = 0) {
      const normalizedAssetId = normalizeText(assetId)
      if (!normalizedAssetId) {
        return
      }

      const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
      stopSpreadsheetOnlyOfficeChangeSync()
      const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)

      const runSync = async () => {
        if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
          return
        }

        try {
          const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
            normalizedAssetId,
            previousLatestChangeKey
          )
          if (changeRecordRefreshed) {
            await refreshCurrentAssets()
            stopSpreadsheetOnlyOfficeChangeSync()
            return
          }
        } catch {
          // Ignore transient polling failures and continue retrying within the window.
        }

        if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
          return
        }
        if (attempt >= 29) {
          return
        }
        spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
          scheduleSpreadsheetOnlyOfficeChangeSync(normalizedAssetId, attempt + 1)
        }, 2000)
      }

      spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
        runSync().catch(() => {})
      }, attempt === 0 ? 800 : 2000)
    }

    function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId) {
      return (
        mountSeq !== spreadsheetOnlyOfficeMountSeq ||
        !selectedSkillUsesSpreadsheet.value ||
        selectedSkill.value?.id !== assetId ||
        selectedSkill.value?.loading
      )
    }

    async function mountSpreadsheetOnlyOfficeEditor(retryAttempt = 0) {
      if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id || selectedSkill.value?.loading) {
        destroySpreadsheetOnlyOfficeEditor()
        return
      }

      const mountSeq = ++spreadsheetOnlyOfficeMountSeq
      const assetId = selectedSkill.value.id
      const editable = canEditSpreadsheetInline.value

      spreadsheetOnlyOfficeLoading.value = true
      spreadsheetOnlyOfficeError.value = ''
      spreadsheetOnlyOfficeReady.value = false
      destroySpreadsheetOnlyOfficeEditor()

      try {
        const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId)
        if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
          return
        }

        await loadOnlyOfficeApi(payload.documentServerUrl)
        if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
          return
        }
        if (!window.DocsAPI?.DocEditor) {
          throw new Error('表格编辑器未正确加载。')
        }

        // Host id must be unique for every mount. ONLYOFFICE mutates its host DOM
        // during lifecycle teardown; reusing the same element can leave the next
        // DocEditor instance with a dead container even though config loading succeeds.
        spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${mountSeq}`
        await nextTick()
        if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
          return
        }

        const config = buildOnlyOfficeEditorConfig(payload.config, {
          viewportHeight: window.innerHeight,
          editable,
          fillContainer: true
        })
        const upstreamEvents = config.events || {}
        spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
          if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
            return
          }
          if (retryAttempt < 1) {
            destroySpreadsheetOnlyOfficeEditor()
            spreadsheetOnlyOfficeLoading.value = true
            window.setTimeout(() => {
              mountSpreadsheetOnlyOfficeEditor(retryAttempt + 1).catch(() => {})
            }, 600)
            return
          }
          spreadsheetOnlyOfficeError.value = '表格加载超时，请退出详情后重试。'
          spreadsheetOnlyOfficeLoading.value = false
          destroySpreadsheetOnlyOfficeEditor()
        }, 15000)
        config.events = {
          ...upstreamEvents,
          onAppReady(event) {
            if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
              return
            }
            if (spreadsheetOnlyOfficeLoadTimer) {
              window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
              spreadsheetOnlyOfficeLoadTimer = null
            }
            spreadsheetOnlyOfficeReady.value = true
            spreadsheetOnlyOfficeLoading.value = false
            upstreamEvents.onAppReady?.(event)
          },
          onError(event) {
            if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
              return
            }
            if (spreadsheetOnlyOfficeLoadTimer) {
              window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
              spreadsheetOnlyOfficeLoadTimer = null
            }
            const errorCode = event?.data?.errorCode
            const errorDescription = event?.data?.errorDescription
            spreadsheetOnlyOfficeError.value = errorDescription
              ? `表格加载失败：${errorDescription}`
              : `表格加载失败${errorCode ? `（错误码 ${errorCode}）` : '。'}`
            spreadsheetOnlyOfficeLoading.value = false
            upstreamEvents.onError?.(event)
          },
          onDocumentStateChange(event) {
            const hasChanges = Boolean(event?.data)
            if (hasChanges) {
              spreadsheetOnlyOfficeHadLocalEdits = true
              if (!spreadsheetOnlyOfficeChangePollTimer) {
                scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
              }
            } else if (
              spreadsheetOnlyOfficeHadLocalEdits &&
              editable &&
              !isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)
            ) {
              spreadsheetOnlyOfficeHadLocalEdits = false
              scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
            }
            upstreamEvents.onDocumentStateChange?.(event)
          }
        }
        spreadsheetOnlyOfficeEditor.value = new window.DocsAPI.DocEditor(
          spreadsheetOnlyOfficeHostId.value,
          config
        )
        if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
          destroySpreadsheetOnlyOfficeEditor()
        }
      } catch (error) {
        if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
          return
        }
        spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败，请稍后重试。'
        spreadsheetOnlyOfficeLoading.value = false
        toast(spreadsheetOnlyOfficeError.value)
      }
    }

    function triggerSpreadsheetUpload() {
      if (!canUploadSpreadsheet.value) {
        return
      }
      spreadsheetUploadInput.value?.click?.()
    }

    async function downloadSpreadsheetFile() {
      if (!canDownloadSpreadsheet.value || !selectedSkill.value?.id) {
        return
      }

      actionState.value = 'download-spreadsheet'
      try {
        const blob = await fetchAgentAssetSpreadsheetBlob(
          selectedSkill.value.id,
          'attachment'
        )
        const objectUrl = URL.createObjectURL(blob)
        const anchor = document.createElement('a')
        anchor.href = objectUrl
        anchor.download = selectedSpreadsheetFileName.value || '规则表.xlsx'
        document.body.appendChild(anchor)
        anchor.click()
        anchor.remove()
        URL.revokeObjectURL(objectUrl)
      } catch (error) {
        toast(error?.message || '规则表下载失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function uploadSpreadsheetFile(file) {
      if (!file || !selectedSkill.value?.id || !canUploadSpreadsheet.value) {
        return
      }

      actionState.value = 'upload-spreadsheet'
      try {
        await importAgentAssetSpreadsheetContent(selectedSkill.value.id, file, {
          actor: resolveActor()
        })
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        await loadSpreadsheetChangeRecords(selectedSkill.value.id)
        toast(`已导入 ${file.name} 的表格内容，右侧会记录本次修改。`)
      } catch (error) {
        toast(error?.message || '规则表内容导入失败，请稍后重试。')
      } finally {
        actionState.value = ''
        spreadsheetUploadInput.value?.reset?.()
      }
    }

    async function handleSpreadsheetFileInput(event) {
      await uploadSpreadsheetFile(event?.target?.files?.[0] || null)
    }

    async function loadRuns(options = {}) {
      if (runLoading.value && !options.force) {
        return
      }

      runLoading.value = true
      try {
        const payload = await fetchAgentRuns({ limit: 50 })
        runs.value = Array.isArray(payload) ? payload : []
      } finally {
        runLoading.value = false
      }
    }

    async function loadAssets(options = {}) {
      const shouldShowLoading = !options.silent && !options.background
      if (shouldShowLoading) {
        loading.value = true
      }
      if (!options.silent) {
        errorMessage.value = ''
      }

      try {
        const payload = await fetchAgentAssets({ assetType: activeMeta.value.assetType })
        const items = Array.isArray(payload) ? payload.map(buildListItem).filter(Boolean) : []

        if (activeMeta.value.assetType === 'rule') {
          const nextBuckets = {
            financialRules: [],
            riskRules: []
          }

          items.forEach((item) => {
            if (item?.tabId === 'financialRules' || item?.tabId === 'riskRules') {
              nextBuckets[item.tabId].push(item)
            }
          })

          assetBuckets.value = {
            ...assetBuckets.value,
            ...nextBuckets
          }
        } else {
          assetBuckets.value = {
            ...assetBuckets.value,
            [activeType.value]: items
          }
        }
      } catch (error) {
        if (options.silent || options.background) {
          return
        }
        if (activeMeta.value.assetType === 'rule') {
          assetBuckets.value = {
            ...assetBuckets.value,
            financialRules:
              activeType.value === 'financialRules' ? [] : assetBuckets.value.financialRules,
            riskRules: []
          }
          errorMessage.value = error?.message || '资产数据加载失败，请稍后重试。'
        } else {
          assetBuckets.value = {
            ...assetBuckets.value,
            [activeType.value]: []
          }
          errorMessage.value = error?.message || '资产数据加载失败，请稍后重试。'
        }
        if (!options.silent) {
          toast(errorMessage.value)
        }
      } finally {
        if (shouldShowLoading) {
          loading.value = false
        }
      }
    }

    async function refreshCurrentAssets() {
      await loadAssets({ force: true, silent: true, background: true })
    }

    async function loadSelectedAssetDetail(assetId) {
      detailLoading.value = true
      detailError.value = ''

      try {
        if (!runs.value.length) {
          await loadRuns()
        }
        const detail = await fetchAgentAssetDetail(assetId)
        selectedSkill.value = buildDetailViewModel(detail, runs.value)
        if (selectedSkill.value?.type === 'rules') {
          if (!selectedSkill.value.usesSpreadsheetRule && !selectedSkill.value.usesJsonRiskRule) {
            loadVersionTimeline(assetId, { silent: true }).catch(() => {})
          }
          if (selectedSkill.value.usesSpreadsheetRule) {
            loadSpreadsheetChangeRecords(assetId).catch(() => {})
          }
          if (selectedSkill.value.usesJsonRiskRule) {
            if (selectedSkill.value.riskRuleGenerationFailed || selectedSkill.value.riskRuleGenerationBusy) {
              return
            }
            try {
              await loadRiskRuleJson(assetId)
            } catch (jsonError) {
              console.warn('Failed to load risk rule JSON:', jsonError)
              const jsonMessage =
                jsonError?.message || '风险规则 JSON 文件缺失或无法读取，请同步规则库后重试。'
              toast(jsonMessage)
              selectedSkill.value = {
                ...selectedSkill.value,
                riskRuleJsonText: '{}',
                riskRuleDescription:
                  selectedSkill.value.riskRuleDescription ||
                  '规则 JSON 尚未就绪，请联系管理员执行平台风险规则同步。'
              }
            }
          }
        }
      } catch (error) {
        detailError.value = error?.message || '资产详情加载失败，请稍后重试。'
        toast(detailError.value)
      } finally {
        detailLoading.value = false
      }
    }

    function mergeSelectedRuleLifecycle(detail) {
      if (!selectedSkill.value || !detail) {
        return
      }
      const next = buildDetailViewModel(detail, runs.value)
      selectedSkill.value = {
        ...selectedSkill.value,
        status: next.status,
        statusValue: next.statusValue,
        statusTone: next.statusTone,
        publishedVersion: next.publishedVersion,
        workingVersion: next.workingVersion,
        currentVersion: next.currentVersion,
        displayVersion: next.displayVersion,
        reviewer: next.reviewer,
        publisher: next.publisher,
        publishedAt: next.publishedAt,
        isOnlineValue: next.isOnlineValue,
        isOnlineLabel: next.isOnlineLabel,
        isOnlineTone: next.isOnlineTone,
        isEnabledValue: next.isEnabledValue,
        isEnabledLabel: next.isEnabledLabel,
        isEnabledTone: next.isEnabledTone,
        latestTestSummary: next.latestTestSummary,
        lastOperationLabel: next.lastOperationLabel,
        lastOperationTone: next.lastOperationTone,
        publishMeta: next.publishMeta,
        publishState: next.publishState,
        updatedAt: next.updatedAt,
        configJson: next.configJson
      }
    }

    async function loadRiskRuleJson(assetId) {
      if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
        return
      }
      const payload = await fetchAgentAssetRuleJson(assetId)
      const rulePayload = payload?.payload && typeof payload.payload === 'object' ? payload.payload : payload
      selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, rulePayload, payload)
    }

    async function saveRiskRuleJson() {
      if (!selectedSkill.value?.id || !canEditMarkdown.value) {
        return
      }
      actionState.value = 'save-risk-json'
      detailBusy.value = true
      try {
        const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
        const saved = await saveAgentAssetRuleJson(selectedSkill.value.id, { payload: parsed })
        const rulePayload = saved?.payload && typeof saved.payload === 'object' ? saved.payload : saved
        selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, rulePayload, saved)
        toast('风险规则 JSON 已保存。')
      } catch (error) {
        toast(error?.message || '风险规则 JSON 保存失败。')
      } finally {
        detailBusy.value = false
        actionState.value = ''
      }
    }

    function formatRiskRuleJson() {
      if (!selectedSkill.value?.usesJsonRiskRule) {
        return
      }
      try {
        const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
        selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, parsed, {
          name: selectedSkill.value.name,
          description: resolveRiskRuleDescription(parsed)
        })
      } catch (error) {
        toast(error?.message || 'JSON 格式无效，无法格式化。')
      }
    }

    function downloadRiskRuleJson() {
      if (!selectedSkill.value?.usesJsonRiskRule) {
        return
      }
      const blob = new Blob([String(selectedSkill.value.riskRuleJsonText || '{}')], {
        type: 'application/json;charset=utf-8'
      })
      const fileName =
        selectedSkill.value.ruleDocument?.file_name ||
        `${selectedSkill.value.code || 'risk-rule'}.json`
      const link = document.createElement('a')
      link.href = URL.createObjectURL(blob)
      link.download = fileName
      link.click()
      URL.revokeObjectURL(link.href)
    }

    async function loadSpreadsheetChangeRecords(assetId) {
      if (!assetId) {
        return
      }
      const payload = await fetchAgentAssetSpreadsheetChangeRecords(assetId, 30)
      spreadsheetChangeRecordsByAsset.value = {
        ...spreadsheetChangeRecordsByAsset.value,
        [assetId]: Array.isArray(payload) ? payload : []
      }
    }

    function openSpreadsheetChangeDetail(item) {
      if (!item?.changed_at) {
        return
      }
      selectedSpreadsheetChangeRecord.value = item
      spreadsheetChangeDetailOpen.value = true
    }

    function closeSpreadsheetChangeDetail() {
      spreadsheetChangeDetailOpen.value = false
    }

    function openAssetDetail(asset) {
      if (asset?.usesJsonRiskRule && asset.statusValue === 'generating') {
        toast('规则仍在后台生成中，生成完成后才能进入详情。')
        return
      }
      destroySpreadsheetOnlyOfficeEditor()
      spreadsheetOnlyOfficeError.value = ''
      spreadsheetOnlyOfficeLoading.value = false
      if (asset?.isPreviewMock) {
        selectedSkill.value = buildPreviewRuleDetail()
        detailError.value = ''
        detailLoading.value = false
        versionSwitchTarget.value = null
        return
      }
      const opensSpreadsheetRule = Boolean(asset?.usesSpreadsheetRule)
      selectedSkill.value = {
        ...asset,
        configJson: {},
        isPreviewMock: false,
        usesSpreadsheetRule: opensSpreadsheetRule,
        usesJsonRiskRule: Boolean(asset?.usesJsonRiskRule),
        riskRuleJsonText: '{}',
        riskRuleSummary: null,
        riskRuleDescription: '',
        riskRuleSourceRef: '',
        ruleDocument: asset?.ruleDocument || null,
        scenarioList: [],
        fields: [],
        promptSections: [],
        outputRules: [],
        tests: [],
        triggers: [],
        tools: [],
        history: [],
        markdownContent: '',
        runtimeRuleText: '',
        ruleTemplateKey: '',
        ruleTemplateLabel: '',
        runtimeKind: 'policy_rule_draft',
        displayVersion: asset.version,
        displayVersionChangeNote: '无版本说明',
        loading: !opensSpreadsheetRule,
        reviewStatusLabel: opensSpreadsheetRule ? '' : '加载中',
        reviewStatusTone: 'draft'
      }
      versionSwitchTarget.value = null
      if (opensSpreadsheetRule) {
        loadSpreadsheetChangeRecords(asset.id).catch(() => {})
      }
      loadSelectedAssetDetail(asset.id).catch(() => {})
    }

    function closeDetail() {
      destroySpreadsheetOnlyOfficeEditor()
      spreadsheetOnlyOfficeError.value = ''
      spreadsheetOnlyOfficeLoading.value = false
      selectedSkill.value = null
      detailError.value = ''
      detailLoading.value = false
      versionSwitchTarget.value = null
      versionTimelineOpen.value = false
      versionTimelineItems.value = []
      riskRuleTestOpen.value = false
      riskRuleDeleteOpen.value = false
      riskRuleReturnOpen.value = false
      riskRulePublishOpen.value = false
      riskRuleReturnNote.value = ''
    }

    function openVersionSwitch(version) {
      if (!selectedSkill.value || version.version === selectedSkill.value.displayVersion) {
        return
      }
      versionSwitchTarget.value = version
    }

    function cancelVersionSwitch() {
      versionSwitchTarget.value = null
    }

    function confirmVersionSwitch() {
      if (!selectedSkill.value || !versionSwitchTarget.value) {
        return
      }

      selectedSkill.value.displayVersion = versionSwitchTarget.value.version
      selectedSkill.value.displayVersionChangeNote = versionSwitchTarget.value.note || '无版本说明'
      if (selectedSkill.value.usesSpreadsheetRule) {
        versionSwitchTarget.value = null
        return
      }
      if (typeof versionSwitchTarget.value.markdownContent === 'string') {
        selectedSkill.value.markdownContent = versionSwitchTarget.value.markdownContent
      }
      const runtimeRule = versionSwitchTarget.value.runtimeRule || buildDefaultRuntimeRule(selectedSkill.value)
      selectedSkill.value.runtimeRuleText = stringifyRuntimeRule(runtimeRule)
      selectedSkill.value.runtimeKind =
        normalizeText(runtimeRule.kind) || selectedSkill.value.runtimeKind || 'policy_rule_draft'
      selectedSkill.value.ruleTemplateKey =
        normalizeText(runtimeRule.template_key) || selectedSkill.value.ruleTemplateKey
      selectedSkill.value.ruleTemplateLabel = resolveRuleTemplateLabel(selectedSkill.value.ruleTemplateKey)
      versionSwitchTarget.value = null
    }

    async function saveRuleMarkdown() {
      if (
        !selectedSkill.value ||
        !selectedSkillIsRule.value ||
        selectedSkill.value.usesSpreadsheetRule ||
        !canEditMarkdown.value ||
        detailBusy.value
      ) {
        return
      }

      if (!normalizeText(selectedSkill.value.markdownContent)) {
        toast('规则 Markdown 内容不能为空。')
        return
      }

      const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
      if (!runtimeRule) {
        toast('运行时 JSON 必须是合法的对象。')
        return
      }

      const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
      actionState.value = 'save-markdown'

      try {
        await createAgentAssetVersion(
          selectedSkill.value.id,
          {
            version: nextVersion,
            content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
            content_type: 'markdown',
            change_note: '通过规则中心保存 Markdown 规则内容，并同步运行时 JSON。',
            created_by: resolveActor()
          },
          { actor: resolveActor() }
        )
        await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast(`规则 Markdown 已保存为 ${nextVersion}。`)
      } catch (error) {
        toast(error?.message || '规则 Markdown 保存失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function saveRuleRuntimeJson() {
      if (
        !selectedSkill.value ||
        !selectedSkillIsRule.value ||
        selectedSkill.value.usesSpreadsheetRule ||
        !canEditMarkdown.value ||
        detailBusy.value
      ) {
        return
      }

      if (!normalizeText(selectedSkill.value.markdownContent)) {
        toast('规则 Markdown 模板不能为空。')
        return
      }

      const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
      if (!runtimeRule) {
        toast('运行时 JSON 必须是合法的对象。')
        return
      }

      const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
      actionState.value = 'save-runtime-json'

      try {
        await createAgentAssetVersion(
          selectedSkill.value.id,
          {
            version: nextVersion,
            content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
            content_type: 'markdown',
            change_note: '通过规则中心保存运行时 JSON 配置。',
            created_by: resolveActor()
          },
          { actor: resolveActor() }
        )
        await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast(`规则 JSON 已保存为 ${nextVersion}。`)
      } catch (error) {
        toast(error?.message || '规则 JSON 保存失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function reviewSelectedRule(reviewStatus) {
      if (!selectedSkill.value || !selectedSkillIsRule.value || detailBusy.value) {
        return
      }
      if (reviewStatus === 'pending' && !canSubmitReview.value) {
        return
      }
      if (reviewStatus !== 'pending' && !canReviewSelected.value) {
        return
      }

      actionState.value = `review-${reviewStatus}`

      try {
        await createAgentAssetReview(
          selectedSkill.value.id,
          {
            version: selectedSkill.value.workingVersion,
            reviewer: resolveActor(),
            review_status: reviewStatus,
            review_note: buildReviewNote(reviewStatus)
          },
          { actor: resolveActor() }
        )
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast(`当前规则版本已标记为${resolveReviewMeta(reviewStatus).label}。`)
      } catch (error) {
        toast(error?.message || '规则审核提交失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function loadReviewSubmitReviewers() {
      reviewSubmitReviewerLoading.value = true
      try {
        const employees = await fetchEmployees()
        reviewSubmitReviewerOptions.value = (Array.isArray(employees) ? employees : [])
          .filter(
            (item) =>
              item.status === '在职' &&
              Array.isArray(item.roleCodes) &&
              item.roleCodes.includes('manager')
          )
          .map((item) => ({
            value: item.name,
            label: `${item.name} · ${item.position || '高级管理员'}`
          }))
      } catch (error) {
        reviewSubmitReviewerOptions.value = []
        toast(error?.message || '审核人列表加载失败，请稍后重试。')
      } finally {
        reviewSubmitReviewerLoading.value = false
      }
    }

    async function openSubmitReviewDialog() {
      if (
        selectedSkillUsesJsonRisk.value &&
        !canOpenRiskRuleReviewSubmit.value
      ) {
        return
      }
      if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
        return
      }
      reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
      reviewSubmitReviewer.value = selectedSkill.value.reviewer || ''
      reviewSubmitOpen.value = true
      await loadReviewSubmitReviewers()
      if (
        !reviewSubmitReviewerOptions.value.some(
          (item) => item.value === reviewSubmitReviewer.value
        )
      ) {
        reviewSubmitReviewer.value = reviewSubmitReviewerOptions.value[0]?.value || ''
      }
    }

    function closeSubmitReviewDialog() {
      if (detailBusy.value) {
        return
      }
      reviewSubmitOpen.value = false
    }

    async function submitSelectedRuleForReview() {
      if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
        return
      }
      if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
        toast('当前规则版本尚未确认测试通过，不能提交审核。')
        return
      }
      const version = normalizeText(reviewSubmitVersion.value)
      const reviewer = normalizeText(reviewSubmitReviewer.value)
      if (!version) {
        toast('请输入送审版本号。')
        return
      }
      if (!reviewer) {
        toast('请选择审核人。')
        return
      }

      actionState.value = 'review-pending'
      try {
        await createAgentAssetReview(
          selectedSkill.value.id,
          {
            version,
            reviewer,
            review_status: 'pending',
            review_note: buildReviewNote('pending')
          },
          { actor: resolveActor() }
        )
        reviewSubmitOpen.value = false
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast(`规则版本 ${version} 已提交给 ${reviewer} 审核。`)
      } catch (error) {
        toast(error?.message || '规则审核提交失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    function openRiskRuleTestDialog() {
      if (detailBusy.value) {
        return
      }
      if (!canOpenRiskRuleTest.value) {
        if (!selectedSkill.value?.id) {
          toast('规则详情还没有加载完成，请稍后再测试。')
        }
        return
      }
      riskRuleTestOpen.value = true
    }

    function closeRiskRuleTestDialog() {
      riskRuleTestOpen.value = false
    }

    async function handleRiskRuleReportSaved(summary) {
      if (selectedSkill.value) {
        selectedSkill.value.latestTestSummary = summary
      }
      await refreshCurrentAssets()
      if (selectedSkill.value?.id) {
        const detail = await fetchAgentAssetDetail(selectedSkill.value.id)
        mergeSelectedRuleLifecycle(detail)
      }
    }

    function openDeleteRiskRuleDialog() {
      if (!canDeleteRiskRule.value) {
        return
      }
      riskRuleDeleteOpen.value = true
    }

    function closeDeleteRiskRuleDialog() {
      if (detailBusy.value) {
        return
      }
      riskRuleDeleteOpen.value = false
    }

    async function deleteSelectedRiskRule() {
      if (!selectedSkill.value || !canDeleteRiskRule.value || detailBusy.value) {
        return
      }
      actionState.value = 'delete-risk-rule'
      try {
        await deleteAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
        riskRuleDeleteOpen.value = false
        const deletedName = selectedSkill.value.name
        closeDetail()
        await refreshCurrentAssets()
        toast(`风险规则“${deletedName}”已删除。`)
      } catch (error) {
        toast(error?.message || '风险规则删除失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    function openReturnRiskRuleDialog() {
      if (!canReturnRiskRule.value) {
        return
      }
      riskRuleReturnNote.value = ''
      riskRuleReturnOpen.value = true
    }

    function closeReturnRiskRuleDialog() {
      if (detailBusy.value) {
        return
      }
      riskRuleReturnOpen.value = false
    }

    async function returnSelectedRiskRule() {
      if (!selectedSkill.value || !canReturnRiskRule.value || detailBusy.value) {
        return
      }
      const note = normalizeText(riskRuleReturnNote.value)
      if (!note) {
        toast('请填写回退原因。')
        return
      }
      actionState.value = 'return-risk-rule'
      try {
        await returnRiskRuleAsset(selectedSkill.value.id, { note }, { actor: resolveActor() })
        riskRuleReturnOpen.value = false
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast('风险规则已回退到草稿。')
      } catch (error) {
        toast(error?.message || '风险规则回退失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    function openPublishRiskRuleDialog() {
      if (!canPublishRiskRule.value) {
        if (!riskRuleTestPassed.value) {
          toast('请先确认测试报告通过，再发布上线。')
        }
        return
      }
      riskRulePublishOpen.value = true
    }

    function closePublishRiskRuleDialog() {
      if (detailBusy.value) {
        return
      }
      riskRulePublishOpen.value = false
    }

    async function publishSelectedRiskRule() {
      if (!selectedSkill.value || !canPublishRiskRule.value || detailBusy.value) {
        return
      }
      actionState.value = 'publish-risk-rule'
      try {
        await publishRiskRuleAsset(selectedSkill.value.id, { actor: resolveActor() })
        riskRulePublishOpen.value = false
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast('风险规则已发布上线。')
      } catch (error) {
        toast(error?.message || '风险规则发布失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function toggleSelectedRiskRuleEnabled() {
      if (!selectedSkill.value || !canToggleRiskRuleEnabled.value || detailBusy.value) {
        return
      }
      const assetId = selectedSkill.value.id
      const nextEnabled = !selectedSkill.value.isOnlineValue
      actionState.value = 'toggle-risk-rule-enabled'
      try {
        const detail = await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
        mergeSelectedRuleLifecycle(detail)
        await refreshCurrentAssets()
        toast(nextEnabled ? '风险规则已上线。' : '风险规则已下线，不会进入业务扫描。')
      } catch (error) {
        toast(error?.message || '风险规则上线状态更新失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function activateSelectedRule() {
      if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
        return
      }

      actionState.value = 'activate'

      try {
        await activateAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast('规则已正式上线。')
      } catch (error) {
        toast(error?.message || '规则上线失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function restoreSelectedVersion(version) {
      if (
        !selectedSkill.value ||
        !selectedSkillIsRule.value ||
        !canManageSelected.value ||
        detailBusy.value ||
        !version
      ) {
        return
      }

      actionState.value = `restore-${version}`
      try {
        await restoreAgentAssetVersion(selectedSkill.value.id, version, { actor: resolveActor() })
        await refreshCurrentAssets()
        await loadSelectedAssetDetail(selectedSkill.value.id)
        toast(`已基于 ${version} 生成新的工作版本。`)
      } catch (error) {
        toast(error?.message || '历史版本恢复失败，请稍后重试。')
      } finally {
        actionState.value = ''
      }
    }

    async function loadVersionTimeline(assetId = selectedSkill.value?.id, options = {}) {
      if (!assetId) {
        return
      }

      versionTimelineLoading.value = true
      versionTimelineError.value = ''
      try {
        const payload = await fetchAgentAssetVersionTimeline(assetId)
        versionTimelineItems.value = Array.isArray(payload) ? payload : []
      } catch (error) {
        versionTimelineError.value = error?.message || '操作记录加载失败，请稍后重试。'
        if (!options.silent) {
          toast(versionTimelineError.value)
        }
      } finally {
        versionTimelineLoading.value = false
      }
    }

    async function openVersionTimeline() {
      if (!selectedSkill.value?.id) {
        return
      }
      versionTimelineOpen.value = true
      await loadVersionTimeline(selectedSkill.value.id)
    }

    function closeVersionTimeline() {
      versionTimelineOpen.value = false
    }

    onMounted(() => {
      document.addEventListener('click', handleDocumentClick)
      loadAssets({ force: true }).catch(() => {})
      loadRuns().catch(() => {})
    })

    onBeforeUnmount(() => {
      destroySpreadsheetOnlyOfficeEditor()
      riskRuleGenerationPollTimers.forEach((timer) => window.clearTimeout(timer))
      riskRuleGenerationPollTimers.clear()
      document.removeEventListener('click', handleDocumentClick)
    })

    return {
      tabs,
      activeType,
      activeTabLabel,
      selectedSkill,
      versionSwitchTarget,
      keyword,
      createButtonLabel,
      hintText,
      searchPlaceholder,
      tableColumns,
      showRuntimeColumn,
      showVersionColumn,
      showMetricColumn,
      showStatusColumn,
      showOnlineColumn,
      showEnabledColumn,
      visibleSkills,
      auditEmptyState,
      loading,
      errorMessage,
      detailLoading,
      detailError,
      selectedDomain,
      selectedOwner,
      selectedRiskLevel,
      selectedStatus,
      selectedRiskScenario,
      selectedOnlineState,
      selectedEnabledState,
      selectedDomainLabel,
      selectedOwnerLabel,
      selectedRiskLevelLabel,
      selectedStatusLabel,
      selectedRiskScenarioLabel,
      selectedOnlineStateLabel,
      selectedEnabledStateLabel,
      showRiskScenarioFilter,
      showOwnerFilter,
      showRiskLevelFilter,
      showStatusFilter,
      showOnlineFilter,
      showEnabledFilter,
      domainOptions,
      ownerOptions,
      riskLevelOptions,
      statusOptions: STATUS_OPTIONS,
      riskScenarioOptions: RISK_SCENARIO_OPTIONS,
      onlineStateOptions: ONLINE_STATE_OPTIONS,
      enabledStateOptions: ENABLED_STATE_OPTIONS,
      activeFilterPopover,
      activeFilterTokens,
      canManageSelected,
      canEditSelected,
      canCreateRiskRule,
      canOpenRiskRuleTest,
      canDeleteRiskRule,
      canOpenRiskRuleReviewSubmit,
      canSubmitRiskRuleReview,
      canReturnRiskRule,
      canPublishRiskRule,
      canToggleRiskRuleEnabled,
      riskRuleTestPassed,
      riskRuleInReview,
      canSubmitReview,
      hasReviewSubmitReviewers,
      canReviewSelected,
      canEditMarkdown,
      canUploadSpreadsheet,
      canDownloadSpreadsheet,
      canEditSpreadsheetInline,
      canActivateSelected,
      activateBlockedReason,
      selectedSkillIsRule,
      selectedSkillUsesSpreadsheet,
      selectedSkillUsesJsonRisk,
      selectedSpreadsheetFileName,
      selectedSpreadsheetModeLabel,
      selectedVersionTimelineItems,
      selectedSpreadsheetChangeRecords,
      detailBusy,
      actionState,
      reviewSubmitOpen,
      reviewSubmitVersion,
      reviewSubmitReviewer,
      reviewSubmitReviewerLoading,
      reviewSubmitReviewerOptions,
      riskRuleAttachmentOptions,
      riskRuleCreateOpen,
      riskRuleCreateForm,
      riskRuleCreateBusy,
      riskRuleTestOpen,
      riskRuleDeleteOpen,
      riskRuleReturnOpen,
      riskRulePublishOpen,
      riskRuleReturnNote,
      riskRuleBusinessStageOptions: RISK_RULE_BUSINESS_STAGE_OPTIONS,
      riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
      showReviewNote,
      spreadsheetUploadInput,
      spreadsheetOnlyOfficeLoading,
      spreadsheetOnlyOfficeError,
      spreadsheetOnlyOfficeReady,
      spreadsheetOnlyOfficeHostId,
      versionTimelineOpen,
      versionTimelineLoading,
      versionTimelineError,
      spreadsheetChangeDetailOpen,
      selectedSpreadsheetChangeRecord,
      selectedSpreadsheetChangeSheetRows,
      selectedSpreadsheetChangeCellRows,
      openAssetDetail,
      closeDetail,
      resetFilters,
      handleAuditEmptyAction,
      toggleFilterPopover,
      selectFilter,
      closeFilterPopover,
      openRiskRuleCreateDialog,
      closeRiskRuleCreateDialog,
      submitRiskRuleCreate,
      openVersionSwitch,
      cancelVersionSwitch,
      confirmVersionSwitch,
      saveRuleMarkdown,
      saveRuleRuntimeJson,
      saveRiskRuleJson,
      formatRiskRuleJson,
      downloadRiskRuleJson,
      triggerSpreadsheetUpload,
      downloadSpreadsheetFile,
      handleSpreadsheetFileInput,
      reviewSelectedRule,
      openSubmitReviewDialog,
      closeSubmitReviewDialog,
      submitSelectedRuleForReview,
      openRiskRuleTestDialog,
      closeRiskRuleTestDialog,
      handleRiskRuleReportSaved,
      openDeleteRiskRuleDialog,
      closeDeleteRiskRuleDialog,
      deleteSelectedRiskRule,
      openReturnRiskRuleDialog,
      closeReturnRiskRuleDialog,
      returnSelectedRiskRule,
      openPublishRiskRuleDialog,
      closePublishRiskRuleDialog,
      publishSelectedRiskRule,
      toggleSelectedRiskRuleEnabled,
      activateSelectedRule,
      restoreSelectedVersion,
      openVersionTimeline,
      closeVersionTimeline,
      openSpreadsheetChangeDetail,
      closeSpreadsheetChangeDetail,
      loadAssets
    }
  }
}
