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, timedelta, timezone 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: from_date_naive = from_date.replace(tzinfo=None) if from_date.tzinfo else from_date q = q.where(func.date(HealthMetric.date) >= from_date_naive.date()) if to_date: to_date_naive = to_date.replace(tzinfo=None) if to_date.tzinfo else to_date q = q.where(func.date(HealthMetric.date) <= to_date_naive.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_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() cutoff = (datetime.now(timezone.utc) - timedelta(days=30)).date() 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, func.date(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), ): from fastapi import HTTPException date_str = body.get("date") if not date_str: raise HTTPException(status_code=400, detail="date required") metric_date = datetime.fromisoformat(date_str) 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"}