<template>
  <PageContainer title="审核管理">
    <AuditQueryToolbar
      v-model:active-tab="activeTab"
      :can-batch-remind="canBatchRemind"
      :can-export="canExport"
      :can-sla-read="canSlaRead"
      :sla-stat="slaStat"
      @load="load"
      @batch-remind="handleBatchRemind"
      @export-opinions="handleExport"
      @tab-change="handleTabChange"
    />
    <AuditListTable
      :rows="rows"
      :can-approve="canApprove"
      :can-reject="canReject"
      :can-read-material="canReadMaterial"
      :submitting-task-id="submittingTaskId"
      :get-current-user-id="getCurrentUserId"
      :meeting-name-formatter="meetingNameFormatter"
      :audit-node-formatter="auditNodeFormatter"
      :assignee-name-formatter="assigneeNameFormatter"
      :status-formatter="statusFormatter"
      :resolve-flow-nodes="resolveFlowNodes"
      :flow-node-tag-type="flowNodeTagType"
      @selection-change="handleSelectionChange"
      @submit-audit="handleSubmitAudit"
      @open-audit-progress="openAuditProgress"
      @open-material="openMaterial"
    />
    <AuditMaterialDrawer
      v-model:material-dialog-visible="materialDialogVisible"
      v-model:material-module="materialModule"
      v-model:expert-review-sub-module="expertReviewSubModule"
      v-model:selected-audit-expert-id="selectedAuditExpertId"
      :material-audit-node="materialAuditNode"
      :material-budget-view="materialBudgetView"
      :format-yuan="formatYuan"
      :basic-view="basicView"
      :doc-view="docView"
      :expert-profile-view="expertProfileView"
      :photo-view="photoView"
      :meeting-invoice-view="meetingInvoiceView"
      :expert-review-rows="expertReviewRows"
      :selected-audit-photos="selectedAuditPhotos"
      :selected-audit-labor-details="selectedAuditLaborDetails"
      :selected-audit-expert-name="selectedAuditExpertName"
      :can-reject="canReject"
      :can-approve="canApprove"
      :to-review-tag-type="toReviewTagType"
      :to-review-result-text="toReviewResultText"
      :get-file-url="getFileUrl"
      :is-image-attachment="isImageAttachment"
      :is-pdf-attachment="isPdfAttachment"
      :open-file-url="openFileUrl"
      :preview-oss-document="openAuditMaterialDocPreview"
      :build-photo-item-key="buildPhotoItemKey"
      :build-labor-item-key="buildLaborItemKey"
      :build-labor-review-item-keys="buildLaborReviewItemKeys"
      :build-meeting-invoice-field-item-key="buildMeetingInvoiceFieldItemKeyForDrawer"
      :build-meeting-invoice-amount-item-key="buildMeetingInvoiceAmountItemKeyForDrawer"
      :approving-current-module="approvingCurrentModule"
      :material-task-id="materialTaskId"
      :material-content="materialContent"
      :handle-select-audit-expert="handleSelectAuditExpert"
      :handle-reject-material-item="handleRejectMaterialItem"
      :handle-approve-current-module="handleApproveCurrentModule"
      @material-tab-change="handleMaterialTabChange"
    />
    <MeetingDocPreviewDialog
      v-model="auditMaterialPreviewVisible"
      :doc-preview-dialog-image-url="auditMaterialPreviewUrl"
      :preview-kind="auditMaterialPreviewKind"
    />
    <MeetingAuditProgressDialog
      v-model="auditProgressDialogVisible"
      :audit-progress-rows="auditProgressRows"
      :audit-progress-page-no="auditProgressPageNo"
      :audit-progress-page-size="auditProgressPageSize"
      :audit-progress-total="auditProgressTotal"
      :resolve-audit-time="resolveAuditTime"
      :detail-timeline-type="detailTimelineType"
      :to-zh-audit-node="toZhAuditNode"
      :to-zh-status="toZhStatus"
      :detail-status-tag-type="detailStatusTagType"
      :resolve-assignee-name="resolveAssigneeName"
      @page-change="handleAuditProgressPageChange"
      @page-size-change="handleAuditProgressPageSizeChange"
    />
  </PageContainer>
</template>

<script setup lang="ts">
import { computed, ref, onMounted } from "vue";
import PageContainer from "../../components/PageContainer.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
  approveAuditMaterialModule,
  approveAuditTask,
  batchRemindAuditTasks,
  exportAuditOpinions,
  fetchFilePresignDownload,
  fetchMeetingDetail,
  fetchMeetings,
  fetchAuditSlaStat,
  fetchAuditTasks,
  readAuditTaskMaterial,
  rejectAuditMaterialItem,
  rejectAuditTask,
  returnAuditTask,
  transferAuditTask,
} from "../../api/modules";
import { PERMS } from "../../constants/permissions";
import { useAuthStore } from "../../stores/auth";
import { toZhAuditNode, toZhStatus } from "../../utils/status";
import AuditListTable from "./audit-page/AuditListTable.vue";
import AuditMaterialDrawer from "./audit-page/AuditMaterialDrawer.vue";
import AuditQueryToolbar from "./audit-page/AuditQueryToolbar.vue";
import MeetingAuditProgressDialog from "./meeting-page/MeetingAuditProgressDialog.vue";
import MeetingDocPreviewDialog from "./meeting-page/MeetingDocPreviewDialog.vue";

const authStore = useAuthStore();
const rows = ref<any[]>([]);
const activeTab = ref<"pendingMine" | "handledMine">("pendingMine");
const canApprove = computed(() => authStore.hasPermission(PERMS.audit.approve));
const canReject = computed(() => authStore.hasPermission(PERMS.audit.reject));
const canReturn = computed(() => authStore.hasPermission(PERMS.audit.back));
const canTransfer = computed(() => authStore.hasPermission(PERMS.audit.transfer));
const canBatchRemind = computed(() => authStore.hasPermission(PERMS.audit.remind));
const canSlaRead = computed(() => authStore.hasPermission(PERMS.audit.slaRead));
const canExport = computed(() => authStore.hasPermission(PERMS.audit.exportOpinions));
const canReadMaterial = computed(() => authStore.hasPermission(PERMS.audit.materialRead));
const getCurrentUserId = () => authStore.userId;
const selectedTaskIds = ref<number[]>([]);
const submittingTaskId = ref<number | null>(null);
const meetingNameMap = ref<Record<number, string>>({});
const meetingBudgetMap = ref<Record<number, number>>({});
const slaStat = ref({
  pendingTotal: 0,
  timeout4h: 0,
  timeout12h: 0,
  timeout24h: 0,
});
const materialDialogVisible = ref(false);
const materialTaskId = ref<number | null>(null);
const materialMeetingId = ref<number | null>(null);
const materialAuditNode = ref("");
const approvingCurrentModule = ref(false);
type MaterialModuleCode = "BASIC_INFO" | "WRITE_OFF_DOCS" | "EXPERT_PROFILE" | "EXPERT_LIST" | "MEETING_INVOICE";
type BackendMaterialModuleCode = "BASIC_INFO" | "WRITE_OFF_DOCS" | "EXPERT_PROFILE" | "EXPERT_LIST" | "MEETING_INVOICE";
const resolveBackendMaterialModuleCode = (moduleCode: MaterialModuleCode): BackendMaterialModuleCode => {
  return moduleCode;
};
type ExpertSubModuleCode = "ONSITE_PHOTO" | "LABOR_PROTOCOL";
type MeetingInvoiceSectionCode =
  | "SETTLEMENT_DETAIL"
  | "VENUE_CONFIRMATION"
  | "CONSTRUCTION_DETAIL"
  | "ACCOMMODATION_DETAIL"
  | "CATERING_DETAIL"
  | "LOCAL_TRANSPORT_DETAIL"
  | "INTERCITY_TRANSPORT_DETAIL"
  | "MATERIAL_DETAIL"
  | "DESIGN_DRAFT_DETAIL"
  | "OTHER";
