package reporting import ( "fmt" "time" "github.com/rcourtman/pulse-go-rewrite/pkg/metrics" "github.com/rs/zerolog/log" ) // ReportEngine implements the reporting.Engine interface with // full CSV and PDF generation capabilities. type ReportEngine struct { metricsStore *metrics.Store metricsStoreGetter func() *metrics.Store csvGen *CSVGenerator pdfGen *PDFGenerator } // EngineConfig holds configuration for the report engine. type EngineConfig struct { // MetricsStore is a direct reference to the metrics store. // Use MetricsStoreGetter instead when the store may be replaced // (e.g., after monitor reloads). MetricsStore *metrics.Store // MetricsStoreGetter dynamically resolves the current metrics store. // When set, this is used instead of MetricsStore, ensuring queries // always target the active store even after monitor reloads. MetricsStoreGetter func() *metrics.Store } // NewReportEngine creates a new reporting engine. func NewReportEngine(cfg EngineConfig) *ReportEngine { return &ReportEngine{ metricsStore: cfg.MetricsStore, metricsStoreGetter: cfg.MetricsStoreGetter, csvGen: NewCSVGenerator(), pdfGen: NewPDFGenerator(), } } // getMetricsStore returns the current metrics store, preferring the dynamic // getter over the static reference. func (e *ReportEngine) getMetricsStore() *metrics.Store { if e.metricsStoreGetter != nil { return e.metricsStoreGetter() } return e.metricsStore } // Generate creates a report in the specified format. func (e *ReportEngine) Generate(req MetricReportRequest) (data []byte, contentType string, err error) { if e.getMetricsStore() == nil { return nil, "", fmt.Errorf("metrics store not initialized") } // Query metrics data reportData, err := e.queryMetrics(req) if err != nil { return nil, "", fmt.Errorf("failed to query metrics: %w", err) } log.Debug(). Str("resourceType", req.ResourceType). Str("resourceID", req.ResourceID). Str("format", string(req.Format)). Int("dataPoints", reportData.TotalPoints). Msg("Generating report") switch req.Format { case FormatCSV: data, err = e.csvGen.Generate(reportData) if err != nil { return nil, "", fmt.Errorf("CSV generation failed: %w", err) } contentType = "text/csv" case FormatPDF: data, err = e.pdfGen.Generate(reportData) if err != nil { return nil, "", fmt.Errorf("PDF generation failed: %w", err) } contentType = "application/pdf" default: return nil, "", fmt.Errorf("unsupported format: %s", req.Format) } return data, contentType, nil } // ReportData holds the data for report generation. type ReportData struct { Title string ResourceType string ResourceID string Start time.Time End time.Time GeneratedAt time.Time Metrics map[string][]MetricDataPoint TotalPoints int Summary MetricSummary // Enrichment data (optional, for richer PDF reports) Resource *ResourceInfo Alerts []AlertInfo Backups []BackupInfo Storage []StorageInfo Disks []DiskInfo } // MetricDataPoint represents a single data point in a report. type MetricDataPoint struct { Timestamp time.Time Value float64 Min float64 Max float64 } // MetricSummary holds aggregated statistics for a report. type MetricSummary struct { ByMetric map[string]MetricStats } // MetricStats holds statistics for a single metric type. type MetricStats struct { MetricType string Count int Min float64 Max float64 Avg float64 Current float64 } // queryMetrics fetches metrics from the store and prepares report data. func (e *ReportEngine) queryMetrics(req MetricReportRequest) (*ReportData, error) { data := &ReportData{ Title: req.Title, ResourceType: req.ResourceType, ResourceID: req.ResourceID, Start: req.Start, End: req.End, GeneratedAt: time.Now(), Metrics: make(map[string][]MetricDataPoint), Summary: MetricSummary{ ByMetric: make(map[string]MetricStats), }, } if data.Title == "" { data.Title = fmt.Sprintf("%s Report: %s", req.ResourceType, req.ResourceID) } // Copy enrichment data from request data.Resource = req.Resource data.Alerts = req.Alerts data.Backups = req.Backups data.Storage = req.Storage data.Disks = req.Disks store := e.getMetricsStore() var metricsMap map[string][]metrics.MetricPoint var err error if req.MetricType != "" { // Query specific metric points, queryErr := store.Query(req.ResourceType, req.ResourceID, req.MetricType, req.Start, req.End, 0) if queryErr != nil { return nil, queryErr } metricsMap = map[string][]metrics.MetricPoint{ req.MetricType: points, } } else { // Query all metrics for the resource metricsMap, err = store.QueryAll(req.ResourceType, req.ResourceID, req.Start, req.End, 0) if err != nil { return nil, err } } if len(metricsMap) == 0 { log.Warn(). Str("resourceType", req.ResourceType). Str("resourceID", req.ResourceID). Str("metricType", req.MetricType). Time("start", req.Start). Time("end", req.End). Msg("Report query returned no metrics — verify resource ID matches stored metrics and time range contains data") } // Convert to report format and calculate statistics for metricType, points := range metricsMap { if len(points) == 0 { continue } dataPoints := make([]MetricDataPoint, len(points)) var sum float64 stats := MetricStats{ MetricType: metricType, Count: len(points), Min: points[0].Value, Max: points[0].Value, } for i, p := range points { dataPoints[i] = MetricDataPoint{ Timestamp: p.Timestamp, Value: p.Value, Min: p.Min, Max: p.Max, } sum += p.Value if p.Value < stats.Min { stats.Min = p.Value } if p.Value > stats.Max { stats.Max = p.Value } } stats.Avg = sum / float64(len(points)) stats.Current = points[len(points)-1].Value data.TotalPoints += len(points) data.Metrics[metricType] = dataPoints data.Summary.ByMetric[metricType] = stats } return data, nil } // GenerateMulti creates a multi-resource report in the specified format. func (e *ReportEngine) GenerateMulti(req MultiReportRequest) (data []byte, contentType string, err error) { if e.getMetricsStore() == nil { return nil, "", fmt.Errorf("metrics store not initialized") } multiData := &MultiReportData{ Title: req.Title, Start: req.Start, End: req.End, GeneratedAt: time.Now(), } if multiData.Title == "" { multiData.Title = "Fleet Performance Report" } // Query metrics for each resource var successCount int for _, resReq := range req.Resources { resReq.Start = req.Start resReq.End = req.End resReq.MetricType = req.MetricType reportData, queryErr := e.queryMetrics(resReq) if queryErr != nil { log.Warn(). Str("resourceType", resReq.ResourceType). Str("resourceID", resReq.ResourceID). Err(queryErr). Msg("Skipping resource in multi-report: failed to query metrics") continue } multiData.Resources = append(multiData.Resources, reportData) multiData.TotalPoints += reportData.TotalPoints successCount++ } if successCount == 0 { return nil, "", fmt.Errorf("all resources failed to query metrics") } log.Debug(). Int("resources", successCount). Int("skipped", len(req.Resources)-successCount). Str("format", string(req.Format)). Int("totalPoints", multiData.TotalPoints). Msg("Generating multi-resource report") switch req.Format { case FormatCSV: data, err = e.csvGen.GenerateMulti(multiData) if err != nil { return nil, "", fmt.Errorf("CSV generation failed: %w", err) } contentType = "text/csv" case FormatPDF: data, err = e.pdfGen.GenerateMulti(multiData) if err != nil { return nil, "", fmt.Errorf("PDF generation failed: %w", err) } contentType = "application/pdf" default: return nil, "", fmt.Errorf("unsupported format: %s", req.Format) } return data, contentType, nil } // GetResourceTypeDisplayName returns a human-readable name for resource types. func GetResourceTypeDisplayName(resourceType string) string { switch resourceType { case "node": return "Node" case "vm": return "Virtual Machine" case "container": return "LXC Container" case "dockerHost": return "Docker Host" case "dockerContainer": return "Docker Container" case "storage": return "Storage" default: return resourceType } } // GetMetricTypeDisplayName returns a human-readable name for metric types. func GetMetricTypeDisplayName(metricType string) string { switch metricType { case "cpu": return "CPU Usage" case "memory": return "Memory Usage" case "disk": return "Disk Usage" case "usage": return "Storage Usage" case "used": return "Used Space" case "total": return "Total Space" case "avail": return "Available Space" default: return metricType } } // GetMetricUnit returns the unit for a metric type. func GetMetricUnit(metricType string) string { switch metricType { case "cpu", "memory", "disk", "usage": return "%" case "used", "total", "avail": return "bytes" default: return "" } }