package handlers import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/services" ) // QueueSessionHandler handles collaborative queue session HTTP requests (v0.203 Lot D1) type QueueSessionHandler struct { svc *services.QueueSessionService logger *zap.Logger } // NewQueueSessionHandler creates a new QueueSessionHandler func NewQueueSessionHandler(svc *services.QueueSessionService, logger *zap.Logger) *QueueSessionHandler { return &QueueSessionHandler{svc: svc, logger: logger} } // CreateSession creates a new shared queue session // @Summary Create collaborative queue session // @Description Creates a shared queue session and returns its share token + URL. The session creator is recorded as host. // @Tags Queue // @Accept json // @Produce json // @Security BearerAuth // @Success 201 {object} APIResponse{data=object{session=object,share_token=string,share_url=string}} "Created session" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 500 {object} APIResponse "Internal Error" // @Router /queue/session [post] func (h *QueueSessionHandler) CreateSession(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } session, err := h.svc.CreateSession(c.Request.Context(), userID) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create session", err)) return } RespondSuccess(c, http.StatusCreated, gin.H{ "session": session, "share_token": session.ShareToken, "share_url": "/queue?session=" + session.ShareToken, }) } // GetSession returns a session's queue (auth optional for joining via link) // @Summary Get queue session by share token // @Description Returns the session metadata + items for a given share token. No auth required (public — anyone with the link can join). // @Tags Queue // @Accept json // @Produce json // @Param token path string true "Session share token" // @Success 200 {object} APIResponse{data=object{session=object,items=[]object}} "Session and queue items" // @Failure 400 {object} APIResponse "Token required" // @Failure 404 {object} APIResponse "Session not found" // @Failure 500 {object} APIResponse "Internal Error" // @Router /queue/session/{token} [get] func (h *QueueSessionHandler) GetSession(c *gin.Context) { token := c.Param("token") if token == "" { RespondWithAppError(c, apperrors.NewValidationError("token is required")) return } session, items, err := h.svc.GetSessionByToken(c.Request.Context(), token) if err != nil { if errors.Is(err, services.ErrQueueSessionNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("session not found")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get session", err)) return } // Map items to response with tracks type itemResp struct { ID string `json:"id"` Position int `json:"position"` AddedAt string `json:"added_at"` Track interface{} `json:"track,omitempty"` } respItems := make([]itemResp, 0, len(items)) for _, it := range items { r := itemResp{ ID: it.ID.String(), Position: it.Position, AddedAt: it.AddedAt.Format("2006-01-02T15:04:05Z07:00"), } if it.Track.ID != uuid.Nil { r.Track = it.Track } respItems = append(respItems, r) } RespondSuccess(c, http.StatusOK, gin.H{ "session": session, "items": respItems, }) } // DeleteSession deletes a session (creator only) // @Summary Delete queue session // @Description Deletes a collaborative queue session. Only the original session creator can delete. // @Tags Queue // @Accept json // @Produce json // @Security BearerAuth // @Param token path string true "Session share token" // @Success 200 {object} APIResponse{data=object{message=string}} "Session deleted" // @Failure 400 {object} APIResponse "Token required" // @Failure 401 {object} APIResponse "Unauthorized" // @Failure 403 {object} APIResponse "Only the creator can delete this session" // @Failure 404 {object} APIResponse "Session not found" // @Router /queue/session/{token} [delete] func (h *QueueSessionHandler) DeleteSession(c *gin.Context) { userID, ok := GetUserIDUUID(c) if !ok { return } token := c.Param("token") if token == "" { RespondWithAppError(c, apperrors.NewValidationError("token is required")) return } if err := h.svc.DeleteSession(c.Request.Context(), token, userID); err != nil { if errors.Is(err, services.ErrQueueSessionNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("session not found")) return } if errors.Is(err, services.ErrQueueSessionForbidden) { RespondWithAppError(c, apperrors.NewForbiddenError("only the creator can delete this session")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete session", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "session deleted"}) } // AddToSessionRequest represents the request body type AddToSessionRequest struct { TrackID uuid.UUID `json:"track_id" binding:"required"` } // AddToSession adds a track to a session's queue // @Summary Add track to queue session // @Description Adds a track to a collaborative queue session. Anyone with the share token can add. // @Tags Queue // @Accept json // @Produce json // @Security BearerAuth // @Param token path string true "Session share token" // @Param request body AddToSessionRequest true "Track to enqueue" // @Success 201 {object} APIResponse{data=object{session=object,items=[]object}} "Updated session and items" // @Failure 400 {object} APIResponse "Validation error" // @Failure 404 {object} APIResponse "Session not found" // @Failure 500 {object} APIResponse "Internal Error" // @Router /queue/session/{token}/items [post] func (h *QueueSessionHandler) AddToSession(c *gin.Context) { token := c.Param("token") if token == "" { RespondWithAppError(c, apperrors.NewValidationError("token is required")) return } var req AddToSessionRequest if err := c.ShouldBindJSON(&req); err != nil { RespondWithAppError(c, apperrors.NewValidationError("track_id is required")) return } if err := h.svc.AddToSession(c.Request.Context(), token, req.TrackID); err != nil { if errors.Is(err, services.ErrQueueSessionNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("session not found")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add to session", err)) return } // Return updated session session, items, _ := h.svc.GetSessionByToken(c.Request.Context(), token) RespondSuccess(c, http.StatusCreated, gin.H{"session": session, "items": items}) } // RemoveFromSession removes an item from a session's queue // @Summary Remove item from queue session // @Description Removes an item from a collaborative queue session by item ID // @Tags Queue // @Accept json // @Produce json // @Security BearerAuth // @Param token path string true "Session share token" // @Param id path string true "Queue item ID (UUID)" // @Success 200 {object} APIResponse{data=object{message=string}} "Item removed" // @Failure 400 {object} APIResponse "Validation error" // @Failure 404 {object} APIResponse "Session or item not found" // @Failure 500 {object} APIResponse "Internal Error" // @Router /queue/session/{token}/items/{id} [delete] func (h *QueueSessionHandler) RemoveFromSession(c *gin.Context) { token := c.Param("token") if token == "" { RespondWithAppError(c, apperrors.NewValidationError("token is required")) return } itemID, err := uuid.Parse(c.Param("id")) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid item ID")) return } if err := h.svc.RemoveFromSession(c.Request.Context(), token, itemID); err != nil { if errors.Is(err, services.ErrQueueSessionNotFound) { RespondWithAppError(c, apperrors.NewNotFoundError("session or item not found")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove from session", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "item removed"}) }