type MeetingInvoiceUploadFieldKey =
  | "settlementFile"
  | "invoiceFile"
  | "detailFile"
  | "equipmentFile"
  | "materialFile"
  | "designDraftFile";
type MeetingInvoiceUploadFieldDef = {
  key: MeetingInvoiceUploadFieldKey;
  label: string;
};
type MeetingInvoiceSectionDef = {
  code: MeetingInvoiceSectionCode;
  order: number;
  title: string;
  needAmount: boolean;
  maxUploadCount: number;
  uploadFields: MeetingInvoiceUploadFieldDef[];
};
/** 与 MeetingPage.vue meetingInvoiceSectionDefs 一致，保证审核端解析与会议端多文件上传结构对齐 */
const meetingInvoiceSectionDefs: MeetingInvoiceSectionDef[] = [
  {
    code: "SETTLEMENT_DETAIL",
    order: 1,
    title: "会议结算单明细",
    needAmount: false,
    maxUploadCount: 5,
    uploadFields: [{ key: "settlementFile", label: "" }],
  },
  {
    code: "VENUE_CONFIRMATION",
    order: 2,
    title: "会场确认函/会场协议",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
    ],
  },
  {
    code: "CONSTRUCTION_DETAIL",
    order: 3,
    title: "会议搭建明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
      { key: "equipmentFile", label: "设备照片" },
    ],
  },
  {
    code: "ACCOMMODATION_DETAIL",
    order: 4,
    title: "住宿明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
    ],
  },
  {
    code: "CATERING_DETAIL",
    order: 5,
    title: "餐饮明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单/水单" },
    ],
  },
  {
    code: "LOCAL_TRANSPORT_DETAIL",
    order: 6,
    title: "小交通明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
    ],
  },
  {
    code: "INTERCITY_TRANSPORT_DETAIL",
    order: 7,
    title: "大交通明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
    ],
  },
  {
    code: "MATERIAL_DETAIL",
    order: 8,
    title: "物料明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
      { key: "materialFile", label: "物料照片" },
    ],
  },
  {
    code: "DESIGN_DRAFT_DETAIL",
    order: 9,
    title: "设计稿明细",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "发票" },
      { key: "detailFile", label: "明细单" },
      { key: "designDraftFile", label: "设计稿" },
    ],
  },
  {
    code: "OTHER",
    order: 10,
    title: "其他",
    needAmount: true,
    maxUploadCount: 5,
    uploadFields: [
      { key: "invoiceFile", label: "其他附件1" },
      { key: "detailFile", label: "其他附件2" },
      { key: "equipmentFile", label: "其他附件3" },
      { key: "materialFile", label: "其他附件4" },
      { key: "designDraftFile", label: "其他附件5" },
    ],
  },
];
const meetingInvoiceSectionCodeSet = new Set<MeetingInvoiceSectionCode>(meetingInvoiceSectionDefs.map((x) => x.code));
const meetingInvoiceFieldKeySet = new Set<MeetingInvoiceUploadFieldKey>(["settlementFile", "invoiceFile", "detailFile", "equipmentFile", "materialFile", "designDraftFile"]);
const isMeetingInvoiceSectionCode = (value: string): value is MeetingInvoiceSectionCode =>
  meetingInvoiceSectionCodeSet.has(value as MeetingInvoiceSectionCode);
const isMeetingInvoiceFieldKey = (value: string): value is MeetingInvoiceUploadFieldKey =>
  meetingInvoiceFieldKeySet.has(value as MeetingInvoiceUploadFieldKey);
const materialModule = ref<
  MaterialModuleCode
>(
  "BASIC_INFO",
);
const expertReviewSubModule = ref<ExpertSubModuleCode>("ONSITE_PHOTO");
const selectedAuditExpertId = ref<number | null>(null);
const materialItemReviewMap = ref<Record<string, { reviewResult?: string; reviewReason?: string; updatedAt?: string }>>({});
const materialContent = ref("");
const basicView = ref<any>({
  chairmanExpertIds: [] as number[],
  speakerExpertIds: [] as number[],
  hostExpertIds: [] as number[],
  discussionGuestExpertIds: [] as number[],
  chairmanExperts: [] as Array<{ expertId: number; expertName?: string | null; hospital?: string | null }>,
  speakerExperts: [] as Array<{ expertId: number; expertName?: string | null; hospital?: string | null }>,
  hostExperts: [] as Array<{ expertId: number; expertName?: string | null; hospital?: string | null }>,
  discussionGuestExperts: [] as Array<{ expertId: number; expertName?: string | null; hospital?: string | null }>,
  guestCount: 0,
  attendeeCount: 0,
  attendeeActualCount: 0,
  targetAudience: "",
  mainAgenda: "",
  improvementSuggestion: "",
  meetingEffect: "",
});
const docView = ref<any>({
  agendas: [] as Array<{ name: string; ossKey: string; itemKey: string }>,
  signInName: "",
  signInOssKey: "",
  themePhotoName: "",
  themePhotoOssKey: "",
  invitations: [],
});
const photoView = ref<any>({
  photos: [],
  summary: "",
});
const fileUrlMap = ref<Record<string, string>>({});
const laborView = ref<any>({
  details: [],
});
const invoiceView = ref<any>({
  invoices: [],
  summary: "",
});
const expertProfileView = ref<any>({
  fileName: "",
  ossKey: "",
});
const meetingInvoiceView = ref<any>({
  sections: [] as Array<{
    sectionCode: MeetingInvoiceSectionCode;
    sectionTitle: string;
    needAmount: boolean;
    totalAmountYuan: number | null;
    files: Array<{
      fieldKey: MeetingInvoiceUploadFieldKey;
      label: string;
      rowLabel: string;
      fileName: string;
      ossKey: string;
      showFieldReviewControls: boolean;
    }>;
  }>,
});
const currentMaterialMeetingForm = ref("");
const materialBudgetView = ref({
  budgetCent: 0,
  laborTotalCent: 0,
  invoiceTotalCent: 0,
  meetingInvoiceTotalCent: 0,
  usedTotalCent: 0,
  remainCent: 0,
  overCent: 0,
  ready: false,
});
const expertReviewRows = computed(() => {
  const map = new Map<number, string>();
  const collect = (expertId: unknown, expertName: unknown) => {
    const id = Number(expertId || 0);
    if (id <= 0) {
      return;
    }
    const name = String(expertName || "").trim() || `专家#${id}`;
    if (!map.has(id)) {
      map.set(id, name);
    }
  };
  (photoView.value.photos || []).forEach((x: any) => collect(x?.expertId, x?.expertName));
  (laborView.value.details || []).forEach((x: any) => collect(x?.expertId, x?.expertName));
  (invoiceView.value.invoices || []).forEach((x: any) => collect(x?.expertId, x?.expertName));
  return Array.from(map.entries()).map(([expertId, expertName]) => ({ expertId, expertName }));
});
const selectedAuditExpertName = computed(() => {
  const id = Number(selectedAuditExpertId.value || 0);
  if (!id) {
    return "";
  }
  return expertReviewRows.value.find((x) => x.expertId === id)?.expertName || `专家#${id}`;
});
const selectedAuditPhotos = computed(() => {
  const id = Number(selectedAuditExpertId.value || 0);
  if (!id) {
    return [];
  }
  return (photoView.value.photos || []).filter((x: any) => Number(x?.expertId || 0) === id);
});
const selectedAuditLaborDetails = computed(() => {
  const id = Number(selectedAuditExpertId.value || 0);
  if (!id) {
    return [];
  }
  return (laborView.value.details || []).filter((x: any) => Number(x?.expertId || 0) === id);
});
const selectedAuditInvoices = computed(() => {
  const id = Number(selectedAuditExpertId.value || 0);
  if (!id) {
    return [];
  }
  return (invoiceView.value.invoices || []).filter((x: any) => Number(x?.expertId || 0) === id);
});

const detailStatusTagType = (status: unknown): "success" | "danger" | "warning" | "info" => {
  const code = String(status || "").toUpperCase();
  if (code === "APPROVED") {
    return "success";
  }
  if (code === "REJECTED") {
    return "danger";
  }
  if (code === "RETURNED" || code === "RETURN" || code === "BACK") {
    return "warning";
  }
  return "info";
};

