from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel from typing import Optional from datetime import datetime from app.core.database import get_db from app.core.security import get_current_user from app.models.user import User, GarminConnectConfig router = APIRouter() class GarminConfigIn(BaseModel): email: str password: str # plaintext; encrypted before storage sync_enabled: bool = True sync_activities: bool = True sync_wellness: bool = True sync_lookback_days: int = 30 # days to look back on first sync; -1 = all-time class GarminConfigOut(BaseModel): email: str sync_enabled: bool sync_activities: bool sync_wellness: bool sync_lookback_days: int last_sync_at: Optional[datetime] last_sync_status: Optional[str] connected: bool class Config: from_attributes = True @router.get("/config", response_model=GarminConfigOut) async def get_config( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(GarminConnectConfig).where(GarminConnectConfig.user_id == current_user.id) ) cfg = result.scalar_one_or_none() if not cfg: return GarminConfigOut( email="", sync_enabled=False, sync_activities=True, sync_wellness=True, sync_lookback_days=30, last_sync_at=None, last_sync_status=None, connected=False, ) return GarminConfigOut( email=cfg.email, sync_enabled=cfg.sync_enabled, sync_activities=cfg.sync_activities, sync_wellness=cfg.sync_wellness, sync_lookback_days=cfg.sync_lookback_days if cfg.sync_lookback_days is not None else 30, last_sync_at=cfg.last_sync_at, last_sync_status=cfg.last_sync_status, connected=True, ) @router.put("/config", response_model=GarminConfigOut) async def save_config( body: GarminConfigIn, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Save (or replace) Garmin Connect credentials. Attempts a test login first so we can store the initial OAuth token and fail fast on wrong credentials. """ from app.services.garmin_connect_sync import encrypt_password, authenticate_garmin enc = encrypt_password(body.password) # Test login — raises HTTPException on auth failure try: garmin, token_store = authenticate_garmin(body.email, enc, None) except Exception as exc: raise HTTPException(status_code=400, detail=f"Garmin login failed: {exc}") result = await db.execute( select(GarminConnectConfig).where(GarminConnectConfig.user_id == current_user.id) ) cfg = result.scalar_one_or_none() if cfg: cfg.email = body.email cfg.password_enc = enc cfg.token_store = token_store cfg.sync_enabled = body.sync_enabled cfg.sync_activities = body.sync_activities cfg.sync_wellness = body.sync_wellness cfg.sync_lookback_days = body.sync_lookback_days cfg.last_sync_status = "Credentials updated" else: cfg = GarminConnectConfig( user_id=current_user.id, email=body.email, password_enc=enc, token_store=token_store, sync_enabled=body.sync_enabled, sync_activities=body.sync_activities, sync_wellness=body.sync_wellness, sync_lookback_days=body.sync_lookback_days, last_sync_status="Connected", ) db.add(cfg) await db.commit() await db.refresh(cfg) return GarminConfigOut( email=cfg.email, sync_enabled=cfg.sync_enabled, sync_activities=cfg.sync_activities, sync_wellness=cfg.sync_wellness, sync_lookback_days=cfg.sync_lookback_days if cfg.sync_lookback_days is not None else 30, last_sync_at=cfg.last_sync_at, last_sync_status=cfg.last_sync_status, connected=True, ) @router.delete("/config") async def delete_config( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): result = await db.execute( select(GarminConnectConfig).where(GarminConnectConfig.user_id == current_user.id) ) cfg = result.scalar_one_or_none() if cfg: await db.delete(cfg) await db.commit() return {"status": "ok"} @router.post("/trigger") async def trigger_sync( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Enqueue an immediate Garmin Connect sync for this user.""" result = await db.execute( select(GarminConnectConfig).where(GarminConnectConfig.user_id == current_user.id) ) cfg = result.scalar_one_or_none() if not cfg or not cfg.sync_enabled: raise HTTPException(status_code=400, detail="Garmin Connect sync is not configured or disabled") from app.workers.tasks import sync_garmin_connect_user task = sync_garmin_connect_user.delay(current_user.id) return {"task_id": task.id, "status": "queued"}