package service import ( "crypto/rand" "encoding/json" "fmt" "math/big" "net/http" "strings" "cmr-backend/internal/apperr" ) const ( AssignmentModeManual = "manual" AssignmentModeRandom = "random" AssignmentModeServerAssigned = "server-assigned" ) type CourseVariantView struct { ID string `json:"id"` Name string `json:"name"` Description *string `json:"description,omitempty"` RouteCode *string `json:"routeCode,omitempty"` Selectable bool `json:"selectable"` } type VariantBindingView struct { ID string `json:"id"` Name string `json:"name"` RouteCode *string `json:"routeCode,omitempty"` AssignmentMode string `json:"assignmentMode"` } type VariantPlan struct { AssignmentMode *string CourseVariants []CourseVariantView } func resolveVariantPlan(payloadJSON *string) VariantPlan { if payloadJSON == nil || strings.TrimSpace(*payloadJSON) == "" { return VariantPlan{} } var payload map[string]any if err := json.Unmarshal([]byte(*payloadJSON), &payload); err != nil { return VariantPlan{} } play, _ := payload["play"].(map[string]any) if len(play) == 0 { return VariantPlan{} } result := VariantPlan{} if rawMode, ok := play["assignmentMode"].(string); ok { if normalized := normalizeAssignmentMode(rawMode); normalized != nil { result.AssignmentMode = normalized } } rawVariants, _ := play["courseVariants"].([]any) if len(rawVariants) == 0 { return result } for _, raw := range rawVariants { item, ok := raw.(map[string]any) if !ok { continue } id, _ := item["id"].(string) name, _ := item["name"].(string) id = strings.TrimSpace(id) name = strings.TrimSpace(name) if id == "" || name == "" { continue } var description *string if value, ok := item["description"].(string); ok && strings.TrimSpace(value) != "" { trimmed := strings.TrimSpace(value) description = &trimmed } var routeCode *string if value, ok := item["routeCode"].(string); ok && strings.TrimSpace(value) != "" { trimmed := strings.TrimSpace(value) routeCode = &trimmed } selectable := true if value, ok := item["selectable"].(bool); ok { selectable = value } result.CourseVariants = append(result.CourseVariants, CourseVariantView{ ID: id, Name: name, Description: description, RouteCode: routeCode, Selectable: selectable, }) } return result } func resolveLaunchVariant(plan VariantPlan, requestedVariantID string) (*VariantBindingView, error) { requestedVariantID = strings.TrimSpace(requestedVariantID) if len(plan.CourseVariants) == 0 { return nil, nil } mode := AssignmentModeManual if plan.AssignmentMode != nil { mode = *plan.AssignmentMode } if requestedVariantID != "" { for _, item := range plan.CourseVariants { if item.ID == requestedVariantID { if !item.Selectable && mode == AssignmentModeManual { return nil, apperr.New(http.StatusBadRequest, "variant_not_selectable", "requested variant is not selectable") } return &VariantBindingView{ ID: item.ID, Name: item.Name, RouteCode: item.RouteCode, AssignmentMode: mode, }, nil } } return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "requested variant does not exist") } selected, err := selectDefaultVariant(plan.CourseVariants, mode) if err != nil { return nil, err } return &VariantBindingView{ ID: selected.ID, Name: selected.Name, RouteCode: selected.RouteCode, AssignmentMode: mode, }, nil } func normalizeAssignmentMode(value string) *string { switch strings.TrimSpace(value) { case AssignmentModeManual: mode := AssignmentModeManual return &mode case AssignmentModeRandom: mode := AssignmentModeRandom return &mode case AssignmentModeServerAssigned: mode := AssignmentModeServerAssigned return &mode default: return nil } } func selectDefaultVariant(items []CourseVariantView, mode string) (*CourseVariantView, error) { candidates := make([]CourseVariantView, 0, len(items)) for _, item := range items { if item.Selectable { candidates = append(candidates, item) } } if len(candidates) == 0 { candidates = append(candidates, items...) } if len(candidates) == 0 { return nil, apperr.New(http.StatusBadRequest, "variant_not_found", "course variants are empty") } switch mode { case AssignmentModeRandom: index, err := rand.Int(rand.Reader, big.NewInt(int64(len(candidates)))) if err != nil { return nil, apperr.New(http.StatusInternalServerError, "variant_select_failed", fmt.Sprintf("failed to select random variant: %v", err)) } selected := candidates[int(index.Int64())] return &selected, nil case AssignmentModeServerAssigned, AssignmentModeManual: fallthrough default: selected := candidates[0] return &selected, nil } }