const detailTimelineType = (status: unknown): "success" | "warning" | "danger" | "primary" | "info" => {
  const code = String(status || "").toUpperCase();
  if (code === "APPROVED") {
    return "success";
  }
  if (code === "REJECTED") {
    return "danger";
  }
  if (code === "RETURNED" || code === "RETURN" || code === "BACK") {
    return "warning";
  }
  return "primary";
};

const resolveAuditTime = (row: any) => {
  const value = String(
    row?.lastActionAt
    || row?.auditAt
    || row?.auditTime
    || row?.updatedAt
    || row?.handledAt
    || row?.completedAt
    || row?.createdAt
    || "",
  ).trim();
  if (value) {
    return value;
  }
  const status = String(row?.status || "").toUpperCase();
  if (status === "PENDING") {
    return "待处理";
  }
  return "-";
};

const resolveAssigneeName = (row: any) => {
  const apiName = String(row?.assigneeUserName || "").trim();
  if (apiName) {
    return apiName;
  }
  const id = Number(row?.assigneeUserId || 0);
  if (!id) {
    return "-";
  }
  return `用户#${id}`;
};

const auditProgressDialogVisible = ref(false);
const auditProgressRows = ref<any[]>([]);
const auditProgressPageNo = ref(1);
const auditProgressPageSize = ref(20);
const auditProgressTotal = ref(0);

const loadAuditProgress = async (meetingId: number) => {
  const resp = await fetchAuditTasks({
    mine: false,
    meetingId,
    pageNo: auditProgressPageNo.value,
    pageSize: auditProgressPageSize.value,
    sortBy: "lastActionAt",
    order: "desc",
  });
  auditProgressRows.value = Array.isArray(resp?.data?.list) ? resp.data.list : [];
  auditProgressTotal.value = Number(resp?.data?.total || 0);
  auditProgressPageNo.value = Number(resp?.data?.pageNo || auditProgressPageNo.value || 1);
  auditProgressPageSize.value = Number(resp?.data?.pageSize || auditProgressPageSize.value || 20);
};

const openAuditProgress = async (row: any) => {
  const meetingId = Number(row?.meetingId || 0);
  if (!meetingId) {
    return;
  }
  auditProgressPageNo.value = 1;
  auditProgressDialogVisible.value = true;
  auditProgressRows.value = [];
  auditProgressTotal.value = 0;
  try {
    await loadAuditProgress(meetingId);
  } catch (_e) {
    auditProgressRows.value = [];
    auditProgressTotal.value = 0;
    ElMessage.warning("当前账号暂无查看审核进度与意见权限");
  }
};

const handleAuditProgressPageChange = async (pageNo: number) => {
  auditProgressPageNo.value = Number(pageNo || 1);
  const current = auditProgressRows.value[0];
  const meetingId = Number(current?.meetingId || 0);
  if (!meetingId) {
    return;
  }
  await loadAuditProgress(meetingId);
};

const handleAuditProgressPageSizeChange = async (pageSize: number) => {
  auditProgressPageSize.value = Number(pageSize || 20);
  auditProgressPageNo.value = 1;
  const current = auditProgressRows.value[0];
  const meetingId = Number(current?.meetingId || 0);
  if (!meetingId) {
    return;
  }
  await loadAuditProgress(meetingId);
};
const formatYuan = (cent: unknown) => Number(((Number(cent) || 0) / 100).toFixed(2));
const statusFormatter = (_row: any, _column: any, value: unknown) => toZhStatus(value);
const auditNodeFormatter = (_row: any, _column: any, value: unknown) => toZhAuditNode(value);
const meetingNameFormatter = (row: any) => {
  const meetingId = Number(row?.meetingId || 0);
  if (!meetingId) {
    return "-";
  }
  return meetingNameMap.value[meetingId] || `会议#${meetingId}`;
};
const assigneeNameFormatter = (row: any) => {
  const apiName = String(row?.assigneeUserName || "").trim();
  if (apiName) {
    return apiName;
  }
  const userId = Number(row?.assigneeUserId || 0);
  if (!userId) {
    return "-";
  }
  return `用户#${userId}`;
};

const normalizeNodeKey = (value: unknown) => String(value ?? "").trim().toUpperCase();
const isHigherReviewNode = (value: unknown) => {
  const node = normalizeNodeKey(value);
  return node === "RE_REVIEW" || node === "FINAL_REVIEW";
};
const matchesAuditNode = (node: any, currentNode: unknown) => {
  const current = normalizeNodeKey(currentNode);
  if (!current) {
    return false;
  }
  const code = normalizeNodeKey(node?.nodeCode);
  const name = normalizeNodeKey(node?.nodeName);
  return code === current || name === current;
};
const resolveFlowNodes = (row: any) => {
  const rowNodes = Array.isArray(row?.flowNodes) ? row.flowNodes : [];
  return rowNodes;
};
const flowNodeTagType = (node: any, row: any): "success" | "warning" | "info" => {
  const nodes = resolveFlowNodes(row);
  const currentIndex = nodes.findIndex((item: any) => matchesAuditNode(item, row?.node));
  const idx = nodes.findIndex((item: any) => item === node);
  if (idx < 0 || currentIndex < 0) {
    return "info";
  }
  if (idx < currentIndex) {
    return "success";
  }
  if (idx === currentIndex) {
    return "warning";
  }
  return "info";
};

const loadNameMaps = async () => {
  try {
    const meetingResp = await fetchMeetings();
    const meetings = meetingResp?.data?.list || [];
    const meetingMap: Record<number, string> = {};
    const budgetMap: Record<number, number> = {};
    meetings.forEach((item: any) => {
      const id = Number(item?.id || 0);
      if (id > 0) {
        const projectName = String(item?.projectName || "").trim();
        const topic = String(item?.topic || "").trim();
        meetingMap[id] = projectName && topic ? `${projectName} / ${topic}` : topic || `会议#${id}`;
        budgetMap[id] = Math.max(0, Number(item?.budgetCent || 0));
      }
    });
    meetingNameMap.value = meetingMap;
    meetingBudgetMap.value = budgetMap;
  } catch (_e) {
    meetingNameMap.value = {};
    meetingBudgetMap.value = {};
  }
};

const load = async () => {
  const currentUserId = getCurrentUserId();
  if (!currentUserId) {
    rows.value = [];
    return;
  }

  if (activeTab.value === "pendingMine") {
    const resp = await fetchAuditTasks({
      mine: true,
      scope: "PENDING_MINE",
      pageNo: 1,
      pageSize: 100,
      sortBy: "lastActionAt",
      order: "desc",
    });
    rows.value = resp?.data?.list || [];
  } else {
    const resp = await fetchAuditTasks({
      mine: true,
      scope: "HANDLED_MINE",
      pageNo: 1,
      pageSize: 100,
      sortBy: "lastActionAt",
      order: "desc",
    });
    rows.value = resp?.data?.list || [];
  }

  if (canSlaRead.value && activeTab.value === "pendingMine") {
    const statResp = await fetchAuditSlaStat();
    slaStat.value = {
      pendingTotal: Number(statResp?.data?.pendingTotal || 0),
      timeout4h: Number(statResp?.data?.timeout4h || 0),
      timeout12h: Number(statResp?.data?.timeout12h || 0),
      timeout24h: Number(statResp?.data?.timeout24h || 0),
    };
  }
};

const handleTabChange = () => {
  load();
};

const handleAction = async (action: "approve" | "reject" | "return", taskId: number) => {
  const dialog = await ElMessageBox.prompt("请输入审核意见", "审核操作", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
  }).catch(() => null);
  if (!dialog) {
    return;
  }
  const payload = {
    idempotencyKey: `${action}-${Date.now()}-${taskId}`,
    opinion: dialog.value || "已处理",
  };
  if (action === "approve") {
    await approveAuditTask(taskId, payload);
    ElMessage.success("审核通过");
  } else if (action === "reject") {
    await rejectAuditTask(taskId, payload);
    ElMessage.success("审核拒绝");
  } else {
    await returnAuditTask(taskId, payload);
    ElMessage.success("已退回修改");
  }
  await load();
};

