package handlers import ( "net/http" "strconv" "github.com/breakpilot/edu-search-service/internal/orchestrator" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // AudienceHandler handles audience-related HTTP requests type AudienceHandler struct { repo orchestrator.AudienceRepository } // NewAudienceHandler creates a new audience handler func NewAudienceHandler(repo orchestrator.AudienceRepository) *AudienceHandler { return &AudienceHandler{repo: repo} } // CreateAudienceRequest represents a request to create an audience type CreateAudienceRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Filters orchestrator.AudienceFilters `json:"filters"` CreatedBy string `json:"created_by"` } // UpdateAudienceRequest represents a request to update an audience type UpdateAudienceRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description"` Filters orchestrator.AudienceFilters `json:"filters"` IsActive bool `json:"is_active"` } // CreateExportRequest represents a request to create an export type CreateExportRequest struct { ExportType string `json:"export_type" binding:"required"` // csv, json, email_list Purpose string `json:"purpose"` ExportedBy string `json:"exported_by"` } // ListAudiences returns all audiences func (h *AudienceHandler) ListAudiences(c *gin.Context) { activeOnly := c.Query("active_only") == "true" audiences, err := h.repo.ListAudiences(c.Request.Context(), activeOnly) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list audiences", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "audiences": audiences, "count": len(audiences), }) } // GetAudience returns a single audience func (h *AudienceHandler) GetAudience(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } audience, err := h.repo.GetAudience(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Audience not found", "details": err.Error()}) return } c.JSON(http.StatusOK, audience) } // CreateAudience creates a new audience func (h *AudienceHandler) CreateAudience(c *gin.Context) { var req CreateAudienceRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } audience := &orchestrator.Audience{ Name: req.Name, Description: req.Description, Filters: req.Filters, CreatedBy: req.CreatedBy, IsActive: true, } if err := h.repo.CreateAudience(c.Request.Context(), audience); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audience", "details": err.Error()}) return } // Update the member count count, _ := h.repo.UpdateAudienceCount(c.Request.Context(), audience.ID) audience.MemberCount = count c.JSON(http.StatusCreated, audience) } // UpdateAudience updates an existing audience func (h *AudienceHandler) UpdateAudience(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } var req UpdateAudienceRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } audience := &orchestrator.Audience{ ID: id, Name: req.Name, Description: req.Description, Filters: req.Filters, IsActive: req.IsActive, } if err := h.repo.UpdateAudience(c.Request.Context(), audience); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update audience", "details": err.Error()}) return } // Update the member count count, _ := h.repo.UpdateAudienceCount(c.Request.Context(), audience.ID) audience.MemberCount = count c.JSON(http.StatusOK, audience) } // DeleteAudience soft-deletes an audience func (h *AudienceHandler) DeleteAudience(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } if err := h.repo.DeleteAudience(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete audience", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"deleted": true, "id": idStr}) } // GetAudienceMembers returns members matching the audience filters func (h *AudienceHandler) GetAudienceMembers(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } // Parse pagination limit := 50 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 500 { limit = parsed } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } members, totalCount, err := h.repo.GetAudienceMembers(c.Request.Context(), id, limit, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get members", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "members": members, "count": len(members), "total_count": totalCount, "limit": limit, "offset": offset, }) } // RefreshAudienceCount recalculates the member count func (h *AudienceHandler) RefreshAudienceCount(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } count, err := h.repo.UpdateAudienceCount(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh count", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "audience_id": idStr, "member_count": count, }) } // PreviewAudienceFilters previews the result of filters without saving func (h *AudienceHandler) PreviewAudienceFilters(c *gin.Context) { var filters orchestrator.AudienceFilters if err := c.ShouldBindJSON(&filters); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } // Return the filters for now - preview functionality can be expanded later c.JSON(http.StatusOK, gin.H{ "filters": filters, "message": "Preview functionality requires direct repository access", }) } // CreateExport creates a new export for an audience func (h *AudienceHandler) CreateExport(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } var req CreateExportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) return } // Get the member count for the export _, totalCount, err := h.repo.GetAudienceMembers(c.Request.Context(), id, 1, 0) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get members", "details": err.Error()}) return } export := &orchestrator.AudienceExport{ AudienceID: id, ExportType: req.ExportType, RecordCount: totalCount, ExportedBy: req.ExportedBy, Purpose: req.Purpose, } if err := h.repo.CreateExport(c.Request.Context(), export); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export", "details": err.Error()}) return } c.JSON(http.StatusCreated, export) } // ListExports lists exports for an audience func (h *AudienceHandler) ListExports(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"}) return } exports, err := h.repo.ListExports(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list exports", "details": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "exports": exports, "count": len(exports), }) } // SetupAudienceRoutes configures audience API routes func SetupAudienceRoutes(r *gin.RouterGroup, h *AudienceHandler) { audiences := r.Group("/audiences") { // Audience CRUD audiences.GET("", h.ListAudiences) audiences.GET("/:id", h.GetAudience) audiences.POST("", h.CreateAudience) audiences.PUT("/:id", h.UpdateAudience) audiences.DELETE("/:id", h.DeleteAudience) // Members audiences.GET("/:id/members", h.GetAudienceMembers) audiences.POST("/:id/refresh", h.RefreshAudienceCount) // Exports audiences.GET("/:id/exports", h.ListExports) audiences.POST("/:id/exports", h.CreateExport) // Preview (no audience required) audiences.POST("/preview", h.PreviewAudienceFilters) } }