package service import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" ) // QuestionService orchestrates question operations. type QuestionService struct { repo port.QuestionRepository } // NewQuestionService creates a new question service. func NewQuestionService(repo port.QuestionRepository) *QuestionService { return &QuestionService{repo: repo} } // CreateQuestion creates a new question. func (s *QuestionService) CreateQuestion(ctx context.Context, conversationID domain.ConversationID, projectID string, questionType domain.QuestionType, text string, choices []string, metadata map[string]string) (*domain.Question, error) { if projectID == "" { return nil, fmt.Errorf("project_id is required") } if text == "" { return nil, fmt.Errorf("text is required") } // Validate choices for choice-based questions if (questionType == domain.QuestionTypeChoice || questionType == domain.QuestionTypeMultiChoice) && len(choices) == 0 { return nil, fmt.Errorf("choices are required for choice-based questions") } if metadata == nil { metadata = make(map[string]string) } question := &domain.Question{ ID: domain.QuestionID(uuid.New().String()), ConversationID: conversationID, ProjectID: projectID, Type: questionType, Text: text, Choices: choices, Metadata: metadata, } startTime := time.Now() if err := s.repo.CreateQuestion(ctx, question); err != nil { return nil, err } log := logging.FromContext(ctx) log.Info("question created", "question_id", question.ID, logging.FieldProjectID, projectID, logging.FieldOperation, "create_question", logging.FieldDuration, time.Since(startTime).Milliseconds(), "question_type", questionType, "conversation_id", conversationID, "choice_count", len(choices), ) return question, nil } // GetQuestion retrieves a question by ID. func (s *QuestionService) GetQuestion(ctx context.Context, id domain.QuestionID) (*domain.Question, error) { return s.repo.GetQuestion(ctx, id) } // ListUnansweredQuestions returns all unanswered questions for a project. func (s *QuestionService) ListUnansweredQuestions(ctx context.Context, projectID string) ([]*domain.Question, error) { return s.repo.ListUnansweredQuestions(ctx, projectID) } // ListQuestionsByConversation returns all questions for a conversation. func (s *QuestionService) ListQuestionsByConversation(ctx context.Context, conversationID domain.ConversationID) ([]*domain.Question, error) { return s.repo.ListQuestionsByConversation(ctx, conversationID) } // AnswerQuestion records an answer to a question. func (s *QuestionService) AnswerQuestion(ctx context.Context, id domain.QuestionID, answer *string, answerChoices []string) error { // Get question to validate answer type question, err := s.repo.GetQuestion(ctx, id) if err != nil { return err } // Validate answer based on question type switch question.Type { case domain.QuestionTypeText: if answer == nil || *answer == "" { return fmt.Errorf("text answer is required") } case domain.QuestionTypeYesNo: if answer == nil || (*answer != "yes" && *answer != "no") { return fmt.Errorf("answer must be 'yes' or 'no'") } case domain.QuestionTypeChoice: if answer == nil || *answer == "" { return fmt.Errorf("choice answer is required") } // Validate choice is in the available choices valid := false for _, choice := range question.Choices { if *answer == choice { valid = true break } } if !valid { return fmt.Errorf("answer must be one of the available choices") } case domain.QuestionTypeMultiChoice: if len(answerChoices) == 0 { return fmt.Errorf("at least one choice must be selected") } // Validate all choices are in the available choices for _, selected := range answerChoices { valid := false for _, choice := range question.Choices { if selected == choice { valid = true break } } if !valid { return fmt.Errorf("answer '%s' is not one of the available choices", selected) } } } startTime := time.Now() if err := s.repo.AnswerQuestion(ctx, id, answer, answerChoices); err != nil { return err } log := logging.FromContext(ctx) log.Info("question answered", "question_id", id, logging.FieldProjectID, question.ProjectID, logging.FieldOperation, "answer_question", logging.FieldDuration, time.Since(startTime).Milliseconds(), "question_type", question.Type, "answer_choice_count", len(answerChoices), ) return nil } // DeleteQuestion deletes a question. func (s *QuestionService) DeleteQuestion(ctx context.Context, id domain.QuestionID) error { startTime := time.Now() if err := s.repo.DeleteQuestion(ctx, id); err != nil { return err } log := logging.FromContext(ctx) log.Info("question deleted", "question_id", id, logging.FieldOperation, "delete_question", logging.FieldDuration, time.Since(startTime).Milliseconds(), ) return nil }