package handler import ( "encoding/json" "errors" "net/http" "github.com/go-chi/chi/v5" "github.com/prabhupadasays/spsays-bff/internal/middleware" "github.com/prabhupadasays/spsays-bff/internal/repository" "github.com/prabhupadasays/spsays-bff/internal/service" "github.com/prabhupadasays/spsays-bff/pkg/response" ) // QuizHandler serves quiz authoring (admin) and quiz taking (users). type QuizHandler struct { svc *service.QuizService } func NewQuizHandler(svc *service.QuizService) *QuizHandler { return &QuizHandler{svc: svc} } func writeQuizError(w http.ResponseWriter, err error) { switch { case errors.Is(err, repository.ErrQuizNotFound), errors.Is(err, service.ErrQuizUnpublished): response.Error(w, http.StatusNotFound, "quiz not found") case errors.Is(err, repository.ErrQuizNotStarted), errors.Is(err, repository.ErrQuizNotFinished): response.Error(w, http.StatusConflict, err.Error()) case errors.Is(err, repository.ErrQuizNotCurrent), errors.Is(err, repository.ErrQuizAlreadyAnswer), errors.Is(err, service.ErrQuizLocked): response.Error(w, http.StatusConflict, err.Error()) case errors.Is(err, repository.ErrQuizBadOption), errors.Is(err, repository.ErrQuizChoiceCount), errors.Is(err, service.ErrQuizNoCorrect), errors.Is(err, service.ErrQuizTitle), errors.Is(err, service.ErrQuizNoQuestions), errors.Is(err, service.ErrQuizTooMany), errors.Is(err, service.ErrQuizPrompt), errors.Is(err, service.ErrQuizOptionCount), errors.Is(err, service.ErrQuizOptionBody), errors.Is(err, service.ErrQuizOneCorrect), errors.Is(err, service.ErrQuizTarget): response.Error(w, http.StatusBadRequest, err.Error()) default: response.Error(w, http.StatusInternalServerError, "quiz request failed") } } // ── Admin ───────────────────────────────────────────────────────────────── type quizOptionReq struct { Body string `json:"body"` IsCorrect bool `json:"is_correct"` } type quizQuestionReq struct { Prompt string `json:"prompt"` Explanation string `json:"explanation"` Multi bool `json:"multi"` Options []quizOptionReq `json:"options"` } type quizReq struct { Title string `json:"title"` Description string `json:"description"` TargetURL string `json:"target_url"` TargetTitle string `json:"target_title"` Questions []quizQuestionReq `json:"questions"` } func (q quizReq) draft() service.QuizDraft { d := service.QuizDraft{ Title: q.Title, Description: q.Description, TargetURL: q.TargetURL, TargetTitle: q.TargetTitle, } for _, qq := range q.Questions { in := repository.QuestionInput{Prompt: qq.Prompt, Explanation: qq.Explanation, Multi: qq.Multi} for _, o := range qq.Options { in.Options = append(in.Options, repository.OptionInput{Body: o.Body, IsCorrect: o.IsCorrect}) } d.Questions = append(d.Questions, in) } return d } func (h *QuizHandler) AdminList(w http.ResponseWriter, r *http.Request) { list, err := h.svc.ListAdmin(r.Context()) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, list) } func (h *QuizHandler) AdminGet(w http.ResponseWriter, r *http.Request) { q, err := h.svc.GetAdmin(r.Context(), chi.URLParam(r, "id")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, q) } func (h *QuizHandler) Create(w http.ResponseWriter, r *http.Request) { var req quizReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON body") return } q, err := h.svc.Create(r.Context(), middleware.UserID(r.Context()), req.draft()) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusCreated, q) } func (h *QuizHandler) Update(w http.ResponseWriter, r *http.Request) { var req quizReq if err := json.NewDecoder(r.Body).Decode(&req); err != nil { response.Error(w, http.StatusBadRequest, "invalid JSON body") return } q, err := h.svc.Update(r.Context(), chi.URLParam(r, "id"), req.draft()) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, q) } func (h *QuizHandler) setPublished(w http.ResponseWriter, r *http.Request, on bool) { q, err := h.svc.SetPublished(r.Context(), chi.URLParam(r, "id"), on) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, q) } func (h *QuizHandler) Publish(w http.ResponseWriter, r *http.Request) { h.setPublished(w, r, true) } func (h *QuizHandler) Unpublish(w http.ResponseWriter, r *http.Request) { h.setPublished(w, r, false) } func (h *QuizHandler) Delete(w http.ResponseWriter, r *http.Request) { if err := h.svc.Delete(r.Context(), chi.URLParam(r, "id")); err != nil { writeQuizError(w, err) return } response.NoContent(w) } func (h *QuizHandler) Participants(w http.ResponseWriter, r *http.Request) { list, err := h.svc.Participants(r.Context(), chi.URLParam(r, "id")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, list) } func (h *QuizHandler) Responses(w http.ResponseWriter, r *http.Request) { stats, err := h.svc.Responses(r.Context(), chi.URLParam(r, "id")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, stats) } func (h *QuizHandler) ParticipantResult(w http.ResponseWriter, r *http.Request) { res, err := h.svc.ParticipantResult(r.Context(), chi.URLParam(r, "id"), chi.URLParam(r, "userID")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, res) } // ResetParticipant — DELETE /admin/quizzes/{id}/participants/{userID}. Deletes // that person's attempt so the quiz is pending for them again. func (h *QuizHandler) ResetParticipant(w http.ResponseWriter, r *http.Request) { if err := h.svc.ResetParticipant(r.Context(), chi.URLParam(r, "id"), chi.URLParam(r, "userID")); err != nil { writeQuizError(w, err) return } w.WriteHeader(http.StatusNoContent) } // ── Users ───────────────────────────────────────────────────────────────── // ForPath — GET /quizzes/for?path=… Signed in only; returns the quiz on that // page with the caller's progress, or null when the page has none. func (h *QuizHandler) ForPath(w http.ResponseWriter, r *http.Request) { card, err := h.svc.ForPath(r.Context(), middleware.UserID(r.Context()), r.URL.Query().Get("path")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, card) } func (h *QuizHandler) List(w http.ResponseWriter, r *http.Request) { list, err := h.svc.ListPublished(r.Context(), middleware.UserID(r.Context())) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, list) } func (h *QuizHandler) Get(w http.ResponseWriter, r *http.Request) { card, err := h.svc.Card(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, card) } func (h *QuizHandler) Start(w http.ResponseWriter, r *http.Request) { play, err := h.svc.Start(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, play) } func (h *QuizHandler) Answer(w http.ResponseWriter, r *http.Request) { var req struct { QuestionID string `json:"question_id"` OptionIDs []string `json:"option_ids"` // Single-option form, still accepted from older clients. OptionID string `json:"option_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.QuestionID == "" { response.Error(w, http.StatusBadRequest, "question_id and option_ids are required") return } if len(req.OptionIDs) == 0 && req.OptionID != "" { req.OptionIDs = []string{req.OptionID} } if len(req.OptionIDs) == 0 { response.Error(w, http.StatusBadRequest, "question_id and option_ids are required") return } play, err := h.svc.Answer(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id"), req.QuestionID, req.OptionIDs) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, play) } // Result — GET /quizzes/{id}/result. The caller's own; the user id is taken // from the session, so there is no way to ask for anyone else's. func (h *QuizHandler) Result(w http.ResponseWriter, r *http.Request) { res, err := h.svc.Result(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id")) if err != nil { writeQuizError(w, err) return } response.JSON(w, http.StatusOK, res) }