from __future__ import annotations from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from sqlalchemy import func, select from sqlalchemy.orm import Session from ..config import settings from ..db import get_db from ..models import KnownDog, Picture, User from ..models.base import SubjectType from ..schemas.common import Page from ..schemas.dog import KnownDogCreate, KnownDogOut, KnownDogUpdate, PictureOut from ..security import get_current_user from ..services.images import ImageValidationError, process_and_store_picture from .helpers import estimated_breeds_for, hydrate_picture, pictures_for router = APIRouter(prefix="/dogs", tags=["dogs"]) def _get_owned_dog(db: Session, dog_id: int, user: User) -> KnownDog: dog = db.get(KnownDog, dog_id) if not dog: raise HTTPException(status.HTTP_404_NOT_FOUND, "Dog not found") if dog.owner_id != user.id and user.role.value != "admin": raise HTTPException(status.HTTP_403_FORBIDDEN, "Not your dog") return dog def _to_out(db: Session, dog: KnownDog) -> KnownDogOut: out = KnownDogOut.model_validate(dog) out.pictures = pictures_for(db, SubjectType.known, dog.id) out.estimated_breeds = estimated_breeds_for(db, SubjectType.known, dog.id, limit=8) return out @router.post("", response_model=KnownDogOut, status_code=status.HTTP_201_CREATED) def create_dog( payload: KnownDogCreate, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> KnownDogOut: dog = KnownDog(owner_id=user.id, **payload.model_dump()) db.add(dog) db.commit() db.refresh(dog) return _to_out(db, dog) @router.get("", response_model=Page[KnownDogOut]) def list_my_dogs( limit: int = 50, offset: int = 0, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> Page[KnownDogOut]: limit = max(1, min(limit, 100)) total = db.execute( select(func.count()).select_from(KnownDog).where(KnownDog.owner_id == user.id) ).scalar_one() dogs = db.execute( select(KnownDog) .where(KnownDog.owner_id == user.id) .order_by(KnownDog.created_at.desc()) .limit(limit) .offset(offset) ).scalars().all() return Page(items=[_to_out(db, d) for d in dogs], total=total, limit=limit, offset=offset) @router.get("/{dog_id}", response_model=KnownDogOut) def get_dog( dog_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> KnownDogOut: return _to_out(db, _get_owned_dog(db, dog_id, user)) @router.patch("/{dog_id}", response_model=KnownDogOut) def update_dog( dog_id: int, payload: KnownDogUpdate, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> KnownDogOut: dog = _get_owned_dog(db, dog_id, user) for field, value in payload.model_dump(exclude_unset=True).items(): setattr(dog, field, value) db.commit() db.refresh(dog) return _to_out(db, dog) @router.post("/{dog_id}/mark-home", response_model=KnownDogOut) def mark_dog_home( dog_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> KnownDogOut: """Set a dog's status back to 'home' — e.g. after a reunion, or to cancel a lost report.""" from ..models.base import KnownDogStatus dog = _get_owned_dog(db, dog_id, user) dog.status = KnownDogStatus.home db.commit() db.refresh(dog) return _to_out(db, dog) @router.post("/{dog_id}/photos", response_model=list[PictureOut], status_code=status.HTTP_201_CREATED) def upload_photos( dog_id: int, files: list[UploadFile] = File(...), user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> list[PictureOut]: dog = _get_owned_dog(db, dog_id, user) existing = db.execute( select(func.count()) .select_from(Picture) .where(Picture.subject_type == SubjectType.known, Picture.subject_id == dog.id) ).scalar_one() if existing + len(files) > settings.max_photos_per_dog: raise HTTPException( status.HTTP_400_BAD_REQUEST, f"Max {settings.max_photos_per_dog} photos per dog", ) out: list[PictureOut] = [] for i, f in enumerate(files): data = f.file.read() try: pic = process_and_store_picture( db, subject_type=SubjectType.known, subject_id=dog.id, data=data, is_primary=(existing == 0 and i == 0), ) except ImageValidationError as exc: raise HTTPException(status.HTTP_400_BAD_REQUEST, str(exc)) from exc out.append(hydrate_picture(pic)) # If this dog has an open lost case, re-run matching now that new photos exist (spec §8 step 6). from ..models import Case from ..models.base import CaseStatus, CaseType from ..services.matching import run_matching_for_case open_case = db.execute( select(Case) .where( Case.known_dog_id == dog.id, Case.type == CaseType.lost, Case.status.in_([CaseStatus.open, CaseStatus.matched]), ) .order_by(Case.created_at.desc()) ).scalars().first() if open_case: run_matching_for_case(db, open_case) db.commit() return out @router.delete("/{dog_id}/photos/{img_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_photo( dog_id: int, img_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> None: dog = _get_owned_dog(db, dog_id, user) pic = db.get(Picture, img_id) if not pic or pic.subject_type != SubjectType.known or pic.subject_id != dog.id: raise HTTPException(status.HTTP_404_NOT_FOUND, "Photo not found") from ..storage import get_storage storage = get_storage() storage.delete(pic.file_path) if pic.thumb_path: storage.delete(pic.thumb_path) db.delete(pic) db.commit()