package api import ( "net/http" "strconv" "time" "github.com/rcourtman/pulse-go-rewrite/internal/ai" "github.com/rcourtman/pulse-go-rewrite/internal/license" "github.com/rcourtman/pulse-go-rewrite/internal/utils" "github.com/rs/zerolog/log" ) const aiIntelligenceUpgradeURL = "https://pulsemonitor.app/pro" // HandleGetPatterns returns detected failure patterns (GET /api/ai/intelligence/patterns) func (h *AISettingsHandler) HandleGetPatterns(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "patterns": []interface{}{}, "message": "Patrol service not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write patterns response") } return } detector := patrol.GetPatternDetector() if detector == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "patterns": []interface{}{}, "message": "Pattern detector not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write patterns response") } return } // Get resource filter if provided resourceID := r.URL.Query().Get("resource_id") patterns := detector.GetPatterns() var result []map[string]interface{} for key, pattern := range patterns { if resourceID != "" && pattern.ResourceID != resourceID { continue } result = append(result, map[string]interface{}{ "key": key, "resource_id": pattern.ResourceID, "event_type": pattern.EventType, "occurrences": pattern.Occurrences, "average_interval": pattern.AverageInterval.String(), "average_duration": pattern.AverageDuration.String(), "last_occurrence": pattern.LastOccurrence, "confidence": pattern.Confidence, }) } locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) if locked { w.Header().Set("X-License-Required", "true") w.Header().Set("X-License-Feature", license.FeatureAIPatrol) } count := len(result) if locked { result = []map[string]interface{}{} } if err := utils.WriteJSONResponse(w, map[string]interface{}{ "patterns": result, "count": count, "license_required": locked, "upgrade_url": aiIntelligenceUpgradeURL, }); err != nil { log.Error().Err(err).Msg("Failed to write patterns response") } } // HandleGetPredictions returns failure predictions (GET /api/ai/intelligence/predictions) func (h *AISettingsHandler) HandleGetPredictions(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "predictions": []interface{}{}, "message": "Patrol service not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write predictions response") } return } detector := patrol.GetPatternDetector() if detector == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "predictions": []interface{}{}, "message": "Pattern detector not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write predictions response") } return } // Get resource filter if provided resourceID := r.URL.Query().Get("resource_id") var predictions []ai.FailurePrediction if resourceID != "" { predictions = detector.GetPredictionsForResource(resourceID) } else { predictions = detector.GetPredictions() } var result []map[string]interface{} for _, pred := range predictions { isOverdue := pred.DaysUntil < 0 result = append(result, map[string]interface{}{ "resource_id": pred.ResourceID, "event_type": pred.EventType, "predicted_at": pred.PredictedAt, "days_until": pred.DaysUntil, "confidence": pred.Confidence, "basis": pred.Basis, "is_overdue": isOverdue, }) } locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) if locked { w.Header().Set("X-License-Required", "true") w.Header().Set("X-License-Feature", license.FeatureAIPatrol) } count := len(result) if locked { result = []map[string]interface{}{} } if err := utils.WriteJSONResponse(w, map[string]interface{}{ "predictions": result, "count": count, "license_required": locked, "upgrade_url": aiIntelligenceUpgradeURL, }); err != nil { log.Error().Err(err).Msg("Failed to write predictions response") } } // HandleGetCorrelations returns detected resource correlations (GET /api/ai/intelligence/correlations) func (h *AISettingsHandler) HandleGetCorrelations(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "correlations": []interface{}{}, "message": "Patrol service not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write correlations response") } return } detector := patrol.GetCorrelationDetector() if detector == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "correlations": []interface{}{}, "message": "Correlation detector not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write correlations response") } return } // Get resource filter if provided resourceID := r.URL.Query().Get("resource_id") var correlations []*ai.Correlation if resourceID != "" { correlations = detector.GetCorrelationsForResource(resourceID) } else { correlations = detector.GetCorrelations() } var result []map[string]interface{} for _, corr := range correlations { result = append(result, map[string]interface{}{ "source_id": corr.SourceID, "source_name": corr.SourceName, "source_type": corr.SourceType, "target_id": corr.TargetID, "target_name": corr.TargetName, "target_type": corr.TargetType, "event_pattern": corr.EventPattern, "occurrences": corr.Occurrences, "avg_delay": corr.AvgDelay.String(), "confidence": corr.Confidence, "last_seen": corr.LastSeen, "description": corr.Description, }) } locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) if locked { w.Header().Set("X-License-Required", "true") w.Header().Set("X-License-Feature", license.FeatureAIPatrol) } count := len(result) if locked { result = []map[string]interface{}{} } if err := utils.WriteJSONResponse(w, map[string]interface{}{ "correlations": result, "count": count, "license_required": locked, "upgrade_url": aiIntelligenceUpgradeURL, }); err != nil { log.Error().Err(err).Msg("Failed to write correlations response") } } // HandleGetRecentChanges returns recent infrastructure changes (GET /api/ai/intelligence/changes) func (h *AISettingsHandler) HandleGetRecentChanges(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "changes": []interface{}{}, "message": "Patrol service not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write changes response") } return } detector := patrol.GetChangeDetector() if detector == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "changes": []interface{}{}, "message": "Change detector not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write changes response") } return } // Get time range - default to 24 hours hoursStr := r.URL.Query().Get("hours") hours := 24 if hoursStr != "" { if h, err := strconv.Atoi(hoursStr); err == nil && h > 0 { hours = h } } since := time.Now().Add(-time.Duration(hours) * time.Hour) changes := detector.GetRecentChanges(100, since) var result []map[string]interface{} for _, change := range changes { result = append(result, map[string]interface{}{ "id": change.ID, "resource_id": change.ResourceID, "resource_name": change.ResourceName, "resource_type": change.ResourceType, "change_type": change.ChangeType, "before": change.Before, "after": change.After, "detected_at": change.DetectedAt, "description": change.Description, }) } locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) if locked { w.Header().Set("X-License-Required", "true") w.Header().Set("X-License-Feature", license.FeatureAIPatrol) } count := len(result) if locked { result = []map[string]interface{}{} } if err := utils.WriteJSONResponse(w, map[string]interface{}{ "changes": result, "count": count, "hours": hours, "license_required": locked, "upgrade_url": aiIntelligenceUpgradeURL, }); err != nil { log.Error().Err(err).Msg("Failed to write changes response") } } // HandleGetBaselines returns learned resource baselines (GET /api/ai/intelligence/baselines) func (h *AISettingsHandler) HandleGetBaselines(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "baselines": []interface{}{}, "message": "Patrol service not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write baselines response") } return } store := patrol.GetBaselineStore() if store == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "baselines": []interface{}{}, "message": "Baseline store not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write baselines response") } return } // Get resource filter if provided resourceID := r.URL.Query().Get("resource_id") baselines := store.GetAllBaselines() var result []map[string]interface{} for key, baseline := range baselines { if resourceID != "" && baseline.ResourceID != resourceID { continue } result = append(result, map[string]interface{}{ "key": key, "resource_id": baseline.ResourceID, "metric": baseline.Metric, "mean": baseline.Mean, "std_dev": baseline.StdDev, "min": baseline.Min, "max": baseline.Max, "samples": baseline.Samples, "last_update": baseline.LastUpdate, }) } locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) if locked { w.Header().Set("X-License-Required", "true") w.Header().Set("X-License-Feature", license.FeatureAIPatrol) } count := len(result) if locked { result = []map[string]interface{}{} } if err := utils.WriteJSONResponse(w, map[string]interface{}{ "baselines": result, "count": count, "license_required": locked, "upgrade_url": aiIntelligenceUpgradeURL, }); err != nil { log.Error().Err(err).Msg("Failed to write baselines response") } } // HandleGetRemediations returns remediation history (GET /api/ai/intelligence/remediations) func (h *AISettingsHandler) HandleGetRemediations(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "remediations": []interface{}{}, "message": "Patrol service not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write remediations response") } return } remediationLog := patrol.GetRemediationLog() if remediationLog == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ "remediations": []interface{}{}, "message": "Remediation log not initialized", }); err != nil { log.Error().Err(err).Msg("Failed to write remediations response") } return } resourceID := r.URL.Query().Get("resource_id") findingID := r.URL.Query().Get("finding_id") limit := 20 if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 { limit = parsed } } if limit > 100 { limit = 100 } hours := 168 if hoursStr := r.URL.Query().Get("hours"); hoursStr != "" { if parsed, err := strconv.Atoi(hoursStr); err == nil && parsed > 0 { hours = parsed } } since := time.Now().Add(-time.Duration(hours) * time.Hour) var records []ai.RemediationRecord switch { case findingID != "": records = remediationLog.GetForFinding(findingID, limit) case resourceID != "": records = remediationLog.GetForResource(resourceID, limit) default: records = remediationLog.GetRecentRemediations(limit, since) } stats := remediationStatsFromRecords(records) if findingID == "" && resourceID == "" { stats = remediationLog.GetRecentRemediationStats(since) } result := make([]map[string]interface{}, 0, len(records)) for _, rec := range records { durationMs := int64(0) if rec.Duration > 0 { durationMs = rec.Duration.Milliseconds() } result = append(result, map[string]interface{}{ "id": rec.ID, "timestamp": rec.Timestamp, "resource_id": rec.ResourceID, "resource_type": rec.ResourceType, "resource_name": rec.ResourceName, "finding_id": rec.FindingID, "problem": rec.Problem, "action": rec.Action, "output": rec.Output, "outcome": rec.Outcome, "duration_ms": durationMs, "note": rec.Note, "automatic": rec.Automatic, }) } locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) if locked { w.Header().Set("X-License-Required", "true") w.Header().Set("X-License-Feature", license.FeatureAIPatrol) } count := len(result) if locked { result = []map[string]interface{}{} } if err := utils.WriteJSONResponse(w, map[string]interface{}{ "remediations": result, "count": count, "stats": stats, "license_required": locked, "upgrade_url": aiIntelligenceUpgradeURL, }); err != nil { log.Error().Err(err).Msg("Failed to write remediations response") } } func remediationStatsFromRecords(records []ai.RemediationRecord) map[string]int { stats := map[string]int{ "total": len(records), "resolved": 0, "partial": 0, "failed": 0, "unknown": 0, "automatic": 0, "manual": 0, } for _, rec := range records { switch rec.Outcome { case ai.OutcomeResolved: stats["resolved"]++ case ai.OutcomePartial: stats["partial"]++ case ai.OutcomeFailed: stats["failed"]++ default: stats["unknown"]++ } if rec.Automatic { stats["automatic"]++ } else { stats["manual"]++ } } return stats }