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

import cn.iocoder.lyzsys.framework.common.pojo.PageResult;
import cn.iocoder.lyzsys.framework.common.util.collection.CollectionUtils;
import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitPageReqVO;
import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitRespVO;
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.dataobject.yearkvalue.YearKValueDO;
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.dal.mysql.yearkvalue.YearKValueMapper;
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.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_NOT_EXISTS;

@Service
@Validated
public class ProjectProfitServiceImpl implements ProjectProfitService {

    private static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    private static final BigDecimal ZERO_RATIO = BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
    private static final BigDecimal ONE_RATIO = BigDecimal.ONE.setScale(4, RoundingMode.HALF_UP);
    private static final BigDecimal DEFAULT_INNOVATION_OUTPUT_RATE = new BigDecimal("0.0100");
    private static final BigDecimal DEFAULT_YEAR_K_VALUE = new BigDecimal("0.4000");

    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectPlanningMapper projectPlanningMapper;
    @Resource
    private ProjectPlanningQuarterMapper projectPlanningQuarterMapper;
    @Resource
    private YearKValueMapper yearKValueMapper;

    @Override
    public ProjectProfitRespVO getProjectProfit(Long projectId) {
        ProjectDO project = validateProjectExists(projectId);
        List<ProjectPlanningDO> planningList = projectPlanningMapper.selectList(ProjectPlanningDO::getProjectId,
                Collections.singleton(projectId));
        return buildProjectProfit(project, planningList);
    }

    @Override
    public PageResult<ProjectProfitRespVO> getProjectProfitPage(ProjectProfitPageReqVO pageReqVO) {
        PageResult<ProjectDO> pageResult = projectMapper.selectPage(pageReqVO, new LambdaQueryWrapperX<ProjectDO>()
                .likeIfPresent(ProjectDO::getProjectName, pageReqVO.getProjectName())
                .eqIfPresent(ProjectDO::getContractSignedFlag, pageReqVO.getContractSignedFlag())
                .eqIfPresent(ProjectDO::getProjectStartYear, pageReqVO.getProjectStartYear())
                .betweenIfPresent(ProjectDO::getCreateTime, pageReqVO.getCreateTime())
                .orderByAsc(ProjectDO::getSortNo)
                .orderByAsc(ProjectDO::getId));
        if (pageResult.getList().isEmpty()) {
            return new PageResult<>(Collections.emptyList(), pageResult.getTotal());
        }
        Set<Long> projectIds = CollectionUtils.convertSet(pageResult.getList(), ProjectDO::getId);
        Map<Long, List<ProjectPlanningDO>> planningMap = CollectionUtils.convertMultiMap(
                projectPlanningMapper.selectList(ProjectPlanningDO::getProjectId, projectIds),
                ProjectPlanningDO::getProjectId);
        List<ProjectProfitRespVO> list = CollectionUtils.convertList(pageResult.getList(),
                project -> buildProjectProfit(project, planningMap.get(project.getId())));
        return new PageResult<>(list, pageResult.getTotal());
    }

    private ProjectProfitRespVO buildProjectProfit(ProjectDO project, List<ProjectPlanningDO> planningList) {
        List<ProjectPlanningDO> safePlanningList = planningList == null ? Collections.emptyList() : planningList;
        BigDecimal comprehensivePlanningAmount = ZERO_AMOUNT;
        BigDecimal specialSubcontractPlanningAmount = ZERO_AMOUNT;
        BigDecimal sourceCoopSubcontractPlanningAmount = ZERO_AMOUNT;
        BigDecimal majorOutputValue = ZERO_AMOUNT;
        List<ProjectPlanningDO> majorPlanningList = safePlanningList.stream()
                .filter(planning -> ProjectPlanningBizTypeConstants.isMajor(planning.getOwnershipType()))
                .collect(Collectors.toList());

        for (ProjectPlanningDO planning : safePlanningList) {
            if (ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())) {
                comprehensivePlanningAmount = comprehensivePlanningAmount.add(amount(planning.getPlanningAmount()));
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isSpecialSubcontract(planning.getOwnershipType())) {
                specialSubcontractPlanningAmount = specialSubcontractPlanningAmount.add(amount(planning.getPlanningAmount()));
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isSourceCoopSubcontract(planning.getOwnershipType())) {
                sourceCoopSubcontractPlanningAmount = sourceCoopSubcontractPlanningAmount.add(amount(planning.getPlanningAmount()));
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isMajor(planning.getOwnershipType())) {
                majorOutputValue = majorOutputValue.add(amount(planning.getAssessmentOutputValue()));
            }
        }

