package com.writeoff.module.audit.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.module.audit.dto.AuditActionRequest;
import com.writeoff.module.audit.dto.AuditMaterialItemRejectRequest;
import com.writeoff.module.audit.dto.AuditMaterialModuleApproveRequest;
import com.writeoff.module.audit.dto.BatchAuditActionRequest;
import com.writeoff.module.audit.dto.BatchRemindRequest;
import com.writeoff.module.audit.dto.TransferAuditTaskRequest;
import com.writeoff.module.audit.model.AuditNode;
import com.writeoff.module.audit.model.AuditFlowNodeInfo;
import com.writeoff.module.audit.model.AuditTask;
import com.writeoff.module.audit.model.AuditTaskStatus;
import com.writeoff.module.audit.repository.AuditTaskRepository;
import com.writeoff.module.notification.dto.DispatchNotificationRequest;
import com.writeoff.module.notification.service.NotificationDispatchService;
import com.writeoff.module.meeting.model.MeetingAuditStatus;
import com.writeoff.module.meeting.model.MeetingMaterial;
import com.writeoff.module.expert.service.ExpertService;
import com.writeoff.module.meeting.service.MeetingMaterialService;
import com.writeoff.module.meeting.service.MeetingService;
import com.writeoff.module.scheduler.service.AsyncJobService;
import com.writeoff.module.system.service.DataPermissionService;
import com.writeoff.security.AuthContext;
import com.writeoff.security.AuthScope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
public class AuditService {
    private static final Logger log = LoggerFactory.getLogger(AuditService.class);
    private final AuditTaskRepository auditTaskRepository;
    private final MeetingService meetingService;
    private final MeetingMaterialService meetingMaterialService;
    private final AsyncJobService asyncJobService;
    private final AuditFlowConfigService auditFlowConfigService;
    private final DataPermissionService dataPermissionService;
    private final NotificationDispatchService notificationDispatchService;
    private final JdbcTemplate jdbcTemplate;
    private final ExpertService expertService;
    private final Map<String, Long> actionIdempotency = new ConcurrentHashMap<>();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    public AuditService(AuditTaskRepository auditTaskRepository, MeetingService meetingService, MeetingMaterialService meetingMaterialService, AsyncJobService asyncJobService, AuditFlowConfigService auditFlowConfigService, DataPermissionService dataPermissionService, NotificationDispatchService notificationDispatchService, JdbcTemplate jdbcTemplate, ExpertService expertService) {
        this.auditTaskRepository = auditTaskRepository;
        this.meetingService = meetingService;
        this.meetingMaterialService = meetingMaterialService;
        this.asyncJobService = asyncJobService;
        this.auditFlowConfigService = auditFlowConfigService;
        this.dataPermissionService = dataPermissionService;
        this.notificationDispatchService = notificationDispatchService;
        this.jdbcTemplate = jdbcTemplate;
        this.expertService = expertService;
    }

    public AuditService(AuditTaskRepository auditTaskRepository, MeetingService meetingService, AsyncJobService asyncJobService) {
        this(auditTaskRepository, meetingService, null, asyncJobService, null, null, null, null, null);
    }

    public PageResult<AuditTask> listTasks(boolean mine) {
        List<AuditTask> filtered = filterTasks(mine, null, null);
        return new PageResult<>(filtered, filtered.size(), 1, filtered.size() == 0 ? 20 : filtered.size());
    }

    public PageResult<AuditTask> listTasks(boolean mine, String scope) {
        List<AuditTask> filtered = filterTasks(mine, scope, null);
        return new PageResult<>(filtered, filtered.size(), 1, filtered.size() == 0 ? 20 : filtered.size());
    }

    public PageResult<AuditTask> listTasks(
            boolean mine,
            String scope,
            Long meetingId,
            Integer pageNo,
            Integer pageSize,
            String sortBy,
            String order
    ) {
        List<AuditTask> filtered = filterTasks(mine, scope, meetingId);
        sortTasks(filtered, sortBy, order);
        int normalizedPageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
        int normalizedPageSize = pageSize == null || pageSize < 1 ? 20 : Math.min(pageSize, 200);
        int from = Math.max((normalizedPageNo - 1) * normalizedPageSize, 0);
        if (from >= filtered.size()) {
            return new PageResult<>(Collections.emptyList(), filtered.size(), normalizedPageNo, normalizedPageSize);
        }
        int to = Math.min(from + normalizedPageSize, filtered.size());
        return new PageResult<>(filtered.subList(from, to), filtered.size(), normalizedPageNo, normalizedPageSize);
    }

