package tournament import ( "context" "database/sql" "fmt" ) // TournamentSummary is a lightweight summary for the tournament lobby view. type TournamentSummary struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` PlayerCount int `json:"player_count"` ActivePlayers int `json:"active_players"` CurrentLevel int `json:"current_level"` SmallBlind int64 `json:"small_blind"` BigBlind int64 `json:"big_blind"` RemainingMs int64 `json:"remaining_ms"` TotalElapsedMs int64 `json:"total_elapsed_ms"` PrizePool int64 `json:"prize_pool"` StartedAt *int64 `json:"started_at,omitempty"` CreatedAt int64 `json:"created_at"` } // MultiManager manages multiple simultaneous tournaments. // It provides lobby views and tournament switching. // Independence guarantee: every piece of state (clock, players, tables, // financials) is scoped by tournament_id. No global singletons. type MultiManager struct { service *Service } // NewMultiManager creates a new multi-tournament manager. func NewMultiManager(service *Service) *MultiManager { return &MultiManager{service: service} } // ListActiveTournaments returns all tournaments with active statuses // (registering, running, paused, final_table) for the lobby view. // This powers the multi-tournament switching UI (MULTI-02). func (m *MultiManager) ListActiveTournaments(ctx context.Context) ([]TournamentSummary, error) { rows, err := m.service.db.QueryContext(ctx, `SELECT t.id, t.name, t.status, t.current_level, t.clock_state, t.clock_remaining_ns, t.total_elapsed_ns, t.started_at, t.created_at, (SELECT COUNT(*) FROM tournament_players tp WHERE tp.tournament_id = t.id) as player_count, (SELECT COUNT(*) FROM tournament_players tp WHERE tp.tournament_id = t.id AND tp.status = 'active') as active_players, COALESCE( (SELECT SUM(amount) FROM transactions tx WHERE tx.tournament_id = t.id AND tx.type IN ('buyin', 'rebuy', 'addon', 'reentry') AND tx.undone = 0), 0 ) - COALESCE( (SELECT SUM(amount) FROM transactions tx WHERE tx.tournament_id = t.id AND tx.type = 'rake' AND tx.undone = 0), 0 ) as prize_pool FROM tournaments t WHERE t.status IN ('created', 'registering', 'running', 'paused', 'final_table') ORDER BY t.created_at DESC`) if err != nil { return nil, fmt.Errorf("tournament: list active: %w", err) } defer rows.Close() return m.scanSummaries(ctx, rows) } // ListAllTournaments returns all tournaments (active + recent completed) for display. func (m *MultiManager) ListAllTournaments(ctx context.Context) ([]TournamentSummary, error) { rows, err := m.service.db.QueryContext(ctx, `SELECT t.id, t.name, t.status, t.current_level, t.clock_state, t.clock_remaining_ns, t.total_elapsed_ns, t.started_at, t.created_at, (SELECT COUNT(*) FROM tournament_players tp WHERE tp.tournament_id = t.id) as player_count, (SELECT COUNT(*) FROM tournament_players tp WHERE tp.tournament_id = t.id AND tp.status = 'active') as active_players, COALESCE( (SELECT SUM(amount) FROM transactions tx WHERE tx.tournament_id = t.id AND tx.type IN ('buyin', 'rebuy', 'addon', 'reentry') AND tx.undone = 0), 0 ) - COALESCE( (SELECT SUM(amount) FROM transactions tx WHERE tx.tournament_id = t.id AND tx.type = 'rake' AND tx.undone = 0), 0 ) as prize_pool FROM tournaments t ORDER BY t.created_at DESC LIMIT 50`) if err != nil { return nil, fmt.Errorf("tournament: list all: %w", err) } defer rows.Close() return m.scanSummaries(ctx, rows) } // GetTournamentSummary returns a lightweight summary for a single tournament. func (m *MultiManager) GetTournamentSummary(ctx context.Context, id string) (*TournamentSummary, error) { row := m.service.db.QueryRowContext(ctx, `SELECT t.id, t.name, t.status, t.current_level, t.clock_state, t.clock_remaining_ns, t.total_elapsed_ns, t.started_at, t.created_at, (SELECT COUNT(*) FROM tournament_players tp WHERE tp.tournament_id = t.id) as player_count, (SELECT COUNT(*) FROM tournament_players tp WHERE tp.tournament_id = t.id AND tp.status = 'active') as active_players, COALESCE( (SELECT SUM(amount) FROM transactions tx WHERE tx.tournament_id = t.id AND tx.type IN ('buyin', 'rebuy', 'addon', 'reentry') AND tx.undone = 0), 0 ) - COALESCE( (SELECT SUM(amount) FROM transactions tx WHERE tx.tournament_id = t.id AND tx.type = 'rake' AND tx.undone = 0), 0 ) as prize_pool FROM tournaments t WHERE t.id = ?`, id) summary, err := m.scanSingleSummary(ctx, row) if err == sql.ErrNoRows { return nil, ErrTournamentNotFound } if err != nil { return nil, fmt.Errorf("tournament: get summary: %w", err) } return summary, nil } func (m *MultiManager) scanSummaries(ctx context.Context, rows *sql.Rows) ([]TournamentSummary, error) { var summaries []TournamentSummary for rows.Next() { var ts TournamentSummary var clockState string var clockRemainingNs, totalElapsedNs int64 var startedAt sql.NullInt64 var playerCount, activePlayers int var prizePool int64 if err := rows.Scan( &ts.ID, &ts.Name, &ts.Status, &ts.CurrentLevel, &clockState, &clockRemainingNs, &totalElapsedNs, &startedAt, &ts.CreatedAt, &playerCount, &activePlayers, &prizePool, ); err != nil { return nil, fmt.Errorf("tournament: scan summary: %w", err) } ts.PlayerCount = playerCount ts.ActivePlayers = activePlayers ts.RemainingMs = clockRemainingNs / 1_000_000 ts.TotalElapsedMs = totalElapsedNs / 1_000_000 ts.PrizePool = prizePool if startedAt.Valid { ts.StartedAt = &startedAt.Int64 } // Get live clock data from registry if available if engine := m.service.registry.Get(ts.ID); engine != nil { snap := engine.Snapshot() ts.RemainingMs = snap.RemainingMs ts.TotalElapsedMs = snap.TotalElapsedMs ts.CurrentLevel = snap.CurrentLevel ts.SmallBlind = snap.Level.SmallBlind ts.BigBlind = snap.Level.BigBlind } summaries = append(summaries, ts) } return summaries, rows.Err() } func (m *MultiManager) scanSingleSummary(ctx context.Context, row *sql.Row) (*TournamentSummary, error) { ts := &TournamentSummary{} var clockState string var clockRemainingNs, totalElapsedNs int64 var startedAt sql.NullInt64 var playerCount, activePlayers int var prizePool int64 if err := row.Scan( &ts.ID, &ts.Name, &ts.Status, &ts.CurrentLevel, &clockState, &clockRemainingNs, &totalElapsedNs, &startedAt, &ts.CreatedAt, &playerCount, &activePlayers, &prizePool, ); err != nil { return nil, err } ts.PlayerCount = playerCount ts.ActivePlayers = activePlayers ts.RemainingMs = clockRemainingNs / 1_000_000 ts.TotalElapsedMs = totalElapsedNs / 1_000_000 ts.PrizePool = prizePool if startedAt.Valid { ts.StartedAt = &startedAt.Int64 } // Get live clock data from registry if available if engine := m.service.registry.Get(ts.ID); engine != nil { snap := engine.Snapshot() ts.RemainingMs = snap.RemainingMs ts.TotalElapsedMs = snap.TotalElapsedMs ts.CurrentLevel = snap.CurrentLevel ts.SmallBlind = snap.Level.SmallBlind ts.BigBlind = snap.Level.BigBlind } return ts, nil }