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

import com.njcn.rdms.framework.common.enums.UserTypeEnum;
import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.security.core.LoginUser;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.constant.WorkReportConstants;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportPlanItemRespVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.common.vo.PersonalReportReviewItemRespVO;
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.monthly.vo.MonthlyReportSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportDefaultDraftReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.project.vo.ProjectReportItemReqVO;
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.project.vo.ProjectReportSaveReqVO;
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.controller.admin.workreport.weekly.vo.WeeklyReportSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportTravelSegmentReqVO;
import com.njcn.rdms.module.project.controller.admin.workreport.weekly.vo.WeeklyReportTravelSegmentRespVO;
import com.njcn.rdms.module.project.dal.dataobject.job.JobRunLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.mysql.job.JobRunLogMapper;
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.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.service.workreport.common.WorkReportCommonService;
import com.njcn.rdms.module.project.service.workreport.defaultdraft.WorkReportDefaultDraftService;
import com.njcn.rdms.module.system.api.dept.DeptApi;
import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.net.InetAddress;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Slf4j
@Service
public class WorkReportAutoGenerateService {

    private static final ZoneId ASIA_SHANGHAI = ZoneId.of("Asia/Shanghai");
    private static final String TRIGGER_SOURCE_SCHEDULE = "SCHEDULE";
    private static final String STATUS_RUNNING = "RUNNING";
    private static final String STATUS_SUCCESS = "SUCCESS";
    private static final String STATUS_PARTIAL_SUCCESS = "PARTIAL_SUCCESS";
    private static final String STATUS_FAILED = "FAILED";
    private static final String JOB_NAME_WEEKLY = "WORK_REPORT_AUTO_GEN_WEEKLY";
    private static final String JOB_NAME_MONTHLY = "WORK_REPORT_AUTO_GEN_MONTHLY";
    private static final String JOB_NAME_PROJECT = "WORK_REPORT_AUTO_GEN_PROJECT";
    private static final String LOCK_KEY_TEMPLATE = "auto-gen:work-report:%s:%s:%s";
    private static final int MAX_FAIL_MESSAGES = 20;

    @Resource
    private WorkReportAutoGenProperties properties;
    @Resource
    private WorkReportCommonService workReportCommonService;
    @Resource
    private WorkReportDefaultDraftService workReportDefaultDraftService;
    @Resource
    private WeeklyReportMapper weeklyReportMapper;
    @Resource
    private MonthlyReportMapper monthlyReportMapper;
    @Resource
    private ProjectReportMapper projectReportMapper;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private JobRunLogMapper jobRunLogMapper;
    @Resource
    private DeptApi deptApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private RedissonClient redissonClient;

    public void autoGenerateWeeklyBySchedule() {
        if (!properties.isEnabled() || !properties.getWeekly().isEnabled()) {
            return;
        }
        LocalDate today = LocalDate.now(ASIA_SHANGHAI);
        AutoGenPeriod period = buildWeeklyPeriod(today);
        runAs(buildSystemLoginUser(), () -> executeWeekly(period, today));
    }

    public void autoGenerateMonthlyBySchedule() {
        if (!properties.isEnabled() || !properties.getMonthly().isEnabled()) {
            return;
        }
        LocalDate today = LocalDate.now(ASIA_SHANGHAI);
        if (!today.equals(today.with(TemporalAdjusters.lastDayOfMonth()))) {
            return;
        }
        AutoGenPeriod period = buildMonthlyPeriod(today);
        runAs(buildSystemLoginUser(), () -> executeMonthly(period, today));
    }

    public void autoGenerateProjectBySchedule() {
        if (!properties.isEnabled() || !properties.getProject().isEnabled()) {
            return;
        }
        LocalDate today = LocalDate.now(ASIA_SHANGHAI);
        if (today.getDayOfMonth() != 15) {
            return;
        }
        AutoGenPeriod period = buildProjectFirstHalfPeriod(today);
        runAs(buildSystemLoginUser(), () -> executeProject(period, today));
    }

