""" Celestial Body Management API routes Handles CRUD operations for celestial bodies (planets, dwarf planets, satellites, probes, etc.) """ import logging from fastapi import APIRouter, HTTPException, Depends, Query, status from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel from typing import Optional, Dict, Any from app.database import get_db from app.models.celestial import BodyInfo from app.services.horizons import horizons_service from app.services.db_service import celestial_body_service, resource_service logger = logging.getLogger(__name__) router = APIRouter(prefix="/celestial", tags=["celestial-body"]) # Pydantic models for CRUD class CelestialBodyCreate(BaseModel): id: str name: str name_zh: Optional[str] = None type: str system_id: Optional[int] = None description: Optional[str] = None details: Optional[str] = None is_active: Optional[bool] = True extra_data: Optional[Dict[str, Any]] = None class CelestialBodyUpdate(BaseModel): name: Optional[str] = None name_zh: Optional[str] = None type: Optional[str] = None system_id: Optional[int] = None description: Optional[str] = None details: Optional[str] = None is_active: Optional[bool] = None extra_data: Optional[Dict[str, Any]] = None @router.post("", status_code=status.HTTP_201_CREATED) async def create_celestial_body( body_data: CelestialBodyCreate, db: AsyncSession = Depends(get_db) ): """Create a new celestial body""" # Check if exists existing = await celestial_body_service.get_body_by_id(body_data.id, db) if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Body with ID {body_data.id} already exists" ) new_body = await celestial_body_service.create_body(body_data.dict(), db) return new_body @router.get("/search") async def search_celestial_body( name: str = Query(..., description="Body name or ID to search in NASA Horizons"), db: AsyncSession = Depends(get_db) ): """ Search for a celestial body in NASA Horizons database by name or ID Returns body information if found, including suggested ID and full name """ logger.info(f"Searching for celestial body: {name}") try: result = await horizons_service.search_body_by_name(name, db) if result["success"]: logger.info(f"Found body: {result['full_name']}") return { "success": True, "data": { "id": result["id"], "name": result["name"], "full_name": result["full_name"], } } else: logger.warning(f"Search failed: {result['error']}") return { "success": False, "error": result["error"] } except Exception as e: logger.error(f"Search error: {e}") raise HTTPException( status_code=500, detail=f"Search failed: {str(e)}" ) @router.get("/{body_id}/nasa-data") async def get_celestial_nasa_data( body_id: str, db: AsyncSession = Depends(get_db) ): """ Get raw text data from NASA Horizons for a celestial body (Hacker terminal style output) """ # Check if body exists body = await celestial_body_service.get_body_by_id(body_id, db) if not body: raise HTTPException(status_code=404, detail="Celestial body not found") try: # Fetch raw text from Horizons using the body_id # Note: body.id corresponds to JPL Horizons ID raw_text = await horizons_service.get_object_data_raw(body.id) return {"id": body.id, "name": body.name, "raw_data": raw_text} except Exception as e: logger.error(f"Failed to fetch raw data for {body_id}: {e}") raise HTTPException(status_code=500, detail=f"Failed to fetch NASA data: {str(e)}") @router.put("/{body_id}") async def update_celestial_body( body_id: str, body_data: CelestialBodyUpdate, db: AsyncSession = Depends(get_db) ): """Update a celestial body""" # Filter out None values update_data = {k: v for k, v in body_data.dict().items() if v is not None} updated = await celestial_body_service.update_body(body_id, update_data, db) if not updated: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Body {body_id} not found" ) return updated @router.delete("/{body_id}") async def delete_celestial_body( body_id: str, db: AsyncSession = Depends(get_db) ): """Delete a celestial body""" deleted = await celestial_body_service.delete_body(body_id, db) if not deleted: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Body {body_id} not found" ) return {"message": "Body deleted successfully"} @router.get("/info/{body_id}", response_model=BodyInfo) async def get_body_info(body_id: str, db: AsyncSession = Depends(get_db)): """ Get detailed information about a specific celestial body Args: body_id: JPL Horizons ID (e.g., '-31' for Voyager 1, '399' for Earth) """ body = await celestial_body_service.get_body_by_id(body_id, db) if not body: raise HTTPException(status_code=404, detail=f"Body {body_id} not found") # Extract extra_data fields extra_data = body.extra_data or {} return BodyInfo( id=body.id, name=body.name, type=body.type, description=body.description, details=body.details, launch_date=extra_data.get("launch_date"), status=extra_data.get("status"), ) @router.get("/list") async def list_bodies( body_type: Optional[str] = Query(None, description="Filter by body type"), system_id: Optional[int] = Query(None, description="Filter by star system ID (1=Solar, 2+=Exoplanets)"), db: AsyncSession = Depends(get_db) ): """ Get a list of all available celestial bodies Args: body_type: Filter by body type (star, planet, dwarf_planet, satellite, probe, comet, etc.) system_id: Filter by star system ID (1=Solar System, 2+=Exoplanet systems) """ bodies = await celestial_body_service.get_all_bodies(db, body_type, system_id) # Bulk load all resources in one query (avoid N+1 problem) body_ids = [body.id for body in bodies] resources_by_body = await resource_service.get_all_resources_grouped_by_body(body_ids, db) bodies_list = [] for body in bodies: # Get resources for this body from the bulk-loaded dict resources = resources_by_body.get(body.id, []) # Group resources by type resources_by_type = {} for resource in resources: if resource.resource_type not in resources_by_type: resources_by_type[resource.resource_type] = [] resources_by_type[resource.resource_type].append({ "id": resource.id, "file_path": resource.file_path, "file_size": resource.file_size, "mime_type": resource.mime_type, }) bodies_list.append( { "id": body.id, "name": body.name, "name_zh": body.name_zh, "type": body.type, "system_id": body.system_id, # Add system_id field "description": body.description, "details": body.details, "is_active": body.is_active, "resources": resources_by_type, "has_resources": len(resources) > 0, } ) return {"bodies": bodies_list}