from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from core.database import get_core_db from core.efc_client import EfcClientError from core.security import get_current_user from . import service from .dto import ( ExpedienteCompleteInput, ExpedienteDocumentResponse, ExpedienteEnsureInput, ExpedienteResponse, ) router = APIRouter() @router.get("/expedientes", response_model=list[ExpedienteResponse]) def list_expedientes( company_id: int = Query(..., description="Company ID"), service_request_id: int | None = Query(None, description="Filtrar por solicitud"), account_id: int | None = Query(None, description="Filtrar por cliente"), status_filter: str | None = Query(None, alias="status", description="abierto|completado|cerrado"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): tenant_id = current_user["tenant_id"] return service.list_expedientes( db, tenant_id, company_id, service_request_id, account_id, status_filter ) @router.get("/expedientes/{expediente_id}", response_model=ExpedienteResponse) def get_expediente( expediente_id: int, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): tenant_id = current_user["tenant_id"] return service.get_expediente(db, expediente_id, tenant_id, company_id) @router.post("/expedientes/ensure", response_model=ExpedienteResponse) def ensure_expediente( payload: ExpedienteEnsureInput, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): """Devuelve el expediente de una solicitud, creándolo si hace falta. Idempotente. Responde 200 y no 201 justamente porque es idempotente: el llamador no puede distinguir —ni le importa— si el expediente ya estaba. """ tenant_id = current_user["tenant_id"] user_id = current_user.get("sub") or current_user.get("id") return service.ensure_expediente(db, payload.service_request_id, tenant_id, company_id, user_id) @router.post("/expedientes/{expediente_id}/completar", response_model=ExpedienteResponse) def complete_expediente( expediente_id: int, payload: ExpedienteCompleteInput, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): tenant_id = current_user["tenant_id"] user_id = current_user.get("sub") or current_user.get("id") return service.complete_expediente(db, expediente_id, payload, tenant_id, company_id, user_id) @router.delete("/expedientes/{expediente_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_expediente( expediente_id: int, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): tenant_id = current_user["tenant_id"] service.delete_expediente(db, expediente_id, tenant_id, company_id) # ── Documentos del expediente ──────────────────────────────────────────────── @router.get( "/expedientes/{expediente_id}/documentos", response_model=list[ExpedienteDocumentResponse], ) def list_expediente_documents( expediente_id: int, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): tenant_id = current_user["tenant_id"] return service.list_expediente_documents(db, expediente_id, tenant_id, company_id) @router.post( "/expedientes/{expediente_id}/documentos", response_model=ExpedienteDocumentResponse, status_code=status.HTTP_201_CREATED, ) async def upload_expediente_document( expediente_id: int, file: UploadFile = File(...), doc_type: str = Form(...), name: str | None = Form(None), company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): """Subida de un paso: guarda, registra y encola la entrega a EFC. Responde **201 aunque EFC esté caído**: el archivo ya está a salvo en el CRM y el carril lo entrega cuando EFC vuelva. Perder el trabajo del usuario porque un sistema de terceros no contesta sería el peor intercambio posible. """ tenant_id = current_user["tenant_id"] user_id = current_user.get("sub") or current_user.get("id") return await service.attach_document( db, expediente_id, file, doc_type, tenant_id, company_id, name, user_id ) @router.get("/expedientes/{expediente_id}/documentos/{document_id}/archivo") async def download_expediente_document( expediente_id: int, document_id: int, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): """Proxy de descarga hacia EFC, con streaming. La traducción de errores es asimétrica **a propósito**: cualquier ``EfcClientError`` sale como 502 —es un fallo de la integración, no del usuario—, salvo un 404 de EFC, que sale como 404 porque significa que ese documento realmente no está. Es ``async`` porque el servicio abre la conexión con EFC y **comprueba su status antes** de que esta función devuelva la ``StreamingResponse``: una vez devuelta, el status ya se envió y traducir el error sería tarde (ver ``service._abrir_upstream``). """ tenant_id = current_user["tenant_id"] try: iterador, content_type, filename = await service.stream_document( db, expediente_id, document_id, tenant_id, company_id ) except EfcClientError as exc: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND if exc.status_code == 404 else status.HTTP_502_BAD_GATEWAY, detail="No se pudo obtener el archivo del expediente electrónico.", ) from exc return StreamingResponse( iterador, media_type=content_type, headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) @router.delete( "/expedientes/{expediente_id}/documentos/{document_id}", status_code=status.HTTP_204_NO_CONTENT, ) def detach_expediente_document( expediente_id: int, document_id: int, company_id: int = Query(..., description="Company ID"), current_user: dict = Depends(get_current_user), db: Session = Depends(get_core_db), ): """Desasocia el documento del CRM. **No lo destruye en EFC** (ver ``service.detach_document``).""" tenant_id = current_user["tenant_id"] service.detach_document(db, expediente_id, document_id, tenant_id, company_id)