from __future__ import annotations from dataclasses import asdict, dataclass from typing import Any BASELINE_STEPS_BY_LEVEL: dict[str, int] = { "sedentary": 5000, "light": 7000, "moderate": 9000, "active": 11000, "very_active": 13000, } WORKOUT_KCAL_PER_SESSION = 200 KCAL_PER_STEP_PER_KG = 0.0005 FALLBACK_KCAL_PER_MIN = 6 @dataclass class ActivityBonus: steps: int steps_baseline: int steps_bonus_kcal: float workout_active_kcal: float workout_baseline_kcal: float workout_bonus_kcal: float total_bonus_kcal: float scale_factor: float def to_dict(self) -> dict[str, Any]: return asdict(self) def baseline_steps(profile: dict[str, Any]) -> int: override = profile.get("baseline_steps") if override is not None: return int(override) level = str(profile.get("activity_level") or "moderate") return BASELINE_STEPS_BY_LEVEL.get(level, 9000) def baseline_workout_kcal_day(profile: dict[str, Any]) -> float: override = profile.get("baseline_workout_kcal") if override is not None: return float(override) weekly = int(profile.get("weekly_workouts") or 3) return round(weekly * WORKOUT_KCAL_PER_SESSION / 7, 1) def estimate_workout_active_kcal(workout: dict[str, Any]) -> float: active = workout.get("active_calories") if active is not None: return float(active) duration = workout.get("duration_min") if duration: return float(duration) * FALLBACK_KCAL_PER_MIN return 0.0 def steps_bonus_kcal(*, steps: int, baseline_steps: int, weight_kg: float) -> float: extra_steps = max(0, steps - baseline_steps) return round(extra_steps * weight_kg * KCAL_PER_STEP_PER_KG, 1) def compute_activity_bonus( profile: dict[str, Any], *, steps_total: int, workouts: list[dict[str, Any]], ) -> ActivityBonus: weight_kg = float(profile.get("weight_kg") or 70) steps_base = baseline_steps(profile) workout_base = baseline_workout_kcal_day(profile) s_bonus = steps_bonus_kcal(steps=steps_total, baseline_steps=steps_base, weight_kg=weight_kg) workout_active = round(sum(estimate_workout_active_kcal(w) for w in workouts), 1) w_bonus = max(0.0, round(workout_active - workout_base, 1)) total_bonus = round(s_bonus + w_bonus, 1) base_cal = float(profile.get("calorie_target") or 2000) scale_factor = 1.0 if base_cal <= 0 else round((base_cal + total_bonus) / base_cal, 4) return ActivityBonus( steps=steps_total, steps_baseline=steps_base, steps_bonus_kcal=s_bonus, workout_active_kcal=workout_active, workout_baseline_kcal=workout_base, workout_bonus_kcal=w_bonus, total_bonus_kcal=total_bonus, scale_factor=scale_factor, ) def _targets_dict( *, calories: float, protein_g: float, fat_g: float, carbs_g: float, water_ml: float, ) -> dict[str, float]: return { "calories": round(calories), "protein_g": round(protein_g), "fat_g": round(fat_g), "carbs_g": round(carbs_g), "water_ml": round(water_ml), } def build_base_targets(profile: dict[str, Any]) -> dict[str, float]: water_l = float(profile.get("water_l") or 2.5) return _targets_dict( calories=float(profile.get("calorie_target") or 2000), protein_g=float(profile.get("protein_g") or 140), fat_g=float(profile.get("fat_g") or 65), carbs_g=float(profile.get("carbs_g") or 200), water_ml=water_l * 1000, ) def scale_targets( base_targets: dict[str, float], bonus_kcal: float, ) -> tuple[dict[str, float], dict[str, float]]: """Return (effective_targets, targets_base). Water is not scaled.""" targets_base = dict(base_targets) base_cal = float(base_targets["calories"]) if bonus_kcal <= 0 or base_cal <= 0: return dict(base_targets), targets_base scale = (base_cal + bonus_kcal) / base_cal effective = _targets_dict( calories=base_cal + bonus_kcal, protein_g=float(base_targets["protein_g"]) * scale, fat_g=float(base_targets["fat_g"]) * scale, carbs_g=float(base_targets["carbs_g"]) * scale, water_ml=float(base_targets["water_ml"]), ) return effective, targets_base