package com.njcn.rdms.module.project.service.project.task.worklog;

import com.google.common.annotations.VisibleForTesting;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.constant.ProjectTaskConstants;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
import com.njcn.rdms.module.project.framework.attachment.AttachmentFileIdResolver;
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
import com.njcn.rdms.module.project.service.project.ProjectService;
import com.njcn.rdms.module.project.service.project.task.ProjectTaskService;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;

@Service
public class TaskWorklogServiceImpl implements TaskWorklogService {

    /**
     * 工时颗粒：0.5 小时。durationHours 必须 > 0 且为本值的整数倍。
     */
    private static final BigDecimal DURATION_GRANULARITY_HOURS = new BigDecimal("0.5");

    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;
    @Resource
    private ProjectTaskMapper projectTaskMapper;
    @Resource
    private TaskAssigneeMapper taskAssigneeMapper;
    @Resource
    private TaskWorklogMapper taskWorklogMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DictDataApi dictDataApi;
    @Resource
    private AttachmentFileIdResolver attachmentFileIdResolver;
    @Resource
    private ProjectService projectService;
    /**
     * 与 ProjectTaskService 互相依赖（ProjectTaskService 也注入本类），用 @Lazy 打破循环。
     * 仅用于 owner 工时变更后同步任务自身进度并触发父任务汇总。
     */
    @Resource
    @Lazy
    private ProjectTaskService projectTaskService;

