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

import com.njcn.rdms.framework.common.biz.system.dict.dto.DictDataRespDTO;
import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.module.project.constant.DueAlertConstants;
import com.njcn.rdms.module.project.dal.dataobject.duealert.DueAlertRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectRequirementDO;
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.mysql.duealert.DueAlertRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectRequirementMapper;
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.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.DueAlertObjectTypeEnum;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
import com.njcn.rdms.module.system.api.notify.NotifyMessageSendApi;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageListReqDTO;
import com.njcn.rdms.module.system.api.notify.dto.NotifyUnreadMessageRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 临期/逾期告警扫描编排。无事务，单条发送的事务在 {@link DueAlertSendService} 上。
 *
 * <p>每类对象一次范围查询（计划日 &lt;= maxDate、排除终态+paused），内存里按
 * 「计划日 &lt; today = 逾期 / &gt;= today = 临期」分类，再按记录表两档判重。
 * 逾期档叠加"未读不重发"：接收人上一条逾期提醒未读则本轮对其跳过，已读后次日恢复。
 * 设计见 docs/domains/project/2026-06-12-临期逾期告警-设计.html。</p>
 */
@Service
@Slf4j
public class DueAlertScanServiceImpl implements DueAlertScanService {

    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectRequirementMapper projectRequirementMapper;
    @Resource
    private ProductRequirementMapper productRequirementMapper;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;
    @Resource
    private ProjectTaskMapper projectTaskMapper;
    @Resource
    private PersonalItemMapper personalItemMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private DueAlertRecordMapper dueAlertRecordMapper;
    @Resource
    private DictDataApi dictDataApi;
    @Resource
    private NotifyMessageSendApi notifyMessageSendApi;
    @Resource
    private DueAlertSendService dueAlertSendService;

    @Override
    public void scan(LocalDate today) {
        Map<String, Integer> advanceDays = loadAdvanceDays();
        for (DueAlertObjectTypeEnum type : DueAlertObjectTypeEnum.values()) {
            try {
                scanObjectType(type, today, advanceDays.get(type.getCode()));
            } catch (Exception e) {
                // 单类失败不影响其余类型
                log.error("[scan][对象类型({}) 扫描失败]", type.getCode(), e);
            }
        }
    }

    /** 提前量字典：value=对象类型 code，label=天数；label 非数字按缺档处理（只停临期不停逾期） */
    private Map<String, Integer> loadAdvanceDays() {
        Map<String, Integer> result = new HashMap<>();
        try {
            CommonResult<List<DictDataRespDTO>> dictResult =
                    dictDataApi.getDictDataList(ProjectDictTypeConstants.DUE_ALERT_ADVANCE_DAYS);
            List<DictDataRespDTO> dataList = dictResult == null ? null : dictResult.getCheckedData();
            if (dataList == null) {
                return result;
            }
            for (DictDataRespDTO item : dataList) {
                if (item == null || item.getValue() == null || item.getLabel() == null) {
                    continue;
                }
                try {
                    result.put(item.getValue(), Integer.parseInt(item.getLabel().trim()));
                } catch (NumberFormatException e) {
                    log.warn("[loadAdvanceDays][提前量字典 label 非数字，按缺档处理 value({}) label({})]",
                            item.getValue(), item.getLabel());
                }
            }
        } catch (Exception e) {
            // 字典拉取失败整体降级为"全部缺档"：本轮只发逾期，不中断扫描
            log.warn("[loadAdvanceDays][提前量字典拉取失败，本轮仅扫逾期]", e);
        }
        return result;
    }

