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

import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO;
import com.njcn.rdms.framework.common.pojo.CommonResult;
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.ProjectTaskConstants;
import com.njcn.rdms.module.project.constant.WorkReportConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.WorkReportMemberSnapshotRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.monthly.vo.MonthlyReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportRespVO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.common.WorkReportMemberSnapshotItem;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService;
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.dept.dto.DeptRespDTO;
import com.njcn.rdms.module.system.api.dept.dto.PostRespDTO;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
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.enums.DictTypeConstants;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
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 WorkReportDefaultDraftService {

    private static final String WORK_ITEM_MY_ITEMS = "我的事项";
    private static final String WORKLOG_DAY_SEPARATOR = "[[WR_DAY_SPLIT]]";

    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private PostApi postApi;
    @Resource
    private UserManagementRelationApi userManagementRelationApi;
    @Resource
    private DictDataApi dictDataApi;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private TaskWorklogMapper taskWorklogMapper;
    @Resource
    private ProjectTaskMapper projectTaskMapper;
    @Resource
    private PersonalItemMapper personalItemMapper;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;
    @Resource
    private WorkReportCommonService workReportCommonService;

    public WeeklyReportRespVO previewWeeklyDefaultDraft(WeeklyReportDefaultDraftReqVO reqVO) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        ObjectStatusModelDO initialStatus = getInitialStatusModel();

        WeeklyReportRespVO respVO = new WeeklyReportRespVO();
        fillPersonalBase(respVO, profile);
        respVO.setPeriodKey(reqVO.getPeriodKey());
        respVO.setPeriodLabel(reqVO.getPeriodLabel());
        respVO.setPeriodStartDate(reqVO.getPeriodStartDate());
        respVO.setPeriodEndDate(reqVO.getPeriodEndDate());
        respVO.setIsBusinessTrip(Boolean.FALSE);
        respVO.setTravelSegments(Collections.emptyList());
        respVO.setTotalTravelDays(BigDecimal.ZERO);
        applyStatusView(respVO, initialStatus);

        LocalDateRange nextRange = nextWeeklyRange(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        List<PersonalReportReviewItemRespVO> reviewItems = buildPersonalReviewItems(
                reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate(), profile.userId(), true);
        List<PersonalReportPlanItemRespVO> planItems = buildPersonalPlanItems(
                nextRange.startDate(), nextRange.endDate(), profile.userId(), true);
        respVO.setReviewItems(reviewItems);
        respVO.setPlanItems(planItems);
        respVO.setTotalWorkHours(sumReviewWorkHours(reviewItems));
        return respVO;
    }

    public MonthlyReportRespVO previewMonthlyDefaultDraft(MonthlyReportDefaultDraftReqVO reqVO) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        ObjectStatusModelDO initialStatus = getInitialStatusModel();

        MonthlyReportRespVO respVO = new MonthlyReportRespVO();
        fillPersonalBase(respVO, profile);
        respVO.setPeriodKey(reqVO.getPeriodKey());
        respVO.setPeriodLabel(reqVO.getPeriodLabel());
        respVO.setPeriodStartDate(reqVO.getPeriodStartDate());
        respVO.setPeriodEndDate(reqVO.getPeriodEndDate());
        applyStatusView(respVO, initialStatus);

        LocalDateRange nextRange = nextMonthlyRange(reqVO.getPeriodEndDate());
        List<PersonalReportReviewItemRespVO> reviewItems = buildPersonalReviewItems(
                reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate(), profile.userId(), false);
        List<PersonalReportPlanItemRespVO> planItems = buildPersonalPlanItems(
                nextRange.startDate(), nextRange.endDate(), profile.userId(), false);
        respVO.setReviewItems(reviewItems);
        respVO.setPlanItems(planItems);
        respVO.setTotalWorkHours(sumReviewWorkHours(reviewItems));
        return respVO;
    }

    public ProjectReportRespVO previewProjectDefaultDraft(Long projectId, ProjectReportDefaultDraftReqVO reqVO) {
        validatePeriod(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        validateProjectFlag(reqVO.getFlag());
        CurrentUserProfile profile = loadCurrentUserProfile(true);
        ProjectDO project = validateProjectExists(projectId);
        ObjectStatusModelDO initialStatus = getInitialStatusModel();

        ProjectReportRespVO respVO = new ProjectReportRespVO();
        respVO.setProjectId(project.getId());
        respVO.setProjectName(project.getProjectName());
        respVO.setProjectOwnerId(profile.userId());
        respVO.setProjectOwnerName(profile.userName());
        respVO.setTechnicalOwnerName(workReportCommonService.resolveProjectTechnicalOwnerName(project.getId()));
        respVO.setProjectMemberSnapshot(BeanUtils.toBean(buildProjectMemberSnapshot(projectId),
                WorkReportMemberSnapshotRespVO.class));
        respVO.setSupervisorUserId(profile.directManagerId());
        respVO.setSupervisorName(profile.directManagerName());
        respVO.setPeriodKey(reqVO.getPeriodKey());
        respVO.setPeriodLabel(reqVO.getPeriodLabel());
        respVO.setPeriodStartDate(reqVO.getPeriodStartDate());
        respVO.setPeriodEndDate(reqVO.getPeriodEndDate());
        respVO.setFlag(reqVO.getFlag());
        applyStatusView(respVO, initialStatus);

        LocalDateRange nextRange = nextHalfMonthRange(reqVO.getPeriodEndDate());
        List<ProjectReportItemRespVO> currentItems = buildProjectCurrentItems(
                projectId, reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        List<ProjectReportItemRespVO> nextItems = buildProjectNextItems(
                projectId, nextRange.startDate(), nextRange.endDate());
        respVO.setCurrentItems(currentItems);
        respVO.setNextItems(nextItems);
        respVO.setTotalWorkHours(sumProjectWorkHours(currentItems));
        return respVO;
    }

    private List<PersonalReportReviewItemRespVO> buildPersonalReviewItems(LocalDate periodStartDate,
                                                                          LocalDate periodEndDate,
                                                                          Long userId,
                                                                          boolean weeklyMode) {
        List<TaskWorklogDO> worklogs = taskWorklogMapper.selectListByUserIdAndPeriod(userId, periodStartDate, periodEndDate);
        if (worklogs.isEmpty()) {
            return Collections.emptyList();
        }

        Set<Long> taskIds = worklogs.stream()
                .map(TaskWorklogDO::getTaskId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (taskIds.isEmpty()) {
            return Collections.emptyList();
        }

        Map<Long, ProjectTaskDO> taskMap = projectTaskMapper.selectBatchIds(taskIds).stream()
                .collect(Collectors.toMap(ProjectTaskDO::getId, item -> item));
        Map<Long, PersonalItemDO> itemMap = personalItemMapper.selectBatchIds(taskIds).stream()
                .collect(Collectors.toMap(PersonalItemDO::getId, item -> item));

        Set<Long> projectIds = taskMap.values().stream()
                .map(ProjectTaskDO::getProjectId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Map<Long, ProjectDO> projectMap = projectIds.isEmpty() ? Collections.emptyMap()
                : projectMapper.selectBatchIds(projectIds).stream()
                .collect(Collectors.toMap(ProjectDO::getId, item -> item));
        Map<String, String> taskItemTypeLabelMap = loadTaskItemTypeLabelMap();

        Map<String, ReviewAggregate> aggregateMap = new LinkedHashMap<>();
        for (TaskWorklogDO worklog : worklogs) {
            if (worklog.getTaskId() == null) {
                continue;
            }
            ProjectTaskDO task = taskMap.get(worklog.getTaskId());
            if (task != null) {
                ProjectDO project = projectMap.get(task.getProjectId());
                String workItemTitle = safeText(project == null ? null : project.getProjectName());
                String groupKey = workItemTitle + "||task||" + task.getId();
                ReviewAggregate aggregate = aggregateMap.computeIfAbsent(groupKey, key -> new ReviewAggregate(
                        workItemTitle,
                        safeText(task.getTaskTitle()),
                        safeText(task.getPriority()),
                        resolveTaskItemTypeLabel(task.getType(), taskItemTypeLabelMap)));
                aggregate.merge(worklog);
                continue;
            }
            PersonalItemDO item = itemMap.get(worklog.getTaskId());
            if (item != null) {
                String groupKey = WORK_ITEM_MY_ITEMS + "||item||" + item.getId();
                ReviewAggregate aggregate = aggregateMap.computeIfAbsent(groupKey, key -> new ReviewAggregate(
                        WORK_ITEM_MY_ITEMS,
                        safeText(item.getTaskTitle()),
                        null,
                        resolveTaskItemTypeLabel(item.getType(), taskItemTypeLabelMap)));
                aggregate.merge(worklog);
            }
        }

        List<PersonalReportReviewItemRespVO> result = new ArrayList<>();
        int itemNumber = 1;
        for (ReviewAggregate aggregate : aggregateMap.values()) {
            if (isZeroProgress(aggregate.latestProgressRate())) {
                continue;
            }
            PersonalReportReviewItemRespVO item = new PersonalReportReviewItemRespVO();
            item.setItemNumber(itemNumber++);
            item.setItemTitle(aggregate.workItemTitle());
            item.setWorkHours(nullToZero(aggregate.totalWorkHours()));
            item.setContentText(weeklyMode ? aggregate.buildWeeklyReviewText() : aggregate.buildMonthlyReviewText());
            item.setReflectionText(null);
            result.add(item);
        }
        return result;
    }

    private List<PersonalReportPlanItemRespVO> buildPersonalPlanItems(LocalDate nextStartDate,
                                                                      LocalDate nextEndDate,
                                                                      Long userId,
                                                                      boolean weeklyMode) {
        List<TaskWorklogDO> nextPeriodWorklogs = taskWorklogMapper.selectListByUserIdAndPeriod(userId, nextStartDate, nextEndDate);
        Map<Long, TaskWorklogAggregate> worklogAggregateMap = aggregateTaskWorklogs(nextPeriodWorklogs);

        Map<Long, ProjectTaskDO> taskMap = loadPlanTaskMap(userId, nextStartDate, nextEndDate, worklogAggregateMap.keySet());
        Map<Long, PersonalItemDO> itemMap = loadPlanItemMap(userId, nextStartDate, nextEndDate, worklogAggregateMap.keySet());

        Set<Long> projectIds = taskMap.values().stream()
                .map(ProjectTaskDO::getProjectId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Map<Long, ProjectDO> projectMap = projectIds.isEmpty() ? Collections.emptyMap()
                : projectMapper.selectBatchIds(projectIds).stream()
                .collect(Collectors.toMap(ProjectDO::getId, item -> item));
        Map<String, String> taskItemTypeLabelMap = loadTaskItemTypeLabelMap();

        Map<String, PlanAggregate> aggregateMap = new LinkedHashMap<>();
        for (ProjectTaskDO task : taskMap.values()) {
            TaskWorklogAggregate worklogAggregate = worklogAggregateMap.get(task.getId());
            if (!shouldIncludeTaskInPlan(task, nextStartDate, nextEndDate, worklogAggregate)) {
                continue;
            }
            ProjectDO project = projectMap.get(task.getProjectId());
            String workItemTitle = safeText(project == null ? null : project.getProjectName());
            String categoryCode = safeText(task.getType());
            String categoryLabel = resolveTaskItemTypeLabel(task.getType(), taskItemTypeLabelMap);
            String groupKey = workItemTitle + "||" + categoryCode;
            PlanAggregate aggregate = aggregateMap.computeIfAbsent(groupKey,
                    key -> new PlanAggregate(workItemTitle, categoryLabel));
            aggregate.addTaskLine(safeText(task.getTaskTitle()), safeText(task.getPriority()), task.getProgressRate(),
                    worklogAggregate, weeklyMode);
        }
        for (PersonalItemDO item : itemMap.values()) {
            TaskWorklogAggregate worklogAggregate = worklogAggregateMap.get(item.getId());
            if (!shouldIncludePersonalItemInPlan(item, nextStartDate, nextEndDate, worklogAggregate)) {
                continue;
            }
            String categoryCode = safeText(item.getType());
            String categoryLabel = resolveTaskItemTypeLabel(item.getType(), taskItemTypeLabelMap);
            String groupKey = WORK_ITEM_MY_ITEMS + "||" + categoryCode;
            PlanAggregate aggregate = aggregateMap.computeIfAbsent(groupKey,
                    key -> new PlanAggregate(WORK_ITEM_MY_ITEMS, categoryLabel));
            aggregate.addItemLine(safeText(item.getTaskTitle()), item.getProgressRate(), worklogAggregate, weeklyMode);
        }

        List<PersonalReportPlanItemRespVO> result = new ArrayList<>();
        int itemNumber = 1;
        for (PlanAggregate aggregate : aggregateMap.values()) {
            if (aggregate.lines().isEmpty()) {
                continue;
            }
            PersonalReportPlanItemRespVO item = new PersonalReportPlanItemRespVO();
            item.setItemNumber(itemNumber++);
            item.setItemTitle(aggregate.workItemTitle());
            item.setTargetText(aggregate.buildTargetText());
            item.setSupportNeed(null);
            result.add(item);
        }
        return result;
    }

    private List<ProjectReportItemRespVO> buildProjectCurrentItems(Long projectId,
                                                                   LocalDate periodStartDate,
                                                                   LocalDate periodEndDate) {
        List<ProjectTaskDO> tasks = projectTaskMapper.selectListByProjectId(projectId);
        if (tasks.isEmpty()) {
            return Collections.emptyList();
        }

        Set<Long> taskIds = tasks.stream()
                .map(ProjectTaskDO::getId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Map<Long, ProjectTaskDO> taskMap = tasks.stream()
                .collect(Collectors.toMap(ProjectTaskDO::getId, item -> item));
        List<TaskWorklogDO> worklogs = taskWorklogMapper.selectListByTaskIdsAndPeriod(taskIds, periodStartDate, periodEndDate);
        if (worklogs.isEmpty()) {
            return Collections.emptyList();
        }

        Map<Long, ProjectExecutionDO> executionMap = projectExecutionMapper.selectListByProjectId(projectId).stream()
                .collect(Collectors.toMap(ProjectExecutionDO::getId, item -> item));
        Map<Long, BigDecimal> hoursByExecutionId = new LinkedHashMap<>();
        for (TaskWorklogDO worklog : worklogs) {
            ProjectTaskDO task = taskMap.get(worklog.getTaskId());
            if (task == null || task.getExecutionId() == null) {
                continue;
            }
            hoursByExecutionId.merge(task.getExecutionId(), nullToZero(worklog.getDurationHours()), BigDecimal::add);
        }
        if (hoursByExecutionId.isEmpty()) {
            return Collections.emptyList();
        }

        List<String> progressExcludedStatusCodes = loadProgressExcludedTaskStatusCodes();
        List<ProjectReportItemRespVO> result = new ArrayList<>();
        for (Map.Entry<Long, BigDecimal> entry : hoursByExecutionId.entrySet()) {
            ProjectExecutionDO execution = executionMap.get(entry.getKey());
            if (execution == null) {
                continue;
            }
            ProjectReportItemRespVO item = new ProjectReportItemRespVO();
            item.setItemTitle(safeText(execution.getExecutionName()));
            item.setWorkHours(entry.getValue());
            item.setPriorityCode(safeText(execution.getPriority()));
            item.setProgressRate(normalizeProgress(projectTaskMapper.selectRootTaskAvgProgressByExecutionId(
                    projectId, execution.getId(), progressExcludedStatusCodes)));
            result.add(item);
        }
        result.sort(Comparator.comparing(ProjectReportItemRespVO::getItemTitle, Comparator.nullsLast(String::compareTo)));
        return result;
    }

    private List<ProjectReportItemRespVO> buildProjectNextItems(Long projectId,
                                                                LocalDate nextStartDate,
                                                                LocalDate nextEndDate) {
        List<ProjectExecutionDO> plannedExecutions = projectExecutionMapper.selectPlannedListByProjectIdAndOverlap(
                projectId, nextStartDate, nextEndDate, ProjectObjectConstants.STATUS_CANCELLED);
        List<ProjectExecutionDO> zeroProgressExecutions = projectExecutionMapper.selectListByProjectIdAndStatusNot(
                projectId, ProjectObjectConstants.STATUS_CANCELLED).stream()
                .filter(execution -> isZeroProgress(execution.getProgressRate()))
                .collect(Collectors.toList());
        Map<Long, ProjectExecutionDO> executionMap = new LinkedHashMap<>();
        plannedExecutions.forEach(item -> executionMap.put(item.getId(), item));
        zeroProgressExecutions.forEach(item -> executionMap.put(item.getId(), item));
        if (executionMap.isEmpty()) {
            return Collections.emptyList();
        }

        List<String> progressExcludedStatusCodes = loadProgressExcludedTaskStatusCodes();
        List<ProjectReportItemRespVO> result = new ArrayList<>();
        for (ProjectExecutionDO execution : executionMap.values()) {
            BigDecimal progress = normalizeProgress(projectTaskMapper.selectRootTaskAvgProgressByExecutionId(
                    projectId, execution.getId(), progressExcludedStatusCodes));
            if (isCompleted(progress)) {
                continue;
            }
            ProjectReportItemRespVO item = new ProjectReportItemRespVO();
            item.setItemTitle(safeText(execution.getExecutionName()));
            item.setWorkHours(BigDecimal.ZERO);
            item.setPriorityCode(safeText(execution.getPriority()));
            item.setProgressRate(progress);
            result.add(item);
        }
        result.sort(Comparator.comparing(ProjectReportItemRespVO::getItemTitle, Comparator.nullsLast(String::compareTo)));
        return result;
    }

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

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

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

    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 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 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 CurrentUserProfile loadCurrentUserProfile(boolean requireManager) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        AdminUserRespDTO user = loadUser(userId);
        String userName = StringUtils.hasText(user.getNickname())
                ? user.getNickname().trim()
                : safeText(SecurityFrameworkUtils.getLoginUserNickname());

        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 = safeText(directManager.getNickname());
        }
        if (requireManager && directManagerId == null) {
            throw exception(ErrorCodeConstants.WORK_REPORT_DIRECT_MANAGER_NOT_EXISTS);
        }
        return new CurrentUserProfile(userId, safeText(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;
    }

    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> result = new ArrayList<>(userIds.size());
        for (Long userId : userIds) {
            WorkReportMemberSnapshotItem item = new WorkReportMemberSnapshotItem();
            item.setUserId(userId);
            AdminUserRespDTO user = userMap.get(userId);
            item.setUserName(user == null ? "" : safeText(user.getNickname()));
            result.add(item);
        }
        return result;
    }

    private Map<String, String> loadTaskItemTypeLabelMap() {
        CommonResult<List<DictDataRespDTO>> result = dictDataApi.getDictDataList(DictTypeConstants.RDMS_TASK_ITEM_TYPE);
        List<DictDataRespDTO> dictDataList = result == null ? null : result.getCheckedData();
        if (dictDataList == null || dictDataList.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<String, String> labelMap = new LinkedHashMap<>();
        for (DictDataRespDTO item : dictDataList) {
            if (item == null || !StringUtils.hasText(item.getValue())) {
                continue;
            }
            labelMap.put(item.getValue().trim(), safeText(item.getLabel()));
        }
        return labelMap;
    }

    private String resolveTaskItemTypeLabel(String typeCode, Map<String, String> labelMap) {
        String normalizedTypeCode = safeText(typeCode);
        if (!StringUtils.hasText(normalizedTypeCode)) {
            return normalizedTypeCode;
        }
        String label = labelMap.get(normalizedTypeCode);
        return StringUtils.hasText(label) ? label : normalizedTypeCode;
    }

    private List<String> loadProgressExcludedTaskStatusCodes() {
        return objectStatusModelMapper.selectProgressExcludedStatusCodesByObjectTypeEnabled(ProjectTaskConstants.OBJECT_TYPE);
    }

    private BigDecimal sumReviewWorkHours(List<PersonalReportReviewItemRespVO> reviewItems) {
        if (reviewItems == null || reviewItems.isEmpty()) {
            return BigDecimal.ZERO;
        }
        return reviewItems.stream()
                .map(PersonalReportReviewItemRespVO::getWorkHours)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private BigDecimal sumProjectWorkHours(List<ProjectReportItemRespVO> items) {
        if (items == null || items.isEmpty()) {
            return BigDecimal.ZERO;
        }
        return items.stream()
                .map(ProjectReportItemRespVO::getWorkHours)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    private boolean isZeroProgress(BigDecimal value) {
        return value == null || value.compareTo(BigDecimal.ZERO) == 0;
    }

    private boolean isCompleted(BigDecimal value) {
        return value != null && value.compareTo(BigDecimal.valueOf(100)) >= 0;
    }

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

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

    private Map<Long, ProjectTaskDO> loadPlanTaskMap(Long userId, LocalDate nextStartDate, LocalDate nextEndDate,
                                                     Set<Long> worklogTaskIds) {
        Map<Long, ProjectTaskDO> taskMap = new LinkedHashMap<>();
        projectTaskMapper.selectPlannedInvolvedListByUserIdAndOverlap(
                userId, nextStartDate, nextEndDate, ProjectObjectConstants.STATUS_CANCELLED)
                .forEach(task -> taskMap.put(task.getId(), task));
        projectTaskMapper.selectInvolvedListByUserIdAndStatusNot(userId, ProjectObjectConstants.STATUS_CANCELLED).stream()
                .filter(task -> worklogTaskIds.contains(task.getId()) || isZeroProgress(task.getProgressRate()))
                .forEach(task -> taskMap.put(task.getId(), task));
        return taskMap;
    }

    private Map<Long, PersonalItemDO> loadPlanItemMap(Long userId, LocalDate nextStartDate, LocalDate nextEndDate,
                                                      Set<Long> worklogTaskIds) {
        Map<Long, PersonalItemDO> itemMap = new LinkedHashMap<>();
        personalItemMapper.selectPlannedListByOwnerIdAndOverlap(
                userId, nextStartDate, nextEndDate, ProjectObjectConstants.STATUS_CANCELLED)
                .forEach(item -> itemMap.put(item.getId(), item));
        personalItemMapper.selectListByOwnerIdAndStatusNot(userId, ProjectObjectConstants.STATUS_CANCELLED).stream()
                .filter(item -> worklogTaskIds.contains(item.getId()) || isZeroProgress(item.getProgressRate()))
                .forEach(item -> itemMap.put(item.getId(), item));
        return itemMap;
    }

    private Map<Long, TaskWorklogAggregate> aggregateTaskWorklogs(List<TaskWorklogDO> worklogs) {
        if (worklogs == null || worklogs.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, TaskWorklogAggregate> aggregateMap = new LinkedHashMap<>();
        for (TaskWorklogDO worklog : worklogs) {
            if (worklog.getTaskId() == null) {
                continue;
            }
            aggregateMap.computeIfAbsent(worklog.getTaskId(), key -> new TaskWorklogAggregate())
                    .merge(worklog);
        }
        return aggregateMap;
    }

    private boolean shouldIncludeTaskInPlan(ProjectTaskDO task, LocalDate nextStartDate, LocalDate nextEndDate,
                                            TaskWorklogAggregate worklogAggregate) {
        if (task == null || isCompleted(task.getProgressRate())) {
            return false;
        }
        if (worklogAggregate != null) {
            return true;
        }
        if (isZeroProgress(task.getProgressRate())) {
            return true;
        }
        return overlapsNextPeriod(task.getPlannedStartDate(), task.getPlannedEndDate(), nextStartDate, nextEndDate);
    }

    private boolean shouldIncludePersonalItemInPlan(PersonalItemDO item, LocalDate nextStartDate, LocalDate nextEndDate,
                                                    TaskWorklogAggregate worklogAggregate) {
        if (item == null || isCompleted(item.getProgressRate())) {
            return false;
        }
        if (worklogAggregate != null) {
            return true;
        }
        if (isZeroProgress(item.getProgressRate())) {
            return true;
        }
        return overlapsNextPeriod(item.getPlannedStartDate(), item.getPlannedEndDate(), nextStartDate, nextEndDate);
    }

    private boolean overlapsNextPeriod(LocalDate plannedStartDate, LocalDate plannedEndDate,
                                       LocalDate nextStartDate, LocalDate nextEndDate) {
        return plannedStartDate != null
                && plannedEndDate != null
                && !plannedStartDate.isAfter(nextEndDate)
                && !plannedEndDate.isBefore(nextStartDate);
    }

    private String safeText(String value) {
        return value == null ? "" : value.trim();
    }

    private LocalDateRange nextWeeklyRange(LocalDate currentStartDate, LocalDate currentEndDate) {
        return new LocalDateRange(currentStartDate.plusWeeks(1), currentEndDate.plusWeeks(1));
    }

    private LocalDateRange nextMonthlyRange(LocalDate currentEndDate) {
        LocalDate nextMonthStart = currentEndDate.withDayOfMonth(1).plusMonths(1);
        return new LocalDateRange(nextMonthStart, nextMonthStart.withDayOfMonth(nextMonthStart.lengthOfMonth()));
    }

    private LocalDateRange nextHalfMonthRange(LocalDate currentEndDate) {
        LocalDate nextDay = currentEndDate.plusDays(1);
        if (nextDay.getDayOfMonth() <= 15) {
            return new LocalDateRange(nextDay.withDayOfMonth(1), nextDay.withDayOfMonth(15));
        }
        return new LocalDateRange(nextDay.withDayOfMonth(16), nextDay.withDayOfMonth(nextDay.lengthOfMonth()));
    }

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

    private record LocalDateRange(LocalDate startDate, LocalDate endDate) {
    }

    private static final class ReviewAggregate {

        private final String workItemTitle;
        private final String lineTitle;
        private final String priority;
        private final String typeCode;
        private BigDecimal totalWorkHours = BigDecimal.ZERO;
        private BigDecimal latestProgressRate = BigDecimal.ZERO;
        private LocalDate latestEndDate;
        private final List<String> workContents = new ArrayList<>();

        private ReviewAggregate(String workItemTitle, String lineTitle, String priority, String typeCode) {
            this.workItemTitle = workItemTitle;
            this.lineTitle = lineTitle;
            this.priority = priority;
            this.typeCode = typeCode;
        }

        private void merge(TaskWorklogDO worklog) {
            totalWorkHours = totalWorkHours.add(
                    worklog.getDurationHours() == null ? BigDecimal.ZERO : worklog.getDurationHours());
            LocalDate endDate = worklog.getEndDate();
            if (endDate != null && (latestEndDate == null || !endDate.isBefore(latestEndDate))) {
                latestEndDate = endDate;
                latestProgressRate = worklog.getProgressRate() == null ? BigDecimal.ZERO : worklog.getProgressRate();
            }
            if (StringUtils.hasText(worklog.getWorkContent())) {
                workContents.add(worklog.getWorkContent().trim());
            }
        }

        private String buildWeeklyReviewText() {
            StringBuilder builder = new StringBuilder();
            if (StringUtils.hasText(typeCode)) {
                builder.append(typeCode).append(" - ");
            }
            builder.append(lineTitle);
            appendBracket(builder, priority, latestProgressRate, totalWorkHours);
            if (!workContents.isEmpty()) {
                builder.append("：").append(String.join(WORKLOG_DAY_SEPARATOR, workContents));
            }
            return builder.toString();
        }

        private String buildMonthlyReviewText() {
            StringBuilder builder = new StringBuilder();
            if (StringUtils.hasText(typeCode)) {
                builder.append(typeCode).append(" - ");
            }
            builder.append(lineTitle);
            appendBracket(builder, priority, latestProgressRate, totalWorkHours);
            return builder.toString();
        }

        private void appendBracket(StringBuilder builder, String priority,
                                   BigDecimal progressRate, BigDecimal hours) {
            List<String> parts = new ArrayList<>();
            if (StringUtils.hasText(priority)) {
                parts.add(priority);
            }
            if (progressRate != null) {
                parts.add("进度" + progressRate.stripTrailingZeros().toPlainString() + "%");
            }
            if (hours != null) {
                parts.add(hours.stripTrailingZeros().toPlainString() + "h");
            }
            if (!parts.isEmpty()) {
                builder.append("（").append(String.join(" / ", parts)).append("）");
            }
        }

        private String workItemTitle() {
            return workItemTitle;
        }

        private BigDecimal totalWorkHours() {
            return totalWorkHours;
        }

        private BigDecimal latestProgressRate() {
            return latestProgressRate;
        }
    }

    private static final class PlanAggregate {

        private final String workItemTitle;
        private final String category;
        private final List<String> lines = new ArrayList<>();

        private PlanAggregate(String workItemTitle, String category) {
            this.workItemTitle = workItemTitle;
            this.category = category;
        }

        private void addTaskLine(String title, String priority, BigDecimal progressRate,
                                 TaskWorklogAggregate worklogAggregate, boolean weeklyMode) {
            if (!StringUtils.hasText(title)) {
                return;
            }
            if (worklogAggregate != null && worklogAggregate.hasWorkContent()) {
                lines.add(buildWorklogStyleLine(title, priority, progressRate, worklogAggregate, weeklyMode));
                return;
            }
            lines.add(buildPlannedStyleLine(title, priority, progressRate));
        }

        private void addItemLine(String title, BigDecimal progressRate,
                                 TaskWorklogAggregate worklogAggregate, boolean weeklyMode) {
            if (!StringUtils.hasText(title)) {
                return;
            }
            if (worklogAggregate != null && worklogAggregate.hasWorkContent()) {
                lines.add(buildWorklogStyleLine(title, null, progressRate, worklogAggregate, weeklyMode));
                return;
            }
            lines.add(buildPlannedStyleLine(title, null, progressRate));
        }

        private String buildWorklogStyleLine(String title, String priority, BigDecimal progressRate,
                                             TaskWorklogAggregate worklogAggregate, boolean weeklyMode) {
            StringBuilder builder = new StringBuilder();
            if (StringUtils.hasText(category)) {
                builder.append(category).append(" - ");
            }
            builder.append(title);
            List<String> parts = new ArrayList<>();
            if (StringUtils.hasText(priority)) {
                parts.add(priority);
            }
            if (progressRate != null) {
                parts.add("进度" + progressRate.stripTrailingZeros().toPlainString() + "%");
            }
            if (worklogAggregate.totalWorkHours() != null) {
                parts.add(worklogAggregate.totalWorkHours().stripTrailingZeros().toPlainString() + "h");
            }
            if (!parts.isEmpty()) {
                builder.append("（").append(String.join(" / ", parts)).append("）");
            }
            if (weeklyMode && worklogAggregate.hasWorkContent()) {
                builder.append("：").append(String.join(WORKLOG_DAY_SEPARATOR, worklogAggregate.workContents()));
            }
            return builder.toString();
        }

        private String buildPlannedStyleLine(String title, String priority, BigDecimal progressRate) {
            StringBuilder builder = new StringBuilder();
            if (StringUtils.hasText(category)) {
                builder.append(category).append(" - ");
            }
            builder.append(title);
            List<String> parts = new ArrayList<>();
            if (StringUtils.hasText(priority)) {
                parts.add(priority);
            }
            if (progressRate != null) {
                parts.add("进度" + progressRate.stripTrailingZeros().toPlainString() + "%");
            }
            if (!parts.isEmpty()) {
                builder.append("（").append(String.join(" / ", parts)).append("）");
            }
            return builder.toString();
        }

        private String buildTargetText() {
            return String.join("\n", lines);
        }

        private String workItemTitle() {
            return workItemTitle;
        }

        private List<String> lines() {
            return lines;
        }
    }

    private static final class TaskWorklogAggregate {

        private BigDecimal totalWorkHours = BigDecimal.ZERO;
        private final List<String> workContents = new ArrayList<>();

        private void merge(TaskWorklogDO worklog) {
            totalWorkHours = totalWorkHours.add(
                    worklog.getDurationHours() == null ? BigDecimal.ZERO : worklog.getDurationHours());
            if (StringUtils.hasText(worklog.getWorkContent())) {
                workContents.add(worklog.getWorkContent().trim());
            }
        }

        private boolean hasWorkContent() {
            return !workContents.isEmpty();
        }

        private BigDecimal totalWorkHours() {
            return totalWorkHours;
        }

        private List<String> workContents() {
            return workContents;
        }
    }
}
