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

import cn.hutool.core.util.StrUtil;
import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
import cn.iocoder.lyzsys.framework.common.util.object.BeanUtils;
import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningPageReqVO;
import cn.iocoder.lyzsys.module.tjt.controller.admin.planning.vo.ProjectPlanningSaveReqVO;
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.planningquarter.ProjectPlanningQuarterDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.project.ProjectDO;
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.project.ProjectMapper;
import cn.iocoder.lyzsys.module.tjt.service.outputsplit.ProjectOutputSplitService;
import cn.iocoder.lyzsys.module.tjt.service.planningguidedetail.ProjectPlanningGuideDetailService;
import cn.iocoder.lyzsys.module.tjt.service.specialtyrolesplit.SpecialtyRoleSplitService;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static cn.iocoder.lyzsys.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_CALCULATION_METHOD_INVALID;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_NOT_EXISTS;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_OWNERSHIP_TYPE_IMMUTABLE;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_OWNERSHIP_TYPE_INVALID;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_PROJECT_NOT_EXISTS;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_VIRTUAL_CALCULATION_METHOD_INVALID;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_WORKING_DAY_COUNT_REQUIRED;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_PLANNING_WORKING_DAY_UNIT_PRICE_REQUIRED;

/**
 * 合约规划 Service 实现类
 *
 * @author Codex
 */
@Service
@Validated
public class ProjectPlanningServiceImpl implements ProjectPlanningService {

