from pathlib import Path from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Response, status from sqlalchemy import desc, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import AsyncSessionLocal, get_db from app.core.security import get_current_user from app.models.palm_report import PalmReport from app.models.share_image_job import ShareImageJob from app.models.uploaded_image import UploadedImage from app.models.user import User from app.schemas.share_image import ShareImageJobResponse from app.schemas.report import ReportCreate, ReportDetail, ReportSummary from app.services.image_service import ImageService from app.services.report_service import ReportService from app.services.share_poster_service import SharePosterService router = APIRouter() async def generate_report_task(report_id: str) -> None: async with AsyncSessionLocal() as session: await ReportService().generate(session, report_id) async def generate_share_image_task(job_id: str) -> None: async with AsyncSessionLocal() as session: result = await session.execute(select(ShareImageJob).where(ShareImageJob.id == job_id)) job = result.scalar_one() job.status = "processing" await session.flush() try: report_result = await session.execute(select(PalmReport).where(PalmReport.id == job.report_id)) report = report_result.scalar_one() image_result = await session.execute(select(UploadedImage).where(UploadedImage.id == report.image_id)) image = image_result.scalar_one_or_none() png = await SharePosterService().render_ai_or_fallback(report, image) share_dir = Path("storage/share_images") share_dir.mkdir(parents=True, exist_ok=True) storage_key = f"{job.id}.png" (share_dir / storage_key).write_bytes(png) job.storage_key = storage_key job.status = "completed" job.error_message = None except Exception as exc: job.status = "failed" job.error_message = str(exc) await session.commit() @router.post("", response_model=ReportDetail, status_code=status.HTTP_201_CREATED) async def create_report( payload: ReportCreate, background_tasks: BackgroundTasks, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): image_result = await db.execute( select(UploadedImage).where(UploadedImage.id == payload.image_id, UploadedImage.user_id == user.id) ) image = image_result.scalar_one_or_none() if image is None: raise HTTPException(status_code=404, detail="Image not found") report = PalmReport(user_id=user.id, image_id=image.id, hand_side=payload.hand_side, status="pending") db.add(report) await db.flush() await db.refresh(report) background_tasks.add_task(generate_report_task, report.id) return report @router.get("/share-image-jobs/{job_id}", response_model=ShareImageJobResponse) async def get_share_image_job( job_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): job = await _get_owned_share_job(db, job_id, user.id) return job @router.get("/share-image-jobs/{job_id}/image") async def get_share_image_job_image( job_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): job = await _get_owned_share_job(db, job_id, user.id) if job.status != "completed" or not job.storage_key: raise HTTPException(status_code=400, detail="Share image is not ready") path = Path("storage/share_images") / job.storage_key if not path.exists(): raise HTTPException(status_code=404, detail="Share image file not found") return Response(content=path.read_bytes(), media_type="image/png") @router.get("", response_model=list[ReportSummary]) async def list_reports(user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): result = await db.execute( select(PalmReport).where(PalmReport.user_id == user.id).order_by(desc(PalmReport.created_at)).limit(50) ) reports = result.scalars().all() return [ ReportSummary( id=report.id, status=report.status, hand_side=report.hand_side, created_at=report.created_at, overall_summary=(report.report_data or {}).get("overall_summary") if report.report_data else None, ) for report in reports ] @router.get("/{report_id}", response_model=ReportDetail) async def get_report(report_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): report = await _get_owned_report(db, report_id, user.id) return report @router.post("/{report_id}/share-image-jobs", response_model=ShareImageJobResponse, status_code=status.HTTP_201_CREATED) async def create_share_image_job( report_id: str, background_tasks: BackgroundTasks, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): report = await _get_owned_report(db, report_id, user.id) if report.status != "completed" or not report.report_data: raise HTTPException(status_code=400, detail="Report is not ready") job = ShareImageJob(user_id=user.id, report_id=report.id, status="pending") db.add(job) await db.flush() await db.refresh(job) background_tasks.add_task(generate_share_image_task, job.id) return job @router.get("/{report_id}/share-image") async def get_report_share_image( report_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): report = await _get_owned_report(db, report_id, user.id) if report.status != "completed" or not report.report_data: raise HTTPException(status_code=400, detail="Report is not ready") image_result = await db.execute(select(UploadedImage).where(UploadedImage.id == report.image_id)) image = image_result.scalar_one_or_none() png = await SharePosterService().render_ai_or_fallback(report, image) return Response( content=png, media_type="image/png", headers={"Content-Disposition": f'inline; filename="palm-report-{report.id}.png"'}, ) @router.delete("/{report_id}") async def delete_report(report_id: str, user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): report = await _get_owned_report(db, report_id, user.id) image_result = await db.execute(select(UploadedImage).where(UploadedImage.id == report.image_id)) image = image_result.scalar_one_or_none() await db.delete(report) await db.flush() if image: ImageService().delete(image.storage_key) await db.delete(image) return {"status": "deleted"} async def _get_owned_report(db: AsyncSession, report_id: str, user_id: str) -> PalmReport: result = await db.execute(select(PalmReport).where(PalmReport.id == report_id, PalmReport.user_id == user_id)) report = result.scalar_one_or_none() if report is None: raise HTTPException(status_code=404, detail="Report not found") return report async def _get_owned_share_job(db: AsyncSession, job_id: str, user_id: str) -> ShareImageJob: result = await db.execute(select(ShareImageJob).where(ShareImageJob.id == job_id, ShareImageJob.user_id == user_id)) job = result.scalar_one_or_none() if job is None: raise HTTPException(status_code=404, detail="Share image job not found") return job