const handleSubmitAudit = async (taskId: number) => {
  if (submittingTaskId.value) {
    return;
  }
  submittingTaskId.value = taskId;
  try {
    const currentTask = rows.value.find((item: any) => Number(item?.id || 0) === taskId);
    const higherReview = isHigherReviewNode(currentTask?.node);
    if (higherReview) {
      let action: "approve" | "reject";
      try {
        await ElMessageBox.confirm(
          "请选择本次审核结果",
          "提交审核",
          {
            confirmButtonText: "审核通过",
            cancelButtonText: "审核拒绝",
            distinguishCancelAndClose: true,
            type: "warning",
          },
        );
        action = "approve";
      } catch (error) {
        if (error === "cancel") {
          action = "reject";
        } else {
          return;
        }
      }

      if (action === "approve" && !canApprove.value) {
        ElMessage.warning("\u60a8\u6ca1\u6709\u5ba1\u6838\u901a\u8fc7\u6743\u9650");
        return;
      }
      if (action === "reject" && !canReject.value) {
        ElMessage.warning("\u60a8\u6ca1\u6709\u5ba1\u6838\u62d2\u7edd\u6743\u9650");
        return;
      }

      const opinionDialog = await ElMessageBox.prompt(
        action === "approve" ? "\u8bf7\u8f93\u5165\u901a\u8fc7\u610f\u89c1" : "\u8bf7\u8f93\u5165\u62d2\u7edd\u539f\u56e0",
        action === "approve" ? "\u786e\u8ba4\u5ba1\u6838\u901a\u8fc7" : "\u786e\u8ba4\u5ba1\u6838\u62d2\u7edd",
        {
          confirmButtonText: "\u786e\u5b9a\u63d0\u4ea4",
          cancelButtonText: "\u53d6\u6d88",
          inputPlaceholder: "\u8bf7\u8f93\u5165\u5ba1\u6838\u610f\u89c1",
          type: action === "approve" ? "success" : "warning",
        },
      ).catch(() => null);

      if (!opinionDialog) {
        return;
      }

      const payload = {
        idempotencyKey: `${action}-${Date.now()}-${taskId}`,
        opinion: opinionDialog.value || (action === "approve" ? "\u5ba1\u6838\u901a\u8fc7" : "\u5ba1\u6838\u62d2\u7edd"),
      };

      if (action === "approve") {
        await approveAuditTask(taskId, payload);
        ElMessage.success("\u5ba1\u6838\u901a\u8fc7");
      } else {
        await rejectAuditTask(taskId, payload);
        ElMessage.success("\u5ba1\u6838\u62d2\u7edd");
      }
      await load();
      return;
    }

    const moduleCodes: BackendMaterialModuleCode[] = ["BASIC_INFO", "WRITE_OFF_DOCS", "EXPERT_PROFILE", "EXPERT_LIST", "MEETING_INVOICE"];
    const results = await Promise.allSettled(
      moduleCodes.map((code) => readAuditTaskMaterial(taskId, code)),
    );

    const rejectedItems: Array<{ itemLabel: string; reason: string }> = [];
    results.forEach((result) => {
      if (result.status !== "fulfilled") {
        return;
      }
      const reviews = Array.isArray(result.value?.data?.itemReviews) ? result.value.data.itemReviews : [];
      reviews.forEach((review: any) => {
        if (String(review?.reviewResult || "").toUpperCase() === "REJECTED") {
          rejectedItems.push({
            itemLabel: String(review?.itemLabel || review?.itemKey || "\u672a\u77e5\u6761\u76ee"),
            reason: String(review?.reviewReason || ""),
          });
        }
      });
    });

    let htmlMessage: string;
    let dialogTitle: string;
    let action: "approve" | "reject";

    if (rejectedItems.length === 0) {
      htmlMessage =
        '<p style="color:#67C23A;font-weight:600;">\u2705 \u6240\u6709\u5ba1\u6838\u6761\u76ee\u5747\u5df2\u901a\u8fc7</p>'
        + '<p>\u786e\u8ba4\u63d0\u4ea4\u5ba1\u6838<b>\u901a\u8fc7</b>\uff1f</p>';
      dialogTitle = "\u786e\u8ba4\u5ba1\u6838\u901a\u8fc7";
      action = "approve";
    } else {
      const itemsHtml = rejectedItems
        .map((item) => {
          const reason = item.reason
            ? `<span style="color:#909399;margin-left:4px;">\uff08${item.reason}\uff09</span>`
            : "";
          return `<li style="color:#F56C6C;margin:4px 0;">${item.itemLabel}${reason}</li>`;
        })
        .join("");
      htmlMessage =
        '<p style="font-weight:600;">\u4ee5\u4e0b\u6761\u76ee\u5ba1\u6838\u4e0d\u901a\u8fc7\uff1a</p>'
        + `<ul style="margin:8px 0;padding-left:20px;">${itemsHtml}</ul>`
        + '<p>\u786e\u8ba4\u63d0\u4ea4\u5ba1\u6838<b style="color:#F56C6C;">\u62d2\u7edd</b>\uff1f</p>';
      dialogTitle = "\u786e\u8ba4\u5ba1\u6838\u62d2\u7edd";
      action = "reject";
    }

    if (action === "approve" && !canApprove.value) {
      ElMessage.warning("\u60a8\u6ca1\u6709\u5ba1\u6838\u901a\u8fc7\u6743\u9650");
      return;
    }
    if (action === "reject" && !canReject.value) {
      ElMessage.warning("\u60a8\u6ca1\u6709\u5ba1\u6838\u62d2\u7edd\u6743\u9650");
      return;
    }

    const dialog = await ElMessageBox.prompt(htmlMessage, dialogTitle, {
      confirmButtonText: "\u786e\u5b9a\u63d0\u4ea4",
      cancelButtonText: "\u53d6\u6d88",
      dangerouslyUseHTMLString: true,
      inputPlaceholder: "\u8bf7\u8f93\u5165\u5ba1\u6838\u610f\u89c1",
      type: action === "approve" ? "success" : "warning",
    }).catch(() => null);

    if (!dialog) {
      return;
    }

    const payload = {
      idempotencyKey: `${action}-${Date.now()}-${taskId}`,
      opinion: dialog.value || (action === "approve" ? "\u5ba1\u6838\u901a\u8fc7" : "\u5ba1\u6838\u62d2\u7edd"),
    };

    if (action === "approve") {
      await approveAuditTask(taskId, payload);
      ElMessage.success("\u5ba1\u6838\u901a\u8fc7");
    } else {
      await rejectAuditTask(taskId, payload);
      ElMessage.success("\u5ba1\u6838\u62d2\u7edd");
    }
    await load();
  } catch (_e) {
    // cancelled dialog or network error
  } finally {
    submittingTaskId.value = null;
  }
};

const handleExport = async () => {
  const resp = await exportAuditOpinions();
  ElMessage.success(`导出成功，共 ${resp?.data?.total || 0} 条`);
};

const handleTransfer = async (row: any) => {
  const toUserDialog = await ElMessageBox.prompt("请输入目标处理人ID", "转审", {
    confirmButtonText: "下一步",
    cancelButtonText: "取消",
    inputPattern: /^\d+$/,
    inputErrorMessage: "请输入数字用户ID",
  }).catch(() => null);
  if (!toUserDialog) {
    return;
  }
  const reasonDialog = await ElMessageBox.prompt("请输入转审原因", "转审", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
  }).catch(() => null);
  if (!reasonDialog) {
    return;
  }
  await transferAuditTask(row.id, {
    idempotencyKey: `transfer-${Date.now()}-${row.id}`,
    toUserId: Number(toUserDialog.value),
    reason: reasonDialog.value || "工作量转交",
  });
  ElMessage.success("转审成功");
  await load();
};

