from datetime import datetime, timezone import json from fastapi import APIRouter, Header, HTTPException, Depends from pydantic import BaseModel from sqlalchemy import select from app.config import AGENT_API_KEY from app.models.base import async_session from app.models.user import User from app.models.workout import Phase, Workout, WorkoutSet, WorkoutSnapshot from app.models.checkin import Checkin router = APIRouter(prefix="/api/agent", tags=["agent"]) async def verify_agent(x_api_key: str = Header("")): if AGENT_API_KEY and x_api_key != AGENT_API_KEY: raise HTTPException(status_code=403, detail="invalid api key") async def snapshot_sets(workout_id: int, changed_by: str, reason: str): async with async_session() as session: result = await session.execute( select(WorkoutSet) .where(WorkoutSet.workout_id == workout_id) .order_by(WorkoutSet.exercise, WorkoutSet.set_number) ) current_sets = result.scalars().all() if not current_sets: return snapshot_data = [ { "id": s.id, "exercise": s.exercise, "set_number": s.set_number, "reps": s.reps, "weight": s.weight, "rpe": s.rpe, "notes": s.notes, } for s in current_sets ] snap = WorkoutSnapshot( workout_id=workout_id, changed_by=changed_by, reason=reason, sets_snapshot=json.dumps(snapshot_data), created_at=datetime.now(timezone.utc).isoformat(), ) session.add(snap) await session.commit() class CreatePhaseRequest(BaseModel): name: str description: str = "" start_date: str | None = None end_date: str | None = None notes: str = "" class CreateWorkoutRequest(BaseModel): username: str name: str date: str phase_id: int | None = None notes: str = "" class CreateSetRequest(BaseModel): exercise: str set_number: int reps: int | None = None weight: float | None = None rpe: float | None = None notes: str = "" reason: str = "" class UpdateSetRequest(BaseModel): exercise: str | None = None set_number: int | None = None reps: int | None = None weight: float | None = None rpe: float | None = None notes: str | None = None reason: str = "" class CreateCheckinRequest(BaseModel): username: str date: str feeling: str | None = None weight_lb: float | None = None calories: int | None = None steps: int | None = None sleep_hours: float | None = None notes: str = "" @router.post("/workouts") async def agent_create_workout( body: CreateWorkoutRequest, _=Depends(verify_agent), ): async with async_session() as session: result = await session.execute( select(User).where(User.username == body.username) ) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="user not found") workout = Workout( user_id=user.id, phase_id=body.phase_id, name=body.name, date=body.date, notes=body.notes, status="plan", ) session.add(workout) await session.commit() await session.refresh(workout) return {"id": workout.id, "name": workout.name, "date": workout.date, "status": workout.status} @router.post("/workouts/{workout_id}/sets") async def agent_add_set( workout_id: int, body: CreateSetRequest, _=Depends(verify_agent), ): if body.reason: await snapshot_sets(workout_id, "agent", body.reason) async with async_session() as session: ws = WorkoutSet( workout_id=workout_id, exercise=body.exercise, set_number=body.set_number, reps=body.reps, weight=body.weight, rpe=body.rpe, notes=body.notes, ) session.add(ws) await session.commit() await session.refresh(ws) return {"id": ws.id, "exercise": ws.exercise, "set_number": ws.set_number} @router.put("/workouts/{workout_id}/sets/{set_id}") async def agent_update_set( workout_id: int, set_id: int, body: UpdateSetRequest, _=Depends(verify_agent), ): if body.reason: await snapshot_sets(workout_id, "agent", body.reason) async with async_session() as session: result = await session.execute( select(WorkoutSet).where( WorkoutSet.id == set_id, WorkoutSet.workout_id == workout_id, ) ) ws = result.scalar_one_or_none() if not ws: raise HTTPException(status_code=404, detail="set not found") if body.exercise is not None: ws.exercise = body.exercise if body.set_number is not None: ws.set_number = body.set_number if body.reps is not None: ws.reps = body.reps if body.weight is not None: ws.weight = body.weight if body.rpe is not None: ws.rpe = body.rpe if body.notes is not None: ws.notes = body.notes await session.commit() return {"id": ws.id, "exercise": ws.exercise, "set_number": ws.set_number} @router.delete("/workouts/{workout_id}/sets/{set_id}") async def agent_delete_set( workout_id: int, set_id: int, reason: str = "", _=Depends(verify_agent), ): if reason: await snapshot_sets(workout_id, "agent", reason) async with async_session() as session: result = await session.execute( select(WorkoutSet).where( WorkoutSet.id == set_id, WorkoutSet.workout_id == workout_id, ) ) ws = result.scalar_one_or_none() if not ws: raise HTTPException(status_code=404, detail="set not found") await session.delete(ws) await session.commit() return {"deleted": set_id} @router.put("/workouts/{workout_id}/complete") async def agent_complete_workout( workout_id: int, _=Depends(verify_agent), ): async with async_session() as session: result = await session.execute( select(Workout).where(Workout.id == workout_id) ) workout = result.scalar_one_or_none() if not workout: raise HTTPException(status_code=404, detail="workout not found") workout.status = "complete" await session.commit() return {"id": workout.id, "status": "complete"} @router.post("/checkins") async def agent_create_checkin( body: CreateCheckinRequest, _=Depends(verify_agent), ): async with async_session() as session: result = await session.execute( select(User).where(User.username == body.username) ) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="user not found") checkin = Checkin( user_id=user.id, date=body.date, feeling=body.feeling, weight_lb=body.weight_lb, calories=body.calories, steps=body.steps, sleep_hours=body.sleep_hours, notes=body.notes, ) session.add(checkin) await session.commit() await session.refresh(checkin) return {"id": checkin.id, "date": checkin.date} @router.get("/phases") async def agent_list_phases(_=Depends(verify_agent)): async with async_session() as session: result = await session.execute(select(Phase).order_by(Phase.start_date.nulls_last())) phases = result.scalars().all() return [ {"id": p.id, "name": p.name, "description": p.description, "start_date": p.start_date, "end_date": p.end_date, "notes": p.notes} for p in phases ] @router.post("/phases") async def agent_create_phase( body: CreatePhaseRequest, _=Depends(verify_agent), ): async with async_session() as session: phase = Phase( name=body.name, description=body.description, start_date=body.start_date, end_date=body.end_date, notes=body.notes, ) session.add(phase) await session.commit() await session.refresh(phase) return {"id": phase.id, "name": phase.name} @router.put("/phases/{phase_id}") async def agent_update_phase( phase_id: int, body: CreatePhaseRequest, _=Depends(verify_agent), ): async with async_session() as session: result = await session.execute(select(Phase).where(Phase.id == phase_id)) phase = result.scalar_one_or_none() if not phase: raise HTTPException(status_code=404, detail="phase not found") phase.name = body.name phase.description = body.description phase.start_date = body.start_date phase.end_date = body.end_date phase.notes = body.notes await session.commit() return {"id": phase.id, "name": phase.name}