package handlers import ( "context" "fmt" "net/http" "strconv" "strings" "time" "game-admin/internal/models" "github.com/gin-gonic/gin" "github.com/uptrace/bun" ) func (h *GameHandler) ListVariables(c *gin.Context) { gameID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"}) return } exists, err := h.gameExists(c, gameID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "Game not found"}) return } hasModule, err := h.gameHasModule(c, gameID, "variables") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !hasModule { c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"}) return } var variables []models.GameVariable err = h.db.NewSelect(). Model(&variables). Relation("Items", func(q *bun.SelectQuery) *bun.SelectQuery { return q.OrderExpr("gvi.id ASC") }). Where("gv.game_id = ?", gameID). OrderExpr("gv.id DESC"). Scan(c) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } for i := range variables { normalizeGameVariableResponse(&variables[i]) } c.JSON(http.StatusOK, variables) } func (h *GameHandler) CreateVariable(c *gin.Context) { gameID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"}) return } var req models.UpsertGameVariableRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateUpsertGameVariableRequest(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } exists, err := h.gameExists(c, gameID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !exists { c.JSON(http.StatusNotFound, gin.H{"error": "Game not found"}) return } hasModule, err := h.gameHasModule(c, gameID, "variables") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !hasModule { c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"}) return } tx, err := h.db.BeginTx(c, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer tx.Rollback() now := time.Now() variable := &models.GameVariable{ GameID: gameID, Key: strings.TrimSpace(req.Key), Type: req.Type, NumberValue: req.NumberValue, StringValue: req.StringValue, TableValueType: req.TableValueType, CreatedAt: now, UpdatedAt: now, } _, err = tx.NewInsert().Model(variable).Exec(c) if err != nil { c.JSON(http.StatusConflict, gin.H{"error": "Game variable key already exists"}) return } if err := insertGameVariableItems(c, tx, variable.ID, req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := tx.Commit(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } created, err := h.getGameVariable(c, gameID, variable.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, created) } func (h *GameHandler) UpdateVariable(c *gin.Context) { gameID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"}) return } variableID, err := strconv.ParseInt(c.Param("var_id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid variable id"}) return } var req models.UpsertGameVariableRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := validateUpsertGameVariableRequest(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } hasModule, err := h.gameHasModule(c, gameID, "variables") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !hasModule { c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"}) return } existing, err := h.getGameVariable(c, gameID, variableID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Game variable not found"}) return } tx, err := h.db.BeginTx(c, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer tx.Rollback() numberValue, stringValue, tableValueType := dbValuesFromVariableRequest(req) _, err = tx.NewUpdate(). Model((*models.GameVariable)(nil)). Set(`"key" = ?`, strings.TrimSpace(req.Key)). Set("type = ?", req.Type). Set("number_value = ?", numberValue). Set("string_value = ?", stringValue). Set("table_value_type = ?", tableValueType). Set("updated_at = ?", time.Now()). Where("id = ? AND game_id = ?", existing.ID, gameID). Exec(c) if err != nil { c.JSON(http.StatusConflict, gin.H{"error": "Could not update game variable"}) return } _, err = tx.NewDelete(). Model((*models.GameVariableItem)(nil)). Where("game_variable_id = ?", existing.ID). Exec(c) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := insertGameVariableItems(c, tx, existing.ID, req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := tx.Commit(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } updated, err := h.getGameVariable(c, gameID, existing.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, updated) } func (h *GameHandler) DeleteVariable(c *gin.Context) { gameID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid game id"}) return } variableID, err := strconv.ParseInt(c.Param("var_id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid variable id"}) return } existing, err := h.getGameVariable(c, gameID, variableID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Game variable not found"}) return } hasModule, err := h.gameHasModule(c, gameID, "variables") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !hasModule { c.JSON(http.StatusForbidden, gin.H{"error": "Variables module is not enabled for this game"}) return } tx, err := h.db.BeginTx(c, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer tx.Rollback() _, err = tx.NewDelete(). Model((*models.GameVariableItem)(nil)). Where("game_variable_id = ?", existing.ID). Exec(c) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } _, err = tx.NewDelete(). Model((*models.GameVariable)(nil)). Where("id = ? AND game_id = ?", existing.ID, gameID). Exec(c) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := tx.Commit(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Game variable deleted"}) } func (h *GameHandler) gameExists(ctx context.Context, gameID int64) (bool, error) { return h.db.NewSelect(). Model((*models.Game)(nil)). Where("id = ?", gameID). Exists(ctx) } func (h *GameHandler) getGameVariable(ctx context.Context, gameID, variableID int64) (*models.GameVariable, error) { variable := new(models.GameVariable) err := h.db.NewSelect(). Model(variable). Relation("Items", func(q *bun.SelectQuery) *bun.SelectQuery { return q.OrderExpr("gvi.id ASC") }). Where("gv.id = ? AND gv.game_id = ?", variableID, gameID). Scan(ctx) if err != nil { return nil, err } normalizeGameVariableResponse(variable) return variable, nil } func insertGameVariableItems(ctx context.Context, tx bun.Tx, variableID int64, req models.UpsertGameVariableRequest) error { if req.Type != models.GameVariableTypeTable && req.Type != models.GameVariableTypeVector { return nil } for _, itemReq := range req.Items { key := strings.TrimSpace(itemReq.Key) if req.Type == models.GameVariableTypeVector { key = strconv.FormatInt(*itemReq.Index, 10) } item := &models.GameVariableItem{ GameVariableID: variableID, Key: key, NumberValue: itemReq.NumberValue, StringValue: itemReq.StringValue, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if _, err := tx.NewInsert().Model(item).Exec(ctx); err != nil { return fmt.Errorf("could not save table items") } } return nil } func validateUpsertGameVariableRequest(req *models.UpsertGameVariableRequest) error { req.Key = strings.TrimSpace(req.Key) if req.Key == "" { return fmt.Errorf("key is required") } switch req.Type { case models.GameVariableTypeNumber: if req.NumberValue == nil { return fmt.Errorf("number_value is required for number type") } if req.StringValue != nil || req.TableValueType != nil || len(req.Items) > 0 { return fmt.Errorf("number type only accepts number_value") } case models.GameVariableTypeString: if req.StringValue == nil { return fmt.Errorf("string_value is required for string type") } if req.NumberValue != nil || req.TableValueType != nil || len(req.Items) > 0 { return fmt.Errorf("string type only accepts string_value") } case models.GameVariableTypeTable: if req.NumberValue != nil || req.StringValue != nil { return fmt.Errorf("table type does not accept scalar values") } if req.TableValueType == nil { return fmt.Errorf("table_value_type is required for table type") } if *req.TableValueType != models.GameVariableTableValueTypeNumber && *req.TableValueType != models.GameVariableTableValueTypeString { return fmt.Errorf("unsupported table_value_type") } seenKeys := make(map[string]struct{}, len(req.Items)) for i := range req.Items { req.Items[i].Key = strings.TrimSpace(req.Items[i].Key) if req.Items[i].Key == "" { return fmt.Errorf("items[%d].key is required", i) } if _, exists := seenKeys[req.Items[i].Key]; exists { return fmt.Errorf("duplicate table item key: %s", req.Items[i].Key) } seenKeys[req.Items[i].Key] = struct{}{} switch *req.TableValueType { case models.GameVariableTableValueTypeNumber: if req.Items[i].NumberValue == nil || req.Items[i].StringValue != nil { return fmt.Errorf("items[%d] must contain only number_value", i) } case models.GameVariableTableValueTypeString: if req.Items[i].StringValue == nil || req.Items[i].NumberValue != nil { return fmt.Errorf("items[%d] must contain only string_value", i) } } } case models.GameVariableTypeVector: if req.NumberValue != nil || req.StringValue != nil { return fmt.Errorf("vector type does not accept scalar values") } if req.TableValueType == nil { return fmt.Errorf("table_value_type is required for vector type") } if *req.TableValueType != models.GameVariableTableValueTypeNumber && *req.TableValueType != models.GameVariableTableValueTypeString { return fmt.Errorf("unsupported table_value_type") } seenIndexes := make(map[int64]struct{}, len(req.Items)) for i := range req.Items { if req.Items[i].Index == nil { return fmt.Errorf("items[%d].index is required", i) } if *req.Items[i].Index < 0 { return fmt.Errorf("items[%d].index must be >= 0", i) } if _, exists := seenIndexes[*req.Items[i].Index]; exists { return fmt.Errorf("duplicate vector item index: %d", *req.Items[i].Index) } seenIndexes[*req.Items[i].Index] = struct{}{} switch *req.TableValueType { case models.GameVariableTableValueTypeNumber: if req.Items[i].NumberValue == nil || req.Items[i].StringValue != nil { return fmt.Errorf("items[%d] must contain only number_value", i) } case models.GameVariableTableValueTypeString: if req.Items[i].StringValue == nil || req.Items[i].NumberValue != nil { return fmt.Errorf("items[%d] must contain only string_value", i) } } } default: return fmt.Errorf("unsupported variable type") } return nil } func dbValuesFromVariableRequest(req models.UpsertGameVariableRequest) (interface{}, interface{}, interface{}) { var numberValue interface{} if req.NumberValue != nil { numberValue = *req.NumberValue } var stringValue interface{} if req.StringValue != nil { stringValue = *req.StringValue } var tableValueType interface{} if req.TableValueType != nil { tableValueType = string(*req.TableValueType) } return numberValue, stringValue, tableValueType } func normalizeGameVariableResponse(variable *models.GameVariable) { if variable.Type != models.GameVariableTypeVector { return } for i := range variable.Items { index, err := strconv.ParseInt(variable.Items[i].Key, 10, 64) if err != nil { continue } variable.Items[i].Index = &index variable.Items[i].Key = "" } }