const handleBatchRemind = async () => {
  const ids = selectedTaskIds.value;
  const resp = await batchRemindAuditTasks({
    idempotencyKey: `batch-remind-${Date.now()}`,
    taskIds: ids.length > 0 ? ids : undefined,
  });
  ElMessage.success(`批量催办已提交，目标 ${resp?.data?.taskCount || 0} 条`);
  await load();
};

const handleSelectionChange = (items: any[]) => {
  selectedTaskIds.value = (items || []).map((x) => Number(x.id)).filter((x) => !!x);
};

const handleSelectAuditExpert = (row: any) => {
  const expertId = Number(row?.expertId || 0);
  selectedAuditExpertId.value = expertId > 0 ? expertId : null;
};

const extractExpertBudgetUsage = (expertContentJson: string) => {
  const parsed = safeParse(expertContentJson || "{}");
  const laborRows = Array.isArray(parsed?.laborProtocol?.details) ? parsed.laborProtocol.details : [];
  const invoiceRows = Array.isArray(parsed?.invoiceDetail?.invoices) ? parsed.invoiceDetail.invoices : [];
  const laborTotalCent = laborRows.reduce((sum: number, row: any) => sum + Math.max(0, Number(row?.amountCent || 0)), 0);
  const invoiceTotalCent = invoiceRows.reduce((sum: number, row: any) => {
    const amount = Number(row?.invoiceAmountCent ?? row?.amountCent ?? 0);
    return sum + Math.max(0, amount);
  }, 0);
  return { laborTotalCent, invoiceTotalCent };
};

const extractMeetingInvoiceBudgetUsage = (
  meetingInvoiceContentJson: string,
  meetingFormRaw?: unknown,
) => {
  const parsed = safeParse(meetingInvoiceContentJson || "{}");
  const sections = normalizeMeetingInvoiceSections(
    parsed,
    filterMeetingInvoiceSectionDefsByMeetingForm(meetingFormRaw),
  );
  return sections.reduce((sum, section) => {
    if (!section.needAmount) {
      return sum;
    }
    const amountYuan = Number(section?.totalAmountYuan);
    if (!Number.isFinite(amountYuan) || amountYuan < 0) {
      return sum;
    }
    return sum + Math.round(amountYuan * 100);
  }, 0);
};

const refreshMaterialBudgetView = async () => {
  const taskId = Number(materialTaskId.value || 0);
  const meetingId = Number(materialMeetingId.value || 0);
  if (!taskId || !meetingId) {
    materialBudgetView.value = {
      budgetCent: 0,
      laborTotalCent: 0,
      invoiceTotalCent: 0,
      meetingInvoiceTotalCent: 0,
      usedTotalCent: 0,
      remainCent: 0,
      overCent: 0,
      ready: false,
    };
    return;
  }
  const [expertResp, meetingInvoiceResp, meetingDetailResp] = await Promise.allSettled([
    readAuditTaskMaterial(taskId, "EXPERT_LIST"),
    readAuditTaskMaterial(taskId, "MEETING_INVOICE"),
    fetchMeetingDetail(meetingId),
  ]);
  const expertContentJson = expertResp.status === "fulfilled"
    ? String(expertResp.value?.data?.material?.contentJson || "")
    : "";
  const meetingInvoiceContentJson = meetingInvoiceResp.status === "fulfilled"
    ? String(meetingInvoiceResp.value?.data?.material?.contentJson || "")
    : "";
  const meetingForm = meetingDetailResp.status === "fulfilled"
    ? String(meetingDetailResp.value?.data?.meetingForm || "").trim()
    : String(currentMaterialMeetingForm.value || "").trim();
  currentMaterialMeetingForm.value = meetingForm;
  const budgetCent = meetingDetailResp.status === "fulfilled"
    ? Math.max(0, Number(meetingDetailResp.value?.data?.budgetCent || 0))
    : Math.max(0, Number(meetingBudgetMap.value[meetingId] || 0));
  const { laborTotalCent, invoiceTotalCent } = extractExpertBudgetUsage(expertContentJson);
  const meetingInvoiceTotalCent = extractMeetingInvoiceBudgetUsage(meetingInvoiceContentJson, meetingForm);
  const usedTotalCent = laborTotalCent + invoiceTotalCent + meetingInvoiceTotalCent;
  const overCent = Math.max(0, usedTotalCent - budgetCent);
  const remainCent = Math.max(0, budgetCent - usedTotalCent);
  materialBudgetView.value = {
    budgetCent,
    laborTotalCent,
    invoiceTotalCent,
    meetingInvoiceTotalCent,
    usedTotalCent,
    remainCent,
    overCent,
    ready: true,
  };
};

const openMaterial = async (row: any) => {
  materialTaskId.value = Number(row?.id || 0) || null;
  materialMeetingId.value = Number(row?.meetingId || 0) || null;
  currentMaterialMeetingForm.value = String(row?.meetingForm || "").trim();
  materialAuditNode.value = String(row?.node || "").trim();
  materialModule.value = "BASIC_INFO";
  expertReviewSubModule.value = "ONSITE_PHOTO";
  selectedAuditExpertId.value = null;
  materialDialogVisible.value = true;
  await refreshMaterialBudgetView();
  await loadMaterial();
};

const handleMaterialTabChange = async (tabName: string | number) => {
  materialModule.value = String(tabName) as MaterialModuleCode;
  if (materialModule.value === "EXPERT_LIST") {
    expertReviewSubModule.value = "ONSITE_PHOTO";
  }
  await loadMaterial();
};

const resetMaterialViews = () => {
  basicView.value = {
    chairmanExpertIds: [],
    speakerExpertIds: [],
    hostExpertIds: [],
    discussionGuestExpertIds: [],
    chairmanExperts: [],
    speakerExperts: [],
    hostExperts: [],
    discussionGuestExperts: [],
    guestCount: 0,
    attendeeCount: 0,
    attendeeActualCount: 0,
    targetAudience: "",
    mainAgenda: "",
    improvementSuggestion: "",
    meetingEffect: "",
  };
  docView.value = { agendas: [], signInName: "", signInOssKey: "", themePhotoName: "", themePhotoOssKey: "", invitations: [] };
  photoView.value = { photos: [], summary: "" };
  laborView.value = { details: [] };
  invoiceView.value = { invoices: [], summary: "" };
  expertProfileView.value = { fileName: "", ossKey: "" };
  meetingInvoiceView.value = { sections: [] };
  selectedAuditExpertId.value = null;
  materialBudgetView.value = {
    budgetCent: 0,
    laborTotalCent: 0,
    invoiceTotalCent: 0,
    meetingInvoiceTotalCent: 0,
    usedTotalCent: 0,
    remainCent: 0,
    overCent: 0,
    ready: false,
  };
};

