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.common.util.object.BeanUtils;
import cn.iocoder.lyzsys.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.lyzsys.framework.security.core.util.SecurityFrameworkUtils;
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.controller.admin.profit.vo.ProjectProfitSettlementSaveReqVO;
import cn.iocoder.lyzsys.module.tjt.controller.admin.profit.vo.ProjectProfitSnapshotRespVO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.planning.ProjectPlanningDO;
import cn.iocoder.lyzsys.module.tjt.dal.dataobject.profit.ProjectCostMeasureSnapshotDO;
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.profit.ProjectCostMeasureSnapshotMapper;
import cn.iocoder.lyzsys.module.tjt.dal.mysql.project.ProjectMapper;
import cn.iocoder.lyzsys.module.tjt.enums.ProjectCostMeasureSnapshotTypeEnum;
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.time.LocalDateTime;
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_COST_MEASURE_ACCOUNTING_LOCKED;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_COST_MEASURE_ACCOUNTING_REQUIRED;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_COST_MEASURE_BUDGET_LOCKED;
import static cn.iocoder.lyzsys.module.tjt.enums.ErrorCodeConstants.PROJECT_COST_MEASURE_BUDGET_REQUIRED;
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 DEFAULT_INNOVATION_OUTPUT_RATE = new BigDecimal("0.0100");
    private static final BigDecimal ASSESSMENT_COEFFICIENT_EXCELLENT = new BigDecimal("1.0200");
    private static final BigDecimal ASSESSMENT_COEFFICIENT_QUALIFIED = new BigDecimal("1.0000");
    private static final BigDecimal ASSESSMENT_COEFFICIENT_NEEDS_IMPROVEMENT = new BigDecimal("0.9500");
    private static final BigDecimal VAT_INCLUDED_DIVISOR = new BigDecimal("1.06");

    @Resource
    private ProjectMapper projectMapper;
    @Resource
    private ProjectPlanningMapper projectPlanningMapper;
    @Resource
    private ProjectCostMeasureSnapshotMapper projectCostMeasureSnapshotMapper;

    @Override
    public ProjectProfitRespVO getProjectProfit(Long projectId) {
        ProjectProfitRespVO respVO = buildCurrentProjectProfit(projectId);
        fillSnapshots(respVO);
        return respVO;
    }

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

    @Override
    public ProjectProfitRespVO lockBudgetSnapshot(Long projectId) {
        ProjectProfitRespVO currentProfit = buildCurrentProjectProfit(projectId);
        if (projectCostMeasureSnapshotMapper.selectByProjectIdAndType(projectId,
                ProjectCostMeasureSnapshotTypeEnum.BUDGET.getCode()) != null) {
            throw exception(PROJECT_COST_MEASURE_BUDGET_LOCKED);
        }
        ProjectCostMeasureSnapshotDO snapshot = buildSnapshot(currentProfit, ProjectCostMeasureSnapshotTypeEnum.BUDGET.getCode());
        snapshot.setLockedFlag(Boolean.TRUE);
        fillActionInfo(snapshot);
        projectCostMeasureSnapshotMapper.insert(snapshot);
        return getProjectProfit(projectId);
    }

    @Override
    public ProjectProfitRespVO lockAccountingSnapshot(Long projectId) {
        ProjectProfitRespVO currentProfit = buildCurrentProjectProfit(projectId);
        ProjectCostMeasureSnapshotDO budgetSnapshot = projectCostMeasureSnapshotMapper.selectByProjectIdAndType(projectId,
                ProjectCostMeasureSnapshotTypeEnum.BUDGET.getCode());
        if (budgetSnapshot == null || !Boolean.TRUE.equals(budgetSnapshot.getLockedFlag())) {
            throw exception(PROJECT_COST_MEASURE_BUDGET_REQUIRED);
        }
        if (projectCostMeasureSnapshotMapper.selectByProjectIdAndType(projectId,
                ProjectCostMeasureSnapshotTypeEnum.ACCOUNTING.getCode()) != null) {
            throw exception(PROJECT_COST_MEASURE_ACCOUNTING_LOCKED);
        }
        ProjectCostMeasureSnapshotDO snapshot = buildSnapshot(currentProfit, ProjectCostMeasureSnapshotTypeEnum.ACCOUNTING.getCode());
        snapshot.setLockedFlag(Boolean.TRUE);
        fillActionInfo(snapshot);
        projectCostMeasureSnapshotMapper.insert(snapshot);
        return getProjectProfit(projectId);
    }

    @Override
    public ProjectProfitRespVO saveSettlementSnapshot(ProjectProfitSettlementSaveReqVO saveReqVO) {
        validateProjectExists(saveReqVO.getProjectId());
        ProjectCostMeasureSnapshotDO accountingSnapshot = projectCostMeasureSnapshotMapper.selectByProjectIdAndType(
                saveReqVO.getProjectId(), ProjectCostMeasureSnapshotTypeEnum.ACCOUNTING.getCode());
        if (accountingSnapshot == null || !Boolean.TRUE.equals(accountingSnapshot.getLockedFlag())) {
            throw exception(PROJECT_COST_MEASURE_ACCOUNTING_REQUIRED);
        }

        ProjectCostMeasureSnapshotDO settlementSnapshot = projectCostMeasureSnapshotMapper.selectByProjectIdAndType(
                saveReqVO.getProjectId(), ProjectCostMeasureSnapshotTypeEnum.SETTLEMENT.getCode());
        boolean create = settlementSnapshot == null;
        if (create) {
            settlementSnapshot = new ProjectCostMeasureSnapshotDO();
        }
        copySnapshotValues(accountingSnapshot, settlementSnapshot);
        settlementSnapshot.setProjectId(saveReqVO.getProjectId());
        settlementSnapshot.setSnapshotType(ProjectCostMeasureSnapshotTypeEnum.SETTLEMENT.getCode());
        settlementSnapshot.setLockedFlag(Boolean.FALSE);
        settlementSnapshot.setAssessmentResult(saveReqVO.getAssessmentResult());
        settlementSnapshot.setAssessmentCoefficient(getAssessmentCoefficient(saveReqVO.getAssessmentResult()));
        settlementSnapshot.setRemark(saveReqVO.getRemark());

        BigDecimal comprehensiveAccountingOutputValue = amount(accountingSnapshot.getComprehensivePlanningAmount());
        BigDecimal majorAccountingOutputValue = amount(accountingSnapshot.getMajorOutputValue());
        settlementSnapshot.setComprehensiveAccountingOutputValue(comprehensiveAccountingOutputValue);
        settlementSnapshot.setMajorAccountingOutputValue(majorAccountingOutputValue);
        settlementSnapshot.setComprehensiveSettlementOutputValue(
                multiplyAmount(comprehensiveAccountingOutputValue, settlementSnapshot.getAssessmentCoefficient()));
        settlementSnapshot.setMajorSettlementOutputValue(
                multiplyAmount(majorAccountingOutputValue, settlementSnapshot.getAssessmentCoefficient()));
        fillActionInfo(settlementSnapshot);

        if (create) {
            projectCostMeasureSnapshotMapper.insert(settlementSnapshot);
        } else {
            projectCostMeasureSnapshotMapper.updateById(settlementSnapshot);
        }
        return getProjectProfit(saveReqVO.getProjectId());
    }

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

    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 comprehensiveSubcontractPlanningAmount = ZERO_AMOUNT;
        BigDecimal majorExpectedPerformance = ZERO_AMOUNT;
        BigDecimal majorOutputValue = ZERO_AMOUNT;

        for (ProjectPlanningDO planning : safePlanningList) {
            BigDecimal planningBudgetOutputValue = amount(planning.getProjectBudgetOutputValue());
            if (ProjectPlanningBizTypeConstants.isComprehensive(planning.getOwnershipType())) {
                comprehensivePlanningAmount = comprehensivePlanningAmount.add(planningBudgetOutputValue);
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isSpecialSubcontract(planning.getOwnershipType())) {
                specialSubcontractPlanningAmount = specialSubcontractPlanningAmount.add(planningBudgetOutputValue);
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isSourceCoopSubcontract(planning.getOwnershipType())) {
                sourceCoopSubcontractPlanningAmount = sourceCoopSubcontractPlanningAmount.add(planningBudgetOutputValue);
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isComprehensiveSubcontract(planning.getOwnershipType())) {
                comprehensiveSubcontractPlanningAmount = comprehensiveSubcontractPlanningAmount.add(planningBudgetOutputValue);
                continue;
            }
            if (ProjectPlanningBizTypeConstants.isMajor(planning.getOwnershipType())) {
                majorExpectedPerformance = majorExpectedPerformance.add(planningBudgetOutputValue);
                majorOutputValue = majorOutputValue.add(amount(planning.getAssessmentOutputValue()));
            }
        }

        BigDecimal contractAmount = amount(project.getContractAmount());
        BigDecimal contractAmountWithoutVat = taxExcludedAmount(contractAmount);
        BigDecimal finalSettlementAmount = amount(project.getFinalSettlementAmount());
        BigDecimal effectiveContractSettlementAmount = finalSettlementAmount.compareTo(BigDecimal.ZERO) > 0
                ? finalSettlementAmount : contractAmount;
        BigDecimal taxExcludedSettlementAmount = taxExcludedAmount(effectiveContractSettlementAmount);
        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)
                .add(comprehensiveSubcontractPlanningAmount);
        BigDecimal profitLossValue = taxExcludedSettlementAmount
                .subtract(comprehensivePlanningAmount)
                .subtract(subcontractPlanningAmount)
                .subtract(majorExpectedPerformance)
                .subtract(innovationOutputValue)
                .subtract(otherCost)
                .setScale(2, RoundingMode.HALF_UP);
        BigDecimal profitLossRate = taxExcludedSettlementAmount.compareTo(BigDecimal.ZERO) == 0
                ? ZERO_RATIO
                : profitLossValue.divide(taxExcludedSettlementAmount, 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);
        // 历史字段名沿用 effectiveSettlementAmount，当前业务含义为“合同产值（不含增值税）”。
        respVO.setEffectiveSettlementAmount(contractAmountWithoutVat);
        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.setComprehensiveSubcontractPlanningAmount(comprehensiveSubcontractPlanningAmount.setScale(2, RoundingMode.HALF_UP));
        respVO.setMajorOutputValue(majorOutputValue.setScale(2, RoundingMode.HALF_UP));
        respVO.setMajorExpectedPerformance(majorExpectedPerformance.setScale(2, RoundingMode.HALF_UP));
        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 void fillSnapshots(ProjectProfitRespVO respVO) {
        Map<String, ProjectCostMeasureSnapshotDO> snapshotMap = projectCostMeasureSnapshotMapper
                .selectListByProjectId(respVO.getProjectId())
                .stream()
                .collect(Collectors.toMap(ProjectCostMeasureSnapshotDO::getSnapshotType, item -> item, (a, b) -> b));
        respVO.setBudgetSnapshot(toSnapshotRespVO(snapshotMap.get(ProjectCostMeasureSnapshotTypeEnum.BUDGET.getCode())));
        respVO.setAccountingSnapshot(toSnapshotRespVO(snapshotMap.get(ProjectCostMeasureSnapshotTypeEnum.ACCOUNTING.getCode())));
        respVO.setSettlementSnapshot(toSnapshotRespVO(snapshotMap.get(ProjectCostMeasureSnapshotTypeEnum.SETTLEMENT.getCode())));
    }

    private ProjectCostMeasureSnapshotDO buildSnapshot(ProjectProfitRespVO profit, String snapshotType) {
        ProjectCostMeasureSnapshotDO snapshot = new ProjectCostMeasureSnapshotDO();
        snapshot.setProjectId(profit.getProjectId());
        snapshot.setSnapshotType(snapshotType);
        snapshot.setContractAmount(amount(profit.getContractAmount()));
        snapshot.setFinalSettlementAmount(amount(profit.getFinalSettlementAmount()));
        snapshot.setEffectiveSettlementAmount(amount(profit.getEffectiveSettlementAmount()));
        snapshot.setComprehensivePlanningAmount(amount(profit.getComprehensivePlanningAmount()));
        snapshot.setSubcontractPlanningAmount(amount(profit.getSubcontractPlanningAmount()));
        snapshot.setSpecialSubcontractPlanningAmount(amount(profit.getSpecialSubcontractPlanningAmount()));
        snapshot.setSourceCoopSubcontractPlanningAmount(amount(profit.getSourceCoopSubcontractPlanningAmount()));
        snapshot.setComprehensiveSubcontractPlanningAmount(amount(profit.getComprehensiveSubcontractPlanningAmount()));
        snapshot.setMajorOutputValue(amount(profit.getMajorOutputValue()));
        snapshot.setMajorExpectedPerformance(amount(profit.getMajorExpectedPerformance()));
        snapshot.setInnovationOutputRate(ratio(profit.getInnovationOutputRate()));
        snapshot.setInnovationOutputValue(amount(profit.getInnovationOutputValue()));
        snapshot.setOtherCost(amount(profit.getOtherCost()));
        snapshot.setProfitLossValue(amount(profit.getProfitLossValue()));
        snapshot.setProfitLossRate(ratio(profit.getProfitLossRate()));
        return snapshot;
    }

    private void copySnapshotValues(ProjectCostMeasureSnapshotDO source, ProjectCostMeasureSnapshotDO target) {
        target.setContractAmount(amount(source.getContractAmount()));
        target.setFinalSettlementAmount(amount(source.getFinalSettlementAmount()));
        target.setEffectiveSettlementAmount(amount(source.getEffectiveSettlementAmount()));
        target.setComprehensivePlanningAmount(amount(source.getComprehensivePlanningAmount()));
        target.setSubcontractPlanningAmount(amount(source.getSubcontractPlanningAmount()));
        target.setSpecialSubcontractPlanningAmount(amount(source.getSpecialSubcontractPlanningAmount()));
        target.setSourceCoopSubcontractPlanningAmount(amount(source.getSourceCoopSubcontractPlanningAmount()));
        target.setComprehensiveSubcontractPlanningAmount(amount(source.getComprehensiveSubcontractPlanningAmount()));
        target.setMajorOutputValue(amount(source.getMajorOutputValue()));
        target.setMajorExpectedPerformance(amount(source.getMajorExpectedPerformance()));
        target.setInnovationOutputRate(ratio(source.getInnovationOutputRate()));
        target.setInnovationOutputValue(amount(source.getInnovationOutputValue()));
        target.setOtherCost(amount(source.getOtherCost()));
        target.setProfitLossValue(amount(source.getProfitLossValue()));
        target.setProfitLossRate(ratio(source.getProfitLossRate()));
    }

    private void fillActionInfo(ProjectCostMeasureSnapshotDO snapshot) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        String nickname = SecurityFrameworkUtils.getLoginUserNickname();
        snapshot.setActionUserId(userId);
        snapshot.setActionUserName(nickname == null ? "" : nickname);
        snapshot.setActionTime(LocalDateTime.now());
    }

    private ProjectProfitSnapshotRespVO toSnapshotRespVO(ProjectCostMeasureSnapshotDO snapshot) {
        return snapshot == null ? null : BeanUtils.toBean(snapshot, ProjectProfitSnapshotRespVO.class);
    }

    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 taxExcludedAmount(BigDecimal amount) {
        BigDecimal value = amount(amount);
        if (value.compareTo(BigDecimal.ZERO) == 0) {
            return ZERO_AMOUNT;
        }
        return value.divide(VAT_INCLUDED_DIVISOR, 2, RoundingMode.HALF_UP);
    }

    private BigDecimal getAssessmentCoefficient(String assessmentResult) {
        if ("优秀".equals(assessmentResult)) {
            return ASSESSMENT_COEFFICIENT_EXCELLENT;
        }
        if ("待改进".equals(assessmentResult)) {
            return ASSESSMENT_COEFFICIENT_NEEDS_IMPROVEMENT;
        }
        return ASSESSMENT_COEFFICIENT_QUALIFIED;
    }

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

}
