package api import ( "database/sql" "encoding/json" "log/slog" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/syncserver/internal/models" "github.com/syncserver/internal/scheduler" ) type ScheduleHandler struct { db *sql.DB } func NewScheduleHandler(db *sql.DB) *ScheduleHandler { return &ScheduleHandler{db: db} } func (h *ScheduleHandler) List(w http.ResponseWriter, r *http.Request) { repo := models.NewScheduleRepository(h.db) schedules, err := repo.GetAll() if err != nil { slog.Error("failed to fetch schedules", "error", err) writeError(w, http.StatusInternalServerError, "failed to fetch schedules") return } pairRepo := models.NewSyncPairRepository(h.db) pairs, err := pairRepo.GetAll() if err != nil { slog.Error("failed to fetch sync pairs", "error", err) writeError(w, http.StatusInternalServerError, "failed to fetch sync pairs") return } pairMap := make(map[int64]string) for _, p := range pairs { pairMap[p.ID] = p.Name } out := make([]ScheduleResponse, len(schedules)) for i, s := range schedules { out[i] = scheduleToResp(s, pairMap) } writeJSON(w, out) } func (h *ScheduleHandler) Get(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } repo := models.NewScheduleRepository(h.db) s, err := repo.GetByID(id) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "schedule not found") return } if err != nil { slog.Error("failed to fetch schedule", "id", id, "error", err) writeError(w, http.StatusInternalServerError, "failed to fetch schedule") return } pairRepo := models.NewSyncPairRepository(h.db) pairs, err := pairRepo.GetAll() if err != nil { slog.Error("failed to fetch sync pairs", "error", err) writeError(w, http.StatusInternalServerError, "failed to fetch sync pairs") return } pairMap := make(map[int64]string) for _, p := range pairs { pairMap[p.ID] = p.Name } writeJSON(w, scheduleToResp(*s, pairMap)) } func (h *ScheduleHandler) Create(w http.ResponseWriter, r *http.Request) { var req CreateScheduleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.SyncPairID <= 0 { writeError(w, http.StatusBadRequest, "sync_pair_id is required") return } if req.CronExpr == "" { writeError(w, http.StatusBadRequest, "cron_expr is required") return } _, err := scheduler.ParseCron(req.CronExpr) if err != nil { writeError(w, http.StatusBadRequest, "invalid cron expression: "+err.Error()) return } pairRepo := models.NewSyncPairRepository(h.db) _, err = pairRepo.GetByID(req.SyncPairID) if err == sql.ErrNoRows { writeError(w, http.StatusBadRequest, "sync_pair not found") return } if err != nil { slog.Error("failed to fetch sync pair", "id", req.SyncPairID, "error", err) writeError(w, http.StatusInternalServerError, "failed to validate sync pair") return } expr, _ := scheduler.ParseCron(req.CronExpr) var nextRun *time.Time if expr != nil { t := scheduler.NextRun(expr, time.Now().UTC()) nextRun = &t } s := &models.Schedule{ SyncPairID: req.SyncPairID, CronExpr: req.CronExpr, NextRunAt: nextRun, Enabled: req.Enabled, } repo := models.NewScheduleRepository(h.db) id, err := repo.Create(s) if err != nil { slog.Error("failed to create schedule", "error", err) writeError(w, http.StatusInternalServerError, "failed to create schedule") return } s.ID = id pairs, _ := pairRepo.GetAll() pairMap := make(map[int64]string) for _, p := range pairs { pairMap[p.ID] = p.Name } w.Header().Set("Location", "/api/schedules/"+strconv.FormatInt(id, 10)) writeJSON(w, scheduleToResp(*s, pairMap), http.StatusCreated) } func (h *ScheduleHandler) Update(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } var req UpdateScheduleRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } repo := models.NewScheduleRepository(h.db) existing, err := repo.GetByID(id) if err == sql.ErrNoRows { writeError(w, http.StatusNotFound, "schedule not found") return } if err != nil { slog.Error("failed to fetch schedule", "id", id, "error", err) writeError(w, http.StatusInternalServerError, "failed to fetch schedule") return } if req.CronExpr != "" { _, err := scheduler.ParseCron(req.CronExpr) if err != nil { writeError(w, http.StatusBadRequest, "invalid cron expression: "+err.Error()) return } existing.CronExpr = req.CronExpr expr, _ := scheduler.ParseCron(req.CronExpr) if expr != nil { t := scheduler.NextRun(expr, time.Now().UTC()) existing.NextRunAt = &t } } if req.Enabled { existing.Enabled = true } else if req.Enabled == false { existing.Enabled = false } if err := repo.Update(existing); err != nil { slog.Error("failed to update schedule", "id", id, "error", err) writeError(w, http.StatusInternalServerError, "failed to update schedule") return } pairRepo := models.NewSyncPairRepository(h.db) pairs, _ := pairRepo.GetAll() pairMap := make(map[int64]string) for _, p := range pairs { pairMap[p.ID] = p.Name } writeJSON(w, scheduleToResp(*existing, pairMap)) } func (h *ScheduleHandler) Delete(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "invalid id") return } repo := models.NewScheduleRepository(h.db) if err := repo.Delete(id); err != nil { slog.Error("failed to delete schedule", "id", id, "error", err) writeError(w, http.StatusInternalServerError, "failed to delete schedule") return } w.WriteHeader(http.StatusNoContent) } func scheduleToResp(s models.Schedule, pairMap map[int64]string) ScheduleResponse { resp := ScheduleResponse{ ID: s.ID, SyncPairID: s.SyncPairID, SyncPairName: pairMap[s.SyncPairID], CronExpr: s.CronExpr, Enabled: s.Enabled, CreatedAt: s.CreatedAt.Format(time.RFC3339), } if s.NextRunAt != nil { t := s.NextRunAt.Format(time.RFC3339) resp.NextRun = &t } return resp }