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

import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.WorkReportConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportRemindRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.team.vo.TeamReportSummaryRespVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.monthly.MonthlyReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.project.ProjectReportDO;
import com.njcn.rdms.module.project.dal.dataobject.workreport.weekly.WeeklyReportDO;
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.workreport.monthly.MonthlyReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.project.ProjectReportMapper;
import com.njcn.rdms.module.project.dal.mysql.workreport.weekly.WeeklyReportMapper;
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import 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.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
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.invalidParamException;

@Service
public class TeamWorkReportServiceImpl implements TeamWorkReportService {

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

    @Resource
    private TeamDashboardAccessService teamDashboardAccessService;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private WeeklyReportMapper weeklyReportMapper;
    @Resource
    private MonthlyReportMapper monthlyReportMapper;
    @Resource
    private ProjectReportMapper projectReportMapper;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public TeamReportSummaryRespVO getSummary(TeamReportSummaryReqVO reqVO) {
        teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
        String reportType = normalizeReportType(reqVO.getReportType());
        DateRange dateRange = normalizeDateRange(reqVO.getPeriodStartDate(), reqVO.getPeriodEndDate());
        ReportContext context;
        if (dateRange != null) {
            context = buildReportContextByDateRange(reportType, dateRange.startDate(), dateRange.endDate());
        } else {
            context = buildReportContext(reportType, reqVO.getPeriodKey());
        }

        List<TeamReportSummaryRespVO.UnsubmittedReport> unsubmittedReports = buildUnsubmittedReports(context);
        TeamReportSummaryRespVO respVO = new TeamReportSummaryRespVO();
        respVO.setPeriodStartDate(dateRange == null ? null : dateRange.startDate());
        respVO.setPeriodEndDate(dateRange == null ? null : dateRange.endDate());
        respVO.setExpectedReportCount(context.expectedSlots().size());
        respVO.setSubmittedReportCount(context.submittedSlotKeys().size());
        respVO.setUnsubmittedReportCount(unsubmittedReports.size());
        respVO.setPendingApprovalReportCount(context.pendingApprovalSlotKeys().size());
        respVO.setUnsubmittedReports(unsubmittedReports);
        return respVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public TeamReportRemindRespVO remind(TeamReportRemindReqVO reqVO) {
        teamDashboardAccessService.validateTeamDashboardPermission(WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
        String reportType = normalizeReportType(reqVO.getReportType());
        ReportContext context = buildReportContext(reportType, reqVO.getPeriodKey());
        int remindedCount = WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)
                ? remindProjectReport(reqVO, context)
                : remindPersonalReport(reportType, reqVO, context);

        TeamReportRemindRespVO respVO = new TeamReportRemindRespVO();
        respVO.setRemindedCount(remindedCount);
        return respVO;
    }

    private int remindPersonalReport(String reportType, TeamReportRemindReqVO reqVO, ReportContext context) {
        List<Long> remindUserIds = resolveRemindUserIds(reqVO.getUserIds(), context);
        if (remindUserIds.isEmpty()) {
            return 0;
        }
        Map<String, Object> params = new HashMap<>();
        params.put("reportTypeName", reportTypeDisplayName(reportType));
        params.put("periodKey", formatPeriodKeyForRemind(reportType, reqVO.getPeriodKey()));
        params.put("managerName", defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        applicationEventPublisher.publishEvent(NotifySendEvent.of(remindUserIds,
                NotifyTemplateCodeConstants.WORK_REPORT_TEAM_REMIND, params, NotifyMessageLevelConstants.REMIND));
        return remindUserIds.size();
    }

    private int remindProjectReport(TeamReportRemindReqVO reqVO, ReportContext context) {
        List<ExpectedReportSlot> remindSlots = resolveProjectRemindSlots(reqVO, context);
        if (remindSlots.isEmpty()) {
            return 0;
        }
        String managerName = defaultText(SecurityFrameworkUtils.getLoginUserNickname());
        for (ExpectedReportSlot slot : remindSlots) {
            Map<String, Object> params = new HashMap<>();
            params.put("projectName", slot.projectName());
            params.put("periodLabel", slot.periodLabel());
            params.put("managerName", managerName);
            applicationEventPublisher.publishEvent(NotifySendEvent.of(List.of(slot.userId()),
                    NotifyTemplateCodeConstants.WORK_REPORT_PROJECT_HALF_MONTH_TEAM_REMIND,
                    params, NotifyMessageLevelConstants.REMIND));
        }
        return remindSlots.size();
    }

    private ReportContext buildReportContext(String reportType, String periodKey) {
        if (!StringUtils.hasText(periodKey)) {
            throw invalidParamException("周期主键不能为空");
        }
        ReportPeriod period = parseRequiredPeriod(reportType, periodKey);
        return buildReportContextByPeriods(reportType, List.of(period));
    }

    private ReportContext buildReportContextByDateRange(String reportType, LocalDate startDate, LocalDate endDate) {
        return buildReportContextByPeriods(reportType, buildPeriodsByDateRange(reportType, startDate, endDate));
    }

    private ReportContext buildReportContextByPeriods(String reportType, List<ReportPeriod> periods) {
        if (WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) {
            return buildProjectContext(periods);
        }
        List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
        if (subordinateIds.isEmpty() || periods.isEmpty()) {
            return ReportContext.empty();
        }
        if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)) {
            return buildWeeklyContext(periods, subordinateIds);
        }
        return buildMonthlyContext(periods, subordinateIds);
    }

