package com.njcn.rdms.module.project.service.overtime;

import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageParam;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.OvertimeApplicationConstants;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationApprovalRecordRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchActionReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationBatchResultRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationExportVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationPageReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusDictRespVO;
import com.njcn.rdms.module.project.controller.admin.overtime.vo.OvertimeApplicationStatusLogRespVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationApprovalRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationDO;
import com.njcn.rdms.module.project.dal.dataobject.overtime.OvertimeApplicationStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationApprovalRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationMapper;
import com.njcn.rdms.module.project.dal.mysql.overtime.OvertimeApplicationStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;

@Service
public class OvertimeApplicationServiceImpl implements OvertimeApplicationService {

    private static final List<String> TEAM_VISIBLE_STATUS_CODES = List.of(
            OvertimeApplicationConstants.STATUS_PENDING,
            OvertimeApplicationConstants.STATUS_APPROVED,
            OvertimeApplicationConstants.STATUS_REJECTED);

    @Resource
    private OvertimeApplicationMapper overtimeApplicationMapper;
    @Resource
    private OvertimeApplicationStatusLogMapper overtimeApplicationStatusLogMapper;
    @Resource
    private OvertimeApplicationApprovalRecordMapper overtimeApplicationApprovalRecordMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Resource
    private StatusActionTextResolver statusActionTextResolver;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private TeamDashboardAccessService teamDashboardAccessService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createApplication(OvertimeApplicationSaveReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());
        String initialStatus = getInitialStatusCode();

        OvertimeApplicationDO application = new OvertimeApplicationDO();
        application.setApplicantId(loginUserId);
        application.setApplicantName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        applySaveFields(application, reqVO, approver);
        application.setStatusCode(initialStatus);
        application.setApprovalComment(null);
        application.setSubmitTime(LocalDateTime.now());
        application.setApprovalTime(null);
        overtimeApplicationMapper.insert(application);

