package com.writeoff.module.meeting.service;

import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.writeoff.module.audit.model.AuditNode;
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.audit.service.AuditFlowConfigService;
import com.writeoff.module.expert.service.ExpertSnapshotService;
import com.writeoff.module.meeting.dto.CreateMeetingRequest;
import com.writeoff.module.meeting.dto.MeetingQueryRequest;
import com.writeoff.module.meeting.dto.SubmitMeetingRequest;
import com.writeoff.module.meeting.dto.WithdrawMeetingRequest;
import com.writeoff.module.meeting.model.Meeting;
import com.writeoff.module.meeting.model.MeetingAuditStatus;
import com.writeoff.module.meeting.model.MeetingStatus;
import com.writeoff.module.meeting.repository.MeetingRepository;
import com.writeoff.module.project.model.Project;
import com.writeoff.module.project.service.ProjectService;
import com.writeoff.module.scheduler.service.AsyncJobService;
import com.writeoff.module.system.model.BizChangeLogInfo;
import com.writeoff.module.system.service.BizChangeLogService;
import com.writeoff.module.system.service.DataPermissionService;
import com.writeoff.security.AuthContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
public class MeetingService {
    private static final DateTimeFormatter SQL_ISO_SECOND_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
    private static final Set<String> LOCATION_OPTIONS = new HashSet<String>();
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    static {
        LOCATION_OPTIONS.add("线上");
        LOCATION_OPTIONS.add("线下");
        LOCATION_OPTIONS.add("线上+线下");
    }
    private final MeetingRepository meetingRepository;
    private final ProjectService projectService;
    private final AuditTaskRepository auditTaskRepository;
    private final AsyncJobService asyncJobService;
    private final AuditFlowConfigService auditFlowConfigService;
    private final DataPermissionService dataPermissionService;
    private final ExpertSnapshotService expertSnapshotService;
    private final BizChangeLogService bizChangeLogService;
    private final Map<String, Long> submitIdempotency = new ConcurrentHashMap<>();
    private final Map<String, Long> withdrawIdempotency = new ConcurrentHashMap<>();

    @Autowired
    public MeetingService(MeetingRepository meetingRepository, ProjectService projectService, AuditTaskRepository auditTaskRepository, AsyncJobService asyncJobService, AuditFlowConfigService auditFlowConfigService, DataPermissionService dataPermissionService, ExpertSnapshotService expertSnapshotService, BizChangeLogService bizChangeLogService) {
        this.meetingRepository = meetingRepository;
        this.projectService = projectService;
        this.auditTaskRepository = auditTaskRepository;
        this.asyncJobService = asyncJobService;
        this.auditFlowConfigService = auditFlowConfigService;
        this.dataPermissionService = dataPermissionService;
        this.expertSnapshotService = expertSnapshotService;
        this.bizChangeLogService = bizChangeLogService;
    }

    public MeetingService(MeetingRepository meetingRepository, ProjectService projectService, AuditTaskRepository auditTaskRepository, AsyncJobService asyncJobService) {
        this(meetingRepository, projectService, auditTaskRepository, asyncJobService, null, null, null, null);
    }

    public PageResult<Meeting> list(MeetingQueryRequest query) {
        boolean includeDeleted = query != null && Boolean.TRUE.equals(query.getIncludeDeleted());
        List<Meeting> list = meetingRepository.findAll(includeDeleted);
        if (dataPermissionService != null) {
            DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
            Set<Long> meetingIds = list.stream().map(Meeting::getId).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(meeting -> {
                        Long projectId = meetingProjectMap.get(meeting.getId());
                        Long meetingCreatedBy = meetingCreatorMap.get(meeting.getId());
                        Long projectCreatedBy = projectId == null ? null : projectCreatorMap.get(projectId);
                        return dataPermissionService.canAccessMeeting(meeting.getId(), meeting.getProjectId(), meetingCreatedBy, projectCreatedBy, scope);
                    })
                    .collect(Collectors.toList());
        }
        list.forEach(this::applyEffectiveStatus);
        list = applyFilters(list, query);
        return new PageResult<>(list, list.size(), 1, 20);
    }

