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 is_duplicate: 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,ge=1,le=100), skip:int=Query(0,ge=0), test_on: bool = Query(True), token: str | None = Query(None), session: AsyncSession = Depends(get_session), ): """Synchronous one-shot sync — kept for scripts/compat. UI uses POST /email/sync.""" try: service=Email(session=session,token=token) if not service.token: raise HTTPException(status_code=401,detail="Unauthorized") summary=await service.run_mailbox_sync_page(top=top,skip=skip,test_on=test_on) if test_on: return JSONResponse(content={ "data":summary["entries"], "triage":summary["triage"], "status_code":200, }) return JSONResponse(content={ "data":summary["entries"], "account_setup":summary["account_setup"], "triage":summary["triage"], "status_code":200, }) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500,detail=str(e)) @router.post("/email/sync") async def start_email_sync( top:int=Query(100,ge=1,le=100), skip:int=Query(0,ge=0), test_on: bool = Query(True), current_user: dict = Depends(require_permission(PermissionTag.INBOX_EDIT)), session: AsyncSession = Depends(get_session), ): """Enqueue mailbox sync on the dedicated mailbox_sync Taskiq queue. Returns immediately with a run id. Poll GET /email/sync/fetch until completed. Closing the browser does not cancel the worker. """ try: service=Email(session=session) if not service.token: raise HTTPException(status_code=401,detail="Unauthorized") data=await service.start_mailbox_sync( current_user=current_user,top=top,skip=skip,test_on=test_on, ) 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("/email/sync/fetch") async def fetch_email_sync( run_id: str | None = Query(None), current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)), session: AsyncSession = Depends(get_session), ): try: service=Email(session=session) data=await service.get_mailbox_sync(run_id=run_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/fetch") async def fetch_inbox( record_id: str | None = Query(None), search: str | None = Query(None), top: int | None = Query(None, ge=1, le=500), 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, is_duplicate=payload.is_duplicate, ) 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), is_duplicate: bool | None = Query(default=None), no_suggestions: bool | None = Query(default=None), search: str | None = Query(None), # Caller-chosen page size (Inbox / Job Matching send 10/25/50/100). None = unpaged. top: int | None = Query(None, ge=1, le=500), 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, is_duplicate=is_duplicate, no_suggestions=no_suggestions) total=await service.count_inbox_messages(search, application_status=application_status, assigned=assigned, is_duplicate=is_duplicate, no_suggestions=no_suggestions) 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, is_duplicate=is_duplicate, no_suggestions=no_suggestions) total=await service.count_inbox_messages(search, isread=False, assigned=assigned, is_duplicate=is_duplicate, no_suggestions=no_suggestions) 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,is_duplicate=is_duplicate,no_suggestions=no_suggestions) total=await service.count_inbox_messages(search,assigned=assigned,is_duplicate=is_duplicate,no_suggestions=no_suggestions) 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/all-applications/count") async def count_all_applications( current_user: dict = Depends(require_permission(PermissionTag.INBOX_VIEW)), session: AsyncSession = Depends(get_session), ): """Unfiltered application total. Called once when Inbox Email opens.""" try: service=Email(session=session) total=await service.count_inbox_messages() return JSONResponse(content={"data":{"total":total},"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))