    private void executeWeekly(AutoGenPeriod period, LocalDate runDate) {
        JobRunLogDO jobLog = createRunningJobLog(JOB_NAME_WEEKLY, runDate, period.getPeriodKey());
        AutoGenResult result = new AutoGenResult();
        result.setStartedAt(jobLog.getStartedAt());
        try {
            for (AdminUserRespDTO user : loadRdUsers()) {
                processWeeklyUser(period, result, user);
            }
        } catch (Exception ex) {
            appendFailMessage(result, "job=" + JOB_NAME_WEEKLY + ", message=" + ex.getMessage());
            log.error("[executeWeekly][periodKey={}]", period.getPeriodKey(), ex);
        } finally {
            finishJobLog(jobLog, result);
        }
    }

    private void executeMonthly(AutoGenPeriod period, LocalDate runDate) {
        JobRunLogDO jobLog = createRunningJobLog(JOB_NAME_MONTHLY, runDate, period.getPeriodKey());
        AutoGenResult result = new AutoGenResult();
        result.setStartedAt(jobLog.getStartedAt());
        try {
            for (AdminUserRespDTO user : loadRdUsers()) {
                processMonthlyUser(period, result, user);
            }
        } catch (Exception ex) {
            appendFailMessage(result, "job=" + JOB_NAME_MONTHLY + ", message=" + ex.getMessage());
            log.error("[executeMonthly][periodKey={}]", period.getPeriodKey(), ex);
        } finally {
            finishJobLog(jobLog, result);
        }
    }

    private void executeProject(AutoGenPeriod period, LocalDate runDate) {
        JobRunLogDO jobLog = createRunningJobLog(JOB_NAME_PROJECT, runDate, period.getPeriodKey());
        AutoGenResult result = new AutoGenResult();
        result.setStartedAt(jobLog.getStartedAt());
        try {
            for (ProjectAutoGenCandidate candidate : loadProjectCandidates()) {
                processProjectCandidate(period, result, candidate);
            }
        } catch (Exception ex) {
            appendFailMessage(result, "job=" + JOB_NAME_PROJECT + ", message=" + ex.getMessage());
            log.error("[executeProject][periodKey={}]", period.getPeriodKey(), ex);
        } finally {
            finishJobLog(jobLog, result);
        }
    }

    private void processWeeklyUser(AutoGenPeriod period, AutoGenResult result, AdminUserRespDTO user) {
        String subjectKey = String.valueOf(user.getId());
        executeWithLock(WorkReportConstants.REPORT_TYPE_WEEKLY, period.getPeriodKey(), subjectKey, result, () -> {
            if (weeklyReportMapper.selectByReporterIdAndPeriodKey(user.getId(), period.getPeriodKey()) != null) {
                result.setSkipCount(result.getSkipCount() + 1);
                return;
            }
            WeeklyReportSaveReqVO reqVO = runAs(buildLoginUser(user), () -> buildWeeklySaveReqVOFromDefaultDraft(period));
            runAs(buildLoginUser(user), () -> workReportCommonService.createWeeklyReport(reqVO, user.getId()));
            result.setSuccessCount(result.getSuccessCount() + 1);
        }, () -> "userId=" + user.getId());
    }

    private void processMonthlyUser(AutoGenPeriod period, AutoGenResult result, AdminUserRespDTO user) {
        String subjectKey = String.valueOf(user.getId());
        executeWithLock(WorkReportConstants.REPORT_TYPE_MONTHLY, period.getPeriodKey(), subjectKey, result, () -> {
            if (monthlyReportMapper.selectByReporterIdAndPeriodKey(user.getId(), period.getPeriodKey()) != null) {
                result.setSkipCount(result.getSkipCount() + 1);
                return;
            }
            MonthlyReportSaveReqVO reqVO = runAs(buildLoginUser(user), () -> buildMonthlySaveReqVOFromDefaultDraft(period));
            runAs(buildLoginUser(user), () -> workReportCommonService.createMonthlyReport(reqVO, user.getId()));
            result.setSuccessCount(result.getSuccessCount() + 1);
        }, () -> "userId=" + user.getId());
    }

