from datetime import date from typing import Any from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.auth.deps import get_current_user from app.db.base import get_db from app.db.models import User from app.fitness.service import FitnessService from app.fitness.structuring import structure_meal, structure_workout from app.integrations.openfoodfacts import OpenFoodFactsClient from app.integrations.wger import WgerClient router = APIRouter() class ProfileUpdate(BaseModel): sex: str | None = None age: int | None = None height_cm: float | None = None weight_kg: float | None = None goal: str | None = None target_weight_kg: float | None = None neat_base_kcal: float | None = Field(default=None, ge=200, le=300) class MealCreate(BaseModel): text: str = Field(min_length=1) meal_type: str | None = None class WaterCreate(BaseModel): amount_ml: int = Field(gt=0) class WeightCreate(BaseModel): weight_kg: float = Field(gt=0) body_fat_pct: float | None = None chest_cm: float | None = None waist_cm: float | None = None neck_cm: float | None = None hip_cm: float | None = None notes: str = "" day: str | None = None days_ago: int | None = Field(default=None, ge=0, le=90) recorded_at: str | None = None class BodyCompositionCalc(BaseModel): weight_kg: float | None = None height_cm: float | None = None sex: str | None = None neck_cm: float | None = None waist_cm: float | None = None hip_cm: float | None = None body_fat_pct: float | None = None class StepsCreate(BaseModel): steps: int = Field(ge=0) active_calories: float | None = None notes: str = "" day: str | None = None days_ago: int | None = Field(default=None, ge=0, le=90) logged_at: str | None = None class WorkoutCreate(BaseModel): text: str = Field(min_length=1) day: str | None = None days_ago: int | None = Field(default=None, ge=0, le=90) logged_at: str | None = None class ReminderUpdate(BaseModel): enabled: bool | None = None hour: int | None = Field(default=None, ge=0, le=23) minute: int | None = Field(default=None, ge=0, le=59) interval_hours: int | None = Field(default=None, ge=1, le=12) @router.get("/fitness") def get_snapshot(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]: return FitnessService(db, user.id).snapshot() @router.get("/fitness/summary") def get_summary( day: str | None = None, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: d = date.fromisoformat(day) if day else None return FitnessService(db, user.id).get_daily_summary(d) @router.get("/fitness/workout-stats") def get_workout_stats( days: int = 7, end: str | None = None, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: end_day = date.fromisoformat(end) if end else None return FitnessService(db, user.id).get_workout_stats(days=days, end_day=end_day) @router.get("/fitness/history") def get_history( days: int = 7, end: str | None = None, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: end_day = date.fromisoformat(end) if end else None return FitnessService(db, user.id).get_history(days=days, end_day=end_day) @router.get("/fitness/charts") def get_charts( weeks: int = 52, trend: bool = True, end: str | None = None, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: end_day = date.fromisoformat(end) if end else None return FitnessService(db, user.id).get_charts(weeks=weeks, trend=trend, end_day=end_day) @router.get("/fitness/profile") def get_profile(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]: profile = FitnessService(db, user.id).get_profile() return profile or {"configured": False} @router.put("/fitness/profile") def update_profile( payload: ProfileUpdate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: return FitnessService(db, user.id).set_profile(payload.model_dump(exclude_none=True)) @router.post("/fitness/profile/calc") def calc_targets( payload: ProfileUpdate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: params = payload.model_dump(exclude_none=True) if not params: raise HTTPException(status_code=400, detail="No parameters") return FitnessService(db, user.id).calc_targets(params) @router.post("/fitness/meals") async def create_meal( payload: MealCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: service = FitnessService(db, user.id) try: structured = await structure_meal(payload.text) except Exception as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc return service.log_meal( description=structured.get("description") or payload.text, meal_type=payload.meal_type or structured.get("meal_type") or "snack", calories=float(structured.get("calories") or 0), protein_g=float(structured.get("protein_g") or 0), fat_g=float(structured.get("fat_g") or 0), carbs_g=float(structured.get("carbs_g") or 0), source="llm", estimated=bool(structured.get("estimated", True)), ) @router.post("/fitness/water") def create_water( payload: WaterCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: return FitnessService(db, user.id).log_water(payload.amount_ml) @router.post("/fitness/weight") def create_weight( payload: WeightCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: day = date.fromisoformat(payload.day) if payload.day else None return FitnessService(db, user.id).log_weight( payload.weight_kg, body_fat_pct=payload.body_fat_pct, chest_cm=payload.chest_cm, waist_cm=payload.waist_cm, neck_cm=payload.neck_cm, hip_cm=payload.hip_cm, notes=payload.notes, recorded_at=payload.recorded_at, day=day, days_ago=payload.days_ago, ) @router.post("/fitness/body-composition/calc") def calc_body_composition( payload: BodyCompositionCalc, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: return FitnessService(db, user.id).calc_body_composition(payload.model_dump(exclude_none=True)) @router.post("/fitness/steps") def create_steps( payload: StepsCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: day = date.fromisoformat(payload.day) if payload.day else None return FitnessService(db, user.id).log_steps( payload.steps, active_calories=payload.active_calories, notes=payload.notes, day=day, days_ago=payload.days_ago, logged_at=payload.logged_at, ) @router.delete("/fitness/steps/{log_id}") def delete_steps(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]: if not FitnessService(db, user.id).delete_step_log(log_id): raise HTTPException(status_code=404, detail="Not found") return {"ok": True} @router.post("/fitness/workouts") async def create_workout( payload: WorkoutCreate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: service = FitnessService(db, user.id) try: structured = await structure_workout(payload.text) except Exception as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc day = date.fromisoformat(payload.day) if payload.day else None return service.log_workout( title=structured.get("title") or "Тренировка", notes=structured.get("notes") or payload.text, duration_min=structured.get("duration_min"), exercises=structured.get("exercises"), active_calories=structured.get("active_calories"), total_calories=structured.get("total_calories"), steps=structured.get("steps"), activity_type=structured.get("activity_type"), met=structured.get("met"), day=day, days_ago=payload.days_ago, logged_at=payload.logged_at, ) @router.get("/fitness/body-metrics") def list_metrics( limit: int = 30, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> list[dict[str, Any]]: return FitnessService(db, user.id).list_body_metrics(limit=limit) @router.delete("/fitness/meals/{log_id}") def delete_meal(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]: if not FitnessService(db, user.id).delete_food_log(log_id): raise HTTPException(status_code=404, detail="Not found") return {"ok": True} @router.delete("/fitness/water/{log_id}") def delete_water(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]: if not FitnessService(db, user.id).delete_water_log(log_id): raise HTTPException(status_code=404, detail="Not found") return {"ok": True} @router.delete("/fitness/workouts/{log_id}") def delete_workout(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]: if not FitnessService(db, user.id).delete_workout_log(log_id): raise HTTPException(status_code=404, detail="Not found") return {"ok": True} @router.get("/fitness/reminders") def list_reminders(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]: return FitnessService(db, user.id).list_reminders() @router.put("/fitness/reminders/{kind}") def update_reminder( kind: str, payload: ReminderUpdate, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> dict[str, Any]: return FitnessService(db, user.id).set_reminder( kind, enabled=payload.enabled, hour=payload.hour, minute=payload.minute, interval_hours=payload.interval_hours, ) @router.get("/fitness/lookup/food") def lookup_food(q: str, limit: int = 5) -> list[dict[str, Any]]: return OpenFoodFactsClient().search(q, limit=limit) @router.get("/fitness/lookup/exercise") def lookup_exercise(q: str, limit: int = 8) -> list[dict[str, Any]]: return WgerClient().search_exercises(q, limit=limit)