const toNumberArray = (value: unknown) => {
  if (!Array.isArray(value)) {
    return [];
  }
  return value.map((x) => Number(x || 0)).filter((x) => x > 0);
};
const extractExt = (fileName: string) => {
  const idx = (fileName || "").lastIndexOf(".");
  if (idx <= 0 || idx >= fileName.length - 1) {
    return "";
  }
  return fileName.substring(idx).toLowerCase();
};
const isImageFile = (fileNameOrKey: string) => {
  const ext = extractExt(fileNameOrKey || "");
  return [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"].includes(ext);
};
const isPdfFile = (fileNameOrKey: string) => extractExt(fileNameOrKey || "") === ".pdf";
const isImageAttachment = (name: unknown, ossKey: unknown) => isImageFile(String(name || "")) || isImageFile(String(ossKey || ""));
const isPdfAttachment = (name: unknown, ossKey: unknown) => isPdfFile(String(name || "")) || isPdfFile(String(ossKey || ""));
const getFileUrl = (ossKey: unknown) => {
  const key = String(ossKey || "");
  return fileUrlMap.value[key] || "";
};
const resolveReviewResult = (itemKey: string | string[]) => {
  if (Array.isArray(itemKey)) {
    for (const key of itemKey) {
      const result = String(materialItemReviewMap.value[String(key || "")]?.reviewResult || "").toUpperCase();
      if (result) {
        return result;
      }
    }
    return "";
  }
  return String(materialItemReviewMap.value[itemKey]?.reviewResult || "").toUpperCase();
};
const toReviewResultText = (itemKey: string | string[]) => {
  const result = resolveReviewResult(itemKey);
  if (result === "APPROVED") {
    return "已通过";
  }
  if (result === "REJECTED") {
    return "不通过";
  }
  return "未审核";
};
const toReviewTagType = (itemKey: string | string[]): "success" | "danger" | "info" => {
  const result = resolveReviewResult(itemKey);
  if (result === "APPROVED") {
    return "success";
  }
  if (result === "REJECTED") {
    return "danger";
  }
  return "info";
};
const buildPhotoItemKey = (row: any, idx: number) => `photo:${row?.ossKey || row?.originIndex || idx + 1}`;

type AuditLaborRoleKey = "chairman" | "speaker" | "host" | "discussionGuest";

const normalizeAuditLaborRoleKey = (raw: unknown): AuditLaborRoleKey | null => {
  const s = String(raw || "").trim().toLowerCase();
  if (s === "chairman" || s === "speaker" || s === "host" || s === "discussionguest") {
    return s === "discussionguest" ? "discussionGuest" : s;
  }
  if (s === "discussion_guest") {
    return "discussionGuest";
  }
  if (s === "discussion-guest") {
    return "discussionGuest";
  }
  return null;
};

/** 与会议端一致：有 role 时为 labor:expertId:role，否则 labor:expertId（兼容旧数据） */
const buildLaborItemKey = (row: any, idx: number) => {
  const id = Number(row?.expertId || 0) || idx + 1;
  const role = normalizeAuditLaborRoleKey(row?.role);
  if (role) {
    return `labor:${id}:${role}`;
  }
  return `labor:${id}`;
};

/** 审核状态解析：优先带角色的 key，回退到仅 expertId（历史审核记录） */
const buildLaborReviewItemKeys = (row: any, idx: number): string | string[] => {
  const id = Number(row?.expertId || 0) || idx + 1;
  const role = normalizeAuditLaborRoleKey(row?.role);
  if (role) {
    return [`labor:${id}:${role}`, `labor:${id}`];
  }
  return `labor:${id}`;
};
const buildInvoiceItemKey = (row: any, idx: number) => `invoice:${row?.invoiceNo || idx + 1}`;
const buildMeetingInvoiceFieldItemKey = (sectionCode: MeetingInvoiceSectionCode, fieldKey: MeetingInvoiceUploadFieldKey) =>
  `meeting_invoice:${sectionCode}:${fieldKey}`;
const buildMeetingInvoiceAmountItemKey = (sectionCode: MeetingInvoiceSectionCode) => `meeting_invoice:${sectionCode}:amount`;
const buildMeetingInvoiceFieldItemKeyForDrawer = (sectionCode: string, fieldKey: string) =>
  buildMeetingInvoiceFieldItemKey(sectionCode as MeetingInvoiceSectionCode, fieldKey as MeetingInvoiceUploadFieldKey);
const buildMeetingInvoiceAmountItemKeyForDrawer = (sectionCode: string) =>
  buildMeetingInvoiceAmountItemKey(sectionCode as MeetingInvoiceSectionCode);
const parseMeetingInvoiceAmountYuan = (raw: any) => {
  const yuan = Number(raw?.totalAmountYuan ?? raw?.totalFeeYuan);
  if (Number.isFinite(yuan) && yuan >= 0) {
    return yuan;
  }
  const cent = Number(raw?.totalAmountCent ?? raw?.totalFeeCent);
  if (Number.isFinite(cent) && cent >= 0) {
    return Number((cent / 100).toFixed(2));
  }
  return null;
};
const toMeetingInvoiceFile = (raw: any) => {
  const ossKey = String(raw?.ossKey || raw?.objectKey || "").trim();
  if (!ossKey) {
    return null;
  }
  return {
    fileName: String(raw?.fileName || raw?.name || "").trim(),
    ossKey,
  };
};

/** 与 MeetingPage normalizeMeetingInvoiceForm 一致：同一 fieldKey 可对应多条 files */
const collectMeetingInvoiceFieldFiles = (sectionRaw: any, fieldKey: MeetingInvoiceUploadFieldKey) => {
  const keyedRows = Array.isArray(sectionRaw?.files) ? sectionRaw.files : [];
  const out: Array<{ fileName: string; ossKey: string }> = [];
  const seen = new Set<string>();
  const pushUnique = (f: { fileName: string; ossKey: string } | null) => {
    if (!f?.ossKey || seen.has(f.ossKey)) {
      return;
    }
    seen.add(f.ossKey);
    out.push(f);
  };
  keyedRows.forEach((x: any) => {
    if (String(x?.fieldKey || x?.key || "").trim() === fieldKey) {
      pushUnique(toMeetingInvoiceFile(x));
    }
  });
  const legacyRaw = sectionRaw?.[fieldKey];
  const legacyList = Array.isArray(legacyRaw) ? legacyRaw : legacyRaw != null ? [legacyRaw] : [];
  legacyList.forEach((item: any) => pushUnique(toMeetingInvoiceFile(item)));
  return out;
};

const rowLabelForMeetingInvoiceField = (fieldLabel: string, index: number) => {
  const base = String(fieldLabel || "").trim() || "附件";
  if (index <= 0) {
    return base;
  }
  return `${base} (${index + 1})`;
};

const filterMeetingInvoiceSectionDefsByMeetingForm = (
  meetingFormRaw: unknown,
  sectionDefs: MeetingInvoiceSectionDef[] = meetingInvoiceSectionDefs,
) => {
  const meetingForm = String(meetingFormRaw || "").trim();
  if (meetingForm !== "线上") {
    return sectionDefs;
  }
  const otherSection = sectionDefs.find((section) => section.code === "OTHER");
  return otherSection ? [otherSection] : sectionDefs;
};

const normalizeMeetingInvoiceSections = (
  parsed: any,
  sectionDefs: MeetingInvoiceSectionDef[] = meetingInvoiceSectionDefs,
) => {
  const sectionList = Array.isArray(parsed?.sections) ? parsed.sections : [];
  if (sectionList.length > 0) {
    return sectionDefs.map((sectionDef) => {
      const sectionRaw = sectionList.find((x: any) => isMeetingInvoiceSectionCode(String(x?.sectionCode || x?.code || "").trim())
        && String(x?.sectionCode || x?.code || "").trim() === sectionDef.code) || {};
      const mappedFiles: Array<{
        fieldKey: MeetingInvoiceUploadFieldKey;
        label: string;
        rowLabel: string;
        fileName: string;
        ossKey: string;
        showFieldReviewControls: boolean;
      }> = [];
      sectionDef.uploadFields.forEach((fieldDef) => {
        const list = collectMeetingInvoiceFieldFiles(sectionRaw, fieldDef.key);
        if (list.length === 0) {
          mappedFiles.push({
            fieldKey: fieldDef.key,
            label: fieldDef.label,
            rowLabel: rowLabelForMeetingInvoiceField(fieldDef.label, 0),
            fileName: "",
            ossKey: "",
            showFieldReviewControls: true,
          });
          return;
        }
        list.forEach((file, idx) => {
          mappedFiles.push({
            fieldKey: fieldDef.key,
            label: fieldDef.label,
            rowLabel: rowLabelForMeetingInvoiceField(fieldDef.label, idx),
            fileName: file.fileName,
            ossKey: file.ossKey,
            showFieldReviewControls: idx === 0,
          });
        });
      });
      return {
        sectionCode: sectionDef.code,
        sectionTitle: `${sectionDef.order}、${sectionDef.title}`,
        needAmount: sectionDef.needAmount,
        totalAmountYuan: parseMeetingInvoiceAmountYuan(sectionRaw),
        files: mappedFiles,
      };
    });
  }
  const legacyAttachments = (Array.isArray(parsed?.attachments) ? parsed.attachments : [])
    .map((x: any) => toMeetingInvoiceFile(x))
    .filter((x: { fileName: string; ossKey: string } | null): x is { fileName: string; ossKey: string } => !!x);
  let cursor = 0;
  return sectionDefs.map((sectionDef) => {
    const files: Array<{
      fieldKey: MeetingInvoiceUploadFieldKey;
      label: string;
      rowLabel: string;
      fileName: string;
      ossKey: string;
      showFieldReviewControls: boolean;
    }> = [];
    sectionDef.uploadFields.forEach((fieldDef) => {
      const row = legacyAttachments[cursor];
      if (row) {
        cursor += 1;
      }
      files.push({
        fieldKey: fieldDef.key,
        label: fieldDef.label,
        rowLabel: rowLabelForMeetingInvoiceField(fieldDef.label, 0),
        fileName: row?.fileName || "",
        ossKey: row?.ossKey || "",
        showFieldReviewControls: true,
      });
    });
    return {
      sectionCode: sectionDef.code,
      sectionTitle: `${sectionDef.order}、${sectionDef.title}`,
      needAmount: sectionDef.needAmount,
      totalAmountYuan: null,
      files,
    };
  });
};
const loadFileUrls = async (items: Array<{ ossKey?: string }>) => {
  const map: Record<string, string> = {};
  const tasks = (items || [])
    .filter((item) => !!item?.ossKey)
    .map(async (item) => {
      const key = String(item.ossKey || "");
      try {
        const resp = await fetchFilePresignDownload({ objectKey: key });
        const url = resp?.data?.signedUrl || "";
        if (url) {
          map[key] = url;
        }
      } catch (_e) {
        // ignore preview failure
      }
    });
  if (tasks.length > 0) {
    await Promise.allSettled(tasks);
  }
  fileUrlMap.value = map;
};
const openFileUrl = (ossKey: unknown) => {
  const url = getFileUrl(ossKey);
  if (!url) {
    ElMessage.warning("暂未获取到文件查看链接");
    return;
  }
  window.open(url, "_blank");
};

const auditMaterialPreviewVisible = ref(false);
const auditMaterialPreviewUrl = ref("");
const auditMaterialPreviewKind = ref<"image" | "pdf">("image");

/** 与会议资料抽屉一致：弹窗内 iframe 预览 PDF 或大图预览图片 */
const openAuditMaterialDocPreview = async (ossKey: unknown, fileNameHint?: string) => {
  const key = String(ossKey || "").trim();
  if (!key) {
    return;
  }
  let url = getFileUrl(key);
  if (!url) {
    try {
      const resp = await fetchFilePresignDownload({ objectKey: key });
      url = String(resp?.data?.signedUrl || "").trim();
      if (url) {
        fileUrlMap.value = { ...fileUrlMap.value, [key]: url };
      }
    } catch (_e) {
      // ignore
    }
  }
  if (!url) {
    ElMessage.warning("暂未获取到文件查看链接");
    return;
  }
  auditMaterialPreviewKind.value = isPdfAttachment(fileNameHint, key) ? "pdf" : "image";
  auditMaterialPreviewUrl.value = url;
  auditMaterialPreviewVisible.value = true;
};

const handleApproveCurrentModule = async () => {
  if (!materialTaskId.value || approvingCurrentModule.value) {
    return;
  }
  const backendModuleCode = resolveBackendMaterialModuleCode(materialModule.value);
  approvingCurrentModule.value = true;
  try {
    const resp = await approveAuditMaterialModule(materialTaskId.value, {
      idempotencyKey: `material-module-approve-${Date.now()}-${materialTaskId.value}-${materialModule.value}`,
      moduleCode: backendModuleCode,
    });
    const itemCount = Number(resp?.data?.itemCount || 0);
    const savedCount = Number(resp?.data?.savedCount || 0);
    const skippedRejected = Number(resp?.data?.skippedRejected || 0);
    if (itemCount <= 0) {
      ElMessage.warning("当前模块没有可审核条目");
    } else if (savedCount <= 0 && skippedRejected > 0) {
      ElMessage.warning(`本模块所有条目均已标记不通过，无需批量通过`);
    } else if (savedCount <= 0) {
      ElMessage.warning("未写入审核结果，请刷新后重试");
    } else if (skippedRejected > 0) {
      ElMessage.success(`本模块审核通过（${savedCount}/${itemCount}），${skippedRejected}项不通过已保留`);
    } else {
      ElMessage.success(`本模块审核通过（${savedCount}/${itemCount}）`);
    }
    await loadMaterial();
  } finally {
    approvingCurrentModule.value = false;
  }
};

const handleRejectMaterialItem = async (itemKey: string, itemLabel: string) => {
  if (!materialTaskId.value) {
    return;
  }
  const backendModuleCode = resolveBackendMaterialModuleCode(materialModule.value);
  const dialog = await ElMessageBox.prompt(`请填写「${itemLabel}」不通过原因`, "资料条目不通过", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    inputType: "textarea",
    inputPlaceholder: "请输入不通过原因",
  }).catch(() => null);
  if (!dialog) {
    return;
  }
  await rejectAuditMaterialItem(materialTaskId.value, {
    idempotencyKey: `material-item-reject-${Date.now()}-${materialTaskId.value}`,
    moduleCode: backendModuleCode,
    itemKey,
    itemLabel,
    reason: dialog.value || "资料条目不符合要求",
  });
  ElMessage.success("已记录条目不通过");
  await loadMaterial();
};

