Ctrl K
ComunidadGuías…

internal/handler/scheduling_handler.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"encoding/json"
	"net/http"
	"strconv"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/johnzastrow/actalog/internal/domain"
	"github.com/johnzastrow/actalog/internal/service"
	"github.com/johnzastrow/actalog/pkg/logger"
	"github.com/johnzastrow/actalog/pkg/middleware"
)

type SchedulingHandler struct {
	schedulingService *service.SchedulingService
	logger            *logger.Logger
}

func NewSchedulingHandler(schedulingService *service.SchedulingService, logger *logger.Logger) *SchedulingHandler {
	return &SchedulingHandler{
		schedulingService: schedulingService,
		logger:            logger,
	}
}

// ============================================
// GYM LOCATION HANDLERS
// ============================================

// CreateLocation handles POST /api/gyms/{gym_id}/locations
func (h *SchedulingHandler) CreateLocation(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	var req struct {
		Name        string  `json:"name"`
		Description *string `json:"description"`
		Address     *string `json:"address"`
		Capacity    int     `json:"capacity"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.Name == "" {
		respondError(w, http.StatusBadRequest, "Location name is required")
		return
	}

	location := &domain.GymLocation{
		OrganizationID: gymID,
		Name:           req.Name,
		Description:    req.Description,
		Address:        req.Address,
		Capacity:       req.Capacity,
	}

	if err := h.schedulingService.CreateLocation(userID, location); err != nil {
		if err == service.ErrOrganizationNotFound {
			respondError(w, http.StatusNotFound, "Organization not found")
			return
		}
		h.logger.Error("Failed to create location: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to create location")
		return
	}

	respondJSON(w, http.StatusCreated, location)
}

// ListLocations handles GET /api/gyms/{gym_id}/locations
func (h *SchedulingHandler) ListLocations(w http.ResponseWriter, r *http.Request) {
	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	includeInactive := r.URL.Query().Get("include_inactive") == "true"

	locations, err := h.schedulingService.GetLocationsByOrganization(gymID, includeInactive)
	if err != nil {
		h.logger.Error("Failed to list locations: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to list locations")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"locations": locations,
		"count":     len(locations),
	})
}

// GetLocation handles GET /api/gyms/{gym_id}/locations/{id}
func (h *SchedulingHandler) GetLocation(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid location ID")
		return
	}

	location, err := h.schedulingService.GetLocationByID(id)
	if err != nil {
		if err == service.ErrGymLocationNotFound {
			respondError(w, http.StatusNotFound, "Location not found")
			return
		}
		h.logger.Error("Failed to get location: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get location")
		return
	}

	respondJSON(w, http.StatusOK, location)
}

// UpdateLocation handles PUT /api/gyms/{gym_id}/locations/{id}
func (h *SchedulingHandler) UpdateLocation(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid location ID")
		return
	}

	var req struct {
		Name        string  `json:"name"`
		Description *string `json:"description"`
		Address     *string `json:"address"`
		Capacity    int     `json:"capacity"`
		IsActive    bool    `json:"is_active"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	location := &domain.GymLocation{
		ID:          id,
		Name:        req.Name,
		Description: req.Description,
		Address:     req.Address,
		Capacity:    req.Capacity,
		IsActive:    req.IsActive,
	}

	if err := h.schedulingService.UpdateLocation(userID, location); err != nil {
		if err == service.ErrGymLocationNotFound {
			respondError(w, http.StatusNotFound, "Location not found")
			return
		}
		h.logger.Error("Failed to update location: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to update location")
		return
	}

	respondJSON(w, http.StatusOK, location)
}

// DeleteLocation handles DELETE /api/gyms/{gym_id}/locations/{id}
func (h *SchedulingHandler) DeleteLocation(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid location ID")
		return
	}

	if err := h.schedulingService.DeleteLocation(userID, id); err != nil {
		if err == service.ErrGymLocationNotFound {
			respondError(w, http.StatusNotFound, "Location not found")
			return
		}
		h.logger.Error("Failed to delete location: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to delete location")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Location deleted successfully"})
}

// ============================================
// CLASS TEMPLATE HANDLERS
// ============================================

// CreateTemplate handles POST /api/gyms/{gym_id}/templates
func (h *SchedulingHandler) CreateTemplate(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	var req struct {
		Name              string  `json:"name"`
		Description       *string `json:"description"`
		WorkoutID         *int64  `json:"workout_id"`
		DurationMinutes   int     `json:"duration_minutes"`
		DefaultCapacity   int     `json:"default_capacity"`
		DefaultLocationID *int64  `json:"default_location_id"`
		Color             string  `json:"color"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.Name == "" {
		respondError(w, http.StatusBadRequest, "Template name is required")
		return
	}

	template := &domain.ClassTemplate{
		OrganizationID:    gymID,
		Name:              req.Name,
		Description:       req.Description,
		WorkoutID:         req.WorkoutID,
		DurationMinutes:   req.DurationMinutes,
		DefaultCapacity:   req.DefaultCapacity,
		DefaultLocationID: req.DefaultLocationID,
		Color:             req.Color,
	}

	if err := h.schedulingService.CreateTemplate(userID, template); err != nil {
		if err == service.ErrOrganizationNotFound {
			respondError(w, http.StatusNotFound, "Organization not found")
			return
		}
		h.logger.Error("Failed to create template: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to create template")
		return
	}

	respondJSON(w, http.StatusCreated, template)
}

// ListTemplates handles GET /api/gyms/{gym_id}/templates
func (h *SchedulingHandler) ListTemplates(w http.ResponseWriter, r *http.Request) {
	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	includeInactive := r.URL.Query().Get("include_inactive") == "true"

	templates, err := h.schedulingService.GetTemplatesByOrganization(gymID, includeInactive)
	if err != nil {
		h.logger.Error("Failed to list templates: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to list templates")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"templates": templates,
		"count":     len(templates),
	})
}

// GetTemplate handles GET /api/templates/{id}
func (h *SchedulingHandler) GetTemplate(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	template, err := h.schedulingService.GetTemplateByID(id)
	if err != nil {
		if err == service.ErrClassTemplateNotFound {
			respondError(w, http.StatusNotFound, "Template not found")
			return
		}
		h.logger.Error("Failed to get template: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get template")
		return
	}

	respondJSON(w, http.StatusOK, template)
}

// UpdateTemplate handles PUT /api/templates/{id}
func (h *SchedulingHandler) UpdateTemplate(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	var req struct {
		Name              string  `json:"name"`
		Description       *string `json:"description"`
		WorkoutID         *int64  `json:"workout_id"`
		DurationMinutes   int     `json:"duration_minutes"`
		DefaultCapacity   int     `json:"default_capacity"`
		DefaultLocationID *int64  `json:"default_location_id"`
		Color             string  `json:"color"`
		IsActive          bool    `json:"is_active"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	template := &domain.ClassTemplate{
		ID:                id,
		Name:              req.Name,
		Description:       req.Description,
		WorkoutID:         req.WorkoutID,
		DurationMinutes:   req.DurationMinutes,
		DefaultCapacity:   req.DefaultCapacity,
		DefaultLocationID: req.DefaultLocationID,
		Color:             req.Color,
		IsActive:          req.IsActive,
	}

	if err := h.schedulingService.UpdateTemplate(userID, template); err != nil {
		if err == service.ErrClassTemplateNotFound {
			respondError(w, http.StatusNotFound, "Template not found")
			return
		}
		h.logger.Error("Failed to update template: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to update template")
		return
	}

	respondJSON(w, http.StatusOK, template)
}

// DeleteTemplate handles DELETE /api/templates/{id}
// Query params:
//   - mode: template_only (default), with_all_sessions, with_future_sessions
func (h *SchedulingHandler) DeleteTemplate(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	// Check for mode parameter
	mode := r.URL.Query().Get("mode")
	if mode != "" {
		// Use the new DeleteTemplateWithMode method
		deleteMode := service.DeleteMode(mode)
		switch deleteMode {
		case service.DeleteModeTemplateOnly, service.DeleteModeWithAllSessions, service.DeleteModeWithFutureSessions:
			// Valid mode
		default:
			respondError(w, http.StatusBadRequest, "Invalid mode. Must be: template_only, with_all_sessions, or with_future_sessions")
			return
		}

		result, err := h.schedulingService.DeleteTemplateWithMode(userID, id, deleteMode)
		if err != nil {
			if err == service.ErrClassTemplateNotFound {
				respondError(w, http.StatusNotFound, "Template not found")
				return
			}
			h.logger.Error("Failed to delete template with mode: %v", err)
			respondError(w, http.StatusInternalServerError, "Failed to delete template")
			return
		}

		respondJSON(w, http.StatusOK, map[string]interface{}{
			"message":            "Template deleted successfully",
			"sessions_deleted":   result.SessionsDeleted,
			"notifications_sent": result.NotificationsSent,
			"credits_refunded":   result.CreditsRefunded,
			"mode":               string(result.Mode),
		})
		return
	}

	// Default behavior: just delete the template
	if err := h.schedulingService.DeleteTemplate(userID, id); err != nil {
		if err == service.ErrClassTemplateNotFound {
			respondError(w, http.StatusNotFound, "Template not found")
			return
		}
		h.logger.Error("Failed to delete template: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to delete template")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Template deleted successfully"})
}

// ============================================
// SCHEDULE PREVIEW HANDLERS
// ============================================

// PreviewSchedule handles GET /api/templates/{id}/preview-schedule
func (h *SchedulingHandler) PreviewSchedule(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	// Parse months parameter (default 3)
	monthsStr := r.URL.Query().Get("months")
	months := 3
	if monthsStr != "" {
		if m, err := strconv.Atoi(monthsStr); err == nil && m > 0 && m <= 12 {
			months = m
		}
	}

	dates, err := h.schedulingService.PreviewScheduleDates(id, months)
	if err != nil {
		if err == service.ErrClassTemplateNotFound {
			respondError(w, http.StatusNotFound, "Template not found")
			return
		}
		h.logger.Error("Failed to preview schedule: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to preview schedule")
		return
	}

	// Convert dates to strings for JSON response
	dateStrings := make([]string, len(dates))
	for i, d := range dates {
		dateStrings[i] = d.Format(time.RFC3339)
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"dates": dateStrings,
		"count": len(dates),
	})
}

// ============================================
// SCHEDULE SLOT HANDLERS
// ============================================

// CreateScheduleSlot handles POST /api/templates/{id}/slots
func (h *SchedulingHandler) CreateScheduleSlot(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	templateIDStr := chi.URLParam(r, "id")
	templateID, err := strconv.ParseInt(templateIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	var req struct {
		LocationID         *int64  `json:"location_id"`
		DayOfWeek          int     `json:"day_of_week"`
		DaysOfWeek         []int   `json:"days_of_week"`
		StartTime          string  `json:"start_time"`
		OverrideCapacity   *int    `json:"override_capacity"`
		RecurrenceInterval int     `json:"recurrence_interval"`
		RecurrenceEndType  string  `json:"recurrence_end_type"`
		RecurrenceEndCount *int    `json:"recurrence_end_count"`
		RecurrenceEndDate  *string `json:"recurrence_end_date"`
		EffectiveStartDate *string `json:"effective_start_date"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.StartTime == "" {
		respondError(w, http.StatusBadRequest, "Start time is required")
		return
	}

	// Use DaysOfWeek if provided, otherwise fall back to DayOfWeek
	daysOfWeek := req.DaysOfWeek
	dayOfWeek := req.DayOfWeek
	if len(daysOfWeek) > 0 {
		dayOfWeek = daysOfWeek[0] // Set legacy field to first day
	} else if dayOfWeek >= 0 {
		daysOfWeek = []int{dayOfWeek}
	}

	slot := &domain.ScheduleSlot{
		TemplateID:         templateID,
		LocationID:         req.LocationID,
		DayOfWeek:          dayOfWeek,
		DaysOfWeek:         daysOfWeek,
		StartTime:          req.StartTime,
		OverrideCapacity:   req.OverrideCapacity,
		RecurrenceInterval: req.RecurrenceInterval,
		RecurrenceEndType:  req.RecurrenceEndType,
		RecurrenceEndCount: req.RecurrenceEndCount,
	}

	// Parse recurrence end date
	if req.RecurrenceEndDate != nil && *req.RecurrenceEndDate != "" {
		t, err := time.Parse("2006-01-02", *req.RecurrenceEndDate)
		if err == nil {
			slot.RecurrenceEndDate = &t
		}
	}

	// Parse effective start date
	if req.EffectiveStartDate != nil && *req.EffectiveStartDate != "" {
		t, err := time.Parse("2006-01-02", *req.EffectiveStartDate)
		if err == nil {
			slot.EffectiveStartDate = &t
		}
	}

	if err := h.schedulingService.CreateScheduleSlot(userID, slot); err != nil {
		if err == service.ErrClassTemplateNotFound {
			respondError(w, http.StatusNotFound, "Template not found")
			return
		}
		if err == service.ErrInvalidDayOfWeek {
			respondError(w, http.StatusBadRequest, "Day of week must be 0-6 (Sunday-Saturday)")
			return
		}
		if err == service.ErrInvalidTimeFormat {
			respondError(w, http.StatusBadRequest, "Time must be in HH:MM format")
			return
		}
		h.logger.Error("Failed to create schedule slot: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to create schedule slot")
		return
	}

	respondJSON(w, http.StatusCreated, slot)
}

// ListScheduleSlots handles GET /api/templates/{id}/slots
func (h *SchedulingHandler) ListScheduleSlots(w http.ResponseWriter, r *http.Request) {
	templateIDStr := chi.URLParam(r, "id")
	templateID, err := strconv.ParseInt(templateIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	includeInactive := r.URL.Query().Get("include_inactive") == "true"

	slots, err := h.schedulingService.GetScheduleSlotsByTemplate(templateID, includeInactive)
	if err != nil {
		h.logger.Error("Failed to list schedule slots: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to list schedule slots")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"slots": slots,
		"count": len(slots),
	})
}

// UpdateScheduleSlot handles PUT /api/templates/{template_id}/slots/{id}
func (h *SchedulingHandler) UpdateScheduleSlot(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "slot_id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid slot ID")
		return
	}

	var req struct {
		LocationID         *int64  `json:"location_id"`
		DayOfWeek          int     `json:"day_of_week"`
		DaysOfWeek         []int   `json:"days_of_week"`
		StartTime          string  `json:"start_time"`
		OverrideCapacity   *int    `json:"override_capacity"`
		IsActive           bool    `json:"is_active"`
		RecurrenceInterval int     `json:"recurrence_interval"`
		RecurrenceEndType  string  `json:"recurrence_end_type"`
		RecurrenceEndCount *int    `json:"recurrence_end_count"`
		RecurrenceEndDate  *string `json:"recurrence_end_date"`
		EffectiveStartDate *string `json:"effective_start_date"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	// Use DaysOfWeek if provided, otherwise fall back to DayOfWeek
	daysOfWeek := req.DaysOfWeek
	dayOfWeek := req.DayOfWeek
	if len(daysOfWeek) > 0 {
		dayOfWeek = daysOfWeek[0] // Set legacy field to first day
	} else if dayOfWeek >= 0 {
		daysOfWeek = []int{dayOfWeek}
	}

	slot := &domain.ScheduleSlot{
		ID:                 id,
		LocationID:         req.LocationID,
		DayOfWeek:          dayOfWeek,
		DaysOfWeek:         daysOfWeek,
		StartTime:          req.StartTime,
		OverrideCapacity:   req.OverrideCapacity,
		IsActive:           req.IsActive,
		RecurrenceInterval: req.RecurrenceInterval,
		RecurrenceEndType:  req.RecurrenceEndType,
		RecurrenceEndCount: req.RecurrenceEndCount,
	}

	// Parse recurrence end date
	if req.RecurrenceEndDate != nil && *req.RecurrenceEndDate != "" {
		t, err := time.Parse("2006-01-02", *req.RecurrenceEndDate)
		if err == nil {
			slot.RecurrenceEndDate = &t
		}
	}

	// Parse effective start date
	if req.EffectiveStartDate != nil && *req.EffectiveStartDate != "" {
		t, err := time.Parse("2006-01-02", *req.EffectiveStartDate)
		if err == nil {
			slot.EffectiveStartDate = &t
		}
	}

	if err := h.schedulingService.UpdateScheduleSlot(userID, slot); err != nil {
		if err == service.ErrScheduleSlotNotFound {
			respondError(w, http.StatusNotFound, "Schedule slot not found")
			return
		}
		if err == service.ErrInvalidDayOfWeek {
			respondError(w, http.StatusBadRequest, "Day of week must be 0-6 (Sunday-Saturday)")
			return
		}
		h.logger.Error("Failed to update schedule slot: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to update schedule slot")
		return
	}

	respondJSON(w, http.StatusOK, slot)
}

// DeleteScheduleSlot handles DELETE /api/templates/{template_id}/slots/{id}
func (h *SchedulingHandler) DeleteScheduleSlot(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "slot_id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid slot ID")
		return
	}

	if err := h.schedulingService.DeleteScheduleSlot(userID, id); err != nil {
		if err == service.ErrScheduleSlotNotFound {
			respondError(w, http.StatusNotFound, "Schedule slot not found")
			return
		}
		h.logger.Error("Failed to delete schedule slot: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to delete schedule slot")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Schedule slot deleted successfully"})
}

// ============================================
// CLASS SESSION HANDLERS
// ============================================

// CreateSession handles POST /api/gyms/{gym_id}/sessions
func (h *SchedulingHandler) CreateSession(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	var req struct {
		TemplateID  *int64  `json:"template_id"`
		LocationID  *int64  `json:"location_id"`
		Name        string  `json:"name"`
		Description *string `json:"description"`
		WorkoutID   *int64  `json:"workout_id"`
		StartTime   string  `json:"start_time"`
		EndTime     string  `json:"end_time"`
		Capacity    int     `json:"capacity"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.StartTime == "" {
		respondError(w, http.StatusBadRequest, "Start time is required")
		return
	}

	startTime, err := time.Parse(time.RFC3339, req.StartTime)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid start time format (use RFC3339)")
		return
	}

	var endTime time.Time
	if req.EndTime != "" {
		endTime, err = time.Parse(time.RFC3339, req.EndTime)
		if err != nil {
			respondError(w, http.StatusBadRequest, "Invalid end time format (use RFC3339)")
			return
		}
	} else {
		// Default to 1 hour session
		endTime = startTime.Add(time.Hour)
	}

	session := &domain.ClassSession{
		OrganizationID: gymID,
		TemplateID:     req.TemplateID,
		LocationID:     req.LocationID,
		Name:           req.Name,
		Description:    req.Description,
		WorkoutID:      req.WorkoutID,
		StartTime:      startTime,
		EndTime:        endTime,
		Capacity:       req.Capacity,
	}

	if err := h.schedulingService.CreateSession(userID, session); err != nil {
		if err == service.ErrOrganizationNotFound {
			respondError(w, http.StatusNotFound, "Organization not found")
			return
		}
		h.logger.Error("Failed to create session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to create session")
		return
	}

	respondJSON(w, http.StatusCreated, session)
}

// ListSessions handles GET /api/gyms/{gym_id}/sessions
func (h *SchedulingHandler) ListSessions(w http.ResponseWriter, r *http.Request) {
	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	// Parse date range from query params
	startDateStr := r.URL.Query().Get("start_date")
	endDateStr := r.URL.Query().Get("end_date")

	var startDate, endDate time.Time

	if startDateStr != "" {
		startDate, err = time.Parse("2006-01-02", startDateStr)
		if err != nil {
			respondError(w, http.StatusBadRequest, "Invalid start_date format (use YYYY-MM-DD)")
			return
		}
	} else {
		// Default to start of today
		now := time.Now()
		startDate = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	}

	if endDateStr != "" {
		endDate, err = time.Parse("2006-01-02", endDateStr)
		if err != nil {
			respondError(w, http.StatusBadRequest, "Invalid end_date format (use YYYY-MM-DD)")
			return
		}
		// Add 1 day to include the end date
		endDate = endDate.Add(24*time.Hour - time.Second)
	} else {
		// Default to 7 days from start
		endDate = startDate.Add(7*24*time.Hour - time.Second)
	}

	// Check if user is authenticated (for user status)
	var sessions []*domain.ClassSession
	userID, hasUser := middleware.GetUserID(r.Context())

	if hasUser {
		sessions, err = h.schedulingService.GetSessionsByOrganizationForUser(gymID, userID, startDate, endDate)
	} else {
		sessions, err = h.schedulingService.GetSessionsByOrganization(gymID, startDate, endDate)
	}

	if err != nil {
		h.logger.Error("Failed to list sessions: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to list sessions")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"sessions":   sessions,
		"count":      len(sessions),
		"start_date": startDate.Format("2006-01-02"),
		"end_date":   endDate.Format("2006-01-02"),
	})
}

// GetSession handles GET /api/gyms/{gym_id}/sessions/{id}
func (h *SchedulingHandler) GetSession(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	// Load session with full workout details
	session, err := h.schedulingService.GetSessionByIDWithWorkout(id)
	if err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		h.logger.Error("Failed to get session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get session")
		return
	}

	respondJSON(w, http.StatusOK, session)
}

// UpdateSession handles PUT /api/gyms/{gym_id}/sessions/{id}
func (h *SchedulingHandler) UpdateSession(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	var req struct {
		Name        string  `json:"name"`
		Description *string `json:"description"`
		WorkoutID   *int64  `json:"workout_id"`
		StartTime   string  `json:"start_time"`
		EndTime     string  `json:"end_time"`
		Capacity    int     `json:"capacity"`
		LocationID  *int64  `json:"location_id"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	startTime, err := time.Parse(time.RFC3339, req.StartTime)
	if err != nil && req.StartTime != "" {
		respondError(w, http.StatusBadRequest, "Invalid start time format")
		return
	}

	endTime, err := time.Parse(time.RFC3339, req.EndTime)
	if err != nil && req.EndTime != "" {
		respondError(w, http.StatusBadRequest, "Invalid end time format")
		return
	}

	session := &domain.ClassSession{
		ID:          id,
		Name:        req.Name,
		Description: req.Description,
		WorkoutID:   req.WorkoutID,
		StartTime:   startTime,
		EndTime:     endTime,
		Capacity:    req.Capacity,
		LocationID:  req.LocationID,
	}

	if err := h.schedulingService.UpdateSession(userID, session); err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		h.logger.Error("Failed to update session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to update session")
		return
	}

	respondJSON(w, http.StatusOK, session)
}

// CancelSession handles POST /api/gyms/{gym_id}/sessions/{id}/cancel
func (h *SchedulingHandler) CancelSession(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	var req struct {
		Reason string `json:"reason"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if err := h.schedulingService.CancelSession(userID, id, req.Reason); err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		if err == service.ErrSessionNotActive {
			respondError(w, http.StatusBadRequest, "Session cannot be cancelled")
			return
		}
		h.logger.Error("Failed to cancel session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to cancel session")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Session cancelled successfully"})
}

// ============================================
// RESERVATION HANDLERS
// ============================================

// CreateReservation handles POST /api/sessions/{session_id}/reserve
func (h *SchedulingHandler) CreateReservation(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	reservation, err := h.schedulingService.CreateReservation(userID, sessionID)
	if err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		if err == service.ErrSessionNotActive {
			respondError(w, http.StatusBadRequest, "Session is not available for reservations")
			return
		}
		if err == service.ErrAlreadyReserved {
			respondError(w, http.StatusConflict, "You already have a reservation for this session")
			return
		}
		if err == service.ErrSessionFull {
			respondError(w, http.StatusConflict, "Session is full")
			return
		}
		h.logger.Error("Failed to create reservation: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to create reservation")
		return
	}

	respondJSON(w, http.StatusCreated, reservation)
}

// CancelReservation handles DELETE /api/sessions/{session_id}/reserve
func (h *SchedulingHandler) CancelReservation(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	var req struct {
		Reason string `json:"reason"`
	}

	// Reason is optional
	_ = json.NewDecoder(r.Body).Decode(&req)

	if err := h.schedulingService.CancelReservation(userID, sessionID, req.Reason); err != nil {
		if err == service.ErrReservationNotFound {
			respondError(w, http.StatusNotFound, "Reservation not found")
			return
		}
		if err == service.ErrCannotCancelReservation {
			respondError(w, http.StatusBadRequest, "This reservation cannot be cancelled")
			return
		}
		h.logger.Error("Failed to cancel reservation: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to cancel reservation")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Reservation cancelled successfully"})
}

// GetUserUpcomingReservations handles GET /api/users/me/reservations/upcoming
func (h *SchedulingHandler) GetUserUpcomingReservations(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	reservations, err := h.schedulingService.GetUpcomingUserReservations(userID)
	if err != nil {
		h.logger.Error("Failed to get upcoming reservations: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get reservations")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"reservations": reservations,
		"count":        len(reservations),
	})
}

// GetSessionRoster handles GET /api/sessions/{session_id}/roster
func (h *SchedulingHandler) GetSessionRoster(w http.ResponseWriter, r *http.Request) {
	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	reservations, err := h.schedulingService.GetReservationRoster(sessionID)
	if err != nil {
		h.logger.Error("Failed to get session roster: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get roster")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"roster": reservations,
		"count":  len(reservations),
	})
}

// CheckInReservation handles POST /api/sessions/{session_id}/check-in/{user_id}
func (h *SchedulingHandler) CheckInReservation(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	reservationIDStr := chi.URLParam(r, "reservation_id")
	reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid reservation ID")
		return
	}

	if err := h.schedulingService.CheckInReservation(coachUserID, reservationID); err != nil {
		if err == service.ErrReservationNotFound {
			respondError(w, http.StatusNotFound, "Reservation not found")
			return
		}
		h.logger.Error("Failed to check in reservation: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to check in")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Checked in successfully"})
}

// ============================================
// COACH ASSIGNMENT HANDLERS
// ============================================

// AssignCoach handles POST /api/gyms/{gym_id}/coaches
func (h *SchedulingHandler) AssignCoach(w http.ResponseWriter, r *http.Request) {
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	var req struct {
		UserID int64 `json:"user_id"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.UserID == 0 {
		respondError(w, http.StatusBadRequest, "User ID is required")
		return
	}

	if err := h.schedulingService.AssignCoach(adminUserID, gymID, req.UserID); err != nil {
		h.logger.Error("Failed to assign coach: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to assign coach")
		return
	}

	respondJSON(w, http.StatusCreated, map[string]string{"message": "Coach assigned successfully"})
}

// ListCoaches handles GET /api/gyms/{gym_id}/coaches
func (h *SchedulingHandler) ListCoaches(w http.ResponseWriter, r *http.Request) {
	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	includeInactive := r.URL.Query().Get("include_inactive") == "true"

	coaches, err := h.schedulingService.GetCoachesByOrganization(gymID, includeInactive)
	if err != nil {
		h.logger.Error("Failed to list coaches: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to list coaches")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"coaches": coaches,
		"count":   len(coaches),
	})
}

// UnassignCoach handles DELETE /api/gyms/{gym_id}/coaches/{id}
func (h *SchedulingHandler) UnassignCoach(w http.ResponseWriter, r *http.Request) {
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid coach assignment ID")
		return
	}

	if err := h.schedulingService.UnassignCoach(adminUserID, id); err != nil {
		if err == service.ErrCoachAssignmentNotFound {
			respondError(w, http.StatusNotFound, "Coach assignment not found")
			return
		}
		h.logger.Error("Failed to unassign coach: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to unassign coach")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Coach unassigned successfully"})
}

// ============================================
// COACH PORTAL HANDLERS
// ============================================

// GetCoachSessions handles GET /api/coaches/me/sessions
// Admins see all upcoming sessions across all organizations.
// Coaches see only sessions they are assigned to.
func (h *SchedulingHandler) GetCoachSessions(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	limitStr := r.URL.Query().Get("limit")
	limit := 20
	if limitStr != "" {
		if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
			limit = l
		}
	}

	role, _ := middleware.GetUserRole(r.Context())

	var sessions []*domain.ClassSession
	var err error

	if role == "admin" {
		sessions, err = h.schedulingService.GetAllUpcomingSessions(limit)
	} else {
		sessions, err = h.schedulingService.GetCoachUpcomingSessions(coachUserID, limit)
	}

	if err != nil {
		h.logger.Error("Failed to get coach sessions: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get sessions")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"sessions": sessions,
		"count":    len(sessions),
	})
}

// CompleteSession handles POST /api/sessions/{session_id}/complete
func (h *SchedulingHandler) CompleteSession(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	if err := h.schedulingService.CompleteSession(userID, sessionID); err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		if err == service.ErrSessionNotActive {
			respondError(w, http.StatusBadRequest, "Session cannot be completed")
			return
		}
		h.logger.Error("Failed to complete session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to complete session")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Session completed successfully"})
}

// AddSessionCoach handles POST /api/sessions/{session_id}/coaches
func (h *SchedulingHandler) AddSessionCoach(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	var req struct {
		UserID int64 `json:"user_id"`
		IsLead bool  `json:"is_lead"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.UserID == 0 {
		respondError(w, http.StatusBadRequest, "user_id is required")
		return
	}

	if err := h.schedulingService.AddCoachToSession(userID, sessionID, req.UserID, req.IsLead); err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		h.logger.Error("Failed to add coach to session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to add coach to session")
		return
	}

	respondJSON(w, http.StatusCreated, map[string]string{"message": "Coach added to session"})
}

// RemoveSessionCoach handles DELETE /api/sessions/{session_id}/coaches/{user_id}
func (h *SchedulingHandler) RemoveSessionCoach(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	coachUserIDStr := chi.URLParam(r, "user_id")
	coachUserID, err := strconv.ParseInt(coachUserIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid user ID")
		return
	}

	if err := h.schedulingService.RemoveCoachFromSession(userID, sessionID, coachUserID); err != nil {
		h.logger.Error("Failed to remove coach from session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to remove coach from session")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Coach removed from session"})
}

// GetSessionCoaches handles GET /api/sessions/{session_id}/coaches
func (h *SchedulingHandler) GetSessionCoaches(w http.ResponseWriter, r *http.Request) {
	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	coaches, err := h.schedulingService.GetSessionCoaches(sessionID)
	if err != nil {
		h.logger.Error("Failed to get session coaches: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get session coaches")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"coaches": coaches,
		"count":   len(coaches),
	})
}

// MarkNoShow handles POST /api/sessions/{session_id}/no-show/{reservation_id}
func (h *SchedulingHandler) MarkNoShow(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	reservationIDStr := chi.URLParam(r, "reservation_id")
	reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid reservation ID")
		return
	}

	if err := h.schedulingService.MarkNoShow(coachUserID, reservationID); err != nil {
		if err == service.ErrReservationNotFound {
			respondError(w, http.StatusNotFound, "Reservation not found")
			return
		}
		h.logger.Error("Failed to mark no-show: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to mark no-show")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Marked as no-show successfully"})
}

// GetTemplateCoaches handles GET /api/templates/{id}/coaches
func (h *SchedulingHandler) GetTemplateCoaches(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	coaches, err := h.schedulingService.GetTemplateCoaches(id)
	if err != nil {
		h.logger.Error("Failed to get template coaches: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get template coaches")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"coaches": coaches,
	})
}

// AddTemplateCoach handles POST /api/templates/{id}/coaches
func (h *SchedulingHandler) AddTemplateCoach(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	idStr := chi.URLParam(r, "id")
	templateID, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	var req struct {
		UserID int64 `json:"user_id"`
		IsLead bool  `json:"is_lead"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if req.UserID == 0 {
		respondError(w, http.StatusBadRequest, "User ID is required")
		return
	}

	if err := h.schedulingService.AddTemplateCoach(userID, templateID, req.UserID, req.IsLead); err != nil {
		h.logger.Error("Failed to add template coach: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to add template coach")
		return
	}

	respondJSON(w, http.StatusCreated, map[string]string{"message": "Coach added successfully"})
}

// RemoveTemplateCoach handles DELETE /api/templates/{id}/coaches/{user_id}
func (h *SchedulingHandler) RemoveTemplateCoach(w http.ResponseWriter, r *http.Request) {
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	templateIDStr := chi.URLParam(r, "id")
	templateID, err := strconv.ParseInt(templateIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid template ID")
		return
	}

	userIDStr := chi.URLParam(r, "user_id")
	userID, err := strconv.ParseInt(userIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid user ID")
		return
	}

	if err := h.schedulingService.RemoveTemplateCoach(adminUserID, templateID, userID); err != nil {
		h.logger.Error("Failed to remove template coach: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to remove template coach")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Coach removed successfully"})
}

// BatchUpdateSessionWorkout handles PUT /api/admin/gyms/{gym_id}/sessions/batch-workout
func (h *SchedulingHandler) BatchUpdateSessionWorkout(w http.ResponseWriter, r *http.Request) {
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	gymIDStr := chi.URLParam(r, "gym_id")
	gymID, err := strconv.ParseInt(gymIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid gym ID")
		return
	}

	var req struct {
		SessionIDs []int64 `json:"session_ids"`
		WorkoutID  *int64  `json:"workout_id"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if len(req.SessionIDs) == 0 {
		respondError(w, http.StatusBadRequest, "session_ids is required")
		return
	}

	count, err := h.schedulingService.BatchUpdateSessionWorkout(adminUserID, gymID, req.SessionIDs, req.WorkoutID)
	if err != nil {
		h.logger.Error("Failed to batch update session workouts: %v", err)
		respondError(w, http.StatusInternalServerError, err.Error())
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"updated_count": count,
	})
}

// ============================================
// COACH-SPECIFIC HANDLERS
// These handlers verify org-level coach access before delegating
// to existing service methods. Used by /api/coaches/ routes.
// ============================================

// CoachGetSessionRoster handles GET /api/coaches/sessions/{session_id}/roster
func (h *SchedulingHandler) CoachGetSessionRoster(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	// Admins bypass org check; coaches must be assigned to the session's org
	role, _ := middleware.GetUserRole(r.Context())
	if role != "admin" {
		if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil {
			if err == service.ErrNotAuthorized {
				respondError(w, http.StatusForbidden, "Not a coach for this organization")
				return
			}
			if err == service.ErrClassSessionNotFound {
				respondError(w, http.StatusNotFound, "Session not found")
				return
			}
			h.logger.Error("Failed to verify coach access: %v", err)
			respondError(w, http.StatusInternalServerError, "Failed to verify access")
			return
		}
	}

	reservations, err := h.schedulingService.GetReservationRoster(sessionID)
	if err != nil {
		h.logger.Error("Failed to get session roster: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to get roster")
		return
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"roster": reservations,
		"count":  len(reservations),
	})
}

// CoachCheckInReservation handles POST /api/coaches/sessions/{session_id}/check-in/{reservation_id}
func (h *SchedulingHandler) CoachCheckInReservation(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	reservationIDStr := chi.URLParam(r, "reservation_id")
	reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid reservation ID")
		return
	}

	role, _ := middleware.GetUserRole(r.Context())
	if role != "admin" {
		if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil {
			if err == service.ErrNotAuthorized {
				respondError(w, http.StatusForbidden, "Not a coach for this organization")
				return
			}
			if err == service.ErrClassSessionNotFound {
				respondError(w, http.StatusNotFound, "Session not found")
				return
			}
			h.logger.Error("Failed to verify coach access: %v", err)
			respondError(w, http.StatusInternalServerError, "Failed to verify access")
			return
		}
	}

	if err := h.schedulingService.CheckInReservation(coachUserID, reservationID); err != nil {
		if err == service.ErrReservationNotFound {
			respondError(w, http.StatusNotFound, "Reservation not found")
			return
		}
		h.logger.Error("Failed to check in reservation: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to check in")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Checked in successfully"})
}

// CoachMarkNoShow handles POST /api/coaches/sessions/{session_id}/no-show/{reservation_id}
func (h *SchedulingHandler) CoachMarkNoShow(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	reservationIDStr := chi.URLParam(r, "reservation_id")
	reservationID, err := strconv.ParseInt(reservationIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid reservation ID")
		return
	}

	role, _ := middleware.GetUserRole(r.Context())
	if role != "admin" {
		if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil {
			if err == service.ErrNotAuthorized {
				respondError(w, http.StatusForbidden, "Not a coach for this organization")
				return
			}
			if err == service.ErrClassSessionNotFound {
				respondError(w, http.StatusNotFound, "Session not found")
				return
			}
			h.logger.Error("Failed to verify coach access: %v", err)
			respondError(w, http.StatusInternalServerError, "Failed to verify access")
			return
		}
	}

	if err := h.schedulingService.MarkNoShow(coachUserID, reservationID); err != nil {
		if err == service.ErrReservationNotFound {
			respondError(w, http.StatusNotFound, "Reservation not found")
			return
		}
		h.logger.Error("Failed to mark no-show: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to mark no-show")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Marked as no-show successfully"})
}

// CoachCompleteSession handles POST /api/coaches/sessions/{session_id}/complete
func (h *SchedulingHandler) CoachCompleteSession(w http.ResponseWriter, r *http.Request) {
	coachUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	sessionIDStr := chi.URLParam(r, "session_id")
	sessionID, err := strconv.ParseInt(sessionIDStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid session ID")
		return
	}

	role, _ := middleware.GetUserRole(r.Context())
	if role != "admin" {
		if err := h.schedulingService.VerifyCoachAccessToSession(coachUserID, sessionID); err != nil {
			if err == service.ErrNotAuthorized {
				respondError(w, http.StatusForbidden, "Not a coach for this organization")
				return
			}
			if err == service.ErrClassSessionNotFound {
				respondError(w, http.StatusNotFound, "Session not found")
				return
			}
			h.logger.Error("Failed to verify coach access: %v", err)
			respondError(w, http.StatusInternalServerError, "Failed to verify access")
			return
		}
	}

	if err := h.schedulingService.CompleteSession(coachUserID, sessionID); err != nil {
		if err == service.ErrClassSessionNotFound {
			respondError(w, http.StatusNotFound, "Session not found")
			return
		}
		if err == service.ErrSessionNotActive {
			respondError(w, http.StatusBadRequest, "Session cannot be completed")
			return
		}
		h.logger.Error("Failed to complete session: %v", err)
		respondError(w, http.StatusInternalServerError, "Failed to complete session")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "Session completed successfully"})
}