package com.huimv.employment.service.draft;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.huimv.employment.common.exception.BizException;
import com.huimv.employment.common.exception.ErrorCode;
import com.huimv.employment.dao.entity.FeEmploymentDraft;
import com.huimv.employment.dao.entity.FeServicePlan;
import com.huimv.employment.dao.entity.FeServicePlanFeeRule;
import com.huimv.employment.dao.mapper.FeServicePlanFeeRuleMapper;
import com.huimv.employment.dao.mapper.FeServicePlanMapper;
import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
import com.huimv.employment.service.draft.dto.McpDraftSchemeCostResponse;
import com.huimv.employment.service.draft.dto.McpDraftSchemeResponse;
import com.huimv.employment.service.draft.dto.McpDraftSchemeWorkerIncomeResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 草稿成本测算引擎（v2 §6.3.4）。
 * <p>从 {@code fe_service_plan} + {@code fe_service_plan_fee_rule} 读取方案与费率，不在代码中写死业务费率。</p>
 */
@Component
public class DraftCostCalculationEngine {

    private static final String CALC_VERSION = "v1";
    private static final String STATUS_ACTIVE = "active";
    private static final String PLAN_CURRENT = "current";
    private static final String FEE_SERVICE = "service_fee";
    private static final String FEE_TAX = "tax_withhold";
    private static final String CALC_RATE = "rate";
    private static final String CALC_FORMULA = "formula";
    private static final String CALC_FIXED = "fixed";

    /** gross*440/3000 或 gross * 0.02 */
    private static final Pattern FORMULA_MUL_DIV = Pattern.compile(
            "(?i)^(gross|labor|company_pay)\\s*\\*\\s*(\\d+(?:\\.\\d+)?)\\s*/\\s*(\\d+(?:\\.\\d+)?)$");
    private static final Pattern FORMULA_MUL = Pattern.compile(
            "(?i)^(gross|labor|company_pay)\\s*\\*\\s*(\\d+(?:\\.\\d+)?)$");

    private final FeServicePlanMapper feServicePlanMapper;
    private final FeServicePlanFeeRuleMapper feServicePlanFeeRuleMapper;
    private final ObjectMapper objectMapper;

    public DraftCostCalculationEngine(FeServicePlanMapper feServicePlanMapper,
                                      FeServicePlanFeeRuleMapper feServicePlanFeeRuleMapper,
                                      ObjectMapper objectMapper) {
        this.feServicePlanMapper = feServicePlanMapper;
        this.feServicePlanFeeRuleMapper = feServicePlanFeeRuleMapper;
        this.objectMapper = objectMapper;
    }

    public List<CalculatedScheme> calculate(FeEmploymentDraft draft) {
        BigDecimal laborTotal = resolveLaborTotal(draft);
        int workerCount = draft.getWorkerCount() != null && draft.getWorkerCount() > 0
                ? draft.getWorkerCount() : 1;

        List<FeServicePlan> plans = listActiveComparisonPlans();
        if (plans.isEmpty()) {
            throw new BizException(ErrorCode.BAD_REQUEST, "未配置可用用工方案，请先初始化 fe_service_plan");
        }

        Map<Long, List<FeServicePlanFeeRule>> rulesByPlan = loadRulesByPlanIds(plans);
        List<CalculatedScheme> result = new ArrayList<>(plans.size());
        boolean recommendedAssigned = false;
        for (FeServicePlan plan : plans) {
            boolean recommended = !recommendedAssigned;
            recommendedAssigned = true;
            result.add(buildScheme(plan, rulesByPlan.getOrDefault(plan.getId(), Collections.emptyList()),
                    laborTotal, workerCount, recommended));
        }
        return result;
    }

