package engine import ( "encoding/json" "math" "bugulma/backend/internal/analysis/regulatory" "bugulma/backend/internal/analysis/risk" "bugulma/backend/internal/analysis/transport" "bugulma/backend/internal/domain" ) // Criteria defines the criteria for finding matches type Criteria struct { ResourceType domain.ResourceType Direction domain.ResourceDirection OrganizationID string SiteID string MaxDistanceKm float64 MinCompatibility float64 MinEconomicScore float64 MinTemporalScore float64 MinQualityScore float64 ExcludeSameOrg bool Limit int } // Candidate represents a potential match before it's created type Candidate struct { SourceFlow *domain.ResourceFlow TargetFlow *domain.ResourceFlow DistanceKm float64 CompatibilityScore float64 EconomicScore float64 TemporalScore float64 QualityScore float64 OverallScore float64 Priority int EstimatedAnnualSavings float64 RiskLevel string } // ScoringResult contains scoring components type ScoringResult struct { Compatibility float64 Economic float64 Temporal float64 Quality float64 Overall float64 } // Engine handles the core matching algorithm and evaluation type Engine struct { riskService *risk.Service transportService *transport.Service regulatoryService *regulatory.Service } // NewEngine creates a new matching engine func NewEngine( riskSvc *risk.Service, transportSvc *transport.Service, regulatorySvc *regulatory.Service, ) *Engine { return &Engine{ riskService: riskSvc, transportService: transportSvc, regulatoryService: regulatorySvc, } } // FindMatches finds potential matches based on criteria func (e *Engine) FindMatches( criteria Criteria, sourceFlows []*domain.ResourceFlow, targetFlows []*domain.ResourceFlow, ) ([]*Candidate, error) { var candidates []*Candidate for _, source := range sourceFlows { for _, target := range targetFlows { // Check if flows are compatible (same type, opposite direction) if !e.areFlowsCompatible(source, target, criteria) { continue } // Calculate distance (simplified - would use actual geographic calculation) distance := 10.0 // Placeholder // Evaluate candidate candidate := e.evaluateCandidate(source, target, distance, criteria) // Check if candidate meets minimum criteria if e.meetsCriteria(candidate, criteria) { candidates = append(candidates, candidate) } } } // Rank and limit results candidates = e.rankCandidates(candidates) if criteria.Limit > 0 && len(candidates) > criteria.Limit { candidates = candidates[:criteria.Limit] } return candidates, nil } // evaluateCandidate performs complete evaluation of a match candidate func (e *Engine) evaluateCandidate( source, target *domain.ResourceFlow, distance float64, criteria Criteria, ) *Candidate { candidate := &Candidate{ SourceFlow: source, TargetFlow: target, DistanceKm: distance, } // Calculate all scoring components candidate.CompatibilityScore = e.calculateCompatibilityScore(source, target) candidate.TemporalScore = e.calculateTemporalScore(source, target) candidate.QualityScore = e.calculateQualityScore(source, target) candidate.EconomicScore = e.calculateEconomicScore(source, target, distance) // Calculate overall score scoring := &ScoringResult{ Compatibility: candidate.CompatibilityScore, Economic: candidate.EconomicScore, Temporal: candidate.TemporalScore, Quality: candidate.QualityScore, } candidate.OverallScore = e.calculateOverallScore(scoring, distance) // Calculate priority candidate.Priority = e.calculatePriority(candidate.OverallScore, candidate.EstimatedAnnualSavings) // Assess risk level riskAssessment := e.riskService.AssessResourceFlowRisk(distance, source.Type, candidate.EconomicScore) candidate.RiskLevel = riskAssessment.RiskLevel return candidate } // calculateCompatibilityScore assesses technical compatibility func (e *Engine) calculateCompatibilityScore(output, input *domain.ResourceFlow) float64 { if output.Type != input.Type { return 0.0 // Must be same resource type } compatibility := 1.0 // Parse quality data var outputQuality, inputQuality domain.Quality json.Unmarshal(output.Quality, &outputQuality) json.Unmarshal(input.Quality, &inputQuality) // Temperature compatibility (critical for heat/steam) if outputQuality.TemperatureCelsius != nil && inputQuality.TemperatureCelsius != nil { tempDiff := math.Abs(*outputQuality.TemperatureCelsius - *inputQuality.TemperatureCelsius) maxTempDiff := 10.0 // 10°C tolerance tempCompat := math.Max(0, 1.0-(tempDiff/maxTempDiff)) compatibility *= tempCompat } // Pressure compatibility if outputQuality.PressureBar != nil && inputQuality.PressureBar != nil { pressureDiff := math.Abs(*outputQuality.PressureBar - *inputQuality.PressureBar) maxPressureDiff := *inputQuality.PressureBar * 0.2 // 20% tolerance if maxPressureDiff > 0 { pressureCompat := math.Max(0, 1.0-(pressureDiff/maxPressureDiff)) compatibility *= pressureCompat } } // Purity compatibility if outputQuality.PurityPct != nil && inputQuality.PurityPct != nil { purityDiff := math.Abs(*outputQuality.PurityPct - *inputQuality.PurityPct) maxPurityDiff := 5.0 // 5% tolerance purityCompat := math.Max(0, 1.0-(purityDiff/maxPurityDiff)) compatibility *= purityCompat } // Physical state compatibility if outputQuality.PhysicalState != "" && inputQuality.PhysicalState != "" { if outputQuality.PhysicalState != inputQuality.PhysicalState { compatibility *= 0.5 // Penalty for state mismatch } } // Enhanced quality compatibility checks compatibility *= e.calculateEnhancedQualityCompatibility(&outputQuality, &inputQuality) return compatibility } // calculateEnhancedQualityCompatibility checks advanced quality parameters func (e *Engine) calculateEnhancedQualityCompatibility(output, input *domain.Quality) float64 { compatibility := 1.0 // Viscosity compatibility if output.ViscosityCp != nil && input.ViscosityCp != nil { viscosityDiff := math.Abs(*output.ViscosityCp - *input.ViscosityCp) maxViscosityDiff := *input.ViscosityCp * 0.3 // 30% tolerance if maxViscosityDiff > 0 { viscosityCompat := math.Max(0, 1.0-(viscosityDiff/maxViscosityDiff)) compatibility *= viscosityCompat } } // Certification compatibility if len(output.Certifications) > 0 && len(input.Certifications) > 0 { matchingCerts := 0 for _, outputCert := range output.Certifications { for _, inputCert := range input.Certifications { if outputCert == inputCert { matchingCerts++ break } } } if len(input.Certifications) > 0 { certCompat := float64(matchingCerts) / float64(len(input.Certifications)) compatibility *= certCompat } } return compatibility } // calculateTemporalScore assesses temporal compatibility func (e *Engine) calculateTemporalScore(output, input *domain.ResourceFlow) float64 { var outputTime, inputTime domain.TimeProfile json.Unmarshal(output.TimeProfile, &outputTime) json.Unmarshal(input.TimeProfile, &inputTime) score := 1.0 // Pattern match patternMatch := 0.5 if outputTime.SupplyPattern == inputTime.SupplyPattern { patternMatch = 1.0 } score *= patternMatch // Seasonality match seasonMatch := 1.0 if len(outputTime.Seasonality) > 0 && len(inputTime.Seasonality) > 0 { overlap := 0 for _, os := range outputTime.Seasonality { for _, is := range inputTime.Seasonality { if os == is { overlap++ } } } seasonMatch = float64(overlap) / float64(len(inputTime.Seasonality)) } score *= seasonMatch // Predictability match predictabilityCompat := 1.0 if outputTime.PredictabilityScore != nil && inputTime.PredictabilityScore != nil { predictabilityDiff := math.Abs(*outputTime.PredictabilityScore - *inputTime.PredictabilityScore) maxPredictabilityDiff := 0.3 // 30% difference tolerance predictabilityCompat = math.Max(0.5, 1.0-(predictabilityDiff/maxPredictabilityDiff)) } score *= predictabilityCompat return score } // calculateQualityScore assesses data quality func (e *Engine) calculateQualityScore(output, input *domain.ResourceFlow) float64 { precisionScores := map[domain.PrecisionLevel]float64{ domain.PrecisionMeasured: 1.0, domain.PrecisionEstimated: 0.75, domain.PrecisionRough: 0.5, } sourceScores := map[domain.SourceType]float64{ domain.SourceDevice: 1.0, domain.SourceCalculated: 0.7, domain.SourceDeclared: 0.6, } outputScore := precisionScores[output.PrecisionLevel] * sourceScores[output.SourceType] inputScore := precisionScores[input.PrecisionLevel] * sourceScores[input.SourceType] return (outputScore + inputScore) / 2 } // calculateEconomicScore assesses economic viability func (e *Engine) calculateEconomicScore(output, input *domain.ResourceFlow, distance float64) float64 { var outputEcon, inputEcon domain.EconomicData json.Unmarshal(output.EconomicData, &outputEcon) json.Unmarshal(input.EconomicData, &inputEcon) // Simplified economic calculation // cost_in - cost_out - transport_cost transportCost := e.transportService.AnalyzeTransport(distance, output.Type).CostPerUnit // Get quantity var outputQty domain.Quantity json.Unmarshal(output.Quantity, &outputQty) savings := inputEcon.CostIn - outputEcon.CostOut - transportCost if savings > 0 { return math.Min(savings*outputQty.Amount*0.01, 1.0) // Scale to 0-1 } return 0.1 // Minimum score for negative economics } // calculateOverallScore computes weighted average func (e *Engine) calculateOverallScore(scoring *ScoringResult, distance float64) float64 { weights := map[string]float64{ "quality": 0.25, "temporal": 0.20, "economic": 0.15, "trust": 0.15, "transport": 0.10, "regulatory": 0.10, "distance": 0.05, } // Transport penalty transportPenalty := math.Min(1.0, distance/50.0) // Regulatory penalty regulatoryPenalty := 0.1 // Simplified overall := scoring.Compatibility*weights["quality"] + scoring.Temporal*weights["temporal"] + scoring.Economic*weights["economic"] + scoring.Quality*weights["trust"] - transportPenalty*weights["transport"] - regulatoryPenalty*weights["regulatory"] + (1.0-transportPenalty)*weights["distance"] return math.Max(0, math.Min(1, overall)) } // areFlowsCompatible checks if two flows can potentially match func (e *Engine) areFlowsCompatible(source, target *domain.ResourceFlow, criteria Criteria) bool { // Must be same resource type if source.Type != target.Type { return false } // Must be opposite directions if source.Direction == target.Direction { return false } // Must be different organizations (unless explicitly allowed) if !criteria.ExcludeSameOrg && source.OrganizationID == target.OrganizationID { return false } return true } // meetsCriteria checks if a candidate meets the specified criteria func (e *Engine) meetsCriteria(candidate *Candidate, criteria Criteria) bool { if candidate.CompatibilityScore < criteria.MinCompatibility { return false } if candidate.EconomicScore < criteria.MinEconomicScore { return false } if candidate.TemporalScore < criteria.MinTemporalScore { return false } if candidate.QualityScore < criteria.MinQualityScore { return false } if criteria.MaxDistanceKm > 0 && candidate.DistanceKm > criteria.MaxDistanceKm { return false } return true } // rankCandidates sorts candidates by overall score and priority func (e *Engine) rankCandidates(candidates []*Candidate) []*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 } // calculatePriority calculates match priority func (e *Engine) calculatePriority(overallScore, annualSavings float64) int { score := overallScore * 0.7 economic := math.Min(annualSavings/10000, 1.0) * 0.3 // Cap at €10k combined := (score + economic) * 100 switch { case combined >= 90: return 1 // High priority case combined >= 70: return 2 // Medium priority case combined >= 50: return 3 // Low priority default: return 4 // Very low priority } }