""" Endpoints del Módulo B con caching """ import logging from typing import Optional from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, Depends from pydantic import BaseModel from app.data.repositories.ruta_repository import SupabaseRutaRepository from app.services.simulador import obtener_simulador from app.services.ws_manager import ws_manager from app.core.cache import cached, cache_client, invalidate_route_cache from app.core.dependencies import get_current_user from app.db.database import get_db router = APIRouter() logger = logging.getLogger(__name__) def _repo() -> SupabaseRutaRepository: return SupabaseRutaRepository() # ── GET /eta/{address_id} con caché ───────────────────────────────────────── @router.get("/eta/{address_id}", summary="Ventana ETA para un domicilio") @cached(prefix="eta", ttl=30) async def get_eta( address_id: int, current_user: dict = Depends(get_current_user) ): """ Devuelve ETA + ventana horaria solo para el domicilio solicitado. Cacheado por 30 segundos para evitar consultas repetidas. """ # Verificar que el domicilio pertenece al usuario (RBAC) db = get_db() try: result = db.table("addresses").select("user_id").eq("id", address_id).execute() if not result.data or result.data[0]["user_id"] != current_user["id"]: raise HTTPException(status_code=403, detail="No autorizado") except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) resultado = _repo().calcular_eta(address_id) if not resultado: raise HTTPException(status_code=404, detail="Domicilio no encontrado") return { "address_id": resultado.address_id, "route_id": resultado.route_id, "status": resultado.status, "eta_minutos": resultado.eta_minutos, "ventana": { "inicio": resultado.ventana_inicio, "fin": resultado.ventana_fin, }, "mensaje": resultado.mensaje, "cached": False, } # ── WS /ws/{address_id} ─────────────────────────────────────────────── @router.websocket("/ws/{address_id}") async def websocket_address(websocket: WebSocket, address_id: int): """WebSocket para recibir notificaciones en tiempo real""" zona_key = str(address_id) await ws_manager.conectar(websocket, zona_key) logger.info(f"[WS] Cliente conectado — address_id={address_id}") try: while True: await websocket.receive_text() # mantener vivo except WebSocketDisconnect: ws_manager.desconectar(websocket, zona_key) logger.info(f"[WS] Cliente desconectado — address_id={address_id}") # ── POST /alerts/breakdown ──────────────────────────────────────────── class BreakdownPayload(BaseModel): route_id: str mensaje: Optional[str] = "El camión reportó una falla mecánica." @router.post("/alerts/breakdown", summary="Reportar avería de camión") async def reportar_averia(payload: BreakdownPayload): """Endpoint para reportar avería - invalida caché automáticamente""" sim = obtener_simulador(payload.route_id) await sim.forzar_averia(payload.mensaje) # Invalidar caché de esta ruta await invalidate_route_cache(payload.route_id) return { "ok": True, "route_id": payload.route_id, "mensaje": "Avería registrada y usuarios notificados", } # ── Admin / Demo ────────────────────────────────────────────────────── @router.post("/admin/route/{route_id}/start", summary="Iniciar simulación") async def iniciar_ruta(route_id: str): """Iniciar el simulador de camión""" obtener_simulador(route_id).iniciar() await invalidate_route_cache(route_id) return {"ok": True, "mensaje": f"Simulador {route_id} iniciado"} @router.post("/admin/route/{route_id}/delay", summary="Forzar retraso") async def forzar_retraso(route_id: str, mensaje: str = "El camión reportó un retraso."): """Forzar retraso en la ruta""" await obtener_simulador(route_id).forzar_retraso(mensaje) await invalidate_route_cache(route_id) return {"ok": True, "mensaje": "Retraso notificado"} @router.get("/admin/route/{route_id}/status", summary="Estado interno del camión") async def estado_ruta(route_id: str): """Obtener estado actual del camión""" repo = _repo() ts = repo.obtener_truck_status(route_id) if not ts: raise HTTPException(status_code=404, detail="Ruta no encontrada") return { "route_id": ts.route_id, "current_position_id": ts.current_position_id, "status": ts.status.value, "last_update": ts.last_update.isoformat(), "ws_clientes_activos": ws_manager.zonas_activas(), } # ── Endpoint para limpiar caché (admin) ────────────────────────────── @router.post("/admin/cache/clear", summary="Limpiar toda la caché") async def clear_cache(): """Limpiar caché de Redis y memoria""" await cache_client.clear_all() return {"ok": True, "mensaje": "Caché limpiada"} @router.get("/admin/cache/stats", summary="Estadísticas de caché") async def cache_stats(): """Estadísticas del sistema de caché""" return { "enabled": cache_client.enabled, "redis_available": cache_client.redis_client is not None, "memory_cache_size": len(cache_client.memory_cache), }