package handler import ( "log/slog" "net/http" "strconv" "github.com/go-chi/chi/v5" "github.com/denisovdennis/autohero/internal/storage" ) // QuestHandler serves quest system API endpoints. type QuestHandler struct { questStore *storage.QuestStore heroStore *storage.HeroStore logStore *storage.LogStore logger *slog.Logger } // NewQuestHandler creates a new QuestHandler. func NewQuestHandler(questStore *storage.QuestStore, heroStore *storage.HeroStore, logStore *storage.LogStore, logger *slog.Logger) *QuestHandler { return &QuestHandler{ questStore: questStore, heroStore: heroStore, logStore: logStore, logger: logger, } } // ListTowns returns all towns. // GET /api/v1/towns func (h *QuestHandler) ListTowns(w http.ResponseWriter, r *http.Request) { towns, err := h.questStore.ListTowns(r.Context()) if err != nil { h.logger.Error("failed to list towns", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to list towns", }) return } writeJSON(w, http.StatusOK, towns) } // ListNPCsByTown returns all NPCs in a town. // GET /api/v1/towns/{townId}/npcs func (h *QuestHandler) ListNPCsByTown(w http.ResponseWriter, r *http.Request) { townIDStr := chi.URLParam(r, "townId") townID, err := strconv.ParseInt(townIDStr, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid townId", }) return } npcs, err := h.questStore.ListNPCsByTown(r.Context(), townID) if err != nil { h.logger.Error("failed to list npcs", "town_id", townID, "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to list npcs", }) return } writeJSON(w, http.StatusOK, npcs) } // ListQuestsByNPC returns all quests offered by an NPC. // GET /api/v1/npcs/{npcId}/quests func (h *QuestHandler) ListQuestsByNPC(w http.ResponseWriter, r *http.Request) { npcIDStr := chi.URLParam(r, "npcId") npcID, err := strconv.ParseInt(npcIDStr, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid npcId", }) return } quests, err := h.questStore.ListQuestsByNPC(r.Context(), npcID) if err != nil { h.logger.Error("failed to list quests", "npc_id", npcID, "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to list quests", }) return } writeJSON(w, http.StatusOK, quests) } // AcceptQuest accepts a quest for the hero. // POST /api/v1/hero/quests/{questId}/accept func (h *QuestHandler) AcceptQuest(w http.ResponseWriter, r *http.Request) { telegramID, ok := resolveTelegramID(r) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "missing telegramId", }) return } questIDStr := chi.URLParam(r, "questId") questID, err := strconv.ParseInt(questIDStr, 10, 64) if err != nil || questID == 0 { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid questId", }) return } req := struct{ QuestID int64 }{QuestID: questID} hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID) if err != nil { h.logger.Error("failed to get hero for quest accept", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to load hero", }) return } if hero == nil { writeJSON(w, http.StatusNotFound, map[string]string{ "error": "hero not found", }) return } if err := h.questStore.AcceptQuest(r.Context(), hero.ID, req.QuestID); err != nil { h.logger.Error("failed to accept quest", "hero_id", hero.ID, "quest_id", req.QuestID, "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to accept quest", }) return } h.logger.Info("quest accepted", "hero_id", hero.ID, "quest_id", req.QuestID) writeJSON(w, http.StatusOK, map[string]string{ "status": "accepted", }) } // ListHeroQuests returns the hero's quest log. // GET /api/v1/hero/quests func (h *QuestHandler) ListHeroQuests(w http.ResponseWriter, r *http.Request) { telegramID, ok := resolveTelegramID(r) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "missing telegramId", }) return } hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID) if err != nil { h.logger.Error("failed to get hero for quest list", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to load hero", }) return } if hero == nil { writeJSON(w, http.StatusNotFound, map[string]string{ "error": "hero not found", }) return } quests, err := h.questStore.ListHeroQuests(r.Context(), hero.ID) if err != nil { h.logger.Error("failed to list hero quests", "hero_id", hero.ID, "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to list quests", }) return } writeJSON(w, http.StatusOK, quests) } // ClaimQuestReward claims a completed quest's reward. // POST /api/v1/hero/quests/{questId}/claim func (h *QuestHandler) ClaimQuestReward(w http.ResponseWriter, r *http.Request) { telegramID, ok := resolveTelegramID(r) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "missing telegramId", }) return } questIDStr := chi.URLParam(r, "questId") questID, err := strconv.ParseInt(questIDStr, 10, 64) if err != nil { h.logger.Error("Error claiming quest", err) writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid questId", }) return } hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID) if err != nil { h.logger.Error("failed to get hero for quest claim", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to load hero", }) return } if hero == nil { writeJSON(w, http.StatusNotFound, map[string]string{ "error": "hero not found", }) return } reward, err := h.questStore.ClaimQuestReward(r.Context(), hero.ID, questID) if err != nil { h.logger.Warn("failed to claim quest reward", "hero_id", hero.ID, "quest_id", questID, "error", err) writeJSON(w, http.StatusBadRequest, map[string]string{ "error": err.Error(), }) return } // Apply rewards to hero. hero.XP += reward.XP hero.Gold += reward.Gold hero.Potions += reward.Potions // Run level-up loop. for hero.LevelUp() { } if err := h.heroStore.Save(r.Context(), hero); err != nil { h.logger.Error("failed to save hero after quest claim", "hero_id", hero.ID, "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to save hero", }) return } h.logger.Info("quest reward claimed", "hero_id", hero.ID, "quest_id", questID, "xp", reward.XP, "gold", reward.Gold, "potions", reward.Potions) writeJSON(w, http.StatusOK, hero) } // AbandonQuest removes a quest from the hero's quest log. // DELETE /api/v1/hero/quests/{questId} func (h *QuestHandler) AbandonQuest(w http.ResponseWriter, r *http.Request) { telegramID, ok := resolveTelegramID(r) if !ok { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "missing telegramId", }) return } questIDStr := chi.URLParam(r, "questId") questID, err := strconv.ParseInt(questIDStr, 10, 64) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid questId", }) return } hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID) if err != nil { h.logger.Error("failed to get hero for quest abandon", "error", err) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "failed to load hero", }) return } if hero == nil { writeJSON(w, http.StatusNotFound, map[string]string{ "error": "hero not found", }) return } if err := h.questStore.AbandonQuest(r.Context(), hero.ID, questID); err != nil { h.logger.Warn("failed to abandon quest", "hero_id", hero.ID, "quest_id", questID, "error", err) writeJSON(w, http.StatusBadRequest, map[string]string{ "error": err.Error(), }) return } h.logger.Info("quest abandoned", "hero_id", hero.ID, "quest_id", questID) writeJSON(w, http.StatusOK, map[string]string{ "status": "abandoned", }) }