package analytics import ( "net/http" "strings" "time" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/handlers" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AdvancedAnalyticsHandler handles advanced analytics HTTP requests (F396-F399) type AdvancedAnalyticsHandler struct { service *services.AdvancedAnalyticsService logger *zap.Logger } // NewAdvancedAnalyticsHandler creates a new advanced analytics handler func NewAdvancedAnalyticsHandler(service *services.AdvancedAnalyticsService, logger *zap.Logger) *AdvancedAnalyticsHandler { if logger == nil { logger = zap.NewNop() } return &AdvancedAnalyticsHandler{service: service, logger: logger} } // getCreatorID extracts and validates the creator user ID from context func (h *AdvancedAnalyticsHandler) getCreatorID(c *gin.Context) (uuid.UUID, bool) { userIDInterface, exists := c.Get("user_id") if !exists { handlers.RespondWithAppError(c, apperrors.New(apperrors.ErrCodeUnauthorized, "authentication required")) return uuid.Nil, false } userID, ok := userIDInterface.(uuid.UUID) if !ok { handlers.RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "invalid user id")) return uuid.Nil, false } return userID, true } // GetTrackHeatmap handles GET /api/v1/creator/analytics/heatmap/:trackId (F396) func (h *AdvancedAnalyticsHandler) GetTrackHeatmap(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } trackIDStr := c.Param("trackId") trackID, err := uuid.Parse(trackIDStr) if err != nil { handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } startDate, endDate := parseDateRange(c) heatmap, err := h.service.GetTrackHeatmap(c.Request.Context(), creatorID, trackID, startDate, endDate) if err != nil { if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "not owned") { handlers.RespondWithAppError(c, apperrors.NewNotFoundError("track")) return } handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get heatmap", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "heatmap": heatmap, "period": gin.H{ "start_date": startDate.Format(time.RFC3339), "end_date": endDate.Format(time.RFC3339), }, }) } // ComparePeriods handles GET /api/v1/creator/analytics/compare (F397) func (h *AdvancedAnalyticsHandler) ComparePeriods(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } // Parse current period now := time.Now() preset := c.DefaultQuery("preset", "week") // week, month, quarter var currentStart, currentEnd, previousStart, previousEnd time.Time switch preset { case "month": currentEnd = now currentStart = now.AddDate(0, -1, 0) previousEnd = currentStart previousStart = currentStart.AddDate(0, -1, 0) case "quarter": currentEnd = now currentStart = now.AddDate(0, -3, 0) previousEnd = currentStart previousStart = currentStart.AddDate(0, -3, 0) default: // week currentEnd = now currentStart = now.AddDate(0, 0, -7) previousEnd = currentStart previousStart = currentStart.AddDate(0, 0, -7) } // Allow custom date overrides if s := c.Query("current_start"); s != "" { if parsed, err := time.Parse(time.RFC3339, s); err == nil { currentStart = parsed } } if e := c.Query("current_end"); e != "" { if parsed, err := time.Parse(time.RFC3339, e); err == nil { currentEnd = parsed } } if s := c.Query("previous_start"); s != "" { if parsed, err := time.Parse(time.RFC3339, s); err == nil { previousStart = parsed } } if e := c.Query("previous_end"); e != "" { if parsed, err := time.Parse(time.RFC3339, e); err == nil { previousEnd = parsed } } comparison, err := h.service.ComparePeriods(c.Request.Context(), creatorID, currentStart, currentEnd, previousStart, previousEnd) if err != nil { handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to compare periods", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "comparison": comparison, }) } // GetMarketplaceAnalytics handles GET /api/v1/creator/analytics/marketplace (F398) func (h *AdvancedAnalyticsHandler) GetMarketplaceAnalytics(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } startDate, endDate := parseDateRange(c) analytics, err := h.service.GetMarketplaceAnalytics(c.Request.Context(), creatorID, startDate, endDate) if err != nil { handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get marketplace analytics", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "marketplace": analytics, "period": gin.H{ "start_date": startDate.Format(time.RFC3339), "end_date": endDate.Format(time.RFC3339), }, }) } // GetMetricAlerts handles GET /api/v1/creator/analytics/alerts (F399) func (h *AdvancedAnalyticsHandler) GetMetricAlerts(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } summary, err := h.service.GetMetricAlerts(c.Request.Context(), creatorID) if err != nil { handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get alerts", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "alerts": summary, }) } // updateAlertPreferenceRequest is the request body for updating alert preferences type updateAlertPreferenceRequest struct { MetricType string `json:"metric_type" binding:"required"` Enabled bool `json:"enabled"` } // UpdateAlertPreference handles PUT /api/v1/creator/analytics/alerts/preferences (F399) func (h *AdvancedAnalyticsHandler) UpdateAlertPreference(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } var req updateAlertPreferenceRequest if err := c.ShouldBindJSON(&req); err != nil { handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid request body")) return } if err := h.service.UpdateAlertPreference(c.Request.Context(), creatorID, req.MetricType, req.Enabled); err != nil { if strings.Contains(err.Error(), "invalid metric type") { handlers.RespondWithAppError(c, apperrors.NewValidationError(err.Error())) return } handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to update preference", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "status": "updated", }) } // createAlertRequest is the request body for creating a metric alert type createAlertRequest struct { MetricType string `json:"metric_type" binding:"required"` Threshold int64 `json:"threshold" binding:"required,min=1"` } // CreateAlert handles POST /api/v1/creator/analytics/alerts (F399) func (h *AdvancedAnalyticsHandler) CreateAlert(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } var req createAlertRequest if err := c.ShouldBindJSON(&req); err != nil { handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid request body")) return } if err := h.service.CreateAlertThreshold(c.Request.Context(), creatorID, req.MetricType, req.Threshold); err != nil { if strings.Contains(err.Error(), "invalid metric type") || strings.Contains(err.Error(), "threshold must be positive") { handlers.RespondWithAppError(c, apperrors.NewValidationError(err.Error())) return } handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to create alert", err)) return } handlers.RespondSuccess(c, http.StatusCreated, gin.H{ "status": "created", }) } // DeleteAlert handles DELETE /api/v1/creator/analytics/alerts/:alertId (F399) func (h *AdvancedAnalyticsHandler) DeleteAlert(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } alertIDStr := c.Param("alertId") alertID, err := uuid.Parse(alertIDStr) if err != nil { handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid alert id")) return } if err := h.service.DeleteAlertThreshold(c.Request.Context(), creatorID, alertID); err != nil { if strings.Contains(err.Error(), "not found") { handlers.RespondWithAppError(c, apperrors.NewNotFoundError("alert")) return } handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete alert", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "status": "deleted", }) } // CheckAlerts handles POST /api/v1/creator/analytics/alerts/check (F399) // Triggers alert checking for the current user func (h *AdvancedAnalyticsHandler) CheckAlerts(c *gin.Context) { creatorID, ok := h.getCreatorID(c) if !ok { return } triggered, err := h.service.CheckAndTriggerAlerts(c.Request.Context(), creatorID) if err != nil { handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to check alerts", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{ "triggered": triggered, "count": len(triggered), }) }