from fastapi import APIRouter, Depends, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc, func from pydantic import BaseModel from typing import Optional, List from datetime import datetime, date from app.core.database import get_db from app.core.security import get_current_user from app.models.user import User, HealthMetric router = APIRouter() class HealthMetricOut(BaseModel): id: int date: datetime resting_hr: Optional[float] max_hr_day: Optional[float] avg_hr_day: Optional[float] hrv_nightly_avg: Optional[float] hrv_status: Optional[str] hrv_5min_high: Optional[float] hrv_5min_low: Optional[float] sleep_duration_s: Optional[float] sleep_deep_s: Optional[float] sleep_light_s: Optional[float] sleep_rem_s: Optional[float] sleep_awake_s: Optional[float] sleep_score: Optional[float] sleep_start: Optional[datetime] sleep_end: Optional[datetime] weight_kg: Optional[float] bmi: Optional[float] body_fat_pct: Optional[float] muscle_mass_kg: Optional[float] vo2max: Optional[float] fitness_age: Optional[int] training_load: Optional[float] recovery_time_h: Optional[float] avg_stress: Optional[float] steps: Optional[int] floors_climbed: Optional[int] active_calories: Optional[float] total_calories: Optional[float] spo2_avg: Optional[float] class Config: from_attributes = True @router.get("/", response_model=List[HealthMetricOut]) async def list_health_metrics( from_date: Optional[datetime] = None, to_date: Optional[datetime] = None, limit: int = Query(365, ge=1, le=1000), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): q = select(HealthMetric).where(HealthMetric.user_id == current_user.id) if from_date: q = q.where(HealthMetric.date >= from_date) if to_date: q = q.where(HealthMetric.date <= to_date) q = q.order_by(desc(HealthMetric.date)).limit(limit) result = await db.execute(q) return result.scalars().all() @router.get("/summary") async def health_summary( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Latest values + 30-day averages for dashboard widgets.""" # Latest record latest_result = await db.execute( select(HealthMetric) .where(HealthMetric.user_id == current_user.id) .order_by(desc(HealthMetric.date)) .limit(1) ) latest = latest_result.scalar_one_or_none() # 30-day averages from datetime import timedelta, timezone cutoff = datetime.now(timezone.utc) - timedelta(days=30) avg_result = await db.execute( select( func.avg(HealthMetric.resting_hr).label("avg_resting_hr"), func.avg(HealthMetric.hrv_nightly_avg).label("avg_hrv"), func.avg(HealthMetric.sleep_duration_s).label("avg_sleep_s"), func.avg(HealthMetric.sleep_score).label("avg_sleep_score"), func.avg(HealthMetric.avg_stress).label("avg_stress"), func.avg(HealthMetric.steps).label("avg_steps"), func.avg(HealthMetric.weight_kg).label("avg_weight"), ).where( HealthMetric.user_id == current_user.id, HealthMetric.date >= cutoff, ) ) avgs = avg_result.one() return { "latest": HealthMetricOut.model_validate(latest) if latest else None, "avg_30d": { "resting_hr": avgs.avg_resting_hr, "hrv": avgs.avg_hrv, "sleep_h": (avgs.avg_sleep_s / 3600) if avgs.avg_sleep_s else None, "sleep_score": avgs.avg_sleep_score, "stress": avgs.avg_stress, "steps": avgs.avg_steps, "weight_kg": avgs.avg_weight, }, } @router.put("/manual") async def add_manual_metric( body: dict, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Manually add or update a health metric for a given date.""" from sqlalchemy.dialects.postgresql import insert as pg_insert date_str = body.get("date") if not date_str: from fastapi import HTTPException raise HTTPException(status_code=400, detail="date required") metric_date = datetime.fromisoformat(date_str) # Check for existing existing = await db.execute( select(HealthMetric).where( HealthMetric.user_id == current_user.id, func.date(HealthMetric.date) == metric_date.date(), ) ) metric = existing.scalar_one_or_none() if metric: for key, val in body.items(): if hasattr(metric, key) and key not in ("id", "user_id"): setattr(metric, key, val) else: metric = HealthMetric(user_id=current_user.id, date=metric_date, **{ k: v for k, v in body.items() if hasattr(HealthMetric, k) and k not in ("id", "user_id") }) db.add(metric) await db.commit() return {"status": "ok"}