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

import com.google.common.annotations.VisibleForTesting;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeSaveReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeLogMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.framework.security.annotation.CheckObjectPermission;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

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

/**
 * 执行协办人 Service 实现类（B 模型 - 多行周期记录）。
 * <p>
 * 同 userId 在同执行内任意时刻只允许一段未失效；重新加入新插一条独立段，
 * 旧段的 removed_reason 永久保留不再覆盖。每次状态变更同步落 rdms_execution_assignee_log。
 */
@Service
public class ProjectExecutionAssigneeServiceImpl implements ProjectExecutionAssigneeService {

    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;
    @Resource
    private ExecutionAssigneeMapper executionAssigneeMapper;
    @Resource
    private ExecutionAssigneeLogMapper executionAssigneeLogMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private AdminUserApi adminUserApi;

    @Override
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public List<ExecutionAssigneeRespVO> getExecutionAssigneeList(Long projectId, Long executionId) {
        validateProjectExists(projectId);
        validateExecutionExists(projectId, executionId);
        // 仅返当前活跃段（removed_at IS NULL）；B 模型下同 userId 至多一段
        List<ExecutionAssigneeDO> activeList = executionAssigneeMapper.selectActiveListByExecutionId(executionId);
        if (activeList.isEmpty()) {
            return Collections.emptyList();
        }
        // 批量回查昵称，避免 N+1
        Map<Long, String> nicknameMap = loadUserNicknameMap(activeList.stream()
                .map(ExecutionAssigneeDO::getUserId).collect(Collectors.toCollection(LinkedHashSet::new)));
        return activeList.stream().map(assignee -> {
            ExecutionAssigneeRespVO respVO = buildAssigneeRespVO(assignee);
            respVO.setUserNickname(nicknameMap.get(assignee.getUserId()));
            return respVO;
        }).collect(Collectors.toList());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectExecutionConstants.PERMISSION_ASSIGNEE)
    public Long createExecutionAssignee(Long projectId, Long executionId, ExecutionAssigneeSaveReqVO reqVO) {
        validateEditableExecution(projectId, executionId);
        validateProjectMember(projectId, reqVO.getUserId());
        // B 模型：只看是否有当前活跃段；旧的失效段允许"重新加入"（新插一行）
        ExecutionAssigneeDO active = executionAssigneeMapper
                .selectActiveByExecutionIdAndUserId(executionId, reqVO.getUserId());
        if (active != null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_ASSIGNEE_ALREADY_EXISTS);
        }

        LocalDateTime now = LocalDateTime.now();
        ExecutionAssigneeDO assignee = new ExecutionAssigneeDO();
        assignee.setExecutionId(executionId);
        assignee.setUserId(reqVO.getUserId());
        assignee.setJoinedAt(now);
        assignee.setRemovedAt(null);
        assignee.setRemovedReason(null);
        executionAssigneeMapper.insert(assignee);

