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", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) api := r.Group("/api") v1 := api.Group("/v1") v1.GET("/time", func(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, }) }) // GET /v1/current v1.GET("/current", 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) }) // POST /v1/current (strict HH:MM:SS already enforced at service layer) type setCurrentReq struct { ID int64 `json:"id" binding:"required"` StartTime string `json:"start_time" binding:"required"` } v1.POST("/current", 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) }) // POST /v1/archive (move one or many IDs from current → current_archive) type moveReq struct { ID *int64 `json:"id,omitempty"` IDs []int64 `json:"ids,omitempty"` } v1.POST("/archive", 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), }) }) // GET /v1/shows — list all rows from "current" v1.GET("/shows", 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) }) // DELETE /v1/shows/:id — delete a row from "current" by id v1.DELETE("/shows/:id", func(c *gin.Context) { idStr := c.Param("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) }) return r }