    private ReportContext buildWeeklyContext(List<ReportPeriod> periods, List<Long> subordinateIds) {
        Map<String, ExpectedReportSlot> expectedSlotMap = buildPersonalExpectedSlotMap(subordinateIds, periods);
        List<WeeklyReportDO> reports = weeklyReportMapper.selectListByReporterIdsAndPeriodDateRange(
                subordinateIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
                SUBMITTED_STATUS_CODES);
        return buildPersonalReportContext(expectedSlotMap, reports.stream()
                .filter(Objects::nonNull)
                .filter(report -> report.getReporterId() != null)
                .map(report -> new ActualReportSlot(
                        personalSlotKey(report.getReporterId(), report.getPeriodKey()),
                        report.getStatusCode()))
                .toList());
    }

    private ReportContext buildMonthlyContext(List<ReportPeriod> periods, List<Long> subordinateIds) {
        Map<String, ExpectedReportSlot> expectedSlotMap = buildPersonalExpectedSlotMap(subordinateIds, periods);
        List<MonthlyReportDO> reports = monthlyReportMapper.selectListByReporterIdsAndPeriodDateRange(
                subordinateIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
                SUBMITTED_STATUS_CODES);
        return buildPersonalReportContext(expectedSlotMap, reports.stream()
                .filter(Objects::nonNull)
                .filter(report -> report.getReporterId() != null)
                .map(report -> new ActualReportSlot(
                        personalSlotKey(report.getReporterId(), report.getPeriodKey()),
                        report.getStatusCode()))
                .toList());
    }

    private ReportContext buildProjectContext(List<ReportPeriod> periods) {
        List<Long> subordinateIds = teamDashboardAccessService.getAllSubordinateUserIds();
        if (subordinateIds.isEmpty() || periods.isEmpty()) {
            return ReportContext.empty();
        }
        List<ProjectDO> activeProjects = loadActiveProjectsForSubordinates(subordinateIds);
        if (activeProjects.isEmpty()) {
            return ReportContext.empty();
        }
        Map<String, ExpectedReportSlot> expectedSlotMap = buildProjectExpectedSlotMap(activeProjects, periods);
        LinkedHashSet<Long> ownerIds = activeProjects.stream()
                .map(ProjectDO::getManagerUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (ownerIds.isEmpty()) {
            return ReportContext.empty();
        }
        List<ProjectReportDO> reports = projectReportMapper.selectListByProjectOwnerIdsAndPeriodDateRange(
                ownerIds, periods.get(0).startDate(), periods.get(periods.size() - 1).startDate(),
                SUBMITTED_STATUS_CODES);
        Set<String> submittedSlotKeys = new LinkedHashSet<>();
        Set<String> pendingApprovalSlotKeys = new LinkedHashSet<>();
        for (ProjectReportDO report : reports) {
            if (report == null || report.getProjectOwnerId() == null || report.getProjectId() == null) {
                continue;
            }
            String slotKey = projectSlotKey(report.getProjectOwnerId(), report.getProjectId(), report.getPeriodKey());
            if (!expectedSlotMap.containsKey(slotKey)) {
                continue;
            }
            submittedSlotKeys.add(slotKey);
            if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(report.getStatusCode())) {
                pendingApprovalSlotKeys.add(slotKey);
            }
        }
        return new ReportContext(new ArrayList<>(expectedSlotMap.values()), submittedSlotKeys, pendingApprovalSlotKeys);
    }

