from fastapi import APIRouter, Depends, HTTPException, 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, NamedRoute, Activity router = APIRouter() class RouteCreate(BaseModel): name: str description: Optional[str] = None sport_type: Optional[str] = None activity_id: int class RouteUpdate(BaseModel): name: Optional[str] = None sport_type: Optional[str] = None class RouteOut(BaseModel): id: int name: str description: Optional[str] sport_type: Optional[str] reference_polyline: Optional[str] bounding_box: Optional[dict] distance_m: Optional[float] auto_detected: Optional[bool] created_at: datetime activity_count: int = 0 class Config: from_attributes = True @router.get("/", response_model=List[RouteOut]) async def list_routes( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): # Fetch routes with activity counts in one query count_subq = ( select(Activity.named_route_id, func.count(Activity.id).label("cnt")) .where(Activity.user_id == current_user.id, Activity.named_route_id.isnot(None)) .group_by(Activity.named_route_id) .subquery() ) result = await db.execute( select(NamedRoute, func.coalesce(count_subq.c.cnt, 0).label("activity_count")) .outerjoin(count_subq, NamedRoute.id == count_subq.c.named_route_id) .where(NamedRoute.user_id == current_user.id) .order_by(desc(NamedRoute.created_at)) ) rows = result.all() out = [] for route, cnt in rows: d = {c.name: getattr(route, c.name) for c in route.__table__.columns} d["activity_count"] = cnt out.append(RouteOut(**d)) return out @router.get("/recent-activities") async def recent_activities_for_route( days: int = Query(14, ge=1, le=90), sport_type: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Return recent activities for the route creation dropdown.""" cutoff = datetime.now(timezone.utc) - timedelta(days=days) q = select(Activity).where( Activity.user_id == current_user.id, Activity.start_time >= cutoff, Activity.sport_type != "swimming", ) if sport_type: q = q.where(Activity.sport_type == sport_type) q = q.order_by(desc(Activity.start_time)).limit(50) result = await db.execute(q) activities = result.scalars().all() return [ { "id": a.id, "name": a.name, "sport_type": a.sport_type, "start_time": a.start_time, "distance_m": a.distance_m, "duration_s": a.duration_s, } for a in activities ] @router.post("/", response_model=RouteOut) async def create_route( body: RouteCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): act_result = await db.execute( select(Activity).where( Activity.id == body.activity_id, Activity.user_id == current_user.id, ) ) activity = act_result.scalar_one_or_none() if not activity: raise HTTPException(status_code=404, detail="Activity not found") route = NamedRoute( user_id=current_user.id, name=body.name, description=body.description, sport_type=body.sport_type or activity.sport_type, reference_polyline=activity.polyline, bounding_box=activity.bounding_box, distance_m=activity.distance_m, auto_detected=False, ) db.add(route) await db.flush() activity.named_route_id = route.id await db.commit() await db.refresh(route) return route @router.get("/{route_id}", response_model=RouteOut) async def get_route( route_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(NamedRoute).where( NamedRoute.id == route_id, NamedRoute.user_id == current_user.id, ) ) route = result.scalar_one_or_none() if not route: raise HTTPException(status_code=404, detail="Route not found") return route @router.patch("/{route_id}", response_model=RouteOut) async def update_route( route_id: int, body: RouteUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(NamedRoute).where( NamedRoute.id == route_id, NamedRoute.user_id == current_user.id, ) ) route = result.scalar_one_or_none() if not route: raise HTTPException(status_code=404, detail="Route not found") if body.name is not None and body.name.strip(): route.name = body.name.strip() if body.sport_type is not None: route.sport_type = body.sport_type await db.commit() await db.refresh(route) return route @router.get("/{route_id}/activities") async def route_activities( route_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(Activity).where( Activity.named_route_id == route_id, Activity.user_id == current_user.id, ).order_by(Activity.duration_s) ) activities = result.scalars().all() return [ { "id": a.id, "name": a.name, "start_time": a.start_time, "duration_s": a.duration_s, "distance_m": a.distance_m, "avg_heart_rate": a.avg_heart_rate, "avg_speed_ms": a.avg_speed_ms, } for a in activities ] @router.post("/{route_id}/merge/{source_id}", response_model=RouteOut) async def merge_routes( route_id: int, source_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Move all activities from source route into route_id, then delete source route.""" from sqlalchemy import update target = (await db.execute( select(NamedRoute).where(NamedRoute.id == route_id, NamedRoute.user_id == current_user.id) )).scalar_one_or_none() source = (await db.execute( select(NamedRoute).where(NamedRoute.id == source_id, NamedRoute.user_id == current_user.id) )).scalar_one_or_none() if not target or not source: raise HTTPException(status_code=404, detail="Route not found") if route_id == source_id: raise HTTPException(status_code=400, detail="Cannot merge a route with itself") await db.execute( update(Activity) .where(Activity.named_route_id == source_id, Activity.user_id == current_user.id) .values(named_route_id=route_id) ) await db.delete(source) await db.commit() await db.refresh(target) return target @router.delete("/{route_id}") async def delete_route( route_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): from sqlalchemy import update as sa_update route = (await db.execute( select(NamedRoute).where(NamedRoute.id == route_id, NamedRoute.user_id == current_user.id) )).scalar_one_or_none() if not route: raise HTTPException(status_code=404, detail="Route not found") # Unlink activities before deleting await db.execute( sa_update(Activity) .where(Activity.named_route_id == route_id, Activity.user_id == current_user.id) .values(named_route_id=None) ) await db.delete(route) await db.commit() return {"status": "ok"} @router.post("/{route_id}/assign-activity") async def assign_activity_to_route( route_id: int, body: dict, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): activity_id = body.get("activity_id") act_result = await db.execute( select(Activity).where( Activity.id == activity_id, Activity.user_id == current_user.id, ) ) activity = act_result.scalar_one_or_none() if not activity: raise HTTPException(status_code=404, detail="Activity not found") activity.named_route_id = route_id await db.commit() return {"status": "ok"}