    private void scanObjectType(DueAlertObjectTypeEnum type, LocalDate today, Integer advanceDays) {
        // 终态（动态查，空集=不排除）+ paused 暂停豁免
        List<String> excludeStatusCodes = new ArrayList<>(
                objectStatusModelMapper.selectTerminalStatusCodesByObjectTypeEnabled(type.getStatusObjectType()));
        excludeStatusCodes.add(DueAlertConstants.STATUS_PAUSED);
        // 缺档时 maxDate=昨天：候选只剩逾期，临期档自然停发
        LocalDate maxDate = advanceDays != null ? today.plusDays(advanceDays) : today.minusDays(1);

        List<DueAlertCandidate> candidates = loadCandidates(type, maxDate, excludeStatusCodes);
        if (candidates.isEmpty()) {
            return;
        }
        List<DueAlertCandidate> approachingList = candidates.stream()
                .filter(c -> !c.getPlannedEndDate().isBefore(today))
                .collect(Collectors.toList());
        List<DueAlertCandidate> overdueList = candidates.stream()
                .filter(c -> c.getPlannedEndDate().isBefore(today))
                .collect(Collectors.toList());

        int approachingSent = sendApproaching(type, approachingList, today);
        int overdueSent = sendOverdue(type, overdueList, today);
        log.info("[scanObjectType][{} 候选({}) 临期新发({}) 逾期新发({})]",
                type.getCode(), candidates.size(), approachingSent, overdueSent);
    }

    /** 临期判重：同对象+同计划日快照发过即跳过（改期=新快照=重发） */
    private int sendApproaching(DueAlertObjectTypeEnum type, List<DueAlertCandidate> list, LocalDate today) {
        if (list.isEmpty()) {
            return 0;
        }
        List<Long> ids = list.stream().map(DueAlertCandidate::getObjectId).collect(Collectors.toList());
        Set<String> sentKeys = dueAlertRecordMapper.selectApproachingListByObjectIds(type.getCode(), ids).stream()
                .map(r -> r.getObjectId() + "#" + r.getPlannedEndDate())
                .collect(Collectors.toSet());
        int sent = 0;
        for (DueAlertCandidate candidate : list) {
            if (sentKeys.contains(candidate.getObjectId() + "#" + candidate.getPlannedEndDate())) {
                continue;
            }
            if (hasNoRecipient(candidate)) {
                continue;
            }
            dueAlertSendService.sendAlert(candidate, DueAlertConstants.ALERT_TYPE_APPROACHING, today);
            sent++;
        }
        return sent;
    }

    /**
     * 逾期判重：当日发过即跳过（昨天的记录不命中=每天一条）。
     * 叠加"未读不重发"：某接收人对该对象的逾期提醒还未读时，本轮跳过该人；
     * 全部接收人都被跳过则整条不发、不写记录（读完后次日自然恢复）。
     */
    private int sendOverdue(DueAlertObjectTypeEnum type, List<DueAlertCandidate> list, LocalDate today) {
        if (list.isEmpty()) {
            return 0;
        }
        List<Long> ids = list.stream().map(DueAlertCandidate::getObjectId).collect(Collectors.toList());
        Set<Long> sentTodayIds = dueAlertRecordMapper
                .selectOverdueListByObjectIdsAndAlertDate(type.getCode(), ids, today).stream()
                .map(DueAlertRecordDO::getObjectId)
                .collect(Collectors.toSet());
        Set<String> unreadKeys = loadUnreadOverdueKeys(type, list);
        int sent = 0;
        for (DueAlertCandidate candidate : list) {
            if (sentTodayIds.contains(candidate.getObjectId())) {
                continue;
            }
            DueAlertCandidate filtered = excludeUnreadRecipients(candidate, unreadKeys);
            if (hasNoRecipient(filtered)) {
                continue;
            }
            dueAlertSendService.sendAlert(filtered, DueAlertConstants.ALERT_TYPE_OVERDUE, today);
            sent++;
        }
        return sent;
    }

