package assignmentgen import ( "context" "fmt" "math" "sort" "time" "boostai-backend/internal/sqlc" ) func (s *Service) BuildMixedPlan(ctx context.Context, params MixedPlanParams) (MixedPlan, error) { if err := validateMixedPlanParams(params); err != nil { return MixedPlan{}, err } ratio := normalizePersonalizedRatio(params.PersonalizedRatio) weaknessSummary, err := s.BuildWeaknessSummary(ctx, params.StudentID) if err != nil { return MixedPlan{}, err } personalizedTopic, personalizedApplied := selectPersonalizedTopic(params.PrimaryTopic, weaknessSummary) personalizedCount := calculatePersonalizedCount(params.TotalQuestions, ratio, personalizedApplied) coreCount := calculateCoreCount(params.TotalQuestions, personalizedCount) baseSeed := normalizeBaseSeed(params.BaseSeed) personalizedDifficulty := normalizePersonalizedDifficulty(params) return MixedPlan{ Plan: buildMixedPlanItems(params, coreCount, personalizedCount, personalizedTopic, personalizedDifficulty, baseSeed), WeaknessSummary: weaknessSummary, CoreCount: coreCount, PersonalizedCount: personalizedCount, PersonalizedTopic: personalizedTopic, PersonalizedApplied: personalizedCount > 0, BaseSeed: baseSeed, }, nil } func validateMixedPlanParams(params MixedPlanParams) error { if params.StudentID <= 0 { return fmt.Errorf("student_id is required") } if params.PrimaryTopic == "" { return fmt.Errorf("primary topic is required") } if params.PrimaryDifficulty == "" { return fmt.Errorf("primary difficulty is required") } if params.TotalQuestions <= 0 { return fmt.Errorf("total_questions must be positive") } if params.PersonalizedRatio < 0 || params.PersonalizedRatio >= 1 { return fmt.Errorf("personalized_ratio must be between 0 and less than 1") } return nil } func normalizePersonalizedRatio(ratio float64) float64 { if ratio == 0 { return defaultPersonalizedRatio } return ratio } func normalizeBaseSeed(seed int64) int64 { if seed == 0 { return time.Now().UnixNano() } return seed } func normalizePersonalizedDifficulty(params MixedPlanParams) sqlc.QuestionDifficulty { if params.PersonalizedDifficulty == "" { return params.PrimaryDifficulty } return params.PersonalizedDifficulty } func calculateCoreCount(totalQuestions, personalizedCount int) int { coreCount := totalQuestions - personalizedCount if totalQuestions > 0 && coreCount <= 0 { return 1 } return coreCount } func buildMixedPlanItems( params MixedPlanParams, coreCount int, personalizedCount int, personalizedTopic sqlc.QuestionTopic, personalizedDifficulty sqlc.QuestionDifficulty, baseSeed int64, ) []PlanItem { plan := make([]PlanItem, 0, 2) if coreCount > 0 { plan = append(plan, PlanItem{ Topic: params.PrimaryTopic, Difficulty: params.PrimaryDifficulty, Count: coreCount, SourceBucket: SourceBucketCoreTopic, Seed: baseSeed, }) } if personalizedCount > 0 { plan = append(plan, PlanItem{ Topic: personalizedTopic, Difficulty: personalizedDifficulty, Count: personalizedCount, SourceBucket: SourceBucketPersonalized, Seed: baseSeed + 7919, }) } return plan } func selectPersonalizedTopic(primaryTopic sqlc.QuestionTopic, summary WeaknessSummary) (sqlc.QuestionTopic, bool) { if len(summary.TopicScores) == 0 { return "", false } type scoredTopic struct { topic sqlc.QuestionTopic score float64 } topics := make([]scoredTopic, 0, len(summary.TopicScores)) for topic, score := range summary.TopicScores { topics = append(topics, scoredTopic{topic: topic, score: score}) } sort.SliceStable(topics, func(i, j int) bool { if topics[i].score == topics[j].score { return string(topics[i].topic) < string(topics[j].topic) } return topics[i].score < topics[j].score }) for _, candidate := range topics { if candidate.topic != primaryTopic { return candidate.topic, true } } return topics[0].topic, true } func calculatePersonalizedCount(totalQuestions int, ratio float64, personalizedApplied bool) int { if !personalizedApplied || totalQuestions <= 1 || ratio <= 0 { return 0 } count := int(math.Floor(float64(totalQuestions) * ratio)) if count == 0 && totalQuestions >= 3 { count = 1 } if count >= totalQuestions { count = totalQuestions - 1 } if count < 0 { count = 0 } return count }