        // 创建即进入初始状态 pending，这里保留 submit 作为业务留痕动作，不依赖状态机流转表存在 submit 配置。
        writeStatusLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null);
        writeAuditLog(application, OvertimeApplicationConstants.ACTION_SUBMIT, null, initialStatus, null, null, null);
        return application.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resubmitApplication(Long id, OvertimeApplicationSaveReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        OvertimeApplicationDO current = validateApplicationExists(id);
        if (!Objects.equals(current.getApplicantId(), loginUserId)) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
        }
        String fromStatus = current.getStatusCode();
        ObjectStatusTransitionDO transition = validateTransition(fromStatus, OvertimeApplicationConstants.ACTION_RESUBMIT,
                null);
        AdminUserRespDTO approver = validateApprover(reqVO.getApproverId());

        OvertimeApplicationDO before = cloneApplication(current);
        OvertimeApplicationDO update = new OvertimeApplicationDO();
        applySaveFields(update, reqVO, approver);
        update.setStatusCode(transition.getToStatusCode());
        update.setApprovalComment(null);
        update.setSubmitTime(LocalDateTime.now());
        update.setApprovalTime(null);

        int updateCount = overtimeApplicationMapper.updateByIdAndStatusesAndApplicantId(update, id,
                List.of(OvertimeApplicationConstants.STATUS_REJECTED), loginUserId);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
        }

        OvertimeApplicationDO after = mergeUpdated(current, update);
        writeStatusLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
                null);
        writeAuditLog(after, OvertimeApplicationConstants.ACTION_RESUBMIT, fromStatus, transition.getToStatusCode(),
                buildFieldChanges(before, after), null, null);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approve(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
        processApprovalAction(id, OvertimeApplicationConstants.ACTION_APPROVE, reqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reject(Long id, OvertimeApplicationStatusActionReqVO reqVO) {
        processApprovalAction(id, OvertimeApplicationConstants.ACTION_REJECT, reqVO);
    }

    @Override
    public OvertimeApplicationBatchResultRespVO batchApprove(OvertimeApplicationBatchActionReqVO reqVO) {
        return processBatchApprovalAction(reqVO, OvertimeApplicationConstants.ACTION_APPROVE);
    }

    @Override
    public OvertimeApplicationBatchResultRespVO batchReject(OvertimeApplicationBatchActionReqVO reqVO) {
        return processBatchApprovalAction(reqVO, OvertimeApplicationConstants.ACTION_REJECT);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteApplication(Long id) {
        OvertimeApplicationDO current = validateApplicationExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(current.getApplicantId(), loginUserId)) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPLICANT_ONLY);
        }
        if (!OvertimeApplicationConstants.STATUS_REJECTED.equals(current.getStatusCode())) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_DELETE_ONLY_REJECTED);
        }
        overtimeApplicationMapper.deleteById(id);
        writeAuditLog(current, OvertimeApplicationConstants.ACTION_DELETE, current.getStatusCode(), null, null, null,
                null);
    }

    @Override
    public OvertimeApplicationRespVO getApplication(Long id) {
        OvertimeApplicationDO application = validateReadableApplication(id);
        return toRespVO(application);
    }

    @Override
    public List<OvertimeApplicationStatusDictRespVO> getStatusDict() {
        List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
                .selectListByObjectTypeEnabled(OvertimeApplicationConstants.STATUS_OBJECT_TYPE);
        return statusModels.stream()
                .map(this::buildStatusDictRespVO)
                .collect(Collectors.toList());
    }

    @Override
    public PageResult<OvertimeApplicationRespVO> getMyPage(OvertimeApplicationPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<OvertimeApplicationDO> page;
        if (reqVO.getApplicantIds() != null) {
            List<Long> applicantIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getApplicantIds(), OvertimeApplicationConstants.PERMISSION_TEAM_DASHBOARD);
            page = overtimeApplicationMapper.selectMyPage(applicantIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
        } else {
            page = overtimeApplicationMapper.selectMyPage(loginUserId, reqVO);
        }
        return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
    }

    @Override
    public PageResult<OvertimeApplicationRespVO> getApprovalPage(OvertimeApplicationPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<OvertimeApplicationDO> page = overtimeApplicationMapper.selectApprovalPage(loginUserId, reqVO);
        return BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView);
    }

    @Override
    public List<OvertimeApplicationStatusLogRespVO> getStatusLogs(Long id) {
        validateReadableApplication(id);
        return BeanUtils.toBean(overtimeApplicationStatusLogMapper.selectListByApplicationId(id),
                OvertimeApplicationStatusLogRespVO.class);
    }

    @Override
    public List<OvertimeApplicationApprovalRecordRespVO> getApprovalRecords(Long id) {
        validateReadableApplication(id);
        return BeanUtils.toBean(overtimeApplicationApprovalRecordMapper.selectListByApplicationId(id),
                OvertimeApplicationApprovalRecordRespVO.class);
    }

    @Override
    public List<OvertimeApplicationExportVO> getExportList(OvertimeApplicationPageReqVO reqVO) {
        reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        PageResult<OvertimeApplicationDO> page;
        if (reqVO.getApplicantIds() != null) {
            List<Long> applicantIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getApplicantIds(), OvertimeApplicationConstants.PERMISSION_TEAM_DASHBOARD);
            Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
            if (loginUserId != null) {
                LinkedHashSet<Long> mergedIds = new LinkedHashSet<>(applicantIds);
                mergedIds.add(loginUserId);
                applicantIds = new ArrayList<>(mergedIds);
            }
            page = overtimeApplicationMapper.selectMyPage(applicantIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
        } else {
            page = overtimeApplicationMapper.selectMyPage(SecurityFrameworkUtils.getLoginUserId(), reqVO);
        }
        return BeanUtils.toBean(BeanUtils.toBean(page, OvertimeApplicationRespVO.class, this::applyStatusView).getList(),
                OvertimeApplicationExportVO.class);
    }

    private void processApprovalAction(Long id, String actionCode, OvertimeApplicationStatusActionReqVO reqVO) {
        OvertimeApplicationDO current = validateApplicationExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(current.getApproverId(), loginUserId)) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_ONLY);
        }
        String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
        String fromStatus = current.getStatusCode();
        ObjectStatusTransitionDO transition = validateTransition(fromStatus, actionCode, reason);

        OvertimeApplicationDO update = new OvertimeApplicationDO();
        update.setStatusCode(transition.getToStatusCode());
        update.setApprovalComment(reason);
        update.setApprovalTime(LocalDateTime.now());
        int updateCount = overtimeApplicationMapper.updateByIdAndStatus(update, id, fromStatus);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_CONCURRENT_MODIFIED);
        }

        OvertimeApplicationDO after = mergeUpdated(current, update);
        OvertimeApplicationStatusLogDO statusLog = writeStatusLog(after, actionCode, fromStatus,
                transition.getToStatusCode(), reason);
        writeApprovalRecord(after, statusLog, reason);
        writeAuditLog(after, actionCode, fromStatus, transition.getToStatusCode(), null, reason, null);
    }

    private OvertimeApplicationBatchResultRespVO processBatchApprovalAction(
            OvertimeApplicationBatchActionReqVO reqVO, String actionCode) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        String loginUserName = defaultText(SecurityFrameworkUtils.getLoginUserNickname());
        String reason = normalizeNullableText(reqVO.getReason());

        // 1. 校验状态机流转（统一校验一次，批量场景下 fromStatus 相同）
        ObjectStatusTransitionDO transition = validateTransition(OvertimeApplicationConstants.STATUS_PENDING, actionCode, reason);
        LocalDateTime approvalTime = LocalDateTime.now();

        // 2. 批量查询所有申请
        List<OvertimeApplicationDO> applications = overtimeApplicationMapper.selectBatchIds(reqVO.getIds());
        Map<Long, OvertimeApplicationDO> appMap = applications.stream()
                .collect(Collectors.toMap(OvertimeApplicationDO::getId, a -> a));

        // 3. 分类：可处理的 vs 需跳过的
        List<OvertimeApplicationDO> validList = new ArrayList<>();
        OvertimeApplicationBatchResultRespVO result = new OvertimeApplicationBatchResultRespVO();
        for (Long id : reqVO.getIds()) {
            OvertimeApplicationDO app = appMap.get(id);
            if (app == null) {
                addFailItem(result, id, "业务学习不存在");
            } else if (!Objects.equals(app.getApproverId(), loginUserId)) {
                addFailItem(result, id, "您不是该申请的审批人");
            } else if (!Objects.equals(app.getStatusCode(), OvertimeApplicationConstants.STATUS_PENDING)) {
                addFailItem(result, id, "当前状态不允许该操作");
            } else {
                validList.add(app);
            }
        }

        if (validList.isEmpty()) {
            result.setSuccessCount(0);
            result.setFailCount(result.getFailItems().size());
            return result;
        }

        // 4. 批量更新申请状态
        List<Long> validIds = validList.stream().map(OvertimeApplicationDO::getId).collect(Collectors.toList());
        OvertimeApplicationDO update = new OvertimeApplicationDO();
        update.setStatusCode(transition.getToStatusCode());
        update.setApprovalComment(reason);
        update.setApprovalTime(approvalTime);
        overtimeApplicationMapper.update(update, new LambdaQueryWrapperX<OvertimeApplicationDO>()
                .in(OvertimeApplicationDO::getId, validIds)
                .eq(OvertimeApplicationDO::getApproverId, loginUserId)
                .eq(OvertimeApplicationDO::getStatusCode, OvertimeApplicationConstants.STATUS_PENDING));

        // 5. 批量写状态日志
        List<OvertimeApplicationStatusLogDO> statusLogs = new ArrayList<>();
        for (OvertimeApplicationDO original : validList) {
            OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO();
            log.setApplicationId(original.getId());
            log.setActionType(actionCode);
            log.setFromStatus(OvertimeApplicationConstants.STATUS_PENDING);
            log.setToStatus(transition.getToStatusCode());
            log.setReason(reason);
            log.setOperatorUserId(loginUserId);
            log.setOperatorName(loginUserName);
            log.setApplicantNameSnapshot(original.getApplicantName());
            log.setOvertimeDateSnapshot(original.getOvertimeDate());
            log.setOvertimeDurationSnapshot(original.getOvertimeDuration());
            log.setRemark(buildSnapshotRemark(original));
            statusLogs.add(log);
        }
        overtimeApplicationStatusLogMapper.insertBatch(statusLogs);

        // 6. 批量写审批记录（需要 statusLogId，逐条设置后批量插入）
        List<OvertimeApplicationApprovalRecordDO> approvalRecords = new ArrayList<>();
        for (int i = 0; i < validList.size(); i++) {
            OvertimeApplicationDO original = validList.get(i);
            OvertimeApplicationStatusLogDO statusLog = statusLogs.get(i);
            OvertimeApplicationApprovalRecordDO record = new OvertimeApplicationApprovalRecordDO();
            record.setOvertimeApplicationId(original.getId());
            record.setStatusLogId(statusLog.getId());
            record.setApprovalRound(overtimeApplicationApprovalRecordMapper.countByApplicationId(original.getId()) + 1);
            record.setConclusion(transition.getToStatusCode());
            record.setOpinion(reason);
            record.setAuditorUserId(loginUserId);
            record.setAuditorName(loginUserName);
            approvalRecords.add(record);
        }
        overtimeApplicationApprovalRecordMapper.insertBatch(approvalRecords);

        // 7. 批量写审计日志
        List<BizAuditLogDO> auditLogs = new ArrayList<>();
        for (OvertimeApplicationDO original : validList) {
            BizAuditLogDO auditLog = new BizAuditLogDO();
            auditLog.setBizType(OvertimeApplicationConstants.BIZ_TYPE);
            auditLog.setBizId(original.getId());
            auditLog.setActionType(actionCode);
            auditLog.setFromStatus(OvertimeApplicationConstants.STATUS_PENDING);
            auditLog.setToStatus(transition.getToStatusCode());
            auditLog.setReason(reason);
            auditLog.setOperatorUserId(loginUserId);
            auditLog.setOperatorName(loginUserName);
            auditLog.setRemark(buildSnapshotRemark(original));
            auditLogs.add(auditLog);
        }
        bizAuditLogMapper.insertBatch(auditLogs);

        result.setSuccessCount(validList.size());
        result.setFailCount(result.getFailItems().size());
        return result;
    }

    private void addFailItem(OvertimeApplicationBatchResultRespVO result, Long id, String reason) {
        OvertimeApplicationBatchResultRespVO.FailItem failItem = new OvertimeApplicationBatchResultRespVO.FailItem();
        failItem.setId(id);
        failItem.setReason(reason);
        result.getFailItems().add(failItem);
    }

    private OvertimeApplicationDO validateApplicationExists(Long id) {
        OvertimeApplicationDO application = overtimeApplicationMapper.selectById(id);
        if (application == null) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_NOT_EXISTS);
        }
        return application;
    }

    private OvertimeApplicationDO validateReadableApplication(Long id) {
        OvertimeApplicationDO application = validateApplicationExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(application.getApplicantId(), loginUserId)
                && !Objects.equals(application.getApproverId(), loginUserId)
                && !teamDashboardAccessService.canReadSubordinateUser(
                        application.getApplicantId(), OvertimeApplicationConstants.PERMISSION_TEAM_DASHBOARD)) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_READ_FORBIDDEN);
        }
        return application;
    }

    private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus,
                        actionCode);
        if (transition == null) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.statusName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, fromStatus),
                    statusActionTextResolver.actionName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_ACTION_REASON_REQUIRED,
                    statusActionTextResolver.actionName(OvertimeApplicationConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                OvertimeApplicationConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode());
        if (toModel == null) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return transition;
    }

    private String getInitialStatusCode() {
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectInitialByObjectTypeEnabled(OvertimeApplicationConstants.STATUS_OBJECT_TYPE);
        if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return statusModel.getStatusCode();
    }

    private AdminUserRespDTO validateApprover(Long approverId) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (Objects.equals(approverId, loginUserId)) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_SELF_FORBIDDEN);
        }
        return loadUser(approverId);
    }

    private AdminUserRespDTO loadUser(Long userId) {
        if (userId == null) {
            throw invalidParamException("用户编号不能为空");
        }
        adminUserApi.validateUserList(Collections.singleton(userId)).getCheckedData();
        CommonResult<AdminUserRespDTO> result = adminUserApi.getUser(userId);
        AdminUserRespDTO user = result.getCheckedData();
        if (user == null) {
            throw exception(ErrorCodeConstants.OVERTIME_APPLICATION_APPROVER_INVALID);
        }
        return user;
    }

    private void applySaveFields(OvertimeApplicationDO target, OvertimeApplicationSaveReqVO reqVO,
                                 AdminUserRespDTO approver) {
        target.setOvertimeDate(reqVO.getOvertimeDate());
        target.setOvertimeDuration(normalizeRequiredText(reqVO.getOvertimeDuration(), "学习时长不能为空"));
        target.setOvertimeReason(normalizeRequiredText(reqVO.getOvertimeReason(), "学习原因不能为空"));
        target.setOvertimeContent(normalizeRequiredText(reqVO.getOvertimeContent(), "学习内容不能为空"));
        target.setApproverId(approver.getId());
        target.setApproverName(defaultText(approver.getNickname()));
    }

    private OvertimeApplicationRespVO toRespVO(OvertimeApplicationDO application) {
        return BeanUtils.toBean(application, OvertimeApplicationRespVO.class, this::applyStatusView);
    }

    private void applyStatusView(OvertimeApplicationRespVO respVO) {
        ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                OvertimeApplicationConstants.STATUS_OBJECT_TYPE, respVO.getStatusCode());
        if (statusModel == null) {
            respVO.setStatusName(respVO.getStatusCode());
            respVO.setAllowEdit(false);
            respVO.setTerminal(false);
            return;
        }
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
        respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
    }

    private OvertimeApplicationStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
        OvertimeApplicationStatusDictRespVO respVO = new OvertimeApplicationStatusDictRespVO();
        respVO.setStatusCode(statusModel.getStatusCode());
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setSort(statusModel.getSort());
        respVO.setInitialFlag(statusModel.getInitialFlag());
        respVO.setTerminalFlag(statusModel.getTerminalFlag());
        respVO.setAllowEdit(statusModel.getAllowEdit());
        return respVO;
    }

    private OvertimeApplicationStatusLogDO writeStatusLog(OvertimeApplicationDO application, String actionType,
                                                          String fromStatus, String toStatus, String reason) {
        OvertimeApplicationStatusLogDO log = new OvertimeApplicationStatusLogDO();
        log.setApplicationId(application.getId());
        log.setActionType(actionType);
        log.setFromStatus(fromStatus);
        log.setToStatus(toStatus);
        log.setReason(reason);
        log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        log.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        log.setApplicantNameSnapshot(application.getApplicantName());
        log.setOvertimeDateSnapshot(application.getOvertimeDate());
        log.setOvertimeDurationSnapshot(application.getOvertimeDuration());
        log.setRemark(buildSnapshotRemark(application));
        overtimeApplicationStatusLogMapper.insert(log);
        return log;
    }

    private void writeApprovalRecord(OvertimeApplicationDO application, OvertimeApplicationStatusLogDO statusLog,
                                     String reason) {
        OvertimeApplicationApprovalRecordDO record = new OvertimeApplicationApprovalRecordDO();
        record.setOvertimeApplicationId(application.getId());
        record.setStatusLogId(statusLog.getId());
        record.setApprovalRound(overtimeApplicationApprovalRecordMapper.countByApplicationId(application.getId()) + 1);
        record.setConclusion(statusLog.getToStatus());
        record.setOpinion(reason);
        record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId());
        record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        overtimeApplicationApprovalRecordMapper.insert(record);
    }

    private void writeAuditLog(OvertimeApplicationDO application, String actionType, String fromStatus,
                               String toStatus, String fieldChanges, String reason, String remark) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(OvertimeApplicationConstants.BIZ_TYPE);
        auditLog.setBizId(application.getId());
        auditLog.setActionType(actionType);
        auditLog.setFromStatus(fromStatus);
        auditLog.setToStatus(toStatus);
        auditLog.setFieldChanges(fieldChanges);
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        auditLog.setRemark(StringUtils.hasText(remark) ? remark : buildSnapshotRemark(application));
        bizAuditLogMapper.insert(auditLog);
    }

    private String buildSnapshotRemark(OvertimeApplicationDO application) {
        return "申请人：" + defaultText(application.getApplicantName())
                + "，学习日期：" + application.getOvertimeDate()
                + "，学习时长：" + defaultText(application.getOvertimeDuration());
    }

    private String buildFieldChanges(OvertimeApplicationDO before, OvertimeApplicationDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "overtimeDate", valueOf(before, OvertimeApplicationDO::getOvertimeDate),
                valueOf(after, OvertimeApplicationDO::getOvertimeDate));
        appendFieldChange(fieldChanges, "overtimeDuration",
                valueOf(before, OvertimeApplicationDO::getOvertimeDuration),
                valueOf(after, OvertimeApplicationDO::getOvertimeDuration));
        appendFieldChange(fieldChanges, "overtimeReason", valueOf(before, OvertimeApplicationDO::getOvertimeReason),
                valueOf(after, OvertimeApplicationDO::getOvertimeReason));
        appendFieldChange(fieldChanges, "overtimeContent", valueOf(before, OvertimeApplicationDO::getOvertimeContent),
                valueOf(after, OvertimeApplicationDO::getOvertimeContent));
        appendFieldChange(fieldChanges, "approverId", valueOf(before, OvertimeApplicationDO::getApproverId),
                valueOf(after, OvertimeApplicationDO::getApproverId));
        appendFieldChange(fieldChanges, "approverName", valueOf(before, OvertimeApplicationDO::getApproverName),
                valueOf(after, OvertimeApplicationDO::getApproverName));
        appendFieldChange(fieldChanges, "statusCode", valueOf(before, OvertimeApplicationDO::getStatusCode),
                valueOf(after, OvertimeApplicationDO::getStatusCode));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
        if (Objects.equals(before, after)) {
            return;
        }
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("before", before);
        value.put("after", after);
        fieldChanges.put(fieldName, value);
    }

    private <T> T valueOf(OvertimeApplicationDO application, Function<OvertimeApplicationDO, T> getter) {
        return application == null ? null : getter.apply(application);
    }

    private OvertimeApplicationDO cloneApplication(OvertimeApplicationDO source) {
        OvertimeApplicationDO target = new OvertimeApplicationDO();
        target.setId(source.getId());
        target.setApplicantId(source.getApplicantId());
        target.setApplicantName(source.getApplicantName());
        target.setOvertimeDate(source.getOvertimeDate());
        target.setOvertimeDuration(source.getOvertimeDuration());
        target.setOvertimeReason(source.getOvertimeReason());
        target.setOvertimeContent(source.getOvertimeContent());
        target.setApproverId(source.getApproverId());
        target.setApproverName(source.getApproverName());
        target.setStatusCode(source.getStatusCode());
        target.setApprovalComment(source.getApprovalComment());
        target.setSubmitTime(source.getSubmitTime());
        target.setApprovalTime(source.getApprovalTime());
        return target;
    }

    private OvertimeApplicationDO mergeUpdated(OvertimeApplicationDO current, OvertimeApplicationDO update) {
        OvertimeApplicationDO after = cloneApplication(current);
        if (update.getOvertimeDate() != null) {
            after.setOvertimeDate(update.getOvertimeDate());
        }
        if (update.getOvertimeDuration() != null) {
            after.setOvertimeDuration(update.getOvertimeDuration());
        }
        if (update.getOvertimeReason() != null) {
            after.setOvertimeReason(update.getOvertimeReason());
        }
        if (update.getOvertimeContent() != null) {
            after.setOvertimeContent(update.getOvertimeContent());
        }
        if (update.getApproverId() != null) {
            after.setApproverId(update.getApproverId());
        }
        if (update.getApproverName() != null) {
            after.setApproverName(update.getApproverName());
        }
        if (update.getStatusCode() != null) {
            after.setStatusCode(update.getStatusCode());
        }
        after.setApprovalComment(update.getApprovalComment());
        if (update.getSubmitTime() != null) {
            after.setSubmitTime(update.getSubmitTime());
        }
        after.setApprovalTime(update.getApprovalTime());
        return after;
    }

    private String normalizeRequiredText(String value, String message) {
        if (!StringUtils.hasText(value)) {
            throw invalidParamException(message);
        }
        return value.trim();
    }

    private String normalizeNullableText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    private String defaultText(String value) {
        return StringUtils.hasText(value) ? value : "";
    }
}
