import logging import time from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, Response from pydantic import BaseModel, Field from api.auth import User, get_current_user from database import engine from repositories.decision_repository import DecisionRepository from repositories.user_repository import UserRepository from services import decision_service logger = logging.getLogger("uvicorn") decision_router = APIRouter(prefix="/api/decisions", tags=["decisions"]) class SetDecisionRequest(BaseModel): decision: str = Field(description="'liked' or 'disliked'") listing_type: str = Field(description="'RENT' or 'BUY'") class DecisionResponse(BaseModel): listing_id: int listing_type: str decision: str created_at: str updated_at: str 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 decision 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 _to_response(d: decision_service.ListingDecision) -> DecisionResponse: return DecisionResponse( listing_id=d.listing_id, listing_type=d.listing_type, decision=d.decision, created_at=d.created_at.isoformat(), updated_at=d.updated_at.isoformat(), ) @decision_router.put("/{listing_id}", response_model=DecisionResponse) async def set_decision( user: Annotated[User, Depends(get_current_user)], listing_id: int, body: SetDecisionRequest, response: Response, ) -> DecisionResponse: """Set or update a like/dislike decision for a listing.""" timings: list[str] = [] t0_total = time.monotonic() t0 = time.monotonic() user_id = _get_user_id(user) timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}") repo = DecisionRepository(engine) try: t0 = time.monotonic() result = decision_service.set_decision( repo, user_id=user_id, listing_id=listing_id, listing_type=body.listing_type, decision=body.decision, ) timings.append(f"upsert;dur={(time.monotonic() - t0) * 1000:.1f}") except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}") response.headers["Server-Timing"] = ", ".join(timings) return _to_response(result) @decision_router.get("", response_model=list[DecisionResponse]) async def get_decisions( user: Annotated[User, Depends(get_current_user)], response: Response, ) -> list[DecisionResponse]: """Get all decisions for the current user.""" timings: list[str] = [] t0_total = time.monotonic() t0 = time.monotonic() user_id = _get_user_id(user) timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}") repo = DecisionRepository(engine) t0 = time.monotonic() decisions = decision_service.get_user_decisions(repo, user_id) timings.append(f"fetch;dur={(time.monotonic() - t0) * 1000:.1f}") timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}") response.headers["Server-Timing"] = ", ".join(timings) return [_to_response(d) for d in decisions] @decision_router.delete("/{listing_id}") async def delete_decision( user: Annotated[User, Depends(get_current_user)], listing_id: int, response: Response, listing_type: str = Query(..., description="RENT or BUY"), ) -> dict[str, bool]: """Remove a decision (un-like/un-dislike).""" timings: list[str] = [] t0_total = time.monotonic() t0 = time.monotonic() user_id = _get_user_id(user) timings.append(f"user_lookup;dur={(time.monotonic() - t0) * 1000:.1f}") repo = DecisionRepository(engine) try: t0 = time.monotonic() deleted = decision_service.remove_decision( repo, user_id, listing_id, listing_type ) timings.append(f"delete;dur={(time.monotonic() - t0) * 1000:.1f}") except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) if not deleted: raise HTTPException(status_code=404, detail="Decision not found") timings.append(f"total;dur={(time.monotonic() - t0_total) * 1000:.1f}") response.headers["Server-Timing"] = ", ".join(timings) return {"success": True}