from typing import Any from app.fitness.activity_budget import workouts_kcal_total DEFAULT_NEAT_KCAL = 200.0 NEAT_KCAL_MIN = 200.0 NEAT_KCAL_MAX = 300.0 KCAL_PER_STEP_REF = 0.04 / 86 # ~0.04 kcal/step at 86 kg WATER_ML_PER_KG = 33 # middle of 30–35 ml/kg range GOAL_CALORIE_ADJUST = { "lose": -500, "maintain": 0, "gain": 300, } PROTEIN_G_PER_KG = { "lose": 2.2, "maintain": 1.8, "gain": 1.8, } FAT_G_PER_KG = 1.0 def bmr_mifflin(*, sex: str, weight_kg: float, height_cm: float, age: int) -> float: base = 10 * weight_kg + 6.25 * height_cm - 5 * age if sex.lower() in ("m", "male", "м", "мужской"): return base + 5 return base - 161 def neat_base_kcal(profile: dict[str, Any]) -> float: raw = profile.get("neat_base_kcal") if raw is not None: return max(NEAT_KCAL_MIN, min(NEAT_KCAL_MAX, float(raw))) return DEFAULT_NEAT_KCAL def steps_kcal(*, steps: int, weight_kg: float) -> float: if steps <= 0: return 0.0 return round(steps * weight_kg * KCAL_PER_STEP_REF, 1) def bmi(weight_kg: float, height_cm: float) -> float: if height_cm <= 0: return 0.0 h = height_cm / 100 return weight_kg / (h * h) def water_target_l(weight_kg: float) -> float: return round(weight_kg * WATER_ML_PER_KG / 1000, 1) def macro_targets( calorie_target: float, weight_kg: float, goal: str = "maintain", ) -> dict[str, float]: protein_g = round(weight_kg * PROTEIN_G_PER_KG.get(goal, 1.8), 0) fat_g = round(weight_kg * FAT_G_PER_KG, 0) protein_cal = protein_g * 4 fat_cal = fat_g * 9 carbs_g = max(0, round((calorie_target - protein_cal - fat_cal) / 4, 0)) return {"protein_g": protein_g, "fat_g": fat_g, "carbs_g": carbs_g} def one_rep_max(weight_kg: float, reps: int) -> float: if reps <= 0: return weight_kg if reps == 1: return weight_kg return round(weight_kg * (1 + reps / 30), 1) def _profile_fields(profile: dict[str, Any]) -> tuple[float, float, int, str, str]: weight = float(profile.get("weight_kg") or 70) height = float(profile.get("height_cm") or 170) age = int(profile.get("age") or 30) sex = str(profile.get("sex") or "male") goal = str(profile.get("goal") or "maintain") return weight, height, age, sex, goal def compute_tdee( profile: dict[str, Any], *, steps_total: int = 0, workouts: list[dict[str, Any]] | None = None, ) -> dict[str, float]: weight, height, age, sex, _ = _profile_fields(profile) bmr = bmr_mifflin(sex=sex, weight_kg=weight, height_cm=height, age=age) neat = neat_base_kcal(profile) s_kcal = steps_kcal(steps=steps_total, weight_kg=weight) w_kcal = workouts_kcal_total(workouts or [], weight_kg=weight) tdee_val = bmr + neat + s_kcal + w_kcal return { "bmr": round(bmr, 0), "neat_kcal": round(neat, 0), "steps_kcal": s_kcal, "workout_kcal": w_kcal, "tdee": round(tdee_val, 0), } def compute_daily_targets( profile: dict[str, Any], *, steps_total: int = 0, workouts: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: weight, height, age, sex, goal = _profile_fields(profile) breakdown = compute_tdee(profile, steps_total=steps_total, workouts=workouts) calorie_target = round(breakdown["tdee"] + GOAL_CALORIE_ADJUST.get(goal, 0), 0) macros = macro_targets(calorie_target, weight, goal) water = water_target_l(weight) return { **breakdown, "calorie_target": calorie_target, "protein_g": macros["protein_g"], "fat_g": macros["fat_g"], "carbs_g": macros["carbs_g"], "water_l": water, "bmi": round(bmi(weight, height), 1), "steps": steps_total, } def targets_to_api(daily: dict[str, Any]) -> dict[str, float]: return { "calories": daily["calorie_target"], "protein_g": daily["protein_g"], "fat_g": daily["fat_g"], "carbs_g": daily["carbs_g"], "water_ml": round(daily["water_l"] * 1000), } def tdee_breakdown_to_api(daily: dict[str, Any]) -> dict[str, Any]: return { "bmr": daily["bmr"], "neat_kcal": daily["neat_kcal"], "steps_kcal": daily["steps_kcal"], "workout_kcal": daily["workout_kcal"], "tdee": daily["tdee"], "calorie_target": daily["calorie_target"], "steps": daily.get("steps", 0), } def compute_targets(profile: dict[str, Any]) -> dict[str, Any]: """Rest-day targets (BMR + NEAT, no steps/workouts) for profile storage.""" daily = compute_daily_targets(profile, steps_total=0, workouts=[]) return { "bmr": daily["bmr"], "tdee": daily["tdee"], "bmi": daily["bmi"], "neat_kcal": daily["neat_kcal"], "steps_kcal": 0, "workout_kcal": 0, "calorie_target": daily["calorie_target"], "protein_g": daily["protein_g"], "fat_g": daily["fat_g"], "carbs_g": daily["carbs_g"], "water_l": daily["water_l"], }