package com.ga.batch.settlement.step;

import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.domain.BatchTaxCalculator;
import com.ga.batch.settlement.domain.TaxBreakdown;
import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.commission.AwardLedgerMapper;
import com.ga.core.mapper.commission.BranchMgrAllowanceLedgerMapper;
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
import com.ga.core.mapper.commission.ClawbackRecoveryMapper;
import com.ga.core.mapper.commission.CollectionCommissionLedgerMapper;
import com.ga.core.mapper.commission.LifeSupportLedgerMapper;
import com.ga.core.mapper.commission.PersistencyBonusMapper;
import com.ga.core.mapper.commission.ReferralCommissionLedgerMapper;
import com.ga.core.mapper.commission.ReinstatementCommissionLedgerMapper;
import com.ga.core.mapper.commission.RenewalCommissionLedgerMapper;
import com.ga.core.mapper.commission.SettlingSupportLedgerMapper;
import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.mapper.settle.AgentDeductionMapper;
import com.ga.core.mapper.settle.ChargebackMapper;
import com.ga.core.mapper.settle.CommissionStatementMapper;
import com.ga.core.mapper.settle.OverrideSettleMapper;
import com.ga.core.mapper.settle.SettleMasterMapper;
import com.ga.core.vo.commission.ClawbackReceivableResp;
import com.ga.core.vo.commission.ClawbackReceivableVO;
import com.ga.core.vo.commission.ClawbackRecoveryVO;
import com.ga.core.vo.org.AgentResp;
import com.ga.core.vo.org.AgentSearchParam;
import com.ga.core.vo.org.AgentVO;
import com.ga.core.vo.org.TaxType;
import com.ga.core.vo.org.VatType;
import com.ga.core.vo.settle.AgentDeductionVO;
import com.ga.core.vo.settle.CommissionStatementVO;
import com.ga.core.vo.settle.DeductionStatus;
import com.ga.core.vo.settle.SettleMasterVO;
import com.ga.core.vo.settle.StatementType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Step 8 — aggregate: 설계사별 통합 집계 → settle_master UPSERT
 *
 * 통합 내용:
 * - BatchTaxCalculator: 설계사 세금유형(TaxType/VatType)에 따라 정확한 세금 계산
 *   (사업소득 3.3% / 기타소득 8.8% + 지방소득세 10% + 부가세 10%)
 * - AgentDeductionMapper: PENDING 차감 합산 → netAmount에서 차감 후 APPLIED 상태로 전이
 * - CommissionStatementMapper: 설계사별 명세서 레코드(ISSUED 상태) INSERT
 * - ClawbackReceivableMapper/ClawbackRecoveryMapper: 환수채권 이월/상계 (V111)
 */
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class AggregateStep implements Tasklet {

    private final SettlementContext ctx;
    private final AgentMapper agentMapper;
    private final RecruitLedgerMapper recruitMapper;
    private final MaintainLedgerMapper maintainMapper;
    private final ExceptionLedgerMapper exceptionMapper;
    private final OverrideSettleMapper overrideMapper;
    private final ChargebackMapper chargebackMapper;
    private final SettleMasterMapper settleMapper;
    private final AgentDeductionMapper deductionMapper;
    private final CommissionStatementMapper statementMapper;
    private final BatchTaxCalculator taxCalculator;
    private final ClawbackReceivableMapper receivableMapper;
    private final ClawbackRecoveryMapper recoveryMapper;

    // 설계사 지급성 9종 원장 Mapper
    private final CollectionCommissionLedgerMapper collectionMapper;
    private final RenewalCommissionLedgerMapper renewalMapper;
    private final ReinstatementCommissionLedgerMapper reinstatementMapper;
    private final PersistencyBonusMapper persistencyBonusMapper;
    private final ReferralCommissionLedgerMapper referralMapper;
    private final SettlingSupportLedgerMapper settlingSupportMapper;
    private final AwardLedgerMapper awardMapper;
    private final LifeSupportLedgerMapper lifeSupportMapper;
    private final BranchMgrAllowanceLedgerMapper branchMgrAllowanceMapper;

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
        log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth());

        Map<Long, BigDecimal> recruitMap    = toMap(recruitMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> maintainMap   = toMap(maintainMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> chargebackMap = toMap(chargebackMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> overrideMap   = toMap(overrideMapper.aggregateByUpper(ctx.getSettleMonth()));

        // 예외는 PLUS/MINUS 분리
        Map<Long, BigDecimal> excPlus  = new HashMap<>();
        Map<Long, BigDecimal> excMinus = new HashMap<>();
        for (Map<String, Object> row : exceptionMapper.aggregateByAgent(ctx.getSettleMonth())) {
            Long agentId = ((Number) row.get("agentId")).longValue();
            excPlus.put(agentId, toBd(row.get("plus")));
            excMinus.put(agentId, toBd(row.get("minus")));
        }

        // 설계사 지급성 9종 집계
        Map<Long, BigDecimal> collectionMap    = toMap(collectionMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> renewalMap       = toMap(renewalMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> reinstatementMap = toMap(reinstatementMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> persistencyMap   = toMap(persistencyBonusMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> referralMap      = toMap(referralMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> settlingSupportMap = toMap(settlingSupportMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> awardMap         = toMap(awardMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> lifeSupportMap   = toMap(lifeSupportMapper.aggregateByAgent(ctx.getSettleMonth()));
        Map<Long, BigDecimal> branchMgrMap     = toMap(branchMgrAllowanceMapper.aggregateByAgent(ctx.getSettleMonth()));

        // 합집합 agent_id
        Set<Long> agentIds = new HashSet<>();
        agentIds.addAll(recruitMap.keySet());
        agentIds.addAll(maintainMap.keySet());
        agentIds.addAll(chargebackMap.keySet());
        agentIds.addAll(overrideMap.keySet());
        agentIds.addAll(excPlus.keySet());
        agentIds.addAll(excMinus.keySet());
        agentIds.addAll(collectionMap.keySet());
        agentIds.addAll(renewalMap.keySet());
        agentIds.addAll(reinstatementMap.keySet());
        agentIds.addAll(persistencyMap.keySet());
        agentIds.addAll(referralMap.keySet());
        agentIds.addAll(settlingSupportMap.keySet());
        agentIds.addAll(awardMap.keySet());
        agentIds.addAll(lifeSupportMap.keySet());
        agentIds.addAll(branchMgrMap.keySet());

        // 활성 설계사 누락 보충
        AgentSearchParam ap = new AgentSearchParam();
        ap.setStatus("ACTIVE");
        ap.setPageSize(Integer.MAX_VALUE);
        for (AgentResp a : agentMapper.selectList(ap)) {
            agentIds.add(a.getAgentId());
        }

        // 정산월 PENDING 차감 목록 일괄 조회 (배치 효율)
        List<AgentDeductionVO> allDeductions = deductionMapper.selectPendingByMonth(ctx.getSettleMonth());
        Map<Long, List<AgentDeductionVO>> deductionByAgent = new HashMap<>();
        for (AgentDeductionVO d : allDeductions) {
            deductionByAgent.computeIfAbsent(d.getAgentId(), k -> new ArrayList<>()).add(d);
        }

        List<Long> appliedDeductionIds = new ArrayList<>();
        List<CommissionStatementVO> statements = new ArrayList<>();
        int count = 0;

        // 멱등 reverse (루프 진입 전 1회):
        // 이 settleMonth의 기존 clawback_recovery 이력을 이용해 각 receivable의
        // balance를 복원한 뒤 회수 이력을 전량 삭제한다.
        // 동일 월 재실행 시 중복 회수가 발생하지 않도록 보장한다.
        reverseClawbackRecovery(ctx.getSettleMonth());

        for (Long agentId : agentIds) {
            BigDecimal recruit  = recruitMap.getOrDefault(agentId, BigDecimal.ZERO);
            BigDecimal maintain = maintainMap.getOrDefault(agentId, BigDecimal.ZERO);
            BigDecimal cb       = chargebackMap.getOrDefault(agentId, BigDecimal.ZERO);
            BigDecimal ov       = overrideMap.getOrDefault(agentId, BigDecimal.ZERO);
            BigDecimal ep       = excPlus.getOrDefault(agentId, BigDecimal.ZERO);
            BigDecimal em       = excMinus.getOrDefault(agentId, BigDecimal.ZERO);

            // 설계사 지급성 9종 합산 (원장 없으면 ZERO → gross 불변 보장)
            BigDecimal otherCommission = BigDecimal.ZERO;
            otherCommission = MoneyUtil.add(otherCommission, collectionMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, renewalMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, reinstatementMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, persistencyMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, referralMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, settlingSupportMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, awardMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, lifeSupportMap.getOrDefault(agentId, BigDecimal.ZERO));
            otherCommission = MoneyUtil.add(otherCommission, branchMgrMap.getOrDefault(agentId, BigDecimal.ZERO));

            BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep)
                                        .subtract(cb).subtract(em);
            gross = MoneyUtil.add(gross, otherCommission);

            // 세금유형 조회 (없으면 기본: BUSINESS_INCOME / NONE)
            AgentVO agentVO = agentMapper.selectById(agentId);
            TaxType taxType = parseTaxType(agentVO);
            VatType vatType = parseVatType(agentVO);

            // BatchTaxCalculator로 정확한 세금 계산
            TaxBreakdown tax = taxCalculator.calculate(gross, taxType, vatType);
            BigDecimal totalTax = tax.incomeTax().add(tax.localTax()); // settle_master.tax_amount

            // 차감 합산
            List<AgentDeductionVO> agentDeductions = deductionByAgent.getOrDefault(agentId, List.of());
            BigDecimal deductionTotal = agentDeductions.stream()
                    .map(AgentDeductionVO::getAmount)
                    .reduce(BigDecimal.ZERO, BigDecimal::add);

            BigDecimal netAmount = tax.netAmount().subtract(deductionTotal);

            // 환수채권 이월/상계 계산 (V111)
            // outstandingBalance: origin_settle_month < settleMonth 인 미결 채권 잔액 합계
            // (당월 발생분은 자기상계 방지를 위해 포함하지 않음)
            BigDecimal priorBalance = receivableMapper.outstandingBalance(agentId, ctx.getSettleMonth());
            if (priorBalance == null) priorBalance = BigDecimal.ZERO;

            BigDecimal recovered;
            BigDecimal carryover;
            BigDecimal payable;
            if (netAmount.signum() >= 0) {
                recovered = netAmount.min(priorBalance);   // 기존 채권 FIFO 상계 (net 한도 내)
                carryover = BigDecimal.ZERO;
                payable   = netAmount.subtract(recovered);
            } else {
                recovered = BigDecimal.ZERO;
                carryover = netAmount.negate();            // 음수분 신규 채권화
                payable   = BigDecimal.ZERO;
            }
            log.info("[Step8 clawback] agentId={}, net={}, priorBalance={}, recovered={}, carryover={}, payable={}",
                    agentId, netAmount, priorBalance, recovered, carryover, payable);

            // settle_master UPSERT
            SettleMasterVO vo = new SettleMasterVO();
            vo.setAgentId(agentId);
            vo.setSettleMonth(ctx.getSettleMonth());
            vo.setRecruitTotal(recruit);
            vo.setMaintainTotal(maintain);
            vo.setExceptionPlus(ep);
            vo.setExceptionMinus(em);
            vo.setOverrideTotal(ov);
            vo.setChargebackTotal(cb);
            vo.setOtherCommissionTotal(otherCommission);
            vo.setGrossAmount(gross);
            vo.setTaxAmount(totalTax);        // incomeTax + localTax 합산
            vo.setNetAmount(netAmount);       // gross - incomeTax - localTax + vat - deduction (불변)
            vo.setClawbackRecovered(recovered);
            vo.setClawbackCarryover(carryover);
            vo.setPayableAmount(payable);
            vo.setStatus("CALCULATED");
            settleMapper.upsert(vo);
            count++;

            // 채권 부수효과: carryover 이월 등록 (UNIQUE upsert — 멱등)
            if (carryover.signum() > 0) {
                ClawbackReceivableVO rcv = new ClawbackReceivableVO();
                rcv.setAgentId(agentId);
                rcv.setOriginSettleMonth(ctx.getSettleMonth());
                rcv.setOriginalAmount(carryover);
                rcv.setBalance(carryover);
                rcv.setRecoveredAmount(BigDecimal.ZERO);
                rcv.setStatus("OUTSTANDING");
                receivableMapper.upsertCarryover(rcv);
            }

            // 채권 부수효과: FIFO 상계 회수 적용
            if (recovered.signum() > 0) {
                applyRecovery(agentId, ctx.getSettleMonth(), recovered);
            }

            // 차감 적용 완료 처리
            if (!agentDeductions.isEmpty()) {
                agentDeductions.forEach(d -> appliedDeductionIds.add(d.getDeductionId()));
            }

            // 명세서 레코드 생성 (commission_statement INSERT, 중복 시 skip)
            CommissionStatementVO existingStmt = statementMapper.selectByAgentMonth(
                    agentId, ctx.getSettleMonth(), StatementType.MONTHLY.code());
            if (existingStmt == null && gross.signum() != 0) {
                CommissionStatementVO stmt = new CommissionStatementVO();
                stmt.setAgentId(agentId);
                stmt.setSettleMonth(ctx.getSettleMonth());
                stmt.setStatementType(StatementType.MONTHLY.code());
                stmt.setGrossAmount(gross);
                stmt.setIncomeTaxAmount(tax.incomeTax());
                stmt.setLocalTaxAmount(tax.localTax());
                stmt.setVatAmount(tax.vat());
                stmt.setDeductionAmount(deductionTotal);
                stmt.setNetAmount(netAmount);
                stmt.setIssueStatus("ISSUED");
                stmt.setIssuedAt(LocalDateTime.now());
                statements.add(stmt);
            }
        }

        // 차감 APPLIED 상태 일괄 전이
        if (!appliedDeductionIds.isEmpty()) {
            deductionMapper.updateStatusBatch(appliedDeductionIds, DeductionStatus.APPLIED.name());
            log.info("[Step8] deductions applied: {}", appliedDeductionIds.size());
        }

        // 명세서 일괄 INSERT
        for (CommissionStatementVO stmt : statements) {
            statementMapper.insert(stmt);
        }
        if (!statements.isEmpty()) {
            log.info("[Step8] commission statements inserted: {}", statements.size());
        }

        ctx.setAggregateCount(count);
        log.info("[Step8] settle master upserted: {}", count);
        return RepeatStatus.FINISHED;
    }

    /**
     * 멱등 reverse: settleMonth의 기존 clawback_recovery 이력을 조회하여
     * 각 receivable의 balance/recoveredAmount/status를 복원한 뒤 회수 이력을 전량 삭제한다.
     *
     * 알고리즘:
     * 1. selectByMonth(settleMonth)로 이번 달 회수 이력 전체 조회
     * 2. receivable별로 이번 달 회수액 합산
     * 3. 각 receivable에 대해 selectById로 현재 상태 조회
     * 4. balance += thisMonthSum, recoveredAmount -= thisMonthSum 후 status 재계산
     * 5. deleteByMonth(settleMonth)로 이력 전량 삭제
     *
     * 재실행 안전: 1회차에 이력이 없으면 즉시 반환. 2회차에 이력이 있으면 정확히 복원.
     */
    private void reverseClawbackRecovery(String settleMonth) {
        List<ClawbackRecoveryVO> prev = recoveryMapper.selectByMonth(settleMonth);
        if (prev.isEmpty()) {
            return;
        }

        // receivable별로 이번 달 회수액 합산 (동일 receivable 다건 방어)
        Map<Long, BigDecimal> sumByReceivable = new HashMap<>();
        for (ClawbackRecoveryVO r : prev) {
            sumByReceivable.merge(r.getReceivableId(), r.getRecoveredAmount(), BigDecimal::add);
        }

        // 각 receivable의 현재 상태를 읽어 balance/recoveredAmount 복원
        for (Map.Entry<Long, BigDecimal> e : sumByReceivable.entrySet()) {
            Long receivableId = e.getKey();
            BigDecimal toRestore = e.getValue();

            ClawbackReceivableResp current = receivableMapper.selectById(receivableId);
            if (current == null) {
                log.warn("[Step8 reverse] receivable not found: receivableId={}", receivableId);
                continue;
            }

            BigDecimal newRecovered = current.getRecoveredAmount().subtract(toRestore);
            BigDecimal newBalance   = current.getBalance().add(toRestore);

            // status 재계산: recovered가 0이면 OUTSTANDING, 0보다 크면 RECOVERING
            String newStatus;
            if (newRecovered.signum() == 0) {
                newStatus = "OUTSTANDING";
            } else {
                newStatus = "RECOVERING";
            }

            receivableMapper.updateRecovery(receivableId, newRecovered, newBalance, newStatus, null);
        }

        recoveryMapper.deleteByMonth(settleMonth);
        log.info("[Step8 reverse] clawback recovery reversed for settleMonth={}, receivables={}",
                settleMonth, sumByReceivable.size());
    }

    /**
     * FIFO 상계 회수 적용: 오래된 origin_settle_month 순으로 채권을 소진하며
     * recoveredTotal 만큼 clawback_receivable balance를 차감하고
     * clawback_recovery 이력을 INSERT한다.
     *
     * recovered는 priorBalance(=selectOutstandingByAgentFifo 대상 채권 합계)를
     * 초과하지 않으므로 remaining은 루프 내에서 정확히 소진된다.
     */
    private void applyRecovery(Long agentId, String settleMonth, BigDecimal recoveredTotal) {
        List<ClawbackReceivableVO> fifoList =
                receivableMapper.selectOutstandingByAgentFifo(agentId, settleMonth);

        BigDecimal remaining = recoveredTotal;
        for (ClawbackReceivableVO rcv : fifoList) {
            if (remaining.signum() == 0) break;

            BigDecimal take       = remaining.min(rcv.getBalance());
            BigDecimal newRecovered = rcv.getRecoveredAmount().add(take);
            BigDecimal newBalance   = rcv.getBalance().subtract(take);
            String newStatus        = newBalance.signum() == 0 ? "CLEARED" : "RECOVERING";

            receivableMapper.updateRecovery(
                    rcv.getReceivableId(), newRecovered, newBalance, newStatus, settleMonth);

            ClawbackRecoveryVO recovery = new ClawbackRecoveryVO();
            recovery.setReceivableId(rcv.getReceivableId());
            recovery.setSettleMonth(settleMonth);
            recovery.setRecoveredAmount(take);
            recoveryMapper.insertOne(recovery);

            remaining = remaining.subtract(take);
        }
    }

    private TaxType parseTaxType(AgentVO agent) {
        if (agent == null || agent.getTaxType() == null) return TaxType.BUSINESS_INCOME;
        try {
            return TaxType.valueOf(agent.getTaxType());
        } catch (IllegalArgumentException e) {
            return TaxType.BUSINESS_INCOME;
        }
    }

    private VatType parseVatType(AgentVO agent) {
        if (agent == null || agent.getVatType() == null) return VatType.NONE;
        try {
            return VatType.valueOf(agent.getVatType());
        } catch (IllegalArgumentException e) {
            return VatType.NONE;
        }
    }

    private Map<Long, BigDecimal> toMap(List<Map<String, Object>> rows) {
        Map<Long, BigDecimal> m = new HashMap<>();
        for (Map<String, Object> r : rows) {
            Long id = ((Number) r.get("agentId")).longValue();
            m.put(id, toBd(r.get("total")));
        }
        return m;
    }

    private BigDecimal toBd(Object v) {
        if (v == null) return BigDecimal.ZERO;
        if (v instanceof BigDecimal bd) return bd;
        return new BigDecimal(v.toString());
    }
}
