"""GMAL asset browse/search endpoints.""" from fastapi import APIRouter, Depends, Query, HTTPException from sqlalchemy import select, func, distinct from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models.gmal import GmalAsset, Role, GmalHours from app.models.gmal import ModelType from app.schemas.gmal import GmalAssetBrief, GmalAssetDetail, GmalAssetWithHours, GmalHoursOut, RoleOut, GmalStatsOut, GmalAssetUpdate, GmalHoursUpdate router = APIRouter() @router.get("/assets", response_model=list[GmalAssetBrief]) async def list_assets( db: AsyncSession = Depends(get_db), search: str | None = None, sub_category: str | None = None, category: str | None = None, complexity_level: int | None = None, has_hours: bool | None = None, skip: int = 0, limit: int = 100, ): query = select(GmalAsset) if search: pattern = f"%{search}%" query = query.where( GmalAsset.gmal_id.ilike(pattern) | GmalAsset.asset_name.ilike(pattern) | GmalAsset.unique_name.ilike(pattern) | GmalAsset.asset_description.ilike(pattern) ) if sub_category: query = query.where(GmalAsset.sub_category == sub_category) if category: query = query.where(GmalAsset.category == category) if complexity_level: query = query.where(GmalAsset.complexity_level == complexity_level) if has_hours is not None: query = query.where(GmalAsset.has_hour_routes == has_hours) query = query.order_by(GmalAsset.gmal_id).offset(skip).limit(limit) result = await db.execute(query) return result.scalars().all() @router.get("/assets/{gmal_id}", response_model=GmalAssetWithHours) async def get_asset(gmal_id: str, db: AsyncSession = Depends(get_db)): result = await db.execute( select(GmalAsset).where(GmalAsset.gmal_id == gmal_id) ) asset = result.scalar_one_or_none() if not asset: raise HTTPException(status_code=404, detail=f"GMAL asset {gmal_id} not found") # Load hours with role info hours_result = await db.execute( select(GmalHours, Role) .join(Role, GmalHours.role_id == Role.id) .where(GmalHours.gmal_asset_id == asset.id) .order_by(Role.sort_order) ) hours_by_role = [] for gh, role in hours_result.all(): hours_by_role.append(GmalHoursOut( role_id=role.id, role_title=role.role_title, discipline=role.discipline, model_type=gh.model_type.value, hours=float(gh.hours), )) return GmalAssetWithHours( **{k: v for k, v in asset.__dict__.items() if not k.startswith("_")}, hours_by_role=hours_by_role, ) @router.put("/assets/{gmal_id}", response_model=GmalAssetDetail) async def update_asset(gmal_id: str, data: GmalAssetUpdate, db: AsyncSession = Depends(get_db)): result = await db.execute(select(GmalAsset).where(GmalAsset.gmal_id == gmal_id)) asset = result.scalar_one_or_none() if not asset: raise HTTPException(status_code=404, detail=f"GMAL asset {gmal_id} not found") for field, value in data.model_dump(exclude_unset=True).items(): setattr(asset, field, value) await db.commit() await db.refresh(asset) return asset @router.put("/assets/{gmal_id}/hours") async def update_asset_hours(gmal_id: str, hours: list[GmalHoursUpdate], db: AsyncSession = Depends(get_db)): """Bulk update hours for a GMAL asset. Replaces existing hours for the given role+model combos.""" result = await db.execute(select(GmalAsset).where(GmalAsset.gmal_id == gmal_id)) asset = result.scalar_one_or_none() if not asset: raise HTTPException(status_code=404, detail=f"GMAL asset {gmal_id} not found") updated = 0 for h in hours: mt = ModelType(h.model_type) existing = await db.execute( select(GmalHours).where( GmalHours.gmal_asset_id == asset.id, GmalHours.role_id == h.role_id, GmalHours.model_type == mt, ) ) gh = existing.scalar_one_or_none() if h.hours == 0 and gh: await db.delete(gh) updated += 1 elif h.hours != 0 and gh: gh.hours = h.hours updated += 1 elif h.hours != 0 and not gh: db.add(GmalHours( gmal_asset_id=asset.id, role_id=h.role_id, model_type=mt, hours=h.hours, )) updated += 1 await db.commit() return {"detail": f"Updated {updated} hour records"} @router.get("/roles", response_model=list[RoleOut]) async def list_roles(db: AsyncSession = Depends(get_db), discipline: str | None = None): query = select(Role).order_by(Role.sort_order) if discipline: query = query.where(Role.discipline == discipline) result = await db.execute(query) return result.scalars().all() @router.get("/stats", response_model=GmalStatsOut) async def get_stats(db: AsyncSession = Depends(get_db)): assets_count = await db.execute(select(func.count(GmalAsset.id))) roles_count = await db.execute(select(func.count(Role.id))) hours_count = await db.execute(select(func.count(GmalHours.id))) categories = await db.execute(select(distinct(GmalAsset.category)).where(GmalAsset.category.isnot(None))) sub_cats = await db.execute(select(distinct(GmalAsset.sub_category)).where(GmalAsset.sub_category.isnot(None))) ai_desc_count = await db.execute( select(func.count(GmalAsset.id)).where(GmalAsset.ai_enhanced_description.isnot(None)) ) return GmalStatsOut( total_assets=assets_count.scalar() or 0, total_roles=roles_count.scalar() or 0, total_hours_records=hours_count.scalar() or 0, categories=sorted([r[0] for r in categories.all()]), sub_categories=sorted([r[0] for r in sub_cats.all()]), ai_descriptions_count=ai_desc_count.scalar() or 0, ) @router.post("/generate-descriptions") async def generate_all_descriptions(db: AsyncSession = Depends(get_db)): """Generate AI-enhanced descriptions for all GMAL assets.""" from app.services.ai_descriptions import generate_descriptions_batch result = await generate_descriptions_batch(db) return result @router.post("/assets/{gmal_id}/generate-description") async def generate_single_description(gmal_id: str, db: AsyncSession = Depends(get_db)): """Generate/regenerate AI-enhanced description for a single GMAL asset.""" from app.services.ai_descriptions import generate_description_single result = await db.execute(select(GmalAsset).where(GmalAsset.gmal_id == gmal_id)) asset = result.scalar_one_or_none() if not asset: raise HTTPException(status_code=404, detail=f"GMAL asset {gmal_id} not found") desc = await generate_description_single(db, asset) return {"gmal_id": gmal_id, "ai_enhanced_description": desc}