package service import ( "context" "encoding/json" "fmt" "math" "bugulma/backend/internal/domain" "bugulma/backend/internal/geospatial" ) // SpatialResourceMatcher enhances resource matching with geographical intelligence type SpatialResourceMatcher struct { geoRepo domain.GeographicalFeatureRepository siteRepo domain.SiteRepository resourceFlowRepo domain.ResourceFlowRepository geospatialSvc *GeospatialService transportSvc *TransportationService geoCalc geospatial.Calculator } // NewSpatialResourceMatcher creates a new spatial resource matcher func NewSpatialResourceMatcher( geoRepo domain.GeographicalFeatureRepository, siteRepo domain.SiteRepository, resourceFlowRepo domain.ResourceFlowRepository, geospatialSvc *GeospatialService, transportSvc *TransportationService, geoCalc geospatial.Calculator, ) *SpatialResourceMatcher { return &SpatialResourceMatcher{ geoRepo: geoRepo, siteRepo: siteRepo, resourceFlowRepo: resourceFlowRepo, geospatialSvc: geospatialSvc, transportSvc: transportSvc, geoCalc: geoCalc, } } // SpatialMatchResult represents a resource match with spatial metadata type SpatialMatchResult struct { ResourceFlow *domain.ResourceFlow `json:"resource_flow"` ProviderSite *domain.Site `json:"provider_site"` RequesterSite *domain.Site `json:"requester_site"` SpatialMetrics *SpatialMetrics `json:"spatial_metrics"` MatchScore float64 `json:"match_score"` } // SpatialMetrics contains geographical analysis for a match type SpatialMetrics struct { StraightLineDistance float64 `json:"straight_line_distance_km"` RoadDistance float64 `json:"road_distance_km,omitempty"` TransportCost float64 `json:"transport_cost_eur_month"` EnvironmentalScore float64 `json:"environmental_score"` InfrastructureScore float64 `json:"infrastructure_score"` TimeToDeliver float64 `json:"time_to_deliver_hours,omitempty"` } // FindNearbyResourceProviders finds resource providers within geographical constraints func (m *SpatialResourceMatcher) FindNearbyResourceProviders( ctx context.Context, resourceType domain.ResourceType, requesterLat, requesterLng float64, maxDistanceKm float64, preferredTransport domain.TransportMode, ) ([]*SpatialMatchResult, error) { // Find sites within radius that offer the requested resource nearbySites, err := m.siteRepo.GetWithinRadius(ctx, requesterLat, requesterLng, maxDistanceKm) if err != nil { return nil, fmt.Errorf("failed to find nearby sites: %w", err) } var results []*SpatialMatchResult // Process each site and check for matching resource flows for _, site := range nearbySites { if site == nil { continue } // Get resource flows for this site allFlows, err := m.resourceFlowRepo.GetBySiteID(ctx, site.ID) if err != nil { continue // Skip if no flows found or error } // Filter for output flows of the requested resource type var flows []*domain.ResourceFlow for _, flow := range allFlows { if flow.Direction == domain.DirectionOutput && flow.Type == resourceType { flows = append(flows, flow) } } // Skip sites that don't have matching flows if len(flows) == 0 { continue } // Calculate spatial metrics once per site (reused for all flows) metrics, err := m.calculateSpatialMetrics(ctx, requesterLat, requesterLng, site, preferredTransport) if err != nil { continue // Skip sites where we can't calculate metrics } // Create match results for each matching flow for _, flow := range flows { matchScore := m.calculateMatchScore(metrics, flow) result := &SpatialMatchResult{ ResourceFlow: flow, ProviderSite: site, SpatialMetrics: metrics, MatchScore: matchScore, } results = append(results, result) } } return results, nil } // siteProvidesResource checks if a site provides a specific resource type // This method is deprecated - resource flow checking is now done directly in FindNearbyResourceProviders // for better efficiency. Kept for backward compatibility if needed elsewhere. func (m *SpatialResourceMatcher) siteProvidesResource(ctx context.Context, site *domain.Site, resourceType domain.ResourceType) (bool, error) { // Check if site has output flows of the requested resource type flows, err := m.resourceFlowRepo.GetBySiteID(ctx, site.ID) if err != nil { return false, err } for _, flow := range flows { if flow.Direction == domain.DirectionOutput && flow.Type == resourceType { return true, nil } } return false, nil } // calculateSpatialMetrics calculates spatial metrics between requester and provider func (m *SpatialResourceMatcher) calculateSpatialMetrics( ctx context.Context, fromLat, fromLng float64, toSite *domain.Site, preferredTransport domain.TransportMode, ) (*SpatialMetrics, error) { if toSite == nil { return nil, fmt.Errorf("toSite cannot be nil") } metrics := &SpatialMetrics{} // Calculate straight-line distance result, err := m.geoCalc.CalculateDistance( geospatial.Point{Latitude: fromLat, Longitude: fromLng}, geospatial.Point{Latitude: toSite.Latitude, Longitude: toSite.Longitude}, ) if err != nil { return nil, fmt.Errorf("failed to calculate distance: %w", err) } metrics.StraightLineDistance = result.DistanceKm // Estimate road distance (simplified approximation) metrics.RoadDistance = metrics.StraightLineDistance * 1.3 // 30% longer due to roads // Calculate transportation cost using dedicated service transportCost, err := m.transportSvc.CalculateTransportCost( fromLat, fromLng, toSite.Latitude, toSite.Longitude, preferredTransport, 10.0, // Assume 10 tons for cost calculation ) if err != nil { // Use fallback calculation if transport service fails metrics.TransportCost = metrics.RoadDistance * 0.1 // €0.10 per km fallback metrics.TimeToDeliver = metrics.RoadDistance / 50.0 // 50 km/h fallback } else { metrics.TransportCost = transportCost.CostEur metrics.TimeToDeliver = transportCost.TimeHours } // Environmental score for the destination envScore, err := m.geospatialSvc.CalculateSiteEnvironmentalScore(ctx, toSite.Latitude, toSite.Longitude) if err != nil { metrics.EnvironmentalScore = 5.0 // Default neutral score } else { metrics.EnvironmentalScore = envScore } // Infrastructure score (simplified) metrics.InfrastructureScore = m.calculateInfrastructureScore(toSite) return metrics, nil } // calculateMatchScore calculates an overall match score func (m *SpatialResourceMatcher) calculateMatchScore(metrics *SpatialMetrics, flow *domain.ResourceFlow) float64 { // Multi-criteria scoring distanceScore := math.Max(0, 10.0-(metrics.StraightLineDistance/10.0)) // Better closer, max 10km costScore := math.Max(0, 10.0-(metrics.TransportCost/100.0)) // Better cheaper, max €100 envScore := metrics.EnvironmentalScore // 0-10 scale infraScore := metrics.InfrastructureScore // 0-10 scale // Weighted average return (distanceScore*0.3 + costScore*0.3 + envScore*0.2 + infraScore*0.2) } // calculateInfrastructureScore calculates infrastructure quality score func (m *SpatialResourceMatcher) calculateInfrastructureScore(site *domain.Site) float64 { score := 5.0 // Base score // Check available utilities if len(site.AvailableUtilities) > 0 { var utilities []string if err := json.Unmarshal(site.AvailableUtilities, &utilities); err == nil { score += float64(len(utilities)) * 0.5 // +0.5 per utility } } // Check parking spaces if site.ParkingSpaces > 0 { score += 1.0 } // Check loading docks if site.LoadingDocks > 0 { score += 1.0 } return math.Min(10.0, score) // Cap at 10 }