package com.zioinfo.mall.loyalty;

import com.zioinfo.mall.admin.AuditService;
import com.zioinfo.mall.loyalty.mapper.LoyaltyMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.*;

/**
 * 등급별 고객관리 서비스 — 등급 자동 산정 + 포인트 적립/사용/만료.
 *
 * <p>등급은 최근 12개월 누적 구매액·주문수를 등급 기준(mall_tier_benefit)과 비교하여
 * 충족하는 가장 높은 등급으로 자동 산정한다. 산정 결과는 mall_member.tier 로 sync 한다.
 * 포인트 적립은 등급별 적립률(pointEarnRate)을 적용한다. 외부 호출 없음.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LoyaltyService {

    private final LoyaltyMapper mapper;
    private final AuditService auditService;

    private static final int POINT_EXPIRE_DAYS = 365;

    /** 등급 혜택 목록(rank ASC). */
    public List<MallTierBenefit> tiers(boolean activeOnly) {
        return mapper.findTiers(activeOnly);
    }

    public MallTierBenefit tierByCode(String code) {
        return mapper.findTierByCode(code == null ? "BASIC" : code.toUpperCase());
    }

    /** 현재 자격 등급 코드(12개월 누적 기준). */
    public String currentTierCode(String owner) {
        return qualify(mapper.spend12m(owner)).getTierCode();
    }

    /**
     * 내 등급/혜택 + 12개월 누적 + 포인트 잔액 통합 조회.
     * 저장된 등급(mall_member.tier)과 현재 자격을 함께 제공한다.
     */
    public Map<String, Object> myStatus(String owner) {
        Map<String, Object> spend = mapper.spend12m(owner);
        MallTierBenefit qualified = qualify(spend);
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("owner", owner);
        out.put("spend12m", spend.getOrDefault("spend", 0));
        out.put("orderCount12m", spend.getOrDefault("orderCount", 0));
        out.put("tier", qualified.getTierCode());
        out.put("tierName", qualified.getName());
        out.put("benefit", qualified);
        out.put("pointBalance", balance(owner));
        out.put("nextTier", nextTierGap(qualified, spend));
        return out;
    }

    /** 12개월 누적값으로 자격 등급 산정(가장 높은 충족 등급). 폴백: BASIC. */
    public MallTierBenefit qualify(Map<String, Object> spend12m) {
        BigDecimal spend = toBig(spend12m.get("spend"));
        long orders = toLong(spend12m.get("orderCount"));
        List<MallTierBenefit> tiers = mapper.findTiers(true);
        MallTierBenefit best = null;
        for (MallTierBenefit t : tiers) {
            BigDecimal minSpend = t.getMinSpend12m() == null ? BigDecimal.ZERO : t.getMinSpend12m();
            int minOrders = t.getMinOrders12m() == null ? 0 : t.getMinOrders12m();
            if (spend.compareTo(minSpend) >= 0 && orders >= minOrders) {
                if (best == null || (t.getTierRank() != null && best.getTierRank() != null
                        && t.getTierRank() > best.getTierRank())) {
                    best = t;
                }
            }
        }
        if (best != null) return best;
        MallTierBenefit basic = mapper.findTierByCode("BASIC");
        return basic != null ? basic : fallbackBasic();
    }

    /** 다음 등급까지 부족분(있으면). */
    private Map<String, Object> nextTierGap(MallTierBenefit current, Map<String, Object> spend12m) {
        List<MallTierBenefit> tiers = mapper.findTiers(true);
        MallTierBenefit next = null;
        int curRank = current.getTierRank() == null ? 1 : current.getTierRank();
        for (MallTierBenefit t : tiers) {
            if (t.getTierRank() != null && t.getTierRank() == curRank + 1) { next = t; break; }
        }
        if (next == null) return Map.of("isTop", true);
        BigDecimal spend = toBig(spend12m.get("spend"));
        long orders = toLong(spend12m.get("orderCount"));
        BigDecimal spendGap = (next.getMinSpend12m() == null ? BigDecimal.ZERO : next.getMinSpend12m())
                .subtract(spend).max(BigDecimal.ZERO);
        long orderGap = Math.max(0, (next.getMinOrders12m() == null ? 0 : next.getMinOrders12m()) - orders);
        Map<String, Object> gap = new LinkedHashMap<>();
        gap.put("isTop", false);
        gap.put("nextTier", next.getTierCode());
        gap.put("nextTierName", next.getName());
        gap.put("spendNeeded", spendGap);
        gap.put("ordersNeeded", orderGap);
        return gap;
    }

    /** 단일 고객 등급 재산정 + member.tier sync. 변경 시 감사로그. */
    @Transactional
    public Map<String, Object> recalcOne(String owner) {
        Map<String, Object> spend = mapper.spend12m(owner);
        MallTierBenefit q = qualify(spend);
        mapper.updateMemberTier(owner, q.getTierCode());
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("owner", owner);
        out.put("tier", q.getTierCode());
        out.put("spend12m", spend.getOrDefault("spend", 0));
        out.put("orderCount12m", spend.getOrDefault("orderCount", 0));
        return out;
    }

    /** 전체 회원 등급 일괄 재산정(배치성). */
    @Transactional
    public Map<String, Object> recalcAll() {
        List<String> users = mapper.allMemberUsernames();
        int updated = 0;
        Map<String, Integer> dist = new LinkedHashMap<>();
        for (String u : users) {
            MallTierBenefit q = qualify(mapper.spend12m(u));
            mapper.updateMemberTier(u, q.getTierCode());
            dist.merge(q.getTierCode(), 1, Integer::sum);
            updated++;
        }
        auditService.log("LOYALTY_RECALC_ALL", "members", "count=" + updated);
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("recalculated", updated);
        out.put("distribution", dist);
        return out;
    }

    // ----------------------------------------------------------- 포인트
    public int balance(String owner) {
        Integer b = mapper.balance(owner);
        return b == null ? 0 : b;
    }

    public List<MallPointLedger> history(String owner, int limit) {
        return mapper.history(owner, limit <= 0 || limit > 500 ? 100 : limit);
    }

    /**
     * 결제 완료 시 등급별 적립률로 포인트 자동 적립(주문 흐름 훅).
     * 이미 동일 주문번호로 EARN 기록이 있으면 중복 방지(idempotent).
     */
    @Transactional
    public int accrueForOrder(String owner, String orderNo, BigDecimal payAmount) {
        if (owner == null || payAmount == null || payAmount.signum() <= 0) return 0;
        MallTierBenefit tier = qualify(mapper.spend12m(owner));
        BigDecimal rate = tier.getPointEarnRate() == null ? BigDecimal.ONE : tier.getPointEarnRate();
        int points = payAmount.multiply(rate).divide(BigDecimal.valueOf(100), 0, RoundingMode.DOWN).intValue();
        if (points <= 0) return 0;
        MallPointLedger e = new MallPointLedger();
        e.setOwner(owner);
        e.setEntryType("EARN");
        e.setPoints(points);
        e.setOrderNo(orderNo);
        e.setReason("Order " + orderNo + " (" + tier.getTierCode() + " " + rate + "%)");
        e.setExpireAt(LocalDate.now().plusDays(POINT_EXPIRE_DAYS));
        mapper.insertPoint(e);
        return points;
    }

    /** 포인트 사용(차감). 잔액 부족 시 예외. */
    @Transactional
    public Map<String, Object> usePoints(String owner, int points, String orderNo) {
        if (points <= 0) throw new IllegalArgumentException("ERR-LOY-400: 사용 포인트는 1 이상이어야 합니다");
        int bal = balance(owner);
        if (points > bal) throw new RuntimeException("ERR-LOY-409: 포인트 잔액 부족 (잔액 " + bal + ")");
        MallPointLedger e = new MallPointLedger();
        e.setOwner(owner);
        e.setEntryType("USE");
        e.setPoints(-points);
        e.setOrderNo(orderNo);
        e.setReason(orderNo == null ? "Point redemption" : "Redeemed on order " + orderNo);
        mapper.insertPoint(e);
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("used", points);
        out.put("balance", balance(owner));
        return out;
    }

    /** 관리자 포인트 조정(가감). */
    @Transactional
    public Map<String, Object> adjust(String owner, int points, String reason, String actor) {
        MallPointLedger e = new MallPointLedger();
        e.setOwner(owner);
        e.setEntryType("ADJUST");
        e.setPoints(points);
        e.setReason(reason == null ? "Admin adjustment" : reason);
        mapper.insertPoint(e);
        auditService.log(actor, "LOYALTY_POINT_ADJUST", owner, "points=" + points);
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("owner", owner);
        out.put("adjusted", points);
        out.put("balance", balance(owner));
        return out;
    }

    /**
     * VIP 우선 슬롯 판정 헬퍼 — 해당 고객 등급에 priority_slot 혜택이 있는지.
     * schedule 모듈이 피크시즌 우선 배정 시 호출.
     */
    public boolean hasPrioritySlot(String owner) {
        MallTierBenefit t = qualify(mapper.spend12m(owner));
        return Boolean.TRUE.equals(t.getPrioritySlot());
    }

    public List<Map<String, Object>> salesByTier(int days) {
        return mapper.salesByTier(days <= 0 ? 30 : days);
    }

    // ----------------------------------------------------------- helpers
    private BigDecimal toBig(Object o) {
        if (o == null) return BigDecimal.ZERO;
        if (o instanceof BigDecimal b) return b;
        if (o instanceof Number n) return BigDecimal.valueOf(n.doubleValue());
        try { return new BigDecimal(o.toString()); } catch (Exception e) { return BigDecimal.ZERO; }
    }

    private long toLong(Object o) {
        if (o == null) return 0L;
        if (o instanceof Number n) return n.longValue();
        try { return Long.parseLong(o.toString()); } catch (Exception e) { return 0L; }
    }

    private MallTierBenefit fallbackBasic() {
        MallTierBenefit b = new MallTierBenefit();
        b.setTierCode("BASIC");
        b.setName("일반 (Basic)");
        b.setTierRank(1);
        b.setDiscountRate(BigDecimal.ZERO);
        b.setPointEarnRate(BigDecimal.ONE);
        b.setPrioritySlot(false);
        return b;
    }
}
