package matching import ( "context" "fmt" "strings" "bugulma/backend/internal/analysis/regulatory" "bugulma/backend/internal/analysis/risk" "bugulma/backend/internal/analysis/transport" "bugulma/backend/internal/domain" "bugulma/backend/internal/geospatial" "bugulma/backend/internal/matching/engine" "bugulma/backend/internal/matching/manager" "bugulma/backend/internal/matching/plugins" ) // Service orchestrates all matching functionality type Service struct { engine *engine.Engine manager *manager.Manager pluginMgr *plugins.Manager matchRepo domain.MatchRepository // Dependencies for data access resourceFlowRepo domain.ResourceFlowRepository siteRepo domain.SiteRepository orgRepo domain.OrganizationRepository productRepo domain.ProductRepository serviceRepo domain.ServiceRepository communityListingRepo domain.CommunityListingRepository eventBus domain.EventBus // Geospatial calculator geoCalc geospatial.Calculator // Discovery matcher for products/services discoveryMatcher *DiscoveryMatcher } // EventBus is now defined in the domain package // NewService creates a new matching service func NewService( matchRepo domain.MatchRepository, negotiationRepo domain.NegotiationHistoryRepository, resourceFlowRepo domain.ResourceFlowRepository, siteRepo domain.SiteRepository, orgRepo domain.OrganizationRepository, productRepo domain.ProductRepository, serviceRepo domain.ServiceRepository, communityListingRepo domain.CommunityListingRepository, riskSvc *risk.Service, transportSvc *transport.Service, regulatorySvc *regulatory.Service, eventBus domain.EventBus, ) *Service { // Create engine with analysis services eng := engine.NewEngine(riskSvc, transportSvc, regulatorySvc) // Create manager for lifecycle operations mgr := manager.NewManager(matchRepo, negotiationRepo, eventBus) // Create plugin registry and register default plugins pluginRegistry := plugins.NewRegistry() if err := plugins.RegisterDefaultPlugins(pluginRegistry); err != nil { // Log error but continue - plugins are optional enhancement // In production, this would be logged properly } // Create plugin manager pluginMgr := plugins.NewManager(pluginRegistry, eng) // Create geospatial calculator geoCalc := geospatial.NewCalculatorWithDefaults() // Create discovery matcher for products/services discoveryMatcher := NewDiscoveryMatcher() return &Service{ engine: eng, manager: mgr, pluginMgr: pluginMgr, matchRepo: matchRepo, resourceFlowRepo: resourceFlowRepo, siteRepo: siteRepo, orgRepo: orgRepo, productRepo: productRepo, serviceRepo: serviceRepo, communityListingRepo: communityListingRepo, eventBus: eventBus, geoCalc: geoCalc, discoveryMatcher: discoveryMatcher, } } // FindMatches finds potential matches based on criteria func (s *Service) FindMatches(ctx context.Context, criteria Criteria) ([]*engine.Candidate, error) { // Find potential source and target flows sourceFlows, err := s.findSourceFlows(ctx, criteria) if err != nil { return nil, err } targetFlows, err := s.findTargetFlows(ctx, criteria) if err != nil { return nil, err } // Use engine to find initial matches candidates, err := s.engine.FindMatches(criteria, sourceFlows, targetFlows) if err != nil { return nil, err } // Enhance candidates with plugin logic var enhancedCandidates []*engine.Candidate for _, candidate := range candidates { // Calculate accurate distance using geospatial calculator sourceSite, err := s.siteRepo.GetByID(ctx, candidate.SourceFlow.SiteID) if err != nil { continue // Skip if we can't get site info } targetSite, err := s.siteRepo.GetByID(ctx, candidate.TargetFlow.SiteID) if err != nil { continue // Skip if we can't get site info } distanceResult, err := s.geoCalc.CalculateDistance( geospatial.Point{Latitude: sourceSite.Latitude, Longitude: sourceSite.Longitude}, geospatial.Point{Latitude: targetSite.Latitude, Longitude: targetSite.Longitude}, ) if err != nil { continue // Skip if distance calculation fails } candidate.DistanceKm = distanceResult.DistanceKm // Enhance candidate with plugin logic if err := s.pluginMgr.EnhanceCandidate(candidate); err != nil { // Log error but continue with basic candidate continue } // Validate candidate with plugins if valid, _ := s.pluginMgr.ValidateWithPlugins(candidate); valid { enhancedCandidates = append(enhancedCandidates, candidate) } } // Re-rank enhanced candidates enhancedCandidates = s.rerankCandidates(enhancedCandidates) return enhancedCandidates, nil } // CreateMatch creates a new match from a candidate func (s *Service) CreateMatch(ctx context.Context, candidate *engine.Candidate, creatorID string) (*domain.Match, error) { return s.manager.CreateMatch(ctx, candidate, creatorID) } // UpdateMatchStatus updates match status func (s *Service) UpdateMatchStatus(ctx context.Context, matchID string, newStatus domain.MatchStatus, actorID string, notes string) error { return s.manager.UpdateMatchStatus(ctx, matchID, newStatus, actorID, notes) } // UpdateMatch updates match details func (s *Service) UpdateMatch(ctx context.Context, match *domain.Match) error { return s.manager.UpdateMatch(ctx, match) } // DeleteMatch deletes a match func (s *Service) DeleteMatch(ctx context.Context, matchID string, actorID string) error { return s.manager.DeleteMatch(ctx, matchID, actorID) } // GetMatchByID retrieves a match by ID func (s *Service) GetMatchByID(ctx context.Context, matchID string) (*domain.Match, error) { return s.matchRepo.GetByID(ctx, matchID) } // GetTopMatches retrieves top matches func (s *Service) GetTopMatches(ctx context.Context, limit int) ([]*domain.Match, error) { return s.matchRepo.GetTopMatches(ctx, limit) } // GetNegotiationHistory retrieves negotiation history for a match func (s *Service) GetNegotiationHistory(ctx context.Context, matchID string) ([]*domain.NegotiationHistoryEntry, error) { return s.manager.GetNegotiationHistory(ctx, matchID) } // GetResourceFlowByID retrieves a resource flow by ID func (s *Service) GetResourceFlowByID(ctx context.Context, id string) (*domain.ResourceFlow, error) { return s.resourceFlowRepo.GetByID(ctx, id) } // GetSiteByID retrieves a site by ID func (s *Service) GetSiteByID(ctx context.Context, id string) (*domain.Site, error) { return s.siteRepo.GetByID(ctx, id) } // CalculateDistance calculates distance between two sites func (s *Service) CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 { result, err := s.geoCalc.CalculateDistance( geospatial.Point{Latitude: lat1, Longitude: lon1}, geospatial.Point{Latitude: lat2, Longitude: lon2}, ) if err != nil { // Fallback to 0 if calculation fails (shouldn't happen with valid coordinates) return 0 } return result.DistanceKm } // findSourceFlows finds resource flows that can be sources (outputs) func (s *Service) findSourceFlows(ctx context.Context, criteria Criteria) ([]*domain.ResourceFlow, error) { // For output matching, we look for flows with opposite direction oppositeDirection := s.getOppositeDirection(criteria.ResourceType) flows, err := s.resourceFlowRepo.GetByTypeAndDirection(ctx, criteria.ResourceType, oppositeDirection) if err != nil { return nil, err } // Filter by organization if specified if criteria.OrganizationID != "" { var filtered []*domain.ResourceFlow for _, flow := range flows { if flow.OrganizationID == criteria.OrganizationID { filtered = append(filtered, flow) } } flows = filtered } return flows, nil } // findTargetFlows finds resource flows that can be targets (inputs) func (s *Service) findTargetFlows(ctx context.Context, criteria Criteria) ([]*domain.ResourceFlow, error) { // For input matching, we look for flows in the specified direction flows, err := s.resourceFlowRepo.GetByTypeAndDirection(ctx, criteria.ResourceType, criteria.Direction) if err != nil { return nil, err } // Apply geographic filtering if site specified if criteria.SiteID != "" { flows = s.filterBySiteProximity(ctx, flows, criteria.SiteID, criteria.MaxDistanceKm) } return flows, nil } // filterBySiteProximity filters flows by distance from a reference site func (s *Service) filterBySiteProximity(ctx context.Context, flows []*domain.ResourceFlow, siteID string, maxDistance float64) []*domain.ResourceFlow { if maxDistance <= 0 { return flows } refSite, err := s.siteRepo.GetByID(ctx, siteID) if err != nil { return flows // Return all if we can't get reference site } var filtered []*domain.ResourceFlow for _, flow := range flows { flowSite, err := s.siteRepo.GetByID(ctx, flow.SiteID) if err != nil { continue } result, err := s.geoCalc.CalculateDistance( geospatial.Point{Latitude: refSite.Latitude, Longitude: refSite.Longitude}, geospatial.Point{Latitude: flowSite.Latitude, Longitude: flowSite.Longitude}, ) if err != nil { continue // Skip if distance calculation fails } distance := result.DistanceKm if distance <= maxDistance { filtered = append(filtered, flow) } } return filtered } // getOppositeDirection returns the opposite resource direction func (s *Service) getOppositeDirection(resourceType domain.ResourceType) domain.ResourceDirection { return domain.DirectionOutput // Simplified - would depend on matching strategy } // rerankCandidates sorts candidates by enhanced overall score func (s *Service) rerankCandidates(candidates []*engine.Candidate) []*engine.Candidate { // Simple sort by overall score (descending) for i := 0; i < len(candidates); i++ { for j := i + 1; j < len(candidates); j++ { if candidates[i].OverallScore < candidates[j].OverallScore { candidates[i], candidates[j] = candidates[j], candidates[i] } } } return candidates } // Import Criteria from engine package type Criteria = engine.Criteria // FindProductMatches finds products matching the discovery query func (s *Service) FindProductMatches(ctx context.Context, query DiscoveryQuery) ([]*DiscoveryMatch, error) { if s.productRepo == nil { return nil, fmt.Errorf("product repository not available") } // Search products based on query var products []*domain.Product var err error if query.Location != nil && query.RadiusKm > 0 { products, err = s.productRepo.GetNearby(ctx, query.Location.Latitude, query.Location.Longitude, query.RadiusKm) } else { products, err = s.productRepo.GetAll(ctx) } if err != nil { return nil, fmt.Errorf("failed to fetch products: %w", err) } // Score and rank matches var matches []*DiscoveryMatch for _, product := range products { // Get organization and site var org *domain.Organization var site *domain.Site if product.OrganizationID != "" { org, _ = s.orgRepo.GetByID(ctx, product.OrganizationID) } if product.SiteID != nil { site, _ = s.siteRepo.GetByID(ctx, *product.SiteID) } match, err := s.discoveryMatcher.ScoreProductMatch(product, query, org, site) if err != nil { continue } // Apply filters if query.AvailabilityStatus != "" && product.AvailabilityStatus != query.AvailabilityStatus { continue } if len(query.Categories) > 0 { categoryMatched := false for _, cat := range query.Categories { if strings.EqualFold(cat, string(product.Category)) { categoryMatched = true break } } if !categoryMatched { continue } } matches = append(matches, match) } // Sort by relevance score for i := 0; i < len(matches); i++ { for j := i + 1; j < len(matches); j++ { if matches[i].RelevanceScore < matches[j].RelevanceScore { matches[i], matches[j] = matches[j], matches[i] } } } // Apply pagination start := query.Offset end := start + query.Limit if start >= len(matches) { return []*DiscoveryMatch{}, nil } if end > len(matches) { end = len(matches) } return matches[start:end], nil } // FindServiceMatches finds services matching the discovery query func (s *Service) FindServiceMatches(ctx context.Context, query DiscoveryQuery) ([]*DiscoveryMatch, error) { if s.serviceRepo == nil { return nil, fmt.Errorf("service repository not available") } // Search services based on query var services []*domain.Service var err error if query.Location != nil && query.RadiusKm > 0 { services, err = s.serviceRepo.GetNearby(ctx, query.Location.Latitude, query.Location.Longitude, query.RadiusKm) } else { services, err = s.serviceRepo.GetAll(ctx) } if err != nil { return nil, fmt.Errorf("failed to fetch services: %w", err) } // Score and rank matches var matches []*DiscoveryMatch for _, service := range services { // Get organization and site var org *domain.Organization var site *domain.Site if service.OrganizationID != "" { org, _ = s.orgRepo.GetByID(ctx, service.OrganizationID) } if service.SiteID != nil { site, _ = s.siteRepo.GetByID(ctx, *service.SiteID) } match, err := s.discoveryMatcher.ScoreServiceMatch(service, query, org, site) if err != nil { continue } // Apply filters if query.AvailabilityStatus != "" && service.AvailabilityStatus != query.AvailabilityStatus { continue } matches = append(matches, match) } // Sort by relevance score for i := 0; i < len(matches); i++ { for j := i + 1; j < len(matches); j++ { if matches[i].RelevanceScore < matches[j].RelevanceScore { matches[i], matches[j] = matches[j], matches[i] } } } // Apply pagination start := query.Offset end := start + query.Limit if start >= len(matches) { return []*DiscoveryMatch{}, nil } if end > len(matches) { end = len(matches) } return matches[start:end], nil } // UniversalSearch performs a unified search across resources, products, services, and community listings func (s *Service) UniversalSearch(ctx context.Context, query DiscoveryQuery) (*UniversalSearchResult, error) { result := &UniversalSearchResult{ Query: query, } // Search products (soft match) if productMatches, err := s.FindProductMatches(ctx, query); err == nil { result.ProductMatches = productMatches } // Search services (soft match) if serviceMatches, err := s.FindServiceMatches(ctx, query); err == nil { result.ServiceMatches = serviceMatches } // Search community listings (soft match) if s.communityListingRepo != nil { var listings []*domain.CommunityListing var err error if query.Location != nil && query.RadiusKm > 0 { listings, err = s.communityListingRepo.GetNearby(ctx, query.Location.Latitude, query.Location.Longitude, query.RadiusKm) } else { listings, err = s.communityListingRepo.GetAll(ctx) } if err == nil { for _, listing := range listings { match, err := s.discoveryMatcher.ScoreCommunityListingMatch(listing, query) if err == nil { result.CommunityMatches = append(result.CommunityMatches, match) } } // Sort community matches for i := 0; i < len(result.CommunityMatches); i++ { for j := i + 1; j < len(result.CommunityMatches); j++ { if result.CommunityMatches[i].RelevanceScore < result.CommunityMatches[j].RelevanceScore { result.CommunityMatches[i], result.CommunityMatches[j] = result.CommunityMatches[j], result.CommunityMatches[i] } } } } } // Note: Resource flow matches (hard match) would be handled separately via FindMatches // This is the "soft match" layer as per concept's layered architecture return result, nil } // UniversalSearchResult contains results from universal search type UniversalSearchResult struct { Query DiscoveryQuery `json:"query"` ProductMatches []*DiscoveryMatch `json:"product_matches"` ServiceMatches []*DiscoveryMatch `json:"service_matches"` CommunityMatches []*DiscoveryMatch `json:"community_matches"` // ResourceMatches would be added via separate FindMatches call (hard match) } // GetProductsByOrganization gets products for a specific organization (efficient method) func (s *Service) GetProductsByOrganization(ctx context.Context, organizationID string) ([]*domain.Product, error) { if s.productRepo == nil { return nil, fmt.Errorf("product repository not available") } return s.productRepo.GetByOrganization(ctx, organizationID) } // GetServicesByOrganization gets services for a specific organization (efficient method) func (s *Service) GetServicesByOrganization(ctx context.Context, organizationID string) ([]*domain.Service, error) { if s.serviceRepo == nil { return nil, fmt.Errorf("service repository not available") } return s.serviceRepo.GetByOrganization(ctx, organizationID) } // CreateProduct creates a new product with site linking support func (s *Service) CreateProduct(ctx context.Context, product *domain.Product) error { if s.productRepo == nil { return fmt.Errorf("product repository not available") } // If SiteID is provided, populate location from site if product.SiteID != nil && *product.SiteID != "" { site, err := s.siteRepo.GetByID(ctx, *product.SiteID) if err == nil && site != nil { // Set location from site coordinates product.Location = domain.Point{ Latitude: site.Latitude, Longitude: site.Longitude, Valid: true, } } } if err := s.productRepo.Create(ctx, product); err != nil { return err } // Sync to graph database if graph sync service is available // Note: This would require passing graph sync service to matching service // For now, we'll rely on event-driven sync return nil } // CreateService creates a new service with site linking support func (s *Service) CreateService(ctx context.Context, service *domain.Service) error { if s.serviceRepo == nil { return fmt.Errorf("service repository not available") } // If SiteID is provided, populate location from site if service.SiteID != nil && *service.SiteID != "" { site, err := s.siteRepo.GetByID(ctx, *service.SiteID) if err == nil && site != nil { // Set location from site coordinates service.ServiceLocation = domain.Point{ Latitude: site.Latitude, Longitude: site.Longitude, Valid: true, } } } if err := s.serviceRepo.Create(ctx, service); err != nil { return err } return nil } // CreateCommunityListing creates a new community listing func (s *Service) CreateCommunityListing(ctx context.Context, listing *domain.CommunityListing) error { if s.communityListingRepo == nil { return fmt.Errorf("community listing repository not available") } // Validate the listing if err := listing.Validate(); err != nil { return fmt.Errorf("invalid listing: %w", err) } // Create the listing if err := s.communityListingRepo.Create(ctx, listing); err != nil { return fmt.Errorf("failed to create community listing: %w", err) } return nil }