    private List<AuditTask> filterTasks(boolean mine, String scope, Long meetingId) {
        List<AuditTask> list = auditTaskRepository.findAll();
        if (dataPermissionService != null) {
            DataPermissionService.DataScope dataScope = dataPermissionService.resolveCurrentUserScope();
            Set<Long> meetingIds = list.stream().map(AuditTask::getMeetingId).collect(Collectors.toCollection(HashSet::new));
            Map<Long, Long> meetingCreatorMap = dataPermissionService.listMeetingCreators(meetingIds);
            Map<Long, Long> meetingProjectMap = dataPermissionService.listMeetingProjectIds(meetingIds);
            Set<Long> projectIds = new HashSet<>(meetingProjectMap.values());
            Map<Long, Long> projectCreatorMap = dataPermissionService.listProjectCreators(projectIds);
            list = list.stream()
                    .filter(task -> {
                        try {
                            Long projectId = meetingProjectMap.get(task.getMeetingId());
                            Long meetingCreatedBy = meetingCreatorMap.get(task.getMeetingId());
                            Long projectCreatedBy = projectId == null ? null : projectCreatorMap.get(projectId);
                            return dataPermissionService.canAccessMeeting(task.getMeetingId(), projectId, meetingCreatedBy, projectCreatedBy, dataScope);
                        } catch (Exception e) {
                            return false;
                        }
                    })
                    .collect(Collectors.toList());
        }
        if (meetingId != null) {
            list = list.stream()
                    .filter(task -> meetingId.equals(task.getMeetingId()))
                    .collect(Collectors.toList());
        }
        if (mine) {
            Long currentUserId = AuthContext.userId();
            String normalizedScope = String.valueOf(scope == null ? "" : scope).trim().toUpperCase(Locale.ROOT);
            if ("HANDLED_MINE".equals(normalizedScope)) {
                // 我处理过的：我曾是处理人，且任务已不在待处理状态
                list = list.stream()
                        .filter(task -> task.getAssigneeUserId() != null && task.getAssigneeUserId().equals(currentUserId))
                        .filter(task -> task.getStatus() != AuditTaskStatus.PENDING)
                        .collect(Collectors.toList());
            } else {
                // 默认：待我处理
                list = list.stream()
                        .filter(task -> task.getAssigneeUserId() != null && task.getAssigneeUserId().equals(currentUserId))
                        .filter(task -> task.getStatus() == AuditTaskStatus.PENDING)
                        .collect(Collectors.toList());
            }
        }
        fillAssigneeUserName(list);
        fillFlowNodes(list);
        return new ArrayList<>(list);
    }

