package service import ( "bugulma/backend/internal/domain" "context" "fmt" "gorm.io/gorm" ) // AnalyticsService provides platform analytics and statistics type AnalyticsService struct { db *gorm.DB orgRepo domain.OrganizationRepository siteRepo domain.SiteRepository resourceFlowRepo domain.ResourceFlowRepository matchRepo domain.MatchRepository sharedAssetRepo domain.SharedAssetRepository } // NewAnalyticsService creates a new analytics service func NewAnalyticsService( db *gorm.DB, orgRepo domain.OrganizationRepository, siteRepo domain.SiteRepository, resourceFlowRepo domain.ResourceFlowRepository, matchRepo domain.MatchRepository, sharedAssetRepo domain.SharedAssetRepository, ) *AnalyticsService { return &AnalyticsService{ db: db, orgRepo: orgRepo, siteRepo: siteRepo, resourceFlowRepo: resourceFlowRepo, matchRepo: matchRepo, sharedAssetRepo: sharedAssetRepo, } } // PlatformStatistics represents overall platform metrics type PlatformStatistics struct { TotalOrganizations int `json:"total_organizations"` TotalSites int `json:"total_sites"` TotalResourceFlows int `json:"total_resource_flows"` TotalMatches int `json:"total_matches"` TotalSharedAssets int `json:"total_shared_assets"` ActiveMatches int `json:"active_matches"` CO2SavingsTonnes float64 `json:"co2_savings_tonnes"` EconomicValueEUR float64 `json:"economic_value_eur"` } // OrganizationStatistics represents statistics for a single organization type OrganizationStatistics struct { OrganizationID string `json:"organization_id"` TotalSites int `json:"total_sites"` TotalResourceFlows int `json:"total_resource_flows"` ActiveMatches int `json:"active_matches"` TotalMatches int `json:"total_matches"` CO2SavingsTonnes float64 `json:"co2_savings_tonnes"` EconomicValueEUR float64 `json:"economic_value_eur"` } // MatchingStatistics represents matching engine performance metrics type MatchingStatistics struct { TotalMatches int `json:"total_matches"` ActiveMatches int `json:"active_matches"` CompletedMatches int `json:"completed_matches"` AvgCompatibilityScore float64 `json:"avg_compatibility_score"` AvgEconomicValue float64 `json:"avg_economic_value"` AvgDistanceKm float64 `json:"avg_distance_km"` } // ResourceFlowStatistics represents resource flow metrics type ResourceFlowStatistics struct { TotalFlows int `json:"total_flows"` InputFlows int `json:"input_flows"` OutputFlows int `json:"output_flows"` FlowsByType map[string]int `json:"flows_by_type"` FlowsByDirection map[string]int `json:"flows_by_direction"` } // ImpactMetrics represents environmental and economic impact type ImpactMetrics struct { TotalCO2SavingsTonnes float64 `json:"total_co2_savings_tonnes"` TotalEconomicValueEUR float64 `json:"total_economic_value_eur"` MaterialsRecycledTonnes float64 `json:"materials_recycled_tonnes"` EnergySharedMWh float64 `json:"energy_shared_mwh"` WaterReclaimedM3 float64 `json:"water_reclaimed_m3"` } // DashboardStatistics represents dashboard overview statistics type DashboardStatistics struct { TotalOrganizations int `json:"total_organizations"` TotalSites int `json:"total_sites"` TotalResourceFlows int `json:"total_resource_flows"` TotalMatches int `json:"total_matches"` ActiveProposals int `json:"active_proposals"` RecentActivity []ActivityItem `json:"recent_activity"` } // ActivityItem represents a recent activity entry type ActivityItem struct { ID string `json:"id"` Type string `json:"type"` Description string `json:"description"` Timestamp string `json:"timestamp"` } // ConnectionStatistics represents connection and relationship statistics type ConnectionStatistics struct { TotalConnections int `json:"total_connections"` ActiveConnections int `json:"active_connections"` PotentialConnections int `json:"potential_connections"` } // SupplyDemandAnalysis represents supply and demand analysis type SupplyDemandAnalysis struct { TopNeeds []ItemCount `json:"top_needs"` TopOffers []ItemCount `json:"top_offers"` } // ItemCount represents an item with its count type ItemCount struct { Item string `json:"item"` Count int `json:"count"` } // GetPlatformStatistics retrieves overall platform statistics func (s *AnalyticsService) GetPlatformStatistics(ctx context.Context) (*PlatformStatistics, error) { stats := &PlatformStatistics{} // Count organizations var orgCount int64 if err := s.db.Model(&domain.Organization{}).Count(&orgCount).Error; err != nil { return nil, err } stats.TotalOrganizations = int(orgCount) // Count sites var siteCount int64 if err := s.db.Model(&domain.Site{}).Count(&siteCount).Error; err != nil { return nil, err } stats.TotalSites = int(siteCount) // Count resource flows var flowCount int64 if err := s.db.Model(&domain.ResourceFlow{}).Count(&flowCount).Error; err != nil { return nil, err } stats.TotalResourceFlows = int(flowCount) // Count matches var matchCount int64 if err := s.db.Model(&domain.Match{}).Count(&matchCount).Error; err != nil { return nil, err } stats.TotalMatches = int(matchCount) // Count active matches var activeMatchCount int64 if err := s.db.Model(&domain.Match{}). Where("status IN ?", []string{"suggested", "negotiating", "reserved", "contracted", "live"}). Count(&activeMatchCount).Error; err != nil { return nil, err } stats.ActiveMatches = int(activeMatchCount) // Count shared assets var assetCount int64 if err := s.db.Model(&domain.SharedAsset{}).Count(&assetCount).Error; err != nil { return nil, err } stats.TotalSharedAssets = int(assetCount) // Calculate aggregate impact metrics var aggregates struct { CO2Savings float64 EconomicValue float64 } s.db.Model(&domain.Match{}). Select("COALESCE(SUM(environmental_impact->>'co2_savings_kg'), 0) as co2_savings, COALESCE(SUM(economic_value), 0) as economic_value"). Where("status IN ?", []string{"contracted", "live"}). Scan(&aggregates) stats.CO2SavingsTonnes = aggregates.CO2Savings / 1000 // Convert kg to tonnes stats.EconomicValueEUR = aggregates.EconomicValue return stats, nil } // GetOrganizationStatistics retrieves statistics for a specific organization func (s *AnalyticsService) GetOrganizationStatistics(ctx context.Context, orgID string) (*OrganizationStatistics, error) { stats := &OrganizationStatistics{ OrganizationID: orgID, } // Count sites var siteCount int64 if err := s.db.Model(&domain.Site{}). Where("owner_organization_id = ?", orgID). Count(&siteCount).Error; err != nil { return nil, err } stats.TotalSites = int(siteCount) // Count resource flows var flowCount int64 if err := s.db.Model(&domain.ResourceFlow{}). Where("organization_id = ?", orgID). Count(&flowCount).Error; err != nil { return nil, err } stats.TotalResourceFlows = int(flowCount) // Count matches (source or target) var matchCount int64 s.db.Model(&domain.Match{}). Joins("JOIN resource_flows rf ON matches.source_resource_id = rf.id OR matches.target_resource_id = rf.id"). Where("rf.organization_id = ?", orgID). Count(&matchCount) stats.TotalMatches = int(matchCount) // Count active matches var activeMatchCount int64 s.db.Model(&domain.Match{}). Joins("JOIN resource_flows rf ON matches.source_resource_id = rf.id OR matches.target_resource_id = rf.id"). Where("rf.organization_id = ? AND matches.status IN ?", orgID, []string{"suggested", "negotiating", "reserved", "contracted", "live"}). Count(&activeMatchCount) stats.ActiveMatches = int(activeMatchCount) return stats, nil } // GetMatchingStatistics retrieves matching engine performance statistics func (s *AnalyticsService) GetMatchingStatistics(ctx context.Context) (*MatchingStatistics, error) { stats := &MatchingStatistics{} // Total matches var matchCount int64 if err := s.db.Model(&domain.Match{}).Count(&matchCount).Error; err != nil { return nil, err } stats.TotalMatches = int(matchCount) // Active matches var activeCount int64 if err := s.db.Model(&domain.Match{}). Where("status IN ?", []string{"suggested", "negotiating", "reserved", "contracted", "live"}). Count(&activeCount).Error; err != nil { return nil, err } stats.ActiveMatches = int(activeCount) // Completed matches var completedCount int64 if err := s.db.Model(&domain.Match{}). Where("status = ?", "completed"). Count(&completedCount).Error; err != nil { return nil, err } stats.CompletedMatches = int(completedCount) // Average metrics var averages struct { AvgScore float64 AvgValue float64 AvgDistance float64 } s.db.Model(&domain.Match{}). Select("AVG(compatibility_score) as avg_score, AVG(economic_value) as avg_value, AVG(distance_km) as avg_distance"). Scan(&averages) stats.AvgCompatibilityScore = averages.AvgScore stats.AvgEconomicValue = averages.AvgValue stats.AvgDistanceKm = averages.AvgDistance return stats, nil } // GetResourceFlowStatistics retrieves resource flow statistics func (s *AnalyticsService) GetResourceFlowStatistics(ctx context.Context) (*ResourceFlowStatistics, error) { stats := &ResourceFlowStatistics{ FlowsByType: make(map[string]int), FlowsByDirection: make(map[string]int), } // Total flows var flowCount int64 if err := s.db.Model(&domain.ResourceFlow{}).Count(&flowCount).Error; err != nil { return nil, err } stats.TotalFlows = int(flowCount) // Count by direction var directionCounts []struct { Direction string Count int } s.db.Model(&domain.ResourceFlow{}). Select("direction, COUNT(*) as count"). Group("direction"). Scan(&directionCounts) for _, dc := range directionCounts { stats.FlowsByDirection[dc.Direction] = dc.Count if dc.Direction == "input" { stats.InputFlows = dc.Count } else if dc.Direction == "output" { stats.OutputFlows = dc.Count } } // Count by type var typeCounts []struct { Type string Count int } s.db.Model(&domain.ResourceFlow{}). Select("type, COUNT(*) as count"). Group("type"). Scan(&typeCounts) for _, tc := range typeCounts { stats.FlowsByType[tc.Type] = tc.Count } return stats, nil } // GetImpactMetrics retrieves environmental and economic impact metrics func (s *AnalyticsService) GetImpactMetrics(ctx context.Context) (*ImpactMetrics, error) { metrics := &ImpactMetrics{} // Aggregate impact from active/completed matches var aggregates struct { CO2Savings float64 EconValue float64 } s.db.Model(&domain.Match{}). Select("COALESCE(SUM(environmental_impact->>'co2_savings_kg'), 0) as co2_savings, COALESCE(SUM(economic_value), 0) as econ_value"). Where("status IN ?", []string{"contracted", "live", "completed"}). Scan(&aggregates) metrics.TotalCO2SavingsTonnes = aggregates.CO2Savings / 1000 metrics.TotalEconomicValueEUR = aggregates.EconValue // TODO: Calculate materials recycled, energy shared, water reclaimed from resource flows // This would require additional analysis of resource flow quantity data return metrics, nil } // GetDashboardStatistics retrieves dashboard overview statistics func (s *AnalyticsService) GetDashboardStatistics(ctx context.Context) (*DashboardStatistics, error) { stats := &DashboardStatistics{} // Get basic counts var orgCount, siteCount, flowCount, matchCount, activeProposalCount int64 s.db.Model(&domain.Organization{}).Count(&orgCount) s.db.Model(&domain.Site{}).Count(&siteCount) s.db.Model(&domain.ResourceFlow{}).Count(&flowCount) s.db.Model(&domain.Match{}).Count(&matchCount) // Active proposals (matches in early stages) s.db.Model(&domain.Match{}). Where("status IN ?", []string{"suggested", "negotiating"}). Count(&activeProposalCount) stats.TotalOrganizations = int(orgCount) stats.TotalSites = int(siteCount) stats.TotalResourceFlows = int(flowCount) stats.TotalMatches = int(matchCount) stats.ActiveProposals = int(activeProposalCount) // Recent activity (last 10 activities) var recentMatches []domain.Match s.db.Order("created_at DESC"). Limit(10). Find(&recentMatches) stats.RecentActivity = make([]ActivityItem, 0, len(recentMatches)) for _, match := range recentMatches { description := fmt.Sprintf("Match created with compatibility score %.2f", match.CompatibilityScore) stats.RecentActivity = append(stats.RecentActivity, ActivityItem{ ID: match.ID, Type: "match", Description: description, Timestamp: match.CreatedAt.Format("2006-01-02T15:04:05Z"), }) } return stats, nil } // GetConnectionStatistics retrieves connection and relationship statistics func (s *AnalyticsService) GetConnectionStatistics(ctx context.Context) (*ConnectionStatistics, error) { stats := &ConnectionStatistics{} // Total connections (active matches) var totalConnections int64 s.db.Model(&domain.Match{}). Where("status IN ?", []string{"contracted", "live", "completed"}). Count(&totalConnections) stats.TotalConnections = int(totalConnections) // Active connections (live matches) var activeConnections int64 s.db.Model(&domain.Match{}). Where("status = ?", "live"). Count(&activeConnections) stats.ActiveConnections = int(activeConnections) // Potential connections (suggested matches) var potentialConnections int64 s.db.Model(&domain.Match{}). Where("status IN ?", []string{"suggested", "negotiating"}). Count(&potentialConnections) stats.PotentialConnections = int(potentialConnections) return stats, nil } // GetSupplyDemandAnalysis retrieves supply and demand analysis func (s *AnalyticsService) GetSupplyDemandAnalysis(ctx context.Context) (*SupplyDemandAnalysis, error) { analysis := &SupplyDemandAnalysis{ TopNeeds: []ItemCount{}, TopOffers: []ItemCount{}, } // Top needs (most requested resource types) var needCounts []struct { ResourceType string Count int } s.db.Model(&domain.ResourceFlow{}). Select("resource_type, COUNT(*) as count"). Where("direction = ?", "input"). Group("resource_type"). Order("count DESC"). Limit(10). Scan(&needCounts) for _, nc := range needCounts { analysis.TopNeeds = append(analysis.TopNeeds, ItemCount{ Item: nc.ResourceType, Count: nc.Count, }) } // Top offers (most offered resource types) var offerCounts []struct { ResourceType string Count int } s.db.Model(&domain.ResourceFlow{}). Select("resource_type, COUNT(*) as count"). Where("direction = ?", "output"). Group("resource_type"). Order("count DESC"). Limit(10). Scan(&offerCounts) for _, oc := range offerCounts { analysis.TopOffers = append(analysis.TopOffers, ItemCount{ Item: oc.ResourceType, Count: oc.Count, }) } return analysis, nil }