from typing import Annotated, Optional from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, or_ from app.core.database import get_db from app.models.tree import Tree from app.models.user import User from app.schemas.tree import TreeCreate, TreeUpdate, TreeResponse, TreeListResponse from app.api.deps import get_current_user, require_engineer_or_admin, require_admin router = APIRouter(prefix="/trees", tags=["trees"]) @router.get("", response_model=list[TreeListResponse]) async def list_trees( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], category: Optional[str] = Query(None, description="Filter by category"), is_active: Optional[bool] = Query(None, description="Filter by active status"), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=100) ): """List all trees with optional filters.""" query = select(Tree) # Apply filters if category: query = query.where(Tree.category == category) if is_active is not None: query = query.where(Tree.is_active == is_active) # Only show active trees or trees owned by user (for now) # Later, add team-based filtering query = query.where( or_( Tree.is_active == True, Tree.author_id == current_user.id ) ) query = query.order_by(Tree.usage_count.desc(), Tree.updated_at.desc()) query = query.offset(skip).limit(limit) result = await db.execute(query) trees = result.scalars().all() return trees @router.get("/categories", response_model=list[str]) async def list_categories( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)] ): """List all unique categories.""" query = select(Tree.category).where( Tree.category.isnot(None), Tree.is_active == True ).distinct() result = await db.execute(query) categories = [row[0] for row in result.all() if row[0]] return sorted(categories) @router.get("/search", response_model=list[TreeListResponse]) async def search_trees( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], q: str = Query(..., min_length=2, description="Search query"), limit: int = Query(20, ge=1, le=50) ): """Full-text search trees by name and description.""" # Using PostgreSQL full-text search search_vector = func.to_tsvector('english', func.coalesce(Tree.name, '') + ' ' + func.coalesce(Tree.description, '')) search_query = func.plainto_tsquery('english', q) query = select(Tree).where( Tree.is_active == True, search_vector.op('@@')(search_query) ).order_by( func.ts_rank(search_vector, search_query).desc() ).limit(limit) result = await db.execute(query) trees = result.scalars().all() return trees @router.get("/{tree_id}", response_model=TreeResponse) async def get_tree( tree_id: UUID, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)] ): """Get a specific tree by ID.""" result = await db.execute(select(Tree).where(Tree.id == tree_id)) tree = result.scalar_one_or_none() if not tree: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Tree not found" ) # Check access: tree must be active OR user is the author if not tree.is_active and tree.author_id != current_user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You don't have access to this tree" ) return tree @router.post("", response_model=TreeResponse, status_code=status.HTTP_201_CREATED) async def create_tree( tree_data: TreeCreate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_engineer_or_admin)] ): """Create a new tree (engineers and admins only).""" new_tree = Tree( name=tree_data.name, description=tree_data.description, category=tree_data.category, tree_structure=tree_data.tree_structure, author_id=current_user.id, team_id=current_user.team_id ) db.add(new_tree) await db.commit() await db.refresh(new_tree) return new_tree @router.put("/{tree_id}", response_model=TreeResponse) async def update_tree( tree_id: UUID, tree_data: TreeUpdate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_engineer_or_admin)] ): """Update an existing tree (engineers and admins only).""" result = await db.execute(select(Tree).where(Tree.id == tree_id)) tree = result.scalar_one_or_none() if not tree: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Tree not found" ) # Check if user can edit: must be author or admin if tree.author_id != current_user.id and current_user.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You can only edit your own trees" ) # Update fields update_data = tree_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(tree, field, value) # Increment version if tree structure changed if "tree_structure" in update_data: tree.version += 1 await db.commit() await db.refresh(tree) return tree @router.delete("/{tree_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_tree( tree_id: UUID, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(require_admin)] ): """Soft delete a tree (admin only). Sets is_active to False.""" result = await db.execute(select(Tree).where(Tree.id == tree_id)) tree = result.scalar_one_or_none() if not tree: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Tree not found" ) tree.is_active = False await db.commit() return None