package seating import ( "context" "database/sql" "encoding/json" "fmt" ) // Blueprint is a venue-level saved table layout configuration. type Blueprint struct { ID int `json:"id"` Name string `json:"name"` TableConfigs []BlueprintTableConfig `json:"table_configs"` } // BlueprintTableConfig is a single table spec within a blueprint. type BlueprintTableConfig struct { Name string `json:"name"` SeatCount int `json:"seat_count"` } // BlueprintService provides CRUD operations for table blueprints. type BlueprintService struct { db *sql.DB } // NewBlueprintService creates a new BlueprintService. func NewBlueprintService(db *sql.DB) *BlueprintService { return &BlueprintService{db: db} } // CreateBlueprint creates a new blueprint. func (s *BlueprintService) CreateBlueprint(ctx context.Context, name string, configs []BlueprintTableConfig) (*Blueprint, error) { if name == "" { return nil, fmt.Errorf("blueprint name is required") } if len(configs) == 0 { return nil, fmt.Errorf("at least one table config is required") } for i, c := range configs { if c.SeatCount < 6 || c.SeatCount > 10 { return nil, fmt.Errorf("table config %d: seat count must be between 6 and 10", i+1) } if c.Name == "" { return nil, fmt.Errorf("table config %d: name is required", i+1) } } configsJSON, err := json.Marshal(configs) if err != nil { return nil, fmt.Errorf("marshal configs: %w", err) } result, err := s.db.ExecContext(ctx, `INSERT INTO table_blueprints (name, table_configs) VALUES (?, ?)`, name, string(configsJSON), ) if err != nil { return nil, fmt.Errorf("create blueprint: %w", err) } id, _ := result.LastInsertId() return &Blueprint{ ID: int(id), Name: name, TableConfigs: configs, }, nil } // GetBlueprint returns a blueprint by ID. func (s *BlueprintService) GetBlueprint(ctx context.Context, id int) (*Blueprint, error) { var bp Blueprint var configsJSON string err := s.db.QueryRowContext(ctx, `SELECT id, name, table_configs FROM table_blueprints WHERE id = ?`, id, ).Scan(&bp.ID, &bp.Name, &configsJSON) if err == sql.ErrNoRows { return nil, fmt.Errorf("blueprint not found") } if err != nil { return nil, fmt.Errorf("get blueprint: %w", err) } if err := json.Unmarshal([]byte(configsJSON), &bp.TableConfigs); err != nil { return nil, fmt.Errorf("unmarshal configs: %w", err) } return &bp, nil } // ListBlueprints returns all blueprints. func (s *BlueprintService) ListBlueprints(ctx context.Context) ([]Blueprint, error) { rows, err := s.db.QueryContext(ctx, `SELECT id, name, table_configs FROM table_blueprints ORDER BY name`, ) if err != nil { return nil, fmt.Errorf("list blueprints: %w", err) } defer rows.Close() var blueprints []Blueprint for rows.Next() { var bp Blueprint var configsJSON string if err := rows.Scan(&bp.ID, &bp.Name, &configsJSON); err != nil { return nil, fmt.Errorf("scan blueprint: %w", err) } if err := json.Unmarshal([]byte(configsJSON), &bp.TableConfigs); err != nil { return nil, fmt.Errorf("unmarshal configs: %w", err) } blueprints = append(blueprints, bp) } return blueprints, rows.Err() } // UpdateBlueprint updates a blueprint. func (s *BlueprintService) UpdateBlueprint(ctx context.Context, bp Blueprint) error { if bp.Name == "" { return fmt.Errorf("blueprint name is required") } for i, c := range bp.TableConfigs { if c.SeatCount < 6 || c.SeatCount > 10 { return fmt.Errorf("table config %d: seat count must be between 6 and 10", i+1) } if c.Name == "" { return fmt.Errorf("table config %d: name is required", i+1) } } configsJSON, err := json.Marshal(bp.TableConfigs) if err != nil { return fmt.Errorf("marshal configs: %w", err) } result, err := s.db.ExecContext(ctx, `UPDATE table_blueprints SET name = ?, table_configs = ?, updated_at = unixepoch() WHERE id = ?`, bp.Name, string(configsJSON), bp.ID, ) if err != nil { return fmt.Errorf("update blueprint: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("blueprint not found") } return nil } // DeleteBlueprint deletes a blueprint. func (s *BlueprintService) DeleteBlueprint(ctx context.Context, id int) error { result, err := s.db.ExecContext(ctx, `DELETE FROM table_blueprints WHERE id = ?`, id, ) if err != nil { return fmt.Errorf("delete blueprint: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("blueprint not found") } return nil } // SaveBlueprintFromTournament creates a blueprint from the current tables in a tournament. func (s *BlueprintService) SaveBlueprintFromTournament(ctx context.Context, db *sql.DB, tournamentID, name string) (*Blueprint, error) { if name == "" { return nil, fmt.Errorf("blueprint name is required") } rows, err := db.QueryContext(ctx, `SELECT name, seat_count FROM tables WHERE tournament_id = ? AND is_active = 1 ORDER BY name`, tournamentID, ) if err != nil { return nil, fmt.Errorf("get tournament tables: %w", err) } defer rows.Close() var configs []BlueprintTableConfig for rows.Next() { var c BlueprintTableConfig if err := rows.Scan(&c.Name, &c.SeatCount); err != nil { return nil, fmt.Errorf("scan table: %w", err) } configs = append(configs, c) } if err := rows.Err(); err != nil { return nil, err } if len(configs) == 0 { return nil, fmt.Errorf("no active tables in tournament") } return s.CreateBlueprint(ctx, name, configs) } // CreateTablesFromBlueprint creates tables in a tournament from a blueprint. func (s *BlueprintService) CreateTablesFromBlueprint(ctx context.Context, db *sql.DB, tournamentID string, blueprintID int) ([]Table, error) { bp, err := s.GetBlueprint(ctx, blueprintID) if err != nil { return nil, err } var tables []Table for _, cfg := range bp.TableConfigs { result, err := db.ExecContext(ctx, `INSERT INTO tables (tournament_id, name, seat_count, is_active) VALUES (?, ?, ?, 1)`, tournamentID, cfg.Name, cfg.SeatCount, ) if err != nil { return nil, fmt.Errorf("create table from blueprint: %w", err) } id, _ := result.LastInsertId() tables = append(tables, Table{ ID: int(id), TournamentID: tournamentID, Name: cfg.Name, SeatCount: cfg.SeatCount, IsActive: true, }) } return tables, nil }