"""Fitness gamification: streaks, cumulative energy balance vs factual TDEE, milestone medals.""" from __future__ import annotations import time from datetime import date, datetime, timezone from typing import Any from sqlalchemy.orm import Session from app.daily.metrics import DayMetricsService from app.fitness.service import FitnessService _CACHE_TTL_SEC = 300 _gamification_cache: dict[str, tuple[float, dict[str, Any]]] = {} def clear_gamification_cache() -> None: _gamification_cache.clear() def _cache_key(user_id: int, end: date) -> str: return f"{user_id}:{end.isoformat()}" MEDAL_DEFS: list[dict[str, Any]] = [ { "id": "meal_streak_3", "title": "Трекер еды", "description": "3 дня подряд с логом питания", "icon": "🍽", "metric": "meal_log_streak", "target": 3, }, { "id": "meal_streak_7", "title": "Неделя еды", "description": "7 дней подряд с логом питания", "icon": "🥗", "metric": "meal_log_streak", "target": 7, }, { "id": "meal_streak_30", "title": "Дисциплина", "description": "30 дней подряд с логом питания", "icon": "🏆", "metric": "meal_log_best_streak", "target": 30, }, { "id": "plan_hit_5_week", "title": "План недели", "description": "5 дней за неделю в целевых калориях", "icon": "🎯", "metric": "plan_hit_week", "target": 5, }, { "id": "plan_hit_30_total", "title": "Стабильность", "description": "30 дней всего в плане по калориям", "icon": "📈", "metric": "plan_hit_total", "target": 30, }, { "id": "workout_streak_3", "title": "В движении", "description": "3 дня подряд с тренировкой", "icon": "💪", "metric": "workout_streak", "target": 3, }, { "id": "workout_week_goal", "title": "Неделя силы", "description": "Выполнена недельная цель тренировок", "icon": "🏋", "metric": "workouts_this_week", "target_metric": "weekly_workout_target", }, { "id": "deficit_week_3500", "title": "Минус кило", "description": "Дефицит ~3500 ккал за неделю", "icon": "🔥", "metric": "deficit_week", "target": 3500, }, { "id": "deficit_month_7000", "title": "Марафон", "description": "Дефицит 7000 ккал за месяц", "icon": "⭐", "metric": "deficit_month", "target": 7000, }, ] def build_gamification(db: Session, user_id: int, *, end_day: date | None = None) -> dict[str, Any]: end = end_day or datetime.now(timezone.utc).date() key = _cache_key(user_id, end) now = time.time() cached = _gamification_cache.get(key) if cached and now - cached[0] < _CACHE_TTL_SEC: return cached[1] fitness = FitnessService(db, user_id) profile = fitness.get_profile() or {} goal = str(profile.get("goal") or "lose") weekly_workout_target = int(profile.get("weekly_workouts") or 3) bundle = DayMetricsService(db, user_id).metrics_bundle(end) workout_stats = fitness.get_workout_stats(days=7, end_day=end) workout_streak = int(workout_stats.get("streak") or 0) workouts_this_week = int(workout_stats.get("count") or 0) metrics: dict[str, int] = { "meal_log_streak": bundle["meal_log_streak"], "meal_log_best_streak": bundle["meal_log_best_streak"], "plan_hit_streak": bundle["plan_hit_streak"], "plan_hit_total": bundle["plan_hit_total"], "plan_hit_week": bundle["plan_hit_week"], "workout_streak": workout_streak, "workouts_this_week": workouts_this_week, "weekly_workout_target": weekly_workout_target, "deficit_week": bundle["deficit_week"], "deficit_month": bundle["deficit_month"], "deficit_all": bundle["deficit_all"], } medals: list[dict[str, Any]] = [] for spec in MEDAL_DEFS: target_metric = spec.get("target_metric") if target_metric: target = int(metrics.get(target_metric, 0)) progress = int(metrics.get(spec["metric"], 0)) earned = progress >= target and target > 0 else: target = int(spec["target"]) progress = int(metrics.get(spec["metric"], 0)) earned = progress >= target medals.append( { "id": spec["id"], "title": spec["title"], "description": spec["description"], "icon": spec["icon"], "earned": earned, "progress": progress, "target": target, } ) if goal == "gain": balance_label = "профицит" elif goal == "maintain": balance_label = "баланс" else: balance_label = "дефицит" payload = { "end_date": end.isoformat(), "goal": goal, "balance_label": balance_label, "streaks": { "meal_log": bundle["meal_log_streak"], "meal_log_best": bundle["meal_log_best_streak"], "plan_hit": bundle["plan_hit_streak"], "workout": workout_streak, }, "totals": { "plan_hit_days": bundle["plan_hit_total"], "plan_hit_week": bundle["plan_hit_week"], }, "balance": { "week": bundle["deficit_week"], "month": bundle["deficit_month"], "all": bundle["deficit_all"], "label": balance_label, }, "workouts": { "count_week": workouts_this_week, "weekly_target": weekly_workout_target, "streak": workout_streak, }, "medals": medals, "metrics": metrics, } _gamification_cache[key] = (now, payload) return payload