""" Endpoints para gestión de geocercas. """ import json from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.core.database import get_db from app.core.security import get_current_user from app.models.usuario import Usuario from app.models.geocerca import Geocerca from app.models.vehiculo import Vehiculo from app.schemas.geocerca import ( GeocercaCircularCreate, GeocercaPoligonoCreate, GeocercaUpdate, GeocercaResponse, GeocercaConVehiculos, AsignarVehiculosRequest, VerificarPuntoRequest, VerificarPuntoResponse, ) from app.services.geocerca_service import GeocercaService router = APIRouter(prefix="/geocercas", tags=["Geocercas"]) @router.get("", response_model=List[GeocercaResponse]) async def listar_geocercas( activa: Optional[bool] = None, tipo: Optional[str] = None, categoria: Optional[str] = None, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Lista todas las geocercas. Args: activa: Filtrar por estado. tipo: Filtrar por tipo (circular/poligono). categoria: Filtrar por categoría. skip: Registros a saltar. limit: Límite de registros. Returns: Lista de geocercas. """ query = select(Geocerca).order_by(Geocerca.nombre) if activa is not None: query = query.where(Geocerca.activa == activa) if tipo: query = query.where(Geocerca.tipo == tipo) if categoria: query = query.where(Geocerca.categoria == categoria) query = query.offset(skip).limit(limit) result = await db.execute(query) geocercas = result.scalars().all() return [ GeocercaResponse( id=g.id, nombre=g.nombre, descripcion=g.descripcion, tipo=g.tipo, color=g.color, opacidad=g.opacidad, color_borde=g.color_borde, categoria=g.categoria, centro_lat=g.centro_lat, centro_lng=g.centro_lng, radio_metros=g.radio_metros, coordenadas_json=g.coordenadas_json, alerta_entrada=g.alerta_entrada, alerta_salida=g.alerta_salida, velocidad_maxima=g.velocidad_maxima, horario_json=g.horario_json, activa=g.activa, aplica_todos_vehiculos=g.aplica_todos_vehiculos, creado_en=g.creado_en, actualizado_en=g.actualizado_en, ) for g in geocercas ] @router.get("/geojson") async def obtener_geocercas_geojson( activa: bool = True, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene todas las geocercas en formato GeoJSON. Args: activa: Solo geocercas activas. Returns: FeatureCollection GeoJSON. """ query = select(Geocerca) if activa: query = query.where(Geocerca.activa == True) result = await db.execute(query) geocercas = result.scalars().all() features = [g.to_geojson() for g in geocercas] return { "type": "FeatureCollection", "features": features, } @router.get("/{geocerca_id}", response_model=GeocercaConVehiculos) async def obtener_geocerca( geocerca_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Obtiene una geocerca por su ID. Args: geocerca_id: ID de la geocerca. Returns: Geocerca con vehículos asignados. """ result = await db.execute( select(Geocerca) .options(selectinload(Geocerca.vehiculos_asignados)) .where(Geocerca.id == geocerca_id) ) geocerca = result.scalar_one_or_none() if not geocerca: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Geocerca con id {geocerca_id} no encontrada", ) from app.schemas.vehiculo import VehiculoResumen return GeocercaConVehiculos( id=geocerca.id, nombre=geocerca.nombre, descripcion=geocerca.descripcion, tipo=geocerca.tipo, color=geocerca.color, opacidad=geocerca.opacidad, color_borde=geocerca.color_borde, categoria=geocerca.categoria, centro_lat=geocerca.centro_lat, centro_lng=geocerca.centro_lng, radio_metros=geocerca.radio_metros, coordenadas_json=geocerca.coordenadas_json, alerta_entrada=geocerca.alerta_entrada, alerta_salida=geocerca.alerta_salida, velocidad_maxima=geocerca.velocidad_maxima, horario_json=geocerca.horario_json, activa=geocerca.activa, aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos, creado_en=geocerca.creado_en, actualizado_en=geocerca.actualizado_en, vehiculos_asignados=[ VehiculoResumen.model_validate(v) for v in geocerca.vehiculos_asignados ], ) @router.post("/circular", response_model=GeocercaResponse, status_code=status.HTTP_201_CREATED) async def crear_geocerca_circular( geocerca_data: GeocercaCircularCreate, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Crea una geocerca circular. Args: geocerca_data: Datos de la geocerca. Returns: Geocerca creada. """ geocerca = Geocerca( nombre=geocerca_data.nombre, descripcion=geocerca_data.descripcion, tipo="circular", centro_lat=geocerca_data.centro_lat, centro_lng=geocerca_data.centro_lng, radio_metros=geocerca_data.radio_metros, color=geocerca_data.color, opacidad=geocerca_data.opacidad, color_borde=geocerca_data.color_borde, categoria=geocerca_data.categoria, alerta_entrada=geocerca_data.alerta_entrada, alerta_salida=geocerca_data.alerta_salida, velocidad_maxima=geocerca_data.velocidad_maxima, horario_json=geocerca_data.horario_json, ) db.add(geocerca) await db.commit() await db.refresh(geocerca) # Asignar vehículos si se especificaron if geocerca_data.vehiculos_ids: result = await db.execute( select(Vehiculo).where(Vehiculo.id.in_(geocerca_data.vehiculos_ids)) ) vehiculos = result.scalars().all() geocerca.vehiculos_asignados = list(vehiculos) await db.commit() return GeocercaResponse( id=geocerca.id, nombre=geocerca.nombre, descripcion=geocerca.descripcion, tipo=geocerca.tipo, color=geocerca.color, opacidad=geocerca.opacidad, color_borde=geocerca.color_borde, categoria=geocerca.categoria, centro_lat=geocerca.centro_lat, centro_lng=geocerca.centro_lng, radio_metros=geocerca.radio_metros, coordenadas_json=geocerca.coordenadas_json, alerta_entrada=geocerca.alerta_entrada, alerta_salida=geocerca.alerta_salida, velocidad_maxima=geocerca.velocidad_maxima, horario_json=geocerca.horario_json, activa=geocerca.activa, aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos, creado_en=geocerca.creado_en, actualizado_en=geocerca.actualizado_en, ) @router.post("/poligono", response_model=GeocercaResponse, status_code=status.HTTP_201_CREATED) async def crear_geocerca_poligono( geocerca_data: GeocercaPoligonoCreate, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Crea una geocerca poligonal. Args: geocerca_data: Datos de la geocerca. Returns: Geocerca creada. """ geocerca = Geocerca( nombre=geocerca_data.nombre, descripcion=geocerca_data.descripcion, tipo="poligono", coordenadas_json=json.dumps(geocerca_data.coordenadas), color=geocerca_data.color, opacidad=geocerca_data.opacidad, color_borde=geocerca_data.color_borde, categoria=geocerca_data.categoria, alerta_entrada=geocerca_data.alerta_entrada, alerta_salida=geocerca_data.alerta_salida, velocidad_maxima=geocerca_data.velocidad_maxima, horario_json=geocerca_data.horario_json, ) db.add(geocerca) await db.commit() await db.refresh(geocerca) # Asignar vehículos si se especificaron if geocerca_data.vehiculos_ids: result = await db.execute( select(Vehiculo).where(Vehiculo.id.in_(geocerca_data.vehiculos_ids)) ) vehiculos = result.scalars().all() geocerca.vehiculos_asignados = list(vehiculos) await db.commit() return GeocercaResponse( id=geocerca.id, nombre=geocerca.nombre, descripcion=geocerca.descripcion, tipo=geocerca.tipo, color=geocerca.color, opacidad=geocerca.opacidad, color_borde=geocerca.color_borde, categoria=geocerca.categoria, centro_lat=geocerca.centro_lat, centro_lng=geocerca.centro_lng, radio_metros=geocerca.radio_metros, coordenadas_json=geocerca.coordenadas_json, alerta_entrada=geocerca.alerta_entrada, alerta_salida=geocerca.alerta_salida, velocidad_maxima=geocerca.velocidad_maxima, horario_json=geocerca.horario_json, activa=geocerca.activa, aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos, creado_en=geocerca.creado_en, actualizado_en=geocerca.actualizado_en, ) @router.put("/{geocerca_id}", response_model=GeocercaResponse) async def actualizar_geocerca( geocerca_id: int, geocerca_data: GeocercaUpdate, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Actualiza una geocerca. Args: geocerca_id: ID de la geocerca. geocerca_data: Datos a actualizar. Returns: Geocerca actualizada. """ result = await db.execute( select(Geocerca).where(Geocerca.id == geocerca_id) ) geocerca = result.scalar_one_or_none() if not geocerca: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Geocerca con id {geocerca_id} no encontrada", ) update_data = geocerca_data.model_dump(exclude_unset=True) # Manejar coordenadas si es polígono if "coordenadas" in update_data and update_data["coordenadas"]: update_data["coordenadas_json"] = json.dumps(update_data.pop("coordenadas")) for field, value in update_data.items(): if hasattr(geocerca, field): setattr(geocerca, field, value) await db.commit() await db.refresh(geocerca) return GeocercaResponse( id=geocerca.id, nombre=geocerca.nombre, descripcion=geocerca.descripcion, tipo=geocerca.tipo, color=geocerca.color, opacidad=geocerca.opacidad, color_borde=geocerca.color_borde, categoria=geocerca.categoria, centro_lat=geocerca.centro_lat, centro_lng=geocerca.centro_lng, radio_metros=geocerca.radio_metros, coordenadas_json=geocerca.coordenadas_json, alerta_entrada=geocerca.alerta_entrada, alerta_salida=geocerca.alerta_salida, velocidad_maxima=geocerca.velocidad_maxima, horario_json=geocerca.horario_json, activa=geocerca.activa, aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos, creado_en=geocerca.creado_en, actualizado_en=geocerca.actualizado_en, ) @router.delete("/{geocerca_id}", status_code=status.HTTP_204_NO_CONTENT) async def eliminar_geocerca( geocerca_id: int, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Elimina una geocerca (soft delete - desactiva). Args: geocerca_id: ID de la geocerca. """ result = await db.execute( select(Geocerca).where(Geocerca.id == geocerca_id) ) geocerca = result.scalar_one_or_none() if not geocerca: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Geocerca con id {geocerca_id} no encontrada", ) geocerca.activa = False await db.commit() @router.post("/{geocerca_id}/vehiculos") async def asignar_vehiculos( geocerca_id: int, request: AsignarVehiculosRequest, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Asigna vehículos a una geocerca. Args: geocerca_id: ID de la geocerca. request: Lista de IDs de vehículos. Returns: Confirmación. """ result = await db.execute( select(Geocerca) .options(selectinload(Geocerca.vehiculos_asignados)) .where(Geocerca.id == geocerca_id) ) geocerca = result.scalar_one_or_none() if not geocerca: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Geocerca con id {geocerca_id} no encontrada", ) # Obtener vehículos result = await db.execute( select(Vehiculo).where(Vehiculo.id.in_(request.vehiculos_ids)) ) vehiculos = result.scalars().all() if request.reemplazar: geocerca.vehiculos_asignados = list(vehiculos) else: for v in vehiculos: if v not in geocerca.vehiculos_asignados: geocerca.vehiculos_asignados.append(v) await db.commit() return { "message": "Vehículos asignados correctamente", "total_asignados": len(geocerca.vehiculos_asignados), } @router.post("/{geocerca_id}/verificar", response_model=VerificarPuntoResponse) async def verificar_punto( geocerca_id: int, request: VerificarPuntoRequest, db: AsyncSession = Depends(get_db), current_user: Usuario = Depends(get_current_user), ): """ Verifica si un punto está dentro de una geocerca. Args: geocerca_id: ID de la geocerca. request: Coordenadas del punto. Returns: Resultado de la verificación. """ result = await db.execute( select(Geocerca).where(Geocerca.id == geocerca_id) ) geocerca = result.scalar_one_or_none() if not geocerca: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Geocerca con id {geocerca_id} no encontrada", ) geocerca_service = GeocercaService(db) dentro, distancia = await geocerca_service.verificar_punto_en_geocerca( request.lat, request.lng, geocerca_id ) return VerificarPuntoResponse( dentro=dentro, geocerca_id=geocerca_id, geocerca_nombre=geocerca.nombre, distancia_metros=distancia, )