        // 双写：旧 BizAuditLog 通用审计 + 新 rdms_execution_assignee_log 业务事件流
        writeExecutionAssigneeAuditLog(executionId, ObjectActivityConstants.EXECUTION_ASSIGNEE_ACTION_ADD, null, assignee, null);
        writeAssigneeLogInternal(executionId, reqVO.getUserId(),
                ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_JOIN, null, now);
        return assignee.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectExecutionConstants.PERMISSION_ASSIGNEE)
    public void inactiveExecutionAssignee(Long projectId, Long executionId, Long assigneeId, ExecutionAssigneeInactiveReqVO reqVO) {
        validateEditableExecution(projectId, executionId);
        ExecutionAssigneeDO assignee = validateExecutionAssigneeExists(executionId, assigneeId);
        if (assignee.getRemovedAt() != null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_ASSIGNEE_NOT_ACTIVE);
        }
        ExecutionAssigneeDO before = cloneExecutionAssignee(assignee);
        LocalDateTime now = LocalDateTime.now();
        String reason = normalizeNullableText(reqVO.getReason());
        assignee.setRemovedAt(now);
        assignee.setRemovedReason(reason);
        executionAssigneeMapper.updateById(assignee);

        writeExecutionAssigneeAuditLog(executionId, ObjectActivityConstants.EXECUTION_ASSIGNEE_ACTION_REMOVE,
                before, assignee, reason);
        writeAssigneeLogInternal(executionId, assignee.getUserId(),
                ObjectActivityConstants.EXECUTION_ASSIGNEE_LOG_ACTION_INACTIVE, reason, now);
    }

    @Override
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public PageResult<ExecutionAssigneeLogRespVO> getExecutionAssigneeLogPage(Long projectId, Long executionId,
                                                                              ExecutionAssigneeLogPageReqVO reqVO) {
        validateProjectExists(projectId);
        validateExecutionExists(projectId, executionId);
        PageResult<ExecutionAssigneeLogDO> page = executionAssigneeLogMapper.selectPageByExecutionId(executionId, reqVO);
        PageResult<ExecutionAssigneeLogRespVO> result = BeanUtils.toBean(page, ExecutionAssigneeLogRespVO.class);
        fillAssigneeLogNicknames(result.getList());
        return result;
    }

    @Override
    public void writeAssigneeLog(Long executionId, Long userId, String actionType, String reason) {
        writeAssigneeLogInternal(executionId, userId, actionType, reason, LocalDateTime.now());
    }

    @VisibleForTesting
    ProjectExecutionDO validateEditableExecution(Long projectId, Long executionId) {
        ProjectDO project = validateProjectExists(projectId);
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(ProjectObjectConstants.OBJECT_TYPE, project.getStatusCode());
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.PROJECT_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_ALLOW_EDIT);
        }
        ProjectExecutionDO execution = validateExecutionExists(projectId, executionId);
        validateExecutionAllowEdit(execution);
        return execution;
    }

    @VisibleForTesting
    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;
    }

    @VisibleForTesting
    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;
    }

    @VisibleForTesting
    void validateProjectMember(Long projectId, Long userId) {
        // 多角色支持：user 在项目内有任意 ACTIVE 角色即可作为 assignee
        if (userObjectRoleMapper
                .selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, projectId, userId)
                .isEmpty()) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_ASSIGNEE_INVALID);
        }
    }

    @VisibleForTesting
    void validateExecutionAllowEdit(ProjectExecutionDO execution) {
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(ProjectExecutionConstants.OBJECT_TYPE, execution.getStatusCode());
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_NOT_ALLOW_EDIT);
        }
    }

    @VisibleForTesting
    ExecutionAssigneeDO validateExecutionAssigneeExists(Long executionId, Long assigneeId) {
        ExecutionAssigneeDO assignee = executionAssigneeMapper.selectByIdAndExecutionId(assigneeId, executionId);
        if (assignee == null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_ASSIGNEE_NOT_EXISTS);
        }
        return assignee;
    }

    private ExecutionAssigneeRespVO buildAssigneeRespVO(ExecutionAssigneeDO assignee) {
        ExecutionAssigneeRespVO respVO = new ExecutionAssigneeRespVO();
        respVO.setId(assignee.getId());
        respVO.setExecutionId(assignee.getExecutionId());
        respVO.setUserId(assignee.getUserId());
        respVO.setJoinedAt(assignee.getJoinedAt());
        respVO.setRemovedAt(assignee.getRemovedAt());
        respVO.setRemovedReason(assignee.getRemovedReason());
        return respVO;
    }

    /**
     * 返回历史日志时，按当前系统用户信息统一回填展示名。
     * <p>
     * 这里只用 userId / operatorUserId 查当前昵称，不依赖历史快照，避免用户改名后前后展示不一致。
     */
    private void fillAssigneeLogNicknames(List<ExecutionAssigneeLogRespVO> logs) {
        if (logs == null || logs.isEmpty()) {
            return;
        }
        Set<Long> userIds = new LinkedHashSet<>();
        for (ExecutionAssigneeLogRespVO log : logs) {
            if (log == null) {
                continue;
            }
            if (log.getUserId() != null) {
                userIds.add(log.getUserId());
            }
            if (log.getOperatorUserId() != null) {
                userIds.add(log.getOperatorUserId());
            }
        }
        if (userIds.isEmpty()) {
            return;
        }
        Map<Long, String> nicknameMap = loadUserNicknameMap(userIds);
        for (ExecutionAssigneeLogRespVO log : logs) {
            if (log == null) {
                continue;
            }
            log.setUserNicknameSnapshot(nicknameMap.get(log.getUserId()));
            log.setOperatorNicknameSnapshot(nicknameMap.get(log.getOperatorUserId()));
        }
    }

    private void writeExecutionAssigneeAuditLog(Long executionId,
                                                String actionType,
                                                ExecutionAssigneeDO before,
                                                ExecutionAssigneeDO after,
                                                String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ProjectExecutionConstants.BIZ_TYPE);
        auditLog.setBizId(executionId);
        auditLog.setActionType(actionType);
        auditLog.setFieldChanges(buildExecutionAssigneeFieldChanges(before, after));
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    /**
     * 写一条 rdms_execution_assignee_log 事件。
     * 只写 ID 和事件语义字段，昵称展示由查询阶段按当前系统用户信息回填。
     */
    private void writeAssigneeLogInternal(Long executionId, Long userId,
                                          String actionType, String reason, LocalDateTime when) {
        ExecutionAssigneeLogDO log = new ExecutionAssigneeLogDO();
        log.setExecutionId(executionId);
        log.setUserId(userId);
        log.setActionType(actionType);
        log.setReason(reason);
        log.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        log.setActionTime(when);
        executionAssigneeLogMapper.insert(log);
    }

    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;
    }

    private String buildExecutionAssigneeFieldChanges(ExecutionAssigneeDO before, ExecutionAssigneeDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "executionId", valueOf(before, ExecutionAssigneeDO::getExecutionId),
                valueOf(after, ExecutionAssigneeDO::getExecutionId));
        appendFieldChange(fieldChanges, "userId", valueOf(before, ExecutionAssigneeDO::getUserId),
                valueOf(after, ExecutionAssigneeDO::getUserId));
        appendFieldChange(fieldChanges, "joinedAt", valueOf(before, ExecutionAssigneeDO::getJoinedAt),
                valueOf(after, ExecutionAssigneeDO::getJoinedAt));
        appendFieldChange(fieldChanges, "removedAt", valueOf(before, ExecutionAssigneeDO::getRemovedAt),
                valueOf(after, ExecutionAssigneeDO::getRemovedAt));
        appendFieldChange(fieldChanges, "removedReason", valueOf(before, ExecutionAssigneeDO::getRemovedReason),
                valueOf(after, ExecutionAssigneeDO::getRemovedReason));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private ExecutionAssigneeDO cloneExecutionAssignee(ExecutionAssigneeDO source) {
        ExecutionAssigneeDO target = new ExecutionAssigneeDO();
        target.setId(source.getId());
        target.setExecutionId(source.getExecutionId());
        target.setUserId(source.getUserId());
        target.setJoinedAt(source.getJoinedAt());
        target.setRemovedAt(source.getRemovedAt());
        target.setRemovedReason(source.getRemovedReason());
        return target;
    }

    private <T> T valueOf(ExecutionAssigneeDO assignee, Function<ExecutionAssigneeDO, T> getter) {
        return assignee == null ? null : getter.apply(assignee);
    }

    private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
        if (Objects.equals(before, after)) {
            return;
        }
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("before", before);
        value.put("after", after);
        fieldChanges.put(fieldName, value);
    }

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

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

}