    /**
     * 劳务费基数：优先人数×天数×日薪；否则用草稿 total_amount（estimated_total）。
     */
    private static BigDecimal resolveLaborTotal(FeEmploymentDraft draft) {
        if (draft.getDailyWage() != null
                && draft.getWorkerCount() != null && draft.getWorkerCount() > 0
                && draft.getWorkDays() != null && draft.getWorkDays().compareTo(BigDecimal.ZERO) > 0) {
            return draft.getDailyWage()
                    .multiply(BigDecimal.valueOf(draft.getWorkerCount()))
                    .multiply(draft.getWorkDays());
        }
        if (draft.getEstimatedTotal() != null
                && draft.getEstimatedTotal().compareTo(BigDecimal.ZERO) > 0) {
            return draft.getEstimatedTotal();
        }
        throw new BizException(ErrorCode.BAD_REQUEST, "草稿劳务费总额为空，无法测算（需 total_amount 或 人数×天数×日薪）");
    }

    /** 方案编码是否为库中 active 对比方案（非 current）。 */
    public boolean isAllowedSchemeCode(String schemeCode) {
        if (!StringUtils.hasText(schemeCode)) {
            return false;
        }
        String code = schemeCode.trim();
        return listActiveComparisonPlans().stream()
                .anyMatch(p -> code.equalsIgnoreCase(p.getPlanCode()));
    }

    public McpDraftCalculateResponse toApiResponse(List<CalculatedScheme> schemes) {
        McpDraftCalculateResponse response = new McpDraftCalculateResponse();
        for (CalculatedScheme scheme : schemes) {
            response.getSchemes().add(toSchemeResponse(scheme));
        }
        return response;
    }

    public String calcVersion() {
        return CALC_VERSION;
    }

    private List<FeServicePlan> listActiveComparisonPlans() {
        LocalDate today = LocalDate.now();
        List<FeServicePlan> plans = feServicePlanMapper.selectList(new LambdaQueryWrapper<FeServicePlan>()
                .eq(FeServicePlan::getStatus, STATUS_ACTIVE)
                .ne(FeServicePlan::getPlanCode, PLAN_CURRENT)
                .orderByAsc(FeServicePlan::getSortOrder)
                .orderByAsc(FeServicePlan::getId));
        List<FeServicePlan> effective = new ArrayList<>();
        for (FeServicePlan plan : plans) {
            if (plan.getEffectiveFrom() != null && today.isBefore(plan.getEffectiveFrom())) {
                continue;
            }
            if (plan.getEffectiveTo() != null && today.isAfter(plan.getEffectiveTo())) {
                continue;
            }
            effective.add(plan);
        }
        return effective;
    }

    private Map<Long, List<FeServicePlanFeeRule>> loadRulesByPlanIds(List<FeServicePlan> plans) {
        List<Long> planIds = new ArrayList<>(plans.size());
        for (FeServicePlan plan : plans) {
            planIds.add(plan.getId());
        }
        List<FeServicePlanFeeRule> rules = feServicePlanFeeRuleMapper.selectList(
                new LambdaQueryWrapper<FeServicePlanFeeRule>()
                        .in(FeServicePlanFeeRule::getPlanId, planIds)
                        .orderByAsc(FeServicePlanFeeRule::getSortOrder));
        Map<Long, List<FeServicePlanFeeRule>> map = new HashMap<>();
        for (FeServicePlanFeeRule rule : rules) {
            map.computeIfAbsent(rule.getPlanId(), k -> new ArrayList<>()).add(rule);
        }
        return map;
    }

