// Package blind provides blind structure management: CRUD operations, // the structure wizard algorithm, and built-in template definitions. package blind import ( "context" "database/sql" "fmt" "time" ) // BlindStructure represents a reusable blind level progression. type BlindStructure struct { ID int64 `json:"id"` Name string `json:"name"` IsBuiltin bool `json:"is_builtin"` GameTypeDefault string `json:"game_type_default"` Notes string `json:"notes"` Levels []BlindLevel `json:"levels,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // BlindLevel represents a single level in a blind structure. type BlindLevel struct { ID int64 `json:"id,omitempty"` StructureID int64 `json:"structure_id,omitempty"` Position int `json:"position"` LevelType string `json:"level_type"` GameType string `json:"game_type"` SmallBlind int64 `json:"small_blind"` BigBlind int64 `json:"big_blind"` Ante int64 `json:"ante"` BBAnte int64 `json:"bb_ante"` DurationSeconds int `json:"duration_seconds"` ChipUpDenominationValue *int64 `json:"chip_up_denomination_value,omitempty"` Notes string `json:"notes"` } // StructureService provides CRUD operations for blind structures. type StructureService struct { db *sql.DB } // NewStructureService creates a new StructureService. func NewStructureService(db *sql.DB) *StructureService { return &StructureService{db: db} } // validateLevels checks that levels meet all requirements. func validateLevels(levels []BlindLevel) error { if len(levels) == 0 { return fmt.Errorf("at least one level is required") } hasRound := false for i, l := range levels { if l.Position != i { return fmt.Errorf("positions must be contiguous starting from 0, got %d at index %d", l.Position, i) } if l.DurationSeconds <= 0 { return fmt.Errorf("level %d: duration must be positive", i) } if l.LevelType == "round" { hasRound = true if l.SmallBlind >= l.BigBlind && l.BigBlind > 0 { return fmt.Errorf("level %d: small blind (%d) must be less than big blind (%d)", i, l.SmallBlind, l.BigBlind) } } if l.LevelType != "round" && l.LevelType != "break" { return fmt.Errorf("level %d: invalid level type %q (must be 'round' or 'break')", i, l.LevelType) } } if !hasRound { return fmt.Errorf("at least one round level is required") } return nil } // CreateStructure creates a new blind structure with levels. func (s *StructureService) CreateStructure(ctx context.Context, name string, levels []BlindLevel) (*BlindStructure, error) { if name == "" { return nil, fmt.Errorf("structure name is required") } if err := validateLevels(levels); err != nil { return nil, err } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return nil, fmt.Errorf("begin transaction: %w", err) } defer tx.Rollback() now := time.Now().Unix() gameTypeDefault := "nlhe" if len(levels) > 0 && levels[0].GameType != "" { gameTypeDefault = levels[0].GameType } res, err := tx.ExecContext(ctx, "INSERT INTO blind_structures (name, is_builtin, game_type_default, notes, created_at, updated_at) VALUES (?, 0, ?, '', ?, ?)", name, gameTypeDefault, now, now, ) if err != nil { return nil, fmt.Errorf("insert structure: %w", err) } id, err := res.LastInsertId() if err != nil { return nil, fmt.Errorf("get structure id: %w", err) } if err := insertLevels(ctx, tx, id, levels); err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("commit: %w", err) } return s.GetStructure(ctx, id) } // insertLevels inserts levels into the blind_levels table within a transaction. func insertLevels(ctx context.Context, tx *sql.Tx, structureID int64, levels []BlindLevel) error { for i, l := range levels { gameType := l.GameType if gameType == "" { gameType = "nlhe" } levelType := l.LevelType if levelType == "" { levelType = "round" } _, err := tx.ExecContext(ctx, `INSERT INTO blind_levels (structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, chip_up_denomination_value, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, structureID, l.Position, levelType, gameType, l.SmallBlind, l.BigBlind, l.Ante, l.BBAnte, l.DurationSeconds, l.ChipUpDenominationValue, l.Notes, ) if err != nil { return fmt.Errorf("insert level %d: %w", i, err) } } return nil } // GetStructure retrieves a blind structure by ID, including levels ordered by position. func (s *StructureService) GetStructure(ctx context.Context, id int64) (*BlindStructure, error) { bs := &BlindStructure{} var isBuiltin int var createdAt, updatedAt int64 err := s.db.QueryRowContext(ctx, "SELECT id, name, is_builtin, game_type_default, notes, created_at, updated_at FROM blind_structures WHERE id = ?", id, ).Scan(&bs.ID, &bs.Name, &isBuiltin, &bs.GameTypeDefault, &bs.Notes, &createdAt, &updatedAt) if err == sql.ErrNoRows { return nil, fmt.Errorf("blind structure not found: %d", id) } if err != nil { return nil, fmt.Errorf("get structure: %w", err) } bs.IsBuiltin = isBuiltin != 0 bs.CreatedAt = time.Unix(createdAt, 0) bs.UpdatedAt = time.Unix(updatedAt, 0) rows, err := s.db.QueryContext(ctx, `SELECT id, structure_id, position, level_type, game_type, small_blind, big_blind, ante, bb_ante, duration_seconds, chip_up_denomination_value, notes FROM blind_levels WHERE structure_id = ? ORDER BY position`, id, ) if err != nil { return nil, fmt.Errorf("get levels: %w", err) } defer rows.Close() for rows.Next() { var l BlindLevel if err := rows.Scan(&l.ID, &l.StructureID, &l.Position, &l.LevelType, &l.GameType, &l.SmallBlind, &l.BigBlind, &l.Ante, &l.BBAnte, &l.DurationSeconds, &l.ChipUpDenominationValue, &l.Notes); err != nil { return nil, fmt.Errorf("scan level: %w", err) } bs.Levels = append(bs.Levels, l) } return bs, rows.Err() } // ListStructures returns all blind structures (without levels). func (s *StructureService) ListStructures(ctx context.Context) ([]BlindStructure, error) { rows, err := s.db.QueryContext(ctx, "SELECT id, name, is_builtin, game_type_default, notes, created_at, updated_at FROM blind_structures ORDER BY is_builtin DESC, name", ) if err != nil { return nil, fmt.Errorf("list structures: %w", err) } defer rows.Close() var structs []BlindStructure for rows.Next() { var bs BlindStructure var isBuiltin int var createdAt, updatedAt int64 if err := rows.Scan(&bs.ID, &bs.Name, &isBuiltin, &bs.GameTypeDefault, &bs.Notes, &createdAt, &updatedAt); err != nil { return nil, fmt.Errorf("scan structure: %w", err) } bs.IsBuiltin = isBuiltin != 0 bs.CreatedAt = time.Unix(createdAt, 0) bs.UpdatedAt = time.Unix(updatedAt, 0) structs = append(structs, bs) } return structs, rows.Err() } // UpdateStructure updates a blind structure name and replaces its levels. func (s *StructureService) UpdateStructure(ctx context.Context, id int64, name string, levels []BlindLevel) error { if name == "" { return fmt.Errorf("structure name is required") } if err := validateLevels(levels); err != nil { return err } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin transaction: %w", err) } defer tx.Rollback() now := time.Now().Unix() res, err := tx.ExecContext(ctx, "UPDATE blind_structures SET name = ?, updated_at = ? WHERE id = ?", name, now, id, ) if err != nil { return fmt.Errorf("update structure: %w", err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("blind structure not found: %d", id) } // Replace levels if _, err := tx.ExecContext(ctx, "DELETE FROM blind_levels WHERE structure_id = ?", id); err != nil { return fmt.Errorf("delete old levels: %w", err) } if err := insertLevels(ctx, tx, id, levels); err != nil { return err } return tx.Commit() } // DeleteStructure deletes a blind structure. Built-in structures cannot be deleted. func (s *StructureService) DeleteStructure(ctx context.Context, id int64) error { var isBuiltin int err := s.db.QueryRowContext(ctx, "SELECT is_builtin FROM blind_structures WHERE id = ?", id).Scan(&isBuiltin) if err == sql.ErrNoRows { return fmt.Errorf("blind structure not found: %d", id) } if err != nil { return fmt.Errorf("check structure: %w", err) } if isBuiltin != 0 { return fmt.Errorf("cannot delete built-in blind structure") } // Check if referenced by active tournaments var refCount int err = s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM tournaments WHERE blind_structure_id = ? AND status NOT IN ('completed', 'cancelled')", id, ).Scan(&refCount) if err != nil { return fmt.Errorf("check references: %w", err) } if refCount > 0 { return fmt.Errorf("blind structure is referenced by %d active tournament(s)", refCount) } res, err := s.db.ExecContext(ctx, "DELETE FROM blind_structures WHERE id = ?", id) if err != nil { return fmt.Errorf("delete structure: %w", err) } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("blind structure not found: %d", id) } return nil } // DuplicateStructure creates an independent copy of a blind structure. func (s *StructureService) DuplicateStructure(ctx context.Context, id int64, newName string) (*BlindStructure, error) { original, err := s.GetStructure(ctx, id) if err != nil { return nil, fmt.Errorf("get original: %w", err) } if newName == "" { newName = original.Name + " (Copy)" } return s.CreateStructure(ctx, newName, original.Levels) }