    private static final int AMOUNT_SCALE = 2;
    private static final int RATIO_SCALE = 4;
    private static final int FACTOR_SCALE = 2;
    private static final int QUANTITY_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);
    private static final BigDecimal ONE_RATIO = BigDecimal.ONE.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    private static final BigDecimal DEFAULT_COMPREHENSIVE_RATIO = new BigDecimal("0.0800");
    private static final BigDecimal DEFAULT_SUBCONTRACT_RATIO = new BigDecimal("0.0400");
    private static final BigDecimal DEFAULT_MAJOR_OUTSOURCE_RATIO = new BigDecimal("0.0600");
    private static final BigDecimal DEFAULT_COMMON_OUTSOURCE_RATIO = new BigDecimal("0.2500");
    private static final BigDecimal MIN_GUIDANCE_PRICE_RATIO = new BigDecimal("0.6000");

    @Resource
    private ProjectPlanningMapper projectPlanningMapper;
    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectPlanningQuarterMapper projectPlanningQuarterMapper;
    @Resource
    private ProjectOutputSplitService projectOutputSplitService;
    @Resource
    private SpecialtyRoleSplitService specialtyRoleSplitService;
    @Resource
    private ProjectPlanningGuideDetailService projectPlanningGuideDetailService;

    @Override
    public Long createProjectPlanning(ProjectPlanningSaveReqVO createReqVO) {
        ProjectDO project = validateProjectExists(createReqVO.getProjectId());
        validateProjectPlanningForSave(createReqVO, null);
        ProjectPlanningDO planning = BeanUtils.toBean(createReqVO, ProjectPlanningDO.class);
        prepareProjectPlanning(planning, project);
        calculateProjectPlanning(planning);
        projectPlanningMapper.insert(planning);
        projectPlanningGuideDetailService.handlePlanningSaved(planning, null);
        return planning.getId();
    }

    @Override
    public void updateProjectPlanning(ProjectPlanningSaveReqVO updateReqVO) {
        ProjectPlanningDO dbPlanning = validateProjectPlanningExists(updateReqVO.getId());
        ProjectDO project = validateProjectExists(updateReqVO.getProjectId());
        validateProjectPlanningForSave(updateReqVO, dbPlanning);
        ProjectPlanningDO updateObj = BeanUtils.toBean(updateReqVO, ProjectPlanningDO.class);
        prepareProjectPlanning(updateObj, project);
        calculateProjectPlanning(updateObj);
        projectPlanningMapper.updateById(updateObj);
        projectPlanningGuideDetailService.handlePlanningSaved(updateObj, dbPlanning);
        if (ProjectPlanningBizTypeConstants.isMajorGuidanceScene(
                updateObj.getOwnershipType(), updateObj.getCalculationMethod())) {
            return;
        }
        refreshQuarterDistributionAmounts(updateObj);
    }

    @Override
    public void deleteProjectPlanning(Long id) {
        validateProjectPlanningExists(id);
        ProjectOutputSplitDO outputSplit = projectOutputSplitService.getProjectOutputSplitMap(Collections.singleton(id)).get(id);
        if (outputSplit != null) {
            specialtyRoleSplitService.deleteByOutputSplitId(outputSplit.getId());
            projectOutputSplitService.deleteByPlanningId(id);
        }
        projectPlanningQuarterMapper.delete(ProjectPlanningQuarterDO::getPlanningId, id);
        projectPlanningGuideDetailService.deleteByPlanningId(id);
        projectPlanningMapper.deleteById(id);
    }

    @Override
    public void deleteProjectPlanningList(List<Long> ids) {
        ids.forEach(this::validateProjectPlanningExists);
        Map<Long, ProjectOutputSplitDO> outputSplitMap = projectOutputSplitService.getProjectOutputSplitMap(ids);
        if (!outputSplitMap.isEmpty()) {
            specialtyRoleSplitService.deleteByOutputSplitIds(outputSplitMap.values().stream()
                    .map(ProjectOutputSplitDO::getId)
                    .collect(java.util.stream.Collectors.toList()));
            projectOutputSplitService.deleteByPlanningIds(ids);
        }
        projectPlanningQuarterMapper.deleteBatch(ProjectPlanningQuarterDO::getPlanningId, ids);
        projectPlanningGuideDetailService.deleteByPlanningIds(ids);
        projectPlanningMapper.deleteBatchIds(ids);
    }

    @Override
    public PageResult<ProjectPlanningDO> getProjectPlanningPage(ProjectPlanningPageReqVO pageReqVO) {
        if (pageReqVO.getProjectId() != null) {
            validateProjectExists(pageReqVO.getProjectId());
        }
        return projectPlanningMapper.selectPage(pageReqVO);
    }

    @Override
    public List<ProjectPlanningDO> getProjectPlanningListByProjectId(Long projectId) {
        validateProjectExists(projectId);
        return projectPlanningMapper.selectDisplayListByProjectId(projectId);
    }

    @Override
    public ProjectPlanningDO getProjectPlanning(Long id) {
        return projectPlanningMapper.selectById(id);
    }

    @Override
    public Map<Long, BigDecimal> getAllocatedAmountMap(Collection<Long> planningIds) {
        if (planningIds == null || planningIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<ProjectPlanningQuarterDO> quarterList = projectPlanningQuarterMapper.selectListByPlanningIds(planningIds);
        Map<Long, BigDecimal> allocatedAmountMap = new HashMap<>();
        for (Long planningId : planningIds) {
            allocatedAmountMap.put(planningId, ZERO_RATIO);
        }
        for (ProjectPlanningQuarterDO quarter : quarterList) {
            allocatedAmountMap.merge(quarter.getPlanningId(), ratio(quarter.getDistributionRatio()), BigDecimal::add);
        }
        allocatedAmountMap.replaceAll((key, value) -> ratio(value));
        return allocatedAmountMap;
    }

    private void validateProjectPlanningForSave(ProjectPlanningSaveReqVO reqVO, ProjectPlanningDO dbPlanning) {
        if (!ProjectPlanningBizTypeConstants.isValidOwnershipType(reqVO.getOwnershipType())) {
            throw exception(PROJECT_PLANNING_OWNERSHIP_TYPE_INVALID);
        }
        if (StrUtil.isNotBlank(reqVO.getCalculationMethod())
                && !ProjectPlanningBizTypeConstants.isValidCalculationMethod(reqVO.getCalculationMethod())) {
            throw exception(PROJECT_PLANNING_CALCULATION_METHOD_INVALID);
        }
        if (ProjectPlanningBizTypeConstants.isVirtualOutput(reqVO.getCalculationMethod())
                && StrUtil.isNotBlank(reqVO.getVirtualCalculationMethod())
                && !ProjectPlanningBizTypeConstants.isValidVirtualCalculationMethod(reqVO.getVirtualCalculationMethod())) {
            throw exception(PROJECT_PLANNING_VIRTUAL_CALCULATION_METHOD_INVALID);
        }
        if (ProjectPlanningBizTypeConstants.isWorkingDay(reqVO.getVirtualCalculationMethod())) {
            if (reqVO.getWorkingDayCount() == null) {
                throw exception(PROJECT_PLANNING_WORKING_DAY_COUNT_REQUIRED);
            }
            if (reqVO.getWorkingDayUnitPrice() == null) {
                throw exception(PROJECT_PLANNING_WORKING_DAY_UNIT_PRICE_REQUIRED);
            }
        }
        if (dbPlanning == null) {
            return;
        }
        if (!Objects.equals(dbPlanning.getOwnershipType(), reqVO.getOwnershipType())) {
            throw exception(PROJECT_PLANNING_OWNERSHIP_TYPE_IMMUTABLE);
        }
    }

    private void prepareProjectPlanning(ProjectPlanningDO planning, ProjectDO project) {
        if (planning.getSortNo() == null) {
            planning.setSortNo(0);
        }
        if (planning.getPlanningStartYear() == null) {
            planning.setPlanningStartYear(project.getProjectStartYear());
        }
        if (planning.getReviewOutsourceFlag() == null) {
            planning.setReviewOutsourceFlag(Boolean.FALSE);
        }
        planning.setContractValueQuantity(quantity(planning.getContractValueQuantity()));
        planning.setContractValueUnitPrice(quantity(planning.getContractValueUnitPrice()));
        planning.setPlanningAmount(multiplyAmount(
                planning.getContractValueQuantity(),
                planning.getContractValueUnitPrice()));
        planning.setManagementFeeRate(ratio(planning.getManagementFeeRate()));
        planning.setVatRate(ratio(planning.getVatRate()));
        if (planning.getPlanningArea() == null) {
            planning.setPlanningArea(project.getTotalConstructionArea());
        }
        planning.setPlanningArea(amount(planning.getPlanningArea()));
        planning.setCurrentDesignStageRatio(ratio(planning.getCurrentDesignStageRatio()));
        planning.setTotalDistributionAmount(planning.getTotalDistributionAmount() == null
                ? ONE_RATIO : ratio(planning.getTotalDistributionAmount()));

        if (planning.getReviewOutsourceRatio() == null) {
            planning.setReviewOutsourceRatio(defaultReviewOutsourceRatio(planning.getOwnershipType(),
                    Boolean.TRUE.equals(planning.getReviewOutsourceFlag())));
        } else {
            planning.setReviewOutsourceRatio(ratio(planning.getReviewOutsourceRatio()));
        }
        if ((ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())
                || ProjectPlanningBizTypeConstants.isSubcontract(planning.getOwnershipType()))
                && planning.getCalculationRatio() == null) {
            planning.setCalculationRatio(defaultCalculationRatio(planning.getOwnershipType()));
        } else if (planning.getCalculationRatio() != null) {
            planning.setCalculationRatio(ratio(planning.getCalculationRatio()));
        }

        planning.setDrawingSetFactor(factorNullable(planning.getDrawingSetFactor()));
        planning.setScaleFactor(factorNullable(planning.getScaleFactor()));
        planning.setModificationFactor(factorNullable(planning.getModificationFactor()));
        planning.setComplexityFactor(ratioNullable(planning.getComplexityFactor()));
        planning.setInternalGuidanceUnitPrice(amountNullable(planning.getInternalGuidanceUnitPrice()));
        planning.setWorkingDayCount(amountNullable(planning.getWorkingDayCount()));
        planning.setWorkingDayUnitPrice(amountNullable(planning.getWorkingDayUnitPrice()));
        planning.setGuidanceUnitPrice(amountNullable(planning.getGuidanceUnitPrice()));
        planning.setGuidanceTotalPrice(amountNullable(planning.getGuidanceTotalPrice()));
    }

    private void calculateProjectPlanning(ProjectPlanningDO planning) {
        planning.setManagementFee(multiplyAmount(planning.getPlanningAmount(), planning.getManagementFeeRate()));
        planning.setVatAmount(multiplyAmount(planning.getPlanningAmount(), planning.getVatRate()));
        planning.setProjectBudgetOutputValue(amount(planning.getPlanningAmount()
                .subtract(planning.getManagementFee())
                .subtract(planning.getVatAmount())));
        planning.setContractUnitPrice(divideAmount(planning.getPlanningAmount(), planning.getPlanningArea()));
        planning.setTotalAdjustmentFactor(ZERO_RATIO);
        planning.setAssessmentArea(ZERO_AMOUNT);
        planning.setVirtualOutputValue(ZERO_AMOUNT);
        planning.setAssessmentOutputValue(ZERO_AMOUNT);

        if (ProjectPlanningBizTypeConstants.isMajor(planning.getOwnershipType())) {
            calculateMajorPlanning(planning);
            return;
        }
        if (ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())
                || ProjectPlanningBizTypeConstants.isSubcontract(planning.getOwnershipType())) {
            planning.setAssessmentOutputValue(multiplyAmount(
                    planning.getPlanningAmount(),
                    ratio(planning.getCalculationRatio()),
                    planning.getCurrentDesignStageRatio(),
                    oneMinus(planning.getReviewOutsourceRatio())));
        }
    }

    private void calculateMajorPlanning(ProjectPlanningDO planning) {
        if (ProjectPlanningBizTypeConstants.isGuidancePrice(planning.getCalculationMethod())) {
            planning.setTotalAdjustmentFactor(ZERO_RATIO);
            planning.setAssessmentArea(ZERO_AMOUNT);
            planning.setAssessmentOutputValue(ZERO_AMOUNT);
            return;
        }
        if (ProjectPlanningBizTypeConstants.isContractPrice(planning.getCalculationMethod())) {
            BigDecimal totalAdjustmentFactor = calculateTotalAdjustmentFactor(planning);
            planning.setTotalAdjustmentFactor(totalAdjustmentFactor);
            planning.setAssessmentArea(multiplyAmount(planning.getPlanningArea(), totalAdjustmentFactor));
            planning.setAssessmentOutputValue(multiplyAmount(
                    planning.getPlanningAmount(),
                    totalAdjustmentFactor,
                    planning.getCurrentDesignStageRatio(),
                    oneMinus(planning.getReviewOutsourceRatio())));
            return;
        }
        if (ProjectPlanningBizTypeConstants.isVirtualOutput(planning.getCalculationMethod())) {
            planning.setTotalAdjustmentFactor(ONE_RATIO);
            planning.setVirtualOutputValue(calculateVirtualOutputValue(planning));
            planning.setAssessmentOutputValue(multiplyAmount(
                    planning.getVirtualOutputValue(),
                    planning.getCurrentDesignStageRatio(),
                    oneMinus(planning.getReviewOutsourceRatio())));
        }
    }

    private BigDecimal calculateTotalAdjustmentFactor(ProjectPlanningDO planning) {
        return productRatio(
                defaultFactor(planning.getDrawingSetFactor()),
                defaultFactor(planning.getScaleFactor()),
                defaultFactor(planning.getModificationFactor()),
                defaultFactor(planning.getComplexityFactor()));
    }

    private BigDecimal calculateApplicableUnitPrice(BigDecimal contractUnitPrice, BigDecimal internalGuidanceUnitPrice) {
        BigDecimal guidanceUnitPrice = amount(internalGuidanceUnitPrice);
        if (guidanceUnitPrice.compareTo(BigDecimal.ZERO) <= 0) {
            return ZERO_AMOUNT;
        }
        BigDecimal unitPrice = amount(contractUnitPrice);
        BigDecimal minimumGuidancePrice = multiplyAmount(guidanceUnitPrice, MIN_GUIDANCE_PRICE_RATIO);
        if (unitPrice.compareTo(guidanceUnitPrice) > 0) {
            return guidanceUnitPrice;
        }
        if (unitPrice.compareTo(minimumGuidancePrice) >= 0) {
            return unitPrice;
        }
        return minimumGuidancePrice;
    }

    private BigDecimal calculateVirtualOutputValue(ProjectPlanningDO planning) {
        if (ProjectPlanningBizTypeConstants.isWorkingDay(planning.getVirtualCalculationMethod())) {
            return multiplyAmount(planning.getWorkingDayCount(), planning.getWorkingDayUnitPrice());
        }
        if (ProjectPlanningBizTypeConstants.isVirtualGuidancePrice(planning.getVirtualCalculationMethod())) {
            return multiplyAmount(planning.getGuidanceUnitPrice(), planning.getPlanningArea());
        }
        if (ProjectPlanningBizTypeConstants.isVirtualGuidanceTotalPrice(planning.getVirtualCalculationMethod())) {
            return amount(planning.getGuidanceTotalPrice());
        }
        return ZERO_AMOUNT;
    }

    private BigDecimal defaultReviewOutsourceRatio(String ownershipType, boolean outsourceFlag) {
        if (!outsourceFlag) {
            return ZERO_RATIO;
        }
        if (ProjectPlanningBizTypeConstants.isMajor(ownershipType)) {
            return DEFAULT_MAJOR_OUTSOURCE_RATIO;
        }
        return DEFAULT_COMMON_OUTSOURCE_RATIO;
    }

    private BigDecimal defaultCalculationRatio(String ownershipType) {
        if (ProjectPlanningBizTypeConstants.isComprehensive(ownershipType)) {
            return DEFAULT_COMPREHENSIVE_RATIO;
        }
        if (ProjectPlanningBizTypeConstants.isSubcontract(ownershipType)) {
            return DEFAULT_SUBCONTRACT_RATIO;
        }
        return null;
    }

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

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

    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 productRatio(BigDecimal... values) {
        BigDecimal result = BigDecimal.ONE;
        for (BigDecimal value : values) {
            result = result.multiply(value == null ? BigDecimal.ONE : 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);
    }

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

    private BigDecimal defaultFactor(BigDecimal value) {
        return value == null ? ONE_RATIO : ratio(value);
    }

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

    private BigDecimal amountNullable(BigDecimal value) {
        return value == null ? null : amount(value);
    }

    private BigDecimal factor(BigDecimal value) {
        return value == null ? BigDecimal.ZERO.setScale(FACTOR_SCALE, RoundingMode.HALF_UP)
                : value.setScale(FACTOR_SCALE, RoundingMode.HALF_UP);
    }

    private BigDecimal factorNullable(BigDecimal value) {
        return value == null ? null : factor(value);
    }

    private BigDecimal quantity(BigDecimal value) {
        return value == null ? BigDecimal.ZERO.setScale(QUANTITY_SCALE, RoundingMode.HALF_UP)
                : value.setScale(QUANTITY_SCALE, RoundingMode.HALF_UP);
    }

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

    private BigDecimal ratioNullable(BigDecimal value) {
        return value == null ? null : ratio(value);
    }

    private boolean isPositive(BigDecimal value) {
        return value != null && value.compareTo(BigDecimal.ZERO) > 0;
    }

    private void refreshQuarterDistributionAmounts(ProjectPlanningDO planning) {
        List<ProjectPlanningQuarterDO> quarterList = projectPlanningQuarterMapper.selectListByPlanningId(planning.getId());
        for (ProjectPlanningQuarterDO quarter : quarterList) {
            quarter.setDistributionAmount(calculateQuarterDistributionAmount(planning, quarter.getDistributionRatio()));
            projectPlanningQuarterMapper.updateById(quarter);
        }
    }

    private BigDecimal calculateQuarterDistributionAmount(ProjectPlanningDO planning, BigDecimal distributionRatio) {
        return multiplyAmount(
                planning.getAssessmentOutputValue(),
                planning.getTotalDistributionAmount(),
                ratio(distributionRatio));
    }

}
