from fastapi import APIRouter,Depends, Query from fastapi.responses import JSONResponse from fastapi import HTTPException from pydantic import BaseModel from db_setup import get_session from inbox.enums import Candidate_application_Status from sqlalchemy.ext.asyncio import AsyncSession from inbox.views import Email from users.permissions import PermissionTag, require_permission from dotenv import load_dotenv load_dotenv() router = APIRouter() class AssignJobPostBody(BaseModel): job_post_id: str | None = None class ProcessingStateBody(BaseModel): processing_state: str class DuplicateBody(BaseModel): is_duplicate: bool class ReadBody(BaseModel): read: bool = True class BulkReadBody(BaseModel): record_ids: list[str] read: bool = True class ReadAllBody(BaseModel): """The caller's CURRENT list filter, echoed back so the update narrows the same way. Every field defaults to the same "no filter" value the list endpoint uses, so an empty body means "the All Applications tab" — exactly what GET /inbox/all-applications returns with no query params. """ read: bool = True search: str | None = None isread: bool = True application_status: Candidate_application_Status = Candidate_application_Status.CLOSED assigned: bool | None = None class TriageOverrideBody(BaseModel): is_application: bool class EmailSendBody(BaseModel): to: str subject: str body: str content_type: str | None = "html" inbox_id: int | None = None class EmailReplyBody(BaseModel): record_id: str body: str @router.get("/email/fetch") async def fetch_email( top:int=Query(100), skip:int=Query(0,ge=0), test_on: bool = Query(True), token: str | None = Query(None), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session,token=token) if not service.token: raise HTTPException(status_code=401,detail="Unauthorized") data=await service.service_email(top,skip) value=data.get("value") items_lst=[] # Classify the whole page first, bounded-parallel, then replay it in upstream # order: the inserts stay serial on the one request session and pending_match_ids # keeps the sequence it has today. decisions=await service.triage_round([item.get("id") for item in value]) for item in value: message_id=item.get("id") service_per_email=await service.get_email_by_id(message_id,test_on,decision=decisions.get(str(message_id))) items_lst.append({"message_id":message_id,"email_contents":service_per_email}) if service.pending_match_ids: await service.enqueue_matching(list(service.pending_match_ids),force=False) skipped=len(service.skipped_message_ids) triage={"ingested":len(items_lst)-skipped,"skipped":skipped,"errors":len(service.triage_errors)} account_setup=[] if test_on: return JSONResponse(content={"data":items_lst,"triage":triage,"status_code":200}) if service.pending_confirmation_emails: account_setup=await service.send_account_setup(list(service.pending_confirmation_emails)) return JSONResponse(content={"data":items_lst,"account_setup":account_setup,"triage":triage,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/inbox/fetch") async def fetch_inbox( record_id: str | None = Query(None), search: str | None = Query(None), top: int | None = Query(None), skip: int = Query(0, ge=0), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) if record_id: item=await service.get_inbox_message_by_id(record_id) return JSONResponse(content={"data":item,"total":1,"status_code":200}) items=await service.get_inbox_messages(top,skip,search) total=await service.count_inbox_messages(search) return JSONResponse(content={"data":items,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/inbox/{record_id}/match") async def rematch_inbox( record_id: str, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) queued_id=await service.queue_rematch(record_id) task_ids=await service.enqueue_matching([queued_id], force=True) return JSONResponse(content={"data":{"queued":True,"task_ids":task_ids},"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/inbox/{record_id}/assign-job-post") async def assign_job_post( record_id: str, payload: AssignJobPostBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.assign_job_post(record_id,payload.job_post_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/inbox/{record_id}/read") async def mark_inbox_read( record_id: str, payload: ReadBody | None = None, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): """Flip one row. The body is OPTIONAL and defaults to read=true, so the original bodyless POST this route shipped with keeps working unchanged.""" try: service=Email(session=session) data=await service.mark_read(record_id,payload.read if payload else True) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/inbox/read") async def bulk_mark_inbox_read( payload: BulkReadBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): """Selected rows -> read/unread. Single segment after /inbox, so it never collides with the two-segment /inbox/{record_id}/read above.""" try: service=Email(session=session) data=await service.set_read_bulk(payload.record_ids,payload.read) return JSONResponse(content={"data":data,"total":data["updated"],"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/inbox/read-all") async def mark_all_inbox_read( payload: ReadAllBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): """Every row matching the caller's current list filter -> read/unread.""" try: service=Email(session=session) data=await service.set_read_all( payload.read, search=payload.search, isread=payload.isread, application_status=payload.application_status, assigned=payload.assigned, ) return JSONResponse(content={"data":data,"total":data["updated"],"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/inbox/{record_id}/read-status") async def get_inbox_read_status( record_id: str, token: str | None = Query(None), current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session,token=token) data=await service.refresh_read_status(record_id) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/inbox/all-applications") async def get_all_applications( record_id: str | None = Query(None), application_status: Candidate_application_Status = Query(default=Candidate_application_Status.CLOSED), isread: bool = Query(default=True), assigned: bool | None = Query(default=None), search: str | None = Query(None), top: int | None = Query(None), skip: int = Query(0, ge=0), current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) if application_status in (Candidate_application_Status.PROCESS, Candidate_application_Status.REJECTED, Candidate_application_Status.SCREENING, Candidate_application_Status.ASSESSMENT, Candidate_application_Status.INTERVIEW, Candidate_application_Status.OFFER, Candidate_application_Status.HIRED): items=await service.get_all_applications(top, skip, search, application_status=application_status, assigned=assigned) total=await service.count_inbox_messages(search, application_status=application_status, assigned=assigned) return JSONResponse(content={"data":items,"total":total,"status_code":200}) if isread==False: items=await service.get_all_applications(top, skip, search, isread=False, assigned=assigned) total=await service.count_inbox_messages(search, isread=False, assigned=assigned) return JSONResponse(content={"data":items,"total":total,"status_code":200}) if record_id: item=await service.get_application_by_id(record_id) return JSONResponse(content={"data":item,"total":1,"status_code":200}) items=await service.get_all_applications(top,skip,search,assigned=assigned) total=await service.count_inbox_messages(search,assigned=assigned) return JSONResponse(content={"data":items,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/inbox/counts") async def get_inbox_counts( current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.get_counts() return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.get("/inbox/triage") async def fetch_triage( search: str | None = Query(None), is_application: bool | None = Query(None), status: str | None = Query(None), top: int = Query(100), skip: int = Query(0, ge=0), current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.get_triage_messages(top,skip,search,is_application,status) total=await service.count_triage(search,is_application,status) return JSONResponse(content={"data":data,"total":total,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/inbox/triage/{record_id}/override") async def override_triage( record_id: str, payload: TriageOverrideBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.override_triage(record_id,payload.is_application,current_user) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/inbox/{record_id}/processing-state") async def set_processing_state( record_id: str, payload: ProcessingStateBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.set_processing_state(record_id,payload.processing_state) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.patch("/inbox/{record_id}/duplicate") async def set_duplicate( record_id: str, payload: DuplicateBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.set_duplicate(record_id,payload.is_duplicate) return JSONResponse(content={"data":data,"total":1,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/email/send") async def send_email( payload: EmailSendBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.send_email(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/email/reply") async def reply_email( payload: EmailReplyBody, current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.reply_email(payload.model_dump(exclude_unset=True),current_user) return JSONResponse(content={"data":data,"status_code":200}) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e))