package valueobjects import ( "fmt" "time" "knowfoolery/backend/shared/types" "knowfoolery/backend/shared/errors" ) // SessionTimer manages the timing for a game session with warnings type SessionTimer struct { startTime types.Timestamp endTime *types.Timestamp maxDuration types.Duration isActive bool warningsSent []TimerWarning } // TimerWarning represents a warning sent during the session type TimerWarning struct { WarningType string `json:"warning_type"` Message string `json:"message"` SentAt types.Timestamp `json:"sent_at"` TimeRemaining types.Duration `json:"time_remaining"` } // NewSessionTimer creates a new session timer func NewSessionTimer(startTime types.Timestamp) *SessionTimer { return &SessionTimer{ startTime: startTime, endTime: nil, maxDuration: types.NewDuration(types.MaxSessionDuration), isActive: true, warningsSent: make([]TimerWarning, 0), } } // NewSessionTimerNow creates a new session timer starting now func NewSessionTimerNow() *SessionTimer { return NewSessionTimer(types.NewTimestamp()) } // StartTime returns the session start time func (st *SessionTimer) StartTime() types.Timestamp { return st.startTime } // EndTime returns the session end time (nil if not ended) func (st *SessionTimer) EndTime() *types.Timestamp { return st.endTime } // MaxDuration returns the maximum allowed session duration func (st *SessionTimer) MaxDuration() types.Duration { return st.maxDuration } // IsActive returns true if the timer is currently active func (st *SessionTimer) IsActive() bool { return st.isActive } // WarningsSent returns all warnings that have been sent func (st *SessionTimer) WarningsSent() []TimerWarning { return st.warningsSent } // ElapsedTime returns the time elapsed since session start func (st *SessionTimer) ElapsedTime() types.Duration { var endPoint types.Timestamp if st.endTime != nil { endPoint = *st.endTime } else { endPoint = types.NewTimestamp() } elapsed := endPoint.Sub(st.startTime) return types.NewDuration(elapsed) } // RemainingTime returns the time remaining in the session func (st *SessionTimer) RemainingTime() types.Duration { if !st.isActive { return types.NewDuration(0) } elapsed := st.ElapsedTime() remaining := st.maxDuration.Duration - elapsed.Duration if remaining < 0 { remaining = 0 } return types.NewDuration(remaining) } // IsExpired returns true if the session has exceeded its maximum duration func (st *SessionTimer) IsExpired() bool { return st.ElapsedTime().Duration >= st.maxDuration.Duration } // TimeUntilExpiry returns the duration until session expires func (st *SessionTimer) TimeUntilExpiry() types.Duration { return st.RemainingTime() } // End marks the session timer as ended func (st *SessionTimer) End() error { if !st.isActive { return errors.ErrOperationNotAllowed("end timer", "timer is not active") } now := types.NewTimestamp() st.endTime = &now st.isActive = false return nil } // GetSessionDuration returns the total duration of the session func (st *SessionTimer) GetSessionDuration() types.Duration { if st.endTime != nil { return types.NewDuration(st.endTime.Sub(st.startTime)) } return st.ElapsedTime() } // ShouldSendWarning checks if a warning should be sent based on remaining time func (st *SessionTimer) ShouldSendWarning() (bool, TimerWarning) { if !st.isActive { return false, TimerWarning{} } remaining := st.RemainingTime() // Check for different warning thresholds warnings := []struct { threshold time.Duration warningType string message string }{ {types.SessionWarningAt5Min, "5min", "5 minutes remaining!"}, {types.SessionWarningAt1Min, "1min", "1 minute remaining!"}, {types.SessionWarningAt10Sec, "10sec", "10 seconds left!"}, } for _, warning := range warnings { // Check if we're at or below this threshold if remaining.Duration <= warning.threshold { // Check if we've already sent this warning if !st.hasWarningSent(warning.warningType) { return true, TimerWarning{ WarningType: warning.warningType, Message: warning.message, SentAt: types.NewTimestamp(), TimeRemaining: remaining, } } } } return false, TimerWarning{} } // AddWarning records that a warning has been sent func (st *SessionTimer) AddWarning(warning TimerWarning) { st.warningsSent = append(st.warningsSent, warning) } // hasWarningSent checks if a specific warning type has already been sent func (st *SessionTimer) hasWarningSent(warningType string) bool { for _, warning := range st.warningsSent { if warning.WarningType == warningType { return true } } return false } // GetFormattedRemainingTime returns the remaining time formatted for display func (st *SessionTimer) GetFormattedRemainingTime() string { if !st.isActive { return "00:00" } remaining := st.RemainingTime() if remaining.Duration <= 0 { return "00:00" } totalSeconds := int(remaining.Seconds()) minutes := totalSeconds / 60 seconds := totalSeconds % 60 return fmt.Sprintf("%02d:%02d", minutes, seconds) } // GetFormattedElapsedTime returns the elapsed time formatted for display func (st *SessionTimer) GetFormattedElapsedTime() string { elapsed := st.ElapsedTime() totalSeconds := int(elapsed.Seconds()) minutes := totalSeconds / 60 seconds := totalSeconds % 60 return fmt.Sprintf("%02d:%02d", minutes, seconds) } // IsInWarningPeriod returns true if the session is in a warning period func (st *SessionTimer) IsInWarningPeriod() bool { if !st.isActive { return false } remaining := st.RemainingTime() return remaining.Duration <= types.SessionWarningAt5Min } // IsInCriticalPeriod returns true if the session is in the critical final period func (st *SessionTimer) IsInCriticalPeriod() bool { if !st.isActive { return false } remaining := st.RemainingTime() return remaining.Duration <= types.SessionWarningAt1Min } // GetRemainingPercentage returns the percentage of time remaining (0-100) func (st *SessionTimer) GetRemainingPercentage() float64 { if !st.isActive { return 0.0 } remaining := st.RemainingTime() if st.maxDuration.Duration == 0 { return 0.0 } percentage := (remaining.Duration.Seconds() / st.maxDuration.Duration.Seconds()) * 100.0 if percentage < 0 { percentage = 0 } if percentage > 100 { percentage = 100 } return percentage } // GetElapsedPercentage returns the percentage of time elapsed (0-100) func (st *SessionTimer) GetElapsedPercentage() float64 { return 100.0 - st.GetRemainingPercentage() } // TimerStatus represents the current status of the timer type TimerStatus 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 []TimerWarning `json:"warnings_sent"` } // GetStatus returns the current comprehensive status of the timer func (st *SessionTimer) GetStatus() *TimerStatus { return &TimerStatus{ IsActive: st.IsActive(), IsExpired: st.IsExpired(), IsInWarningPeriod: st.IsInWarningPeriod(), IsInCriticalPeriod: st.IsInCriticalPeriod(), StartTime: st.StartTime(), EndTime: st.EndTime(), ElapsedTime: st.ElapsedTime(), RemainingTime: st.RemainingTime(), ElapsedFormatted: st.GetFormattedElapsedTime(), RemainingFormatted: st.GetFormattedRemainingTime(), ElapsedPercentage: st.GetElapsedPercentage(), RemainingPercentage: st.GetRemainingPercentage(), WarningsSent: st.WarningsSent(), } } // SessionTimerFromValues creates a SessionTimer from stored values // Used when loading from persistence func SessionTimerFromValues(startTime types.Timestamp, endTime *types.Timestamp, warningsSent []TimerWarning) *SessionTimer { isActive := endTime == nil timer := &SessionTimer{ startTime: startTime, endTime: endTime, maxDuration: types.NewDuration(types.MaxSessionDuration), isActive: isActive, warningsSent: warningsSent, } // If timer was active but is now expired, mark it as inactive if isActive && timer.IsExpired() { now := types.NewTimestamp() timer.endTime = &now timer.isActive = false } return timer }