package com.njcn.rdms.module.project.service.workreport.common;

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.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.constant.SignOffConstants;
import com.njcn.rdms.module.project.constant.WorkReportConstants;
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.*;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.*;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.*;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.*;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
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.dataobject.workreport.common.PersonalReportPlanItemDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.common.PersonalReportReviewItemDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportMemberSnapshotItem;
import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportApprovalRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportSignOffRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportApprovalRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportCurrentItemDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportNextItemDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportApprovalRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportTravelSegmentDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
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.dal.mysql.workreport.common.PersonalReportPlanItemMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.common.PersonalReportReviewItemMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.common.WorkReportStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportApprovalRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportSignOffMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.monthly.MonthlyReportSignOffRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportApprovalRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportCurrentItemMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportNextItemMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportApprovalRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportTravelSegmentMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.signoff.EmployeeSignOffStatusService;
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.dept.DeptApi;
import com.njcn.rdms.module.system.api.dept.PostApi;
import com.njcn.rdms.module.system.api.user.UserSignatureApi;
import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO;
import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.UserManagementRelationApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import com.njcn.rdms.module.system.api.user.dto.UserSignatureRespDTO;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.IsoFields;
import java.util.*;
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 WorkReportCommonService {

    private static final List<String> ALLOW_DELETE_STATUSES = List.of(
            WorkReportConstants.STATUS_DRAFT,
            WorkReportConstants.STATUS_REJECTED);
    private static final List<String> TEAM_VISIBLE_STATUS_CODES = List.of(
            WorkReportConstants.STATUS_PENDING_APPROVAL,
            WorkReportConstants.STATUS_APPROVED,
            WorkReportConstants.STATUS_REJECTED);

    @Resource
    private WeeklyReportMapper weeklyReportMapper;
    @Resource
    private MonthlyReportMapper monthlyReportMapper;
    @Resource
    private ProjectReportMapper projectReportMapper;
    @Resource
    private PersonalReportReviewItemMapper personalReportReviewItemMapper;
    @Resource
    private PersonalReportPlanItemMapper personalReportPlanItemMapper;
    @Resource
    private WeeklyReportTravelSegmentMapper weeklyReportTravelSegmentMapper;
    @Resource
    private ProjectReportCurrentItemMapper projectReportCurrentItemMapper;
    @Resource
    private ProjectReportNextItemMapper projectReportNextItemMapper;
    @Resource
    private WeeklyReportApprovalRecordMapper weeklyReportApprovalRecordMapper;
    @Resource
    private MonthlyReportApprovalRecordMapper monthlyReportApprovalRecordMapper;
    @Resource
    private MonthlyReportSignOffMapper monthlyReportSignOffMapper;
    @Resource
    private MonthlyReportSignOffRecordMapper monthlyReportSignOffRecordMapper;
    @Resource
    private ProjectReportApprovalRecordMapper projectReportApprovalRecordMapper;
    @Resource
    private WorkReportStatusLogMapper workReportStatusLogMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Resource
    private StatusActionTextResolver statusActionTextResolver;
    @Resource
    private EmployeeSignOffStatusService employeeSignOffStatusService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private PostApi postApi;
    @Resource
    private UserManagementRelationApi userManagementRelationApi;
    @Resource
    private UserSignatureApi userSignatureApi;
    @Resource
    private ObjectPermissionApi objectPermissionApi;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private TeamDashboardAccessService teamDashboardAccessService;
    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    public List<WorkReportStatusDictRespVO> getStatusDict() {
        return objectStatusModelMapper.selectListByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE).stream()
                .map(this::toStatusDictRespVO)
                .collect(Collectors.toList());
    }

    public List<ProjectReportOwnerProjectOptionRespVO> getProjectReportOwnerProjectOptions() {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        return projectMapper.selectListByManagerUserId(loginUserId).stream()
                .map(project -> {
                    ProjectReportOwnerProjectOptionRespVO respVO = new ProjectReportOwnerProjectOptionRespVO();
                    respVO.setId(project.getId());
                    respVO.setProjectCode(project.getProjectCode());
                    respVO.setProjectName(project.getProjectName());
                    return respVO;
                })
                .collect(Collectors.toList());
    }

    public void validateCurrentUserIsProjectReportProjectOwner(Long projectId) {
        ProjectDO project = validateProjectExists(projectId);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(project.getManagerUserId(), loginUserId)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_PROJECT_OWNER_ONLY);
        }
    }

    public WeeklyReportRespVO initWeeklyReport() {
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        WeeklyReportRespVO respVO = new WeeklyReportRespVO();
        fillPersonalBase(respVO, profile);
        applyStatusView(respVO, getInitialStatusModel());
        respVO.setIsBusinessTrip(false);
        respVO.setReviewItems(Collections.emptyList());
        respVO.setPlanItems(Collections.emptyList());
        respVO.setTravelSegments(Collections.emptyList());
        return respVO;
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO) {
        return createWeeklyReportInternal(reqVO, null);
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createWeeklyReport(WeeklyReportSaveReqVO reqVO, Long overrideReporterId) {
        return createWeeklyReportInternal(reqVO, overrideReporterId);
    }

    private Long createWeeklyReportInternal(WeeklyReportSaveReqVO reqVO, Long overrideReporterId) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        CurrentUserProfile profile = resolveProfile(overrideReporterId, true);
        validateWeeklyDuplicate(profile.userId(), reqVO.getPeriodKey(), null);

        WeeklyReportDO report = new WeeklyReportDO();
        applyWeeklySaveFields(report, reqVO, profile);
        report.setStatusCode(getInitialStatusCode());
        weeklyReportMapper.insert(report);
        replaceWeeklyChildren(report.getId(), reqVO);
        WeeklyReportDO latest = weeklyReportMapper.selectById(report.getId());
        writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, latest.getId(), WorkReportConstants.ACTION_CREATE,
                null, latest.getStatusCode(), null, buildWeeklyRemark(latest));
        return report.getId();
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateWeeklyReport(Long id, WeeklyReportSaveReqVO reqVO) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        WeeklyReportDO current = validateWeeklyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        validateAllowEdit(current.getStatusCode());
        validateWeeklyDuplicate(loginUserId, reqVO.getPeriodKey(), id);

        CurrentUserProfile profile = loadCurrentUserProfile(false);
        WeeklyReportDO update = new WeeklyReportDO();
        update.setId(id);
        applyWeeklySaveFields(update, reqVO, profile);
        int updateCount = weeklyReportMapper.updateByIdAndStatus(update, id, current.getStatusCode());
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }
        replaceWeeklyChildren(id, reqVO);
        WeeklyReportDO latest = weeklyReportMapper.selectById(id);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, WorkReportConstants.ACTION_UPDATE,
                current.getStatusCode(), latest.getStatusCode(), null, buildWeeklyRemark(latest));
    }

    public WeeklyReportRespVO getWeeklyReport(Long id) {
        WeeklyReportDO report = validateReadableWeeklyReport(id);
        return toWeeklyRespVO(report, true);
    }

    public PageResult<WeeklyReportRespVO> getWeeklyReportPage(WeeklyReportPageReqVO reqVO) {
        return getWeeklyReportPage(reqVO, false);
    }

    public PageResult<WeeklyReportRespVO> getWeeklyReportExportPage(WeeklyReportPageReqVO reqVO) {
        return getWeeklyReportPage(reqVO, true);
    }

    private PageResult<WeeklyReportRespVO> getWeeklyReportPage(WeeklyReportPageReqVO reqVO,
            boolean includeSelfInTeamMode) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<WeeklyReportDO> pageResult;
        if (reqVO.getReporterIds() != null) {
            List<Long> reporterIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getReporterIds(), WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
            if (includeSelfInTeamMode) {
                reporterIds = appendLoginUser(reporterIds);
            }
            pageResult = weeklyReportMapper.selectReporterPage(reporterIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
        } else {
            pageResult = weeklyReportMapper.selectReporterPage(loginUserId, reqVO, getEnabledStatusCodes());
        }
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toWeeklyRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    public PageResult<WeeklyReportRespVO> getWeeklyApprovalPage(WeeklyReportPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<WeeklyReportDO> pageResult = weeklyReportMapper.selectApprovalPage(loginUserId, reqVO);
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toWeeklyRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    @Transactional(rollbackFor = Exception.class)
    public void submitWeeklyReport(Long id) {
        WeeklyReportDO current = validateWeeklyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        String actionCode = resolveSubmitAction(current.getStatusCode());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, null);
        CurrentUserProfile profile = loadCurrentUserProfile(true);

        LocalDateTime submitTime = LocalDateTime.now();
        int updateCount = weeklyReportMapper.updateSubmitFieldsByIdAndStatus(id, current.getStatusCode(),
                profile.deptName(), profile.postName(), profile.directManagerId(), profile.directManagerName(),
                transition.getToStatusCode(), submitTime, String.valueOf(loginUserId));
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        WeeklyReportDO latest = weeklyReportMapper.selectById(id);
        writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_WEEKLY, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), null, buildWeeklyRemark(latest));
        writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), null, buildWeeklyRemark(latest));
        publishPersonalReportPendingApprovalNotice(latest.getSupervisorUserId(),
                NotifyTemplateCodeConstants.WORK_REPORT_PENDING_APPROVAL_NOTIFY,
                "周报", latest.getReporterName(), formatWeeklyPeriodLabelForNotify(latest));
    }

    @Transactional(rollbackFor = Exception.class)
    public void approveWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO) {
        processWeeklyApproval(id, WorkReportConstants.ACTION_APPROVE, normalizeReason(reqVO));
    }

    @Transactional(rollbackFor = Exception.class)
    public void rejectWeeklyReport(Long id, WorkReportStatusActionReqVO reqVO) {
        processWeeklyApproval(id, WorkReportConstants.ACTION_REJECT, normalizeReason(reqVO));
    }

    @Transactional(rollbackFor = Exception.class)
    public void deleteWeeklyReport(Long id) {
        WeeklyReportDO current = validateWeeklyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        validateDeleteAllowed(current.getStatusCode());
        int deleteCount = weeklyReportMapper.deleteByIdAndStatusesAndReporterId(id, ALLOW_DELETE_STATUSES, loginUserId);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED);
        }
        personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id);
        personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id);
        weeklyReportTravelSegmentMapper.deleteByWeeklyReportId(id);
        weeklyReportApprovalRecordMapper.deleteByWeeklyReportId(id);
        workReportStatusLogMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, WorkReportConstants.ACTION_DELETE,
                current.getStatusCode(), null, null, buildWeeklyRemark(current));
    }

    public List<WorkReportStatusLogRespVO> getWeeklyStatusLogs(Long id) {
        validateReadableWeeklyReport(id);
        return BeanUtils.toBean(workReportStatusLogMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, id),
                WorkReportStatusLogRespVO.class);
    }

    public List<WorkReportApprovalRecordRespVO> getWeeklyApprovalRecords(Long id) {
        validateReadableWeeklyReport(id);
        return BeanUtils.toBean(weeklyReportApprovalRecordMapper.selectListByWeeklyReportId(id),
                WorkReportApprovalRecordRespVO.class);
    }

    public List<WeeklyReportExportVO> getWeeklyExportList(WeeklyReportPageReqVO reqVO) {
        reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        return BeanUtils.toBean(getWeeklyReportPage(reqVO).getList(), WeeklyReportExportVO.class);
    }

    public MonthlyReportRespVO initMonthlyReport() {
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        MonthlyReportRespVO respVO = new MonthlyReportRespVO();
        fillPersonalBase(respVO, profile);
        applyStatusView(respVO, getInitialStatusModel());
        respVO.setReviewItems(Collections.emptyList());
        respVO.setPlanItems(Collections.emptyList());
        return respVO;
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO) {
        return createMonthlyReportInternal(reqVO, null);
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createMonthlyReport(MonthlyReportSaveReqVO reqVO, Long overrideReporterId) {
        return createMonthlyReportInternal(reqVO, overrideReporterId);
    }

    private Long createMonthlyReportInternal(MonthlyReportSaveReqVO reqVO, Long overrideReporterId) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        CurrentUserProfile profile = resolveProfile(overrideReporterId, true);
        validateMonthlyDuplicate(profile.userId(), reqVO.getPeriodKey(), null);

        MonthlyReportDO report = new MonthlyReportDO();
        applyMonthlySaveFields(report, reqVO, profile);
        report.setStatusCode(getInitialStatusCode());
        report.setSignOffStatus(SignOffConstants.STATUS_NOT_STARTED);
        monthlyReportMapper.insert(report);
        replaceMonthlyChildren(report.getId(), reqVO);
        MonthlyReportDO latest = monthlyReportMapper.selectById(report.getId());
        writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, latest.getId(), WorkReportConstants.ACTION_CREATE,
                null, latest.getStatusCode(), null, buildMonthlyRemark(latest));
        return report.getId();
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateMonthlyReport(Long id, MonthlyReportSaveReqVO reqVO) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        MonthlyReportDO current = validateMonthlyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        validateAllowEdit(current.getStatusCode());
        validateMonthlyDuplicate(loginUserId, reqVO.getPeriodKey(), id);

        CurrentUserProfile profile = loadCurrentUserProfile(false);
        MonthlyReportDO update = new MonthlyReportDO();
        update.setId(id);
        applyMonthlySaveFields(update, reqVO, profile);
        int updateCount = monthlyReportMapper.updateByIdAndStatus(update, id, current.getStatusCode());
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }
        replaceMonthlyChildren(id, reqVO);
        MonthlyReportDO latest = monthlyReportMapper.selectById(id);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, WorkReportConstants.ACTION_UPDATE,
                current.getStatusCode(), latest.getStatusCode(), null, buildMonthlyRemark(latest));
    }

    public MonthlyReportRespVO getMonthlyReport(Long id) {
        MonthlyReportDO report = validateReadableMonthlyReport(id);
        return toMonthlyRespVO(report, true);
    }

    public PageResult<MonthlyReportRespVO> getMonthlyReportPage(MonthlyReportPageReqVO reqVO) {
        return getMonthlyReportPage(reqVO, false);
    }

    public PageResult<MonthlyReportRespVO> getMonthlyReportExportPage(MonthlyReportPageReqVO reqVO) {
        return getMonthlyReportPage(reqVO, true);
    }

    private PageResult<MonthlyReportRespVO> getMonthlyReportPage(MonthlyReportPageReqVO reqVO,
            boolean includeSelfInTeamMode) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<MonthlyReportDO> pageResult;
        if (reqVO.getReporterIds() != null) {
            List<Long> reporterIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getReporterIds(), WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
            if (includeSelfInTeamMode) {
                reporterIds = appendLoginUser(reporterIds);
            }
            pageResult = monthlyReportMapper.selectReporterPage(reporterIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
        } else {
            pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO, getEnabledStatusCodes());
        }
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toMonthlyRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    public PageResult<MonthlyReportRespVO> getMonthlyApprovalPage(MonthlyReportPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<MonthlyReportDO> pageResult = monthlyReportMapper.selectApprovalPage(loginUserId, reqVO);
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toMonthlyRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    public PageResult<MonthlyReportRespVO> getMonthlySignOffPage(MonthlyReportPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        reqVO.setStatusCode(WorkReportConstants.STATUS_APPROVED);
        reqVO.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
        PageResult<MonthlyReportDO> pageResult = monthlyReportMapper.selectReporterPage(loginUserId, reqVO,
                List.of(WorkReportConstants.STATUS_APPROVED));
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toMonthlyRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    public MonthlyReportSignOffDetailRespVO getMonthlySignOffDetail(Long id) {
        MonthlyReportDO report = validateMonthlyReportExists(id);
        validateReporter(SecurityFrameworkUtils.getLoginUserId(), report.getReporterId());
        MonthlyReportSignOffDetailRespVO respVO = new MonthlyReportSignOffDetailRespVO();
        respVO.setReport(toMonthlyRespVO(report, true));
        MonthlyReportApprovalRecordDO latestApprovalRecord = monthlyReportApprovalRecordMapper
                .selectLatestApprovedByMonthlyReportId(id);
        respVO.setLatestApprovalRecord(latestApprovalRecord == null ? null
                : BeanUtils.toBean(latestApprovalRecord, MonthlyReportApprovalRecordRespVO.class));
        return respVO;
    }

    @Transactional(rollbackFor = Exception.class)
    public void submitMonthlyReport(Long id) {
        MonthlyReportDO current = validateMonthlyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        String actionCode = resolveSubmitAction(current.getStatusCode());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, null);
        CurrentUserProfile profile = loadCurrentUserProfile(true);

        LocalDateTime submitTime = LocalDateTime.now();
        int updateCount = monthlyReportMapper.updateSubmitFieldsByIdAndStatus(id, current.getStatusCode(),
                profile.deptName(), profile.postName(), profile.directManagerId(), profile.directManagerName(),
                transition.getToStatusCode(), submitTime, String.valueOf(loginUserId));
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        MonthlyReportDO latest = monthlyReportMapper.selectById(id);
        writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_MONTHLY, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), null, buildMonthlyRemark(latest));
        writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), null, buildMonthlyRemark(latest));
        publishPersonalReportPendingApprovalNotice(latest.getSupervisorUserId(),
                NotifyTemplateCodeConstants.WORK_REPORT_PENDING_APPROVAL_NOTIFY,
                "月报", latest.getReporterName(), latest.getPeriodLabel());
    }

    @Transactional(rollbackFor = Exception.class)
    public void approveMonthlyReport(Long id, MonthlyReportApproveReqVO reqVO) {
        processMonthlyApproval(id, WorkReportConstants.ACTION_APPROVE, reqVO);
    }

    @Transactional(rollbackFor = Exception.class)
    public void rejectMonthlyReport(Long id, WorkReportStatusActionReqVO reqVO) {
        MonthlyReportApproveReqVO rejectReqVO = new MonthlyReportApproveReqVO();
        rejectReqVO.setReason(normalizeReason(reqVO));
        processMonthlyApproval(id, WorkReportConstants.ACTION_REJECT, rejectReqVO);
    }

    @Transactional(rollbackFor = Exception.class)
    public void confirmMonthlySignOff(Long id, MonthlyReportSignOffConfirmReqVO reqVO) {
        MonthlyReportDO current = validateMonthlyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        if (!WorkReportConstants.STATUS_APPROVED.equals(current.getStatusCode())
                || !SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM.equals(current.getSignOffStatus())) {
            throw exception(ErrorCodeConstants.WORK_REPORT_SIGN_OFF_NOT_ALLOWED);
        }
        MonthlyReportSignOffDO signOff = validateMonthlySignOffExists(current.getId());
        employeeSignOffStatusService.validateTransition(signOff.getStatusCode(), SignOffConstants.ACTION_CONFIRM, null);
        MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper
                .selectLatestApprovedByMonthlyReportId(current.getId());
        if (approvalRecord == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS);
        }

        UserSignatureRespDTO userSignature = getUserSignature(loginUserId);
        LocalDate signedDate = reqVO.getEmployeeSignedDate() != null ? reqVO.getEmployeeSignedDate() : LocalDate.now();
        MonthlyReportApprovalRecordDO approvalUpdate = new MonthlyReportApprovalRecordDO();
        approvalUpdate.setId(approvalRecord.getId());
        approvalUpdate.setMeetingDate(reqVO.getMeetingDate());
        approvalUpdate.setEmployeeSignName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        approvalUpdate.setEmployeeSignatureFileId(userSignature == null ? null : userSignature.getFileId());
        approvalUpdate.setEmployeeSignedDate(signedDate);
        monthlyReportApprovalRecordMapper.updateById(approvalUpdate);

        MonthlyReportDO reportUpdate = new MonthlyReportDO();
        reportUpdate.setSignOffStatus(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED);
        int reportUpdateCount = monthlyReportMapper.updateByIdAndStatusAndSignOffStatus(reportUpdate, current.getId(),
                current.getStatusCode(), current.getSignOffStatus());
        if (reportUpdateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        MonthlyReportSignOffDO signOffUpdate = new MonthlyReportSignOffDO();
        signOffUpdate.setStatusCode(SignOffConstants.STATUS_EMPLOYEE_CONFIRMED);
        signOffUpdate.setConfirmUserId(loginUserId);
        signOffUpdate.setConfirmUserName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        signOffUpdate.setConfirmTime(LocalDateTime.now());
        int signOffUpdateCount = monthlyReportSignOffMapper.updateByMonthlyReportIdAndStatus(signOffUpdate,
                current.getId(), signOff.getStatusCode());
        if (signOffUpdateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }
        writeMonthlySignOffRecord(signOff.getId(), current.getId(), approvalRecord.getId(), signOff.getBatchNo(),
                SignOffConstants.ACTION_CONFIRM, signOff.getStatusCode(), SignOffConstants.STATUS_EMPLOYEE_CONFIRMED);
    }

    @Transactional(rollbackFor = Exception.class)
    public void deleteMonthlyReport(Long id) {
        MonthlyReportDO current = validateMonthlyReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getReporterId());
        validateDeleteAllowed(current.getStatusCode());
        int deleteCount = monthlyReportMapper.deleteByIdAndStatusesAndReporterId(id, ALLOW_DELETE_STATUSES, loginUserId);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED);
        }
        personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id);
        personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id);
        monthlyReportApprovalRecordMapper.deleteByMonthlyReportId(id);
        workReportStatusLogMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, WorkReportConstants.ACTION_DELETE,
                current.getStatusCode(), null, null, buildMonthlyRemark(current));
    }

    public List<WorkReportStatusLogRespVO> getMonthlyStatusLogs(Long id) {
        validateReadableMonthlyReport(id);
        return BeanUtils.toBean(workReportStatusLogMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, id),
                WorkReportStatusLogRespVO.class);
    }

    public List<MonthlyReportApprovalRecordRespVO> getMonthlyApprovalRecords(Long id) {
        validateReadableMonthlyReport(id);
        return BeanUtils.toBean(monthlyReportApprovalRecordMapper.selectListByMonthlyReportId(id),
                MonthlyReportApprovalRecordRespVO.class);
    }

    public List<MonthlyReportExportVO> getMonthlyExportList(MonthlyReportPageReqVO reqVO) {
        reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        return BeanUtils.toBean(getMonthlyReportPage(reqVO).getList(), MonthlyReportExportVO.class);
    }

    public List<MonthlyReportDO> listMonthlyReportsForSignOff(Collection<Long> reporterIds, String periodMonth) {
        return monthlyReportMapper.selectListByReporterIdsAndPeriodKey(reporterIds, periodMonth,
                List.of(WorkReportConstants.STATUS_APPROVED));
    }

    public void issueMonthlySignOff(List<MonthlyReportDO> reports, Long batchNo) {
        if (reports == null || reports.isEmpty()) {
            throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_CANDIDATE_EMPTY);
        }
        Long operatorUserId = SecurityFrameworkUtils.getLoginUserId();
        String operatorName = defaultText(SecurityFrameworkUtils.getLoginUserNickname());
        LocalDateTime issueTime = LocalDateTime.now();
        for (MonthlyReportDO report : reports) {
            if (!WorkReportConstants.STATUS_APPROVED.equals(report.getStatusCode())
                    || !isIssueAllowed(report.getSignOffStatus())) {
                throw exception(ErrorCodeConstants.WORK_REPORT_SIGN_OFF_NOT_ALLOWED);
            }
            MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper
                    .selectLatestApprovedByMonthlyReportId(report.getId());
            if (approvalRecord == null) {
                throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_APPROVAL_RECORD_NOT_EXISTS);
            }

            MonthlyReportDO reportUpdate = new MonthlyReportDO();
            reportUpdate.setSignOffStatus(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
            int reportUpdateCount = monthlyReportMapper.updateByIdAndStatusAndSignOffStatus(reportUpdate, report.getId(),
                    report.getStatusCode(), report.getSignOffStatus());
            if (reportUpdateCount != 1) {
                throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
            }

            MonthlyReportSignOffDO currentSignOff = monthlyReportSignOffMapper.selectByMonthlyReportId(report.getId());
            if (currentSignOff == null) {
                MonthlyReportSignOffDO signOff = new MonthlyReportSignOffDO();
                signOff.setMonthlyReportId(report.getId());
                signOff.setBatchNo(batchNo);
                signOff.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
                signOff.setIssueUserId(operatorUserId);
                signOff.setIssueUserName(operatorName);
                signOff.setIssueTime(issueTime);
                monthlyReportSignOffMapper.insert(signOff);
                writeMonthlySignOffRecord(signOff.getId(), report.getId(), approvalRecord.getId(), batchNo,
                        SignOffConstants.ACTION_ISSUE, SignOffConstants.STATUS_NOT_STARTED,
                        SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
            } else {
                employeeSignOffStatusService.validateTransition(currentSignOff.getStatusCode(), SignOffConstants.ACTION_ISSUE, null);
                MonthlyReportSignOffDO signOffUpdate = new MonthlyReportSignOffDO();
                signOffUpdate.setStatusCode(SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
                signOffUpdate.setBatchNo(batchNo);
                signOffUpdate.setIssueUserId(operatorUserId);
                signOffUpdate.setIssueUserName(operatorName);
                signOffUpdate.setIssueTime(issueTime);
                signOffUpdate.setConfirmUserId(null);
                signOffUpdate.setConfirmUserName(null);
                signOffUpdate.setConfirmTime(null);
                int signOffUpdateCount = monthlyReportSignOffMapper.updateByMonthlyReportIdAndStatus(signOffUpdate,
                        report.getId(), currentSignOff.getStatusCode());
                if (signOffUpdateCount != 1) {
                    throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
                }
                writeMonthlySignOffRecord(currentSignOff.getId(), report.getId(), approvalRecord.getId(), batchNo,
                        SignOffConstants.ACTION_ISSUE, currentSignOff.getStatusCode(),
                        SignOffConstants.STATUS_PENDING_EMPLOYEE_CONFIRM);
            }
        }
    }

    public void syncPerformanceResult(Long reporterId, String periodMonth, String performanceResult) {
        MonthlyReportDO report = monthlyReportMapper.selectByReporterIdAndPeriodKey(reporterId, periodMonth);
        if (report == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MONTHLY_REPORT_NOT_EXISTS);
        }
        MonthlyReportApprovalRecordDO approvalRecord = monthlyReportApprovalRecordMapper
                .selectLatestApprovedByMonthlyReportId(report.getId());
        if (approvalRecord == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MONTHLY_APPROVAL_RECORD_NOT_EXISTS);
        }
        MonthlyReportApprovalRecordDO update = new MonthlyReportApprovalRecordDO();
        update.setId(approvalRecord.getId());
        update.setPerformanceResult(performanceResult);
        monthlyReportApprovalRecordMapper.updateById(update);
    }

    public ProjectReportRespVO initProjectReport(Long projectId) {
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        ProjectDO project = validateProjectExists(projectId);
        ProjectReportRespVO respVO = new ProjectReportRespVO();
        respVO.setProjectId(project.getId());
        respVO.setProjectName(project.getProjectName());
        respVO.setProjectOwnerId(profile.userId());
        respVO.setProjectOwnerName(profile.userName());
        respVO.setTechnicalOwnerName(resolveProjectTechnicalOwnerName(projectId));
        respVO.setProjectMemberSnapshot(BeanUtils.toBean(buildProjectMemberSnapshot(projectId),
                WorkReportMemberSnapshotRespVO.class));
        respVO.setSupervisorUserId(profile.directManagerId());
        respVO.setSupervisorName(profile.directManagerName());
        applyStatusView(respVO, getInitialStatusModel());
        respVO.setCurrentItems(Collections.emptyList());
        respVO.setNextItems(Collections.emptyList());
        return respVO;
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createProjectReport(ProjectReportSaveReqVO reqVO) {
        return createProjectReportInternal(reqVO, null);
    }

    @Transactional(rollbackFor = Exception.class)
    public Long createProjectReport(ProjectReportSaveReqVO reqVO, Long overrideReporterId) {
        return createProjectReportInternal(reqVO, overrideReporterId);
    }

    public ProjectReportRespVO mergeProjectDraft(ProjectReportRespVO latestDraft, ProjectReportRefreshDraftReqVO reqVO) {
        ProjectReportRespVO respVO = latestDraft;
        respVO.setProjectStatusDesc(normalizeNullableText(reqVO.getProjectStatusDesc()));
        respVO.setProjectProgressPlan(normalizeNullableText(reqVO.getProjectProgressPlan()));
        respVO.setProjectKeyPoints(normalizeNullableText(reqVO.getProjectKeyPoints()));
        respVO.setProjectProblems(normalizeNullableText(reqVO.getProjectProblems()));
        respVO.setCurrentItems(mergeProjectItems(reqVO.getCurrentItems(), latestDraft.getCurrentItems()));
        respVO.setNextItems(mergeProjectItems(reqVO.getNextItems(), latestDraft.getNextItems()));
        respVO.setTotalWorkHours(sumProjectCurrentWorkHoursResp(respVO.getCurrentItems()));
        return respVO;
    }

    private Long createProjectReportInternal(ProjectReportSaveReqVO reqVO, Long overrideReporterId) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        validateProjectFlag(reqVO.getFlag());
        CurrentUserProfile profile = resolveProfile(overrideReporterId, true);
        validateProjectReportDuplicate(reqVO.getProjectId(), reqVO.getPeriodKey(), profile.userId(), null);
        ProjectDO project = validateProjectExists(reqVO.getProjectId());

        ProjectReportDO report = new ProjectReportDO();
        applyProjectSaveFields(report, reqVO, profile, project);
        report.setStatusCode(getInitialStatusCode());
        projectReportMapper.insert(report);
        replaceProjectChildren(report.getId(), reqVO);
        ProjectReportDO latest = projectReportMapper.selectById(report.getId());
        writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, latest.getId(), WorkReportConstants.ACTION_CREATE,
                null, latest.getStatusCode(), null, buildProjectRemark(latest));
        return report.getId();
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateProjectReport(Long id, ProjectReportSaveReqVO reqVO) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        validateProjectFlag(reqVO.getFlag());
        ProjectReportDO current = validateProjectReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getProjectOwnerId());
        validateAllowEdit(current.getStatusCode());
        validateProjectReportDuplicate(reqVO.getProjectId(), reqVO.getPeriodKey(), loginUserId, id);

        CurrentUserProfile profile = loadCurrentUserProfile(false);
        ProjectDO project = validateProjectExists(reqVO.getProjectId());
        ProjectReportDO update = new ProjectReportDO();
        update.setId(id);
        applyProjectSaveFields(update, reqVO, profile, project);
        update.setTechnicalOwnerName(current.getTechnicalOwnerName());
        int updateCount = projectReportMapper.updateByIdAndStatus(update, id, current.getStatusCode());
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }
        replaceProjectChildren(id, reqVO);
        ProjectReportDO latest = projectReportMapper.selectById(id);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, WorkReportConstants.ACTION_UPDATE,
                current.getStatusCode(), latest.getStatusCode(), null, buildProjectRemark(latest));
    }

    public ProjectReportRespVO getProjectReport(Long id) {
        ProjectReportDO report = validateReadableProjectReport(id);
        return toProjectRespVO(report, true);
    }

    public PageResult<ProjectReportRespVO> getProjectReportPage(ProjectReportPageReqVO reqVO) {
        return getProjectReportPage(reqVO, false);
    }

    public PageResult<ProjectReportRespVO> getProjectReportExportPage(ProjectReportPageReqVO reqVO) {
        return getProjectReportPage(reqVO, true);
    }

    private PageResult<ProjectReportRespVO> getProjectReportPage(ProjectReportPageReqVO reqVO,
            boolean includeSelfInTeamMode) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<ProjectReportDO> pageResult;
        if (reqVO.getProjectOwnerIds() != null) {
            List<Long> projectOwnerIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getProjectOwnerIds(), WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
            if (includeSelfInTeamMode) {
                projectOwnerIds = appendLoginUser(projectOwnerIds);
            }
            pageResult = projectReportMapper.selectReporterPage(projectOwnerIds, reqVO, TEAM_VISIBLE_STATUS_CODES);
        } else {
            pageResult = projectReportMapper.selectReporterPage(loginUserId, reqVO, getEnabledStatusCodes());
        }
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toProjectRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    public PageResult<ProjectReportRespVO> getProjectApprovalPage(ProjectReportPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PageResult<ProjectReportDO> pageResult = projectReportMapper.selectApprovalPage(loginUserId, reqVO);
        return new PageResult<>(pageResult.getList().stream()
                .map(report -> toProjectRespVO(report, false))
                .collect(Collectors.toList()), pageResult.getTotal());
    }

    @Transactional(rollbackFor = Exception.class)
    public void submitProjectReport(Long id) {
        ProjectReportDO current = validateProjectReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getProjectOwnerId());
        String actionCode = resolveSubmitAction(current.getStatusCode());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, null);
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        ProjectDO project = validateProjectExists(current.getProjectId());

        LocalDateTime submitTime = LocalDateTime.now();
        int updateCount = projectReportMapper.updateSubmitFieldsByIdAndStatus(id, current.getStatusCode(),
                project.getProjectName(), profile.userId(), profile.userName(), buildProjectMemberSnapshot(project.getId()),
                profile.directManagerId(), profile.directManagerName(), transition.getToStatusCode(), submitTime,
                String.valueOf(loginUserId));
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        ProjectReportDO latest = projectReportMapper.selectById(id);
        writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_PROJECT, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), null, buildProjectRemark(latest));
        writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), null, buildProjectRemark(latest));
        publishProjectReportPendingApprovalNotice(latest);
    }

    @Transactional(rollbackFor = Exception.class)
    public void approveProjectReport(Long id, WorkReportStatusActionReqVO reqVO) {
        processProjectApproval(id, WorkReportConstants.ACTION_APPROVE, normalizeReason(reqVO));
    }

    @Transactional(rollbackFor = Exception.class)
    public void rejectProjectReport(Long id, WorkReportStatusActionReqVO reqVO) {
        processProjectApproval(id, WorkReportConstants.ACTION_REJECT, normalizeReason(reqVO));
    }

    @Transactional(rollbackFor = Exception.class)
    public void deleteProjectReport(Long id) {
        ProjectReportDO current = validateProjectReportExists(id);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateReporter(loginUserId, current.getProjectOwnerId());
        validateDeleteAllowed(current.getStatusCode());
        int deleteCount = projectReportMapper.deleteByIdAndStatusesAndReporterId(id, ALLOW_DELETE_STATUSES, loginUserId);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED);
        }
        projectReportCurrentItemMapper.deleteByReportId(id);
        projectReportNextItemMapper.deleteByReportId(id);
        projectReportApprovalRecordMapper.deleteByProjectReportId(id);
        workReportStatusLogMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_PROJECT, id);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, WorkReportConstants.ACTION_DELETE,
                current.getStatusCode(), null, null, buildProjectRemark(current));
    }

    public List<WorkReportStatusLogRespVO> getProjectStatusLogs(Long id) {
        validateReadableProjectReport(id);
        return BeanUtils.toBean(workReportStatusLogMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_PROJECT, id),
                WorkReportStatusLogRespVO.class);
    }

    public List<WorkReportApprovalRecordRespVO> getProjectApprovalRecords(Long id) {
        validateReadableProjectReport(id);
        return BeanUtils.toBean(projectReportApprovalRecordMapper.selectListByProjectReportId(id),
                WorkReportApprovalRecordRespVO.class);
    }

    public List<ProjectReportExportVO> getProjectExportList(ProjectReportPageReqVO reqVO) {
        reqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        return BeanUtils.toBean(getProjectReportPage(reqVO).getList(), ProjectReportExportVO.class);
    }

    private void processWeeklyApproval(Long id, String actionCode, String reason) {
        WeeklyReportDO current = validateWeeklyReportExists(id);
        Long approverUserId = SecurityFrameworkUtils.getLoginUserId();
        validateApprover(approverUserId, current.getSupervisorUserId());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, reason);

        LocalDateTime approvalTime = LocalDateTime.now();
        int updateCount = weeklyReportMapper.updateApprovalFieldsByIdAndStatus(id, current.getStatusCode(),
                transition.getToStatusCode(), approvalTime, reason, reason, String.valueOf(approverUserId));
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        WeeklyReportDO latest = weeklyReportMapper.selectById(id);
        WorkReportStatusLogDO statusLog = writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_WEEKLY, actionCode,
                current.getStatusCode(), transition.getToStatusCode(), reason, buildWeeklyRemark(latest));
        writeWeeklyApprovalRecord(latest, statusLog, reason);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_WEEKLY, id, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), reason, buildWeeklyRemark(latest));
    }

    private void processMonthlyApproval(Long id, String actionCode, MonthlyReportApproveReqVO reqVO) {
        MonthlyReportDO current = validateMonthlyReportExists(id);
        Long approverUserId = SecurityFrameworkUtils.getLoginUserId();
        validateApprover(approverUserId, current.getSupervisorUserId());
        String reason = normalizeReason(reqVO);
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, reason);

        LocalDateTime approvalTime = LocalDateTime.now();
        int updateCount = monthlyReportMapper.updateApprovalFieldsByIdAndStatus(id, current.getStatusCode(),
                transition.getToStatusCode(), approvalTime, reason, reason, String.valueOf(approverUserId));
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        MonthlyReportDO latest = monthlyReportMapper.selectById(id);
        WorkReportStatusLogDO statusLog = writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_MONTHLY, actionCode,
                current.getStatusCode(), transition.getToStatusCode(), reason, buildMonthlyRemark(latest));
        writeMonthlyApprovalRecord(latest, statusLog, actionCode, reqVO);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_MONTHLY, id, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), reason, buildMonthlyRemark(latest));
    }

    private void processProjectApproval(Long id, String actionCode, String reason) {
        ProjectReportDO current = validateProjectReportExists(id);
        Long approverUserId = SecurityFrameworkUtils.getLoginUserId();
        validateApprover(approverUserId, current.getSupervisorUserId());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionCode, reason);

        LocalDateTime approvalTime = LocalDateTime.now();
        int updateCount = projectReportMapper.updateApprovalFieldsByIdAndStatus(id, current.getStatusCode(),
                transition.getToStatusCode(), approvalTime, reason, reason, String.valueOf(approverUserId));
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_CONCURRENT_MODIFIED);
        }

        ProjectReportDO latest = projectReportMapper.selectById(id);
        WorkReportStatusLogDO statusLog = writeStatusLog(latest, WorkReportConstants.REPORT_TYPE_PROJECT, actionCode,
                current.getStatusCode(), transition.getToStatusCode(), reason, buildProjectRemark(latest));
        writeProjectApprovalRecord(latest, statusLog, reason);
        writeAuditLog(WorkReportConstants.BIZ_TYPE_PROJECT, id, actionCode, current.getStatusCode(),
                transition.getToStatusCode(), reason, buildProjectRemark(latest));
    }

    private WeeklyReportDO validateWeeklyReportExists(Long id) {
        WeeklyReportDO report = weeklyReportMapper.selectById(id);
        if (report == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_NOT_EXISTS);
        }
        return report;
    }

    private MonthlyReportDO validateMonthlyReportExists(Long id) {
        MonthlyReportDO report = monthlyReportMapper.selectById(id);
        if (report == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_NOT_EXISTS);
        }
        return report;
    }

    private ProjectReportDO validateProjectReportExists(Long id) {
        ProjectReportDO report = projectReportMapper.selectById(id);
        if (report == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_NOT_EXISTS);
        }
        return report;
    }

    private WeeklyReportDO validateReadableWeeklyReport(Long id) {
        WeeklyReportDO report = validateWeeklyReportExists(id);
        validateReadable(report.getReporterId(), report.getSupervisorUserId());
        return report;
    }

    private MonthlyReportDO validateReadableMonthlyReport(Long id) {
        MonthlyReportDO report = validateMonthlyReportExists(id);
        validateReadable(report.getReporterId(), report.getSupervisorUserId());
        return report;
    }

    private MonthlyReportSignOffDO validateMonthlySignOffExists(Long monthlyReportId) {
        MonthlyReportSignOffDO signOff = monthlyReportSignOffMapper.selectByMonthlyReportId(monthlyReportId);
        if (signOff == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_MONTHLY_SIGN_OFF_NOT_EXISTS);
        }
        return signOff;
    }

    private ProjectReportDO validateReadableProjectReport(Long id) {
        ProjectReportDO report = validateProjectReportExists(id);
        validateReadable(report.getProjectOwnerId(), report.getSupervisorUserId());
        return report;
    }

    private void validateReadable(Long reporterId, Long supervisorUserId) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(loginUserId, reporterId)
                && !Objects.equals(loginUserId, supervisorUserId)
                && !teamDashboardAccessService.canReadSubordinateUser(
                        reporterId, WorkReportConstants.PERMISSION_TEAM_DASHBOARD)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_READ_FORBIDDEN);
        }
    }

    private void validateReporter(Long loginUserId, Long reporterId) {
        if (!Objects.equals(loginUserId, reporterId)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_REPORTER_ONLY);
        }
    }

    private void validateApprover(Long loginUserId, Long approverId) {
        if (!Objects.equals(loginUserId, approverId)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_APPROVER_ONLY);
        }
    }

    private void validateDeleteAllowed(String statusCode) {
        if (WorkReportConstants.STATUS_APPROVED.equals(statusCode)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_DELETE_NOT_ALLOWED);
        }
    }

    private void validateAllowEdit(String statusCode) {
        ObjectStatusModelDO statusModel = getStatusModel(statusCode);
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_NOT_ALLOW_EDIT);
        }
    }

    private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(WorkReportConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
        if (transition == null) {
            // TD-012：用户可见文案不外泄内部 code，翻成状态机中文动作名（见 docs/architecture/用户可见错误文案规范.html）
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.actionName(WorkReportConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_ACTION_REASON_REQUIRED,
                    statusActionTextResolver.actionName(WorkReportConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        getStatusModel(transition.getToStatusCode());
        return transition;
    }

    private ObjectStatusModelDO getStatusModel(String statusCode) {
        ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                WorkReportConstants.STATUS_OBJECT_TYPE, statusCode);
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return statusModel;
    }

    private List<String> getEnabledStatusCodes() {
        return objectStatusModelMapper.selectListByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE).stream()
                .map(ObjectStatusModelDO::getStatusCode)
                .filter(StringUtils::hasText)
                .collect(Collectors.toList());
    }

    private List<Long> appendLoginUser(List<Long> userIds) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (loginUserId == null) {
            return userIds;
        }
        LinkedHashSet<Long> merged = new LinkedHashSet<>(userIds);
        merged.add(loginUserId);
        return new ArrayList<>(merged);
    }

    private void publishPersonalReportPendingApprovalNotice(Long supervisorUserId, String templateCode,
            String reportTypeName, String reporterName, String periodLabel) {
        if (supervisorUserId == null) {
            return;
        }
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("reportTypeName", reportTypeName);
        params.put("reporterName", defaultText(reporterName));
        params.put("periodLabel", defaultText(periodLabel));
        applicationEventPublisher.publishEvent(NotifySendEvent.of(
                List.of(supervisorUserId), templateCode, params, NotifyMessageLevelConstants.REMIND));
    }

    private void publishProjectReportPendingApprovalNotice(ProjectReportDO report) {
        if (report == null || report.getSupervisorUserId() == null) {
            return;
        }
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("reporterName", defaultText(report.getProjectOwnerName()));
        params.put("projectName", defaultText(report.getProjectName()));
        params.put("periodLabel", defaultText(report.getPeriodLabel()));
        applicationEventPublisher.publishEvent(NotifySendEvent.of(
                List.of(report.getSupervisorUserId()),
                NotifyTemplateCodeConstants.WORK_REPORT_PROJECT_HALF_MONTH_PENDING_APPROVAL_NOTIFY,
                params, NotifyMessageLevelConstants.REMIND));
    }

    private String formatWeeklyPeriodLabelForNotify(WeeklyReportDO report) {
        if (report == null || report.getPeriodStartDate() == null || report.getPeriodEndDate() == null) {
            return defaultText(report == null ? null : report.getPeriodLabel());
        }
        LocalDate startDate = report.getPeriodStartDate();
        LocalDate endDate = report.getPeriodEndDate();
        int weekBasedYear = startDate.get(IsoFields.WEEK_BASED_YEAR);
        int weekOfYear = startDate.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
        return weekBasedYear + "年" + weekOfYear + "周（" + formatMonthDayRange(startDate, endDate) + "）";
    }

    private String formatMonthDayRange(LocalDate startDate, LocalDate endDate) {
        return startDate.getMonthValue() + "月" + startDate.getDayOfMonth()
                + "日-" + endDate.getMonthValue() + "月" + endDate.getDayOfMonth() + "日";
    }

    private ObjectStatusModelDO getInitialStatusModel() {
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectInitialByObjectTypeEnabled(WorkReportConstants.STATUS_OBJECT_TYPE);
        if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
            throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return statusModel;
    }

    private String getInitialStatusCode() {
        return getInitialStatusModel().getStatusCode();
    }

    private String resolveSubmitAction(String statusCode) {
        if (WorkReportConstants.STATUS_DRAFT.equals(statusCode)) {
            return WorkReportConstants.ACTION_SUBMIT;
        }
        if (WorkReportConstants.STATUS_REJECTED.equals(statusCode)) {
            return WorkReportConstants.ACTION_RESUBMIT;
        }
        throw exception(ErrorCodeConstants.WORK_REPORT_STATUS_ACTION_NOT_ALLOWED,
                statusActionTextResolver.actionName(WorkReportConstants.STATUS_OBJECT_TYPE, WorkReportConstants.ACTION_SUBMIT));
    }

    private void validateWeeklyDuplicate(Long reporterId, String periodKey, Long excludeId) {
        WeeklyReportDO exist = weeklyReportMapper.selectByReporterIdAndPeriodKey(reporterId, normalizeRequiredText(periodKey, "周期编码不能为空"));
        if (exist != null && !Objects.equals(exist.getId(), excludeId)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE);
        }
    }

    private void validateMonthlyDuplicate(Long reporterId, String periodKey, Long excludeId) {
        MonthlyReportDO exist = monthlyReportMapper.selectByReporterIdAndPeriodKey(reporterId, normalizeRequiredText(periodKey, "周期编码不能为空"));
        if (exist != null && !Objects.equals(exist.getId(), excludeId)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE);
        }
    }

    private void validateProjectReportDuplicate(Long projectId, String periodKey, Long reporterId, Long excludeId) {
        ProjectReportDO exist = projectReportMapper.selectByProjectIdAndPeriodKeyAndProjectOwnerId(projectId,
                normalizeRequiredText(periodKey, "周期编码不能为空"), reporterId);
        if (exist != null && !Objects.equals(exist.getId(), excludeId)) {
            throw exception(ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE);
        }
    }

    private void validatePeriod(LocalDate startDate, LocalDate endDate) {
        if (startDate == null || endDate == null) {
            throw invalidParamException("周期开始日期和结束日期不能为空");
        }
        if (endDate.isBefore(startDate)) {
            throw invalidParamException("周期结束日期不能早于开始日期");
        }
    }

    private void validateProjectFlag(Integer flag) {
        if (!Objects.equals(flag, 1) && !Objects.equals(flag, 2)) {
            throw invalidParamException("上半月/下半月标记只能为 1 或 2");
        }
    }

    private CurrentUserProfile resolveProfile(Long overrideReporterId, boolean requireManager) {
        return overrideReporterId != null
                ? loadUserProfile(overrideReporterId, null, requireManager)
                : loadCurrentUserProfile(requireManager);
    }

    private CurrentUserProfile loadCurrentUserProfile(boolean requireManager) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        return loadUserProfile(userId, SecurityFrameworkUtils.getLoginUserNickname(), requireManager);
    }

    private CurrentUserProfile loadUserProfile(Long userId, String fallbackUserName, boolean requireManager) {
        AdminUserRespDTO user = loadUser(userId);
        String userName = StringUtils.hasText(user.getNickname())
                ? user.getNickname().trim()
                : defaultText(fallbackUserName);

        String deptName = null;
        if (user.getDeptId() != null) {
            CommonResult<DeptRespDTO> deptResult = deptApi.getDept(user.getDeptId());
            DeptRespDTO dept = deptResult == null ? null : deptResult.getCheckedData();
            deptName = dept == null ? null : dept.getName();
        }

        String postName = null;
        if (user.getPositionId() != null) {
            Map<Long, PostRespDTO> postMap = postApi.getPostMap(Collections.singleton(user.getPositionId()));
            PostRespDTO post = postMap.get(user.getPositionId());
            postName = post == null ? null : post.getName();
        }

        Long directManagerId = null;
        String directManagerName = null;
        CommonResult<AdminUserRespDTO> directManagerResult = userManagementRelationApi.getDirectManager(userId);
        AdminUserRespDTO directManager = directManagerResult == null ? null : directManagerResult.getCheckedData();
        if (directManager != null) {
            directManagerId = directManager.getId();
            directManagerName = defaultText(directManager.getNickname());
        }
        if (requireManager && directManagerId == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_DIRECT_MANAGER_NOT_EXISTS);
        }
        return new CurrentUserProfile(userId, defaultText(userName), deptName, postName, directManagerId, directManagerName);
    }

    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 == null ? null : result.getCheckedData();
        if (user == null) {
            throw invalidParamException("用户不存在：{}", userId);
        }
        return user;
    }

    public String resolveProjectTechnicalOwnerName(Long projectId) {
        if (projectId == null) {
            return null;
        }
        ObjectRoleRespDTO technicalOwnerRole = objectPermissionApi.getObjectRoleByCode(
                ProjectObjectConstants.TECHNICAL_OWNER_ROLE_CODE,
                com.njcn.rdms.module.project.constant.ObjectRoleConstants.ROLE_SCOPE_OBJECT,
                ProjectObjectConstants.OBJECT_TYPE).getCheckedData();
        if (technicalOwnerRole == null || technicalOwnerRole.getId() == null) {
            throw exception(ErrorCodeConstants.PROJECT_INTERNAL_ROLE_NOT_CONFIGURED,
                    ProjectObjectConstants.TECHNICAL_OWNER_ROLE_CODE);
        }
        for (UserObjectRoleDO member : userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId)) {
            if (member == null || !Objects.equals(member.getStatus(), 0)
                    || !Objects.equals(member.getRoleId(), technicalOwnerRole.getId())
                    || member.getUserId() == null) {
                continue;
            }
            return defaultText(loadUser(member.getUserId()).getNickname());
        }
        return null;
    }

    private ProjectDO validateProjectExists(Long projectId) {
        ProjectDO project = projectMapper.selectById(projectId);
        if (project == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_PROJECT_NOT_EXISTS);
        }
        return project;
    }

    private List<WorkReportMemberSnapshotItem> buildProjectMemberSnapshot(Long projectId) {
        List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId);
        Set<Long> userIds = new LinkedHashSet<>();
        for (UserObjectRoleDO member : members) {
            if (member == null || !Objects.equals(member.getStatus(), 0) || member.getUserId() == null) {
                continue;
            }
            userIds.add(member.getUserId());
        }
        if (userIds.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        List<WorkReportMemberSnapshotItem> snapshot = new ArrayList<>(userIds.size());
        for (Long userId : userIds) {
            WorkReportMemberSnapshotItem item = new WorkReportMemberSnapshotItem();
            item.setUserId(userId);
            AdminUserRespDTO user = userMap.get(userId);
            item.setUserName(user == null ? "" : defaultText(user.getNickname()));
            snapshot.add(item);
        }
        return snapshot;
    }

    private void applyWeeklySaveFields(WeeklyReportDO target, WeeklyReportSaveReqVO reqVO, CurrentUserProfile profile) {
        List<WeeklyReportTravelSegmentReqVO> travelSegments = Boolean.TRUE.equals(reqVO.getIsBusinessTrip())
                ? defaultList(reqVO.getTravelSegments()) : Collections.emptyList();
        validateTravelSegments(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate(), travelSegments);
        target.setReporterId(profile.userId());
        target.setReporterName(profile.userName());
        target.setReporterDeptName(profile.deptName());
        target.setReporterPostName(profile.postName());
        target.setSupervisorUserId(profile.directManagerId());
        target.setSupervisorName(profile.directManagerName());
        target.setPeriodKey(normalizeRequiredText(reqVO.getPeriodKey(), "周期编码不能为空"));
        target.setPeriodLabel(normalizeRequiredText(reqVO.getPeriodLabel(), "周期名称不能为空"));
        target.setPeriodStartDate(reqVO.getPeriodStartDate());
        target.setPeriodEndDate(reqVO.getPeriodEndDate());
        target.setIsBusinessTrip(Boolean.TRUE.equals(reqVO.getIsBusinessTrip()));
        target.setTotalTravelDays(sumTravelDays(travelSegments));
        target.setTotalWorkHours(sumReviewWorkHours(reqVO.getReviewItems()));
    }

    private void applyMonthlySaveFields(MonthlyReportDO target, MonthlyReportSaveReqVO reqVO, CurrentUserProfile profile) {
        target.setReporterId(profile.userId());
        target.setReporterName(profile.userName());
        target.setReporterDeptName(profile.deptName());
        target.setReporterPostName(profile.postName());
        target.setSupervisorUserId(profile.directManagerId());
        target.setSupervisorName(profile.directManagerName());
        target.setPeriodKey(normalizeRequiredText(reqVO.getPeriodKey(), "周期编码不能为空"));
        target.setPeriodLabel(normalizeRequiredText(reqVO.getPeriodLabel(), "周期名称不能为空"));
        target.setPeriodStartDate(reqVO.getPeriodStartDate());
        target.setPeriodEndDate(reqVO.getPeriodEndDate());
        target.setTotalWorkHours(sumReviewWorkHours(reqVO.getReviewItems()));
    }

    private void applyProjectSaveFields(ProjectReportDO target, ProjectReportSaveReqVO reqVO,
                                        CurrentUserProfile profile, ProjectDO project) {
        target.setProjectId(project.getId());
        target.setProjectName(project.getProjectName());
        target.setProjectOwnerId(profile.userId());
        target.setProjectOwnerName(profile.userName());
        target.setTechnicalOwnerName(resolveProjectTechnicalOwnerName(project.getId()));
        target.setProjectMemberSnapshot(buildProjectMemberSnapshot(project.getId()));
        target.setSupervisorUserId(profile.directManagerId());
        target.setSupervisorName(profile.directManagerName());
        target.setPeriodKey(normalizeRequiredText(reqVO.getPeriodKey(), "周期编码不能为空"));
        target.setPeriodLabel(normalizeRequiredText(reqVO.getPeriodLabel(), "周期名称不能为空"));
        target.setPeriodStartDate(reqVO.getPeriodStartDate());
        target.setPeriodEndDate(reqVO.getPeriodEndDate());
        target.setFlag(reqVO.getFlag());
        target.setProjectStatusDesc(normalizeNullableText(reqVO.getProjectStatusDesc()));
        target.setProjectProgressPlan(normalizeNullableText(reqVO.getProjectProgressPlan()));
        target.setProjectKeyPoints(normalizeNullableText(reqVO.getProjectKeyPoints()));
        target.setProjectProblems(normalizeNullableText(reqVO.getProjectProblems()));
        target.setTotalWorkHours(sumProjectCurrentWorkHours(reqVO.getCurrentItems()));
    }

    private void replaceWeeklyChildren(Long reportId, WeeklyReportSaveReqVO reqVO) {
        personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId);
        personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId);
        weeklyReportTravelSegmentMapper.deleteByWeeklyReportId(reportId);
        insertReviewItems(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId, reqVO.getReviewItems());
        insertPlanItems(WorkReportConstants.REPORT_TYPE_WEEKLY, reportId, reqVO.getPlanItems());
        if (Boolean.TRUE.equals(reqVO.getIsBusinessTrip())) {
            insertTravelSegments(reportId, reqVO.getTravelSegments());
        }
    }

    private void replaceMonthlyChildren(Long reportId, MonthlyReportSaveReqVO reqVO) {
        personalReportReviewItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId);
        personalReportPlanItemMapper.deleteByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId);
        insertReviewItems(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId, reqVO.getReviewItems());
        insertPlanItems(WorkReportConstants.REPORT_TYPE_MONTHLY, reportId, reqVO.getPlanItems());
    }

    private void replaceProjectChildren(Long reportId, ProjectReportSaveReqVO reqVO) {
        projectReportCurrentItemMapper.deleteByReportId(reportId);
        projectReportNextItemMapper.deleteByReportId(reportId);
        insertProjectCurrentItems(reportId, reqVO.getCurrentItems());
        insertProjectNextItems(reportId, reqVO.getNextItems());
    }

    private List<PersonalReportReviewItemRespVO> mergeReviewItems(List<PersonalReportReviewItemReqVO> currentItems,
                                                                  List<PersonalReportReviewItemRespVO> latestItems) {
        List<PersonalReportReviewItemRespVO> merged = BeanUtils.toBean(defaultList(currentItems),
                PersonalReportReviewItemRespVO.class);
        Set<String> existingKeys = merged.stream()
                .map(item -> normalizeMergeText(item.getItemTitle()))
                .filter(StringUtils::hasText)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        for (PersonalReportReviewItemRespVO item : defaultList(latestItems)) {
            String key = normalizeMergeText(item.getItemTitle());
            if (StringUtils.hasText(key) && existingKeys.contains(key)) {
                continue;
            }
            merged.add(BeanUtils.toBean(item, PersonalReportReviewItemRespVO.class));
            if (StringUtils.hasText(key)) {
                existingKeys.add(key);
            }
        }
        for (int i = 0; i < merged.size(); i++) {
            merged.get(i).setItemNumber(i + 1);
        }
        return merged;
    }

    private List<PersonalReportReviewItemRespVO> mergeWeeklyReviewItems(List<PersonalReportReviewItemReqVO> currentItems,
                                                                        List<PersonalReportReviewItemRespVO> latestItems) {
        List<PersonalReportReviewItemRespVO> merged = BeanUtils.toBean(defaultList(currentItems),
                PersonalReportReviewItemRespVO.class);
        Map<String, PersonalReportReviewItemRespVO> latestItemMap = defaultList(latestItems).stream()
                .filter(Objects::nonNull)
                .filter(item -> StringUtils.hasText(item.getItemTitle()))
                .collect(Collectors.toMap(item -> normalizeMergeText(item.getItemTitle()),
                        item -> item, (left, right) -> right, LinkedHashMap::new));
        Set<String> existingKeys = new LinkedHashSet<>();
        for (PersonalReportReviewItemRespVO item : merged) {
            String key = normalizeMergeText(item.getItemTitle());
            if (!StringUtils.hasText(key)) {
                continue;
            }
            PersonalReportReviewItemRespVO latestItem = latestItemMap.get(key);
            if (latestItem != null) {
                item.setWorkHours(latestItem.getWorkHours());
                item.setContentText(latestItem.getContentText());
                item.setContentJson(latestItem.getContentJson());
            }
            existingKeys.add(key);
        }
        for (PersonalReportReviewItemRespVO item : defaultList(latestItems)) {
            String key = normalizeMergeText(item.getItemTitle());
            if (StringUtils.hasText(key) && existingKeys.contains(key)) {
                continue;
            }
            merged.add(BeanUtils.toBean(item, PersonalReportReviewItemRespVO.class));
            if (StringUtils.hasText(key)) {
                existingKeys.add(key);
            }
        }
        for (int i = 0; i < merged.size(); i++) {
            merged.get(i).setItemNumber(i + 1);
        }
        return merged;
    }

    private List<PersonalReportPlanItemRespVO> mergePlanItems(List<PersonalReportPlanItemReqVO> currentItems,
                                                              List<PersonalReportPlanItemRespVO> latestItems) {
        List<PersonalReportPlanItemRespVO> merged = BeanUtils.toBean(defaultList(currentItems),
                PersonalReportPlanItemRespVO.class);
        Set<String> existingKeys = merged.stream()
                .map(item -> normalizeMergeText(item.getItemTitle()))
                .filter(StringUtils::hasText)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        for (PersonalReportPlanItemRespVO item : defaultList(latestItems)) {
            String key = normalizeMergeText(item.getItemTitle());
            if (StringUtils.hasText(key) && existingKeys.contains(key)) {
                continue;
            }
            merged.add(BeanUtils.toBean(item, PersonalReportPlanItemRespVO.class));
            if (StringUtils.hasText(key)) {
                existingKeys.add(key);
            }
        }
        for (int i = 0; i < merged.size(); i++) {
            merged.get(i).setItemNumber(i + 1);
        }
        return merged;
    }

    private List<ProjectReportItemRespVO> mergeProjectItems(List<ProjectReportItemReqVO> currentItems,
                                                            List<ProjectReportItemRespVO> latestItems) {
        List<ProjectReportItemRespVO> merged = BeanUtils.toBean(defaultList(currentItems), ProjectReportItemRespVO.class);
        Set<String> existingKeys = merged.stream()
                .map(item -> buildProjectItemMergeKey(item.getItemTitle(), item.getPriorityCode()))
                .filter(StringUtils::hasText)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        for (ProjectReportItemRespVO item : defaultList(latestItems)) {
            String key = buildProjectItemMergeKey(item.getItemTitle(), item.getPriorityCode());
            if (StringUtils.hasText(key) && existingKeys.contains(key)) {
                continue;
            }
            merged.add(BeanUtils.toBean(item, ProjectReportItemRespVO.class));
            if (StringUtils.hasText(key)) {
                existingKeys.add(key);
            }
        }
        return merged;
    }

    private void insertReviewItems(String reportType, Long reportId, List<PersonalReportReviewItemReqVO> items) {
        List<PersonalReportReviewItemReqVO> source = defaultList(items);
        for (int i = 0; i < source.size(); i++) {
            PersonalReportReviewItemReqVO item = source.get(i);
            if (!StringUtils.hasText(item.getItemTitle())) {
                continue;
            }
            PersonalReportReviewItemDO data = new PersonalReportReviewItemDO();
            data.setReportType(reportType);
            data.setReportId(reportId);
            data.setItemNumber(item.getItemNumber() != null ? item.getItemNumber() : i + 1);
            data.setItemTitle(item.getItemTitle().trim());
            data.setWorkHours(item.getWorkHours());
            data.setContentText(normalizeNullableText(item.getContentText()));
            data.setContentJson(item.getContentJson());
            data.setReflectionText(normalizeNullableText(item.getReflectionText()));
            personalReportReviewItemMapper.insert(data);
        }
    }

    private void insertPlanItems(String reportType, Long reportId, List<PersonalReportPlanItemReqVO> items) {
        List<PersonalReportPlanItemReqVO> source = defaultList(items);
        for (int i = 0; i < source.size(); i++) {
            PersonalReportPlanItemReqVO item = source.get(i);
            if (!StringUtils.hasText(item.getItemTitle())) {
                continue;
            }
            PersonalReportPlanItemDO data = new PersonalReportPlanItemDO();
            data.setReportType(reportType);
            data.setReportId(reportId);
            data.setItemNumber(item.getItemNumber() != null ? item.getItemNumber() : i + 1);
            data.setItemTitle(item.getItemTitle().trim());
            data.setTargetText(normalizeNullableText(item.getTargetText()));
            data.setTargetJson(item.getTargetJson());
            data.setSupportNeed(normalizeNullableText(item.getSupportNeed()));
            personalReportPlanItemMapper.insert(data);
        }
    }

    private void insertTravelSegments(Long reportId, List<WeeklyReportTravelSegmentReqVO> items) {
        List<WeeklyReportTravelSegmentReqVO> source = defaultList(items);
        for (int i = 0; i < source.size(); i++) {
            WeeklyReportTravelSegmentReqVO item = source.get(i);
            if (item.getStartDate() == null || item.getEndDate() == null) {
                continue;
            }
            WeeklyReportTravelSegmentDO data = new WeeklyReportTravelSegmentDO();
            data.setWeeklyReportId(reportId);
            data.setSort(item.getSort() != null ? item.getSort() : i + 1);
            data.setStartDate(item.getStartDate());
            data.setEndDate(item.getEndDate());
            data.setTravelDays(item.getTravelDays());
            data.setLocation(normalizeNullableText(item.getLocation()));
            weeklyReportTravelSegmentMapper.insert(data);
        }
    }

    private void insertProjectCurrentItems(Long reportId, List<ProjectReportItemReqVO> items) {
        List<ProjectReportItemReqVO> source = defaultList(items);
        for (ProjectReportItemReqVO item : source) {
            if (!StringUtils.hasText(item.getItemTitle())) {
                continue;
            }
            ProjectReportCurrentItemDO data = new ProjectReportCurrentItemDO();
            data.setReportId(reportId);
            data.setItemTitle(item.getItemTitle().trim());
            data.setWorkHours(item.getWorkHours());
            data.setPriorityCode(normalizeNullableText(item.getPriorityCode()));
            data.setProgressRate(item.getProgressRate());
            projectReportCurrentItemMapper.insert(data);
        }
    }

    private void insertProjectNextItems(Long reportId, List<ProjectReportItemReqVO> items) {
        List<ProjectReportItemReqVO> source = defaultList(items);
        for (ProjectReportItemReqVO item : source) {
            if (!StringUtils.hasText(item.getItemTitle())) {
                continue;
            }
            ProjectReportNextItemDO data = new ProjectReportNextItemDO();
            data.setReportId(reportId);
            data.setItemTitle(item.getItemTitle().trim());
            data.setPriorityCode(normalizeNullableText(item.getPriorityCode()));
            data.setProgressRate(item.getProgressRate());
            projectReportNextItemMapper.insert(data);
        }
    }

    private WorkReportStatusLogDO writeStatusLog(Object report, String reportType, String actionType,
                                                 String fromStatus, String toStatus, String reason, String remark) {
        WorkReportStatusLogDO log = new WorkReportStatusLogDO();
        log.setReportType(reportType);
        log.setReportId(extractReportId(report));
        log.setActionType(actionType);
        log.setFromStatus(fromStatus);
        log.setToStatus(toStatus);
        log.setReason(reason);
        log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        log.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        log.setPeriodLabelSnapshot(extractPeriodLabel(report));
        log.setRemark(remark);
        workReportStatusLogMapper.insert(log);
        return log;
    }

    private void writeAuditLog(String bizType, Long bizId, String actionType, String fromStatus,
                               String toStatus, String reason, String remark) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(bizType);
        auditLog.setBizId(bizId);
        auditLog.setActionType(actionType);
        auditLog.setFromStatus(fromStatus);
        auditLog.setToStatus(toStatus);
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        auditLog.setRemark(remark);
        bizAuditLogMapper.insert(auditLog);
    }

    private void writeWeeklyApprovalRecord(WeeklyReportDO report, WorkReportStatusLogDO statusLog, String reason) {
        WeeklyReportApprovalRecordDO record = new WeeklyReportApprovalRecordDO();
        record.setWeeklyReportId(report.getId());
        record.setStatusLogId(statusLog.getId());
        record.setApprovalRound(weeklyReportApprovalRecordMapper.countByWeeklyReportId(report.getId()) + 1);
        record.setConclusion(statusLog.getToStatus());
        record.setOpinion(reason);
        record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId());
        record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        weeklyReportApprovalRecordMapper.insert(record);
    }

    private void writeMonthlyApprovalRecord(MonthlyReportDO report, WorkReportStatusLogDO statusLog,
                                            String actionCode, MonthlyReportApproveReqVO reqVO) {
        MonthlyReportApprovalRecordDO record = new MonthlyReportApprovalRecordDO();
        record.setMonthlyReportId(report.getId());
        record.setStatusLogId(statusLog.getId());
        record.setApprovalRound(monthlyReportApprovalRecordMapper.countByMonthlyReportId(report.getId()) + 1);
        record.setConclusion(statusLog.getToStatus());
        record.setOpinion(normalizeReason(reqVO));
        if (WorkReportConstants.ACTION_APPROVE.equals(actionCode)) {
            UserSignatureRespDTO userSignature = getUserSignature(SecurityFrameworkUtils.getLoginUserId());
            record.setMeetingDate(reqVO.getMeetingDate());
            record.setStrengthDesc(normalizeNullableText(reqVO.getStrengthDesc()));
            record.setStrengthExample(normalizeNullableText(reqVO.getStrengthExample()));
            record.setWeaknessDesc(normalizeNullableText(reqVO.getWeaknessDesc()));
            record.setWeaknessExample(normalizeNullableText(reqVO.getWeaknessExample()));
            record.setImprovementSuggestion(normalizeNullableText(reqVO.getImprovementSuggestion()));
            record.setPerformanceResult(normalizeNullableText(reqVO.getPerformanceResult()));
            record.setEmployeeSignName(normalizeNullableText(reqVO.getEmployeeSignName()));
            record.setEmployeeSignatureFileId(null);
            record.setEmployeeSignedDate(reqVO.getEmployeeSignedDate());
            record.setSupervisorSignName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
            record.setSupervisorSignatureFileId(userSignature == null ? null : userSignature.getFileId());
            record.setSupervisorSignedDate(LocalDate.now());
        }
        record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId());
        record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        monthlyReportApprovalRecordMapper.insert(record);
    }

    private void writeMonthlySignOffRecord(Long signOffId, Long monthlyReportId, Long approvalRecordId, Long batchNo,
                                           String actionType, String fromStatus, String toStatus) {
        MonthlyReportSignOffRecordDO record = new MonthlyReportSignOffRecordDO();
        record.setSignOffId(signOffId);
        record.setMonthlyReportId(monthlyReportId);
        record.setApprovalRecordId(approvalRecordId);
        record.setBatchNo(batchNo);
        record.setActionType(actionType);
        record.setFromSignOffStatus(fromStatus);
        record.setToSignOffStatus(toStatus);
        record.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        record.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        monthlyReportSignOffRecordMapper.insert(record);
    }

    private void writeProjectApprovalRecord(ProjectReportDO report, WorkReportStatusLogDO statusLog, String reason) {
        ProjectReportApprovalRecordDO record = new ProjectReportApprovalRecordDO();
        record.setProjectReportId(report.getId());
        record.setStatusLogId(statusLog.getId());
        record.setApprovalRound(projectReportApprovalRecordMapper.countByProjectReportId(report.getId()) + 1);
        record.setConclusion(statusLog.getToStatus());
        record.setOpinion(reason);
        record.setAuditorUserId(SecurityFrameworkUtils.getLoginUserId());
        record.setAuditorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        projectReportApprovalRecordMapper.insert(record);
    }

    private Long extractReportId(Object report) {
        if (report instanceof WeeklyReportDO weeklyReport) {
            return weeklyReport.getId();
        }
        if (report instanceof MonthlyReportDO monthlyReport) {
            return monthlyReport.getId();
        }
        if (report instanceof ProjectReportDO projectReport) {
            return projectReport.getId();
        }
        return null;
    }

    private String extractPeriodLabel(Object report) {
        if (report instanceof WeeklyReportDO weeklyReport) {
            return weeklyReport.getPeriodLabel();
        }
        if (report instanceof MonthlyReportDO monthlyReport) {
            return monthlyReport.getPeriodLabel();
        }
        if (report instanceof ProjectReportDO projectReport) {
            return projectReport.getPeriodLabel();
        }
        return null;
    }

    private WeeklyReportRespVO toWeeklyRespVO(WeeklyReportDO report, boolean withChildren) {
        WeeklyReportRespVO respVO = BeanUtils.toBean(report, WeeklyReportRespVO.class);
        applyStatusView(respVO, getStatusModel(report.getStatusCode()));
        if (withChildren) {
            respVO.setReviewItems(BeanUtils.toBean(
                    personalReportReviewItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, report.getId()),
                    PersonalReportReviewItemRespVO.class));
            respVO.setPlanItems(BeanUtils.toBean(
                    personalReportPlanItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_WEEKLY, report.getId()),
                    PersonalReportPlanItemRespVO.class));
            respVO.setTravelSegments(BeanUtils.toBean(
                    weeklyReportTravelSegmentMapper.selectListByWeeklyReportId(report.getId()),
                    WeeklyReportTravelSegmentRespVO.class));
        }
        return respVO;
    }

    private MonthlyReportRespVO toMonthlyRespVO(MonthlyReportDO report, boolean withChildren) {
        MonthlyReportRespVO respVO = BeanUtils.toBean(report, MonthlyReportRespVO.class);
        applyStatusView(respVO, getStatusModel(report.getStatusCode()));
        String signOffStatus = StringUtils.hasText(report.getSignOffStatus())
                ? report.getSignOffStatus()
                : SignOffConstants.STATUS_NOT_STARTED;
        respVO.setSignOffStatus(signOffStatus);
        respVO.setSignOffStatusName(employeeSignOffStatusService.statusName(signOffStatus));
        if (withChildren) {
            respVO.setReviewItems(BeanUtils.toBean(
                    personalReportReviewItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId()),
                    PersonalReportReviewItemRespVO.class));
            respVO.setPlanItems(BeanUtils.toBean(
                    personalReportPlanItemMapper.selectListByReport(WorkReportConstants.REPORT_TYPE_MONTHLY, report.getId()),
                    PersonalReportPlanItemRespVO.class));
        }
        return respVO;
    }

    private ProjectReportRespVO toProjectRespVO(ProjectReportDO report, boolean withChildren) {
        ProjectReportRespVO respVO = BeanUtils.toBean(report, ProjectReportRespVO.class);
        applyStatusView(respVO, getStatusModel(report.getStatusCode()));
        respVO.setProjectMemberSnapshot(BeanUtils.toBean(defaultList(report.getProjectMemberSnapshot()),
                WorkReportMemberSnapshotRespVO.class));
        if (withChildren) {
            respVO.setCurrentItems(BeanUtils.toBean(projectReportCurrentItemMapper.selectListByReportId(report.getId()),
                    ProjectReportItemRespVO.class));
            respVO.setNextItems(BeanUtils.toBean(projectReportNextItemMapper.selectListByReportId(report.getId()),
                    ProjectReportItemRespVO.class));
        }
        return respVO;
    }

    private void fillPersonalBase(WeeklyReportRespVO respVO, CurrentUserProfile profile) {
        respVO.setReporterId(profile.userId());
        respVO.setReporterName(profile.userName());
        respVO.setReporterDeptName(profile.deptName());
        respVO.setReporterPostName(profile.postName());
        respVO.setSupervisorUserId(profile.directManagerId());
        respVO.setSupervisorName(profile.directManagerName());
    }

    private void fillPersonalBase(MonthlyReportRespVO respVO, CurrentUserProfile profile) {
        respVO.setReporterId(profile.userId());
        respVO.setReporterName(profile.userName());
        respVO.setReporterDeptName(profile.deptName());
        respVO.setReporterPostName(profile.postName());
        respVO.setSupervisorUserId(profile.directManagerId());
        respVO.setSupervisorName(profile.directManagerName());
    }

    private void applyStatusView(WeeklyReportRespVO respVO, ObjectStatusModelDO statusModel) {
        respVO.setStatusCode(statusModel.getStatusCode());
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
        respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
    }

    private void applyStatusView(MonthlyReportRespVO respVO, ObjectStatusModelDO statusModel) {
        respVO.setStatusCode(statusModel.getStatusCode());
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
        respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
    }

    private void applyStatusView(ProjectReportRespVO respVO, ObjectStatusModelDO statusModel) {
        respVO.setStatusCode(statusModel.getStatusCode());
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setAllowEdit(Boolean.TRUE.equals(statusModel.getAllowEdit()));
        respVO.setTerminal(Boolean.TRUE.equals(statusModel.getTerminalFlag()));
    }

    private WorkReportStatusDictRespVO toStatusDictRespVO(ObjectStatusModelDO statusModel) {
        WorkReportStatusDictRespVO respVO = new WorkReportStatusDictRespVO();
        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 void validateTravelSegments(LocalDate periodStartDate, LocalDate periodEndDate,
                                        List<WeeklyReportTravelSegmentReqVO> travelSegments) {
        for (WeeklyReportTravelSegmentReqVO item : defaultList(travelSegments)) {
            if (item.getStartDate() == null || item.getEndDate() == null) {
                continue;
            }
            if (item.getEndDate().isBefore(item.getStartDate())) {
                throw invalidParamException("出差结束日期不能早于开始日期");
            }
            if (item.getStartDate().isBefore(periodStartDate) || item.getEndDate().isAfter(periodEndDate)) {
                throw invalidParamException("出差分段必须落在报告周期内");
            }
        }
    }

    private BigDecimal sumReviewWorkHours(List<PersonalReportReviewItemReqVO> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (PersonalReportReviewItemReqVO item : defaultList(items)) {
            if (item.getWorkHours() != null) {
                total = total.add(item.getWorkHours());
            }
        }
        return total.compareTo(BigDecimal.ZERO) == 0 ? null : total;
    }

    private BigDecimal sumReviewWorkHoursResp(List<PersonalReportReviewItemRespVO> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (PersonalReportReviewItemRespVO item : defaultList(items)) {
            if (item.getWorkHours() != null) {
                total = total.add(item.getWorkHours());
            }
        }
        return total.compareTo(BigDecimal.ZERO) == 0 ? null : total;
    }

    private BigDecimal sumProjectCurrentWorkHours(List<ProjectReportItemReqVO> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (ProjectReportItemReqVO item : defaultList(items)) {
            if (item.getWorkHours() != null) {
                total = total.add(item.getWorkHours());
            }
        }
        return total.compareTo(BigDecimal.ZERO) == 0 ? null : total;
    }

    private BigDecimal sumProjectCurrentWorkHoursResp(List<ProjectReportItemRespVO> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (ProjectReportItemRespVO item : defaultList(items)) {
            if (item.getWorkHours() != null) {
                total = total.add(item.getWorkHours());
            }
        }
        return total.compareTo(BigDecimal.ZERO) == 0 ? null : total;
    }

    private BigDecimal sumTravelDays(List<WeeklyReportTravelSegmentReqVO> items) {
        BigDecimal total = BigDecimal.ZERO;
        for (WeeklyReportTravelSegmentReqVO item : defaultList(items)) {
            if (item.getTravelDays() != null) {
                total = total.add(item.getTravelDays());
            }
        }
        return total.compareTo(BigDecimal.ZERO) == 0 ? null : total;
    }

    private String buildWeeklyRemark(WeeklyReportDO report) {
        return "周报：" + defaultText(report.getReporterName()) + " / " + defaultText(report.getPeriodLabel());
    }

    private String buildMonthlyRemark(MonthlyReportDO report) {
        return "月报：" + defaultText(report.getReporterName()) + " / " + defaultText(report.getPeriodLabel());
    }

    private String buildProjectRemark(ProjectReportDO report) {
        return "项目半月报：" + defaultText(report.getProjectName()) + " / " + defaultText(report.getPeriodLabel());
    }

    private String normalizeReason(WorkReportStatusActionReqVO reqVO) {
        return reqVO == null ? null : normalizeNullableText(reqVO.getReason());
    }

    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 normalizeMergeText(String value) {
        return StringUtils.hasText(value) ? value.trim() : null;
    }

    private String buildProjectItemMergeKey(String itemTitle, String priorityCode) {
        String normalizedTitle = normalizeMergeText(itemTitle);
        String normalizedPriorityCode = normalizeMergeText(priorityCode);
        if (!StringUtils.hasText(normalizedTitle)) {
            return null;
        }
        return normalizedTitle + "||" + defaultText(normalizedPriorityCode);
    }

    private BigDecimal defaultIfNull(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }

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

    private boolean isIssueAllowed(String signOffStatus) {
        return !StringUtils.hasText(signOffStatus)
                || SignOffConstants.STATUS_NOT_STARTED.equals(signOffStatus);
    }

    private UserSignatureRespDTO getUserSignature(Long userId) {
        if (userId == null) {
            return null;
        }
        CommonResult<UserSignatureRespDTO> result = userSignatureApi.getByUserId(userId);
        return result == null ? null : result.getCheckedData();
    }

    private <T> List<T> defaultList(List<T> list) {
        return list == null ? Collections.emptyList() : list;
    }

    private record CurrentUserProfile(Long userId, String userName, String deptName, String postName,
                                      Long directManagerId, String directManagerName) {
    }
}
