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

import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
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.vo.member.ProjectMemberBatchCreateReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberUpdateReqVO;
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.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.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.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
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.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
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 实现类
 */
@Service
public class ProjectMemberServiceImpl implements ProjectMemberService {

    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private ObjectPermissionApi objectPermissionApi;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;

    @Override
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId", accessible = true)
    public List<ProjectMemberRespVO> getProjectMemberList(Long projectId) {
        ProjectDO project = validateProjectExists(projectId);
        List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId);
        Map<Long, ObjectRoleRespDTO> roleMap = getRoleMap(members.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()));
        // 过滤掉 visible=0 的角色行（业务自动赋予的角色，如创建者 / 隐式观察者；不在团队列表显示）；
        // visible=null 视同显示，兼容旧数据未回填的场景。ACTIVE / INACTIVE 行一并过滤。
        members = members.stream()
                .filter(m -> {
                    ObjectRoleRespDTO role = roleMap.get(m.getRoleId());
                    return role == null || !Integer.valueOf(0).equals(role.getVisible());
                })
                .toList();
        Map<Long, AdminUserRespDTO> userMap = getUserMap(members.stream().map(UserObjectRoleDO::getUserId).collect(Collectors.toSet()));

        // 拆分 ACTIVE / INACTIVE：
        // - ACTIVE 行按 userId 聚合，同人多角色合并成一行（manager 优先做主），非主角色名放 additionalRoleNames
        // - INACTIVE 行保持独立（历史角色行各自留痕，便于审计）
        List<UserObjectRoleDO> activeRows = new ArrayList<>();
        List<UserObjectRoleDO> inactiveRows = new ArrayList<>();
        for (UserObjectRoleDO m : members) {
            if (Objects.equals(m.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
                activeRows.add(m);
            } else {
                inactiveRows.add(m);
            }
        }
        Map<Long, List<UserObjectRoleDO>> activeByUser = activeRows.stream()
                .collect(Collectors.groupingBy(UserObjectRoleDO::getUserId, LinkedHashMap::new, Collectors.toList()));

        List<ProjectMemberRespVO> result = new ArrayList<>();
        activeByUser.forEach((userId, rows) -> {
            UserObjectRoleDO primary = pickPrimaryRow(rows, roleMap);
            List<String> additionalRoleNames = rows.stream()
                    .filter(r -> !Objects.equals(r.getId(), primary.getId()))
                    .map(r -> {
                        ObjectRoleRespDTO role = roleMap.get(r.getRoleId());
                        return role == null ? null : role.getName();
                    })
                    .filter(Objects::nonNull)
                    .sorted()
                    .collect(Collectors.toList());
            result.add(toRespVO(primary, roleMap, userMap, project, additionalRoleNames));
        });
        // INACTIVE 行各自独立成行
        inactiveRows.forEach(m -> result.add(toRespVO(m, roleMap, userMap, project, Collections.emptyList())));
        return result;
    }

    /**
     * 同 userId 多角色时选主角色行：MANAGER_ROLE_CODE 优先，否则按 roleId 升序兜底。
     */
    private UserObjectRoleDO pickPrimaryRow(List<UserObjectRoleDO> rows, Map<Long, ObjectRoleRespDTO> roleMap) {
        return rows.stream()
                .filter(r -> {
                    ObjectRoleRespDTO role = roleMap.get(r.getRoleId());
                    return role != null && Objects.equals(ProjectObjectConstants.MANAGER_ROLE_CODE, role.getCode());
                })
                .findFirst()
                .orElseGet(() -> rows.stream()
                        .min(Comparator.comparing(UserObjectRoleDO::getRoleId))
                        .orElseThrow());
    }

    private ProjectMemberRespVO toRespVO(UserObjectRoleDO member,
                                         Map<Long, ObjectRoleRespDTO> roleMap,
                                         Map<Long, AdminUserRespDTO> userMap,
                                         ProjectDO project,
                                         List<String> additionalRoleNames) {
        ProjectMemberRespVO respVO = new ProjectMemberRespVO();
        respVO.setId(member.getId());
        respVO.setUserId(member.getUserId());
        AdminUserRespDTO user = userMap.get(member.getUserId());
        respVO.setUserNickname(user == null ? null : user.getNickname());
        respVO.setRoleId(member.getRoleId());
        ObjectRoleRespDTO role = roleMap.get(member.getRoleId());
        respVO.setRoleName(role == null ? null : role.getName());
        respVO.setRoleCode(role == null ? null : role.getCode());
        respVO.setManagerFlag(Objects.equals(member.getUserId(), project.getManagerUserId())
                && Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE));
        respVO.setStatus(member.getStatus());
        respVO.setJoinedTime(member.getJoinedTime());
        respVO.setLeftTime(member.getLeftTime());
        respVO.setRemark(member.getRemark());
        respVO.setAdditionalRoleNames(additionalRoleNames);
        return respVO;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectObjectConstants.PERMISSION_MEMBER)
    public Long createProjectMember(Long projectId, ProjectMemberSaveReqVO reqVO) {
        ProjectDO project = validateProjectEditable(projectId);
        validateMemberUser(reqVO.getUserId());
        ObjectRoleRespDTO targetRole = validateProjectRole(reqVO.getRoleId());
        // 多角色支持：按 (user, object, role) 三元组判存在；不带 status 过滤以便 INACTIVE 行复活（避免唯一索引 INSERT 冲突）
        UserObjectRoleDO existingMember = userObjectRoleMapper
                .selectByObjectUserAndRole(ProjectObjectConstants.OBJECT_TYPE, projectId,
                        reqVO.getUserId(), targetRole.getId());
        if (existingMember != null && Objects.equals(existingMember.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS);
        }

        UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
        UserObjectRoleDO member = existingMember == null ? new UserObjectRoleDO() : existingMember;
        member.setUserId(reqVO.getUserId());
        member.setObjectType(ProjectObjectConstants.OBJECT_TYPE);
        member.setObjectId(projectId);
        member.setRoleId(targetRole.getId());
        member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
        member.setJoinedTime(LocalDateTime.now());
        member.setLeftTime(null);
        member.setRemark(normalizeNullableText(reqVO.getRemark()));
        // 物理 INSERT vs INACTIVE 复活的 audit 语义区分：null=新增；INACTIVE 复活=REACTIVATE
        String actionType;
        if (existingMember == null) {
            userObjectRoleMapper.insert(member);
            actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
        } else {
            userObjectRoleMapper.updateById(member);
            actionType = ObjectActivityConstants.MEMBER_ACTION_REACTIVATE;
        }
        writeMemberAuditLog(member, actionType, before, member, null);

        if (isManagerRole(targetRole)) {
            transferManager(project, member, reqVO.getPreviousManagerUserId(), reqVO.getPreviousManagerRoleId(), null);
        }
        return member.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectObjectConstants.PERMISSION_MEMBER)
    public List<Long> batchCreateProjectMembers(Long projectId, ProjectMemberBatchCreateReqVO reqVO) {
        validateProjectEditable(projectId);

        List<ProjectMemberBatchCreateReqVO.Item> items = reqVO.getMembers();
        // 同请求内 userId 重复一律拒绝（user-level），即使是不同角色也算重复——批量语义就是一人一行
        Set<Long> seenUserIds = new HashSet<>(items.size());
        for (ProjectMemberBatchCreateReqVO.Item item : items) {
            if (!seenUserIds.add(item.getUserId())) {
                throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_USER_DUPLICATE);
            }
        }
        // 批量校验用户存在/启用——按需求 §4 兜底
        validateMemberUsers(seenUserIds);

        List<Long> memberIds = new ArrayList<>(items.size());
        LocalDateTime now = LocalDateTime.now();
        for (ProjectMemberBatchCreateReqVO.Item item : items) {
            ObjectRoleRespDTO targetRole = validateProjectRole(item.getRoleId());
            if (isManagerRole(targetRole)) {
                throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED);
            }
            // user-level 拒绝：该用户在本项目下有任意 ACTIVE 行（含其他角色），不允许再批量加入
            List<UserObjectRoleDO> activeRows = userObjectRoleMapper
                    .selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, projectId, item.getUserId());
            if (!activeRows.isEmpty()) {
                throw exception(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS);
            }

            // 多角色支持：按 (user, object, role) 三元组判存在；INACTIVE 行复活，避免唯一索引 INSERT 冲突
            UserObjectRoleDO existingMember = userObjectRoleMapper
                    .selectByObjectUserAndRole(ProjectObjectConstants.OBJECT_TYPE, projectId,
                            item.getUserId(), targetRole.getId());
            UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
            UserObjectRoleDO member = existingMember == null ? new UserObjectRoleDO() : existingMember;
            member.setUserId(item.getUserId());
            member.setObjectType(ProjectObjectConstants.OBJECT_TYPE);
            member.setObjectId(projectId);
            member.setRoleId(targetRole.getId());
            member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
            member.setJoinedTime(now);
            member.setLeftTime(null);
            member.setRemark(normalizeNullableText(item.getRemark()));
            String actionType;
            if (existingMember == null) {
                userObjectRoleMapper.insert(member);
                actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
            } else {
                userObjectRoleMapper.updateById(member);
                actionType = ObjectActivityConstants.MEMBER_ACTION_REACTIVATE;
            }
            writeMemberAuditLog(member, actionType, before, member, null);
            memberIds.add(member.getId());
        }
        return memberIds;
    }

    private void validateMemberUsers(Set<Long> userIds) {
        try {
            Boolean valid = adminUserApi.validateUserList(new ArrayList<>(userIds)).getCheckedData();
            if (Boolean.TRUE.equals(valid)) {
                return;
            }
        } catch (RuntimeException ex) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID);
        }
        throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectObjectConstants.PERMISSION_MEMBER)
    public void updateProjectMember(Long projectId, Long memberId, ProjectMemberUpdateReqVO reqVO) {
        ProjectDO project = validateProjectEditable(projectId);
        UserObjectRoleDO member = validateMemberExists(projectId, memberId);
        if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_NOT_ACTIVE);
        }
        ObjectRoleRespDTO targetRole = validateProjectRole(reqVO.getRoleId());
        UserObjectRoleDO before = cloneMember(member);
        member.setRemark(normalizeNullableText(reqVO.getRemark()));

        // 多角色边界：避免 setRoleId 撞唯一索引——若 user 在该 project 已有 (targetRole) 的另一行（含 INACTIVE 历史行），
        // 直接 update 会触发唯一索引 (user_id, object_type, object_id, role_id, deleted) 冲突。业务侧明确报错，避免 SQL 异常透出。
        if (!Objects.equals(member.getRoleId(), targetRole.getId())) {
            UserObjectRoleDO conflict = userObjectRoleMapper.selectByObjectUserAndRole(
                    ProjectObjectConstants.OBJECT_TYPE, projectId, member.getUserId(), targetRole.getId());
            if (conflict != null && !Objects.equals(conflict.getId(), member.getId())) {
                throw exception(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS);
            }
        }

        if (isManagerRole(targetRole)) {
            // 项目经理交接只切换负责人并调整原经理角色，不再把原经理自动移出项目团队。
            member.setRoleId(targetRole.getId());
            userObjectRoleMapper.updateById(member);
            transferManager(project, member, reqVO.getPreviousManagerUserId(),
                    reqVO.getPreviousManagerRoleId(), normalizeNullableText(reqVO.getReason()));
        } else {
            if (Objects.equals(member.getUserId(), project.getManagerUserId())) {
                throw exception(ErrorCodeConstants.PROJECT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE);
            }
            member.setRoleId(targetRole.getId());
            userObjectRoleMapper.updateById(member);
        }

        writeMemberAuditLog(member, ObjectActivityConstants.MEMBER_ACTION_UPDATE,
                before, member, normalizeNullableText(reqVO.getReason()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectObjectConstants.PERMISSION_MEMBER)
    public void inactiveProjectMember(Long projectId, Long memberId, ProjectMemberInactiveReqVO reqVO) {
        ProjectDO project = validateProjectEditable(projectId);
        UserObjectRoleDO member = validateMemberExists(projectId, memberId);
        if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_NOT_ACTIVE);
        }
        if (Objects.equals(member.getUserId(), project.getManagerUserId())) {
            throw exception(ErrorCodeConstants.PROJECT_MANAGER_MEMBER_NOT_ALLOW_REMOVE);
        }
        validateNoOpenOwnedExecutions(projectId, member.getUserId());

        UserObjectRoleDO before = cloneMember(member);
        member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE);
        member.setLeftTime(LocalDateTime.now());
        userObjectRoleMapper.updateById(member);
        writeMemberAuditLog(member, ObjectActivityConstants.MEMBER_ACTION_REMOVE, before, member,
                normalizeNullableText(reqVO.getReason()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
            permission = ProjectObjectConstants.PERMISSION_MEMBER)
    public void batchInactiveProjectMembers(Long projectId, ProjectMemberBatchInactiveReqVO reqVO) {
        ProjectDO project = validateProjectEditable(projectId);

        List<Long> memberIds = reqVO.getMemberIds();
        // 同请求 memberId 重复一律拒绝，避免对同一行重复 update 造成 audit 重复落库
        Set<Long> seen = new HashSet<>(memberIds.size());
        for (Long memberId : memberIds) {
            if (!seen.add(memberId)) {
                throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE);
            }
        }

        String reason = normalizeNullableText(reqVO.getReason());
        LocalDateTime now = LocalDateTime.now();
        // 收集批次摘要，循环外聚合写一条审计日志（需求 §6 口径）
        List<UserObjectRoleDO> removed = new ArrayList<>(memberIds.size());
        for (Long memberId : memberIds) {
            UserObjectRoleDO member = validateMemberExists(projectId, memberId);
            if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
                throw exception(ErrorCodeConstants.PROJECT_MEMBER_NOT_ACTIVE);
            }
            // 经理判定沿用单条 inactive 口径：按 project.managerUserId 比对，不引入 role.code 判定避免分叉
            if (Objects.equals(member.getUserId(), project.getManagerUserId())) {
                throw exception(ErrorCodeConstants.PROJECT_MANAGER_MEMBER_NOT_ALLOW_REMOVE);
            }
            // 与单条 inactive 一致：仍担任未关闭执行负责人的成员不能直接移出，需先完成执行负责人交接
            validateNoOpenOwnedExecutions(projectId, member.getUserId());

            member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE);
            member.setLeftTime(now);
            userObjectRoleMapper.updateById(member);
            removed.add(member);
        }

        writeBatchInactiveAuditLog(projectId, removed, reason);
    }

    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 ProjectDO validateProjectEditable(Long projectId) {
        ProjectDO project = validateProjectExists(projectId);
        // 项目成员维护当前完全跟随状态模型 allow_edit，不再按具体状态编码写死可编辑范围。
        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_STATUS_NOT_ALLOW_EDIT);
        }
        return project;
    }

    private void validateMemberUser(Long userId) {
        try {
            Boolean valid = adminUserApi.validateUserList(List.of(userId)).getCheckedData();
            if (Boolean.TRUE.equals(valid)) {
                return;
            }
        } catch (RuntimeException ex) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID);
        }
        throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID);
    }

    private UserObjectRoleDO validateMemberExists(Long projectId, Long memberId) {
        UserObjectRoleDO member = userObjectRoleMapper.selectByIdAndObject(memberId, ProjectObjectConstants.OBJECT_TYPE, projectId);
        if (member == null) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_NOT_EXISTS);
        }
        return member;
    }

    private void validateNoOpenOwnedExecutions(Long projectId, Long userId) {
        Integer count = projectExecutionMapper.countNonTerminalByProjectIdAndOwnerId(projectId, userId,
                getExecutionTerminalStatusCodes());
        if (count != null && count > 0) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED);
        }
    }

    private List<String> getExecutionTerminalStatusCodes() {
        List<ObjectStatusModelDO> statusModels = objectStatusModelMapper
                .selectListByObjectType(ProjectExecutionConstants.OBJECT_TYPE);
        if (statusModels == null || statusModels.isEmpty()) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        List<String> terminalStatusCodes = statusModels.stream()
                .filter(statusModel -> Boolean.TRUE.equals(statusModel.getTerminalFlag()))
                .map(ObjectStatusModelDO::getStatusCode)
                .filter(StringUtils::hasText)
                .toList();
        if (terminalStatusCodes.isEmpty()) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return terminalStatusCodes;
    }

    private ObjectRoleRespDTO validateProjectRole(Long roleId) {
        ObjectRoleRespDTO role = objectPermissionApi
                .getObjectRoleById(roleId, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        if (role == null) {
            throw exception(ErrorCodeConstants.PROJECT_MEMBER_ROLE_INVALID);
        }
        return role;
    }

    private void transferManager(ProjectDO project,
                                 UserObjectRoleDO targetManagerMember,
                                 Long previousManagerUserId,
                                 Long previousManagerRoleId,
                                 String reason) {
        Long currentManagerUserId = project.getManagerUserId();
        Long targetManagerUserId = targetManagerMember.getUserId();
        if (Objects.equals(currentManagerUserId, targetManagerUserId)) {
            project.setManagerUserId(targetManagerUserId);
            projectMapper.updateById(project);
            return;
        }

        ObjectRoleRespDTO previousManagerRole = validatePreviousManagerTransfer(currentManagerUserId,
                previousManagerUserId, previousManagerRoleId);
        transferPreviousManager(project.getId(), previousManagerUserId, previousManagerRole.getId(), reason);

        project.setManagerUserId(targetManagerUserId);
        projectMapper.updateById(project);
        writeManagerChangeAuditLog(project.getId(), currentManagerUserId, targetManagerUserId, reason);
    }

    private ObjectRoleRespDTO validatePreviousManagerTransfer(Long currentManagerUserId,
                                                              Long previousManagerUserId,
                                                              Long previousManagerRoleId) {
        if (currentManagerUserId == null
                || previousManagerUserId == null
                || previousManagerRoleId == null) {
            throw exception(ErrorCodeConstants.PROJECT_MANAGER_TRANSFER_INFO_REQUIRED);
        }
        if (!Objects.equals(currentManagerUserId, previousManagerUserId)) {
            throw exception(ErrorCodeConstants.PROJECT_MANAGER_TRANSFER_SOURCE_INVALID);
        }
        ObjectRoleRespDTO previousManagerRole = validateProjectRole(previousManagerRoleId);
        if (isManagerRole(previousManagerRole)) {
            throw exception(ErrorCodeConstants.PROJECT_MANAGER_TRANSFER_ROLE_INVALID);
        }
        return previousManagerRole;
    }

    private void transferPreviousManager(Long projectId, Long previousManagerUserId, Long previousManagerRoleId, String reason) {
        // 多角色边界校验：若 user 在 (project, previousManagerRoleId) 已有任意行（ACTIVE 或 INACTIVE 历史行），
        // 后续 update / INSERT 都会触发唯一索引 (user_id, object_type, object_id, role_id, deleted) 冲突 ——
        // 一刀切抛业务异常，让用户先去人工清理历史行。selectByObjectUserAndRole 不带 status 过滤。
        UserObjectRoleDO targetRoleExisting = userObjectRoleMapper.selectByObjectUserAndRole(
                ProjectObjectConstants.OBJECT_TYPE, projectId, previousManagerUserId, previousManagerRoleId);
        if (targetRoleExisting != null) {
            throw exception(ErrorCodeConstants.PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE, previousManagerRoleId);
        }

        // 多角色支持：按 (user, object, manager_role_id) 三元组定位 manager 行；user 的 creator/dev 等其他角色行不动
        Long projectManagerRoleId = resolveProjectManagerRoleId();
        UserObjectRoleDO existingMember = userObjectRoleMapper
                .selectActiveByObjectUserAndRole(ProjectObjectConstants.OBJECT_TYPE, projectId,
                        previousManagerUserId, projectManagerRoleId);
        UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
        LocalDateTime now = LocalDateTime.now();
        UserObjectRoleDO member;
        String actionType;
        if (existingMember == null) {
            // user 当前没有 manager 角色 ACTIVE 行（罕见，可能业务上不该走到这）—— 仍兼容老逻辑：插入 previousManagerRoleId 行
            member = new UserObjectRoleDO();
            member.setUserId(previousManagerUserId);
            member.setObjectType(ProjectObjectConstants.OBJECT_TYPE);
            member.setObjectId(projectId);
            member.setRoleId(previousManagerRoleId);
            member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
            member.setJoinedTime(now);
            member.setLeftTime(null);
            member.setRemark(null);
            userObjectRoleMapper.insert(member);
            actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
        } else {
            // existingMember 是 manager 行 ACTIVE，update 改 role_id 成 previousManagerRoleId（"降级"该行）
            member = existingMember;
            member.setRoleId(previousManagerRoleId);
            member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
            member.setLeftTime(null);
            actionType = ObjectActivityConstants.MEMBER_ACTION_UPDATE;
            userObjectRoleMapper.updateById(member);
        }
        writeMemberAuditLog(member, actionType, before, member, reason);
    }

    private Long resolveProjectManagerRoleId() {
        ObjectRoleRespDTO role = objectPermissionApi
                .getObjectRoleByCode(ProjectObjectConstants.MANAGER_ROLE_CODE,
                        ObjectRoleConstants.ROLE_SCOPE_OBJECT,
                        ProjectObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        if (role == null || role.getId() == null) {
            throw exception(ErrorCodeConstants.PROJECT_INTERNAL_ROLE_NOT_CONFIGURED,
                    ProjectObjectConstants.MANAGER_ROLE_CODE);
        }
        return role.getId();
    }

    private boolean isManagerRole(ObjectRoleRespDTO role) {
        return Objects.equals(ProjectObjectConstants.MANAGER_ROLE_CODE, role.getCode());
    }

    private Map<Long, ObjectRoleRespDTO> getRoleMap(Set<Long> roleIds) {
        if (roleIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<ObjectRoleRespDTO> roles = objectPermissionApi
                .getObjectRoleList(roleIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT, ProjectObjectConstants.OBJECT_TYPE)
                .getCheckedData();
        if (roles == null || roles.isEmpty()) {
            return Collections.emptyMap();
        }
        return roles.stream().collect(Collectors.toMap(ObjectRoleRespDTO::getId, Function.identity()));
    }

    private Map<Long, AdminUserRespDTO> getUserMap(Set<Long> userIds) {
        if (userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return adminUserApi.getUserMap(userIds);
    }

    private void writeMemberAuditLog(UserObjectRoleDO member,
                                     String actionType,
                                     UserObjectRoleDO before,
                                     UserObjectRoleDO after,
                                     String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ObjectActivityConstants.MEMBER_BIZ_TYPE);
        auditLog.setBizId(member.getId());
        auditLog.setActionType(actionType);
        auditLog.setFieldChanges(buildMemberFieldChanges(before, after));
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private void writeManagerChangeAuditLog(Long projectId, Long beforeManagerUserId, Long afterManagerUserId, String reason) {
        if (Objects.equals(beforeManagerUserId, afterManagerUserId)) {
            return;
        }
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ProjectObjectConstants.OBJECT_TYPE);
        auditLog.setBizId(projectId);
        auditLog.setActionType(ObjectActivityConstants.PROJECT_ACTION_CHANGE_MANAGER);
        auditLog.setFieldChanges(buildManagerFieldChanges(beforeManagerUserId, afterManagerUserId));
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    /**
     * 批量移出场景的聚合审计：写一条对象维度日志（bizType=project, bizId=projectId），
     * fieldChanges 体现批量摘要 batchCount + members 数组，与 writeManagerChangeAuditLog 风格对齐。
     * 单条 before/after 字段差异在聚合视图下故意丢弃，由读侧按需展开。
     */
    private void writeBatchInactiveAuditLog(Long projectId, List<UserObjectRoleDO> members, String reason) {
        if (members.isEmpty()) {
            return;
        }
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ProjectObjectConstants.OBJECT_TYPE);
        auditLog.setBizId(projectId);
        auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_REMOVE);
        auditLog.setFieldChanges(buildBatchInactiveFieldChanges(members));
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private String buildBatchInactiveFieldChanges(List<UserObjectRoleDO> members) {
        List<Map<String, Object>> memberSummaries = new ArrayList<>(members.size());
        for (UserObjectRoleDO member : members) {
            Map<String, Object> summary = new LinkedHashMap<>();
            summary.put("memberId", member.getId());
            summary.put("userId", member.getUserId());
            summary.put("roleId", member.getRoleId());
            memberSummaries.add(summary);
        }
        Map<String, Object> root = new LinkedHashMap<>();
        root.put("batchCount", members.size());
        root.put("members", memberSummaries);
        return JsonUtils.toJsonString(root);
    }

    private String buildMemberFieldChanges(UserObjectRoleDO before, UserObjectRoleDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "userId", valueOf(before, UserObjectRoleDO::getUserId),
                valueOf(after, UserObjectRoleDO::getUserId));
        appendFieldChange(fieldChanges, "roleId", valueOf(before, UserObjectRoleDO::getRoleId),
                valueOf(after, UserObjectRoleDO::getRoleId));
        appendFieldChange(fieldChanges, "status", valueOf(before, UserObjectRoleDO::getStatus),
                valueOf(after, UserObjectRoleDO::getStatus));
        appendFieldChange(fieldChanges, "joinedTime", valueOf(before, UserObjectRoleDO::getJoinedTime),
                valueOf(after, UserObjectRoleDO::getJoinedTime));
        appendFieldChange(fieldChanges, "leftTime", valueOf(before, UserObjectRoleDO::getLeftTime),
                valueOf(after, UserObjectRoleDO::getLeftTime));
        appendFieldChange(fieldChanges, "remark", valueOf(before, UserObjectRoleDO::getRemark),
                valueOf(after, UserObjectRoleDO::getRemark));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private String buildManagerFieldChanges(Long beforeManagerUserId, Long afterManagerUserId) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "managerUserId", beforeManagerUserId, afterManagerUserId);
        return JsonUtils.toJsonString(fieldChanges);
    }

    private UserObjectRoleDO cloneMember(UserObjectRoleDO source) {
        UserObjectRoleDO clone = new UserObjectRoleDO();
        clone.setId(source.getId());
        clone.setUserId(source.getUserId());
        clone.setObjectType(source.getObjectType());
        clone.setObjectId(source.getObjectId());
        clone.setRoleId(source.getRoleId());
        clone.setStatus(source.getStatus());
        clone.setJoinedTime(source.getJoinedTime());
        clone.setLeftTime(source.getLeftTime());
        clone.setRemark(source.getRemark());
        return clone;
    }

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

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

}
