package com.njcn.rdms.module.project.dal.mysql.project.task;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.mybatis.core.dataobject.BaseDO;
import com.njcn.rdms.framework.mybatis.core.mapper.BaseMapperX;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskStatusBoardReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.ProjectTaskPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregatePageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.aggregate.ProjectTaskAggregateStatusBoardReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.service.project.permission.VisibilityScope;
import lombok.Data;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@Mapper
public interface ProjectTaskMapper extends BaseMapperX<ProjectTaskDO> {

    default ProjectTaskDO selectByProjectIdAndExecutionIdAndId(Long projectId, Long executionId, Long taskId) {
        return selectOne(new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getProjectId, projectId)
                .eq(ProjectTaskDO::getExecutionId, executionId)
                .eq(ProjectTaskDO::getId, taskId));
    }

    default PageResult<ProjectTaskDO> selectPageByExecutionId(Long projectId, Long executionId,
                                                              VisibilityScope scope,
                                                              ProjectTaskPageReqVO reqVO) {
        // 可见性短路：非 seesAll 且无任何可见任务 → 空页
        if (!scope.seesAll() && scope.taskIds().isEmpty()) {
            return PageResult.empty();
        }
        LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<>();
        queryWrapper.eq(ProjectTaskDO::getProjectId, projectId);
        queryWrapper.eq(ProjectTaskDO::getExecutionId, executionId);
        queryWrapper.eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId());
        queryWrapper.eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId());
        queryWrapper.eqIfPresent(ProjectTaskDO::getStatusCode, reqVO.getStatusCode());
        queryWrapper.eqIfPresent(ProjectTaskDO::getPriority, reqVO.getPriority());
        queryWrapper.betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
        queryWrapper.orderByAsc(ProjectTaskDO::getParentTaskId);
        queryWrapper.orderByAsc(ProjectTaskDO::getPriority);
        queryWrapper.orderByDesc(BaseDO::getCreateTime);
        queryWrapper.orderByDesc(ProjectTaskDO::getId);
        if (StringUtils.hasText(reqVO.getKeyword())) {
            queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
        }
        if (!scope.seesAll()) {
            queryWrapper.in(ProjectTaskDO::getId, scope.taskIds());
        }
        return selectPage(reqVO, queryWrapper);
    }

    default int updateStatusByIdAndStatus(Long id, String fromStatus, String toStatus, String lastStatusReason) {
        ProjectTaskDO update = new ProjectTaskDO();
        update.setStatusCode(toStatus);
        update.setLastStatusReason(lastStatusReason);
        return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getId, id)
                .eq(ProjectTaskDO::getStatusCode, fromStatus));
    }

    /**
     * 软删任务（按状态 CAS）。仅在 statusCode 与传入 fromStatus 匹配时才标 deleted=1。
     * 返回 1 表示成功；返回 0 视为并发修改。
     */
    default int deleteByIdAndStatus(Long id, String fromStatus) {
        return delete(new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getId, id)
                .eq(ProjectTaskDO::getStatusCode, fromStatus));
    }

    /**
     * 仅更新实际开始/结束日期。null 字段依据全局 FieldStrategy 不会被覆盖。
     */
    default int updateActualDatesById(Long id, LocalDate actualStartDate, LocalDate actualEndDate) {
        if (actualStartDate == null && actualEndDate == null) {
            return 0;
        }
        ProjectTaskDO update = new ProjectTaskDO();
        update.setActualStartDate(actualStartDate);
        update.setActualEndDate(actualEndDate);
        return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getId, id));
    }

    default Integer countChildrenNotInStatus(Long parentTaskId, List<String> terminalStatusCodes) {
        LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getParentTaskId, parentTaskId);
        if (terminalStatusCodes != null && !terminalStatusCodes.isEmpty()) {
            queryWrapper.notIn(ProjectTaskDO::getStatusCode, terminalStatusCodes);
        }
        return Math.toIntExact(selectCount(queryWrapper));
    }

    /**
     * 统计指定执行下处于非终态的任务数。用于执行 complete 前置校验（要求所有任务必须终态）。
     */
    default Integer countByExecutionIdNotInStatus(Long executionId, List<String> terminalStatusCodes) {
        LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getExecutionId, executionId);
        if (terminalStatusCodes != null && !terminalStatusCodes.isEmpty()) {
            queryWrapper.notIn(ProjectTaskDO::getStatusCode, terminalStatusCodes);
        }
        return Math.toIntExact(selectCount(queryWrapper));
    }

    /**
     * 数指定父任务下的直接子任务（不区分状态）。用于"是否叶子任务"判定与进度汇总。
     */
    default int countChildrenByParentTaskId(Long parentTaskId) {
        if (parentTaskId == null) {
            return 0;
        }
        return Math.toIntExact(selectCount(new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getParentTaskId, parentTaskId)));
    }

    /**
     * 取直接子任务的 progressRate 列表（用于父任务进度简单平均汇总）。
     * 只读取必要字段，避免拉回整行；逻辑删除的子任务由 BaseMapper 自动过滤。
     */
    default List<ProjectTaskDO> selectChildrenProgressByParentTaskId(Long parentTaskId,
                                                                     Collection<String> excludedStatusCodes) {
        if (parentTaskId == null) {
            return Collections.emptyList();
        }
        LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<>();
        queryWrapper.select(ProjectTaskDO::getId, ProjectTaskDO::getProgressRate);
        queryWrapper.eq(ProjectTaskDO::getParentTaskId, parentTaskId);
        if (excludedStatusCodes != null && !excludedStatusCodes.isEmpty()) {
            queryWrapper.notIn(ProjectTaskDO::getStatusCode, excludedStatusCodes);
        }
        return selectList(queryWrapper);
    }

    /**
     * 执行详情进度：按当前执行下一级任务 progressRate 简单平均；无一级任务时 SQL 返回 null。
     */
    @Select("""
            <script>
            SELECT AVG(COALESCE(progress_rate, 0))
            FROM rdms_task
            WHERE deleted = b'0'
              AND project_id = #{projectId}
              AND execution_id = #{executionId}
              AND parent_task_id IS NULL
              <if test="excludedStatusCodes != null and excludedStatusCodes.size() > 0">
              AND status_code NOT IN
                <foreach collection="excludedStatusCodes" item="statusCode" open="(" separator="," close=")">#{statusCode}</foreach>
              </if>
            </script>
            """)
    BigDecimal selectRootTaskAvgProgressByExecutionId(@Param("projectId") Long projectId,
                                                      @Param("executionId") Long executionId,
                                                      @Param("excludedStatusCodes") Collection<String> excludedStatusCodes);

    /**
     * 执行分页进度：按当前页 executionId 批量聚合一级任务 progressRate，避免列表 N+1。
     */
    @Select("""
            <script>
            SELECT execution_id AS executionId, AVG(COALESCE(progress_rate, 0)) AS progressRate
            FROM rdms_task
            WHERE deleted = b'0'
              AND project_id = #{projectId}
              AND execution_id IN
                <foreach collection="executionIds" item="id" open="(" separator="," close=")">#{id}</foreach>
              AND parent_task_id IS NULL
              <if test="excludedStatusCodes != null and excludedStatusCodes.size() > 0">
              AND status_code NOT IN
                <foreach collection="excludedStatusCodes" item="statusCode" open="(" separator="," close=")">#{statusCode}</foreach>
              </if>
            GROUP BY execution_id
            </script>
            """)
    List<Map<String, Object>> selectRootTaskAvgProgressGroupByExecutionIds(
            @Param("projectId") Long projectId,
            @Param("executionIds") Collection<Long> executionIds,
            @Param("excludedStatusCodes") Collection<String> excludedStatusCodes);

    /**
     * 执行详情完成态聚合：返回根任务总数与"已完成"数，用于 execution complete 按钮判定。
     * 与 selectRootTaskAvgProgressByExecutionId 共用同一根任务筛选口径（execution_id + parent_task_id IS NULL + excludedStatusCodes）。
     * 业务侧判定 totals > 0 && totals == completedCount 即视为"根任务全部已完成"，空集（totals = 0）按"不全部完成"处理。
     */
    @Select("""
            <script>
            SELECT COUNT(*) AS totals,
                   SUM(CASE WHEN status_code = #{completedStatusCode} THEN 1 ELSE 0 END) AS completedCount
            FROM rdms_task
            WHERE deleted = b'0'
              AND project_id = #{projectId}
              AND execution_id = #{executionId}
              AND parent_task_id IS NULL
              <if test="excludedStatusCodes != null and excludedStatusCodes.size() > 0">
              AND status_code NOT IN
                <foreach collection="excludedStatusCodes" item="statusCode" open="(" separator="," close=")">#{statusCode}</foreach>
              </if>
            </script>
            """)
    Map<String, Object> selectRootTaskCompletionStateByExecutionId(@Param("projectId") Long projectId,
                                                                   @Param("executionId") Long executionId,
                                                                   @Param("completedStatusCode") String completedStatusCode,
                                                                   @Param("excludedStatusCodes") Collection<String> excludedStatusCodes);

    /**
     * 执行分页完成态批量聚合：按 executionId 一次性返回 (totals, completedCount)，避免列表 N+1。
     * 筛选口径与 selectRootTaskCompletionStateByExecutionId 同源。
     */
    @Select("""
            <script>
            SELECT execution_id AS executionId,
                   COUNT(*) AS totals,
                   SUM(CASE WHEN status_code = #{completedStatusCode} THEN 1 ELSE 0 END) AS completedCount
            FROM rdms_task
            WHERE deleted = b'0'
              AND project_id = #{projectId}
              AND execution_id IN
                <foreach collection="executionIds" item="id" open="(" separator="," close=")">#{id}</foreach>
              AND parent_task_id IS NULL
              <if test="excludedStatusCodes != null and excludedStatusCodes.size() > 0">
              AND status_code NOT IN
                <foreach collection="excludedStatusCodes" item="statusCode" open="(" separator="," close=")">#{statusCode}</foreach>
              </if>
            GROUP BY execution_id
            </script>
            """)
    List<Map<String, Object>> selectRootTaskCompletionStateGroupByExecutionIds(
            @Param("projectId") Long projectId,
            @Param("executionIds") Collection<Long> executionIds,
            @Param("completedStatusCode") String completedStatusCode,
            @Param("excludedStatusCodes") Collection<String> excludedStatusCodes);

    /**
     * 仅更新单个任务的 progressRate，不动其他字段（避免污染 lastStatusReason 等）。
     */
    default int updateProgressRateById(Long id, BigDecimal progressRate) {
        ProjectTaskDO update = new ProjectTaskDO();
        update.setProgressRate(progressRate);
        return update(update, new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getId, id));
    }

    /**
     * 递归 CTE：从"userId 在指定项目下作为 owner_id 的全部任务"出发，向下展开包含所有子孙的任务 ID。
     * 用于 VisibilityScopeResolver 中"任务负责人 → 自己 + 全部子孙"规则的 scope 收集。
     *
     * 任务表已逻辑删除的行不参与递归（WHERE 子句过滤 deleted）。
     * 单棵子树最大深度受 MySQL `cte_max_recursion_depth`（默认 1000）限制，业务实际任务树远低于此。
     */
    @Select("""
            WITH RECURSIVE owned (id) AS (
                SELECT id FROM rdms_task
                 WHERE deleted = b'0'
                   AND project_id = #{projectId}
                   AND owner_id   = #{userId}
                UNION ALL
                SELECT t.id FROM rdms_task t
                  JOIN owned o ON t.parent_task_id = o.id
                 WHERE t.deleted = b'0'
            )
            SELECT id FROM owned
            """)
    List<Long> selectOwnedTaskAndDescendantIdsByProjectIdAndUserId(
            @Param("projectId") Long projectId,
            @Param("userId") Long userId);

    /**
     * 同 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 但再加 execution_id 维度。
     * 注意：递归向下展开只跟着 parent_task_id，子任务必然与父任务在同一 execution 下，
     * 因此 execution_id 过滤仅作用于种子（owned）那一步即可。
     */
    @Select("""
            WITH RECURSIVE owned (id) AS (
                SELECT id FROM rdms_task
                 WHERE deleted = b'0'
                   AND project_id   = #{projectId}
                   AND execution_id = #{executionId}
                   AND owner_id     = #{userId}
                UNION ALL
                SELECT t.id FROM rdms_task t
                  JOIN owned o ON t.parent_task_id = o.id
                 WHERE t.deleted = b'0'
            )
            SELECT id FROM owned
            """)
    List<Long> selectOwnedTaskAndDescendantIdsByExecutionIdAndUserId(
            @Param("projectId") Long projectId,
            @Param("executionId") Long executionId,
            @Param("userId") Long userId);

    default Integer countByProjectIdAndExecutionIdAndStatusCode(Long projectId, Long executionId,
                                                                VisibilityScope scope,
                                                                ProjectTaskStatusBoardReqVO reqVO,
                                                                String statusCode) {
        // 可见性短路：非 seesAll 且无任何可见任务 → 0
        if (!scope.seesAll() && scope.taskIds().isEmpty()) {
            return 0;
        }
        LambdaQueryWrapperX<ProjectTaskDO> queryWrapper = new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getProjectId, projectId)
                .eq(ProjectTaskDO::getExecutionId, executionId)
                .eq(ProjectTaskDO::getStatusCode, statusCode)
                .eqIfPresent(ProjectTaskDO::getParentTaskId, reqVO.getParentTaskId())
                .eqIfPresent(ProjectTaskDO::getOwnerId, reqVO.getOwnerId())
                .betweenIfPresent(BaseDO::getUpdateTime, reqVO.getUpdateTime());
        if (StringUtils.hasText(reqVO.getKeyword())) {
            queryWrapper.and(wrapper -> wrapper.like(ProjectTaskDO::getTaskTitle, reqVO.getKeyword()));
        }
        if (!scope.seesAll()) {
            queryWrapper.in(ProjectTaskDO::getId, scope.taskIds());
        }
        return Math.toIntExact(selectCount(queryWrapper));
    }

    /**
     * 收集执行下的所有任务 id（含子孙——子孙必然同 execution_id，所以一把抓即可）。
     * 用于"删除执行"时的级联软删。
     */
    default List<Long> selectIdsByExecutionId(Long executionId) {
        if (executionId == null) {
            return Collections.emptyList();
        }
        return selectList(new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getExecutionId, executionId)
                .select(ProjectTaskDO::getId))
                .stream()
                .map(ProjectTaskDO::getId)
                .toList();
    }

    /**
     * 从给定任务出发，递归向下收集自身 + 所有子孙任务 id（递归 CTE）。
     * 用于"删除任务"时的级联软删。复用与 selectOwnedTaskAndDescendantIdsByProjectIdAndUserId 同款 CTE 模式。
     *
     * 任务表已逻辑删除的行不参与递归。
     */
    @Select("""
            WITH RECURSIVE descendants (id) AS (
                SELECT id FROM rdms_task
                 WHERE deleted = b'0'
                   AND id = #{taskId}
                UNION ALL
                SELECT t.id FROM rdms_task t
                  JOIN descendants d ON t.parent_task_id = d.id
                 WHERE t.deleted = b'0'
            )
            SELECT id FROM descendants
            """)
    List<Long> selectSelfAndDescendantIds(@Param("taskId") Long taskId);

    /**
     * 按 id 集合批量软删任务。@TableLogic 自动落 deleted=1。
     */
    default int deleteByIdIn(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return 0;
        }
        return delete(new LambdaQueryWrapperX<ProjectTaskDO>()
                .in(ProjectTaskDO::getId, ids));
    }

    /**
     * 执行下任务计数（用于删除预检）。
     */
    default int countByExecutionId(Long executionId) {
        if (executionId == null) {
            return 0;
        }
        return Math.toIntExact(selectCount(new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getExecutionId, executionId)));
    }

    // ======================== 项目级跨执行聚合查询 ========================

    /**
     * 项目级跨执行任务分页查询。
     *
     * 语义:
     * - scope.seesAll() = true → 不附加 taskIds 过滤
     * - scope.seesAll() = false → 附加 t.id IN (scope.taskIds()) 短路过滤
     * - involveUserId 不为 null → 附加 (t.owner_id = ? OR exists active assignee user_id = ?)
     * - statusCodes 非空 → t.status_code IN (...)
     * - dueRange='overdue' 且 terminalStatusCodes 非空 → 排除终态
     */
    @Select("""
            <script>
            SELECT t.*
            FROM rdms_task t
            <where>
                t.project_id = #{projectId}
                AND t.deleted = b'0'
                <if test="!seesAll and visibleTaskIds != null and !visibleTaskIds.isEmpty()">
                    AND t.id IN
                    <foreach collection="visibleTaskIds" item="tid" open="(" separator="," close=")">#{tid}</foreach>
                </if>
                <if test="!seesAll and (visibleTaskIds == null or visibleTaskIds.isEmpty())">
                    AND 1=0
                </if>
                <if test="reqVO.keyword != null and reqVO.keyword != ''">
                    AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
                </if>
                <if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
                    AND t.execution_id IN
                    <foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
                </if>
                <if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
                    AND EXISTS (
                        SELECT 1 FROM rdms_project_execution e
                        WHERE e.id = t.execution_id
                          AND e.deleted = b'0'
                          AND e.status_code IN
                          <foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
                    )
                </if>
                <if test="reqVO.involveUserId != null">
                    AND (
                        t.owner_id = #{reqVO.involveUserId}
                        OR EXISTS (
                            SELECT 1 FROM rdms_task_assignee a
                            WHERE a.task_id = t.id
                              AND a.user_id = #{reqVO.involveUserId}
                              AND a.removed_at IS NULL
                              AND a.deleted = b'0'
                        )
                    )
                </if>
                <if test="reqVO.ownerId != null">
                    AND t.owner_id = #{reqVO.ownerId}
                </if>
                <if test="reqVO.statusCodes != null and !reqVO.statusCodes.isEmpty()">
                    AND t.status_code IN
                    <foreach collection="reqVO.statusCodes" item="sc" open="(" separator="," close=")">#{sc}</foreach>
                </if>
                <if test="reqVO.priority != null and reqVO.priority != ''">
                    AND t.priority = #{reqVO.priority}
                </if>
                <if test="reqVO.parentTaskId != null">
                    AND t.parent_task_id = #{reqVO.parentTaskId}
                </if>
                <if test="reqVO.dueRange == 'overdue'">
                    AND t.planned_end_date &lt; #{today}
                    <if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
                        AND t.status_code NOT IN
                        <foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
                    </if>
                </if>
                <if test="reqVO.dueRange == 'today'">
                    AND t.planned_end_date = #{today}
                </if>
                <if test="reqVO.dueRange == 'thisWeek'">
                    AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
                </if>
                <if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
                    AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
                </if>
            </where>
            <choose>
                <when test="reqVO.sortBy == 'priority'">ORDER BY t.priority</when>
                <when test="reqVO.sortBy == 'updateTime'">ORDER BY t.update_time</when>
                <when test="reqVO.sortBy == 'createTime'">ORDER BY t.create_time</when>
                <otherwise>ORDER BY t.planned_end_date</otherwise>
            </choose>
            <choose>
                <when test="reqVO.sortOrder == 'desc'">DESC</when>
                <otherwise>ASC</otherwise>
            </choose>
            , t.id DESC
            </script>
            """)
    IPage<ProjectTaskDO> selectAggregatePageByProjectId(
            @Param("projectId") Long projectId,
            @Param("seesAll") boolean seesAll,
            @Param("visibleTaskIds") java.util.Collection<Long> visibleTaskIds,
            @Param("reqVO") ProjectTaskAggregatePageReqVO reqVO,
            @Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
            @Param("weekStart") LocalDate weekStart,
            @Param("weekEnd") LocalDate weekEnd,
            @Param("today") LocalDate today,
            Page<ProjectTaskDO> page);

    /**
     * 项目级跨执行任务按状态分组计数(status-board)。
     * 入参同 page 但去除分页 / sort / statusCodes 筛选。
     */
    @Select("""
            <script>
            SELECT t.status_code AS statusCode, COUNT(*) AS count
            FROM rdms_task t
            <where>
                t.project_id = #{projectId}
                AND t.deleted = b'0'
                <if test="!seesAll and visibleTaskIds != null and !visibleTaskIds.isEmpty()">
                    AND t.id IN
                    <foreach collection="visibleTaskIds" item="tid" open="(" separator="," close=")">#{tid}</foreach>
                </if>
                <if test="!seesAll and (visibleTaskIds == null or visibleTaskIds.isEmpty())">
                    AND 1=0
                </if>
                <if test="reqVO.keyword != null and reqVO.keyword != ''">
                    AND t.task_title LIKE CONCAT('%', #{reqVO.keyword}, '%')
                </if>
                <if test="reqVO.executionIds != null and !reqVO.executionIds.isEmpty()">
                    AND t.execution_id IN
                    <foreach collection="reqVO.executionIds" item="eid" open="(" separator="," close=")">#{eid}</foreach>
                </if>
                <if test="reqVO.executionStatusCodes != null and !reqVO.executionStatusCodes.isEmpty()">
                    AND EXISTS (
                        SELECT 1 FROM rdms_project_execution e
                        WHERE e.id = t.execution_id
                          AND e.deleted = b'0'
                          AND e.status_code IN
                          <foreach collection="reqVO.executionStatusCodes" item="esc" open="(" separator="," close=")">#{esc}</foreach>
                    )
                </if>
                <if test="reqVO.involveUserId != null">
                    AND (
                        t.owner_id = #{reqVO.involveUserId}
                        OR EXISTS (
                            SELECT 1 FROM rdms_task_assignee a
                            WHERE a.task_id = t.id
                              AND a.user_id = #{reqVO.involveUserId}
                              AND a.removed_at IS NULL
                              AND a.deleted = b'0'
                        )
                    )
                </if>
                <if test="reqVO.ownerId != null">
                    AND t.owner_id = #{reqVO.ownerId}
                </if>
                <if test="reqVO.priority != null and reqVO.priority != ''">
                    AND t.priority = #{reqVO.priority}
                </if>
                <if test="reqVO.parentTaskId != null">
                    AND t.parent_task_id = #{reqVO.parentTaskId}
                </if>
                <if test="reqVO.dueRange == 'overdue'">
                    AND t.planned_end_date &lt; #{today}
                    <if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
                        AND t.status_code NOT IN
                        <foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
                    </if>
                </if>
                <if test="reqVO.dueRange == 'today'">
                    AND t.planned_end_date = #{today}
                </if>
                <if test="reqVO.dueRange == 'thisWeek'">
                    AND t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
                </if>
                <if test="reqVO.updateTime != null and reqVO.updateTime.length == 2">
                    AND t.update_time BETWEEN #{reqVO.updateTime[0]} AND #{reqVO.updateTime[1]}
                </if>
            </where>
            GROUP BY t.status_code
            </script>
            """)
    List<StatusCountRow> selectAggregateStatusCount(
            @Param("projectId") Long projectId,
            @Param("seesAll") boolean seesAll,
            @Param("visibleTaskIds") java.util.Collection<Long> visibleTaskIds,
            @Param("reqVO") ProjectTaskAggregateStatusBoardReqVO reqVO,
            @Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
            @Param("weekStart") LocalDate weekStart,
            @Param("weekEnd") LocalDate weekEnd,
            @Param("today") LocalDate today);

    /**
     * summary 的 4 个数字一次查出来,避免 4 次扫表。
     *
     * 返回 Map 结构:
     *   overdue → Long
     *   dueToday → Long
     *   dueThisWeek → Long
     *   doneThisWeek → Long
     */
    @Select("""
            <script>
            SELECT
                CAST(SUM(CASE WHEN t.planned_end_date &lt; #{today}
                    <if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
                        AND t.status_code NOT IN
                        <foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
                    </if>
                    THEN 1 ELSE 0 END) AS SIGNED) AS overdue,
                CAST(SUM(CASE WHEN t.planned_end_date = #{today}
                    <if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
                        AND t.status_code NOT IN
                        <foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
                    </if>
                    THEN 1 ELSE 0 END) AS SIGNED) AS dueToday,
                CAST(SUM(CASE WHEN t.planned_end_date BETWEEN #{weekStart} AND #{weekEnd}
                    <if test="terminalStatusCodes != null and !terminalStatusCodes.isEmpty()">
                        AND t.status_code NOT IN
                        <foreach collection="terminalStatusCodes" item="tc" open="(" separator="," close=")">#{tc}</foreach>
                    </if>
                    THEN 1 ELSE 0 END) AS SIGNED) AS dueThisWeek,
                CAST(SUM(CASE WHEN t.actual_end_date BETWEEN #{weekStart} AND #{today}
                    AND t.status_code = #{completedStatusCode}
                    THEN 1 ELSE 0 END) AS SIGNED) AS doneThisWeek
            FROM rdms_task t
            <where>
                t.project_id = #{projectId}
                AND t.deleted = b'0'
                <if test="!seesAll and visibleTaskIds != null and !visibleTaskIds.isEmpty()">
                    AND t.id IN
                    <foreach collection="visibleTaskIds" item="tid" open="(" separator="," close=")">#{tid}</foreach>
                </if>
                <if test="!seesAll and (visibleTaskIds == null or visibleTaskIds.isEmpty())">
                    AND 1=0
                </if>
                <if test="involveUserId != null">
                    AND (
                        t.owner_id = #{involveUserId}
                        OR EXISTS (
                            SELECT 1 FROM rdms_task_assignee a
                            WHERE a.task_id = t.id
                              AND a.user_id = #{involveUserId}
                              AND a.removed_at IS NULL
                              AND a.deleted = b'0'
                        )
                    )
                </if>
            </where>
            </script>
            """)
    Map<String, Long> selectAggregateSummaryCounts(
            @Param("projectId") Long projectId,
            @Param("seesAll") boolean seesAll,
            @Param("visibleTaskIds") java.util.Collection<Long> visibleTaskIds,
            @Param("involveUserId") Long involveUserId,
            @Param("terminalStatusCodes") Collection<String> terminalStatusCodes,
            @Param("completedStatusCode") String completedStatusCode,
            @Param("today") LocalDate today,
            @Param("weekStart") LocalDate weekStart,
            @Param("weekEnd") LocalDate weekEnd);

    /**
     * status-board 计数行 — 内嵌静态类,与 mapper 共享生命周期。
     */
    @Data
    class StatusCountRow {
        private String statusCode;
        private Long count;
    }

}
