package plugins import ( "fmt" "bugulma/backend/internal/domain" "bugulma/backend/internal/matching/engine" ) // Manager handles plugin-based matching logic integration type Manager struct { registry PluginRegistry engine *engine.Engine } // NewManager creates a new plugin manager func NewManager(registry PluginRegistry, engine *engine.Engine) *Manager { return &Manager{ registry: registry, engine: engine, } } // EnhanceCandidate uses plugins to enhance candidate evaluation func (pm *Manager) EnhanceCandidate(candidate *engine.Candidate) error { sourceFlow := candidate.SourceFlow targetFlow := candidate.TargetFlow // Get plugins for this resource type plugins := pm.registry.GetByResourceType(sourceFlow.Type) if len(plugins) == 0 { // No plugins available, use default engine logic return nil } // Use the first available plugin (in production, you might want plugin selection logic) plugin := plugins[0] // Calculate distance (simplified - should use geospatial calculator) distance := candidate.DistanceKm if distance == 0 { distance = 10.0 // Placeholder distance } // Apply plugin enhancements if plugin.SupportsQualityCheck() { if qualityScore, err := plugin.CheckQualityCompatibility(sourceFlow, targetFlow); err != nil { return fmt.Errorf("quality check failed: %w", err) } else { // Blend plugin score with existing score candidate.QualityScore = (candidate.QualityScore + qualityScore) / 2 } } if plugin.SupportsEconomicCalculation() { if economicValue, err := plugin.CalculateEconomicImpact(sourceFlow, targetFlow, distance); err != nil { // Log warning but don't fail - use existing calculation fmt.Printf("Warning: Plugin economic calculation failed: %v\n", err) } else { // Use plugin's economic calculation if available candidate.EstimatedAnnualSavings = economicValue } } if plugin.SupportsTemporalCheck() { if temporalScore, err := plugin.CheckTemporalCompatibility(sourceFlow, targetFlow); err != nil { return fmt.Errorf("temporal check failed: %w", err) } else { // Blend plugin score with existing score candidate.TemporalScore = (candidate.TemporalScore + temporalScore) / 2 } } // Recalculate overall score with enhanced components scoring := &engine.ScoringResult{ Compatibility: candidate.CompatibilityScore, Economic: pm.normalizeEconomicScore(candidate.EstimatedAnnualSavings), Temporal: candidate.TemporalScore, Quality: candidate.QualityScore, } // Simple weighted calculation (should match engine logic) weights := map[string]float64{ "compatibility": 0.3, "economic": 0.25, "temporal": 0.25, "quality": 0.2, } candidate.OverallScore = scoring.Compatibility*weights["compatibility"] + scoring.Economic*weights["economic"] + scoring.Temporal*weights["temporal"] + scoring.Quality*weights["quality"] candidate.OverallScore = max(0, min(1, candidate.OverallScore)) return nil } // ValidateWithPlugins validates a candidate using plugin validation logic func (pm *Manager) ValidateWithPlugins(candidate *engine.Candidate) (bool, string) { plugins := pm.registry.GetByResourceType(candidate.SourceFlow.Type) if len(plugins) == 0 { // No plugins available, use default validation return pm.defaultValidation(candidate) } plugin := plugins[0] // Use plugin validation valid, reason := plugin.ValidateCandidate(candidate) if !valid { return false, fmt.Sprintf("Plugin validation failed: %s", reason) } // Also run default validation valid, defaultReason := pm.defaultValidation(candidate) if !valid { return false, fmt.Sprintf("Default validation failed: %s", defaultReason) } return true, "All validations passed" } // GetEnhancedMatchingCriteria returns enhanced criteria based on plugins func (pm *Manager) GetEnhancedMatchingCriteria(resourceType domain.ResourceType) *engine.Criteria { baseCriteria := &engine.Criteria{ ResourceType: resourceType, MaxDistanceKm: 50.0, MinCompatibility: 0.5, MinEconomicScore: 0.3, MinTemporalScore: 0.4, MinQualityScore: 0.4, ExcludeSameOrg: true, Limit: 50, } // Enhance criteria based on resource type switch resourceType { case domain.TypeHeat: baseCriteria.MaxDistanceKm = 30.0 // Heat has shorter transport distance baseCriteria.MinEconomicScore = 0.4 // Higher economic threshold for heat case domain.TypeBiowaste: baseCriteria.MaxDistanceKm = 100.0 // Biowaste can travel farther baseCriteria.MinQualityScore = 0.6 // Higher quality threshold for biowaste case domain.TypeWater: baseCriteria.MaxDistanceKm = 25.0 // Water transport is expensive baseCriteria.MinCompatibility = 0.6 // Water requires better compatibility } return baseCriteria } // normalizeEconomicScore converts economic value to a 0-1 score func (pm *Manager) normalizeEconomicScore(annualSavings float64) float64 { if annualSavings <= 0 { return 0 } // Simple normalization: cap at €100,000 annual savings = score of 1.0 maxSavings := 100000.0 score := min(annualSavings/maxSavings, 1.0) return score } // defaultValidation provides basic validation when no plugins are available func (pm *Manager) defaultValidation(candidate *engine.Candidate) (bool, string) { // Basic validation rules if candidate.OverallScore < 0.3 { return false, "Overall score below minimum threshold" } if candidate.DistanceKm > 100.0 { return false, "Distance exceeds maximum transport limit" } if candidate.EstimatedAnnualSavings < 1000.0 { return false, "Annual savings below minimum threshold" } return true, "Default validation passed" } // RegisterDefaultPlugins registers the built-in plugins func RegisterDefaultPlugins(registry PluginRegistry) error { // Register heat plugin heatPlugin := NewHeatPlugin(nil) if err := registry.Register(heatPlugin); err != nil { return fmt.Errorf("failed to register heat plugin: %w", err) } // Register biowaste plugin biowastePlugin := NewBiowastePlugin(nil) if err := registry.Register(biowastePlugin); err != nil { return fmt.Errorf("failed to register biowaste plugin: %w", err) } return nil } // Helper functions func min(a, b float64) float64 { if a < b { return a } return b } func max(a, b float64) float64 { if a > b { return a } return b }