    private void processProjectCandidate(AutoGenPeriod period, AutoGenResult result, ProjectAutoGenCandidate candidate) {
        String subjectKey = candidate.projectId() + ":" + candidate.user().getId();
        executeWithLock(WorkReportConstants.REPORT_TYPE_PROJECT, period.getPeriodKey(), subjectKey, result, () -> {
            if (projectReportMapper.selectByProjectIdAndPeriodKeyAndProjectOwnerId(
                    candidate.projectId(), period.getPeriodKey(), candidate.user().getId()) != null) {
                result.setSkipCount(result.getSkipCount() + 1);
                return;
            }
            ProjectReportSaveReqVO reqVO = runAs(buildLoginUser(candidate.user()),
                    () -> buildProjectSaveReqVOFromDefaultDraft(candidate.projectId(), period));
            runAs(buildLoginUser(candidate.user()), () -> workReportCommonService.createProjectReport(reqVO, candidate.user().getId()));
            result.setSuccessCount(result.getSuccessCount() + 1);
        }, () -> "projectId=" + candidate.projectId() + ", userId=" + candidate.user().getId());
    }

    private WeeklyReportSaveReqVO buildWeeklySaveReqVOFromDefaultDraft(AutoGenPeriod period) {
        WeeklyReportDefaultDraftReqVO draftReqVO = new WeeklyReportDefaultDraftReqVO();
        draftReqVO.setPeriodKey(period.getPeriodKey());
        draftReqVO.setPeriodLabel(period.getPeriodLabel());
        draftReqVO.setPeriodStartDate(period.getPeriodStartDate());
        draftReqVO.setPeriodEndDate(period.getPeriodEndDate());
        WeeklyReportRespVO draft = workReportDefaultDraftService.previewWeeklyDefaultDraft(draftReqVO);

        WeeklyReportSaveReqVO reqVO = new WeeklyReportSaveReqVO();
        reqVO.setPeriodKey(draft.getPeriodKey());
        reqVO.setPeriodLabel(draft.getPeriodLabel());
        reqVO.setPeriodStartDate(draft.getPeriodStartDate());
        reqVO.setPeriodEndDate(draft.getPeriodEndDate());
        reqVO.setIsBusinessTrip(Boolean.TRUE.equals(draft.getIsBusinessTrip()));
        reqVO.setReviewItems(toReviewItemReqList(draft.getReviewItems()));
        reqVO.setPlanItems(toPlanItemReqList(draft.getPlanItems()));
        reqVO.setTravelSegments(toTravelSegmentReqList(draft.getTravelSegments()));
        return reqVO;
    }

    private MonthlyReportSaveReqVO buildMonthlySaveReqVOFromDefaultDraft(AutoGenPeriod period) {
        MonthlyReportDefaultDraftReqVO draftReqVO = new MonthlyReportDefaultDraftReqVO();
        draftReqVO.setPeriodKey(period.getPeriodKey());
        draftReqVO.setPeriodLabel(period.getPeriodLabel());
        draftReqVO.setPeriodStartDate(period.getPeriodStartDate());
        draftReqVO.setPeriodEndDate(period.getPeriodEndDate());
        MonthlyReportRespVO draft = workReportDefaultDraftService.previewMonthlyDefaultDraft(draftReqVO);

        MonthlyReportSaveReqVO reqVO = new MonthlyReportSaveReqVO();
        reqVO.setPeriodKey(draft.getPeriodKey());
        reqVO.setPeriodLabel(draft.getPeriodLabel());
        reqVO.setPeriodStartDate(draft.getPeriodStartDate());
        reqVO.setPeriodEndDate(draft.getPeriodEndDate());
        reqVO.setReviewItems(toReviewItemReqList(draft.getReviewItems()));
        reqVO.setPlanItems(toPlanItemReqList(draft.getPlanItems()));
        return reqVO;
    }

