package handler import ( "math" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/prabhupadasays/spsays-bff/internal/repository" "github.com/prabhupadasays/spsays-bff/pkg/response" "go.mongodb.org/mongo-driver/v2/bson" ) type BookHandler struct { repo *repository.BookRepo } func NewBookHandler(repo *repository.BookRepo) *BookHandler { return &BookHandler{repo: repo} } // --------------------------------------------------------------------------- // GET /books — list all books (lightweight summaries only) // --------------------------------------------------------------------------- func (h *BookHandler) ListBooks(w http.ResponseWriter, r *http.Request) { docs, err := h.repo.GetAllBookMetas(r.Context()) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch books") return } response.JSON(w, http.StatusOK, docs) } // --------------------------------------------------------------------------- // GET /books/{code} — full book metadata (chapters, cantos, līlās, etc.) // --------------------------------------------------------------------------- func (h *BookHandler) GetBook(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") doc, err := h.repo.GetBookMeta(r.Context(), code) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch book") return } if doc == nil { response.Error(w, http.StatusNotFound, "book not found") return } response.JSON(w, http.StatusOK, doc) } // --------------------------------------------------------------------------- // VERSE BOOKS WITHOUT DIVISIONS (BG, SB1962, BS, MM, NBS) // // GET /books/{code}/{chapter} → verse index for sidebar // GET /books/{code}/{chapter}/{verse} → single verse with full content // GET /books/{code}/{chapter}/verses → paginated verses for the chapter // --------------------------------------------------------------------------- // GetChapterVerseIndex returns the verse index for a chapter (for sidebar). // GET /books/{code}/{chapter} func (h *BookHandler) GetChapterVerseIndex(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") chapterStr := chi.URLParam(r, "chapter") chapter, err := strconv.Atoi(chapterStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid chapter number") return } verses, err := h.repo.GetVerseIndex(r.Context(), code, nil, chapter) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch chapter info") return } response.JSON(w, http.StatusOK, map[string]interface{}{ "book": code, "chapter": chapter, "total_verses": len(verses), "verses": verses, }) } // GetVerse returns a single verse with full content. // GET /books/{code}/{chapter}/{verse} func (h *BookHandler) GetVerse(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") chapterStr := chi.URLParam(r, "chapter") verseStr := chi.URLParam(r, "verse") chapter, err := strconv.Atoi(chapterStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid chapter number") return } verse, err := strconv.Atoi(verseStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid verse number") return } doc, err := h.repo.GetSingleVerse(r.Context(), code, nil, chapter, verse) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch verse") return } if doc == nil { response.Error(w, http.StatusNotFound, "verse not found") return } response.JSON(w, http.StatusOK, doc) } // GetChapterVerses returns paginated verses for a chapter. // GET /books/{code}/{chapter}/verses?page=&limit= func (h *BookHandler) GetChapterVerses(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") chapterStr := chi.URLParam(r, "chapter") chapter, err := strconv.Atoi(chapterStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid chapter number") return } page, limit := parsePagination(r) docs, total, err := h.repo.GetVerses(r.Context(), code, nil, chapter, page, limit) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch verses") return } totalPages := int(math.Ceil(float64(total) / float64(limit))) response.JSONWithMeta(w, http.StatusOK, docs, &response.Meta{ Page: page, Limit: limit, Total: total, TotalPages: totalPages, }) } // --------------------------------------------------------------------------- // VERSE BOOKS WITH DIVISIONS // // SB: division = canto → /books/sb/{canto}/{chapter}/{verse} // CC: division = līlā → /books/cc/{lila}/{chapter}/{verse} // // GET /books/{code}/{division} → chapter list for that division // GET /books/{code}/{division}/{chapter} → verse index for sidebar // GET /books/{code}/{division}/{chapter}/{verse} → single verse // GET /books/{code}/{division}/{chapter}/verses → paginated verses // --------------------------------------------------------------------------- // GetDivisionInfo returns the chapter listing for a specific division (canto/līlā). // It extracts the relevant division from the book_meta data blob. // GET /books/sb/{division} or /books/cc/{division} func (h *BookHandler) GetDivisionInfo(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") division := chi.URLParam(r, "division") doc, err := h.repo.GetBookMeta(r.Context(), code) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch book") return } if doc == nil { response.Error(w, http.StatusNotFound, "book not found") return } // Extract the division's chapter list from the data blob. // SB data: { cantos: [{ number, title, totalChapters, chapters: [{number, title, totalVerses}] }] } // CC data: { lilas: [{ key, name, totalChapters, chapters: [{number, title, totalVerses}] }] } result := extractDivisionFromMeta(code, division, doc.Data) if result == nil { response.Error(w, http.StatusNotFound, "division not found") return } response.JSON(w, http.StatusOK, result) } // GetDivisionChapterVerseIndex returns the verse index for a chapter within a division. // GET /books/{code}/{division}/{chapter} func (h *BookHandler) GetDivisionChapterVerseIndex(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") division := chi.URLParam(r, "division") chapterStr := chi.URLParam(r, "chapter") chapter, err := strconv.Atoi(chapterStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid chapter number") return } verses, err := h.repo.GetVerseIndex(r.Context(), code, &division, chapter) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch chapter info") return } response.JSON(w, http.StatusOK, map[string]interface{}{ "book": code, "division": division, "chapter": chapter, "total_verses": len(verses), "verses": verses, }) } // GetDivisionVerse returns a single verse within a division. // GET /books/{code}/{division}/{chapter}/{verse} func (h *BookHandler) GetDivisionVerse(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") division := chi.URLParam(r, "division") chapterStr := chi.URLParam(r, "chapter") verseStr := chi.URLParam(r, "verse") chapter, err := strconv.Atoi(chapterStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid chapter number") return } verse, err := strconv.Atoi(verseStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid verse number") return } doc, err := h.repo.GetSingleVerse(r.Context(), code, &division, chapter, verse) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch verse") return } if doc == nil { response.Error(w, http.StatusNotFound, "verse not found") return } response.JSON(w, http.StatusOK, doc) } // GetDivisionChapterVerses returns paginated verses within a division. // GET /books/{code}/{division}/{chapter}/verses?page=&limit= func (h *BookHandler) GetDivisionChapterVerses(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") division := chi.URLParam(r, "division") chapterStr := chi.URLParam(r, "chapter") chapter, err := strconv.Atoi(chapterStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid chapter number") return } page, limit := parsePagination(r) docs, total, err := h.repo.GetVerses(r.Context(), code, &division, chapter, page, limit) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch verses") return } totalPages := int(math.Ceil(float64(total) / float64(limit))) response.JSONWithMeta(w, http.StatusOK, docs, &response.Meta{ Page: page, Limit: limit, Total: total, TotalPages: totalPages, }) } // --------------------------------------------------------------------------- // PROSE BOOKS — simple chapters (BBD, EJ, EK, MG, NOD, TLC, SSR, DS, etc.) // // GET /books/{code}/chapters → chapter list (sidebar) // GET /books/{code}/chapters/{slug} → single chapter content // --------------------------------------------------------------------------- // ListProseChapters returns chapter listing for a prose book. // GET /books/{code}/chapters?volume=&type= // // For books with verses (ISO, BS, NOI), also appends verse entries // (mantras/texts) from the book metadata so the sidebar is complete. func (h *BookHandler) ListProseChapters(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") volumeStr := r.URL.Query().Get("volume") chapterType := r.URL.Query().Get("type") volume := 0 if v, e := strconv.Atoi(volumeStr); e == nil { volume = v } var typePtr *string if chapterType != "" { typePtr = &chapterType } docs, err := h.repo.GetProseChapters(r.Context(), code, volume, typePtr) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch chapters") return } // Strip paragraphs from list response — only return sidebar info type ChapterSummary struct { ID interface{} `json:"id"` Book string `json:"book"` Volume int `json:"volume"` Chapter *int `json:"chapter,omitempty"` Title string `json:"title"` Type string `json:"type"` Slug string `json:"slug"` Order int `json:"order"` Mantra *int `json:"mantra,omitempty"` Text *int `json:"text,omitempty"` } summaries := make([]ChapterSummary, 0, len(docs)) for _, d := range docs { summaries = append(summaries, ChapterSummary{ ID: d.ID, Book: d.Book, Volume: d.Volume, Chapter: d.Chapter, Title: d.Title, Type: d.Type, Slug: d.Slug, Order: d.Order, }) } // For verse+prose books (ISO, BS, NOI), append verse entries from book meta // so the sidebar includes mantras/texts alongside intro segments. if chapterType == "" || chapterType == "verse" { meta, metaErr := h.repo.GetBookMeta(r.Context(), code) if metaErr == nil && meta != nil { if dataMap, ok := meta.Data.(bson.D); ok { for _, elem := range dataMap { if elem.Key == "mantras" { // ISO: mantras array with {number, slug, title} if arr, ok := elem.Value.(bson.A); ok { baseOrder := len(summaries) + 1 for i, item := range arr { if m, ok := item.(bson.D); ok { num := 0 slug := "" title := "" for _, f := range m { switch f.Key { case "number": if n, ok := f.Value.(int32); ok { num = int(n) } else if n, ok := f.Value.(int64); ok { num = int(n) } case "slug": if s, ok := f.Value.(string); ok { slug = s } case "title": if s, ok := f.Value.(string); ok { title = s } } } if slug == "" { slug = strconv.Itoa(num) } mantraNum := num summaries = append(summaries, ChapterSummary{ Book: code, Title: title, Type: "verse", Slug: slug, Order: baseOrder + i, Mantra: &mantraNum, }) } } } } else if elem.Key == "texts" { // BS, NOI: texts array with {number, slug, title} if arr, ok := elem.Value.(bson.A); ok { baseOrder := len(summaries) + 1 for i, item := range arr { if m, ok := item.(bson.D); ok { num := 0 slug := "" title := "" for _, f := range m { switch f.Key { case "number": if n, ok := f.Value.(int32); ok { num = int(n) } else if n, ok := f.Value.(int64); ok { num = int(n) } case "slug": if s, ok := f.Value.(string); ok { slug = s } case "title": if s, ok := f.Value.(string); ok { title = s } } } if slug == "" { slug = strconv.Itoa(num) } textNum := num summaries = append(summaries, ChapterSummary{ Book: code, Title: title, Type: "verse", Slug: slug, Order: baseOrder + i, Text: &textNum, }) } } } } } } } } response.JSON(w, http.StatusOK, summaries) } // GetProseChapter returns a single prose chapter with full content. // GET /books/{code}/chapters/{slug}?volume= func (h *BookHandler) GetProseChapter(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") slug := chi.URLParam(r, "slug") volumeStr := r.URL.Query().Get("volume") volume := 0 if v, e := strconv.Atoi(volumeStr); e == nil { volume = v } doc, err := h.repo.GetProseChapter(r.Context(), code, volume, slug) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch chapter") return } if doc == nil { response.Error(w, http.StatusNotFound, "chapter not found") return } response.JSON(w, http.StatusOK, doc) } // --------------------------------------------------------------------------- // VOLUME-BASED PROSE BOOKS (KB) // // GET /books/{code}/volumes/{volume} → chapter list for volume // GET /books/{code}/volumes/{volume}/{slug} → single chapter content // --------------------------------------------------------------------------- // ListVolumeChapters returns chapters for a specific volume. // GET /books/{code}/volumes/{volume} func (h *BookHandler) ListVolumeChapters(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") volumeStr := chi.URLParam(r, "volume") volume, err := strconv.Atoi(volumeStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid volume number") return } docs, err := h.repo.GetProseChapters(r.Context(), code, volume, nil) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch chapters") return } // Strip paragraphs from list response type ChapterSummary struct { ID interface{} `json:"id"` Book string `json:"book"` Volume int `json:"volume"` Chapter *int `json:"chapter,omitempty"` Title string `json:"title"` Type string `json:"type"` Slug string `json:"slug"` Order int `json:"order"` } summaries := make([]ChapterSummary, len(docs)) for i, d := range docs { summaries[i] = ChapterSummary{ ID: d.ID, Book: d.Book, Volume: d.Volume, Chapter: d.Chapter, Title: d.Title, Type: d.Type, Slug: d.Slug, Order: d.Order, } } response.JSON(w, http.StatusOK, summaries) } // GetVolumeChapter returns a single chapter within a volume. // GET /books/{code}/volumes/{volume}/{slug} func (h *BookHandler) GetVolumeChapter(w http.ResponseWriter, r *http.Request) { code := chi.URLParam(r, "code") volumeStr := chi.URLParam(r, "volume") slug := chi.URLParam(r, "slug") volume, err := strconv.Atoi(volumeStr) if err != nil { response.Error(w, http.StatusBadRequest, "invalid volume number") return } doc, err := h.repo.GetProseChapter(r.Context(), code, volume, slug) if err != nil { response.Error(w, http.StatusInternalServerError, "failed to fetch chapter") return } if doc == nil { response.Error(w, http.StatusNotFound, "chapter not found") return } response.JSON(w, http.StatusOK, doc) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func parsePagination(r *http.Request) (page, limit int) { page = 1 limit = 20 if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 0 { page = p } if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 100 { limit = l } return } // extractDivisionFromMeta pulls the relevant division (canto/līlā) from the // book_meta data blob and returns it as a map, or nil if not found. func extractDivisionFromMeta(code, division string, data interface{}) map[string]interface{} { dataMap, ok := data.(bson.D) if !ok { return nil } divNum, numErr := strconv.Atoi(division) switch code { case "sb": // SB: data.cantos is an array, division is canto number (1-indexed) for _, elem := range dataMap { if elem.Key != "cantos" { continue } cantos, ok := elem.Value.(bson.A) if !ok { return nil } if numErr != nil || divNum < 1 || divNum > len(cantos) { return nil } canto, ok := cantos[divNum-1].(bson.D) if !ok { return nil } return bsonDToMap(canto) } case "cc": // CC: data.lilas is an array, division is līlā key ("adi", "madhya", "antya") for _, elem := range dataMap { if elem.Key != "lilas" { continue } lilas, ok := elem.Value.(bson.A) if !ok { return nil } for _, l := range lilas { lila, ok := l.(bson.D) if !ok { continue } for _, field := range lila { if field.Key == "key" && field.Value == division { return bsonDToMap(lila) } } } } } return nil } // bsonDToMap converts a bson.D to a map[string]interface{} for JSON serialization. func bsonDToMap(d bson.D) map[string]interface{} { m := make(map[string]interface{}, len(d)) for _, elem := range d { switch v := elem.Value.(type) { case bson.D: m[elem.Key] = bsonDToMap(v) case bson.A: arr := make([]interface{}, len(v)) for i, item := range v { if sub, ok := item.(bson.D); ok { arr[i] = bsonDToMap(sub) } else { arr[i] = item } } m[elem.Key] = arr default: m[elem.Key] = v } } return m }