    private ReportContext buildPersonalReportContext(Map<String, ExpectedReportSlot> expectedSlotMap,
            List<ActualReportSlot> actualSlots) {
        if (expectedSlotMap.isEmpty()) {
            return ReportContext.empty();
        }
        Set<String> submittedSlotKeys = new LinkedHashSet<>();
        Set<String> pendingApprovalSlotKeys = new LinkedHashSet<>();
        for (ActualReportSlot actualSlot : actualSlots) {
            if (actualSlot == null || !expectedSlotMap.containsKey(actualSlot.slotKey())) {
                continue;
            }
            submittedSlotKeys.add(actualSlot.slotKey());
            if (WorkReportConstants.STATUS_PENDING_APPROVAL.equals(actualSlot.statusCode())) {
                pendingApprovalSlotKeys.add(actualSlot.slotKey());
            }
        }
        return new ReportContext(new ArrayList<>(expectedSlotMap.values()), submittedSlotKeys, pendingApprovalSlotKeys);
    }

    private Map<String, ExpectedReportSlot> buildPersonalExpectedSlotMap(List<Long> subordinateIds,
            List<ReportPeriod> periods) {
        LinkedHashMap<String, ExpectedReportSlot> result = new LinkedHashMap<>();
        for (Long subordinateId : subordinateIds) {
            if (subordinateId == null) {
                continue;
            }
            for (ReportPeriod period : periods) {
                String slotKey = personalSlotKey(subordinateId, period.periodKey());
                result.put(slotKey, new ExpectedReportSlot(
                        slotKey, subordinateId, period.periodKey(), period.periodLabel(), null, null));
            }
        }
        return result;
    }

    private Map<String, ExpectedReportSlot> buildProjectExpectedSlotMap(List<ProjectDO> activeProjects,
            List<ReportPeriod> periods) {
        LinkedHashMap<String, ExpectedReportSlot> result = new LinkedHashMap<>();
        for (ProjectDO project : activeProjects) {
            if (project == null || project.getManagerUserId() == null || project.getId() == null) {
                continue;
            }
            for (ReportPeriod period : periods) {
                String slotKey = projectSlotKey(project.getManagerUserId(), project.getId(), period.periodKey());
                result.put(slotKey, new ExpectedReportSlot(
                        slotKey,
                        project.getManagerUserId(),
                        period.periodKey(),
                        period.periodLabel(),
                        project.getId(),
                        defaultText(project.getProjectName())));
            }
        }
        return result;
    }

    private List<ProjectDO> loadActiveProjectsForSubordinates(Collection<Long> subordinateIds) {
        if (subordinateIds == null || subordinateIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<String> terminalStatusCodes = objectStatusModelMapper
                .selectTerminalStatusCodesByObjectTypeEnabled(
                        com.njcn.rdms.module.project.constant.ProjectObjectConstants.OBJECT_TYPE);
        List<ProjectDO> allProjects = projectMapper.selectListByManagerUserIdsAndStatusCodesNotIn(
                subordinateIds, terminalStatusCodes);
        if (allProjects.isEmpty()) {
            return Collections.emptyList();
        }
        return allProjects.stream()
                .filter(Objects::nonNull)
                .filter(project -> project.getManagerUserId() != null)
                .collect(Collectors.toList());
    }

    private List<Long> resolveRemindUserIds(List<Long> requestedUserIds, ReportContext context) {
        LinkedHashSet<Long> unsubmittedUserIds = context.expectedSlots().stream()
                .filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
                .map(ExpectedReportSlot::userId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (requestedUserIds == null) {
            return new ArrayList<>(unsubmittedUserIds);
        }
        List<Long> validatedIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                requestedUserIds, WorkReportConstants.PERMISSION_TEAM_DASHBOARD);
        return validatedIds.stream()
                .filter(unsubmittedUserIds::contains)
                .collect(Collectors.toList());
    }

    private List<ExpectedReportSlot> resolveProjectRemindSlots(TeamReportRemindReqVO reqVO, ReportContext context) {
        List<ExpectedReportSlot> pendingSlots = context.expectedSlots().stream()
                .filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
                .toList();
        if (pendingSlots.isEmpty()) {
            return Collections.emptyList();
        }

        Set<Long> validatedUserIds = reqVO.getUserIds() == null
                ? null
                : new LinkedHashSet<>(teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                        reqVO.getUserIds(), WorkReportConstants.PERMISSION_TEAM_DASHBOARD));

        Long requestedProjectId = reqVO.getProjectId();
        return pendingSlots.stream()
                .filter(slot -> validatedUserIds == null || validatedUserIds.contains(slot.userId()))
                .filter(slot -> requestedProjectId == null || Objects.equals(slot.projectId(), requestedProjectId))
                .toList();
    }

