import asyncio from fastapi import APIRouter, Depends, HTTPException, Query from api.contracts.captcha_schemas import ( CaptchaAbortRequest, CaptchaActionResponse, CaptchaExtendRequest, CaptchaNotifyRequest, CaptchaSolvedRequest, CaptchaStatusResponse, ) from api.mappers.captcha_mapper import CaptchaMapper from api.routes.dependencies import get_task_service from api.services.protocols import TaskServiceProtocol router = APIRouter(prefix="/api/browser", tags=["browser-captcha"]) @router.post("/tasks/{task_id}/captcha/notify", response_model=CaptchaActionResponse, status_code=202) async def captcha_notify( task_id: str, payload: CaptchaNotifyRequest, service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaActionResponse: rec = await service.notify_captcha( task_id=task_id, kind=payload.captcha_kind, reason=payload.reason, view_url=payload.browser_view_url, timeout_seconds=payload.timeout_seconds, ) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_action(rec, message="captcha awaiting solution") @router.get("/tasks/{task_id}/captcha", response_model=CaptchaStatusResponse) async def captcha_status( task_id: str, service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaStatusResponse: rec = await service.get_task(task_id) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_status(rec) @router.get("/tasks/{task_id}/captcha/wait", response_model=CaptchaStatusResponse) async def captcha_wait( task_id: str, timeout: float = Query(default=30.0, ge=0.1, le=600.0, description="long-poll cap, seconds"), service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaStatusResponse: rec = await service.wait_captcha(task_id=task_id, timeout=timeout) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_status(rec) @router.post("/tasks/{task_id}/captcha/solved", response_model=CaptchaActionResponse) async def captcha_solved( task_id: str, payload: CaptchaSolvedRequest | None = None, service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaActionResponse: rec = await service.mark_captcha_solved(task_id=task_id, detector=(payload.detector if payload else None)) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_action(rec, message="captcha marked as solved") @router.post("/tasks/{task_id}/captcha/timeout-prompt", response_model=CaptchaActionResponse) async def captcha_timeout_prompt( task_id: str, service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaActionResponse: rec = await service.prompt_captcha_timeout(task_id=task_id) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_action(rec, message="user must choose: extend or abort") @router.post("/tasks/{task_id}/captcha/extend", response_model=CaptchaActionResponse) async def captcha_extend( task_id: str, payload: CaptchaExtendRequest, service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaActionResponse: rec = await service.extend_captcha(task_id=task_id, extra_seconds=payload.extra_seconds) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_action(rec, message=f"captcha extended by {payload.extra_seconds}s") @router.post("/tasks/{task_id}/captcha/abort", response_model=CaptchaActionResponse) async def captcha_abort( task_id: str, payload: CaptchaAbortRequest | None = None, service: TaskServiceProtocol = Depends(get_task_service), ) -> CaptchaActionResponse: rec = await service.abort_captcha(task_id=task_id, reason=(payload.reason if payload else None)) if rec is None: raise HTTPException(status_code=404, detail="Task not found") return CaptchaMapper.to_action(rec, message="captcha aborted by user")