    private void fillAssigneeUserName(List<AuditTask> list) {
        if (list == null || list.isEmpty() || jdbcTemplate == null) {
            return;
        }
        Set<Long> userIds = list.stream()
                .map(AuditTask::getAssigneeUserId)
                .filter(id -> id != null && id > 0)
                .collect(Collectors.toSet());
        if (userIds.isEmpty()) {
            return;
        }
        String placeholders = userIds.stream().map(id -> "?").collect(Collectors.joining(","));
        List<Object> args = new ArrayList<>();
        String sql;
        if (AuthContext.scope() == AuthScope.PLATFORM) {
            sql = "SELECT id, user_name FROM platform_user WHERE is_deleted=0 AND id IN (" + placeholders + ")";
            args.addAll(userIds);
        } else {
            sql = "SELECT id, user_name FROM sys_user WHERE tenant_id=? AND is_deleted=0 AND id IN (" + placeholders + ")";
            args.add(tenantId());
            args.addAll(userIds);
        }
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, args.toArray());
        Map<Long, String> userNameMap = new HashMap<>();
        for (Map<String, Object> row : rows) {
            Number idNum = (Number) row.get("id");
            if (idNum == null) {
                continue;
            }
            Long id = idNum.longValue();
            String userName = String.valueOf(row.get("user_name") == null ? "" : row.get("user_name")).trim();
            if (!userName.isEmpty()) {
                userNameMap.put(id, userName);
            }
        }
        for (AuditTask task : list) {
            Long assigneeUserId = task.getAssigneeUserId();
            if (assigneeUserId == null || assigneeUserId <= 0) {
                continue;
            }
            String userName = userNameMap.get(assigneeUserId);
            if (userName != null && !userName.isEmpty()) {
                task.setAssigneeUserName(userName);
            }
        }
    }

    private void fillFlowNodes(List<AuditTask> list) {
        if (list == null || list.isEmpty()) {
            return;
        }
        List<AuditFlowNodeInfo> nodes = buildFlowNodes();
        if (nodes.isEmpty()) {
            return;
        }
        for (AuditTask task : list) {
            task.setFlowNodes(nodes);
        }
    }

    private List<AuditFlowNodeInfo> buildFlowNodes() {
        List<AuditNode> enabledNodes;
        if (auditFlowConfigService == null) {
            enabledNodes = new ArrayList<>();
            enabledNodes.add(AuditNode.INIT_REVIEW);
            enabledNodes.add(AuditNode.RE_REVIEW);
            enabledNodes.add(AuditNode.FINAL_REVIEW);
        } else {
            enabledNodes = auditFlowConfigService.getEnabledNodes(tenantId());
        }
        List<AuditFlowNodeInfo> result = new ArrayList<>();
        for (int i = 0; i < enabledNodes.size(); i++) {
            AuditNode node = enabledNodes.get(i);
            if (node == null) {
                continue;
            }
            String code = node.name();
            result.add(new AuditFlowNodeInfo(null, code, code, i + 1, "ENABLED", null, null));
        }
        return result.stream().filter(Objects::nonNull).collect(Collectors.toList());
    }

    private void sortTasks(List<AuditTask> list, String sortBy, String order) {
        String normalizedSortBy = String.valueOf(sortBy == null ? "lastActionAt" : sortBy).trim().toLowerCase(Locale.ROOT);
        String normalizedOrder = String.valueOf(order == null ? "desc" : order).trim().toLowerCase(Locale.ROOT);
        Comparator<AuditTask> comparator;
        if ("id".equals(normalizedSortBy)) {
            comparator = Comparator.comparing(task -> task.getId() == null ? 0L : task.getId());
        } else if ("sladeadlineat".equals(normalizedSortBy)) {
            comparator = Comparator.comparing(task -> String.valueOf(task.getSlaDeadlineAt() == null ? "" : task.getSlaDeadlineAt()));
        } else {
            comparator = Comparator
                    .comparing((AuditTask task) -> String.valueOf(task.getLastActionAt() == null ? "" : task.getLastActionAt()))
                    .thenComparing(task -> task.getId() == null ? 0L : task.getId());
        }
        if (!"asc".equals(normalizedOrder)) {
            comparator = comparator.reversed();
        }
        list.sort(comparator);
    }

    public PageResult<AuditTask> listTasks() {
        return listTasks(false);
    }

    public Map<String, Object> approve(Long taskId, AuditActionRequest request) {
        AuditTask task = getPendingTask(taskId, request.getIdempotencyKey());
        task.setStatus(AuditTaskStatus.APPROVED);
        task.setOpinion(request.getOpinion());
        auditTaskRepository.save(task);
        Long meetingId = task.getMeetingId();

        AuditNode nextNode;
        if (auditFlowConfigService == null) {
            if (task.getNode() == AuditNode.INIT_REVIEW) {
                nextNode = AuditNode.RE_REVIEW;
            } else if (task.getNode() == AuditNode.RE_REVIEW) {
                nextNode = AuditNode.FINAL_REVIEW;
            } else {
                nextNode = null;
            }
        } else {
            nextNode = auditFlowConfigService.nextNode(tenantId(), task.getNode());
        }
        if (nextNode == null) {
            meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.APPROVED);
            asyncJobService.enqueue("EXPORT_REPORT", "meetingId=" + meetingId, "job-export-report-" + meetingId);
            triggerAuditNotification(task, request.getOpinion(), "AUDIT_APPROVED");
        } else {
            Long nextAssignee = auditFlowConfigService == null ? null : auditFlowConfigService.resolveAssigneeUserId(tenantId(), nextNode);
            auditTaskRepository.save(new AuditTask(null, meetingId, nextNode, nextAssignee, AuditTaskStatus.PENDING, ""));
            // 同步会议当前节点和审核人信息，保证前端展示与任务一致
            meetingService.updateCurrentAuditNode(meetingId, nextNode.name(), nextAssignee);
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("taskId", task.getId());
        result.put("nodeStatus", task.getStatus().name());
        return result;
    }

    public Map<String, Object> reject(Long taskId, AuditActionRequest request) {
        AuditTask task = getPendingTask(taskId, request.getIdempotencyKey());
        task.setStatus(AuditTaskStatus.REJECTED);
        task.setOpinion(request.getOpinion());
        auditTaskRepository.save(task);
        Long meetingId = task.getMeetingId();

        if (task.getNode() == AuditNode.INIT_REVIEW) {
            // 初审拒绝：整体审核结束，会议审核状态置为已拒绝
            meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.REJECTED);
            triggerAuditNotification(task, request.getOpinion(), "AUDIT_REJECTED");
        } else {
            // 复审 / 终审拒绝：整体回到初审阶段，重新创建初审任务
            AuditNode firstNode;
            Long assigneeUserId;
            if (auditFlowConfigService == null) {
                firstNode = AuditNode.INIT_REVIEW;
                assigneeUserId = null;
            } else {
                Long tenantId = tenantId();
                firstNode = auditFlowConfigService.firstNode(tenantId);
                assigneeUserId = auditFlowConfigService.resolveAssigneeUserId(tenantId, firstNode);
            }

            meetingService.updateAuditStatus(meetingId, MeetingAuditStatus.IN_REVIEW);
            meetingService.updateCurrentAuditNode(meetingId, firstNode.name(), assigneeUserId);
            auditTaskRepository.save(new AuditTask(
                    null,
                    meetingId,
                    firstNode,
                    assigneeUserId,
                    AuditTaskStatus.PENDING,
                    ""
            ));
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("taskId", task.getId());
        result.put("nodeStatus", task.getStatus().name());
        return result;
    }

    public Map<String, Object> back(Long taskId, AuditActionRequest request) {
        AuditTask task = getPendingTask(taskId, request.getIdempotencyKey());
        task.setStatus(AuditTaskStatus.REJECTED);
        task.setOpinion("退回修改:" + request.getOpinion());
        auditTaskRepository.save(task);
        meetingService.updateAuditStatus(task.getMeetingId(), MeetingAuditStatus.PENDING);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("taskId", task.getId());
        result.put("nodeStatus", "RETURNED");
        return result;
    }

    public Map<String, Object> exportOpinions() {
        if (dataPermissionService != null && !dataPermissionService.canExportCurrentUser()) {
            throw new BusinessException(20001, "当前账号无导出权限");
        }
        List<AuditTask> list = listTasks(false).getList();
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("total", list.size());
        data.put("records", list);
        return data;
    }

    public Map<String, Object> readTaskMaterial(Long taskId, String moduleCode) {
        AuditTask task = auditTaskRepository.findById(taskId)
                .orElseThrow(() -> new BusinessException(10003, "审核任务不存在"));
        if (meetingMaterialService == null) {
            throw new BusinessException(10001, "资料服务不可用");
        }
        MeetingMaterial material = meetingMaterialService.current(task.getMeetingId(), moduleCode);
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("taskId", taskId);
        data.put("meetingId", task.getMeetingId());
        data.put("moduleCode", moduleCode);
        data.put("material", material);
        data.put("materialItems", meetingMaterialService.listMaterialReviewItems(task.getMeetingId(), moduleCode));
        data.put("itemReviews", meetingMaterialService.listMaterialItemReviews(taskId, task.getNode().name(), moduleCode));
        attachBasicInfoExpertSummaries(data, material, moduleCode);
        return data;
    }

    private Map<String, Object> emptyBasicInfoExpertsPayload() {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("chairman", Collections.emptyList());
        m.put("speaker", Collections.emptyList());
        m.put("host", Collections.emptyList());
        m.put("discussionGuest", Collections.emptyList());
        return m;
    }

    private List<Long> parseExpertIdListFromJson(Object raw) {
        List<Long> out = new ArrayList<>();
        if (!(raw instanceof List)) {
            return out;
        }
        for (Object x : (List<?>) raw) {
            long v = 0L;
            if (x instanceof Number) {
                v = ((Number) x).longValue();
            } else if (x != null) {
                try {
                    v = Long.parseLong(String.valueOf(x).trim());
                } catch (NumberFormatException ignored) {
                    v = 0L;
                }
            }
            if (v > 0) {
                out.add(v);
            }
        }
        return out;
    }

    private List<Map<String, Object>> toExpertDisplayRows(List<Long> orderedIds, Map<Long, Map<String, Object>> displayMap) {
        List<Map<String, Object>> rows = new ArrayList<>();
        for (Long id : orderedIds) {
            if (id == null || id <= 0) {
                continue;
            }
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("expertId", id);
            Map<String, Object> found = displayMap.get(id);
            if (found != null) {
                row.put("expertName", found.get("expertName"));
                row.put("hospital", found.get("hospital"));
            } else {
                row.put("expertName", null);
                row.put("hospital", null);
            }
            rows.add(row);
        }
        return rows;
    }

    @SuppressWarnings("unchecked")
    private void attachBasicInfoExpertSummaries(Map<String, Object> data, MeetingMaterial material, String moduleCode) {
        if (!"BASIC_INFO".equals(moduleCode)) {
            return;
        }
        if (material == null || material.getContentJson() == null || material.getContentJson().trim().isEmpty()) {
            data.put("basicInfoExperts", emptyBasicInfoExpertsPayload());
            return;
        }
        try {
            Map<String, Object> parsed = objectMapper.readValue(material.getContentJson(), LinkedHashMap.class);
            List<Long> chairman = parseExpertIdListFromJson(parsed.get("chairmanExpertIds"));
            List<Long> speaker = parseExpertIdListFromJson(parsed.get("speakerExpertIds"));
            List<Long> host = parseExpertIdListFromJson(parsed.get("hostExpertIds"));
            List<Long> discussionGuest = parseExpertIdListFromJson(parsed.get("discussionGuestExpertIds"));
            Set<Long> all = new HashSet<>();
            all.addAll(chairman);
            all.addAll(speaker);
            all.addAll(host);
            all.addAll(discussionGuest);
            Map<Long, Map<String, Object>> displayMap = expertService == null
                    ? Collections.emptyMap()
                    : expertService.mapExpertDisplayByIds(all);
            Map<String, Object> payload = new LinkedHashMap<>();
            payload.put("chairman", toExpertDisplayRows(chairman, displayMap));
            payload.put("speaker", toExpertDisplayRows(speaker, displayMap));
            payload.put("host", toExpertDisplayRows(host, displayMap));
            payload.put("discussionGuest", toExpertDisplayRows(discussionGuest, displayMap));
            data.put("basicInfoExperts", payload);
        } catch (Exception e) {
            log.warn("readTaskMaterial BASIC_INFO expert display parse failed taskId={}", data.get("taskId"), e);
            data.put("basicInfoExperts", emptyBasicInfoExpertsPayload());
        }
    }

    public Map<String, Object> approveMaterialModule(Long taskId, AuditMaterialModuleApproveRequest request) {
        checkIdempotencyOnly(request.getIdempotencyKey(), "material-module-approve");
        AuditTask task = requirePendingTask(taskId);
        if (meetingMaterialService == null) {
            throw new BusinessException(10001, "资料服务不可用");
        }
        List<Map<String, Object>> items = meetingMaterialService.listMaterialReviewItems(task.getMeetingId(), request.getModuleCode());

        // 查出当前已有 REJECTED 状态的 itemKey，批量通过时跳过它们，避免覆盖单项不通过结果
        List<Map<String, Object>> existingReviews = meetingMaterialService.listMaterialItemReviews(
                taskId, task.getNode().name(), request.getModuleCode());
        Set<String> rejectedKeys = new HashSet<>();
        for (Map<String, Object> review : existingReviews) {
            String result = String.valueOf(review.get("reviewResult") == null ? "" : review.get("reviewResult")).trim().toUpperCase(Locale.ROOT);
            if ("REJECTED".equals(result)) {
                rejectedKeys.add(String.valueOf(review.get("itemKey") == null ? "" : review.get("itemKey")));
            }
        }
        List<Map<String, Object>> approveItems = new ArrayList<>();
        for (Map<String, Object> item : items) {
            String itemKey = String.valueOf(item.get("itemKey") == null ? "" : item.get("itemKey"));
            if (!rejectedKeys.contains(itemKey)) {
                approveItems.add(item);
            }
        }

        int affected = meetingMaterialService.saveMaterialItemReviewRecords(
                task.getMeetingId(),
                taskId,
                task.getNode().name(),
                request.getModuleCode(),
                approveItems,
                "APPROVED",
                null,
                AuthContext.userId()
        );
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("taskId", taskId);
        data.put("moduleCode", request.getModuleCode());
        data.put("reviewNode", task.getNode().name());
        data.put("itemCount", items.size());
        data.put("skippedRejected", rejectedKeys.size());
        data.put("savedCount", affected);
        return data;
    }

    public Map<String, Object> rejectMaterialItem(Long taskId, AuditMaterialItemRejectRequest request) {
        checkIdempotencyOnly(request.getIdempotencyKey(), "material-item-reject");
        AuditTask task = requirePendingTask(taskId);
        if (meetingMaterialService == null) {
            throw new BusinessException(10001, "资料服务不可用");
        }
        Map<String, Object> item = new LinkedHashMap<>();
        item.put("itemKey", request.getItemKey());
        item.put("itemLabel", request.getItemLabel());
        int affected = meetingMaterialService.saveMaterialItemReviewRecords(
                task.getMeetingId(),
                taskId,
                task.getNode().name(),
                request.getModuleCode(),
                Collections.singletonList(item),
                "REJECTED",
                request.getReason(),
                AuthContext.userId()
        );
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("taskId", taskId);
        data.put("moduleCode", request.getModuleCode());
        data.put("itemKey", request.getItemKey());
        data.put("reviewNode", task.getNode().name());
        data.put("savedCount", affected);
        return data;
    }

    public Map<String, Object> transfer(Long taskId, TransferAuditTaskRequest request) {
        AuditTask task = getPendingTask(taskId, request.getIdempotencyKey());
        Long operatorUserId = AuthContext.userId();
        auditTaskRepository.transfer(taskId, request.getToUserId(), request.getReason(), operatorUserId);
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("taskId", taskId);
        data.put("fromUserId", task.getAssigneeUserId());
        data.put("toUserId", request.getToUserId());
        data.put("status", "TRANSFERRED");
        return data;
    }

    public Map<String, Object> batchRemind(BatchRemindRequest request) {
        checkIdempotencyOnly(request.getIdempotencyKey(), "batch-remind");
        List<Long> taskIds = request.getTaskIds();
        if (taskIds == null || taskIds.isEmpty()) {
            taskIds = listTasks(false).getList().stream()
                    .filter(t -> t.getStatus() == AuditTaskStatus.PENDING)
                    .map(AuditTask::getId)
                    .collect(Collectors.toList());
        }
        Long operatorUserId = AuthContext.userId();
        int count = auditTaskRepository.batchRemind(taskIds, operatorUserId);
        for (Long taskId : taskIds) {
            asyncJobService.enqueue(
                    "AUDIT_REMIND",
                    "taskId=" + taskId + "&operatorUserId=" + (operatorUserId == null ? 0L : operatorUserId),
                    "job-audit-remind-" + taskId + "-" + request.getIdempotencyKey()
            );
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("taskCount", taskIds.size());
        data.put("acceptedCount", count);
        return data;
    }

    public Map<String, Object> slaStat() {
        return auditTaskRepository.slaStat();
    }

    public Map<String, Object> batchApprove(BatchAuditActionRequest request) {
        checkIdempotencyOnly(request.getIdempotencyKey(), "batch-approve");
        List<Long> taskIds = request.getTaskIds();
        int successCount = 0;
        List<Long> failedTaskIds = new ArrayList<>();
        List<String> errors = new ArrayList<>();

        for (Long taskId : taskIds) {
            try {
                AuditActionRequest actionRequest = new AuditActionRequest();
                actionRequest.setIdempotencyKey(request.getIdempotencyKey() + "-approve-" + taskId);
                actionRequest.setOpinion(request.getOpinion());
                approve(taskId, actionRequest);
                successCount++;
            } catch (Exception e) {
                failedTaskIds.add(taskId);
                errors.add("任务" + taskId + ": " + e.getMessage());
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("totalCount", taskIds.size());
        result.put("successCount", successCount);
        result.put("failCount", failedTaskIds.size());
        result.put("failedTaskIds", failedTaskIds);
        result.put("errors", errors);
        return result;
    }

    public Map<String, Object> batchReject(BatchAuditActionRequest request) {
        checkIdempotencyOnly(request.getIdempotencyKey(), "batch-reject");
        List<Long> taskIds = request.getTaskIds();
        int successCount = 0;
        List<Long> failedTaskIds = new ArrayList<>();
        List<String> errors = new ArrayList<>();

        for (Long taskId : taskIds) {
            try {
                AuditActionRequest actionRequest = new AuditActionRequest();
                actionRequest.setIdempotencyKey(request.getIdempotencyKey() + "-reject-" + taskId);
                actionRequest.setOpinion(request.getOpinion());
                reject(taskId, actionRequest);
                successCount++;
            } catch (Exception e) {
                failedTaskIds.add(taskId);
                errors.add("任务" + taskId + ": " + e.getMessage());
            }
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("totalCount", taskIds.size());
        result.put("successCount", successCount);
        result.put("failCount", failedTaskIds.size());
        result.put("failedTaskIds", failedTaskIds);
        result.put("errors", errors);
        return result;
    }

    private AuditTask getPendingTask(Long taskId, String idempotencyKey) {
        if (actionIdempotency.containsKey(idempotencyKey)) {
            throw new BusinessException(10002, "请求幂等冲突");
        }
        actionIdempotency.put(idempotencyKey, taskId);

        AuditTask task = auditTaskRepository.findById(taskId)
                .orElseThrow(() -> new BusinessException(10003, "审核任务不存在"));
        if (task.getStatus() != AuditTaskStatus.PENDING) {
            throw new BusinessException(30003, "审核任务已处理");
        }
        return task;
    }

    private void checkIdempotencyOnly(String idempotencyKey, String marker) {
        if (actionIdempotency.containsKey(idempotencyKey)) {
            throw new BusinessException(10002, "请求幂等冲突");
        }
        actionIdempotency.put(idempotencyKey, -1L);
        actionIdempotency.put(marker + ":" + idempotencyKey, -1L);
    }

    private AuditTask requirePendingTask(Long taskId) {
        AuditTask task = auditTaskRepository.findById(taskId)
                .orElseThrow(() -> new BusinessException(10003, "审核任务不存在"));
        if (task.getStatus() != AuditTaskStatus.PENDING) {
            throw new BusinessException(30003, "审核任务已处理");
        }
        return task;
    }

    private Long tenantId() {
        return AuthContext.requireTenantId();
    }

    private void triggerAuditNotification(AuditTask task, String opinion, String eventCode) {
        if (notificationDispatchService == null) {
            return;
        }
        try {
            com.writeoff.module.meeting.model.Meeting meeting = meetingService.getById(task.getMeetingId());
            Map<String, Object> vars = new LinkedHashMap<String, Object>();
            vars.put("meetingId", task.getMeetingId());
            vars.put("meetingTopic", meeting.getTopic() == null ? "" : meeting.getTopic());
            vars.put("auditNode", task.getNode() == null ? "" : task.getNode().name());
            vars.put("auditTaskId", task.getId() == null ? 0L : task.getId());
            vars.put("result", "AUDIT_APPROVED".equals(eventCode) ? "通过" : "不通过");
            vars.put("opinion", opinion == null ? "" : opinion);

            DispatchNotificationRequest dispatchRequest = new DispatchNotificationRequest();
            dispatchRequest.setIdempotencyKey("audit-auto-notify-" + eventCode + "-" + (task.getId() == null ? 0L : task.getId()));
            dispatchRequest.setEventCode(eventCode);
            dispatchRequest.setBizType("MEETING");
            dispatchRequest.setBizId("meeting-" + task.getMeetingId());
            dispatchRequest.setVariablesJson(objectMapper.writeValueAsString(vars));
            notificationDispatchService.dispatch(dispatchRequest);
        } catch (BusinessException ex) {
            if (ex.getCode() != ErrorCodes.RESOURCE_NOT_FOUND) {
                log.warn("自动触发审核通知失败, eventCode={}, taskId={}, code={}, msg={}",
                        eventCode, task.getId(), ex.getCode(), ex.getMessage());
            }
        } catch (Exception ex) {
            log.warn("自动触发审核通知异常, eventCode={}, taskId={}", eventCode, task.getId(), ex);
        }
    }
}