    private List<TeamReportSummaryRespVO.UnsubmittedReport> buildUnsubmittedReports(ReportContext context) {
        List<ExpectedReportSlot> pendingSlots = context.expectedSlots().stream()
                .filter(slot -> !context.submittedSlotKeys().contains(slot.slotKey()))
                .toList();
        if (pendingSlots.isEmpty()) {
            return Collections.emptyList();
        }
        LinkedHashSet<Long> userIds = pendingSlots.stream()
                .map(ExpectedReportSlot::userId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Map<Long, AdminUserRespDTO> userMap = loadUserMap(userIds);

        List<TeamReportSummaryRespVO.UnsubmittedReport> respList = new ArrayList<>();
        for (ExpectedReportSlot pendingSlot : pendingSlots) {
            AdminUserRespDTO user = userMap.get(pendingSlot.userId());
            TeamReportSummaryRespVO.UnsubmittedReport item = new TeamReportSummaryRespVO.UnsubmittedReport();
            item.setUserId(pendingSlot.userId());
            item.setUserNickname(user == null ? "" : defaultText(user.getNickname()));
            item.setPeriodKey(pendingSlot.periodKey());
            item.setPeriodLabel(pendingSlot.periodLabel());
            item.setProjectId(pendingSlot.projectId());
            item.setProjectName(pendingSlot.projectName());
            respList.add(item);
        }
        return respList;
    }

    private Map<Long, AdminUserRespDTO> loadUserMap(Collection<Long> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Map.of();
        }
        CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserList(userIds);
        List<AdminUserRespDTO> users = result == null ? null : result.getCheckedData();
        if (users == null || users.isEmpty()) {
            return Map.of();
        }
        return users.stream()
                .filter(Objects::nonNull)
                .filter(user -> user.getId() != null)
                .collect(Collectors.toMap(
                        AdminUserRespDTO::getId, user -> user, (left, right) -> left, LinkedHashMap::new));
    }

    private List<ReportPeriod> buildPeriodsByDateRange(String reportType, LocalDate startDate, LocalDate endDate) {
        return switch (reportType) {
            case WorkReportConstants.REPORT_TYPE_WEEKLY -> buildWeeklyPeriodsByDateRange(startDate, endDate);
            case WorkReportConstants.REPORT_TYPE_MONTHLY -> buildMonthlyPeriodsByDateRange(startDate, endDate);
            case WorkReportConstants.REPORT_TYPE_PROJECT -> buildProjectPeriodsByDateRange(startDate, endDate);
            default -> throw invalidParamException("报告类型不合法");
        };
    }

    private List<ReportPeriod> buildWeeklyPeriodsByDateRange(LocalDate startDate, LocalDate endDate) {
        LocalDate firstStart = startDate.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
        if (firstStart.isAfter(endDate)) {
            return Collections.emptyList();
        }
        List<ReportPeriod> periods = new ArrayList<>();
        for (LocalDate currentStart = firstStart; !currentStart.isAfter(endDate); currentStart = currentStart.plusWeeks(1)) {
            periods.add(buildWeeklyPeriod(currentStart, currentStart.plusDays(6)));
        }
        return periods;
    }

    private List<ReportPeriod> buildMonthlyPeriodsByDateRange(LocalDate startDate, LocalDate endDate) {
        LocalDate firstStart = startDate.withDayOfMonth(1);
        if (firstStart.isBefore(startDate)) {
            firstStart = firstStart.plusMonths(1);
        }
        if (firstStart.isAfter(endDate)) {
            return Collections.emptyList();
        }
        List<ReportPeriod> periods = new ArrayList<>();
        for (LocalDate currentStart = firstStart; !currentStart.isAfter(endDate); currentStart = currentStart.plusMonths(1)) {
            periods.add(buildMonthlyPeriod(currentStart));
        }
        return periods;
    }

