from __future__ import annotations import re import time from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from motor.motor_asyncio import AsyncIOMotorDatabase from fastcheck_api.app.api.dependencies import CurrentUser, require_permission from fastcheck_api.app.core.mongodb import get_db from fastcheck_api.app.schemas.checks import ( CreateCheckRequest, EvaluationJobOut, ListJobsResponse, ListResultsResponse, ) from fastcheck_api.app.services.audit_service import AuditService from fastcheck_api.app.services.check_service import CheckService, InsufficientCreditsError from fastcheck_api.app.utils.mongo import sanitize_rut router = APIRouter(prefix="/checks", tags=["checks"]) _MASK_REPLACEMENTS: dict[str, str] = { "sheriff": "provider_a", "equifax": "provider_b", "dequienes": "provider_c", } _MASK_PATTERN = re.compile("|".join(re.escape(k) for k in _MASK_REPLACEMENTS), re.IGNORECASE) def _mask_text(value: str) -> str: return _MASK_PATTERN.sub(lambda m: _MASK_REPLACEMENTS[m.group(0).lower()], value) def _mask_json(value): # type: ignore[no-untyped-def] if isinstance(value, str): return _mask_text(value) if isinstance(value, list): return [_mask_json(v) for v in value] if isinstance(value, dict): out = {} for k, v in value.items(): if isinstance(k, str) and k.lower() == "datasourcedesc": continue masked_key = _mask_text(k) if isinstance(k, str) else k out[masked_key] = _mask_json(v) return out return value @router.get( "", response_model=ListJobsResponse, summary="List checks", description="List evaluation jobs (checks) for the current tenant.", ) async def list_checks( user: Annotated[CurrentUser, Depends(require_permission("evaluation:read"))], db: Annotated[AsyncIOMotorDatabase, Depends(get_db)], page: int = Query(default=1, ge=1), limit: int = Query(default=10, ge=1, le=100), ): return await CheckService.list_jobs(db=db, tenant_id=user.tenant, page=page, limit=limit) @router.post( "", response_model=EvaluationJobOut, status_code=status.HTTP_201_CREATED, summary="Create a check", description="Create a single evaluation job and enqueue processing in RabbitMQ using the existing queue and message format.", ) async def create_check( request: Request, payload: CreateCheckRequest, user: Annotated[CurrentUser, Depends(require_permission("evaluation:create"))], db: Annotated[AsyncIOMotorDatabase, Depends(get_db)], ): start = time.perf_counter() clean_rut = sanitize_rut(payload.supplier.rut) try: job = await CheckService.create_single_check( db=db, tenant_id=user.tenant, user_id=user.id, rut=clean_rut, name=payload.supplier.name, ) await AuditService.log_consulta( db=db, tenant_id=user.tenant, user_id=user.id, consulta_type="individual", rut=clean_rut, endpoint="/api/v1/checks", response_status=status.HTTP_201_CREATED, request_data={"supplier": {"rut": clean_rut, "name": payload.supplier.name}}, processing_time_ms=int((time.perf_counter() - start) * 1000), credits_used=1, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), metadata={"evaluationType": "single"}, ) return job except InsufficientCreditsError: await AuditService.log_consulta( db=db, tenant_id=user.tenant, user_id=user.id, consulta_type="individual", rut=clean_rut, endpoint="/api/v1/checks", response_status=status.HTTP_402_PAYMENT_REQUIRED, request_data={"supplier": {"rut": clean_rut, "name": payload.supplier.name}}, processing_time_ms=int((time.perf_counter() - start) * 1000), credits_used=0, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent"), metadata={"reason": "insufficient_credits"}, ) raise HTTPException( status_code=status.HTTP_402_PAYMENT_REQUIRED, detail="Insufficient credits", ) @router.get( "/{job_id}", response_model=EvaluationJobOut, summary="Get a check", description="Fetch a single evaluation job by id (tenant-scoped).", ) async def get_check( job_id: str, user: Annotated[CurrentUser, Depends(require_permission("evaluation:read"))], db: Annotated[AsyncIOMotorDatabase, Depends(get_db)], ): job = await CheckService.get_job(db=db, tenant_id=user.tenant, job_id=job_id) if not job: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found") return job @router.get( "/{job_id}/results", response_model=ListResultsResponse, summary="List check results", description="List evaluation results for a given job id (tenant-scoped).", ) async def get_check_results( job_id: str, user: Annotated[CurrentUser, Depends(require_permission("evaluation:read"))], db: Annotated[AsyncIOMotorDatabase, Depends(get_db)], page: int = Query(default=1, ge=1), limit: int = Query(default=10, ge=1, le=100), ): try: payload = await CheckService.list_results(db=db, tenant_id=user.tenant, job_id=job_id, page=page, limit=limit) return _mask_json(payload) except LookupError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Job not found") except ValueError as e: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))