"""유저별 포인트 누적 — grade_prediction 의 (key, value) 를 user_points 에 반영.

채점(apply_result)에서 호출. 결과 정정(재채점)에도 안전하도록 영향받은
이메일의 채점 가능한 픽 전체를 재집계해 upsert 한다(idempotent — 몇 번을
다시 돌려도 같은 결과, 증분 방식의 이중 누적 위험 없음).
"""
from __future__ import annotations

import logging

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from ..domain import now_utc
from ..models import Match, UserPoints, UserPrediction
from ..scoring import grade_prediction

log = logging.getLogger("triplepick.points")

# scoring.json 의 key → user_points 등급별 횟수 컬럼
_KEY_TO_COL = {
    "score_exact": "exact_count",
    "score_close": "close_count",
    "score_outcome": "outcome_count",
    "score_partial": "partial_count",
    "score_miss": "miss_count",
}


def _empty() -> dict:
    return {
        "total_points": 0,
        "exact_count": 0,
        "close_count": 0,
        "outcome_count": 0,
        "partial_count": 0,
        "miss_count": 0,
        "matches_played": 0,
        "first_scored_at": None,
    }


async def accumulate_user_points(
    db: AsyncSession, emails: set[str | None]
) -> int:
    """이메일별 누적 포인트 재집계 → user_points upsert. 갱신 행 수 반환.

    커밋은 호출자(apply_result) 책임 — 채점과 누적이 한 트랜잭션으로 묶인다.
    """
    targets = {e.strip().lower() for e in emails if e and e.strip()}
    if not targets:
        return 0

    rows = (
        await db.execute(
            select(UserPrediction, Match)
            .join(Match, UserPrediction.match_id == Match.match_id)
            .where(
                func.lower(UserPrediction.email).in_(targets),
                Match.result_score_a.is_not(None),
                Match.result_score_b.is_not(None),
            )
        )
    ).all()

    agg: dict[str, dict] = {e: _empty() for e in targets}
    for pick, match in rows:
        key, value = grade_prediction(
            pick.score_a, pick.score_b, match.result_score_a, match.result_score_b
        )
        t = agg[pick.email.strip().lower()]
        t["total_points"] += value
        t[_KEY_TO_COL[key]] += 1
        t["matches_played"] += 1
        ts = pick.scored_at or pick.created_at
        if ts and (t["first_scored_at"] is None or ts < t["first_scored_at"]):
            t["first_scored_at"] = ts

    existing = {
        up.email: up
        for up in (
            await db.execute(select(UserPoints).where(UserPoints.email.in_(targets)))
        ).scalars()
    }
    for email, t in agg.items():
        row = existing.get(email)
        if row is None:
            row = UserPoints(email=email)
            db.add(row)
        for col, val in t.items():
            setattr(row, col, val)
        row.updated_at = now_utc()

    log.info(
        "user_points: %d명 누적 갱신 (%s)",
        len(agg),
        ", ".join(f"{e}={t['total_points']}p" for e, t in agg.items()),
    )
    return len(agg)
