package routes import ( "encoding/json" "fmt" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/felt-app/felt/internal/seating" "github.com/felt-app/felt/internal/server/middleware" ) // TableHandler handles table, seating, balancing, and break table API routes. type TableHandler struct { tables *seating.TableService balance *seating.BalanceEngine breakTable *seating.BreakTableService blueprints *seating.BlueprintService } // NewTableHandler creates a new table route handler. func NewTableHandler( tables *seating.TableService, balance *seating.BalanceEngine, breakTable *seating.BreakTableService, blueprints *seating.BlueprintService, ) *TableHandler { return &TableHandler{ tables: tables, balance: balance, breakTable: breakTable, blueprints: blueprints, } } // RegisterRoutes registers table and seating routes on the given router. func (h *TableHandler) RegisterRoutes(r chi.Router) { // Tournament-scoped routes r.Route("/tournaments/{id}", func(r chi.Router) { // Read-only (any authenticated user) r.Get("/tables", h.handleGetTables) r.Get("/balance", h.handleCheckBalance) // Mutation routes (admin or floor) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleFloor)) // Table management r.Post("/tables", h.handleCreateTable) r.Post("/tables/from-blueprint", h.handleCreateFromBlueprint) r.Post("/tables/save-blueprint", h.handleSaveBlueprint) r.Put("/tables/{tableId}", h.handleUpdateTable) r.Delete("/tables/{tableId}", h.handleDeactivateTable) // Seating r.Post("/players/{playerId}/auto-seat", h.handleAutoSeat) r.Post("/players/{playerId}/seat", h.handleAssignSeat) r.Post("/seats/swap", h.handleSwapSeats) // Balancing r.Post("/balance/suggest", h.handleSuggestMoves) r.Post("/balance/suggestions/{suggId}/accept", h.handleAcceptSuggestion) r.Post("/balance/suggestions/{suggId}/cancel", h.handleCancelSuggestion) // Break Table r.Post("/tables/{tableId}/break", h.handleBreakTable) // Dealer Button r.Post("/tables/{tableId}/button", h.handleSetButton) r.Post("/tables/{tableId}/button/advance", h.handleAdvanceButton) // Hand-for-Hand r.Post("/hand-for-hand", h.handleSetHandForHand) r.Post("/tables/{tableId}/hand-complete", h.handleTableHandComplete) }) }) // Venue-level blueprint routes r.Route("/blueprints", func(r chi.Router) { r.Get("/", h.handleListBlueprints) r.Get("/{id}", h.handleGetBlueprint) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", h.handleCreateBlueprint) r.Put("/{id}", h.handleUpdateBlueprint) r.Delete("/{id}", h.handleDeleteBlueprint) }) }) } // ---------- Table Handlers ---------- func (h *TableHandler) handleGetTables(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tables, err := h.tables.GetTables(r.Context(), tournamentID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, tables) } type createTableRequest struct { Name string `json:"name"` SeatCount int `json:"seat_count"` } func (h *TableHandler) handleCreateTable(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") var req createTableRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } table, err := h.tables.CreateTable(r.Context(), tournamentID, req.Name, req.SeatCount) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, table) } type fromBlueprintRequest struct { BlueprintID int `json:"blueprint_id"` } func (h *TableHandler) handleCreateFromBlueprint(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") var req fromBlueprintRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } tables, err := h.blueprints.CreateTablesFromBlueprint(r.Context(), h.tables.DB(), tournamentID, req.BlueprintID) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, tables) } func (h *TableHandler) handleUpdateTable(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tableID, err := strconv.Atoi(chi.URLParam(r, "tableId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid table id") return } var req createTableRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := h.tables.UpdateTable(r.Context(), tournamentID, tableID, req.Name, req.SeatCount); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (h *TableHandler) handleDeactivateTable(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tableID, err := strconv.Atoi(chi.URLParam(r, "tableId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid table id") return } if err := h.tables.DeactivateTable(r.Context(), tournamentID, tableID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deactivated"}) } // ---------- Seating Handlers ---------- func (h *TableHandler) handleAutoSeat(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") playerID := chi.URLParam(r, "playerId") assignment, err := h.tables.AutoAssignSeat(r.Context(), tournamentID, playerID) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, assignment) } type assignSeatRequest struct { TableID int `json:"table_id"` SeatPosition int `json:"seat_position"` } func (h *TableHandler) handleAssignSeat(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") playerID := chi.URLParam(r, "playerId") var req assignSeatRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := h.tables.AssignSeat(r.Context(), tournamentID, playerID, req.TableID, req.SeatPosition); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "seated"}) } type swapSeatsRequest struct { Player1ID string `json:"player1_id"` Player2ID string `json:"player2_id"` } func (h *TableHandler) handleSwapSeats(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") var req swapSeatsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := h.tables.SwapSeats(r.Context(), tournamentID, req.Player1ID, req.Player2ID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "swapped"}) } // ---------- Balance Handlers ---------- func (h *TableHandler) handleCheckBalance(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") status, err := h.balance.CheckBalance(r.Context(), tournamentID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, status) } func (h *TableHandler) handleSuggestMoves(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") suggestions, err := h.balance.SuggestMoves(r.Context(), tournamentID) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "suggestions": suggestions, }) } type acceptSuggestionRequest struct { FromSeat int `json:"from_seat"` ToSeat int `json:"to_seat"` } func (h *TableHandler) handleAcceptSuggestion(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") suggID, err := strconv.Atoi(chi.URLParam(r, "suggId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid suggestion id") return } var req acceptSuggestionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := h.balance.AcceptSuggestion(r.Context(), tournamentID, suggID, req.FromSeat, req.ToSeat); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "accepted"}) } func (h *TableHandler) handleCancelSuggestion(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") suggID, err := strconv.Atoi(chi.URLParam(r, "suggId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid suggestion id") return } if err := h.balance.CancelSuggestion(r.Context(), tournamentID, suggID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "cancelled"}) } // ---------- Break Table Handler ---------- func (h *TableHandler) handleBreakTable(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tableID, err := strconv.Atoi(chi.URLParam(r, "tableId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid table id") return } result, err := h.breakTable.BreakTable(r.Context(), tournamentID, tableID) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, result) } // ---------- Dealer Button Handlers ---------- type setButtonRequest struct { Position int `json:"position"` } func (h *TableHandler) handleSetButton(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tableID, err := strconv.Atoi(chi.URLParam(r, "tableId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid table id") return } var req setButtonRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := h.tables.SetDealerButton(r.Context(), tournamentID, tableID, req.Position); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "button_set"}) } func (h *TableHandler) handleAdvanceButton(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tableID, err := strconv.Atoi(chi.URLParam(r, "tableId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid table id") return } if err := h.tables.AdvanceDealerButton(r.Context(), tournamentID, tableID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "button_advanced"}) } // ---------- Hand-for-Hand Handlers ---------- type handForHandRequest struct { Enabled bool `json:"enabled"` } func (h *TableHandler) handleSetHandForHand(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") var req handForHandRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if err := h.tables.SetHandForHand(r.Context(), tournamentID, req.Enabled); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{ "status": fmt.Sprintf("hand_for_hand_%s", boolStr(req.Enabled)), }) } func (h *TableHandler) handleTableHandComplete(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") tableID, err := strconv.Atoi(chi.URLParam(r, "tableId")) if err != nil { writeError(w, http.StatusBadRequest, "invalid table id") return } if err := h.tables.TableHandComplete(r.Context(), tournamentID, tableID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "hand_complete"}) } // ---------- Blueprint Handlers ---------- func (h *TableHandler) handleListBlueprints(w http.ResponseWriter, r *http.Request) { blueprints, err := h.blueprints.ListBlueprints(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, blueprints) } func (h *TableHandler) handleGetBlueprint(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid blueprint id") return } bp, err := h.blueprints.GetBlueprint(r.Context(), id) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, bp) } type createBlueprintRequest struct { Name string `json:"name"` TableConfigs []seating.BlueprintTableConfig `json:"table_configs"` } func (h *TableHandler) handleCreateBlueprint(w http.ResponseWriter, r *http.Request) { var req createBlueprintRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } bp, err := h.blueprints.CreateBlueprint(r.Context(), req.Name, req.TableConfigs) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, bp) } func (h *TableHandler) handleUpdateBlueprint(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid blueprint id") return } var req createBlueprintRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } bp := seating.Blueprint{ ID: id, Name: req.Name, TableConfigs: req.TableConfigs, } if err := h.blueprints.UpdateBlueprint(r.Context(), bp); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (h *TableHandler) handleDeleteBlueprint(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(chi.URLParam(r, "id")) if err != nil { writeError(w, http.StatusBadRequest, "invalid blueprint id") return } if err := h.blueprints.DeleteBlueprint(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } type saveBlueprintRequest struct { Name string `json:"name"` } func (h *TableHandler) handleSaveBlueprint(w http.ResponseWriter, r *http.Request) { tournamentID := chi.URLParam(r, "id") var req saveBlueprintRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } bp, err := h.blueprints.SaveBlueprintFromTournament(r.Context(), h.tables.DB(), tournamentID, req.Name) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, bp) } // boolStr returns "enabled" or "disabled" for a bool value. func boolStr(b bool) string { if b { return "enabled" } return "disabled" }