package cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit;

import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
import cn.iocoder.lyzsys.module.tjt.controller.admin.specialtyrolesplit.vo.*;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.employee.EmployeeDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.outputsplit.ProjectOutputSplitDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.projectroleperson.ProjectRolePersonDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.specialtyrolesplit.SpecialtyRoleSplitDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.specialtyrolesplitperson.SpecialtyRoleSplitPersonDO;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.projectroleperson.ProjectRolePersonMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.specialtyrolesplit.SpecialtyRoleSplitMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.specialtyrolesplitperson.SpecialtyRoleSplitPersonMapper;
import cn.iocoder.lyzsys.module.tjt.enums.OutputSplitBizConstants;
import cn.iocoder.lyzsys.module.tjt.service.employee.EmployeeService;
import cn.iocoder.lyzsys.module.tjt.service.outputsplit.ProjectOutputSplitService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;

import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.*;

@Service
@Validated
public class SpecialtyRoleSplitServiceImpl implements SpecialtyRoleSplitService {

    private static final int RATIO_SCALE = 4;
    private static final int AMOUNT_SCALE = 2;
    private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    private static final BigDecimal ONE_RATIO = BigDecimal.ONE.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    private static final BigDecimal HALF_RATIO = new BigDecimal("0.5000");
    private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);

    @Resource
    private SpecialtyRoleSplitMapper specialtyRoleSplitMapper;
    @Resource
    private SpecialtyRoleSplitPersonMapper specialtyRoleSplitPersonMapper;
    @Resource
    private ProjectOutputSplitService projectOutputSplitService;
    @Resource
    private ProjectPlanningMapper projectPlanningMapper;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectRolePersonMapper projectRolePersonMapper;
    @Resource
    private EmployeeService employeeService;

    @Override
    public List<SpecialtyRoleSplitRespVO> getSpecialtyRoleSplitListByPlanningId(Long planningId) {
        ProjectPlanningDO planning = validatePlanningExists(planningId);
        validateProjectExists(planning.getProjectId());
        return getSpecialtyRoleSplitListMapByPlanningIds(Collections.singletonList(planningId))
                .getOrDefault(planningId, Collections.emptyList());
    }

    @Override
    public Map<Long, List<SpecialtyRoleSplitRespVO>> getSpecialtyRoleSplitListMapByPlanningIds(Collection<Long> planningIds) {
        if (planningIds == null || planningIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<Long> normalizedPlanningIds = planningIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (normalizedPlanningIds.isEmpty()) {
            return Collections.emptyMap();
        }

        Map<Long, ProjectPlanningDO> planningMap = projectPlanningMapper.selectBatchIds(normalizedPlanningIds).stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(ProjectPlanningDO::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
        if (planningMap.isEmpty()) {
            return Collections.emptyMap();
        }
        Set<Long> projectIds = planningMap.values().stream()
                .map(ProjectPlanningDO::getProjectId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Map<Long, ProjectDO> projectMap = projectIds.isEmpty()
                ? Collections.emptyMap()
                : projectMapper.selectBatchIds(projectIds).stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(ProjectDO::getId, item -> item, (a, b) -> a, LinkedHashMap::new));

        Map<Long, ProjectOutputSplitDO> outputSplitMap = new LinkedHashMap<>(
                projectOutputSplitService.getProjectOutputSplitMap(planningMap.keySet()));
        for (ProjectPlanningDO planning : planningMap.values()) {
            if (!outputSplitMap.containsKey(planning.getId())) {
                outputSplitMap.put(planning.getId(),
                        projectOutputSplitService.getOrCreateProjectOutputSplit(planning.getId()));
            }
        }

        List<Long> outputSplitIds = outputSplitMap.values().stream()
                .filter(Objects::nonNull)
                .map(ProjectOutputSplitDO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        Map<Long, List<SpecialtyRoleSplitDO>> roleSplitMap = specialtyRoleSplitMapper
                .selectListByOutputSplitIds(outputSplitIds).stream()
                .collect(Collectors.groupingBy(SpecialtyRoleSplitDO::getOutputSplitId,
                        LinkedHashMap::new, Collectors.toList()));
        List<Long> roleSplitIds = roleSplitMap.values().stream()
                .flatMap(Collection::stream)
                .map(SpecialtyRoleSplitDO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        Map<Long, List<SpecialtyRoleSplitPersonDO>> personMap = specialtyRoleSplitPersonMapper
                .selectListByRoleSplitIds(roleSplitIds)
                .stream()
                .collect(Collectors.groupingBy(SpecialtyRoleSplitPersonDO::getRoleSplitId,
                        LinkedHashMap::new, Collectors.toList()));

        Map<Long, Map<String, List<ProjectRolePersonDO>>> projectRolePersonMap = projectRolePersonMapper
                .selectListByProjectIds(projectIds).stream()
                .collect(Collectors.groupingBy(ProjectRolePersonDO::getProjectId, LinkedHashMap::new,
                        Collectors.groupingBy(ProjectRolePersonDO::getRoleCode, LinkedHashMap::new, Collectors.toList())));

        Map<Long, List<SpecialtyRoleSplitRespVO>> result = new LinkedHashMap<>();
        for (Long planningId : normalizedPlanningIds) {
            ProjectPlanningDO planning = planningMap.get(planningId);
            if (planning == null) {
                continue;
            }
            ProjectOutputSplitDO outputSplit = outputSplitMap.get(planningId);
            if (outputSplit == null) {
                continue;
            }
            ProjectDO project = projectMap.get(planning.getProjectId());
            List<SpecialtyRoleSplitDO> roleList = roleSplitMap.getOrDefault(outputSplit.getId(), Collections.emptyList());
            Map<String, List<ProjectRolePersonDO>> projectPersons = projectRolePersonMap
                    .getOrDefault(planning.getProjectId(), Collections.emptyMap());
            result.put(planningId, buildRoleSplitRespList(planning, project, outputSplit, roleList, projectPersons, personMap));
        }
        return result;
    }

    private List<SpecialtyRoleSplitRespVO> buildRoleSplitRespList(ProjectPlanningDO planning,
                                                                  ProjectDO project,
                                                                  ProjectOutputSplitDO outputSplit,
                                                                  List<SpecialtyRoleSplitDO> dbList,
                                                                  Map<String, List<ProjectRolePersonDO>> projectRolePersonMap,
                                                                  Map<Long, List<SpecialtyRoleSplitPersonDO>> personMap) {
        List<SpecialtyRoleSplitDO> safeDbList = dbList == null ? Collections.emptyList() : dbList;
        Map<String, SpecialtyRoleSplitDO> dbMap = safeDbList.stream().collect(Collectors.toMap(
                item -> item.getSpecialtyCode() + ":" + item.getRoleCode(), item -> item, (a, b) -> b));
        List<SpecialtyRoleSplitRespVO> result = new ArrayList<>();
        BigDecimal assessmentOutputValue = planning.getAssessmentOutputValue();
        boolean useDefaultProjectLeadRoleRatio = shouldUseDefaultProjectLeadRoleRatio(dbMap, projectRolePersonMap);
        for (OutputSplitBizConstants.SpecialtyItem specialtyItem : OutputSplitBizConstants.ASSIGNMENT_SPECIALTY_ITEMS) {
            BigDecimal specialtyAmount = getSpecialtyAmount(outputSplit, assessmentOutputValue, specialtyItem.getCode());
            Map<String, BigDecimal> defaultRoleRatioMap = buildDefaultRoleRatioMap(
                    specialtyItem.getCode(), projectRolePersonMap);
            for (OutputSplitBizConstants.RoleItem roleItem : OutputSplitBizConstants.getRoleItems(specialtyItem.getCode())) {
                SpecialtyRoleSplitDO dbItem = dbMap.get(specialtyItem.getCode() + ":" + roleItem.getCode());
                BigDecimal roleRatio = useDefaultProjectLeadRoleRatio
                        && OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyItem.getCode())
                        ? ratio(defaultRoleRatioMap.get(roleItem.getCode()))
                        : getStoredRoleRatio(dbItem, specialtyItem.getCode(), roleItem.getCode(), defaultRoleRatioMap);
                BigDecimal roleAmount = multiplyAmount(specialtyAmount, roleRatio);
                List<SpecialtyRolePersonRespVO> persons = buildRespPersons(
                        dbItem, specialtyItem.getCode(), roleItem.getCode(), roleAmount, projectRolePersonMap, personMap);
                SpecialtyRoleSplitRespVO respVO = dbItem == null
                        ? new SpecialtyRoleSplitRespVO()
                        : BeanUtils.toBean(dbItem, SpecialtyRoleSplitRespVO.class);
                if (respVO.getId() == null) {
                    respVO.setOutputSplitId(outputSplit.getId());
                    respVO.setSortNo(OutputSplitBizConstants.getSpecialtySortNo(specialtyItem.getCode()) * 10
                            + OutputSplitBizConstants.getRoleSortNo(roleItem.getCode()));
                }
                respVO.setPlanningId(planning.getId());
                respVO.setProjectName(project == null ? "" : project.getProjectName());
                respVO.setPlanningContent(planning.getPlanningContent());
                respVO.setSpecialtyCode(specialtyItem.getCode());
                respVO.setSpecialtyName(specialtyItem.getName());
                respVO.setSpecialtyAmount(specialtyAmount);
                respVO.setRoleCode(roleItem.getCode());
                respVO.setRoleName(roleItem.getName());
                respVO.setRoleRatio(roleRatio);
                respVO.setRoleAmount(roleAmount);
                respVO.setPersons(persons);
                result.add(respVO);
            }
        }
        return result;
    }

    private boolean shouldUseDefaultProjectLeadRoleRatio(Map<String, SpecialtyRoleSplitDO> dbMap,
                                                         Map<String, List<ProjectRolePersonDO>> projectRolePersonMap) {
        if (projectRolePersonMap == null || projectRolePersonMap.isEmpty()) {
            return false;
        }
        boolean hasProjectLeadPerson = (projectRolePersonMap.get(OutputSplitBizConstants.ROLE_PROJECT_MANAGER) != null
                && !projectRolePersonMap.get(OutputSplitBizConstants.ROLE_PROJECT_MANAGER).isEmpty())
                || (projectRolePersonMap.get(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL) != null
                && !projectRolePersonMap.get(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL).isEmpty());
        if (!hasProjectLeadPerson || dbMap == null || dbMap.isEmpty()) {
            return false;
        }
        BigDecimal storedTotal = ZERO_RATIO;
        boolean hasStoredProjectLead = false;
        for (String roleCode : Arrays.asList(
                OutputSplitBizConstants.ROLE_PROJECT_MANAGER,
                OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL)) {
            SpecialtyRoleSplitDO dbItem = dbMap.get(OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD + ":" + roleCode);
            if (dbItem == null) {
                continue;
            }
            hasStoredProjectLead = true;
            storedTotal = storedTotal.add(ratio(dbItem.getRoleRatio())).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
        }
        return hasStoredProjectLead && storedTotal.compareTo(ZERO_RATIO) == 0;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveSpecialtyRoleSplitBatch(SpecialtyRoleSplitBatchSaveReqVO reqVO) {
        ProjectPlanningDO planning = validatePlanningExists(reqVO.getPlanningId());
        ProjectOutputSplitDO outputSplit = projectOutputSplitService.getOrCreateProjectOutputSplit(reqVO.getPlanningId());
        List<SpecialtyRoleSplitDO> dbList = specialtyRoleSplitMapper.selectListByOutputSplitId(outputSplit.getId());
        Map<String, SpecialtyRoleSplitDO> dbMap = dbList.stream().collect(Collectors.toMap(
                item -> item.getSpecialtyCode() + ":" + item.getRoleCode(), item -> item, (a, b) -> b));
        Map<String, Map<String, SpecialtyRoleSplitSaveItemReqVO>> groupedMap = buildGroupedInput(reqVO.getItems());
        BigDecimal assessmentOutputValue = planning.getAssessmentOutputValue();
        boolean temporarySave = Boolean.TRUE.equals(reqVO.getTemporarySave());

        saveSpecialtyRoleSplits(outputSplit, dbMap, groupedMap, assessmentOutputValue, !temporarySave);
    }

    /**
     * 专业人员分配共用保存流程：正式保存执行强校验，临时保存仅做基础格式处理后落库。
     */
    private void saveSpecialtyRoleSplits(ProjectOutputSplitDO outputSplit,
                                         Map<String, SpecialtyRoleSplitDO> dbMap,
                                         Map<String, Map<String, SpecialtyRoleSplitSaveItemReqVO>> groupedMap,
                                         BigDecimal assessmentOutputValue,
                                         boolean strictValidate) {
        for (OutputSplitBizConstants.SpecialtyItem specialtyItem : OutputSplitBizConstants.ASSIGNMENT_SPECIALTY_ITEMS) {
            if (!groupedMap.containsKey(specialtyItem.getCode())) {
                continue;
            }
            Map<String, SpecialtyRoleSplitSaveItemReqVO> roleMap = groupedMap.getOrDefault(
                    specialtyItem.getCode(), new LinkedHashMap<>());
            BigDecimal roleTotal = ZERO_RATIO;
            BigDecimal specialtyAmount = getSpecialtyAmount(outputSplit, assessmentOutputValue, specialtyItem.getCode());
            for (OutputSplitBizConstants.RoleItem roleItem : OutputSplitBizConstants.getRoleItems(specialtyItem.getCode())) {
                BigDecimal roleRatio = getSaveRoleRatio(roleMap, specialtyItem.getCode(), roleItem.getCode());
                BigDecimal roleAmount = multiplyAmount(specialtyAmount, roleRatio);
                List<SpecialtyRolePersonSaveReqVO> persons = normalizePersons(
                        getSavePersons(roleMap, roleItem.getCode()), strictValidate);
                if (strictValidate) {
                    validateRolePersons(roleItem.getCode(), roleAmount, persons);
                }
                roleTotal = roleTotal.add(roleRatio).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
                SpecialtyRoleSplitDO role = upsertRole(dbMap, outputSplit.getId(), specialtyItem.getCode(),
                        specialtyItem.getName(), roleItem.getCode(), roleRatio);
                refreshRolePersons(role, outputSplit, persons);
            }
            if (strictValidate) {
                validateRoleTotal(roleTotal);
            }
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteByOutputSplitId(Long outputSplitId) {
        specialtyRoleSplitPersonMapper.deleteByOutputSplitId(outputSplitId);
        specialtyRoleSplitMapper.delete(SpecialtyRoleSplitDO::getOutputSplitId, outputSplitId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteByOutputSplitIds(List<Long> outputSplitIds) {
        if (outputSplitIds == null || outputSplitIds.isEmpty()) {
            return;
        }
        specialtyRoleSplitPersonMapper.deleteByOutputSplitIds(outputSplitIds);
        specialtyRoleSplitMapper.delete(new cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX<SpecialtyRoleSplitDO>()
                .in(SpecialtyRoleSplitDO::getOutputSplitId, outputSplitIds));
    }

    private Map<String, Map<String, SpecialtyRoleSplitSaveItemReqVO>> buildGroupedInput(List<SpecialtyRoleSplitSaveItemReqVO> items) {
        Map<String, Map<String, SpecialtyRoleSplitSaveItemReqVO>> result = new LinkedHashMap<>();
        if (items == null || items.isEmpty()) {
            return result;
        }
        for (SpecialtyRoleSplitSaveItemReqVO item : items) {
            if (!OutputSplitBizConstants.isValidSpecialtyCode(item.getSpecialtyCode())) {
                throw exception(SPECIALTY_ROLE_SPLIT_SPECIALTY_INVALID);
            }
            if (!OutputSplitBizConstants.isValidRoleCode(item.getSpecialtyCode(), item.getRoleCode())) {
                throw exception(SPECIALTY_ROLE_SPLIT_ROLE_INVALID);
            }
            result.computeIfAbsent(item.getSpecialtyCode(), key -> new LinkedHashMap<>())
                    .put(item.getRoleCode(), item);
        }
        return result;
    }

    private SpecialtyRoleSplitDO upsertRole(Map<String, SpecialtyRoleSplitDO> dbMap, Long outputSplitId,
                                            String specialtyCode, String specialtyName, String roleCode,
                                            BigDecimal roleRatio) {
        String key = specialtyCode + ":" + roleCode;
        SpecialtyRoleSplitDO dbItem = dbMap.get(key);
        Integer sortNo = dbItem != null && dbItem.getSortNo() != null
                ? dbItem.getSortNo()
                : OutputSplitBizConstants.getSpecialtySortNo(specialtyCode) * 10
                + OutputSplitBizConstants.getRoleSortNo(roleCode);
        if (dbItem == null) {
            SpecialtyRoleSplitDO createObj = new SpecialtyRoleSplitDO();
            createObj.setOutputSplitId(outputSplitId);
            createObj.setSpecialtyCode(specialtyCode);
            createObj.setSpecialtyName(specialtyName);
            createObj.setRoleCode(roleCode);
            createObj.setRoleName(OutputSplitBizConstants.getRoleName(roleCode));
            createObj.setRoleRatio(ratio(roleRatio));
            createObj.setSortNo(sortNo);
            specialtyRoleSplitMapper.insert(createObj);
            dbMap.put(key, createObj);
            return createObj;
        }
        SpecialtyRoleSplitDO updateObj = new SpecialtyRoleSplitDO();
        updateObj.setId(dbItem.getId());
        updateObj.setOutputSplitId(outputSplitId);
        updateObj.setSpecialtyCode(specialtyCode);
        updateObj.setSpecialtyName(specialtyName);
        updateObj.setRoleCode(roleCode);
        updateObj.setRoleName(OutputSplitBizConstants.getRoleName(roleCode));
        updateObj.setRoleRatio(ratio(roleRatio));
        updateObj.setSortNo(sortNo);
        specialtyRoleSplitMapper.updateById(updateObj);
        dbMap.put(key, updateObj);
        return updateObj;
    }

    private void refreshRolePersons(SpecialtyRoleSplitDO role, ProjectOutputSplitDO outputSplit,
                                    List<SpecialtyRolePersonSaveReqVO> persons) {
        if (role == null || role.getId() == null) {
            return;
        }
        specialtyRoleSplitPersonMapper.deleteByRoleSplitId(role.getId());
        if (OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(role.getSpecialtyCode())
                || persons == null || persons.isEmpty()) {
            return;
        }
        List<SpecialtyRoleSplitPersonDO> createList = new ArrayList<>();
        for (int i = 0; i < persons.size(); i++) {
            SpecialtyRolePersonSaveReqVO person = persons.get(i);
            if (person == null || person.getEmployeeId() == null) {
                continue;
            }
            SpecialtyRoleSplitPersonDO createObj = new SpecialtyRoleSplitPersonDO();
            createObj.setRoleSplitId(role.getId());
            createObj.setOutputSplitId(outputSplit.getId());
            createObj.setProjectId(outputSplit.getProjectId());
            createObj.setPlanningId(outputSplit.getPlanningId());
            createObj.setSpecialtyCode(role.getSpecialtyCode());
            createObj.setSpecialtyName(role.getSpecialtyName());
            createObj.setRoleCode(role.getRoleCode());
            createObj.setRoleName(role.getRoleName());
            createObj.setEmployeeId(person.getEmployeeId());
            createObj.setEmployeeName(person.getEmployeeName());
            createObj.setPersonRatio(ratio(person.getPersonRatio()));
            createObj.setSortNo(i + 1);
            createList.add(createObj);
        }
        if (!createList.isEmpty()) {
            specialtyRoleSplitPersonMapper.insertBatch(createList);
        }
    }

    private List<SpecialtyRolePersonSaveReqVO> getSavePersons(Map<String, SpecialtyRoleSplitSaveItemReqVO> roleMap,
                                                              String roleCode) {
        SpecialtyRoleSplitSaveItemReqVO item = roleMap.get(roleCode);
        return item == null ? new ArrayList<>() : item.getPersons();
    }

    private BigDecimal getSaveRoleRatio(Map<String, SpecialtyRoleSplitSaveItemReqVO> roleMap,
                                        String specialtyCode, String roleCode) {
        SpecialtyRoleSplitSaveItemReqVO item = roleMap.get(roleCode);
        BigDecimal roleRatio = item == null ? ZERO_RATIO : ratio(item.getRoleRatio());
        if (!OutputSplitBizConstants.isValidRoleCode(specialtyCode, roleCode)) {
            throw exception(SPECIALTY_ROLE_SPLIT_ROLE_INVALID);
        }
        if (roleRatio.compareTo(ZERO_RATIO) < 0 || roleRatio.compareTo(ONE_RATIO) > 0) {
            throw exception(SPECIALTY_ROLE_SPLIT_ROLE_RATIO_INVALID);
        }
        return roleRatio;
    }

    private List<SpecialtyRolePersonSaveReqVO> normalizePersons(List<SpecialtyRolePersonSaveReqVO> persons,
                                                               boolean strictValidate) {
        List<SpecialtyRolePersonSaveReqVO> result = new ArrayList<>();
        if (persons == null || persons.isEmpty()) {
            return result;
        }
        for (SpecialtyRolePersonSaveReqVO person : persons) {
            if (person == null) {
                continue;
            }
            Long employeeId = person.getEmployeeId();
            BigDecimal personRatio = person.getPersonRatio();
            boolean hasEmployee = employeeId != null;
            boolean hasRatio = personRatio != null;
            if (!hasEmployee && !hasRatio) {
                continue;
            }
            if (!strictValidate && !hasEmployee) {
                // 临时保存允许半成品输入，没有选员工的行不落库。
                continue;
            }
            if (strictValidate && (!hasEmployee || !hasRatio)) {
                throw exception(SPECIALTY_ROLE_SPLIT_PERSON_INVALID);
            }
            EmployeeDO employee = employeeService.validateEmployeeExists(employeeId);
            SpecialtyRolePersonSaveReqVO normalized = new SpecialtyRolePersonSaveReqVO();
            normalized.setEmployeeId(employeeId);
            normalized.setEmployeeName(employee.getEmployeeName());
            BigDecimal normalizedRatio = ratio(strictValidate ? personRatio : (personRatio == null ? ZERO_RATIO : personRatio));
            if (normalizedRatio.compareTo(ZERO_RATIO) < 0 || normalizedRatio.compareTo(ONE_RATIO) > 0) {
                throw exception(SPECIALTY_ROLE_SPLIT_PERSON_RATIO_INVALID);
            }
            normalized.setPersonRatio(normalizedRatio);
            result.add(normalized);
        }
        return result;
    }

    private void validateRolePersons(String roleCode, BigDecimal roleAmount, List<SpecialtyRolePersonSaveReqVO> persons) {
        BigDecimal personTotal = sumPersonRatios(persons);
        if (personTotal.compareTo(ONE_RATIO) > 0) {
            throw exception(SPECIALTY_ROLE_SPLIT_PERSON_RATIO_INVALID);
        }
        if (OutputSplitBizConstants.ROLE_DESIGN.equals(roleCode)
                && roleAmount.compareTo(ZERO_AMOUNT) > 0
                && persons.isEmpty()) {
            throw exception(SPECIALTY_ROLE_SPLIT_DESIGN_PERSON_REQUIRED);
        }
    }

    private void validateRoleTotal(BigDecimal roleTotal) {
        if (ratio(roleTotal).compareTo(ONE_RATIO) != 0) {
            throw exception(SPECIALTY_ROLE_SPLIT_ROLE_RATIO_INVALID);
        }
    }

    private BigDecimal getStoredRoleRatio(SpecialtyRoleSplitDO dbItem, String specialtyCode, String roleCode,
                                          Map<String, BigDecimal> defaultRoleRatioMap) {
        if (dbItem != null) {
            return ratio(dbItem.getRoleRatio());
        }
        if (OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode)) {
            return ratio(defaultRoleRatioMap.get(roleCode));
        }
        return OutputSplitBizConstants.ROLE_DESIGN.equals(roleCode) ? ONE_RATIO : ZERO_RATIO;
    }

    private List<SpecialtyRolePersonRespVO> buildRespPersons(SpecialtyRoleSplitDO dbItem, String specialtyCode,
                                                             String roleCode, BigDecimal roleAmount,
                                                             Map<String, List<ProjectRolePersonDO>> projectRolePersonMap,
                                                             Map<Long, List<SpecialtyRoleSplitPersonDO>> personMap) {
        List<SpecialtyRolePersonSaveReqVO> sourcePersons;
        if (OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode)) {
            sourcePersons = buildDefaultProjectLeadPersons(projectRolePersonMap.get(roleCode));
        } else {
            sourcePersons = buildStoredPersons(dbItem, personMap);
        }
        List<SpecialtyRolePersonRespVO> result = new ArrayList<>();
        for (SpecialtyRolePersonSaveReqVO person : sourcePersons) {
            SpecialtyRolePersonRespVO respVO = new SpecialtyRolePersonRespVO();
            respVO.setEmployeeId(person.getEmployeeId());
            respVO.setEmployeeName(person.getEmployeeName());
            respVO.setPersonRatio(ratio(person.getPersonRatio()));
            respVO.setPersonAmount(multiplyAmount(roleAmount, respVO.getPersonRatio()));
            result.add(respVO);
        }
        return result;
    }

    private List<SpecialtyRolePersonSaveReqVO> buildStoredPersons(SpecialtyRoleSplitDO dbItem,
                                                                  Map<Long, List<SpecialtyRoleSplitPersonDO>> personMap) {
        if (dbItem == null || dbItem.getId() == null || personMap == null || personMap.isEmpty()) {
            return new ArrayList<>();
        }
        List<SpecialtyRoleSplitPersonDO> persons = personMap.get(dbItem.getId());
        if (persons == null || persons.isEmpty()) {
            return new ArrayList<>();
        }
        return persons.stream()
                .filter(Objects::nonNull)
                .map(person -> {
                    SpecialtyRolePersonSaveReqVO normalized = new SpecialtyRolePersonSaveReqVO();
                    normalized.setEmployeeId(person.getEmployeeId());
                    normalized.setEmployeeName(person.getEmployeeName());
                    normalized.setPersonRatio(ratio(person.getPersonRatio()));
                    return normalized;
                })
                .filter(person -> person.getEmployeeId() != null
                        && person.getEmployeeName() != null
                        && !person.getEmployeeName().trim().isEmpty())
                .collect(Collectors.toList());
    }

    private List<SpecialtyRolePersonSaveReqVO> buildDefaultProjectLeadPersons(List<ProjectRolePersonDO> rolePersons) {
        if (rolePersons == null || rolePersons.isEmpty()) {
            return new ArrayList<>();
        }
        List<SpecialtyRolePersonSaveReqVO> result = new ArrayList<>();
        BigDecimal perRatio = BigDecimal.ONE.divide(BigDecimal.valueOf(rolePersons.size()), RATIO_SCALE, RoundingMode.HALF_UP);
        BigDecimal accumulated = ZERO_RATIO;
        for (int i = 0; i < rolePersons.size(); i++) {
            ProjectRolePersonDO rolePerson = rolePersons.get(i);
            SpecialtyRolePersonSaveReqVO person = new SpecialtyRolePersonSaveReqVO();
            person.setEmployeeId(rolePerson.getEmployeeId());
            person.setEmployeeName(rolePerson.getEmployeeName());
            BigDecimal personRatio = i == rolePersons.size() - 1
                    ? ONE_RATIO.subtract(accumulated).setScale(RATIO_SCALE, RoundingMode.HALF_UP)
                    : perRatio;
            person.setPersonRatio(personRatio);
            accumulated = accumulated.add(personRatio).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
            result.add(person);
        }
        return result;
    }

    private Map<String, BigDecimal> buildDefaultRoleRatioMap(String specialtyCode,
                                                             Map<String, List<ProjectRolePersonDO>> projectRolePersonMap) {
        if (!OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode)) {
            return Collections.emptyMap();
        }
        boolean hasProjectManager = projectRolePersonMap.containsKey(OutputSplitBizConstants.ROLE_PROJECT_MANAGER)
                && !projectRolePersonMap.get(OutputSplitBizConstants.ROLE_PROJECT_MANAGER).isEmpty();
        boolean hasEngineeringPrincipal = projectRolePersonMap.containsKey(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL)
                && !projectRolePersonMap.get(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL).isEmpty();
        Map<String, BigDecimal> result = new LinkedHashMap<>();
        if (hasProjectManager && hasEngineeringPrincipal) {
            result.put(OutputSplitBizConstants.ROLE_PROJECT_MANAGER, HALF_RATIO);
            result.put(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL, HALF_RATIO);
            return result;
        }
        if (hasProjectManager) {
            result.put(OutputSplitBizConstants.ROLE_PROJECT_MANAGER, ONE_RATIO);
            result.put(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL, ZERO_RATIO);
            return result;
        }
        if (hasEngineeringPrincipal) {
            result.put(OutputSplitBizConstants.ROLE_PROJECT_MANAGER, ZERO_RATIO);
            result.put(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL, ONE_RATIO);
            return result;
        }
        result.put(OutputSplitBizConstants.ROLE_PROJECT_MANAGER, HALF_RATIO);
        result.put(OutputSplitBizConstants.ROLE_ENGINEERING_PRINCIPAL, HALF_RATIO);
        return result;
    }

    private BigDecimal getSpecialtyAmount(ProjectOutputSplitDO outputSplit, BigDecimal assessmentOutputValue,
                                          String specialtyCode) {
        if (OutputSplitBizConstants.SPECIALTY_PROJECT_LEAD.equals(specialtyCode)) {
            return multiplyAmount(assessmentOutputValue, outputSplit.getProjectLeadRatio());
        }
        return projectOutputSplitService.getSpecialtyAmount(outputSplit, assessmentOutputValue, specialtyCode);
    }

    private BigDecimal sumPersonRatios(List<SpecialtyRolePersonSaveReqVO> persons) {
        BigDecimal sum = ZERO_RATIO;
        if (persons == null || persons.isEmpty()) {
            return sum;
        }
        for (SpecialtyRolePersonSaveReqVO person : persons) {
            if (person == null) {
                continue;
            }
            sum = sum.add(ratio(person.getPersonRatio())).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
        }
        return sum;
    }

    private ProjectPlanningDO validatePlanningExists(Long planningId) {
        ProjectPlanningDO planning = projectPlanningMapper.selectById(planningId);
        if (planning == null) {
            throw exception(PROJECT_OUTPUT_SPLIT_PLANNING_NOT_EXISTS);
        }
        return planning;
    }

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

    private BigDecimal multiplyAmount(BigDecimal left, BigDecimal right) {
        return amount(left).multiply(ratio(right)).setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal amount(BigDecimal value) {
        return value == null ? ZERO_AMOUNT : value.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal ratio(BigDecimal value) {
        return value == null ? ZERO_RATIO : value.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    }

}