    private List<ReportPeriod> buildProjectPeriodsByDateRange(LocalDate startDate, LocalDate endDate) {
        LocalDate firstStart;
        int day = startDate.getDayOfMonth();
        if (day <= 1) {
            firstStart = startDate.withDayOfMonth(1);
        } else if (day <= 16) {
            firstStart = startDate.withDayOfMonth(16);
        } else {
            firstStart = startDate.plusMonths(1).withDayOfMonth(1);
        }
        if (firstStart.isAfter(endDate)) {
            return Collections.emptyList();
        }
        List<ReportPeriod> periods = new ArrayList<>();
        for (LocalDate currentStart = firstStart; !currentStart.isAfter(endDate); currentStart = nextHalfMonthStart(currentStart)) {
            periods.add(buildProjectPeriod(currentStart));
        }
        return periods;
    }

    private LocalDate nextHalfMonthStart(LocalDate currentStart) {
        if (currentStart.getDayOfMonth() == 1) {
            return currentStart.withDayOfMonth(16);
        }
        return currentStart.plusMonths(1).withDayOfMonth(1);
    }

    private ReportPeriod parseRequiredPeriod(String reportType, String periodKey) {
        return switch (reportType) {
            case WorkReportConstants.REPORT_TYPE_WEEKLY -> parseWeeklyPeriod(periodKey);
            case WorkReportConstants.REPORT_TYPE_MONTHLY -> parseMonthlyPeriod(periodKey);
            case WorkReportConstants.REPORT_TYPE_PROJECT -> parseProjectPeriod(periodKey);
            default -> throw invalidParamException("报告类型不合法");
        };
    }

