package dtos import ( "fmt" "strings" "knowfoolery/backend/shared/types" "knowfoolery/backend/shared/errors" "knowfoolery/backend/services/game-session-service/internal/domain/valueobjects" ) // Request DTOs // StartSessionRequest represents a request to start a new session type StartSessionRequest struct { UserID types.UserID `json:"user_id" validate:"required"` PlayerName string `json:"player_name,omitempty" validate:"omitempty,min=2,max=50"` } // Validate validates the start session request func (r *StartSessionRequest) Validate() error { if r.UserID.IsEmpty() { return errors.ErrValidationFailed("user_id", "user ID is required") } if r.PlayerName != "" { playerName := strings.TrimSpace(r.PlayerName) if len(playerName) < 2 { return errors.ErrValidationFailed("player_name", "player name must be at least 2 characters") } if len(playerName) > 50 { return errors.ErrValidationFailed("player_name", "player name must be at most 50 characters") } } return nil } // ResumeSessionRequest represents a request to resume a session type ResumeSessionRequest struct { SessionID types.GameSessionID `json:"session_id" validate:"required"` } // Validate validates the resume session request func (r *ResumeSessionRequest) Validate() error { if r.SessionID.IsEmpty() { return errors.ErrValidationFailed("session_id", "session ID is required") } return nil } // SubmitAnswerRequest represents a request to submit an answer type SubmitAnswerRequest struct { SessionID types.GameSessionID `json:"session_id" validate:"required"` QuestionID types.QuestionID `json:"question_id" validate:"required"` PlayerAnswer string `json:"player_answer" validate:"required,max=500"` HintUsed bool `json:"hint_used"` } // Validate validates the submit answer request func (r *SubmitAnswerRequest) Validate() error { if r.SessionID.IsEmpty() { return errors.ErrValidationFailed("session_id", "session ID is required") } if r.QuestionID.IsEmpty() { return errors.ErrValidationFailed("question_id", "question ID is required") } playerAnswer := strings.TrimSpace(r.PlayerAnswer) if playerAnswer == "" { return errors.ErrValidationFailed("player_answer", "player answer is required") } if len(playerAnswer) > types.MaxAnswerLength { return errors.ErrValidationFailed("player_answer", fmt.Sprintf("player answer must be at most %d characters", types.MaxAnswerLength)) } return nil } // RequestHintRequest represents a request for a hint type RequestHintRequest struct { SessionID types.GameSessionID `json:"session_id" validate:"required"` QuestionID types.QuestionID `json:"question_id" validate:"required"` } // Validate validates the request hint request func (r *RequestHintRequest) Validate() error { if r.SessionID.IsEmpty() { return errors.ErrValidationFailed("session_id", "session ID is required") } if r.QuestionID.IsEmpty() { return errors.ErrValidationFailed("question_id", "question ID is required") } return nil } // CompleteSessionRequest represents a request to complete a session type CompleteSessionRequest struct { SessionID types.GameSessionID `json:"session_id" validate:"required"` Reason string `json:"reason" validate:"required,max=100"` } // Validate validates the complete session request func (r *CompleteSessionRequest) Validate() error { if r.SessionID.IsEmpty() { return errors.ErrValidationFailed("session_id", "session ID is required") } reason := strings.TrimSpace(r.Reason) if reason == "" { return errors.ErrValidationFailed("reason", "completion reason is required") } if len(reason) > 100 { return errors.ErrValidationFailed("reason", "completion reason must be at most 100 characters") } return nil } // AbandonSessionRequest represents a request to abandon a session type AbandonSessionRequest struct { SessionID types.GameSessionID `json:"session_id" validate:"required"` } // Validate validates the abandon session request func (r *AbandonSessionRequest) Validate() error { if r.SessionID.IsEmpty() { return errors.ErrValidationFailed("session_id", "session ID is required") } return nil } // GetSessionStatusRequest represents a request to get session status type GetSessionStatusRequest struct { SessionID types.GameSessionID `json:"session_id" validate:"required"` } // Validate validates the get session status request func (r *GetSessionStatusRequest) Validate() error { if r.SessionID.IsEmpty() { return errors.ErrValidationFailed("session_id", "session ID is required") } return nil } // GetUserSessionsRequest represents a request to get user sessions type GetUserSessionsRequest struct { UserID types.UserID `json:"user_id" validate:"required"` Limit int `json:"limit" validate:"min=1,max=100"` Offset int `json:"offset" validate:"min=0"` } // Validate validates the get user sessions request func (r *GetUserSessionsRequest) Validate() error { if r.UserID.IsEmpty() { return errors.ErrValidationFailed("user_id", "user ID is required") } if r.Limit <= 0 { r.Limit = 20 // Default limit } if r.Limit > 100 { return errors.ErrValidationFailed("limit", "limit must be at most 100") } if r.Offset < 0 { return errors.ErrValidationFailed("offset", "offset must be non-negative") } return nil } // GetActiveSessionRequest represents a request to get active session type GetActiveSessionRequest struct { UserID types.UserID `json:"user_id" validate:"required"` } // Validate validates the get active session request func (r *GetActiveSessionRequest) Validate() error { if r.UserID.IsEmpty() { return errors.ErrValidationFailed("user_id", "user ID is required") } return nil } // Response DTOs // StartSessionResponse represents the response to starting a session type StartSessionResponse struct { SessionID types.GameSessionID `json:"session_id"` Status string `json:"status"` CreatedAt types.Timestamp `json:"created_at"` PlayerName string `json:"player_name,omitempty"` MaxDuration types.Duration `json:"max_duration"` RemainingTime types.Duration `json:"remaining_time"` } // ResumeSessionResponse represents the response to resuming a session type ResumeSessionResponse struct { SessionID types.GameSessionID `json:"session_id"` CanResume bool `json:"can_resume"` TimeoutOccurred bool `json:"timeout_occurred"` Status string `json:"status"` RemainingTime *types.Duration `json:"remaining_time,omitempty"` CurrentScore *ScoreDTO `json:"current_score,omitempty"` QuestionsProgress map[string]QuestionProgressDTO `json:"questions_progress,omitempty"` CompletionResult *SessionCompletionResultDTO `json:"completion_result,omitempty"` } // SubmitAnswerResponse represents the response to submitting an answer type SubmitAnswerResponse struct { Accepted bool `json:"accepted"` ProcessingResult *AnswerProcessingResult `json:"processing_result,omitempty"` AntiCheatViolations []string `json:"anti_cheat_violations,omitempty"` UpdatedScore *ScoreDTO `json:"updated_score,omitempty"` RemainingTime types.Duration `json:"remaining_time"` TimeoutOccurred bool `json:"timeout_occurred"` CompletionResult *SessionCompletionResultDTO `json:"completion_result,omitempty"` RejectionReason *string `json:"rejection_reason,omitempty"` } // RequestHintResponse represents the response to requesting a hint type RequestHintResponse struct { HintText string `json:"hint_text"` QuestionID types.QuestionID `json:"question_id"` RequestedAt types.Timestamp `json:"requested_at"` } // CompleteSessionResponse represents the response to completing a session type CompleteSessionResponse struct { SessionID types.GameSessionID `json:"session_id"` FinalScore int `json:"final_score"` CompletedAt types.Timestamp `json:"completed_at"` Reason string `json:"reason"` Duration types.Duration `json:"duration"` CompletionResult *SessionCompletionResultDTO `json:"completion_result"` } // AbandonSessionResponse represents the response to abandoning a session type AbandonSessionResponse struct { SessionID types.GameSessionID `json:"session_id"` AbandonedAt types.Timestamp `json:"abandoned_at"` FinalScore int `json:"final_score"` } // GetSessionStatusResponse represents the response to getting session status type GetSessionStatusResponse struct { SessionID types.GameSessionID `json:"session_id"` Status string `json:"status"` CurrentScore *ScoreDTO `json:"current_score"` TimerStatus *TimerStatusDTO `json:"timer_status"` QuestionsProgress map[string]QuestionProgressDTO `json:"questions_progress"` TimeoutOccurred bool `json:"timeout_occurred"` CompletionResult *SessionCompletionResultDTO `json:"completion_result,omitempty"` } // GetUserSessionsResponse represents the response to getting user sessions type GetUserSessionsResponse struct { Sessions []SessionSummary `json:"sessions"` TotalCount int `json:"total_count"` Offset int `json:"offset"` Limit int `json:"limit"` } // GetActiveSessionResponse represents the response to getting active session type GetActiveSessionResponse struct { HasActiveSession bool `json:"has_active_session"` SessionID types.GameSessionID `json:"session_id,omitempty"` Status string `json:"status,omitempty"` CreatedAt types.Timestamp `json:"created_at,omitempty"` PlayerName *string `json:"player_name,omitempty"` CurrentScore *ScoreDTO `json:"current_score,omitempty"` RemainingTime types.Duration `json:"remaining_time,omitempty"` QuestionsProgress map[string]QuestionProgressDTO `json:"questions_progress,omitempty"` } // Nested DTOs // ScoreDTO represents score information type ScoreDTO struct { Total int `json:"total"` QuestionsAttempted int `json:"questions_attempted"` QuestionsCorrect int `json:"questions_correct"` SuccessRate float64 `json:"success_rate"` PointsFromHints int `json:"points_from_hints"` PointsWithoutHints int `json:"points_without_hints"` HintsUsed int `json:"hints_used"` AveragePointsPerQuestion float64 `json:"average_points_per_question"` } // TimerStatusDTO represents timer status information type TimerStatusDTO struct { IsActive bool `json:"is_active"` IsExpired bool `json:"is_expired"` IsInWarningPeriod bool `json:"is_in_warning_period"` IsInCriticalPeriod bool `json:"is_in_critical_period"` StartTime types.Timestamp `json:"start_time"` EndTime *types.Timestamp `json:"end_time,omitempty"` ElapsedTime types.Duration `json:"elapsed_time"` RemainingTime types.Duration `json:"remaining_time"` ElapsedFormatted string `json:"elapsed_formatted"` RemainingFormatted string `json:"remaining_formatted"` ElapsedPercentage float64 `json:"elapsed_percentage"` RemainingPercentage float64 `json:"remaining_percentage"` WarningsSent []valueobjects.TimerWarning `json:"warnings_sent"` } // QuestionProgressDTO represents progress on a single question type QuestionProgressDTO struct { QuestionID types.QuestionID `json:"question_id"` PresentedAt types.Timestamp `json:"presented_at"` HintUsed bool `json:"hint_used"` IsCompleted bool `json:"is_completed"` Attempts []AttemptDTO `json:"attempts"` } // AttemptDTO represents a single attempt type AttemptDTO struct { ID types.AttemptID `json:"id"` AttemptNumber int `json:"attempt_number"` PlayerAnswer string `json:"player_answer"` IsCorrect bool `json:"is_correct"` IsTimeout bool `json:"is_timeout"` PointsAwarded int `json:"points_awarded"` HintUsed bool `json:"hint_used"` MatchType string `json:"match_type"` Similarity float64 `json:"similarity"` AttemptedAt types.Timestamp `json:"attempted_at"` } // AnswerProcessingResult represents the result of processing an answer type AnswerProcessingResult struct { IsCorrect bool `json:"is_correct"` Score int `json:"score"` AttemptNumber int `json:"attempt_number"` AttemptsRemaining int `json:"attempts_remaining"` Similarity float64 `json:"similarity"` MatchType string `json:"match_type"` IsQuestionComplete bool `json:"is_question_complete"` } // SessionCompletionResultDTO represents the result of session completion type SessionCompletionResultDTO struct { SessionID types.GameSessionID `json:"session_id"` FinalScore int `json:"final_score"` CompletedAt types.Timestamp `json:"completed_at"` Reason string `json:"reason"` Duration types.Duration `json:"duration"` } // SessionSummary represents a summary of a session type SessionSummary struct { SessionID types.GameSessionID `json:"session_id"` Status string `json:"status"` CreatedAt types.Timestamp `json:"created_at"` UpdatedAt types.Timestamp `json:"updated_at"` Duration types.Duration `json:"duration"` PlayerName *string `json:"player_name,omitempty"` Score *ScoreDTO `json:"score"` QuestionsCount int `json:"questions_count"` } // Helper methods for DTOs // GetScoreSummary returns a human-readable score summary func (s *ScoreDTO) GetScoreSummary() string { return fmt.Sprintf("%d points (%d/%d correct, %.1f%% success rate)", s.Total, s.QuestionsCorrect, s.QuestionsAttempted, s.SuccessRate) } // GetTimerSummary returns a human-readable timer summary func (t *TimerStatusDTO) GetTimerSummary() string { if !t.IsActive { return fmt.Sprintf("Inactive (Duration: %s)", t.ElapsedFormatted) } if t.IsExpired { return "Session expired" } status := "Active" if t.IsInCriticalPeriod { status = "Critical" } else if t.IsInWarningPeriod { status = "Warning" } return fmt.Sprintf("%s (%s remaining)", status, t.RemainingFormatted) } // GetProgressSummary returns a summary of question progress func (q *QuestionProgressDTO) GetProgressSummary() string { if q.IsCompleted { return fmt.Sprintf("Completed (%d attempts)", len(q.Attempts)) } return fmt.Sprintf("In progress (%d attempts)", len(q.Attempts)) } // GetAttemptSummary returns a summary of an attempt func (a *AttemptDTO) GetAttemptSummary() string { status := "incorrect" if a.IsTimeout { status = "timeout" } else if a.IsCorrect { status = "correct" } return fmt.Sprintf("Attempt %d: %s (%d points)", a.AttemptNumber, status, a.PointsAwarded) } // GetSessionSummary returns a summary of the session func (s *SessionSummary) GetSessionSummary() string { playerInfo := "" if s.PlayerName != nil { playerInfo = fmt.Sprintf(" - %s", *s.PlayerName) } return fmt.Sprintf("Session %s: %s%s (%s, %d questions)", s.SessionID, s.Status, playerInfo, s.Duration.String(), s.QuestionsCount) } // IsSuccessful returns true if the answer processing was successful func (apr *AnswerProcessingResult) IsSuccessful() bool { return apr.IsCorrect && apr.Score > 0 } // GetCompletionSummary returns a summary of the completion result func (scr *SessionCompletionResultDTO) GetCompletionSummary() string { return fmt.Sprintf("Session completed: %d points in %s (reason: %s)", scr.FinalScore, scr.Duration.String(), scr.Reason) }