package routes import ( "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/felt-app/felt/internal/financial" "github.com/felt-app/felt/internal/server/middleware" "github.com/felt-app/felt/internal/tournament" ) // TournamentHandler handles tournament lifecycle, state, and deal API routes. type TournamentHandler struct { service *tournament.Service multi *tournament.MultiManager chop *financial.ChopEngine } // NewTournamentHandler creates a new tournament route handler. func NewTournamentHandler( service *tournament.Service, multi *tournament.MultiManager, chop *financial.ChopEngine, ) *TournamentHandler { return &TournamentHandler{ service: service, multi: multi, chop: chop, } } // RegisterRoutes registers tournament routes on the given router. func (h *TournamentHandler) RegisterRoutes(r chi.Router) { r.Route("/tournaments", func(r chi.Router) { // Read-only (any authenticated user) r.Get("/", h.handleListTournaments) r.Get("/active", h.handleListActive) // Mutations (admin role for create) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleAdmin)) r.Post("/", h.handleCreateFromTemplate) r.Post("/manual", h.handleCreateManual) }) // Tournament-scoped routes r.Route("/{id}", func(r chi.Router) { // Read-only r.Get("/", h.handleGetTournament) r.Get("/state", h.handleGetState) r.Get("/activity", h.handleGetActivity) // Deal/chop read-only r.Get("/deal/proposals", h.handleListDealProposals) // Mutations (floor or admin) r.Group(func(r chi.Router) { r.Use(middleware.RequireRole(middleware.RoleFloor)) r.Post("/start", h.handleStart) r.Post("/pause", h.handlePause) r.Post("/resume", h.handleResume) r.Post("/cancel", h.handleCancel) // Deal/chop mutations r.Post("/deal/propose", h.handleProposeDeal) r.Post("/deal/proposals/{proposalId}/confirm", h.handleConfirmDeal) r.Post("/deal/proposals/{proposalId}/cancel", h.handleCancelDeal) }) }) }) } // ---------- Tournament CRUD ---------- type createFromTemplateRequest struct { TemplateID int64 `json:"template_id"` Name string `json:"name,omitempty"` Overrides tournament.TournamentOverrides `json:"overrides"` } func (h *TournamentHandler) handleCreateFromTemplate(w http.ResponseWriter, r *http.Request) { var req createFromTemplateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if req.TemplateID == 0 { writeError(w, http.StatusBadRequest, "template_id is required") return } // Allow name at top level or in overrides if req.Name != "" && req.Overrides.Name == "" { req.Overrides.Name = req.Name } t, err := h.service.CreateFromTemplate(r.Context(), req.TemplateID, req.Overrides) if err != nil { status := http.StatusBadRequest if err == tournament.ErrTemplateNotFound { status = http.StatusNotFound } writeError(w, status, err.Error()) return } writeJSON(w, http.StatusCreated, t) } func (h *TournamentHandler) handleCreateManual(w http.ResponseWriter, r *http.Request) { var config tournament.TournamentConfig if err := json.NewDecoder(r.Body).Decode(&config); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } t, err := h.service.CreateManual(r.Context(), config) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusCreated, t) } func (h *TournamentHandler) handleListTournaments(w http.ResponseWriter, r *http.Request) { summaries, err := h.multi.ListAllTournaments(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, summaries) } func (h *TournamentHandler) handleListActive(w http.ResponseWriter, r *http.Request) { summaries, err := h.multi.ListActiveTournaments(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, summaries) } func (h *TournamentHandler) handleGetTournament(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") detail, err := h.service.GetTournament(r.Context(), id) if err != nil { if err == tournament.ErrTournamentNotFound { writeError(w, http.StatusNotFound, err.Error()) return } writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, detail) } func (h *TournamentHandler) handleGetState(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") state, err := h.service.GetTournamentState(r.Context(), id) if err != nil { if err == tournament.ErrTournamentNotFound { writeError(w, http.StatusNotFound, err.Error()) return } writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, state) } func (h *TournamentHandler) handleGetActivity(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") limitStr := r.URL.Query().Get("limit") limit := 20 if limitStr != "" { if v, err := strconv.Atoi(limitStr); err == nil && v > 0 && v <= 100 { limit = v } } activity := h.service.BuildActivityFeed(r.Context(), id, limit) writeJSON(w, http.StatusOK, map[string]interface{}{ "activity": activity, }) } // ---------- Tournament Lifecycle ---------- func (h *TournamentHandler) handleStart(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.service.StartTournament(r.Context(), id); err != nil { status := http.StatusBadRequest if err == tournament.ErrTournamentNotFound { status = http.StatusNotFound } writeError(w, status, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "started"}) } func (h *TournamentHandler) handlePause(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.service.PauseTournament(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "paused"}) } func (h *TournamentHandler) handleResume(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.service.ResumeTournament(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "resumed"}) } func (h *TournamentHandler) handleCancel(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if err := h.service.CancelTournament(r.Context(), id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "cancelled"}) } // ---------- Deal/Chop Routes ---------- type proposeDealRequest struct { Type string `json:"type"` // icm, chip_chop, even_chop, custom, partial_chop PlayerStacks map[string]int64 `json:"stacks,omitempty"` CustomAmounts map[string]int64 `json:"custom_amounts,omitempty"` PartialPool int64 `json:"partial_pool,omitempty"` RemainingPool int64 `json:"remaining_pool,omitempty"` } func (h *TournamentHandler) handleProposeDeal(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") var req proposeDealRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error()) return } if h.chop == nil { writeError(w, http.StatusInternalServerError, "chop engine not available") return } params := financial.DealParams{ PlayerStacks: req.PlayerStacks, CustomAmounts: req.CustomAmounts, PartialPool: req.PartialPool, RemainingPool: req.RemainingPool, } proposal, err := h.chop.ProposeDeal(r.Context(), id, req.Type, params) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, proposal) } func (h *TournamentHandler) handleConfirmDeal(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") proposalID := chi.URLParam(r, "proposalId") if h.chop == nil { writeError(w, http.StatusInternalServerError, "chop engine not available") return } if err := h.chop.ConfirmDeal(r.Context(), id, proposalID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "confirmed"}) } func (h *TournamentHandler) handleCancelDeal(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") proposalID := chi.URLParam(r, "proposalId") if h.chop == nil { writeError(w, http.StatusInternalServerError, "chop engine not available") return } if err := h.chop.CancelDeal(r.Context(), id, proposalID); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "cancelled"}) } func (h *TournamentHandler) handleListDealProposals(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "id") if h.chop == nil { writeJSON(w, http.StatusOK, []interface{}{}) return } proposals, err := h.chop.ListProposals(r.Context(), id) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, proposals) }