package httpapi import ( "errors" "net/http" "strconv" "time" "watch-party-backend/internal/repo" "watch-party-backend/internal/service" "github.com/gin-gonic/gin" ) func NewRouter(svc service.EpisodeService) *gin.Engine { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) r.GET("healthz", healthzHandler) api := r.Group("/api") v1 := api.Group("/v1") v1.GET("/time", timeHandler) v1.GET("/current", getCurrentHandler(svc)) v1.POST("/current", setCurrentHandler(svc)) v1.POST("/archive", moveToArchiveHandler(svc)) v1.GET("/shows", listShowsHandler(svc)) v1.DELETE("/shows", deleteShowsHandler(svc)) return r } // GET /healthz func healthzHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) } // GET /v1/time func timeHandler(c *gin.Context) { now := time.Now().UTC().UnixMilli() c.Header("Cache-Control", "no-store, max-age=0, must-revalidate") c.JSON(http.StatusOK, gin.H{ "now": now, }) } // getCurrentHandler godoc // @Summary Get current show // @Description Returns the current row from `current` table. // @Tags current // @Produce json // @Success 200 {object} CurrentResponse // @Failure 404 {object} HTTPError "no current row found" // @Failure 500 {object} HTTPError "query failed" // @Router /api/v1/current [get] func getCurrentHandler(svc service.EpisodeService) gin.HandlerFunc { return func(c *gin.Context) { cur, err := svc.GetCurrent(c.Request.Context()) if err != nil { if err == repo.ErrNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "no current row found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) return } c.JSON(http.StatusOK, cur) } } // setCurrentHandler godoc // @Summary Set current show // @Description Set the current show ID and start time. // @Tags current // @Accept json // @Produce json // @Param body body SetCurrentReq true "Current show payload" // @Success 200 {object} CurrentResponse // @Failure 400 {object} HTTPError "invalid payload or invalid time" // @Failure 404 {object} HTTPError "id not found" // @Failure 500 {object} HTTPError "update failed" // @Router /api/v1/current [post] func setCurrentHandler(svc service.EpisodeService) gin.HandlerFunc { return func(c *gin.Context) { var req SetCurrentReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } cur, err := svc.SetCurrent(c.Request.Context(), req.ID, req.StartTime) if err != nil { switch err { case service.ErrInvalidTime: c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return case repo.ErrNotFound: c.JSON(http.StatusNotFound, gin.H{"error": "id not found"}) return default: c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"}) return } } c.JSON(http.StatusOK, cur) } } // moveToArchiveHandler godoc // @Summary Move shows to archive // @Description Move one or many IDs from `current` to `current_archive`. // @Tags archive // @Accept json // @Produce json // @Param body body MoveReq true "IDs to move" // @Success 200 {object} MoveRes // @Failure 400 {object} HTTPError "empty ids" // @Failure 500 {object} HTTPError "move failed" // @Router /api/v1/archive [post] func moveToArchiveHandler(svc service.EpisodeService) gin.HandlerFunc { return func(c *gin.Context) { var req MoveReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } ids := make([]int64, 0, len(req.IDs)+1) if req.ID != nil { ids = append(ids, *req.ID) } ids = append(ids, req.IDs...) res, err := svc.MoveToArchive(c.Request.Context(), ids) if err != nil { if err == service.ErrEmptyIDs { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "move failed"}) return } c.JSON(http.StatusOK, gin.H{ "moved_ids": res.MovedIDs, "deleted_ids": res.DeletedIDs, "skipped_ids": res.SkippedIDs, "inserted": len(res.MovedIDs), "deleted": len(res.DeletedIDs), "skipped": len(res.SkippedIDs), }) } } // listShowsHandler godoc // @Summary List current shows // @Description List all rows from `current` table. // @Tags shows // @Produce json // @Success 200 {array} CurrentResponse // @Failure 500 {object} HTTPError "list failed" // @Router /api/v1/shows [get] func listShowsHandler(svc service.EpisodeService) gin.HandlerFunc { return func(c *gin.Context) { items, err := svc.ListAll(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "list failed"}) return } c.JSON(http.StatusOK, items) } } // deleteShowHandler godoc // @Summary Delete show // @Description Delete a row from `current` by ID. // @Tags shows // @Produce json // @Param id query int64 true "Show ID" // @Success 204 "No Content" // @Failure 400 {object} HTTPError "invalid id" // @Failure 404 {object} HTTPError "id not found" // @Failure 500 {object} HTTPError "delete failed" // @Router /api/v1/shows [delete] func deleteShowsHandler(svc service.EpisodeService) gin.HandlerFunc { return func(c *gin.Context) { idStr := c.Query("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } err = svc.Delete(c.Request.Context(), id) if err != nil { if errors.Is(err, repo.ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "id not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"}) return } c.Status(http.StatusNoContent) } }