package com.ga.batch.settlement.step;

import com.ga.batch.settlement.SettlementContext;
import com.ga.common.util.MoneyUtil;
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.org.OrganizationMapper;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.mapper.settle.OverrideSettleMapper;
import com.ga.core.vo.org.AgentResp;
import com.ga.core.vo.org.AgentSearchParam;
import com.ga.core.vo.rule.OverrideRuleVO;
import com.ga.core.vo.settle.OverrideSettleVO;
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.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Step 7 — calcOverride: 조직 트리 기반 오버라이드 계산.
 *
 * 정책:
 *  - 모든 ACTIVE 설계사를 순회하며 본인의 grade 와 일치하는 from_grade 규칙을 찾음
 *  - 본인 조직 + 모든 상위 조직에서 to_grade 직급의 ACTIVE 설계사 1명을 찾아 분배 (가장 가까운 조직 우선)
 *  - base = 하위 설계사의 정산월 모집 + 유지 합계
 *  - amount = base × override_pct (cap 적용)
 */
@Slf4j
@Component
@JobScope
@RequiredArgsConstructor
public class CalcOverrideStep implements Tasklet {

    private final SettlementContext ctx;
    private final RuleMapper ruleMapper;
    private final AgentMapper agentMapper;
    private final OrganizationMapper organizationMapper;
    private final OverrideSettleMapper overrideMapper;
    private final RecruitLedgerMapper recruitMapper;
    private final MaintainLedgerMapper maintainMapper;

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

        overrideMapper.deleteBySettleMonth(ctx.getSettleMonth());
        List<OverrideRuleVO> rules = ruleMapper.selectOverrideRules();

        LocalDate baseDate = LocalDate.parse(
                ctx.getSettleMonth().substring(0, 4) + "-" +
                ctx.getSettleMonth().substring(4) + "-01");
        rules.removeIf(r -> r.getEffectiveFrom() != null && r.getEffectiveFrom().isAfter(baseDate));
        rules.removeIf(r -> r.getEffectiveTo() != null && r.getEffectiveTo().isBefore(baseDate));

        AgentSearchParam ap = new AgentSearchParam();
        ap.setStatus("ACTIVE");
        ap.setPageSize(Integer.MAX_VALUE);
        List<AgentResp> agents = agentMapper.selectList(ap);

        Map<String, Long> upperCache = new HashMap<>();
        List<OverrideSettleVO> records = new ArrayList<>();

        for (AgentResp lower : agents) {
            for (OverrideRuleVO rule : rules) {
                if (!rule.getFromGrade().equals(lower.getGradeId())) continue;

                Long upperAgentId = findUpperAgentByGrade(
                        lower.getOrgId(), rule.getToGrade(), lower.getAgentId(), upperCache);
                if (upperAgentId == null) continue;

                BigDecimal base = sumLowerCommission(
                        lower.getAgentId(), ctx.getSettleMonth(), rule.getInsuranceTypeCond());
                if (MoneyUtil.isZero(base)) continue;

                BigDecimal amount = MoneyUtil.pct(base, rule.getOverridePct());
                if (rule.getCapAmount() != null && amount.compareTo(rule.getCapAmount()) > 0) {
                    amount = rule.getCapAmount();
                }
                if (MoneyUtil.isZero(amount)) continue;

                OverrideSettleVO os = new OverrideSettleVO();
                os.setUpperAgentId(upperAgentId);
                os.setLowerAgentId(lower.getAgentId());
                os.setSettleMonth(ctx.getSettleMonth());
                os.setBaseAmount(base);
                os.setOverridePct(rule.getOverridePct());
                os.setOverrideAmount(amount);
                records.add(os);
            }
        }
        if (!records.isEmpty()) overrideMapper.insertBatch(records);
        ctx.setOverrideCount(records.size());
        log.info("[Step7] override inserted: {}", records.size());
        return RepeatStatus.FINISHED;
    }

    /**
     * 하위 설계사의 정산월 모집 + 유지 합계.
     * 룰에 insurance_type_cond 가 지정되면 해당 보험종류 실적만 base 로 집계.
     * (지정 없음/ALL 이면 전 보험종류 합계 — 보험종류 한정 룰이 전체 실적에 곱해져 과지급되는 것을 방지)
     */
    private BigDecimal sumLowerCommission(Long agentId, String settleMonth, String insuranceTypeCond) {
        if (insuranceTypeCond != null && !insuranceTypeCond.isBlank()
                && !"ALL".equalsIgnoreCase(insuranceTypeCond)) {
            BigDecimal r = recruitMapper.sumByAgentMonthAndType(agentId, settleMonth, insuranceTypeCond);
            BigDecimal m = maintainMapper.sumByAgentMonthAndType(agentId, settleMonth, insuranceTypeCond);
            return MoneyUtil.add(r, m);
        }
        BigDecimal r = recruitMapper.sumByAgentMonth(agentId, settleMonth);
        BigDecimal m = maintainMapper.sumByAgentMonth(agentId, settleMonth);
        return MoneyUtil.add(r, m);
    }

    /**
     * 자기 조직부터 상향 탐색하며 to_grade 직급의 ACTIVE 설계사 1명 반환.
     * 자기 자신은 제외. 캐시: (lowerOrgId, toGrade) → upperAgentId.
     */
    private Long findUpperAgentByGrade(Long lowerOrgId, Long toGrade, Long lowerAgentId,
                                       Map<String, Long> cache) {
        if (lowerOrgId == null) return null;
        String key = lowerOrgId + ":" + toGrade;
        Long cached = cache.get(key);
        if (cached != null) return cached.equals(0L) ? null : cached;

        List<Long> ancestorIds = organizationMapper.selectAncestorIds(lowerOrgId);
        for (Long orgId : ancestorIds) {
            AgentSearchParam p = new AgentSearchParam();
            p.setOrgId(orgId);
            p.setGradeId(toGrade);
            p.setStatus("ACTIVE");
            p.setPageSize(5);
            List<AgentResp> upper = agentMapper.selectList(p);
            for (AgentResp a : upper) {
                if (!a.getAgentId().equals(lowerAgentId)) {
                    cache.put(key, a.getAgentId());
                    return a.getAgentId();
                }
            }
        }
        cache.put(key, 0L);
        return null;
    }
}