    private ProjectReportSaveReqVO buildProjectSaveReqVOFromDefaultDraft(Long projectId, AutoGenPeriod period) {
        ProjectReportDefaultDraftReqVO draftReqVO = new ProjectReportDefaultDraftReqVO();
        draftReqVO.setPeriodKey(period.getPeriodKey());
        draftReqVO.setPeriodLabel(period.getPeriodLabel());
        draftReqVO.setPeriodStartDate(period.getPeriodStartDate());
        draftReqVO.setPeriodEndDate(period.getPeriodEndDate());
        draftReqVO.setFlag(period.getFlag());
        ProjectReportRespVO draft = workReportDefaultDraftService.previewProjectDefaultDraft(projectId, draftReqVO);

        ProjectReportSaveReqVO reqVO = new ProjectReportSaveReqVO();
        reqVO.setProjectId(projectId);
        reqVO.setPeriodKey(draft.getPeriodKey());
        reqVO.setPeriodLabel(draft.getPeriodLabel());
        reqVO.setPeriodStartDate(draft.getPeriodStartDate());
        reqVO.setPeriodEndDate(draft.getPeriodEndDate());
        reqVO.setFlag(draft.getFlag());
        reqVO.setProjectStatusDesc(draft.getProjectStatusDesc());
        reqVO.setProjectProgressPlan(draft.getProjectProgressPlan());
        reqVO.setProjectKeyPoints(draft.getProjectKeyPoints());
        reqVO.setProjectProblems(draft.getProjectProblems());
        reqVO.setCurrentItems(toProjectItemReqList(draft.getCurrentItems()));
        reqVO.setNextItems(toProjectItemReqList(draft.getNextItems()));
        return reqVO;
    }

