package repository import ( "context" "time" "github.com/jackc/pgx/v5/pgxpool" "financeiro-carvalho/internal/middleware" "financeiro-carvalho/internal/model" ) type GameRepository struct { pool *pgxpool.Pool } func NewGameRepository(pool *pgxpool.Pool) *GameRepository { return &GameRepository{pool: pool} } func (r *GameRepository) GetProfile(ctx context.Context) (*model.PlayerProfile, error) { pid := middleware.ProfileIDFromCtx(ctx) var p model.PlayerProfile err := r.pool.QueryRow(ctx, `SELECT id, level, xp, xp_to_next FROM player_profile WHERE profile_id = $1`, pid). Scan(&p.ID, &p.Level, &p.XP, &p.XPToNext) return &p, err } func xpThreshold(level int) int { return level * level * 100 } func (r *GameRepository) AddXP(ctx context.Context, eventType string, amount int, description string) (*model.PlayerProfile, error) { pid := middleware.ProfileIDFromCtx(ctx) tx, err := r.pool.Begin(ctx) if err != nil { return nil, err } defer tx.Rollback(ctx) _, err = tx.Exec(ctx, `INSERT INTO xp_events (event_type, xp_earned, description, profile_id) VALUES ($1, $2, $3, $4)`, eventType, amount, description, pid) if err != nil { return nil, err } var p model.PlayerProfile err = tx.QueryRow(ctx, `SELECT id, level, xp, xp_to_next FROM player_profile WHERE profile_id = $1`, pid). Scan(&p.ID, &p.Level, &p.XP, &p.XPToNext) if err != nil { return nil, err } p.XP += amount for p.XP >= p.XPToNext { p.XP -= p.XPToNext p.Level++ p.XPToNext = xpThreshold(p.Level) } _, err = tx.Exec(ctx, `UPDATE player_profile SET level=$1, xp=$2, xp_to_next=$3 WHERE id=$4`, p.Level, p.XP, p.XPToNext, p.ID) if err != nil { return nil, err } return &p, tx.Commit(ctx) } func (r *GameRepository) ListActiveQuests(ctx context.Context) ([]model.PlayerQuest, error) { pid := middleware.ProfileIDFromCtx(ctx) now := time.Now() dailyPeriod := now.Format("2006-01-02") weeklyPeriod := now.Format("2006") + "-W" + now.Format("01") monthlyPeriod := now.Format("2006-01") rows, err := r.pool.Query(ctx, ` SELECT q.id, q.title, q.quest_type, q.target_count, q.target_pct, q.xp_reward, COALESCE(pq.id, 0), COALESCE(pq.period, ''), COALESCE(pq.current_count, 0), COALESCE(pq.current_pct, 0), COALESCE(pq.completed, false), COALESCE(pq.claimed, false) FROM quests q LEFT JOIN player_quests pq ON pq.quest_id = q.id AND pq.profile_id = $4 AND pq.period = CASE WHEN q.quest_type = 'daily' THEN $1 WHEN q.quest_type = 'weekly' THEN $2 WHEN q.quest_type = 'monthly' THEN $3 END WHERE q.active = true ORDER BY q.quest_type, q.id `, dailyPeriod, weeklyPeriod, monthlyPeriod, pid) if err != nil { return nil, err } defer rows.Close() var out []model.PlayerQuest for rows.Next() { var pq model.PlayerQuest var pqID int if err := rows.Scan(&pq.QuestID, &pq.Title, &pq.QuestType, &pq.TargetCount, &pq.TargetPct, &pq.XPReward, &pqID, &pq.Period, &pq.CurrentCount, &pq.CurrentPct, &pq.Completed, &pq.Claimed); err != nil { return nil, err } pq.ID = pqID if pq.Period == "" { switch pq.QuestType { case "daily": pq.Period = dailyPeriod case "weekly": pq.Period = weeklyPeriod case "monthly": pq.Period = monthlyPeriod } } out = append(out, pq) } return out, rows.Err() } func (r *GameRepository) EnsurePlayerQuest(ctx context.Context, questID int, period string) (int, error) { pid := middleware.ProfileIDFromCtx(ctx) var id int err := r.pool.QueryRow(ctx, ` INSERT INTO player_quests (quest_id, period, profile_id) VALUES ($1, $2, $3) ON CONFLICT (quest_id, period, profile_id) DO UPDATE SET quest_id = EXCLUDED.quest_id RETURNING id `, questID, period, pid).Scan(&id) return id, err } func (r *GameRepository) IncrementQuestCount(ctx context.Context, questType, period string, delta int) error { pid := middleware.ProfileIDFromCtx(ctx) _, err := r.pool.Exec(ctx, ` UPDATE player_quests pq SET current_count = current_count + $1, completed = (current_count + $1 >= q.target_count), completed_at = CASE WHEN (current_count + $1 >= q.target_count) AND NOT completed THEN NOW() ELSE completed_at END FROM quests q WHERE pq.quest_id = q.id AND q.quest_type = $2 AND pq.period = $3 AND pq.profile_id = $4 AND q.target_count IS NOT NULL AND NOT pq.claimed `, delta, questType, period, pid) return err } func (r *GameRepository) UpdateQuestPct(ctx context.Context, period string, pct float64) error { pid := middleware.ProfileIDFromCtx(ctx) _, err := r.pool.Exec(ctx, ` UPDATE player_quests pq SET current_pct = $1, completed = ($1 >= q.target_pct), completed_at = CASE WHEN ($1 >= q.target_pct) AND NOT completed THEN NOW() ELSE completed_at END FROM quests q WHERE pq.quest_id = q.id AND q.quest_type = 'monthly' AND pq.period = $2 AND pq.profile_id = $3 AND q.target_pct IS NOT NULL AND NOT pq.claimed `, pct, period, pid) return err } func (r *GameRepository) ClaimQuest(ctx context.Context, playerQuestID int) (int, error) { pid := middleware.ProfileIDFromCtx(ctx) var xpReward int err := r.pool.QueryRow(ctx, ` UPDATE player_quests pq SET claimed = true FROM quests q WHERE pq.quest_id = q.id AND pq.id = $1 AND pq.profile_id = $2 AND pq.completed AND NOT pq.claimed RETURNING q.xp_reward `, playerQuestID, pid).Scan(&xpReward) return xpReward, err } func (r *GameRepository) ListAchievements(ctx context.Context) ([]model.Achievement, error) { pid := middleware.ProfileIDFromCtx(ctx) rows, err := r.pool.Query(ctx, ` SELECT a.id, a.title, a.description, a.icon, a.xp_reward, a.unlock_condition, pa.id IS NOT NULL, COALESCE(pa.earned_at::text, '') FROM achievements a LEFT JOIN player_achievements pa ON pa.achievement_id = a.id AND pa.profile_id = $1 ORDER BY pa.earned_at DESC NULLS LAST, a.id `, pid) if err != nil { return nil, err } defer rows.Close() var out []model.Achievement for rows.Next() { var a model.Achievement if err := rows.Scan(&a.ID, &a.Title, &a.Description, &a.Icon, &a.XPReward, &a.UnlockCondition, &a.Earned, &a.EarnedAt); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } func (r *GameRepository) IsAchievementUnlocked(ctx context.Context, condition string) (bool, error) { pid := middleware.ProfileIDFromCtx(ctx) var count int err := r.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM player_achievements pa JOIN achievements a ON a.id = pa.achievement_id WHERE a.unlock_condition = $1 AND pa.profile_id = $2 `, condition, pid).Scan(&count) return count > 0, err } func (r *GameRepository) UnlockAchievement(ctx context.Context, condition string) (int, error) { pid := middleware.ProfileIDFromCtx(ctx) var xpReward int err := r.pool.QueryRow(ctx, ` INSERT INTO player_achievements (achievement_id, profile_id) SELECT id, $2 FROM achievements WHERE unlock_condition = $1 ON CONFLICT (achievement_id, profile_id) DO NOTHING RETURNING (SELECT xp_reward FROM achievements WHERE unlock_condition = $1) `, condition, pid).Scan(&xpReward) return xpReward, err } func (r *GameRepository) ListCosmetics(ctx context.Context) ([]model.Cosmetic, error) { pid := middleware.ProfileIDFromCtx(ctx) var level int _ = r.pool.QueryRow(ctx, `SELECT level FROM player_profile WHERE profile_id = $1`, pid).Scan(&level) rows, err := r.pool.Query(ctx, ` SELECT c.id, c.name, c.type, c.unlock_level, c.css_data::text, (c.unlock_level <= $1) AS unlocked, COALESCE(pc.equipped, false) FROM cosmetics c LEFT JOIN player_cosmetics pc ON pc.cosmetic_id = c.id AND pc.profile_id = $2 ORDER BY c.unlock_level, c.id `, level, pid) if err != nil { return nil, err } defer rows.Close() var out []model.Cosmetic for rows.Next() { var c model.Cosmetic if err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.UnlockLevel, &c.CSSData, &c.Unlocked, &c.Equipped); err != nil { return nil, err } out = append(out, c) } return out, rows.Err() } func (r *GameRepository) EquipCosmetic(ctx context.Context, cosmeticID int) error { pid := middleware.ProfileIDFromCtx(ctx) tx, err := r.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) _, err = tx.Exec(ctx, ` UPDATE player_cosmetics pc SET equipped = false FROM cosmetics c WHERE pc.cosmetic_id = c.id AND c.type = (SELECT type FROM cosmetics WHERE id = $1) AND pc.profile_id = $2 `, cosmeticID, pid) if err != nil { return err } _, err = tx.Exec(ctx, ` INSERT INTO player_cosmetics (cosmetic_id, equipped, profile_id) VALUES ($1, true, $2) ON CONFLICT (cosmetic_id, profile_id) DO UPDATE SET equipped = true `, cosmeticID, pid) if err != nil { return err } return tx.Commit(ctx) } func (r *GameRepository) UnlockCosmeticsForLevel(ctx context.Context, level int) error { pid := middleware.ProfileIDFromCtx(ctx) _, err := r.pool.Exec(ctx, ` INSERT INTO player_cosmetics (cosmetic_id, profile_id) SELECT id, $2 FROM cosmetics WHERE unlock_level <= $1 ON CONFLICT (cosmetic_id, profile_id) DO NOTHING `, level, pid) return err }