    private ReportPeriod parseWeeklyPeriod(String periodKey) {
        String[] parts = periodKey.split("-");
        if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_WEEKLY.equals(parts[0])) {
            throw invalidParamException("周报周期主键不合法");
        }
        LocalDate startDate = parsePeriodDate(parts, 1);
        LocalDate endDate = parsePeriodDate(parts, 4);
        return buildWeeklyPeriod(startDate, endDate);
    }

    private ReportPeriod parseMonthlyPeriod(String periodKey) {
        String[] parts = periodKey.split("-");
        if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_MONTHLY.equals(parts[0])) {
            throw invalidParamException("月报周期主键不合法");
        }
        LocalDate startDate = parsePeriodDate(parts, 1);
        return buildMonthlyPeriod(startDate);
    }

    private ReportPeriod parseProjectPeriod(String periodKey) {
        String[] parts = periodKey.split("-");
        if (parts.length != 8 || !WorkReportConstants.REPORT_TYPE_PROJECT.equals(parts[0])) {
            throw invalidParamException("项目半月报周期主键不合法");
        }
        LocalDate startDate = parsePeriodDate(parts, 1);
        return buildProjectPeriod(startDate);
    }

    private ReportPeriod buildWeeklyPeriod(LocalDate startDate, LocalDate endDate) {
        return new ReportPeriod(
                String.format("weekly-%s-%s", startDate, endDate),
                String.format("%s 至 %s", startDate, endDate),
                startDate,
                endDate);
    }

    private ReportPeriod buildMonthlyPeriod(LocalDate startDate) {
        LocalDate normalizedStartDate = startDate.withDayOfMonth(1);
        LocalDate endDate = normalizedStartDate.withDayOfMonth(normalizedStartDate.lengthOfMonth());
        return new ReportPeriod(
                String.format("monthly-%s-%s", normalizedStartDate, endDate),
                String.format("%d-%02d", normalizedStartDate.getYear(), normalizedStartDate.getMonthValue()),
                normalizedStartDate,
                endDate);
    }

    private ReportPeriod buildProjectPeriod(LocalDate startDate) {
        LocalDate normalizedStartDate = startDate.getDayOfMonth() <= 15
                ? startDate.withDayOfMonth(1)
                : startDate.withDayOfMonth(16);
        boolean firstHalf = normalizedStartDate.getDayOfMonth() == 1;
        LocalDate endDate = firstHalf
                ? normalizedStartDate.withDayOfMonth(15)
                : normalizedStartDate.withDayOfMonth(normalizedStartDate.lengthOfMonth());
        String half = firstHalf ? "上半月" : "下半月";
        return new ReportPeriod(
                String.format("project-%s-%s-%d", normalizedStartDate, endDate, firstHalf ? 1 : 2),
                String.format("%d-%02d %s", normalizedStartDate.getYear(), normalizedStartDate.getMonthValue(), half),
                normalizedStartDate,
                endDate);
    }

    private DateRange normalizeDateRange(LocalDate startDate, LocalDate endDate) {
        if (startDate == null && endDate == null) {
            return null;
        }
        LocalDate normalizedStartDate = startDate == null ? endDate : startDate;
        LocalDate normalizedEndDate = endDate == null ? startDate : endDate;
        if (normalizedStartDate.isAfter(normalizedEndDate)) {
            throw invalidParamException("周期起始日期不能晚于结束日期");
        }
        return new DateRange(normalizedStartDate, normalizedEndDate);
    }

    private String normalizeReportType(String reportType) {
        if (WorkReportConstants.REPORT_TYPE_WEEKLY.equals(reportType)
                || WorkReportConstants.REPORT_TYPE_MONTHLY.equals(reportType)
                || WorkReportConstants.REPORT_TYPE_PROJECT.equals(reportType)) {
            return reportType;
        }
        throw invalidParamException("报告类型不合法");
    }

    private String reportTypeDisplayName(String reportType) {
        return switch (reportType) {
            case WorkReportConstants.REPORT_TYPE_WEEKLY -> "周报";
            case WorkReportConstants.REPORT_TYPE_MONTHLY -> "月报";
            case WorkReportConstants.REPORT_TYPE_PROJECT -> "项目半月报";
            default -> reportType;
        };
    }

    private String formatPeriodKeyForRemind(String reportType, String periodKey) {
        if (!StringUtils.hasText(periodKey)) {
            return "";
        }
        try {
            return switch (reportType) {
                case WorkReportConstants.REPORT_TYPE_WEEKLY -> formatWeeklyPeriodKey(periodKey);
                case WorkReportConstants.REPORT_TYPE_MONTHLY -> formatMonthlyPeriodKey(periodKey);
                case WorkReportConstants.REPORT_TYPE_PROJECT -> formatProjectPeriodKey(periodKey);
                default -> periodKey;
            };
        } catch (RuntimeException ex) {
            return periodKey;
        }
    }

    private String formatWeeklyPeriodKey(String periodKey) {
        String[] parts = periodKey.split("-");
        if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_WEEKLY.equals(parts[0])) {
            return periodKey;
        }
        LocalDate startDate = parsePeriodDate(parts, 1);
        LocalDate endDate = parsePeriodDate(parts, 4);
        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 formatMonthlyPeriodKey(String periodKey) {
        String[] parts = periodKey.split("-");
        if (parts.length != 7 || !WorkReportConstants.REPORT_TYPE_MONTHLY.equals(parts[0])) {
            return periodKey;
        }
        LocalDate startDate = parsePeriodDate(parts, 1);
        return startDate.getYear() + "年" + startDate.getMonthValue() + "月";
    }

    private String formatProjectPeriodKey(String periodKey) {
        String[] parts = periodKey.split("-");
        if (parts.length != 8 || !WorkReportConstants.REPORT_TYPE_PROJECT.equals(parts[0])) {
            return periodKey;
        }
        LocalDate startDate = parsePeriodDate(parts, 1);
        String half = switch (parts[7]) {
            case "1" -> "上半月";
            case "2" -> "下半月";
            default -> startDate.getDayOfMonth() <= 15 ? "上半月" : "下半月";
        };
        return startDate.getYear() + "年" + startDate.getMonthValue() + "月" + half;
    }

    private LocalDate parsePeriodDate(String[] parts, int startIndex) {
        try {
            return LocalDate.parse(parts[startIndex] + "-" + parts[startIndex + 1] + "-" + parts[startIndex + 2]);
        } catch (DateTimeParseException ex) {
            throw new IllegalArgumentException("Invalid period date", ex);
        }
    }

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

    private String personalSlotKey(Long userId, String periodKey) {
        return userId + ":" + periodKey;
    }

    private String projectSlotKey(Long userId, Long projectId, String periodKey) {
        return userId + ":" + projectId + ":" + periodKey;
    }

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

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

    private record ReportPeriod(String periodKey, String periodLabel, LocalDate startDate, LocalDate endDate) {
    }

    private record ExpectedReportSlot(
            String slotKey,
            Long userId,
            String periodKey,
            String periodLabel,
            Long projectId,
            String projectName) {
    }

    private record ActualReportSlot(String slotKey, String statusCode) {
    }

    private record ReportContext(
            List<ExpectedReportSlot> expectedSlots,
            Set<String> submittedSlotKeys,
            Set<String> pendingApprovalSlotKeys) {
        private static ReportContext empty() {
            return new ReportContext(Collections.emptyList(), Collections.emptySet(), Collections.emptySet());
        }
    }
}
