package plugins import ( "encoding/json" "fmt" "math" "strings" "bugulma/backend/internal/domain" "bugulma/backend/internal/matching/engine" ) // BiowastePlugin implements resource-specific matching logic for biowaste resources type BiowastePlugin struct { config *BiowastePluginConfig } // BiowastePluginConfig contains configuration for the biowaste plugin type BiowastePluginConfig struct { // Contamination limits MaxContaminantPct float64 `json:"max_contaminant_pct"` ContaminationPenalty float64 `json:"contamination_penalty"` // Moisture content requirements MinMoisturePct float64 `json:"min_moisture_pct"` MaxMoisturePct float64 `json:"max_moisture_pct"` MoistureMismatchPenalty float64 `json:"moisture_mismatch_penalty"` // Biodegradability requirements MinBiodegradabilityPct float64 `json:"min_biodegradability_pct"` BiodegradabilityBonus float64 `json:"biodegradability_bonus"` // Regulatory requirements RequiredCertifications []string `json:"required_certifications"` CertificationBonus float64 `json:"certification_bonus"` // Economic factors WasteDisposalSavings float64 `json:"waste_disposal_savings"` // €/tonne saved by avoiding landfill TransportCostPerKm float64 `json:"transport_cost_per_km"` ProcessingCostPerTonne float64 `json:"processing_cost_per_tonne"` // Distance constraints MaxTransportDistance float64 `json:"max_transport_distance"` DistancePenaltyFactor float64 `json:"distance_penalty_factor"` // Temporal constraints ShelfLifeDays int `json:"shelf_life_days"` StorageCostPerDay float64 `json:"storage_cost_per_day"` } // DefaultBiowastePluginConfig returns default configuration for biowaste plugin func DefaultBiowastePluginConfig() *BiowastePluginConfig { return &BiowastePluginConfig{ MaxContaminantPct: 5.0, ContaminationPenalty: 0.3, MinMoisturePct: 40.0, MaxMoisturePct: 80.0, MoistureMismatchPenalty: 0.15, MinBiodegradabilityPct: 70.0, BiodegradabilityBonus: 0.1, RequiredCertifications: []string{"ISO 14001", "HACCP"}, CertificationBonus: 0.1, WasteDisposalSavings: 50.0, // €50/tonne landfill savings TransportCostPerKm: 0.5, // €0.50/tonne/km ProcessingCostPerTonne: 20.0, // €20/tonne processing MaxTransportDistance: 100.0, // 100km max for biowaste DistancePenaltyFactor: 0.01, ShelfLifeDays: 7, // Biowaste degrades quickly StorageCostPerDay: 2.0, // €2/tonne/day storage } } // NewBiowastePlugin creates a new biowaste resource plugin func NewBiowastePlugin(config *BiowastePluginConfig) *BiowastePlugin { if config == nil { config = DefaultBiowastePluginConfig() } return &BiowastePlugin{ config: config, } } // Name returns the plugin name func (bp *BiowastePlugin) Name() string { return "biowaste_plugin" } // ResourceType returns the resource type this plugin handles func (bp *BiowastePlugin) ResourceType() domain.ResourceType { return domain.TypeBiowaste } // SupportsQualityCheck returns true as biowaste plugin provides quality checks func (bp *BiowastePlugin) SupportsQualityCheck() bool { return true } // SupportsEconomicCalculation returns true as biowaste plugin provides economic calculations func (bp *BiowastePlugin) SupportsEconomicCalculation() bool { return true } // SupportsTemporalCheck returns true as biowaste plugin provides temporal checks func (bp *BiowastePlugin) SupportsTemporalCheck() bool { return true } // CheckQualityCompatibility performs biowaste-specific quality compatibility assessment func (bp *BiowastePlugin) CheckQualityCompatibility(source, target *domain.ResourceFlow) (float64, error) { var sourceQuality, targetQuality domain.Quality if err := json.Unmarshal(source.Quality, &sourceQuality); err != nil { return 0, fmt.Errorf("failed to unmarshal source quality: %w", err) } if err := json.Unmarshal(target.Quality, &targetQuality); err != nil { return 0, fmt.Errorf("failed to unmarshal target quality: %w", err) } score := 1.0 var issues []string // Contamination check (critical for biowaste) if sourceQuality.PurityPct != nil { contaminationPct := 100.0 - *sourceQuality.PurityPct // Contamination = 100 - purity if contaminationPct > bp.config.MaxContaminantPct { return 0, fmt.Errorf("contamination level %.1f%% exceeds maximum %.1f%%", contaminationPct, bp.config.MaxContaminantPct) } score -= (contaminationPct / 100.0) * bp.config.ContaminationPenalty if contaminationPct > bp.config.MaxContaminantPct/2 { issues = append(issues, fmt.Sprintf("High contamination: %.1f%%", contaminationPct)) } } // Moisture content compatibility if sourceQuality.MoisturePct != nil { moisture := *sourceQuality.MoisturePct if moisture < bp.config.MinMoisturePct { score -= bp.config.MoistureMismatchPenalty issues = append(issues, fmt.Sprintf("Low moisture: %.1f%% (min %.1f%%)", moisture, bp.config.MinMoisturePct)) } else if moisture > bp.config.MaxMoisturePct { score -= bp.config.MoistureMismatchPenalty issues = append(issues, fmt.Sprintf("High moisture: %.1f%% (max %.1f%%)", moisture, bp.config.MaxMoisturePct)) } } // Biodegradability check if sourceQuality.Composition != "" { // Simple check for biodegradability keywords composition := strings.ToLower(sourceQuality.Composition) biodegradable := strings.Contains(composition, "organic") || strings.Contains(composition, "biodegradable") || strings.Contains(composition, "food waste") || strings.Contains(composition, "green waste") if biodegradable { score += bp.config.BiodegradabilityBonus } else { score -= 0.2 // Penalty for non-biodegradable waste issues = append(issues, "Composition may not be fully biodegradable") } } // Certification compatibility if len(sourceQuality.Certifications) > 0 { matchingCerts := 0 for _, required := range bp.config.RequiredCertifications { for _, has := range sourceQuality.Certifications { if strings.Contains(has, required) { matchingCerts++ break } } } if matchingCerts > 0 { score += bp.config.CertificationBonus } } // Particle size consideration for processing if sourceQuality.ParticleSizeMicron != nil { size := *sourceQuality.ParticleSizeMicron if size > 50000 { // Very large particles score -= 0.05 issues = append(issues, "Large particle size may require additional processing") } } if len(issues) > 0 && score < 0.7 { return score, fmt.Errorf("quality issues: %s", strings.Join(issues, "; ")) } return math.Max(0, math.Min(1, score)), nil } // CalculateEconomicImpact performs biowaste-specific economic impact calculation func (bp *BiowastePlugin) CalculateEconomicImpact(source, target *domain.ResourceFlow, distance float64) (float64, error) { var sourceEcon, targetEcon domain.EconomicData var sourceQty domain.Quantity if err := json.Unmarshal(source.EconomicData, &sourceEcon); err != nil { return 0, fmt.Errorf("failed to unmarshal source economic data: %w", err) } if err := json.Unmarshal(target.EconomicData, &targetEcon); err != nil { return 0, fmt.Errorf("failed to unmarshal target economic data: %w", err) } if err := json.Unmarshal(source.Quantity, &sourceQty); err != nil { return 0, fmt.Errorf("failed to unmarshal source quantity: %w", err) } // Base economic calculation for biowaste // Savings = waste disposal cost avoided + value from reuse - transport - processing // Waste disposal savings (avoiding landfill/incineration) disposalSavings := bp.config.WasteDisposalSavings * sourceQty.Amount // Transport costs transportCost := distance * bp.config.TransportCostPerKm * sourceQty.Amount // Processing costs for the receiving facility processingCost := bp.config.ProcessingCostPerTonne * sourceQty.Amount // Value from reuse (what the target pays for the waste as input) reuseValue := targetEcon.CostIn * sourceQty.Amount // Storage costs due to short shelf life storageDays := float64(bp.config.ShelfLifeDays) storageCost := bp.config.StorageCostPerDay * sourceQty.Amount * storageDays // Distance penalty for biowaste degradation distancePenalty := distance * bp.config.DistancePenaltyFactor * sourceQty.Amount // Annual volume calculation annualVolume := sourceQty.Amount switch sourceQty.TemporalUnit { case domain.UnitPerDay: annualVolume *= 365 case domain.UnitPerWeek: annualVolume *= 52 case domain.UnitPerMonth: annualVolume *= 12 } // Total annual economic impact totalAnnualSavings := (disposalSavings + reuseValue - transportCost - processingCost - storageCost - distancePenalty) * annualVolume // Distance constraint check if distance > bp.config.MaxTransportDistance { return totalAnnualSavings, fmt.Errorf("transport distance %.1fkm exceeds maximum %.1fkm for biowaste", distance, bp.config.MaxTransportDistance) } // Minimum viability check if totalAnnualSavings < 1000.0 { // €1,000 minimum annual savings return totalAnnualSavings, fmt.Errorf("insufficient economic viability: annual savings €%.0f below minimum €1,000", totalAnnualSavings) } return totalAnnualSavings, nil } // CheckTemporalCompatibility performs biowaste-specific temporal compatibility assessment func (bp *BiowastePlugin) CheckTemporalCompatibility(source, target *domain.ResourceFlow) (float64, error) { var sourceTime, targetTime domain.TimeProfile if err := json.Unmarshal(source.TimeProfile, &sourceTime); err != nil { return 0, fmt.Errorf("failed to unmarshal source time profile: %w", err) } if err := json.Unmarshal(target.TimeProfile, &targetTime); err != nil { return 0, fmt.Errorf("failed to unmarshal target time profile: %w", err) } score := 1.0 // Biowaste has short shelf life - temporal alignment is critical sourceHours := bp.calculateWeeklyHours(sourceTime.Availability) targetHours := bp.calculateWeeklyHours(targetTime.Availability) overlapHours := math.Min(sourceHours, targetHours) // Biowaste requires more frequent collection/processing minOverlapHours := 40.0 // 40 hours/week minimum for biowaste if overlapHours < minOverlapHours { return 0, fmt.Errorf("insufficient temporal overlap: %.0f hours/week < minimum %.0f hours/week for biowaste", overlapHours, minOverlapHours) } overlapScore := overlapHours / 168.0 // 168 hours in a week score *= overlapScore // Seasonality consideration (food waste peaks in certain seasons) if len(sourceTime.Seasonality) > 0 && len(targetTime.Seasonality) > 0 { seasonOverlap := bp.calculateSeasonalOverlap(sourceTime.Seasonality, targetTime.Seasonality) score *= (0.8 + 0.2*seasonOverlap) // 80-100% based on seasonal alignment } // Predictability is crucial for biowaste logistics sourcePredictability := 0.5 targetPredictability := 0.5 if sourceTime.PredictabilityScore != nil { sourcePredictability = *sourceTime.PredictabilityScore } if targetTime.PredictabilityScore != nil { targetPredictability = *targetTime.PredictabilityScore } // Biowaste requires high predictability minPredictability := 0.7 avgPredictability := (sourcePredictability + targetPredictability) / 2 if avgPredictability < minPredictability { score *= 0.5 // Significant penalty for unpredictable supply } else { score *= avgPredictability } // Lead time consideration requiredLeadTime := 24.0 // 24 hours for biowaste if sourceTime.LeadTimeDays != nil && targetTime.LeadTimeDays != nil { actualLeadTime := math.Max(float64(*sourceTime.LeadTimeDays), float64(*targetTime.LeadTimeDays)) * 24 if actualLeadTime > requiredLeadTime { score *= 0.9 // Minor penalty for longer lead times } } return math.Max(0, math.Min(1, score)), nil } // ValidateCandidate performs biowaste-specific candidate validation func (bp *BiowastePlugin) ValidateCandidate(candidate *engine.Candidate) (bool, string) { // Biowaste-specific validation rules // 1. Distance constraint if candidate.DistanceKm > bp.config.MaxTransportDistance { return false, fmt.Sprintf("Transport distance %.1fkm exceeds maximum %.1fkm for biowaste", candidate.DistanceKm, bp.config.MaxTransportDistance) } // 2. Minimum annual savings if candidate.EstimatedAnnualSavings < 2000.0 { // Higher threshold for biowaste return false, "Annual savings below €2,000 threshold for biowaste matching" } // 3. Quality score threshold (higher for biowaste due to contamination risks) if candidate.QualityScore < 0.7 { return false, "Quality score below 0.7 threshold for biowaste matching" } // 4. Temporal score (critical for perishable waste) if candidate.TemporalScore < 0.6 { return false, "Temporal compatibility below 0.6 threshold for biowaste matching" } return true, "Biowaste matching candidate validation passed" } // calculateWeeklyHours calculates total available hours per week from availability map func (bp *BiowastePlugin) calculateWeeklyHours(availability map[string]domain.TimeRange) float64 { totalHours := 0.0 for _, timeRange := range availability { // Simple calculation - in production this would parse time ranges properly hours := 8.0 // Default 8 hours per day totalHours += hours _ = timeRange // Mark as used to avoid linter warning } return totalHours } // calculateSeasonalOverlap calculates overlap between seasonality arrays func (bp *BiowastePlugin) calculateSeasonalOverlap(source, target []string) float64 { if len(source) == 0 || len(target) == 0 { return 0 } overlap := 0 for _, s := range source { for _, t := range target { if s == t { overlap++ break } } } return float64(overlap) / math.Max(float64(len(source)), float64(len(target))) }