    private List<Meeting> applyFilters(List<Meeting> source, MeetingQueryRequest query) {
        if (query == null) {
            return source;
        }
        final String projectName = normalize(query.getProjectName());
        final String topic = normalize(query.getTopic());
        final String meetingStatus = normalize(query.getMeetingStatus());
        final String auditStatus = normalize(query.getAuditStatus());
        final String currentNode = normalize(query.getCurrentAuditNode());
        final LocalDateTime meetingStartFrom = parseDateTime(query.getMeetingStartFrom());
        final LocalDateTime meetingStartTo = parseDateTime(query.getMeetingStartTo());
        final LocalDateTime lastSubmitFrom = parseDateTime(query.getLastSubmitFrom());
        final LocalDateTime lastSubmitTo = parseDateTime(query.getLastSubmitTo());

        return source.stream()
                .filter(m -> query.getProjectId() == null || query.getProjectId().equals(m.getProjectId()))
                .filter(m -> projectName == null || containsIgnoreCase(m.getProjectName(), projectName))
                .filter(m -> topic == null || containsIgnoreCase(m.getTopic(), topic))
                .filter(m -> meetingStatus == null || (m.getStatus() != null && m.getStatus().name().equalsIgnoreCase(meetingStatus)))
                .filter(m -> auditStatus == null || (m.getAuditStatus() != null && m.getAuditStatus().name().equalsIgnoreCase(auditStatus)))
                .filter(m -> currentNode == null || containsIgnoreCase(m.getCurrentAuditNode(), currentNode))
                .filter(m -> query.getCurrentAuditorUserId() == null || query.getCurrentAuditorUserId().equals(m.getCurrentAuditorUserId()))
                .filter(m -> inRange(parseDateTime(m.getStartTime()), meetingStartFrom, meetingStartTo))
                .filter(m -> inRange(parseDateTime(m.getLastSubmitAt()), lastSubmitFrom, lastSubmitTo))
                .collect(Collectors.toList());
    }

    private boolean inRange(LocalDateTime value, LocalDateTime from, LocalDateTime to) {
        if (from == null && to == null) {
            return true;
        }
        if (value == null) {
            return false;
        }
        if (from != null && value.isBefore(from)) {
            return false;
        }
        return to == null || !value.isAfter(to);
    }

    private String normalize(String val) {
        if (val == null) {
            return null;
        }
        String trimmed = val.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }

    private boolean containsIgnoreCase(String origin, String keyword) {
        if (origin == null) {
            return false;
        }
        return origin.toLowerCase().contains(keyword.toLowerCase());
    }

    private LocalDateTime parseDateTime(String value) {
        if (value == null || value.trim().isEmpty()) {
            return null;
        }
        String trimmed = value.trim();
        DateTimeFormatter[] patterns = new DateTimeFormatter[] {
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
                DateTimeFormatter.ISO_LOCAL_DATE_TIME
        };
        for (DateTimeFormatter pattern : patterns) {
            try {
                return LocalDateTime.parse(trimmed, pattern);
            } catch (DateTimeParseException ignore) {
            }
        }
        return null;
    }