    /**
     * 拉取本批接收人对这批对象的未读逾期消息，组装 "userId#objectId" 键集。
     * 按消息参数里的对象标识匹配（历史消息无该参数 → 不拦截，照发一次后进入新口径）；
     * 查询失败整体降级为"无未读"（照发，最坏退回旧的每天一条行为，不丢催办）。
     */
    private Set<String> loadUnreadOverdueKeys(DueAlertObjectTypeEnum type, List<DueAlertCandidate> list) {
        Set<Long> userIds = new HashSet<>();
        for (DueAlertCandidate candidate : list) {
            if (candidate.getOwnerUserIds() != null) {
                userIds.addAll(candidate.getOwnerUserIds());
            }
            if (candidate.getAssigneeUserIds() != null) {
                userIds.addAll(candidate.getAssigneeUserIds());
            }
        }
        if (userIds.isEmpty()) {
            return Set.of();
        }
        try {
            CommonResult<List<NotifyUnreadMessageRespDTO>> result =
                    notifyMessageSendApi.getUnreadNotifyMessageListByTemplateCodes(
                            new NotifyUnreadMessageListReqDTO()
                                    .setUserIds(new ArrayList<>(userIds))
                                    .setTemplateCodes(List.of(NotifyTemplateCodeConstants.DUE_ALERT_OVERDUE_OWNER,
                                            NotifyTemplateCodeConstants.DUE_ALERT_OVERDUE_ASSIGNEE)));
            List<NotifyUnreadMessageRespDTO> messages = result == null ? null : result.getCheckedData();
            if (messages == null) {
                return Set.of();
            }
            Set<String> keys = new HashSet<>();
            for (NotifyUnreadMessageRespDTO message : messages) {
                Map<String, Object> params = message.getTemplateParams();
                if (message.getUserId() == null || params == null) {
                    continue;
                }
                Object objectId = params.get(DueAlertConstants.PARAM_OBJECT_ID);
                // 同一模板编码跨六类对象共用，必须连对象类型一起匹配
                if (objectId == null
                        || !type.getCode().equals(params.get(DueAlertConstants.PARAM_OBJECT_TYPE))) {
                    continue;
                }
                keys.add(message.getUserId() + "#" + objectId);
            }
            return keys;
        } catch (Exception e) {
            log.warn("[loadUnreadOverdueKeys][未读消息查询失败，本轮按无未读处理（照发）objectType({})]",
                    type.getCode(), e);
            return Set.of();
        }
    }

    /** 按 "userId#objectId" 键剔除还压着未读逾期提醒的接收人（不改原 candidate） */
    private DueAlertCandidate excludeUnreadRecipients(DueAlertCandidate candidate, Set<String> unreadKeys) {
        if (unreadKeys.isEmpty()) {
            return candidate;
        }
        String objectIdText = String.valueOf(candidate.getObjectId());
        return buildCandidate(candidate.getObjectType(), candidate.getObjectId(), candidate.getObjectName(),
                candidate.getPlannedEndDate(),
                excludeUnreadUsers(candidate.getOwnerUserIds(), objectIdText, unreadKeys),
                excludeUnreadUsers(candidate.getAssigneeUserIds(), objectIdText, unreadKeys));
    }

    private List<Long> excludeUnreadUsers(List<Long> userIds, String objectIdText, Set<String> unreadKeys) {
        if (userIds == null || userIds.isEmpty()) {
            return List.of();
        }
        return userIds.stream()
                .filter(userId -> !unreadKeys.contains(userId + "#" + objectIdText))
                .collect(Collectors.toList());
    }

    /** 两组接收人都为空：跳过且不写记录（人补上后次日还能发） */
    private boolean hasNoRecipient(DueAlertCandidate candidate) {
        boolean noOwner = candidate.getOwnerUserIds() == null || candidate.getOwnerUserIds().isEmpty();
        boolean noAssignee = candidate.getAssigneeUserIds() == null || candidate.getAssigneeUserIds().isEmpty();
        return noOwner && noAssignee;
    }