const loadMaterial = async () => {
  if (!materialTaskId.value) {
    return;
  }
  const backendModuleCode = resolveBackendMaterialModuleCode(materialModule.value);
  try {
    const resp = await readAuditTaskMaterial(materialTaskId.value, backendModuleCode);
    let basicInfoResp: Awaited<ReturnType<typeof readAuditTaskMaterial>> | null = null;
    if (materialModule.value === "EXPERT_LIST") {
      try {
        basicInfoResp = await readAuditTaskMaterial(materialTaskId.value, "BASIC_INFO");
      } catch (_e) {
        basicInfoResp = null;
      }
    }
    const raw = resp?.data?.material?.contentJson || "{}";
    const reviewRows = Array.isArray(resp?.data?.itemReviews) ? resp.data.itemReviews : [];
    const reviewMap: Record<string, { reviewResult?: string; reviewReason?: string; updatedAt?: string }> = {};
    reviewRows.forEach((row: any) => {
      const key = String(row?.itemKey || "");
      if (!key) {
        return;
      }
      reviewMap[key] = {
        reviewResult: String(row?.reviewResult || ""),
        reviewReason: String(row?.reviewReason || ""),
        updatedAt: String(row?.updatedAt || ""),
      };
    });
    materialItemReviewMap.value = reviewMap;
    materialContent.value = raw;
    const parsed = safeParse(raw);
    resetMaterialViews();
    fileUrlMap.value = {};
    if (materialModule.value === "BASIC_INFO") {
      const bio = resp?.data?.basicInfoExperts;
      basicView.value = {
        chairmanExpertIds: toNumberArray(parsed.chairmanExpertIds),
        speakerExpertIds: toNumberArray(parsed.speakerExpertIds),
        hostExpertIds: toNumberArray(parsed.hostExpertIds),
        discussionGuestExpertIds: toNumberArray(parsed.discussionGuestExpertIds),
        chairmanExperts: Array.isArray(bio?.chairman) ? bio.chairman : [],
        speakerExperts: Array.isArray(bio?.speaker) ? bio.speaker : [],
        hostExperts: Array.isArray(bio?.host) ? bio.host : [],
        discussionGuestExperts: Array.isArray(bio?.discussionGuest) ? bio.discussionGuest : [],
        guestCount: Number(parsed.guestCount || 0),
        attendeeCount: Number(parsed.attendeeCount || 0),
        attendeeActualCount: Number(parsed.attendeeActualCount || 0),
        targetAudience: parsed.targetAudience || "",
        mainAgenda: parsed.mainAgenda || "",
        improvementSuggestion: parsed.improvementSuggestion || "",
        meetingEffect: parsed.meetingEffect || "",
      };
    } else if (materialModule.value === "WRITE_OFF_DOCS") {
      const agendaRaw = parsed.agenda;
      let agendas: Array<{ name: string; ossKey: string; itemKey: string }> = [];
      if (Array.isArray(agendaRaw)) {
        agendas = agendaRaw
          .map((x: any, i: number) => ({
            name: String(x?.name || "").trim(),
            ossKey: String(x?.ossKey || "").trim(),
            itemKey: `agenda:${i + 1}`,
          }))
          .filter((x: { ossKey: string }) => !!x.ossKey);
      } else if (agendaRaw && typeof agendaRaw === "object" && String((agendaRaw as { ossKey?: string }).ossKey || "").trim()) {
        const a = agendaRaw as { name?: string; ossKey?: string };
        agendas = [{
          name: String(a.name || "").trim(),
          ossKey: String(a.ossKey || "").trim(),
          itemKey: "agenda",
        }];
      }
      docView.value = {
        agendas,
        signInName: parsed.signInSheet?.name || "",
        signInOssKey: parsed.signInSheet?.ossKey || "",
        themePhotoName: parsed.themePhoto?.name || parsed.themePhoto?.fileName || "",
        themePhotoOssKey: parsed.themePhoto?.ossKey || "",
        invitations: Array.isArray(parsed.invitation) ? parsed.invitation : [],
      };
      expertProfileView.value = {
        fileName: String(parsed?.profileFile?.name || parsed?.fileName || "").trim(),
        ossKey: String(parsed?.profileFile?.ossKey || parsed?.ossKey || "").trim(),
      };
      const items = [
        ...docView.value.agendas.map((x: { ossKey: string }) => ({ ossKey: x?.ossKey || "" })),
        { ossKey: docView.value.signInOssKey },
        { ossKey: docView.value.themePhotoOssKey },
        ...docView.value.invitations.map((x: any) => ({ ossKey: x?.ossKey || "" })),
      ].filter((x) => !!x.ossKey);
      await loadFileUrls(items);
    } else if (materialModule.value === "EXPERT_PROFILE") {
      expertProfileView.value = {
        fileName: String(parsed?.profileFile?.name || parsed?.fileName || "").trim(),
        ossKey: String(parsed?.profileFile?.ossKey || parsed?.ossKey || "").trim(),
      };
      await loadFileUrls([{ ossKey: expertProfileView.value.ossKey }]);
    } else if (materialModule.value === "EXPERT_LIST") {
      if (basicInfoResp) {
        const basicRaw = String(basicInfoResp?.data?.material?.contentJson || "{}");
        const basicParsed = safeParse(basicRaw);
        const bio = basicInfoResp?.data?.basicInfoExperts;
        basicView.value = {
          chairmanExpertIds: toNumberArray(basicParsed.chairmanExpertIds),
          speakerExpertIds: toNumberArray(basicParsed.speakerExpertIds),
          hostExpertIds: toNumberArray(basicParsed.hostExpertIds),
          discussionGuestExpertIds: toNumberArray(basicParsed.discussionGuestExpertIds),
          chairmanExperts: Array.isArray(bio?.chairman) ? bio.chairman : [],
          speakerExperts: Array.isArray(bio?.speaker) ? bio.speaker : [],
          hostExperts: Array.isArray(bio?.host) ? bio.host : [],
          discussionGuestExperts: Array.isArray(bio?.discussionGuest) ? bio.discussionGuest : [],
          guestCount: Number(basicParsed.guestCount || 0),
          attendeeCount: Number(basicParsed.attendeeCount || 0),
          attendeeActualCount: Number(basicParsed.attendeeActualCount || 0),
          targetAudience: basicParsed.targetAudience || "",
          mainAgenda: basicParsed.mainAgenda || "",
          improvementSuggestion: basicParsed.improvementSuggestion || "",
          meetingEffect: basicParsed.meetingEffect || "",
        };
      }
      const onsitePhoto = parsed?.onsitePhoto || {};
      const laborProtocol = parsed?.laborProtocol || {};
      const invoiceDetail = parsed?.invoiceDetail || {};
      const photos = Array.isArray(onsitePhoto.photos) ? onsitePhoto.photos : [];
      photoView.value = {
        photos: photos.map((x: any, index: number) => ({
          expertId: Number(x?.expertId || 0),
          expertName: x?.expertName || (x?.expertId ? `专家#${x.expertId}` : "-"),
          name: x?.name || x?.fileName || "",
          ossKey: x?.ossKey || "",
          originIndex: index + 1,
        })),
        summary: onsitePhoto.summary || "",
      };
      laborView.value = {
        details: (Array.isArray(laborProtocol.details) ? laborProtocol.details : []).map((x: any) => {
          const invoiceFiles = Array.isArray(x?.invoiceFiles) ? x.invoiceFiles : [];
          const primaryInvoiceFile = invoiceFiles[0] || x?.invoiceFile || {};
          return {
          expertId: Number(x?.expertId || 0),
          expertName: x?.expertName || (x?.expertId ? `专家#${x.expertId}` : "-"),
          role: normalizeAuditLaborRoleKey(x?.role) ?? "",
          protocolName: x?.protocolFile?.name || "",
          protocolOssKey: x?.protocolFile?.ossKey || "",
          invoiceFiles: invoiceFiles.map((file: any) => ({
            fileName: file?.fileName || file?.name || "",
            ossKey: file?.ossKey || "",
          })),
          invoiceFileName: primaryInvoiceFile?.name || primaryInvoiceFile?.fileName || "",
          invoiceOssKey: primaryInvoiceFile?.ossKey || "",
          amountCent: Number(x?.amountCent || 0),
          remark: x?.remark || "",
          };
        }),
      };
      invoiceView.value = {
        invoices: (Array.isArray(invoiceDetail.invoices) ? invoiceDetail.invoices : []).map((x: any, index: number) => {
          const firstFile = Array.isArray(x?.files) && x.files.length > 0 ? x.files[0] : (x?.file || {});
          return {
            expertId: Number(x?.expertId || 0),
            expertName: x?.expertName || (x?.expertId ? `专家#${x.expertId}` : "-"),
            expenseType: x?.expenseType || "-",
            invoiceNo: x?.invoiceNo || "",
            amountCent: Number(x?.invoiceAmountCent ?? x?.amountCent ?? 0),
            taxCent: Number(x?.taxAmountCent ?? x?.taxCent ?? 0),
            fileName: firstFile?.fileName || firstFile?.name || "",
            ossKey: firstFile?.ossKey || "",
            originIndex: index + 1,
          };
        }),
        summary: invoiceDetail.summary || "",
      };
      const items = [
        ...photoView.value.photos.map((x: any) => ({ ossKey: x?.ossKey || "" })),
        ...laborView.value.details.map((x: any) => ({ ossKey: x?.protocolOssKey || "" })),
        ...laborView.value.details.map((x: any) => ({ ossKey: x?.invoiceOssKey || "" })),
        ...invoiceView.value.invoices.map((x: any) => ({ ossKey: x?.ossKey || "" })),
      ];
      await loadFileUrls(items);
      const expertIds = expertReviewRows.value.map((x) => Number(x.expertId || 0)).filter((x) => x > 0);
      if (expertIds.length > 0) {
        selectedAuditExpertId.value = expertIds.includes(Number(selectedAuditExpertId.value || 0))
          ? Number(selectedAuditExpertId.value)
          : expertIds[0];
      }
    } else {
      const sections = normalizeMeetingInvoiceSections(
        parsed,
        filterMeetingInvoiceSectionDefsByMeetingForm(currentMaterialMeetingForm.value),
      );
      meetingInvoiceView.value = { sections };
      const items = sections.flatMap((section) => section.files.map((x) => ({ ossKey: x?.ossKey || "" })));
      await loadFileUrls(items);
    }
  } catch (_e) {
    materialContent.value = "{}";
    materialItemReviewMap.value = {};
    resetMaterialViews();
    fileUrlMap.value = {};
  }
};

const safeParse = (val: string) => {
  try {
    return JSON.parse(val || "{}");
  } catch (_e) {
    return {};
  }
};

onMounted(async () => {
  await Promise.all([loadNameMaps(), load()]);
});
</script>
