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

import cn.iocoder.lyzsys.framework.common.util.collection.CollectionUtils;
import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
import cn.iocoder.lyzsys.module.tjt.controller.admin.planningguidedetail.vo.ProjectPlanningGuideDetailBatchSaveReqVO;
import cn.iocoder.lyzsys.module.tjt.controller.admin.planningguidedetail.vo.ProjectPlanningGuideDetailSaveReqVO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningquarter.ProjectPlanningQuarterDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planningguidedetail.ProjectPlanningGuideDetailDO;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.planning.ProjectPlanningMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.planningquarter.ProjectPlanningQuarterMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.planningguidedetail.ProjectPlanningGuideDetailMapper;
import cn.iocoder.lyzsys.module.tjt.enums.ProjectPlanningBizTypeConstants;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
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.PROJECT_PLANNING_DESIGN_PART_INVALID;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_GUIDE_DETAIL_NOT_EXISTS;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_GUIDE_DETAIL_SCENE_INVALID;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_NOT_EXISTS;

@Service
@Validated
public class ProjectPlanningGuideDetailServiceImpl implements ProjectPlanningGuideDetailService {

    private static final int AMOUNT_SCALE = 2;
    private static final int RATIO_SCALE = 4;

    private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
    private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);

    @Resource
    private ProjectPlanningGuideDetailMapper projectPlanningGuideDetailMapper;
    @Resource
    private ProjectPlanningMapper projectPlanningMapper;
    @Resource
    private ProjectPlanningQuarterMapper projectPlanningQuarterMapper;

    @Override
    public void batchSaveProjectPlanningGuideDetail(ProjectPlanningGuideDetailBatchSaveReqVO reqVO) {
        ProjectPlanningDO planning = validateGuideDetailPlanning(reqVO.getPlanningId());
        List<ProjectPlanningGuideDetailSaveReqVO> reqDetails = reqVO.getDetails() == null
                ? Collections.emptyList() : reqVO.getDetails();
        List<ProjectPlanningGuideDetailDO> existingList = projectPlanningGuideDetailMapper.selectListByPlanningId(reqVO.getPlanningId());
        Map<Long, ProjectPlanningGuideDetailDO> existingMap =
                CollectionUtils.convertMap(existingList, ProjectPlanningGuideDetailDO::getId);
        Set<Long> retainedIds = reqDetails.stream()
                .map(ProjectPlanningGuideDetailSaveReqVO::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());

        for (int i = 0; i < reqDetails.size(); i++) {
            ProjectPlanningGuideDetailSaveReqVO detailReqVO = reqDetails.get(i);
            validateGuideDetailForSave(detailReqVO);
            ProjectPlanningGuideDetailDO detail = BeanUtils.toBean(detailReqVO, ProjectPlanningGuideDetailDO.class);
            detail.setPlanningId(planning.getId());
            detail.setProjectId(planning.getProjectId());
            detail.setSortNo(detailReqVO.getSortNo() == null ? i + 1 : detailReqVO.getSortNo());
            calculateGuideDetail(detail, planning.getReviewOutsourceRatio());
            if (detail.getId() == null) {
                projectPlanningGuideDetailMapper.insert(detail);
                continue;
            }
            if (!existingMap.containsKey(detail.getId())) {
                throw exception(PROJECT_PLANNING_GUIDE_DETAIL_NOT_EXISTS);
            }
            projectPlanningGuideDetailMapper.updateById(detail);
        }

        List<Long> deletedIds = existingList.stream()
                .map(ProjectPlanningGuideDetailDO::getId)
                .filter(id -> !retainedIds.contains(id))
                .collect(Collectors.toList());
        if (!deletedIds.isEmpty()) {
            projectPlanningGuideDetailMapper.deleteBatchIds(deletedIds);
        }

        recalculatePlanningSummary(planning.getId());
    }

    @Override
    public void deleteProjectPlanningGuideDetail(Long id) {
        ProjectPlanningGuideDetailDO detail = projectPlanningGuideDetailMapper.selectById(id);
        if (detail == null) {
            throw exception(PROJECT_PLANNING_GUIDE_DETAIL_NOT_EXISTS);
        }
        projectPlanningGuideDetailMapper.deleteById(id);
        recalculatePlanningSummary(detail.getPlanningId());
    }

    @Override
    public List<ProjectPlanningGuideDetailDO> getProjectPlanningGuideDetailListByPlanningId(Long planningId) {
        validatePlanningExists(planningId);
        return projectPlanningGuideDetailMapper.selectListByPlanningId(planningId);
    }

    @Override
    public Map<Long, List<ProjectPlanningGuideDetailDO>> getProjectPlanningGuideDetailMapByPlanningIds(Collection<Long> planningIds) {
        if (planningIds == null || planningIds.isEmpty()) {
            return Collections.emptyMap();
        }
        return CollectionUtils.convertMultiMap(
                projectPlanningGuideDetailMapper.selectListByPlanningIds(planningIds),
                ProjectPlanningGuideDetailDO::getPlanningId
        );
    }

    @Override
    public void deleteByPlanningId(Long planningId) {
        if (planningId == null) {
            return;
        }
        projectPlanningGuideDetailMapper.delete(ProjectPlanningGuideDetailDO::getPlanningId, planningId);
    }

    @Override
    public void deleteByPlanningIds(Collection<Long> planningIds) {
        if (planningIds == null || planningIds.isEmpty()) {
            return;
        }
        projectPlanningGuideDetailMapper.deleteBatch(ProjectPlanningGuideDetailDO::getPlanningId, planningIds);
    }

    @Override
    public void handlePlanningSaved(ProjectPlanningDO planning, ProjectPlanningDO dbPlanning) {
        boolean currentGuideScene = isGuideDetailScene(planning);
        boolean previousGuideScene = isGuideDetailScene(dbPlanning);
        if (!currentGuideScene) {
            if (previousGuideScene) {
                deleteByPlanningId(planning.getId());
            }
            return;
        }
        recalculateGuideDetails(planning.getId(), ratio(planning.getReviewOutsourceRatio()));
        recalculatePlanningSummary(planning.getId());
    }

    private void recalculateGuideDetails(Long planningId, BigDecimal reviewOutsourceRatio) {
        List<ProjectPlanningGuideDetailDO> detailList = projectPlanningGuideDetailMapper.selectListByPlanningId(planningId);
        if (detailList.isEmpty()) {
            return;
        }
        for (ProjectPlanningGuideDetailDO detail : detailList) {
            calculateGuideDetail(detail, reviewOutsourceRatio);
            projectPlanningGuideDetailMapper.updateById(detail);
        }
    }

    private void recalculatePlanningSummary(Long planningId) {
        ProjectPlanningDO planning = validatePlanningExists(planningId);
        List<ProjectPlanningGuideDetailDO> detailList = projectPlanningGuideDetailMapper.selectListByPlanningId(planningId);

        BigDecimal totalDesignArea = ZERO_AMOUNT;
        BigDecimal totalAssessmentArea = ZERO_AMOUNT;
        BigDecimal totalAssessmentOutputValue = ZERO_AMOUNT;
        for (ProjectPlanningGuideDetailDO detail : detailList) {
            totalDesignArea = totalDesignArea.add(amount(detail.getDesignArea())).setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
            totalAssessmentArea = totalAssessmentArea.add(amount(detail.getAssessmentArea())).setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
            totalAssessmentOutputValue = totalAssessmentOutputValue.add(amount(detail.getAssessmentOutputValue()))
                    .setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
        }

        ProjectPlanningDO updateObj = new ProjectPlanningDO();
        updateObj.setId(planningId);
        updateObj.setPlanningArea(totalDesignArea);
        updateObj.setContractUnitPrice(divideAmount(planning.getPlanningAmount(), totalDesignArea));
        updateObj.setTotalAdjustmentFactor(ZERO_RATIO);
        updateObj.setAssessmentArea(totalAssessmentArea);
        updateObj.setVirtualOutputValue(ZERO_AMOUNT);
        updateObj.setAssessmentOutputValue(totalAssessmentOutputValue);
        projectPlanningMapper.updateById(updateObj);

        ProjectPlanningDO refreshedPlanning = projectPlanningMapper.selectById(planningId);
        refreshQuarterDistributionAmounts(refreshedPlanning);
    }

    private void validateGuideDetailForSave(ProjectPlanningGuideDetailSaveReqVO reqVO) {
        if (!ProjectPlanningBizTypeConstants.isValidDesignPart(reqVO.getDesignPart())) {
            throw exception(PROJECT_PLANNING_DESIGN_PART_INVALID);
        }
    }

    private ProjectPlanningDO validateGuideDetailPlanning(Long planningId) {
        ProjectPlanningDO planning = validatePlanningExists(planningId);
        if (!isGuideDetailScene(planning)) {
            throw exception(PROJECT_PLANNING_GUIDE_DETAIL_SCENE_INVALID);
        }
        return planning;
    }

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

    private boolean isGuideDetailScene(ProjectPlanningDO planning) {
        return planning != null && ProjectPlanningBizTypeConstants.isMajorGuidanceScene(
                planning.getOwnershipType(), planning.getCalculationMethod());
    }

    private void calculateGuideDetail(ProjectPlanningGuideDetailDO detail, BigDecimal reviewOutsourceRatio) {
        detail.setDesignArea(amount(detail.getDesignArea()));
        detail.setInternalGuidanceUnitPrice(amount(detail.getInternalGuidanceUnitPrice()));
        detail.setDrawingSetFactor(ratio(detail.getDrawingSetFactor()));
        detail.setScaleFactor(ratio(detail.getScaleFactor()));
        detail.setModificationFactor(ratio(detail.getModificationFactor()));
        detail.setComplexityFactor(ratio(detail.getComplexityFactor()));
        detail.setDesignRatio(ratio(detail.getDesignRatio()));
        detail.setTotalAdjustmentFactor(multiplyRatio(
                detail.getDrawingSetFactor(),
                detail.getScaleFactor(),
                detail.getModificationFactor(),
                detail.getComplexityFactor()
        ));
        detail.setAssessmentArea(multiplyAmount(detail.getDesignArea(), detail.getTotalAdjustmentFactor()));
        detail.setAssessmentOutputValue(multiplyAmount(
                detail.getInternalGuidanceUnitPrice(),
                detail.getAssessmentArea(),
                detail.getDesignRatio(),
                oneMinus(reviewOutsourceRatio)
        ));
    }

    private void refreshQuarterDistributionAmounts(ProjectPlanningDO planning) {
        if (planning == null) {
            return;
        }
        List<ProjectPlanningQuarterDO> quarterList = projectPlanningQuarterMapper.selectListByPlanningId(planning.getId());
        for (ProjectPlanningQuarterDO quarter : quarterList) {
            quarter.setDistributionAmount(multiplyAmount(
                    planning.getAssessmentOutputValue(),
                    planning.getTotalDistributionAmount(),
                    ratio(quarter.getDistributionRatio())
            ));
            projectPlanningQuarterMapper.updateById(quarter);
        }
    }

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

    private BigDecimal oneMinus(BigDecimal value) {
        return BigDecimal.ONE.subtract(ratio(value)).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal multiplyAmount(BigDecimal... values) {
        BigDecimal result = BigDecimal.ONE;
        for (BigDecimal value : values) {
            result = result.multiply(value == null ? BigDecimal.ZERO : value);
        }
        return result.setScale(AMOUNT_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal multiplyRatio(BigDecimal... values) {
        BigDecimal result = BigDecimal.ONE;
        for (BigDecimal value : values) {
            result = result.multiply(value == null ? BigDecimal.ZERO : value);
        }
        return result.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal divideAmount(BigDecimal dividend, BigDecimal divisor) {
        if (dividend == null || divisor == null || divisor.compareTo(BigDecimal.ZERO) == 0) {
            return ZERO_AMOUNT;
        }
        return dividend.divide(divisor, AMOUNT_SCALE, RoundingMode.HALF_UP);
    }

}