    private CalculatedScheme buildScheme(FeServicePlan plan,
                                         List<FeServicePlanFeeRule> rules,
                                         BigDecimal laborTotal,
                                         int workerCount,
                                         boolean recommended) {
        BigDecimal companyPay = scaleMoney(laborTotal);
        BigDecimal gross = companyPay;

        FeServicePlanFeeRule serviceRule = findRule(rules, FEE_SERVICE);
        BigDecimal serviceFee = null;
        if (serviceRule != null) {
            serviceFee = scaleMoney(calcFeeAmount(gross, serviceRule, plan));
        } else if (plan.getServiceFeeRate() != null) {
            serviceFee = scaleMoney(gross.multiply(plan.getServiceFeeRate()));
        }

        FeServicePlanFeeRule taxRule = findRule(rules, FEE_TAX);
        BigDecimal tax = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        if (taxRule != null) {
            tax = scaleMoney(calcFeeAmount(gross, taxRule, plan));
        }

        BigDecimal net = scaleMoney(gross.subtract(tax));
        BigDecimal totalCost = serviceFee != null ? companyPay.add(serviceFee) : companyPay;

        return new CalculatedScheme(
                plan.getId(),
                plan.getPlanCode(),
                plan.getPlanName(),
                parseTags(plan.getComplianceTags()),
                recommended,
                companyPay, serviceFee, totalCost,
                gross, tax, net,
                totalCost, net, workerCount);
    }

    private static FeServicePlanFeeRule findRule(List<FeServicePlanFeeRule> rules, String feeCode) {
        for (FeServicePlanFeeRule rule : rules) {
            if (feeCode.equalsIgnoreCase(rule.getFeeCode())) {
                return rule;
            }
        }
        return null;
    }

    private BigDecimal calcFeeAmount(BigDecimal base, FeServicePlanFeeRule rule, FeServicePlan plan) {
        String calcType = rule.getCalcType() == null ? "" : rule.getCalcType().trim().toLowerCase();
        if (CALC_RATE.equals(calcType)) {
            BigDecimal rate = rule.getDefaultRate();
            if (rate == null && plan.getServiceFeeRate() != null
                    && FEE_SERVICE.equalsIgnoreCase(rule.getFeeCode())) {
                rate = plan.getServiceFeeRate();
            }
            if (rate == null) {
                rate = parsePlainRate(rule.getCalcExpression());
            }
            if (rate == null) {
                return BigDecimal.ZERO;
            }
            return base.multiply(rate);
        }
        if (CALC_FIXED.equals(calcType)) {
            return rule.getDefaultRate() != null ? rule.getDefaultRate() : BigDecimal.ZERO;
        }
        if (CALC_FORMULA.equals(calcType)) {
            return evalFormula(base, rule.getCalcExpression(), plan);
        }
        return BigDecimal.ZERO;
    }

    private static BigDecimal evalFormula(BigDecimal base, String expression, FeServicePlan plan) {
        if (!StringUtils.hasText(expression)) {
            return BigDecimal.ZERO;
        }
        String expr = expression.trim().replace(" ", "");
        if ("gross".equalsIgnoreCase(expr) || "labor".equalsIgnoreCase(expr)
                || "company_pay".equalsIgnoreCase(expr)) {
            return base;
        }
        if (expr.toLowerCase().contains("service_fee_rate") && plan.getServiceFeeRate() != null) {
            return base.multiply(plan.getServiceFeeRate());
        }
        Matcher mulDiv = FORMULA_MUL_DIV.matcher(expr);
        if (mulDiv.matches()) {
            return base.multiply(new BigDecimal(mulDiv.group(2)))
                    .divide(new BigDecimal(mulDiv.group(3)), 8, RoundingMode.HALF_UP);
        }
        Matcher mul = FORMULA_MUL.matcher(expr);
        if (mul.matches()) {
            return base.multiply(new BigDecimal(mul.group(2)));
        }
        BigDecimal plain = parsePlainRate(expression);
        return plain != null ? base.multiply(plain) : BigDecimal.ZERO;
    }

    private static BigDecimal parsePlainRate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
            return new BigDecimal(text.trim());
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private List<String> parseTags(String complianceTagsJson) {
        if (!StringUtils.hasText(complianceTagsJson)) {
            return Collections.emptyList();
        }
        try {
            List<String> tags = objectMapper.readValue(complianceTagsJson, new TypeReference<List<String>>() {
            });
            return tags != null ? tags : Collections.emptyList();
        } catch (Exception ex) {
            return Collections.emptyList();
        }
    }

