You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

146 lines
3.8 KiB
Go

package handler
import (
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/denisovdennis/autohero/internal/storage"
)
// DailyTaskHandler serves daily/weekly task API endpoints.
type DailyTaskHandler struct {
taskStore *storage.DailyTaskStore
heroStore *storage.HeroStore
logger *slog.Logger
}
// NewDailyTaskHandler creates a new DailyTaskHandler.
func NewDailyTaskHandler(taskStore *storage.DailyTaskStore, heroStore *storage.HeroStore, logger *slog.Logger) *DailyTaskHandler {
return &DailyTaskHandler{
taskStore: taskStore,
heroStore: heroStore,
logger: logger,
}
}
// ListHeroTasks returns current daily and weekly tasks with progress.
// GET /api/v1/hero/tasks
func (h *DailyTaskHandler) ListHeroTasks(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 tasks", "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
}
// Lazily create task rows for the current period.
now := time.Now()
if err := h.taskStore.EnsureHeroTasks(r.Context(), hero.ID, now); err != nil {
h.logger.Error("failed to ensure hero tasks", "hero_id", hero.ID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to initialize tasks",
})
return
}
tasks, err := h.taskStore.ListHeroTasks(r.Context(), hero.ID)
if err != nil {
h.logger.Error("failed to list hero tasks", "hero_id", hero.ID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to load tasks",
})
return
}
writeJSON(w, http.StatusOK, tasks)
}
// ClaimTask claims a completed task's reward.
// POST /api/v1/hero/tasks/{taskId}/claim
func (h *DailyTaskHandler) ClaimTask(w http.ResponseWriter, r *http.Request) {
telegramID, ok := resolveTelegramID(r)
if !ok {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "missing telegramId",
})
return
}
taskID := chi.URLParam(r, "taskId")
if taskID == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "taskId is required",
})
return
}
hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
if err != nil {
h.logger.Error("failed to get hero for task 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.taskStore.ClaimTask(r.Context(), hero.ID, taskID)
if err != nil {
h.logger.Warn("failed to claim task", "hero_id", hero.ID, "task_id", taskID, "error", err)
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
return
}
// Apply reward to hero.
switch reward.RewardType {
case "gold":
hero.Gold += int64(reward.RewardAmount)
case "potion":
hero.Potions += reward.RewardAmount
}
if err := h.heroStore.Save(r.Context(), hero); err != nil {
h.logger.Error("failed to save hero after task claim", "hero_id", hero.ID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to save hero",
})
return
}
h.logger.Info("task reward claimed", "hero_id", hero.ID, "task_id", taskID,
"reward_type", reward.RewardType, "reward_amount", reward.RewardAmount)
now := time.Now()
hero.RefreshDerivedCombatStats(now)
writeJSON(w, http.StatusOK, map[string]any{
"reward": reward,
"hero": hero,
})
}