package store import ( "context" "database/sql" "fmt" "strings" "time" "github.com/google/uuid" "plumber/internal/pacific" ) // RankedQuestion is a question row with score / vote annotations for lists. type RankedQuestion struct { ID string AuthorID string AuthorName string Title string Body string City string HuntDate string Hidden bool CreatedAt string Score int Answered bool UserVote int db *sql.DB } // NewQuestion returns a question bound to db (not yet inserted). func NewQuestion(db *sql.DB) *RankedQuestion { return &RankedQuestion{db: db} } // Create inserts the question. Sets ID, HuntDate, and CreatedAt when empty. func (q *RankedQuestion) Create(ctx context.Context) error { if q == nil || q.db == nil { return fmt.Errorf("question: no database") } q.Title = strings.TrimSpace(q.Title) q.Body = strings.TrimSpace(q.Body) q.City = strings.TrimSpace(q.City) if q.ID == "" { q.ID = uuid.NewString() } if q.HuntDate == "" { q.HuntDate = pacific.Today() } if q.CreatedAt == "" { q.CreatedAt = time.Now().UTC().Format(time.RFC3339) } _, err := q.db.ExecContext(ctx, `INSERT INTO questions (id, author_id, title, body, city, hunt_date, hidden, created_at) VALUES ($1, $2, $3, $4, $5, $6, 0, $7)`, q.ID, q.AuthorID, q.Title, q.Body, q.City, q.HuntDate, q.CreatedAt) return err } // Hide marks the question hidden. func (q *RankedQuestion) Hide(ctx context.Context) error { if q == nil || q.db == nil { return fmt.Errorf("question: no database") } _, err := q.db.ExecContext(ctx, `UPDATE questions SET hidden = 1 WHERE id = $1`, q.ID) if err == nil { q.Hidden = true } return err } func ListHunt(ctx context.Context, db *sql.DB, huntDate, viewerID string) ([]RankedQuestion, error) { rows, err := db.QueryContext(ctx, ` SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, COALESCE(SUM(v.value), 0) AS score, CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END AS answered, COALESCE((SELECT value FROM votes WHERE user_id = $1 AND question_id = q.id), 0) AS user_vote FROM questions q JOIN users u ON u.id = q.author_id LEFT JOIN votes v ON v.question_id = q.id LEFT JOIN answers a ON a.question_id = q.id WHERE q.hunt_date = $2 AND q.hidden = 0 GROUP BY q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, a.question_id ORDER BY score DESC, q.created_at ASC`, viewerID, huntDate) if err != nil { return nil, err } defer rows.Close() return scanRankedList(db, rows) } func GetQuestion(ctx context.Context, db *sql.DB, id, viewerID string) (*RankedQuestion, error) { row := db.QueryRowContext(ctx, ` SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, COALESCE((SELECT SUM(value) FROM votes WHERE question_id = q.id), 0) AS score, CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END AS answered, COALESCE((SELECT value FROM votes WHERE user_id = $1 AND question_id = q.id), 0) AS user_vote FROM questions q JOIN users u ON u.id = q.author_id LEFT JOIN answers a ON a.question_id = q.id WHERE q.id = $2`, viewerID, id) q, err := scanRanked(db, row) if err != nil { return nil, err } return &q, nil } func ListQuestionsByAuthor(ctx context.Context, db *sql.DB, authorID string) ([]RankedQuestion, error) { rows, err := db.QueryContext(ctx, ` SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, COALESCE((SELECT SUM(value) FROM votes WHERE question_id = q.id), 0) AS score, CASE WHEN a.question_id IS NULL THEN 0 ELSE 1 END AS answered, 0 AS user_vote FROM questions q JOIN users u ON u.id = q.author_id LEFT JOIN answers a ON a.question_id = q.id WHERE q.author_id = $1 AND q.hidden = 0 ORDER BY q.created_at DESC`, authorID) if err != nil { return nil, err } defer rows.Close() return scanRankedList(db, rows) } func ListQuestionsAnsweredBy(ctx context.Context, db *sql.DB, adminID string) ([]RankedQuestion, error) { rows, err := db.QueryContext(ctx, ` SELECT q.id, q.author_id, u.name, q.title, q.body, q.city, q.hunt_date, q.hidden, q.created_at, COALESCE((SELECT SUM(value) FROM votes WHERE question_id = q.id), 0) AS score, 1 AS answered, 0 AS user_vote FROM answers ans JOIN questions q ON q.id = ans.question_id JOIN users u ON u.id = q.author_id WHERE ans.author_id = $1 AND q.hidden = 0 ORDER BY ans.updated_at DESC`, adminID) if err != nil { return nil, err } defer rows.Close() return scanRankedList(db, rows) } type scanned interface { Scan(dest ...any) error } func scanRanked(db *sql.DB, rows scanned) (RankedQuestion, error) { var q RankedQuestion var hidden, answered int err := rows.Scan(&q.ID, &q.AuthorID, &q.AuthorName, &q.Title, &q.Body, &q.City, &q.HuntDate, &hidden, &q.CreatedAt, &q.Score, &answered, &q.UserVote) q.Hidden = hidden != 0 q.Answered = answered != 0 q.db = db return q, err } func scanRankedList(db *sql.DB, rows *sql.Rows) ([]RankedQuestion, error) { var out []RankedQuestion for rows.Next() { q, err := scanRanked(db, rows) if err != nil { return nil, err } out = append(out, q) } return out, rows.Err() }