        BigDecimal majorExpectedPerformance = calculateMajorExpectedPerformance(majorPlanningList);
        BigDecimal contractAmount = amount(project.getContractAmount());
        BigDecimal finalSettlementAmount = amount(project.getFinalSettlementAmount());
        BigDecimal effectiveSettlementAmount = finalSettlementAmount.compareTo(BigDecimal.ZERO) > 0
                ? finalSettlementAmount : contractAmount;
        BigDecimal innovationOutputRate = project.getInnovationOutputRate() == null
                ? DEFAULT_INNOVATION_OUTPUT_RATE : ratio(project.getInnovationOutputRate());
        BigDecimal innovationOutputValue = contractAmount.compareTo(BigDecimal.ZERO) <= 0
                ? ZERO_AMOUNT
                : contractAmount.multiply(innovationOutputRate).setScale(2, RoundingMode.HALF_UP);
        BigDecimal otherCost = amount(project.getOtherCost());
        BigDecimal subcontractPlanningAmount = specialSubcontractPlanningAmount.add(sourceCoopSubcontractPlanningAmount);
        BigDecimal profitLossValue = effectiveSettlementAmount
                .subtract(comprehensivePlanningAmount)
                .subtract(subcontractPlanningAmount)
                .subtract(majorExpectedPerformance)
                .subtract(innovationOutputValue)
                .subtract(otherCost)
                .setScale(2, RoundingMode.HALF_UP);
        BigDecimal profitLossRate = effectiveSettlementAmount.compareTo(BigDecimal.ZERO) == 0
                ? ZERO_RATIO
                : profitLossValue.divide(effectiveSettlementAmount, 4, RoundingMode.HALF_UP);

        ProjectProfitRespVO respVO = new ProjectProfitRespVO();
        respVO.setProjectId(project.getId());
        respVO.setProjectName(project.getProjectName());
        respVO.setSortNo(project.getSortNo());
        respVO.setContractSignedFlag(project.getContractSignedFlag());
        respVO.setContractAmount(contractAmount);
        respVO.setFinalSettlementAmount(finalSettlementAmount);
        respVO.setEffectiveSettlementAmount(effectiveSettlementAmount);
        respVO.setComprehensivePlanningAmount(comprehensivePlanningAmount.setScale(2, RoundingMode.HALF_UP));
        respVO.setSubcontractPlanningAmount(subcontractPlanningAmount.setScale(2, RoundingMode.HALF_UP));
        respVO.setSpecialSubcontractPlanningAmount(specialSubcontractPlanningAmount.setScale(2, RoundingMode.HALF_UP));
        respVO.setSourceCoopSubcontractPlanningAmount(sourceCoopSubcontractPlanningAmount.setScale(2, RoundingMode.HALF_UP));
        respVO.setMajorOutputValue(majorOutputValue.setScale(2, RoundingMode.HALF_UP));
        respVO.setMajorExpectedPerformance(majorExpectedPerformance);
        respVO.setInnovationOutputRate(innovationOutputRate);
        respVO.setInnovationOutputValue(innovationOutputValue);
        respVO.setOtherCost(otherCost);
        respVO.setProfitLossValue(profitLossValue);
        respVO.setProfitLossRate(profitLossRate);
        respVO.setProjectStartYear(project.getProjectStartYear());
        respVO.setCreateTime(project.getCreateTime());
        return respVO;
    }

    private BigDecimal calculateMajorExpectedPerformance(List<ProjectPlanningDO> majorPlanningList) {
        if (majorPlanningList == null || majorPlanningList.isEmpty()) {
            return ZERO_AMOUNT;
        }
        List<Long> planningIds = majorPlanningList.stream().map(ProjectPlanningDO::getId).collect(Collectors.toList());
        Map<Long, List<ProjectPlanningQuarterDO>> quarterMap = CollectionUtils.convertMultiMap(
                projectPlanningQuarterMapper.selectListByPlanningIds(planningIds), ProjectPlanningQuarterDO::getPlanningId);
        Set<Integer> years = quarterMap.values().stream()
                .flatMap(Collection::stream)
                .map(ProjectPlanningQuarterDO::getDistributionYear)
                .collect(Collectors.toSet());
        Map<Integer, BigDecimal> yearKValueMap = yearKValueMapper.selectEnabledListByYears(years).stream()
                .collect(Collectors.toMap(YearKValueDO::getKYear, item -> ratio(item.getKValue()), (a, b) -> b));

        BigDecimal total = ZERO_AMOUNT;
        for (ProjectPlanningDO planning : majorPlanningList) {
            BigDecimal assessmentOutputValue = amount(planning.getAssessmentOutputValue());
            Map<Integer, BigDecimal> yearRatioMap = quarterMap.getOrDefault(planning.getId(), Collections.emptyList()).stream()
                    .collect(Collectors.groupingBy(ProjectPlanningQuarterDO::getDistributionYear,
                            Collectors.reducing(ZERO_RATIO,
                                    item -> ratio(item.getDistributionRatio()),
                                    BigDecimal::add)));
            BigDecimal allocatedRatio = ZERO_RATIO;
            for (Map.Entry<Integer, BigDecimal> entry : yearRatioMap.entrySet()) {
                BigDecimal yearRatio = ratio(entry.getValue());
                allocatedRatio = allocatedRatio.add(yearRatio).setScale(4, RoundingMode.HALF_UP);
                BigDecimal kValue = yearKValueMap.getOrDefault(entry.getKey(), DEFAULT_YEAR_K_VALUE);
                total = total.add(multiplyAmount(assessmentOutputValue, yearRatio, kValue));
            }
            BigDecimal unallocatedRatio = ONE_RATIO.subtract(allocatedRatio).setScale(4, RoundingMode.HALF_UP);
            if (unallocatedRatio.compareTo(ZERO_RATIO) > 0) {
                total = total.add(multiplyAmount(assessmentOutputValue, unallocatedRatio, DEFAULT_YEAR_K_VALUE));
            }
        }
        return total.setScale(2, RoundingMode.HALF_UP);
    }

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

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

    private BigDecimal ratio(BigDecimal value) {
        return value == null ? ZERO_RATIO : value.setScale(4, 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(2, RoundingMode.HALF_UP);
    }

}