    public Meeting create(CreateMeetingRequest request) {
        Project project = projectService.getById(request.getProjectId());
        validateProjectForMeetingCreate(project);
        validateMeetingTimeInProjectCycle(project, request);
        int existingMeetingCount = countMeetingsByProjectId(project.getId());
        if (existingMeetingCount >= project.getMeetingTotal()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "项目可创建的会议数量已达上限");
        }
        validateLocation(request.getLocation());
        long defaultBudgetCent = calculateDefaultMeetingBudgetCent(project);
        if (defaultBudgetCent <= 0L) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "默认会议预算必须大于 0");
        }
        String now = nowIsoSeconds();
        Meeting meeting = new Meeting(
                null,
                request.getProjectId(),
                request.getTopic(),
                request.getMeetingCategory(),
                request.getMeetingForm(),
                request.getLocation(),
                request.getStartTime(),
                request.getEndTime(),
                defaultBudgetCent,
                request.getLaborRatio() == null ? 0d : request.getLaborRatio(),
                request.getCateringRatio() == null ? 0d : request.getCateringRatio(),
                MeetingStatus.NOT_STARTED,
                MeetingAuditStatus.PENDING,
                null,
                null,
                null,
                0,
                null,
                false,
                null,
                null,
                null,
                0,
                null,
                now,
                safeUserId(),
                null,
                null,
                null,
                0,
                null,
                null,
                null
        );
        Meeting saved = meetingRepository.save(meeting);
        projectService.markInProgress(request.getProjectId());
        logMeetingCreate(saved);
        return saved;
    }

    private void validateProjectForMeetingCreate(Project project) {
        if (project.getMeetingTotal() <= 0) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "创建会议前请先配置项目会议场次");
        }
        LocalDate startDate = project.getStartDate();
        LocalDate endDate = project.getEndDate();
        if (startDate == null || endDate == null || endDate.isBefore(startDate)) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "创建会议前请先配置有效的项目起止日期");
        }
    }

    private int countMeetingsByProjectId(Long projectId) {
        return (int) meetingRepository.findAll(false).stream()
                .filter(item -> !item.isDeleted())
                .filter(item -> projectId.equals(item.getProjectId()))
                .count();
    }

    private long calculateDefaultMeetingBudgetCent(Project project) {
        long projectBudgetCent = Math.max(0L, project.getBudgetCent());
        ProjectFeeSummary feeSummary = parseProjectFeeSummary(project.getProjectFeeJson());
        long distributableBudgetCent = projectBudgetCent - feeSummary.managementFeeCent - feeSummary.taxFeeCent - feeSummary.customFeeTotalCent;
        if (project.getMeetingTotal() <= 0) {
            return 0L;
        }
        return distributableBudgetCent / project.getMeetingTotal();
    }

    private ProjectFeeSummary parseProjectFeeSummary(String projectFeeJson) {
        if (projectFeeJson == null || projectFeeJson.trim().isEmpty()) {
            return new ProjectFeeSummary(0L, 0L, 0L);
        }
        try {
            JsonNode root = OBJECT_MAPPER.readTree(projectFeeJson);
            long managementFeeCent = readNonNegativeLong(root, "managementFeeCent");
            long taxFeeCent = readNonNegativeLong(root, "taxFeeCent");
            long customFeeTotalCent = 0L;
            JsonNode customFees = root.path("customFees");
            if (customFees.isArray()) {
                for (JsonNode feeNode : customFees) {
                    if (feeNode == null || !feeNode.isObject()) {
                        continue;
                    }
                    customFeeTotalCent += readNonNegativeLong(feeNode, "amountCent");
                }
            }
            return new ProjectFeeSummary(managementFeeCent, taxFeeCent, customFeeTotalCent);
        } catch (BusinessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "项目费用配置格式不正确");
        }
    }

    private long readNonNegativeLong(JsonNode node, String fieldName) {
        if (node == null || node.isNull()) {
            return 0L;
        }
        JsonNode valueNode = node.get(fieldName);
        if (valueNode == null || valueNode.isNull()) {
            return 0L;
        }
        if (!valueNode.isNumber()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "项目费用字段格式不正确：" + fieldName);
        }
        long value = valueNode.asLong();
        if (value < 0L) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "项目费用字段不能为负数：" + fieldName);
        }
        return value;
    }

    private static class ProjectFeeSummary {
        private final long managementFeeCent;
        private final long taxFeeCent;
        private final long customFeeTotalCent;

        private ProjectFeeSummary(long managementFeeCent, long taxFeeCent, long customFeeTotalCent) {
            this.managementFeeCent = managementFeeCent;
            this.taxFeeCent = taxFeeCent;
            this.customFeeTotalCent = customFeeTotalCent;
        }
    }

    public Meeting update(Long meetingId, CreateMeetingRequest request) {
        Meeting existing = getById(meetingId);
        if (request.getProjectId() != null && !existing.getProjectId().equals(request.getProjectId())) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "编辑会议时不能修改所属项目");
        }
        Project project = projectService.getById(existing.getProjectId());
        validateProjectForMeetingCreate(project);
        validateMeetingTimeInProjectCycle(project, request);
        validateLocation(request.getLocation());
        Meeting updated = new Meeting(
                existing.getId(),
                existing.getProjectId(),
                request.getTopic(),
                request.getMeetingCategory(),
                request.getMeetingForm(),
                request.getLocation(),
                request.getStartTime(),
                request.getEndTime(),
                request.getBudgetCent(),
                request.getLaborRatio() == null ? 0d : request.getLaborRatio(),
                request.getCateringRatio() == null ? 0d : request.getCateringRatio(),
                existing.getStatus(),
                existing.getAuditStatus(),
                existing.getCurrentAuditNode(),
                existing.getLastSubmitAt(),
                existing.getLastRejectReason(),
                existing.getOverdueDays(),
                existing.getRiskFlagsJson(),
                existing.isFrozen(),
                existing.getFreezeReason(),
                existing.getCurrentAuditorUserId(),
                existing.getNodeDeadlineAt(),
                existing.getRejectCount(),
                nowIsoSeconds(),
                nowIsoSeconds(),
                safeUserId(),
                existing.getCancelReason(),
                existing.getPostponeReason(),
                existing.getWithdrawReason(),
                existing.getLockVersion() + 1,
                nowIsoSeconds(),
                safeUserId(),
                existing.getInvoiceConfigJson()
        );
        updated.setProjectName(existing.getProjectName());
        Meeting saved = meetingRepository.save(updated);
        logMeetingUpdate(existing, saved);
        return saved;
    }

    private void validateLocation(String location) {
        if (location == null || location.trim().isEmpty()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "\u4f1a\u8bae\u5730\u70b9\u4e0d\u80fd\u4e3a\u7a7a");
        }
    }

    private void validateLocationLegacy(String location) {
        validateLocation(location);
    }


    private void validateMeetingTimeInProjectCycle(Project project, CreateMeetingRequest request) {
        LocalDateTime startTime = parseDateTime(request.getStartTime());
        LocalDateTime endTime = parseDateTime(request.getEndTime());
        if (startTime == null || endTime == null) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议时间格式必须为 yyyy-MM-dd HH:mm:ss");
        }
        if (endTime.isBefore(startTime)) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议结束时间不能早于开始时间");
        }
        LocalDate projectStartDate = project.getStartDate();
        LocalDate projectEndDate = project.getEndDate();
        if (projectStartDate == null || projectEndDate == null) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "项目日期范围配置不完整");
        }
        LocalDate meetingStartDate = startTime.toLocalDate();
        LocalDate meetingEndDate = endTime.toLocalDate();
        if (meetingStartDate.isBefore(projectStartDate) || meetingEndDate.isAfter(projectEndDate)) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "会议时间必须在项目周期内");
        }
    }

    public Map<String, Object> submit(Long meetingId, SubmitMeetingRequest request) {
        Meeting meeting = meetingRepository.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在"));

        if (submitIdempotency.containsKey(request.getIdempotencyKey())) {
            throw new BusinessException(ErrorCodes.IDEMPOTENCY_CONFLICT, "请求重复，请勿重复提交");
        }
        submitIdempotency.put(request.getIdempotencyKey(), meetingId);

        MeetingStatus effectiveStatus = resolveEffectiveStatus(meeting);
        if (effectiveStatus != MeetingStatus.COMPLETED) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "会议未完成，不能提交审核");
        }

        if (meeting.getAuditStatus() == MeetingAuditStatus.APPROVED) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "会议已终审通过，不能重复提交");
        }
        if (meeting.getAuditStatus() == MeetingAuditStatus.IN_REVIEW) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "会议正在审核中");
        }
        if (expertSnapshotService != null) {
            expertSnapshotService.snapshotOnMeetingSubmit(meetingId);
        }
        meeting.setAuditStatus(MeetingAuditStatus.IN_REVIEW);
        meetingRepository.save(meeting);

        Long tenantId = tenantId();
        AuditNode firstNode = auditFlowConfigService == null ? AuditNode.INIT_REVIEW : auditFlowConfigService.firstNode(tenantId);
        Long assigneeUserId = auditFlowConfigService == null ? null : auditFlowConfigService.resolveAssigneeUserId(tenantId, firstNode);
        meeting.setCurrentAuditNode(firstNode.name());
        meeting.setLastSubmitAt(nowIsoSeconds());
        meeting.setLastActionAt(nowIsoSeconds());
        meeting.setCurrentAuditorUserId(assigneeUserId);
        meetingRepository.save(meeting);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("MEETING", meetingId, "MEETING_SUBMIT", request.getRemark());
        }
        auditTaskRepository.save(new AuditTask(
                null,
                meetingId,
                firstNode,
                assigneeUserId,
                AuditTaskStatus.PENDING,
                request.getRemark()
        ));
        asyncJobService.enqueue(
                "AUDIT_REMIND",
                "meetingId=" + meetingId,
                "job-audit-remind-" + meetingId + "-" + request.getIdempotencyKey()
        );

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("meetingId", meetingId);
        result.put("auditStatus", meeting.getAuditStatus().name());
        result.put("currentNode", firstNode.name());
        return result;
    }

    public Map<String, Object> withdraw(Long meetingId, WithdrawMeetingRequest request) {
        Meeting meeting = meetingRepository.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在"));

        if (withdrawIdempotency.containsKey(request.getIdempotencyKey())) {
            throw new BusinessException(ErrorCodes.IDEMPOTENCY_CONFLICT, "请求重复，请勿重复提交");
        }
        withdrawIdempotency.put(request.getIdempotencyKey(), meetingId);

        if (meeting.getAuditStatus() != MeetingAuditStatus.IN_REVIEW) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "只有审核中的会议才可撤回");
        }

        int closedTaskCount = auditTaskRepository.withdrawPendingByMeetingId(meetingId, request.getReason(), safeUserId());
        meeting.setAuditStatus(MeetingAuditStatus.PENDING);
        meeting.setCurrentAuditorUserId(null);
        meeting.setWithdrawReason(request.getReason());
        meeting.setLastActionAt(nowIsoSeconds());
        meetingRepository.save(meeting);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("MEETING", meetingId, "MEETING_WITHDRAW", request.getReason());
        }

        Map<String, Object> result = new LinkedHashMap<String, Object>();
        result.put("meetingId", meetingId);
        result.put("auditStatus", meeting.getAuditStatus().name());
        result.put("closedTaskCount", closedTaskCount);
        return result;
    }

    /** Soft-delete a draft meeting. */

    public void deleteDraft(Long meetingId) {
        Meeting meeting = meetingRepository.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在"));

        MeetingStatus effectiveStatus = resolveEffectiveStatus(meeting);
        // 婵炲濮撮幊搴ｇ礊鐎ｎ兘鍋撻崗澶婂⒉闁绘濞婇幃鈺呮嚋绾版ê浜惧ù锝囨焿缁€?PENDING闂佹寧绋戦悧鍡楃暤閸℃顩烽幖娣灪瀵捇鏌熺紒妯哄婵﹫闄勫濠氬炊妞嬪海顦繛鎴炴尰濮婄懓锕㈤鐘冲仏妞ゆ劑鍨归弸娆戠磽娴ｈ灏版俊鐐插€垮畷妤佹媴缁涘鏅犻梺鍛婂笧婵炩偓婵?
        if (meeting.getAuditStatus() != MeetingAuditStatus.PENDING) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "只有待审核的草稿会议才可删除");
        }
        if (meeting.getLastSubmitAt() != null) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "已提交过的会议不能直接删除");
        }
        meetingRepository.softDelete(meetingId);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("MEETING", meetingId, "MEETING_DELETE_DRAFT", null);
        }
    }

    /** Cancel a meeting before it starts. */

    public Map<String, Object> cancel(Long meetingId, String reason) {
        Meeting meeting = meetingRepository.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在"));

        if (reason == null || reason.trim().isEmpty()) {
            throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "取消原因不能为空");
        }

        MeetingStatus effectiveStatus = resolveEffectiveStatus(meeting);
        if (effectiveStatus != MeetingStatus.NOT_STARTED) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "只有未开始的会议才可取消");
        }
        if (meeting.getAuditStatus() == MeetingAuditStatus.IN_REVIEW) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "请先撤回审核，再取消会议");
        }
        if (meeting.getAuditStatus() == MeetingAuditStatus.APPROVED) {
            throw new BusinessException(ErrorCodes.INVALID_STATE, "已审核通过的会议不能取消");
        }

        meeting.setStatus(MeetingStatus.CANCELED);
        meeting.setCancelReason(reason.trim());
        meeting.setLastActionAt(nowIsoSeconds());
        meetingRepository.save(meeting);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("MEETING", meetingId, "MEETING_CANCEL", reason.trim());
        }

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("meetingId", meetingId);
        result.put("status", MeetingStatus.CANCELED.name());
        result.put("reason", reason.trim());
        return result;
    }

    public Meeting getById(Long meetingId) {
        Meeting meeting = meetingRepository.findById(meetingId)
                .orElseThrow(() -> new BusinessException(ErrorCodes.RESOURCE_NOT_FOUND, "会议不存在"));
        applyEffectiveStatus(meeting);
        return meeting;
    }

    /** Resolve the effective meeting status from start/end time and current time. */

    public MeetingStatus resolveEffectiveStatus(Meeting meeting) {
        MeetingStatus dbStatus = meeting.getStatus();
        if (dbStatus == MeetingStatus.CANCELED
                || dbStatus == MeetingStatus.FROZEN
                || dbStatus == MeetingStatus.DELAYED) {
            return dbStatus;
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime start = parseDateTime(meeting.getStartTime());
        LocalDateTime end = parseDateTime(meeting.getEndTime());
        if (start == null || end == null) {
            return dbStatus != null ? dbStatus : MeetingStatus.NOT_STARTED;
        }
        if (now.isBefore(start)) {
            return MeetingStatus.NOT_STARTED;
        } else if (now.isAfter(end)) {
            return MeetingStatus.COMPLETED;
        } else {
            return MeetingStatus.IN_PROGRESS;
        }
    }

    private void applyEffectiveStatus(Meeting meeting) {
        meeting.setStatus(resolveEffectiveStatus(meeting));
    }

    public void updateAuditStatus(Long meetingId, MeetingAuditStatus status) {
        Meeting meeting = getById(meetingId);
        meeting.setAuditStatus(status);
        meetingRepository.save(meeting);
    }

    /** Update only the current audit node and current auditor. */

    public void updateCurrentAuditNode(Long meetingId, String node, Long auditorUserId) {
        Meeting meeting = getById(meetingId);
        meeting.setCurrentAuditNode(node);
        meeting.setCurrentAuditorUserId(auditorUserId);
        meeting.setLastActionAt(nowIsoSeconds());
        meetingRepository.save(meeting);
    }

    public Meeting updateInvoiceConfig(Long meetingId, com.writeoff.module.meeting.dto.MeetingInvoiceConfigRequest request) {
        Meeting meeting = getById(meetingId);
        String beforeConfigJson = meeting.getInvoiceConfigJson();
        String configJson = null;
        if (request.getInvoiceModules() != null) {
            try {
                configJson = OBJECT_MAPPER.writeValueAsString(request.getInvoiceModules());
            } catch (Exception e) {
                throw new BusinessException(ErrorCodes.VALIDATION_ERROR, "发票配置序列化失败");
            }
        }
        meeting.setInvoiceConfigJson(configJson);
        Meeting saved = meetingRepository.save(meeting);
        if (bizChangeLogService != null) {
            bizChangeLogService.logFieldChange("MEETING", meetingId, "MEETING_INVOICE_CONFIG_UPDATE", "invoiceConfigJson", "发票模块配置", beforeConfigJson, configJson, null, null);
        }
        return saved;
    }

    public List<Map<String, Object>> listChangeLogs(Long meetingId) {
        getById(meetingId);
        if (bizChangeLogService == null) {
            return new ArrayList<Map<String, Object>>();
        }
        return bizChangeLogService.listByBiz("MEETING", meetingId).stream()
                .map(this::toMeetingChangeLogRow)
                .collect(Collectors.toList());
    }

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

    private Long safeUserId() {
        Long userId = AuthContext.userId();
        return userId == null ? 0L : userId;
    }

    private String nowIsoSeconds() {
        return LocalDateTime.now().format(SQL_ISO_SECOND_FORMATTER);
    }

    private void logMeetingCreate(Meeting meeting) {
        if (bizChangeLogService == null) {
            return;
        }
        bizChangeLogService.logAction("MEETING", meeting.getId(), "MEETING_CREATE", null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "topic", "会议主题", null, meeting.getTopic(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "meetingCategory", "会议类别", null, meeting.getMeetingCategory(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "meetingForm", "会议形式", null, meeting.getMeetingForm(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "location", "会议地点", null, meeting.getLocation(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "startTime", "会议开始时间", null, meeting.getStartTime(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "endTime", "会议结束时间", null, meeting.getEndTime(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "budgetCent", "会议预算(分)", null, meeting.getBudgetCent(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "laborRatio", "劳务占比", null, meeting.getLaborRatio(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "cateringRatio", "餐费占比", null, meeting.getCateringRatio(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "status", "会议状态", null, meeting.getStatus(), null);
        logMeetingFieldChange(meeting.getId(), "MEETING_CREATE", "auditStatus", "审核状态", null, meeting.getAuditStatus(), null);
    }

    private void logMeetingUpdate(Meeting before, Meeting after) {
        if (bizChangeLogService == null) {
            return;
        }
        String batchId = bizChangeLogService.newBatchId();
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "topic", "会议主题", before.getTopic(), after.getTopic(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "meetingCategory", "会议类别", before.getMeetingCategory(), after.getMeetingCategory(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "meetingForm", "会议形式", before.getMeetingForm(), after.getMeetingForm(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "location", "会议地点", before.getLocation(), after.getLocation(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "startTime", "会议开始时间", before.getStartTime(), after.getStartTime(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "endTime", "会议结束时间", before.getEndTime(), after.getEndTime(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "budgetCent", "会议预算(分)", before.getBudgetCent(), after.getBudgetCent(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "laborRatio", "劳务占比", before.getLaborRatio(), after.getLaborRatio(), batchId);
        logMeetingFieldChange(after.getId(), "MEETING_UPDATE", "cateringRatio", "餐费占比", before.getCateringRatio(), after.getCateringRatio(), batchId);
    }

    private void logMeetingFieldChange(Long meetingId,
                                       String changeType,
                                       String fieldCode,
                                       String fieldName,
                                       Object beforeValue,
                                       Object afterValue,
                                       String batchId) {
        if (bizChangeLogService == null) {
            return;
        }
        bizChangeLogService.logFieldChange(
                "MEETING",
                meetingId,
                changeType,
                fieldCode,
                fieldName,
                normalizeMeetingFieldValue(beforeValue),
                normalizeMeetingFieldValue(afterValue),
                batchId,
                null
        );
    }

    private Object normalizeMeetingFieldValue(Object value) {
        if (value instanceof Enum<?>) {
            return ((Enum<?>) value).name();
        }
        return value;
    }

    private Map<String, Object> toMeetingChangeLogRow(BizChangeLogInfo item) {
        Map<String, Object> row = new LinkedHashMap<String, Object>();
        row.put("id", item.getId());
        row.put("fieldCode", item.getFieldCode());
        row.put("fieldName", resolveMeetingFieldName(item));
        row.put("beforeValue", item.getBeforeValue());
        row.put("afterValue", item.getAfterValue());
        row.put("remark", item.getRemark());
        row.put("changeType", item.getChangeType());
        row.put("operatorUserId", item.getOperatorUserId());
        row.put("operatorUserName", item.getOperatorUserName());
        row.put("relatedUserId", item.getRelatedUserId());
        row.put("relatedUserName", item.getRelatedUserName());
        row.put("batchId", item.getBatchId());
        row.put("createdAt", item.getCreatedAt());
        return row;
    }

    private String resolveMeetingFieldName(BizChangeLogInfo item) {
        String fieldName = item.getFieldName() == null ? "" : item.getFieldName().trim();
        if (!fieldName.isEmpty()) {
            return fieldName;
        }
        if ("MEETING_CREATE".equals(item.getChangeType())) {
            return "会议创建";
        }
        if ("MEETING_SUBMIT".equals(item.getChangeType())) {
            return "提交审核";
        }
        if ("MEETING_WITHDRAW".equals(item.getChangeType())) {
            return "撤回审核";
        }
        if ("MEETING_DELETE_DRAFT".equals(item.getChangeType())) {
            return "删除草稿";
        }
        if ("MEETING_CANCEL".equals(item.getChangeType())) {
            return "取消会议";
        }
        if ("MEETING_INVOICE_CONFIG_UPDATE".equals(item.getChangeType())) {
            return "发票模块配置";
        }
        return "会议变更";
    }
}
