package matching import ( "context" "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 eventBus domain.EventBus // Geospatial calculator geoCalc geospatial.Calculator } // 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, 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() return &Service{ engine: eng, manager: mgr, pluginMgr: pluginMgr, matchRepo: matchRepo, resourceFlowRepo: resourceFlowRepo, siteRepo: siteRepo, orgRepo: orgRepo, eventBus: eventBus, geoCalc: geoCalc, } } // 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