"""Customer CRUD API endpoints with automatic deployment on create.""" import logging from datetime import datetime from typing import Optional from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.database import get_db from app.dependencies import get_current_user from app.models import Customer, Deployment, DeploymentLog, User from app.services import netbird_service from app.utils.validators import CustomerCreate, CustomerUpdate logger = logging.getLogger(__name__) router = APIRouter() @router.post("", status_code=status.HTTP_201_CREATED) async def create_customer( payload: CustomerCreate, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Create a new customer and trigger auto-deployment. Validates that the subdomain is unique, creates the customer record, and launches deployment in the background. Args: payload: Customer creation data. background_tasks: FastAPI background task runner. Returns: Created customer dict with deployment status. """ # Check subdomain uniqueness existing = db.query(Customer).filter(Customer.subdomain == payload.subdomain).first() if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Subdomain '{payload.subdomain}' is already in use.", ) customer = Customer( name=payload.name, company=payload.company, subdomain=payload.subdomain, email=payload.email, max_devices=payload.max_devices, notes=payload.notes, status="deploying", ) db.add(customer) db.commit() db.refresh(customer) logger.info("Customer %d (%s) created by %s.", customer.id, customer.subdomain, current_user.username) # Deploy in background result = await netbird_service.deploy_customer(db, customer.id) response = customer.to_dict() response["deployment"] = result return response @router.get("") async def list_customers( page: int = Query(default=1, ge=1), per_page: int = Query(default=25, ge=1, le=100), search: Optional[str] = Query(default=None), status_filter: Optional[str] = Query(default=None, alias="status"), current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """List customers with pagination, search, and status filter. Args: page: Page number (1-indexed). per_page: Items per page. search: Search in name, subdomain, email. status_filter: Filter by status. Returns: Paginated customer list with metadata. """ query = db.query(Customer) if search: like_term = f"%{search}%" query = query.filter( (Customer.name.ilike(like_term)) | (Customer.subdomain.ilike(like_term)) | (Customer.email.ilike(like_term)) | (Customer.company.ilike(like_term)) ) if status_filter: query = query.filter(Customer.status == status_filter) total = query.count() customers = ( query.order_by(Customer.created_at.desc()) .offset((page - 1) * per_page) .limit(per_page) .all() ) items = [] for c in customers: data = c.to_dict() if c.deployment: data["deployment"] = c.deployment.to_dict() else: data["deployment"] = None items.append(data) return { "items": items, "total": total, "page": page, "per_page": per_page, "pages": (total + per_page - 1) // per_page if total > 0 else 1, } @router.get("/{customer_id}") async def get_customer( customer_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Get detailed customer information including deployment and logs. Args: customer_id: Customer ID. Returns: Customer dict with deployment info and recent logs. """ customer = db.query(Customer).filter(Customer.id == customer_id).first() if not customer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.", ) data = customer.to_dict() data["deployment"] = customer.deployment.to_dict() if customer.deployment else None data["logs"] = [ log.to_dict() for log in db.query(DeploymentLog) .filter(DeploymentLog.customer_id == customer_id) .order_by(DeploymentLog.created_at.desc()) .limit(50) .all() ] return data @router.put("/{customer_id}") async def update_customer( customer_id: int, payload: CustomerUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Update customer information. Args: customer_id: Customer ID. payload: Fields to update. Returns: Updated customer dict. """ customer = db.query(Customer).filter(Customer.id == customer_id).first() if not customer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.", ) update_data = payload.model_dump(exclude_none=True) for field, value in update_data.items(): if hasattr(customer, field): setattr(customer, field, value) customer.updated_at = datetime.utcnow() db.commit() db.refresh(customer) logger.info("Customer %d updated by %s.", customer_id, current_user.username) return customer.to_dict() @router.delete("/{customer_id}") async def delete_customer( customer_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): """Delete a customer and clean up all resources. Removes containers, NPM proxy, instance directory, and database records. Args: customer_id: Customer ID. Returns: Confirmation message. """ customer = db.query(Customer).filter(Customer.id == customer_id).first() if not customer: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.", ) # Undeploy first (containers, NPM, files) await netbird_service.undeploy_customer(db, customer_id) # Delete customer record (cascades to deployment + logs) db.delete(customer) db.commit() logger.info("Customer %d deleted by %s.", customer_id, current_user.username) return {"message": f"Customer {customer_id} deleted successfully."}