    @Override
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public PageResult<TaskWorklogRespVO> getWorklogPage(Long projectId, Long executionId, Long taskId,
                                                        TaskWorklogPageReqVO reqVO) {
        validateExecutionAndTaskExists(projectId, executionId, taskId);
        PageResult<TaskWorklogDO> page = taskWorklogMapper.selectPageByTaskId(taskId, reqVO);
        if (page.getList().isEmpty()) {
            return new PageResult<>(Collections.emptyList(), page.getTotal());
        }
        Map<Long, String> nicknameMap = loadUserNicknameMap(page.getList().stream()
                .map(TaskWorklogDO::getUserId)
                .collect(Collectors.toCollection(LinkedHashSet::new)));
        List<TaskWorklogRespVO> list = page.getList().stream().map(worklog -> {
            TaskWorklogRespVO vo = BeanUtils.toBean(worklog, TaskWorklogRespVO.class);
            vo.setUserNickname(nicknameMap.get(worklog.getUserId()));
            return vo;
        }).collect(Collectors.toList());
        return new PageResult<>(list, page.getTotal());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectTaskConstants.PERMISSION_WORKLOG)
    public Long createWorklog(Long projectId, Long executionId, Long taskId, TaskWorklogSaveReqVO reqVO) {
        ProjectTaskDO task = validateEditableContext(projectId, executionId, taskId);
        validateCreateAllowed(task);
        validateLeafTask(taskId);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateFileWorklogPermission(taskId, task.getOwnerId(), loginUserId);
        validateDateRange(reqVO.getStartDate(), reqVO.getEndDate());
        validateDurationGranularity(reqVO.getDurationHours());
        validateNoOverlap(taskId, loginUserId, reqVO.getStartDate(), reqVO.getEndDate(), null);
        validateProgressMonotonicity(taskId, loginUserId, reqVO.getEndDate(), reqVO.getProgressRate(), null);
        validateDifficulty(reqVO.getDifficulty());
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        TaskWorklogDO worklog = buildWorklog(taskId, loginUserId, reqVO);
        taskWorklogMapper.insert(worklog);
        projectTaskService.syncActualDatesFromWorklogs(taskId);
        projectService.syncActualStartDateFromWorklogs(projectId);
        // 任意填报人（owner / 协办人）都触发任务自动开始（仅当任务仍处初始态时生效），推进任务状态。
        // 任务"是否开始"是客观事实，协办人开工同样代表任务已开始，不应等 owner 补工时才反映。
        projectTaskService.internalAutoStartByWorklog(task);
        // 仅 owner 填报触发任务进度同步：任务整体进度以 owner 本人最新一条工时为权威源，
        // 协办人填报的是个体进度，不参与任务整体进度计算。
        if (Objects.equals(loginUserId, task.getOwnerId())) {
            projectTaskService.syncOwnerProgressFromLatestWorklog(taskId);
        }
        return worklog.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectTaskConstants.PERMISSION_WORKLOG)
    public void updateWorklog(Long projectId, Long executionId, Long taskId, Long worklogId,
                              TaskWorklogSaveReqVO reqVO) {
        ProjectTaskDO task = validateEditableContext(projectId, executionId, taskId);
        TaskWorklogDO worklog = loadWorklog(worklogId, taskId);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        // 仅原填报人可改；任务负责人也不能改协办人的记录（避免争议）
        if (!Objects.equals(worklog.getUserId(), loginUserId)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_EDIT_NOT_OWN);
        }
        validateDateRange(reqVO.getStartDate(), reqVO.getEndDate());
        validateDurationGranularity(reqVO.getDurationHours());
        // 与该用户其他工时记录不可重叠（排除自身）
        validateNoOverlap(taskId, worklog.getUserId(), reqVO.getStartDate(), reqVO.getEndDate(), worklog.getId());
        validateProgressMonotonicity(taskId, worklog.getUserId(), reqVO.getEndDate(), reqVO.getProgressRate(), worklog.getId());
        validateDifficulty(reqVO.getDifficulty());
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        TaskWorklogDO update = new TaskWorklogDO();
        update.setId(worklog.getId());
        update.setStartDate(reqVO.getStartDate());
        update.setEndDate(reqVO.getEndDate());
        update.setDurationHours(reqVO.getDurationHours());
        update.setProgressRate(reqVO.getProgressRate());
        update.setDifficulty(reqVO.getDifficulty());
        update.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
        update.setAttachments(reqVO.getAttachments());
        taskWorklogMapper.updateById(update);
        projectTaskService.syncActualDatesFromWorklogs(taskId);
        projectService.syncActualStartDateFromWorklogs(projectId);
        // owner 改自己工时：按"owner 最新一条"重算任务进度
        if (Objects.equals(worklog.getUserId(), task.getOwnerId())) {
            projectTaskService.syncOwnerProgressFromLatestWorklog(taskId);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectTaskConstants.PERMISSION_WORKLOG)
    public void deleteWorklog(Long projectId, Long executionId, Long taskId, Long worklogId) {
        ProjectTaskDO task = validateEditableContext(projectId, executionId, taskId);
        validateDeleteAllowed(task);
        TaskWorklogDO worklog = loadWorklog(worklogId, taskId);
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        boolean isFiler = Objects.equals(worklog.getUserId(), loginUserId);
        boolean isOwner = Objects.equals(task.getOwnerId(), loginUserId);
        if (!isFiler && !isOwner) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN);
        }
        taskWorklogMapper.deleteById(worklog.getId());
        projectTaskService.syncActualDatesFromWorklogs(taskId);
        projectService.syncActualStartDateFromWorklogs(projectId);
        // 删的是 owner 自己的工时：按"owner 剩余最新一条"重算任务进度；
        // 若 owner 已无任何工时，syncOwnerProgressFromLatestWorklog 内部会保留原值不动（不归零）
        if (Objects.equals(worklog.getUserId(), task.getOwnerId())) {
            projectTaskService.syncOwnerProgressFromLatestWorklog(taskId);
        }
    }

    @Override
    public Map<Long, BigDecimal> sumDurationGroupedByTaskIds(Collection<Long> taskIds) {
        if (taskIds == null || taskIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<Map<String, Object>> rows = taskWorklogMapper.sumDurationGroupByTaskIds(taskIds);
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, BigDecimal> result = new HashMap<>(rows.size());
        for (Map<String, Object> row : rows) {
            Object idValue = row.getOrDefault("taskId", row.get("task_id"));
            Object totalValue = row.get("total");
            if (idValue instanceof Number idNum && totalValue != null) {
                BigDecimal totalBd = totalValue instanceof BigDecimal bd
                        ? bd : new BigDecimal(totalValue.toString());
                result.put(idNum.longValue(), totalBd);
            }
        }
        return result;
    }

    // -------------------- 内部辅助 --------------------

    private TaskWorklogDO buildWorklog(Long taskId, Long userId, TaskWorklogSaveReqVO reqVO) {
        TaskWorklogDO worklog = new TaskWorklogDO();
        worklog.setTaskId(taskId);
        worklog.setUserId(userId);
        worklog.setStartDate(reqVO.getStartDate());
        worklog.setEndDate(reqVO.getEndDate());
        worklog.setDurationHours(reqVO.getDurationHours());
        worklog.setProgressRate(reqVO.getProgressRate());
        worklog.setDifficulty(reqVO.getDifficulty());
        worklog.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
        worklog.setAttachments(reqVO.getAttachments());
        worklog.setDifficulty(reqVO.getDifficulty());
        return worklog;
    }

    /**
     * 校验项目/执行/任务都允许编辑，并返回任务实体。与 TaskAssigneeServiceImpl 同口径。
     */
    private ProjectTaskDO validateEditableContext(Long projectId, Long executionId, Long taskId) {
        ProjectDO project = validateProjectExists(projectId);
        validateAllowEdit(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode());
        ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
        validateAllowEdit(ProjectExecutionConstants.OBJECT_TYPE, execution.getStatusCode());
        ProjectTaskDO task = validateTaskExists(projectId, executionId, taskId);
        // 工作日志维护在任务 completed 状态下统一放行；新增/删除是否允许由各自接口单独判定。
        if (!isCompletedWorklogContext(task)) {
            validateAllowEdit(ProjectTaskConstants.OBJECT_TYPE, task.getStatusCode());
        }
        return task;
    }

    /**
     * 是否处于“任务已完成”的工作日志维护场景。
     */
    private boolean isCompletedWorklogContext(ProjectTaskDO task) {
        return "completed".equals(task.getStatusCode());
    }

    private void validateCreateAllowed(ProjectTaskDO task) {
        if (isCompletedWorklogContext(task)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_CREATE_NOT_ALLOWED_BY_TASK_STATUS);
        }
    }

    private void validateDeleteAllowed(ProjectTaskDO task) {
        if (isCompletedWorklogContext(task)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_NOT_ALLOWED_BY_TASK_STATUS);
        }
    }

    private void validateExecutionAndTaskExists(Long projectId, Long executionId, Long taskId) {
        validateExecutionExists(projectId, executionId);
        validateTaskExists(projectId, executionId, taskId);
    }

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

    private ProjectExecutionDO validateExecutionExists(Long projectId, Long executionId) {
        if (executionId == null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_EXISTS);
        }
        ProjectExecutionDO execution = projectExecutionMapper.selectByProjectIdAndId(projectId, executionId);
        if (execution == null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_EXISTS);
        }
        return execution;
    }

    private ProjectTaskDO validateTaskExists(Long projectId, Long executionId, Long taskId) {
        if (taskId == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_NOT_EXISTS);
        }
        ProjectTaskDO task = projectTaskMapper.selectByProjectIdAndExecutionIdAndId(projectId, executionId, taskId);
        if (task == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_NOT_EXISTS);
        }
        return task;
    }

    private void validateAllowEdit(String objectType, String statusCode) {
        ObjectStatusModelDO status = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(objectType, statusCode);
        if (status == null || !Boolean.TRUE.equals(status.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_NOT_ALLOW_EDIT);
        }
    }

    /**
     * 校验任务为叶子（无任何子任务）。父任务不允许挂工时。
     */
    private void validateLeafTask(Long taskId) {
        long childCount = projectTaskMapper.selectCount(new LambdaQueryWrapperX<ProjectTaskDO>()
                .eq(ProjectTaskDO::getParentTaskId, taskId));
        if (childCount > 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_LEAF_TASK);
        }
    }

    /**
     * 校验登录人具备工时填报资格：是 owner，或在该任务上有当前活跃的协办段。
     */
    private void validateFileWorklogPermission(Long taskId, Long ownerId, Long loginUserId) {
        if (loginUserId == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_OWNER_OR_ASSIGNEE);
        }
        if (Objects.equals(loginUserId, ownerId)) {
            return;
        }
        if (taskAssigneeMapper.selectActiveByTaskIdAndUserId(taskId, loginUserId) != null) {
            return;
        }
        throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_OWNER_OR_ASSIGNEE);
    }

    /**
     * 校验段日期：startDate 不能晚于 endDate（NotNull 已由 Bean Validation 保证）。
     */
    private void validateDateRange(LocalDate startDate, LocalDate endDate) {
        if (startDate == null || endDate == null || startDate.isAfter(endDate)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DATE_RANGE_INVALID);
        }
    }

    /**
     * 校验小时颗粒：> 0 且为 0.5 的整数倍（用 remainder 处理 scale 差异）。
     */
    private void validateDurationGranularity(BigDecimal durationHours) {
        if (durationHours == null
                || durationHours.compareTo(BigDecimal.ZERO) <= 0
                || durationHours.remainder(DURATION_GRANULARITY_HOURS).compareTo(BigDecimal.ZERO) != 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DURATION_INVALID);
        }
    }

    /**
     * 校验本次提交的段与该 task × user 的已有工时记录无日期重叠（更新场景需排除自身）。
     */
    private void validateNoOverlap(Long taskId, Long userId, LocalDate startDate, LocalDate endDate, Long excludeId) {
        if (taskWorklogMapper.existsOverlapping(taskId, userId, startDate, endDate, excludeId)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DATE_OVERLAP);
        }
    }

    /**
     * 校验"按 endDate 排序后进度非递减"。仅与目标段相邻的前/后一条比较：
     * - 早段（endDate 更小）的 progressRate 不得大于本段
     * - 晚段（endDate 更大）的 progressRate 不得小于本段
     * progressRate 为 null 时跳过比较；excludeId 用于更新场景排除自身。
     * 仅校验邻段以避开历史脏数据：库内若已有违反单调的记录，本次更新只要不再扩大违反范围即可放过。
     */
    private void validateProgressMonotonicity(Long taskId, Long userId, LocalDate endDate,
                                              BigDecimal progressRate, Long excludeId) {
        if (progressRate == null || endDate == null) {
            return;
        }
        TaskWorklogDO prev = taskWorklogMapper.selectPrevByEndDate(taskId, userId, endDate, excludeId);
        if (prev != null && prev.getProgressRate() != null
                && prev.getProgressRate().compareTo(progressRate) > 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC);
        }
        TaskWorklogDO next = taskWorklogMapper.selectNextByEndDate(taskId, userId, endDate, excludeId);
        if (next != null && next.getProgressRate() != null
                && next.getProgressRate().compareTo(progressRate) < 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC);
        }
    }

    private TaskWorklogDO loadWorklog(Long worklogId, Long taskId) {
        if (worklogId == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_EXISTS);
        }
        TaskWorklogDO worklog = taskWorklogMapper.selectByIdAndTaskId(worklogId, taskId);
        if (worklog == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_EXISTS);
        }
        return worklog;
    }

    private Map<Long, String> loadUserNicknameMap(Collection<Long> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        if (userMap == null || userMap.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, String> nicknameMap = new HashMap<>(userMap.size());
        userMap.forEach((id, user) -> nicknameMap.put(id, user == null ? null : user.getNickname()));
        return nicknameMap;
    }

    /**
     * 校验完成难度为字典 {@code rdms_worklog_difficulty} 中的合法 value。
     * 走 DictDataApi 而非本地 Enum：运维加新档无需代码发版。
     */
    @VisibleForTesting
    void validateDifficulty(String difficulty) {
        try {
            Boolean valid = dictDataApi.validateDictDataList(ProjectDictTypeConstants.WORKLOG_DIFFICULTY,
                    java.util.List.of(difficulty.trim())).getCheckedData();
            if (Boolean.TRUE.equals(valid)) {
                return;
            }
        } catch (RuntimeException ex) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DIFFICULTY_INVALID);
        }
        throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DIFFICULTY_INVALID);
    }

    private String normalizeNullableText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

}
