from uuid import UUID from fastapi import UploadFile from sqlalchemy.ext.asyncio import AsyncSession from Backend.core.events import event_bus, IssueCreated from Backend.core.logging import get_logger from Backend.core.schemas import IssueCreate, IssueState from Backend.database.models import Issue, IssueImage from Backend.services.geocoding import geocoding_service from Backend.utils.storage import save_upload, get_upload_url, validate_file_extension, validate_file_size logger = get_logger(__name__) class IngestionService: def __init__(self, db: AsyncSession): self.db = db async def create_issue( self, data: IssueCreate, images: list[UploadFile], user_id: str | None = None ) -> tuple[Issue, list[str]]: if not images: raise ValueError("At least one image is required") for image in images: if not validate_file_extension(image.filename or ""): raise ValueError(f"Invalid file extension: {image.filename}") location_info = await geocoding_service.reverse_geocode( data.latitude, data.longitude ) logger.info(f"Location resolved: {location_info.city}, {location_info.locality}") final_description = data.description or "Issue reported" issue = Issue( user_id=user_id, description=final_description, latitude=data.latitude, longitude=data.longitude, accuracy_meters=data.accuracy_meters, platform=data.platform, device_model=data.device_model, state=IssueState.REPORTED, city=location_info.city, locality=location_info.locality, full_address=location_info.full_address, ) self.db.add(issue) await self.db.flush() image_paths = [] for image in images: file_path = await save_upload(image, subfolder=str(issue.id)) issue_image = IssueImage( issue_id=issue.id, file_path=file_path, original_filename=image.filename, ) self.db.add(issue_image) image_paths.append(file_path) await self.db.flush() event = IssueCreated( issue_id=issue.id, image_paths=image_paths, latitude=issue.latitude, longitude=issue.longitude, description=issue.description, ) await event_bus.publish(event) logger.info(f"Issue created: {issue.id} in {issue.city}") return issue, image_paths async def get_issue(self, issue_id: UUID) -> Issue | None: return await self.db.get(Issue, issue_id)