    private List<PersonalReportReviewItemReqVO> toReviewItemReqList(List<PersonalReportReviewItemRespVO> source) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        List<PersonalReportReviewItemReqVO> result = new ArrayList<>(source.size());
        for (PersonalReportReviewItemRespVO item : source) {
            if (item == null) {
                continue;
            }
            PersonalReportReviewItemReqVO target = new PersonalReportReviewItemReqVO();
            target.setItemNumber(item.getItemNumber());
            target.setItemTitle(item.getItemTitle());
            target.setWorkHours(item.getWorkHours());
            target.setContentText(item.getContentText());
            target.setContentJson(item.getContentJson());
            target.setReflectionText(item.getReflectionText());
            result.add(target);
        }
        return result;
    }

    private List<PersonalReportPlanItemReqVO> toPlanItemReqList(List<PersonalReportPlanItemRespVO> source) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        List<PersonalReportPlanItemReqVO> result = new ArrayList<>(source.size());
        for (PersonalReportPlanItemRespVO item : source) {
            if (item == null) {
                continue;
            }
            PersonalReportPlanItemReqVO target = new PersonalReportPlanItemReqVO();
            target.setItemNumber(item.getItemNumber());
            target.setItemTitle(item.getItemTitle());
            target.setTargetText(item.getTargetText());
            target.setTargetJson(item.getTargetJson());
            target.setSupportNeed(item.getSupportNeed());
            result.add(target);
        }
        return result;
    }

    private List<ProjectReportItemReqVO> toProjectItemReqList(List<ProjectReportItemRespVO> source) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        List<ProjectReportItemReqVO> result = new ArrayList<>(source.size());
        for (ProjectReportItemRespVO item : source) {
            if (item == null) {
                continue;
            }
            ProjectReportItemReqVO target = new ProjectReportItemReqVO();
            target.setItemTitle(item.getItemTitle());
            target.setWorkHours(item.getWorkHours());
            target.setPriorityCode(item.getPriorityCode());
            target.setProgressRate(item.getProgressRate());
            result.add(target);
        }
        return result;
    }

    private List<WeeklyReportTravelSegmentReqVO> toTravelSegmentReqList(List<WeeklyReportTravelSegmentRespVO> source) {
        if (source == null || source.isEmpty()) {
            return Collections.emptyList();
        }
        List<WeeklyReportTravelSegmentReqVO> result = new ArrayList<>(source.size());
        for (WeeklyReportTravelSegmentRespVO item : source) {
            if (item == null) {
                continue;
            }
            WeeklyReportTravelSegmentReqVO target = new WeeklyReportTravelSegmentReqVO();
            target.setSort(item.getSort());
            target.setStartDate(item.getStartDate());
            target.setEndDate(item.getEndDate());
            target.setTravelDays(item.getTravelDays());
            target.setLocation(item.getLocation());
            result.add(target);
        }
        return result;
    }

    private void executeWithLock(String reportType, String periodKey, String subjectKey, AutoGenResult result,
                                 Runnable action, Supplier<String> failContextSupplier) {
        RLock lock = redissonClient.getLock(String.format(LOCK_KEY_TEMPLATE, reportType, periodKey, subjectKey));
        try {
            if (!lock.tryLock(0, 30, TimeUnit.SECONDS)) {
                result.setSkipCount(result.getSkipCount() + 1);
                return;
            }
            try {
                action.run();
            } catch (ServiceException ex) {
                if (Objects.equals(ex.getCode(), ErrorCodeConstants.WORK_REPORT_PERIOD_DUPLICATE.getCode())) {
                    result.setSkipCount(result.getSkipCount() + 1);
                    return;
                }
                result.setFailCount(result.getFailCount() + 1);
                appendFailMessage(result, failContextSupplier.get() + ", code=" + ex.getCode() + ", message=" + ex.getMessage());
                log.error("[executeWithLock][reportType={}][periodKey={}][subjectKey={}]", reportType, periodKey, subjectKey, ex);
            } catch (Exception ex) {
                result.setFailCount(result.getFailCount() + 1);
                appendFailMessage(result, failContextSupplier.get() + ", message=" + ex.getMessage());
                log.error("[executeWithLock][reportType={}][periodKey={}][subjectKey={}]", reportType, periodKey, subjectKey, ex);
            }
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            result.setFailCount(result.getFailCount() + 1);
            appendFailMessage(result, failContextSupplier.get() + ", message=lock interrupted");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private List<AdminUserRespDTO> loadRdUsers() {
        List<Long> rootDeptIds = properties.getScope().getDeptIds();
        if (rootDeptIds == null || rootDeptIds.isEmpty()) {
            log.warn("[loadRdUsers][未配置 rdms.work-report.auto-gen.scope.dept-ids，自动生成功能跳过]");
            return Collections.emptyList();
        }
        Set<Long> deptIds = new LinkedHashSet<>();
        for (Long rootDeptId : rootDeptIds) {
            if (rootDeptId == null) {
                continue;
            }
            deptIds.add(rootDeptId);
            CommonResult<List<DeptRespDTO>> childResult = deptApi.getChildDeptList(rootDeptId);
            List<DeptRespDTO> childDepts = childResult == null || childResult.getCheckedData() == null
                    ? Collections.emptyList()
                    : childResult.getCheckedData();
            for (DeptRespDTO dept : childDepts) {
                if (dept != null && dept.getId() != null) {
                    deptIds.add(dept.getId());
                }
            }
        }
        if (deptIds.isEmpty()) {
            return Collections.emptyList();
        }
        CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserListByDeptIds(deptIds);
        List<AdminUserRespDTO> users = result == null || result.getCheckedData() == null
                ? Collections.emptyList()
                : result.getCheckedData();
        Map<Long, AdminUserRespDTO> deduplicated = new LinkedHashMap<>();
        for (AdminUserRespDTO user : users) {
            if (user == null || user.getId() == null || !Objects.equals(user.getStatus(), 0)) {
                continue;
            }
            deduplicated.put(user.getId(), user);
        }
        return new ArrayList<>(deduplicated.values());
    }

    private List<ProjectAutoGenCandidate> loadProjectCandidates() {
        List<String> terminalStatusCodes = objectStatusModelMapper
                .selectTerminalStatusCodesByObjectTypeEnabled(ProjectObjectConstants.OBJECT_TYPE);
        List<ProjectDO> projects = projectMapper.selectListByStatusCodesNotIn(terminalStatusCodes);
        if (projects == null || projects.isEmpty()) {
            return Collections.emptyList();
        }
        Set<Long> managerUserIds = projects.stream()
                .map(ProjectDO::getManagerUserId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (managerUserIds.isEmpty()) {
            return Collections.emptyList();
        }
        CommonResult<List<AdminUserRespDTO>> result = adminUserApi.getUserList(managerUserIds);
        List<AdminUserRespDTO> users = result == null || result.getCheckedData() == null
                ? Collections.emptyList()
                : result.getCheckedData();
        Map<Long, AdminUserRespDTO> userMap = users.stream()
                .filter(Objects::nonNull)
                .filter(user -> user.getId() != null)
                .collect(Collectors.toMap(AdminUserRespDTO::getId, user -> user, (left, right) -> left, LinkedHashMap::new));
        List<ProjectAutoGenCandidate> candidates = new ArrayList<>();
        for (ProjectDO project : projects) {
            if (project.getManagerUserId() == null) {
                continue;
            }
            AdminUserRespDTO user = userMap.get(project.getManagerUserId());
            if (user == null || !Objects.equals(user.getStatus(), 0)) {
                continue;
            }
            candidates.add(new ProjectAutoGenCandidate(project.getId(), user));
        }
        return candidates;
    }

    private AutoGenPeriod buildWeeklyPeriod(LocalDate today) {
        LocalDate periodStartDate = today.minusDays(today.getDayOfWeek().getValue() - 1L);
        LocalDate periodEndDate = periodStartDate.plusDays(6);
        return new AutoGenPeriod()
                .setPeriodKey(String.format("weekly-%s-%s", periodStartDate, periodEndDate))
                .setPeriodLabel(String.format("%s 至 %s", periodStartDate, periodEndDate))
                .setPeriodStartDate(periodStartDate)
                .setPeriodEndDate(periodEndDate);
    }

    private AutoGenPeriod buildMonthlyPeriod(LocalDate today) {
        LocalDate periodStartDate = today.withDayOfMonth(1);
        LocalDate periodEndDate = today.withDayOfMonth(today.lengthOfMonth());
        return new AutoGenPeriod()
                .setPeriodKey(String.format("monthly-%s-%s", periodStartDate, periodEndDate))
                .setPeriodLabel(String.format("%d-%02d", today.getYear(), today.getMonthValue()))
                .setPeriodStartDate(periodStartDate)
                .setPeriodEndDate(periodEndDate);
    }

    private AutoGenPeriod buildProjectFirstHalfPeriod(LocalDate today) {
        LocalDate periodStartDate = today.withDayOfMonth(1);
        LocalDate periodEndDate = today.withDayOfMonth(15);
        return new AutoGenPeriod()
                .setPeriodKey(String.format("project-%s-%s-1", periodStartDate, periodEndDate))
                .setPeriodLabel(String.format("%d-%02d 上半月", today.getYear(), today.getMonthValue()))
                .setPeriodStartDate(periodStartDate)
                .setPeriodEndDate(periodEndDate)
                .setFlag(1);
    }

    private JobRunLogDO createRunningJobLog(String jobName, LocalDate runDate, String periodKey) {
        JobRunLogDO jobLog = new JobRunLogDO();
        jobLog.setRunId(UUID.randomUUID().toString().replace("-", ""));
        jobLog.setJobName(jobName);
        jobLog.setJobStatus(STATUS_RUNNING);
        jobLog.setRunDate(runDate);
        jobLog.setPeriodKey(periodKey);
        jobLog.setTriggerSource(TRIGGER_SOURCE_SCHEDULE);
        jobLog.setNodeName(resolveNodeName());
        jobLog.setStartedAt(LocalDateTime.now(ASIA_SHANGHAI));
        jobLog.setSuccessCount(0);
        jobLog.setSkipCount(0);
        jobLog.setFailCount(0);
        jobRunLogMapper.insert(jobLog);
        return jobLog;
    }

    private void finishJobLog(JobRunLogDO jobLog, AutoGenResult result) {
        LocalDateTime finishedAt = LocalDateTime.now(ASIA_SHANGHAI);
        result.setFinishedAt(finishedAt);
        jobLog.setFinishedAt(finishedAt);
        jobLog.setSuccessCount(result.getSuccessCount());
        jobLog.setSkipCount(result.getSkipCount());
        jobLog.setFailCount(result.getFailCount());
        jobLog.setCostMs(java.time.Duration.between(result.getStartedAt(), finishedAt).toMillis());
        jobLog.setJobStatus(resolveJobStatus(result));
        jobLog.setMessage(buildMessage(result.getFailMessages()));
        jobRunLogMapper.updateById(jobLog);
    }

    private String resolveJobStatus(AutoGenResult result) {
        if (result.getFailCount() == 0) {
            return STATUS_SUCCESS;
        }
        if (result.getSuccessCount() > 0 || result.getSkipCount() > 0) {
            return STATUS_PARTIAL_SUCCESS;
        }
        return STATUS_FAILED;
    }

    private String buildMessage(List<String> failMessages) {
        if (failMessages == null || failMessages.isEmpty()) {
            return null;
        }
        String message = JsonUtils.toJsonString(failMessages);
        return message.length() > 2000 ? message.substring(0, 2000) : message;
    }

    private void appendFailMessage(AutoGenResult result, String message) {
        if (result.getFailMessages().size() >= MAX_FAIL_MESSAGES) {
            return;
        }
        result.getFailMessages().add(StringUtils.hasText(message) ? message : "unknown error");
    }

    private LoginUser buildLoginUser(AdminUserRespDTO user) {
        LoginUser loginUser = new LoginUser();
        loginUser.setId(user.getId());
        loginUser.setUserType(UserTypeEnum.ADMIN.getValue());
        Map<String, String> info = new LinkedHashMap<>();
        info.put(LoginUser.INFO_KEY_NICKNAME, safeText(user.getNickname()));
        if (user.getDeptId() != null) {
            info.put(LoginUser.INFO_KEY_DEPT_ID, String.valueOf(user.getDeptId()));
        }
        loginUser.setInfo(info);
        return loginUser;
    }

    private LoginUser buildSystemLoginUser() {
        LoginUser loginUser = new LoginUser();
        loginUser.setId(0L);
        loginUser.setUserType(UserTypeEnum.ADMIN.getValue());
        Map<String, String> info = new LinkedHashMap<>();
        info.put(LoginUser.INFO_KEY_NICKNAME, "system");
        loginUser.setInfo(info);
        return loginUser;
    }

    private void runAs(LoginUser loginUser, Runnable action) {
        runAs(loginUser, () -> {
            action.run();
            return null;
        });
    }

    private <T> T runAs(LoginUser loginUser, Supplier<T> supplier) {
        SecurityContext previousContext = SecurityContextHolder.getContext();
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList()));
        SecurityContextHolder.setContext(context);
        try {
            return supplier.get();
        } finally {
            SecurityContextHolder.setContext(previousContext);
        }
    }

    private String resolveNodeName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (Exception ex) {
            return "unknown";
        }
    }

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

    private record ProjectAutoGenCandidate(Long projectId, AdminUserRespDTO user) {
    }
}