    private McpDraftSchemeResponse toSchemeResponse(CalculatedScheme scheme) {
        McpDraftSchemeResponse response = new McpDraftSchemeResponse();
        response.setCode(scheme.getCode());
        response.setTitle(scheme.getTitle());
        response.setTags(scheme.getTags());

        McpDraftSchemeCostResponse cost = new McpDraftSchemeCostResponse();
        cost.setCompanyPay(formatMoney(scheme.getCompanyPay()));
        if (scheme.getServiceFee() != null) {
            cost.setServiceFee(formatMoney(scheme.getServiceFee()));
        }
        cost.setTotalCost(formatMoney(scheme.getTotalCost()));
        response.setCost(cost);

        McpDraftSchemeWorkerIncomeResponse income = new McpDraftSchemeWorkerIncomeResponse();
        income.setGross(formatMoney(scheme.getGross()));
        income.setTax(formatMoney(scheme.getTax()));
        income.setNet(formatMoney(scheme.getNet()));
        response.setWorkerIncome(income);
        return response;
    }

    private static BigDecimal scaleMoney(BigDecimal value) {
        return value.setScale(2, RoundingMode.HALF_UP);
    }

    private static String formatMoney(BigDecimal value) {
        return scaleMoney(value).toPlainString();
    }

    /**
     * 测算结果内部模型，供快照落库与 API 转换共用。
     */
    public static final class CalculatedScheme {

        private final Long planId;
        private final String code;
        private final String title;
        private final List<String> tags;
        private final boolean recommended;
        private final BigDecimal companyPay;
        private final BigDecimal serviceFee;
        private final BigDecimal totalCost;
        private final BigDecimal gross;
        private final BigDecimal tax;
        private final BigDecimal net;
        private final BigDecimal employerTotalOutflow;
        private final BigDecimal workerTakeHomePerWorker;
        private final int workerCount;

        public CalculatedScheme(Long planId,
                                String code,
                                String title,
                                List<String> tags,
                                boolean recommended,
                                BigDecimal companyPay,
                                BigDecimal serviceFee,
                                BigDecimal totalCost,
                                BigDecimal gross,
                                BigDecimal tax,
                                BigDecimal net,
                                BigDecimal employerTotalOutflow,
                                BigDecimal workerTakeHomePerWorker,
                                int workerCount) {
            this.planId = planId;
            this.code = code;
            this.title = title;
            this.tags = tags;
            this.recommended = recommended;
            this.companyPay = companyPay;
            this.serviceFee = serviceFee;
            this.totalCost = totalCost;
            this.gross = gross;
            this.tax = tax;
            this.net = net;
            this.employerTotalOutflow = employerTotalOutflow;
            this.workerTakeHomePerWorker = workerCount > 0
                    ? workerTakeHomePerWorker.divide(BigDecimal.valueOf(workerCount), 2, RoundingMode.HALF_UP)
                    : workerTakeHomePerWorker;
            this.workerCount = workerCount;
        }

        public Long getPlanId() {
            return planId;
        }

        public String getCode() {
            return code;
        }

        public String getTitle() {
            return title;
        }

        public List<String> getTags() {
            return tags;
        }

        public boolean isRecommended() {
            return recommended;
        }

        public BigDecimal getCompanyPay() {
            return companyPay;
        }

        public BigDecimal getServiceFee() {
            return serviceFee;
        }

        public BigDecimal getTotalCost() {
            return totalCost;
        }

        public BigDecimal getGross() {
            return gross;
        }

        public BigDecimal getTax() {
            return tax;
        }

        public BigDecimal getNet() {
            return net;
        }

        public BigDecimal getEmployerTotalOutflow() {
            return employerTotalOutflow;
        }

        public BigDecimal getEmployerAvgCostPerWorker() {
            return workerCount > 0
                    ? employerTotalOutflow.divide(BigDecimal.valueOf(workerCount), 2, RoundingMode.HALF_UP)
                    : employerTotalOutflow;
        }

        public BigDecimal getWorkerTakeHomePerWorker() {
            return workerTakeHomePerWorker;
        }
    }
}
