package com.pandoli365.bibimbap.jam;

import com.pandoli365.bibimbap.data.JamAwardData;
import com.pandoli365.bibimbap.mapper.JamAwardsMapper;
import com.pandoli365.bibimbap.mapper.JamReviewRatingMapper;
import com.pandoli365.bibimbap.mapper.JamScoreStatsMapper;
import com.pandoli365.bibimbap.mapper.JamVotesMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 시상 산정 코어 — 3트랙 독립 랭킹(JUDGE/USER_RATING/POPULAR) + GRAND 종합(순위점수
 * 정규화 가중합). W2-3 동결 jam_awards 소비, 4개 소스(jam_score_stats/game_review_stats/
 * jam_votes/jam_entries)는 SELECT 만(단방향 G4/AC-13).
 *
 * <p>멱등 재산정: {@link #recompute}는 4트랙 deleteByJamTrack 후 재INSERT 를 단일
 * @Transactional 경계에서 수행 — 두 번 돌려도 결과 동일, 부분 실패 시 트랙 비는 상태 회피.
 *
 * <p>NULL/미달 트랙 제외(A4, 부당 0점 회피):
 * <ul>
 *   <li>JUDGE — weighted_total NULL(채점 0) 행 제외(매퍼 NULLS LAST 후 본 코드 필터)</li>
 *   <li>USER_RATING — review_count>=3 임계(매퍼 WHERE 에서 제외)</li>
 *   <li>POPULAR — 득표>0 출품작만(매퍼 GROUP BY 결과; 0표는 행 없음)</li>
 *   <li>GRAND — 최소 1트랙 가용 출품작. 가용 트랙만 분자·분모에 포함</li>
 * </ul>
 */
@Service
public class JamAwardService {

    /** 트랙 균등 가중치(A3 상수). 잼별 가변은 후속(design concern 4). */
    private static final double WEIGHT_JUDGE = 1.0 / 3.0;
    private static final double WEIGHT_USER_RATING = 1.0 / 3.0;
    private static final double WEIGHT_POPULAR = 1.0 / 3.0;

    private final JamAwardsMapper jamAwardsMapper;
    private final JamScoreStatsMapper jamScoreStatsMapper;
    private final JamReviewRatingMapper jamReviewRatingMapper;
    private final JamVotesMapper jamVotesMapper;

    public JamAwardService(JamAwardsMapper jamAwardsMapper,
                           JamScoreStatsMapper jamScoreStatsMapper,
                           JamReviewRatingMapper jamReviewRatingMapper,
                           JamVotesMapper jamVotesMapper) {
        this.jamAwardsMapper = jamAwardsMapper;
        this.jamScoreStatsMapper = jamScoreStatsMapper;
        this.jamReviewRatingMapper = jamReviewRatingMapper;
        this.jamVotesMapper = jamVotesMapper;
    }

    /**
     * 산정/재산정(멱등). 4트랙 초기화 후 재기록. 반환=트랙별 수상 행수.
     *
     * @param jamId 산정 대상 잼
     * @return {JUDGE:n, USER_RATING:n, POPULAR:n, GRAND:n} 트랙별 수상 행수
     */
    @Transactional
    public Map<String, Integer> recompute(long jamId) {
        for (AwardTrack track : AwardTrack.values()) {
            jamAwardsMapper.deleteByJamTrack(jamId, track.name());
        }

        // 트랙별 모집단(gameId -> raw 점수). NULL/미달 제외 후의 수상권 모집단.
        Map<Long, Double> judgePop = trackPopulation(
                jamScoreStatsMapper.listStatsByJam(jamId), "gameId", "weightedTotal");
        Map<Long, Double> ratingPop = trackPopulation(
                jamReviewRatingMapper.listAvgByJam(jamId), "gameId", "avgRating");
        Map<Long, Double> popularPop = trackPopulation(
                jamVotesMapper.listCountsByJam(jamId), "gameId", "voteCount");

        Map<String, Integer> counts = new LinkedHashMap<>();
        counts.put(AwardTrack.JUDGE.name(), insertTrack(jamId, AwardTrack.JUDGE, judgePop));
        counts.put(AwardTrack.USER_RATING.name(), insertTrack(jamId, AwardTrack.USER_RATING, ratingPop));
        counts.put(AwardTrack.POPULAR.name(), insertTrack(jamId, AwardTrack.POPULAR, popularPop));
        counts.put(AwardTrack.GRAND.name(), insertGrand(jamId, judgePop, ratingPop, popularPop));
        return counts;
    }

    /**
     * 단일 트랙 산정: 모집단을 raw 점수 DESC 로 랭크(competition) 후 jam_awards 기록.
     * score_value = 트랙 raw 점수.
     */
    private int insertTrack(long jamId, AwardTrack track, Map<Long, Double> population) {
        if (population.isEmpty()) {
            return 0;
        }
        List<Long> games = new ArrayList<>(population.keySet());
        Comparator<Long> byScore = Comparator.comparingDouble(
                (Long g) -> population.get(g)).reversed();
        Comparator<Long> byOrder = byScore.thenComparing(Comparator.naturalOrder()); // game_id ASC tiebreak
        Map<Long, Integer> ranks = RankScores.standardCompetitionRank(games, byOrder, byScore);

        int n = 0;
        for (Map.Entry<Long, Integer> e : ranks.entrySet()) {
            jamAwardsMapper.insert(award(jamId, e.getKey(), track, e.getValue(),
                    BigDecimal.valueOf(population.get(e.getKey()))));
            n++;
        }
        return n;
    }

    /**
     * GRAND 종합: 각 트랙 rankScore (N-rank+1)/N 의 가용트랙 가중평균.
     * grand = Σ(rankScore_track × weight_track) / Σ(weight_track 가용) ∈ [1/N, 1].
     * 모집단 = 최소 1트랙 가용 출품작 union. 가용 트랙만 분자·분모 포함(부당 0점 회피).
     */
    private int insertGrand(long jamId,
                            Map<Long, Double> judgePop,
                            Map<Long, Double> ratingPop,
                            Map<Long, Double> popularPop) {
        Map<Long, Double> judgeScore = rankScoreMap(judgePop);
        Map<Long, Double> ratingScore = rankScoreMap(ratingPop);
        Map<Long, Double> popularScore = rankScoreMap(popularPop);

        Set<Long> grandPop = new LinkedHashSet<>();
        grandPop.addAll(judgePop.keySet());
        grandPop.addAll(ratingPop.keySet());
        grandPop.addAll(popularPop.keySet());
        if (grandPop.isEmpty()) {
            return 0;
        }

        Map<Long, Double> grand = new LinkedHashMap<>();
        for (Long gameId : grandPop) {
            double weighted = 0.0;
            double weightSum = 0.0;
            if (judgeScore.containsKey(gameId)) {
                weighted += judgeScore.get(gameId) * WEIGHT_JUDGE;
                weightSum += WEIGHT_JUDGE;
            }
            if (ratingScore.containsKey(gameId)) {
                weighted += ratingScore.get(gameId) * WEIGHT_USER_RATING;
                weightSum += WEIGHT_USER_RATING;
            }
            if (popularScore.containsKey(gameId)) {
                weighted += popularScore.get(gameId) * WEIGHT_POPULAR;
                weightSum += WEIGHT_POPULAR;
            }
            // grandPop 은 최소 1트랙 가용이므로 weightSum > 0 보장
            grand.put(gameId, weighted / weightSum);
        }

        List<Long> games = new ArrayList<>(grand.keySet());
        Comparator<Long> byScore = Comparator.comparingDouble(
                (Long g) -> grand.get(g)).reversed();
        Comparator<Long> byOrder = byScore.thenComparing(Comparator.naturalOrder());
        Map<Long, Integer> ranks = RankScores.standardCompetitionRank(games, byOrder, byScore);

        int n = 0;
        for (Map.Entry<Long, Integer> e : ranks.entrySet()) {
            jamAwardsMapper.insert(award(jamId, e.getKey(), AwardTrack.GRAND, e.getValue(),
                    BigDecimal.valueOf(grand.get(e.getKey()))));
            n++;
        }
        return n;
    }

    /** 모집단 gameId -> rankScore (N-rank+1)/N. 빈 모집단이면 빈 맵. */
    private Map<Long, Double> rankScoreMap(Map<Long, Double> population) {
        if (population.isEmpty()) {
            return Map.of();
        }
        List<Long> games = new ArrayList<>(population.keySet());
        Comparator<Long> byScore = Comparator.comparingDouble(
                (Long g) -> population.get(g)).reversed();
        Comparator<Long> byOrder = byScore.thenComparing(Comparator.naturalOrder());
        return RankScores.rankScore(games, byOrder, byScore);
    }

    /**
     * 집계 소스 Map 리스트를 gameId -> 점수 모집단으로 변환. 점수 NULL 행은 제외(A4).
     * JUDGE 의 weighted_total NULL(채점 0) 행 필터 위치이기도 하다(매퍼 NULLS LAST 후).
     */
    private Map<Long, Double> trackPopulation(List<Map<String, Object>> rows,
                                              String idKey, String scoreKey) {
        Map<Long, Double> population = new LinkedHashMap<>();
        for (Map<String, Object> row : rows) {
            Object idObj = row.get(idKey);
            Object scoreObj = row.get(scoreKey);
            if (idObj == null || scoreObj == null) {
                continue; // 점수 NULL → 트랙 제외(부당 0점 회피)
            }
            long gameId = ((Number) idObj).longValue();
            double score = ((Number) scoreObj).doubleValue();
            population.put(gameId, score);
        }
        return population;
    }

    private JamAwardData award(long jamId, long gameId, AwardTrack track, int rank, BigDecimal scoreValue) {
        JamAwardData award = new JamAwardData();
        award.setJamId(jamId);
        award.setGameId(gameId);
        award.setAwardTrack(track.name());
        award.setRank(rank);
        award.setScoreValue(scoreValue);
        return award;
    }
}