    private List<DueAlertCandidate> loadCandidates(DueAlertObjectTypeEnum type, LocalDate maxDate,
                                                    List<String> excludeStatusCodes) {
        switch (type) {
            case PROJECT:
                return projectMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream()
                        .map(this::toCandidate).collect(Collectors.toList());
            case PROJECT_REQUIREMENT:
                return projectRequirementMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream()
                        .map(this::toCandidate).collect(Collectors.toList());
            case PRODUCT_REQUIREMENT:
                return productRequirementMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream()
                        .map(this::toCandidate).collect(Collectors.toList());
            case EXECUTION:
                return projectExecutionMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream()
                        .map(this::toCandidate).collect(Collectors.toList());
            case TASK:
                return projectTaskMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream()
                        .map(this::toCandidate).collect(Collectors.toList());
            case PERSONAL_ITEM:
                return personalItemMapper.selectDueAlertCandidateList(maxDate, excludeStatusCodes).stream()
                        .map(this::toCandidate).collect(Collectors.toList());
            default:
                return List.of();
        }
    }

    private DueAlertCandidate toCandidate(ProjectDO project) {
        return buildCandidate(DueAlertObjectTypeEnum.PROJECT, project.getId(), project.getProjectName(),
                project.getPlannedEndDate(), singletonOrEmpty(project.getManagerUserId()), List.of());
    }

    /** 需求接收人：当前处理人优先，空则提出人（设计定稿） */
    private DueAlertCandidate toCandidate(ProjectRequirementDO requirement) {
        Long recipient = requirement.getCurrentHandlerUserId() != null
                ? requirement.getCurrentHandlerUserId() : requirement.getProposerId();
        return buildCandidate(DueAlertObjectTypeEnum.PROJECT_REQUIREMENT, requirement.getId(),
                requirement.getTitle(), requirement.getExpectedTime(), singletonOrEmpty(recipient), List.of());
    }

    private DueAlertCandidate toCandidate(ProductRequirementDO requirement) {
        Long recipient = requirement.getCurrentHandlerUserId() != null
                ? requirement.getCurrentHandlerUserId() : requirement.getProposerId();
        return buildCandidate(DueAlertObjectTypeEnum.PRODUCT_REQUIREMENT, requirement.getId(),
                requirement.getTitle(), requirement.getExpectedTime(), singletonOrEmpty(recipient), List.of());
    }

    private DueAlertCandidate toCandidate(PersonalItemDO item) {
        return buildCandidate(DueAlertObjectTypeEnum.PERSONAL_ITEM, item.getId(), item.getTaskTitle(),
                item.getPlannedEndDate(), singletonOrEmpty(item.getOwnerId()), List.of());
    }

    /** 任务：仅通知负责人（2026-07-15 定稿，协办人不再接收临期/逾期告警） */
    private DueAlertCandidate toCandidate(ProjectTaskDO task) {
        return buildCandidate(DueAlertObjectTypeEnum.TASK, task.getId(), task.getTaskTitle(),
                task.getPlannedEndDate(), singletonOrEmpty(task.getOwnerId()), List.of());
    }

    /** 执行：仅通知负责人（同任务口径） */
    private DueAlertCandidate toCandidate(ProjectExecutionDO execution) {
        return buildCandidate(DueAlertObjectTypeEnum.EXECUTION, execution.getId(), execution.getExecutionName(),
                execution.getPlannedEndDate(), singletonOrEmpty(execution.getOwnerId()), List.of());
    }

    private DueAlertCandidate buildCandidate(DueAlertObjectTypeEnum type, Long objectId, String objectName,
                                              LocalDate plannedEndDate, List<Long> owners, List<Long> assignees) {
        DueAlertCandidate candidate = new DueAlertCandidate();
        candidate.setObjectType(type);
        candidate.setObjectId(objectId);
        candidate.setObjectName(objectName);
        candidate.setPlannedEndDate(plannedEndDate);
        candidate.setOwnerUserIds(owners);
        candidate.setAssigneeUserIds(assignees);
        return candidate;
    }

    private List<Long> singletonOrEmpty(Long userId) {
        return userId == null ? List.of() : List.of(userId);
    }

}
