import logging from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from api.auth import User, get_current_user from database import engine from models.listing import ListingType from repositories.poi_repository import POIRepository from repositories.user_repository import UserRepository from services import poi_service, task_service logger = logging.getLogger("uvicorn") poi_router = APIRouter(prefix="/api/poi", tags=["poi"]) class CreatePOIRequest(BaseModel): name: str = Field(max_length=200) address: str = Field(max_length=500) latitude: float = Field(ge=-90, le=90) longitude: float = Field(ge=-180, le=180) class UpdatePOIRequest(BaseModel): name: str | None = Field(default=None, max_length=200) address: str | None = Field(default=None, max_length=500) latitude: float | None = Field(default=None, ge=-90, le=90) longitude: float | None = Field(default=None, ge=-180, le=180) class POIResponse(BaseModel): id: int name: str address: str latitude: float longitude: float created_at: str class CalculateRequest(BaseModel): travel_modes: list[str] # WALK, BICYCLE, TRANSIT listing_type: ListingType = ListingType.RENT listing_ids: list[int] | None = None class POIDistanceResponse(BaseModel): poi_id: int poi_name: str travel_mode: str duration_seconds: int distance_meters: int def _get_user_id(user: User) -> int: """Resolve auth User to database user ID.""" user_repo = UserRepository(engine) db_user = user_repo.get_user_by_email(user.email) if db_user is None: # Auto-create user on first POI interaction db_user = user_repo.create_user(user.email) if db_user.id is None: raise HTTPException(status_code=500, detail="Failed to create user") return db_user.id def _poi_to_response(poi: "poi_service.PointOfInterest") -> POIResponse: return POIResponse( id=poi.id, # type: ignore[arg-type] name=poi.name, address=poi.address, latitude=poi.latitude, longitude=poi.longitude, created_at=poi.created_at.isoformat(), ) @poi_router.get("", response_model=list[POIResponse]) async def list_pois( user: Annotated[User, Depends(get_current_user)], ) -> list[POIResponse]: """List all POIs for the current user.""" user_id = _get_user_id(user) repo = POIRepository(engine) pois = poi_service.get_user_pois(repo, user_id) return [_poi_to_response(p) for p in pois] @poi_router.post("", response_model=POIResponse) async def create_poi( user: Annotated[User, Depends(get_current_user)], body: CreatePOIRequest, ) -> POIResponse: """Create a new POI.""" user_id = _get_user_id(user) repo = POIRepository(engine) result = poi_service.create_poi( repo, user_id=user_id, name=body.name, address=body.address, latitude=body.latitude, longitude=body.longitude, ) return _poi_to_response(result.poi) @poi_router.put("/{poi_id}", response_model=POIResponse) async def update_poi( user: Annotated[User, Depends(get_current_user)], poi_id: int, body: UpdatePOIRequest, ) -> POIResponse: """Update an existing POI.""" user_id = _get_user_id(user) repo = POIRepository(engine) result = poi_service.update_poi( repo, poi_id=poi_id, user_id=user_id, name=body.name, address=body.address, latitude=body.latitude, longitude=body.longitude, ) if result is None: raise HTTPException(status_code=404, detail="POI not found") return _poi_to_response(result.poi) @poi_router.delete("/{poi_id}") async def delete_poi( user: Annotated[User, Depends(get_current_user)], poi_id: int, ) -> dict[str, bool]: """Delete a POI and its associated distances.""" user_id = _get_user_id(user) repo = POIRepository(engine) deleted = poi_service.delete_poi(repo, poi_id, user_id) if not deleted: raise HTTPException(status_code=404, detail="POI not found") return {"success": True} @poi_router.post("/{poi_id}/calculate") async def calculate_distances( user: Annotated[User, Depends(get_current_user)], poi_id: int, body: CalculateRequest, ) -> dict[str, str]: """Trigger distance calculation for a POI.""" user_id = _get_user_id(user) repo = POIRepository(engine) # Verify POI exists and belongs to user poi = poi_service.get_poi(repo, poi_id) if poi is None or poi.user_id != user_id: raise HTTPException(status_code=404, detail="POI not found") result = poi_service.trigger_calculation( poi_id=poi_id, travel_modes=body.travel_modes, listing_type=body.listing_type, user_email=user.email, listing_ids=body.listing_ids, ) if result.task_id: task_service.add_task_for_user(user.email, result.task_id) return {"task_id": result.task_id or "", "message": result.message} @poi_router.get("/distances") async def get_distances( user: Annotated[User, Depends(get_current_user)], listing_id: int, listing_type: ListingType = ListingType.RENT, ) -> list[POIDistanceResponse]: """Get POI distances for a specific listing.""" user_id = _get_user_id(user) repo = POIRepository(engine) poi_repo_pois = { p.id: p for p in poi_service.get_user_pois(repo, user_id) } distances = poi_service.get_distances_for_listing( repo, listing_id, listing_type, user_id ) return [ POIDistanceResponse( poi_id=d.poi_id, poi_name=poi_repo_pois[d.poi_id].name if d.poi_id in poi_repo_pois else "Unknown", travel_mode=d.travel_mode, duration_seconds=d.duration_seconds, distance_meters=d.distance_meters, ) for d in distances ]