""" Gamification Router - XP, Levels, Leaderboards """ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from typing import List from pydantic import BaseModel from datetime import datetime from database import get_session from models import User, Badge, UserBadge from routers.auth import get_current_user from services.gamification import ( calculate_level, xp_for_next_level, update_streak, check_and_award_badges, get_leaderboard, seed_badges, LEVEL_NAMES, XP_REWARDS, ) router = APIRouter(prefix="/gamification", tags=["gamification"]) class LevelProgress(BaseModel): current_xp: int level: int level_name: str xp_for_next: int xp_progress: int progress_percent: float streak_days: int class LeaderboardEntry(BaseModel): rank: int username: str xp: int level: int level_name: str streak: int class BadgeResponse(BaseModel): id: int name: str description: str icon: str slug: str tier: str category: str awarded_at: datetime | None = None class XPRewardsInfo(BaseModel): rewards: dict level_thresholds: List[int] level_names: dict @router.get("/me", response_model=LevelProgress) async def get_my_progress( current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Get current user's XP and level progress""" xp_needed, xp_progress = xp_for_next_level(current_user.xp) progress_percent = (xp_progress / xp_needed * 100) if xp_needed > 0 else 100 return LevelProgress( current_xp=current_user.xp, level=current_user.level, level_name=LEVEL_NAMES.get(current_user.level, "Unknown"), xp_for_next=xp_needed, xp_progress=xp_progress, progress_percent=round(progress_percent, 1), streak_days=current_user.streak_days, ) @router.post("/activity") async def record_activity( current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Record user activity and update streak""" streak = update_streak(session, current_user) new_badges = check_and_award_badges(session, current_user) session.commit() return { "streak_days": streak, "new_badges": [b.name for b in new_badges], "xp": current_user.xp, "level": current_user.level, } @router.get("/leaderboard", response_model=List[LeaderboardEntry]) async def get_xp_leaderboard( limit: int = 10, session: Session = Depends(get_session) ): """Get top users by XP""" leaders = get_leaderboard(session, limit) return [ LeaderboardEntry( rank=i + 1, username=l["email"], xp=l["xp"], level=l["level"], level_name=l["level_name"], streak=l["streak"], ) for i, l in enumerate(leaders) ] @router.get("/badges", response_model=List[BadgeResponse]) async def get_all_badges(session: Session = Depends(get_session)): """Get all available badges""" badges = session.exec(select(Badge).order_by(Badge.tier, Badge.category)).all() return [ BadgeResponse( id=b.id, name=b.name, description=b.description, icon=b.icon, slug=b.slug, tier=b.tier, category=b.category, ) for b in badges ] @router.get("/badges/me", response_model=List[BadgeResponse]) async def get_my_badges( current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Get current user's earned badges""" user_badges = session.exec( select(UserBadge, Badge) .join(Badge) .where(UserBadge.user_id == current_user.id) .order_by(UserBadge.awarded_at.desc()) ).all() return [ BadgeResponse( id=b.id, name=b.name, description=b.description, icon=b.icon, slug=b.slug, tier=b.tier, category=b.category, awarded_at=ub.awarded_at, ) for ub, b in user_badges ] @router.get("/info", response_model=XPRewardsInfo) async def get_xp_info(): """Get XP reward values and level thresholds""" from services.gamification import LEVEL_THRESHOLDS return XPRewardsInfo( rewards=XP_REWARDS, level_thresholds=LEVEL_THRESHOLDS, level_names=LEVEL_NAMES, ) @router.post("/seed-badges") async def seed_badge_data( current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Seed badge definitions (admin only)""" if current_user.role not in ["admin", "moderator"]: raise HTTPException(status_code=403, detail="Admin access required") seed_badges(session) return {"message": "Badges seeded successfully"}