package server import ( "context" "fmt" "log" "bugulma/backend/internal/analysis/regulatory" "bugulma/backend/internal/analysis/risk" "bugulma/backend/internal/analysis/transport" "bugulma/backend/internal/domain" "bugulma/backend/internal/financial" "bugulma/backend/internal/geospatial" "bugulma/backend/internal/graph" "bugulma/backend/internal/handler" "bugulma/backend/internal/matching" "bugulma/backend/internal/middleware" "bugulma/backend/internal/repository" "bugulma/backend/internal/routes" "bugulma/backend/internal/service" "bugulma/backend/pkg/config" "github.com/gin-gonic/gin" "github.com/joho/godotenv" "github.com/neo4j/neo4j-go-driver/v5/neo4j" "gorm.io/driver/postgres" "gorm.io/gorm" ) // StartServer initializes and starts the Bugulma backend server func StartServer(port string) error { // Load .env file if err := godotenv.Load(); err != nil { log.Printf("Warning: .env file not found, using environment variables") } // Load configuration cfg := config.Load() // Override port if provided if port != "" { cfg.ServerPort = port } // Initialize database db, err := initializeDatabase(cfg) if err != nil { return fmt.Errorf("failed to initialize database: %w", err) } // Initialize repositories userRepo, orgRepo, siteRepo, _, resourceFlowRepo, matchRepo, negotiationHistoryRepo, sharedAssetRepo, proposalRepo, geoFeatureRepo, productRepo, serviceRepo, communityListingRepo, publicTransportRepo := initializeRepositories(db) // Seed default data if err := seedDefaultData(userRepo); err != nil { log.Printf("Warning: Failed to seed default data: %v", err) } // (Public transport seeding will occur after services are initialized) // Initialize graph database (optional) graphSyncService, graphHandler, graphTraversalHandler, orgGraphRepo, siteGraphRepo, addressGraphRepo, flowGraphRepo, matchGraphRepo, sharedAssetGraphRepo := initializeGraphDatabase(cfg) // Initialize services orgService, siteService, resourceFlowService, geospatialService, sharedAssetService, analyticsService, authService, matchingSvc, cacheService, websocketService, aiService, proposalService, spatialMatcher, environmentalSvc, facilityOptimizer, economicService, userService, adminService, i18nService, subscriptionService, verificationService, productRepo, serviceRepo, communityListingRepo, publicTransportService := initializeServices( cfg, userRepo, orgRepo, siteRepo, resourceFlowRepo, matchRepo, negotiationHistoryRepo, sharedAssetRepo, proposalRepo, geoFeatureRepo, orgGraphRepo, siteGraphRepo, addressGraphRepo, flowGraphRepo, matchGraphRepo, sharedAssetGraphRepo, db, productRepo, serviceRepo, communityListingRepo, ) // Store geographical services for future API endpoints _ = spatialMatcher // Will be used in future API handlers _ = environmentalSvc // Will be used in future API handlers _ = facilityOptimizer // Will be used in future API handlers // Seed public transport data (if repository and imported files present) if publicTransportRepo != nil && publicTransportService != nil { if err := service.ImportPublicTransportData(context.Background(), publicTransportService, publicTransportRepo); err != nil { log.Printf("Warning: failed to import public transport data: %v", err) } } // Set graph repositories on services that need them if orgGraphRepo != nil { orgService.SetGraphRepository(orgGraphRepo) } // Initialize GTFS repository and schedule service gtfsRepo := repository.NewPublicTransportGTFSRepository(db) scheduleService := service.NewScheduleService(gtfsRepo, publicTransportRepo) // Initialize handlers (orgHandler is created inside initializeHandlers) // Create SettingsService early so we can inject it into handlers and the router settingsService := service.NewSettingsService(repository.NewSystemSettingsRepository(db)) orgHandler, siteHandler, resourceFlowHandler, proposalHandler, matchingHandler, authHandler, sharedAssetHandler, geospatialHandler, analyticsHandler, aiHandler, heritageHandler, userHandler, orgAdminHandler, i18nHandler, contentHandler, adminHandler, settingsHandler, subscriptionHandler, discoveryHandler, publicTransportHandler := initializeHandlers( cfg, orgService, siteService, resourceFlowService, matchingSvc, authService, sharedAssetService, proposalService, geospatialService, analyticsService, aiService, cacheService, db, userService, adminService, i18nService, subscriptionService, verificationService, publicTransportService, scheduleService, settingsService, ) // Setup router (pass settings service so maintenance middleware can be registered) router := setupRouter(authService, settingsService) // Initialize event-driven matching service with proper dependencies // Note: eventBus is created in initializeServices, but we need to recreate it here for event-driven service eventBus, err := service.NewRedisEventBus("redis://localhost:6379") if err != nil { log.Printf("Warning: Failed to create Redis event bus for event-driven matching: %v", err) eventBus = nil } eventDrivenMatchingService := service.NewEventDrivenMatchingService( eventBus, matchingSvc, resourceFlowRepo, matchRepo, websocketService.GetHub(), economicService, ) if eventBus != nil { if err := eventDrivenMatchingService.StartEventProcessing(context.Background()); err != nil { log.Printf("Warning: Failed to start event-driven matching: %v", err) } } // Setup routes setupRoutes(router, orgHandler, siteHandler, resourceFlowHandler, proposalHandler, matchingHandler, authHandler, sharedAssetHandler, geospatialHandler, analyticsHandler, aiHandler, heritageHandler, graphHandler, graphTraversalHandler, websocketService, orgService, siteService, geospatialService, graphSyncService, userHandler, orgAdminHandler, i18nHandler, contentHandler, adminHandler, settingsHandler, subscriptionHandler, authService, discoveryHandler, publicTransportHandler) // Start server log.Printf("Server starting on port %s", cfg.ServerPort) if err := router.Run(fmt.Sprintf(":%s", cfg.ServerPort)); err != nil { return fmt.Errorf("failed to start server: %w", err) } return nil } // initializeDatabase sets up the database connection and runs migrations func initializeDatabase(cfg *config.Config) (*gorm.DB, error) { dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", cfg.PostgresHost, cfg.PostgresPort, cfg.PostgresUser, cfg.PostgresPassword, cfg.PostgresDB, cfg.PostgresSSLMode, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err) } log.Println("Successfully connected to PostgreSQL database") // Run PostGIS-specific migrations FIRST (before AutoMigrate) if err := domain.RunPostGISMigrations(db); err != nil { log.Printf("Warning: Failed to run PostGIS migrations: %v", err) log.Println("Continuing without PostGIS spatial features") } else { log.Println("PostGIS migrations completed successfully") } // Run search migrations (pg_trgm extension and indexes) if err := domain.RunSearchMigrations(db); err != nil { log.Printf("Warning: Failed to run search migrations: %v", err) log.Println("Continuing without fuzzy search features") } else { log.Println("Search migrations completed successfully") } // Auto-migrate database schema if err := domain.AutoMigrate(db); err != nil { return nil, fmt.Errorf("failed to migrate database: %w", err) } // Create additional indexes after AutoMigrate if err := domain.CreateIndexes(db); err != nil { log.Printf("Warning: Failed to create indexes: %v", err) } else { log.Println("Indexes created successfully") } log.Println("Database migration completed successfully") return db, nil } // initializeRepositories creates all repository instances func initializeRepositories(db *gorm.DB) (domain.UserRepository, domain.OrganizationRepository, domain.SiteRepository, domain.AddressRepository, domain.ResourceFlowRepository, domain.MatchRepository, domain.NegotiationHistoryRepository, domain.SharedAssetRepository, domain.ProposalRepository, domain.GeographicalFeatureRepository, domain.ProductRepository, domain.ServiceRepository, domain.CommunityListingRepository, domain.PublicTransportRepository) { orgRepo := repository.NewOrganizationRepository(db) siteRepo := repository.NewSiteRepository(db) addressRepo := repository.NewAddressRepository(db) resourceFlowRepo := repository.NewResourceFlowRepository(db) matchRepo := repository.NewMatchRepository(db) negotiationHistoryRepo := repository.NewNegotiationHistoryRepository(db) sharedAssetRepo := repository.NewSharedAssetRepository(db) proposalRepo := repository.NewProposalRepository(db) userRepo := repository.NewUserRepository(db) geoFeatureRepo := repository.NewGeographicalFeatureRepository(db) productRepo := repository.NewProductRepository(db) serviceRepo := repository.NewServiceRepository(db) communityListingRepo := repository.NewCommunityListingRepository(db) publicTransportRepo := repository.NewPublicTransportRepository(db) _ = repository.NewResourceFlowVersionRepository(db) return userRepo, orgRepo, siteRepo, addressRepo, resourceFlowRepo, matchRepo, negotiationHistoryRepo, sharedAssetRepo, proposalRepo, geoFeatureRepo, productRepo, serviceRepo, communityListingRepo, publicTransportRepo } // seedDefaultData seeds initial data like admin user func seedDefaultData(userRepo domain.UserRepository) error { _, err := userRepo.GetByEmail(context.Background(), "admin@turash.dev") if err == nil { return nil } adminUser := &domain.User{ ID: "admin-1", Email: "admin@turash.dev", Name: "Admin User", Password: "$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", // "password" hashed with bcrypt Role: domain.UserRoleAdmin, } return userRepo.Create(context.Background(), adminUser) } // initializeGraphDatabase initializes Neo4j graph database if enabled func initializeGraphDatabase(cfg *config.Config) (*service.GraphSyncService, *handler.GraphHandler, *handler.GraphTraversalHandler, *repository.GraphOrganizationRepository, *repository.GraphSiteRepository, *repository.GraphAddressRepository, *repository.GraphResourceFlowRepository, *repository.GraphMatchRepository, *repository.GraphSharedAssetRepository) { if !cfg.Neo4jEnabled { return nil, nil, nil, nil, nil, nil, nil, nil, nil } neo4jURI := cfg.Neo4jURI if neo4jURI == "" { log.Println("Warning: Neo4j URI not configured, graph database disabled") return nil, nil, nil, nil, nil, nil, nil, nil, nil } auth := neo4j.BasicAuth(cfg.Neo4jUsername, cfg.Neo4jPassword, "") driver, err := neo4j.NewDriverWithContext(neo4jURI, auth) if err != nil { log.Printf("Warning: Failed to create Neo4j driver: %v", err) log.Println("Continuing without graph database features") return nil, nil, nil, nil, nil, nil, nil, nil, nil } // Verify connectivity ctx := context.Background() if err := driver.VerifyConnectivity(ctx); err != nil { log.Printf("Warning: Failed to connect to Neo4j: %v", err) log.Println("Continuing without graph database features") return nil, nil, nil, nil, nil, nil, nil, nil, nil } log.Println("Successfully connected to Neo4j graph database") // Initialize graph repositories orgGraphRepo := repository.NewGraphOrganizationRepository(driver, cfg.Neo4jDatabase) siteGraphRepo := repository.NewGraphSiteRepository(driver, cfg.Neo4jDatabase) addressGraphRepo := repository.NewGraphAddressRepository(driver, cfg.Neo4jDatabase) flowGraphRepo := repository.NewGraphResourceFlowRepository(driver, cfg.Neo4jDatabase) matchGraphRepo := repository.NewGraphMatchRepository(driver, cfg.Neo4jDatabase) sharedAssetGraphRepo := repository.NewGraphSharedAssetRepository(driver, cfg.Neo4jDatabase) productGraphRepo := repository.NewGraphProductRepository(driver, cfg.Neo4jDatabase) serviceGraphRepo := repository.NewGraphServiceRepository(driver, cfg.Neo4jDatabase) // Initialize graph sync service graphSyncService := service.NewGraphSyncService( orgGraphRepo, siteGraphRepo, addressGraphRepo, flowGraphRepo, matchGraphRepo, sharedAssetGraphRepo, productGraphRepo, serviceGraphRepo, ) // Initialize graph calculator for traversal service graphCalculator := graph.NewCalculatorWithDefaults(driver, cfg.Neo4jDatabase) // Initialize services graphTraversalService := service.NewGraphTraversalService(graphCalculator) // Initialize handlers graphHandler := handler.NewGraphHandler(driver, cfg.Neo4jDatabase, graphSyncService) graphTraversalHandler := handler.NewGraphTraversalHandler(graphTraversalService) return graphSyncService, graphHandler, graphTraversalHandler, orgGraphRepo, siteGraphRepo, addressGraphRepo, flowGraphRepo, matchGraphRepo, sharedAssetGraphRepo } // initializeServices creates all service instances func initializeServices( cfg *config.Config, userRepo domain.UserRepository, orgRepo domain.OrganizationRepository, siteRepo domain.SiteRepository, resourceFlowRepo domain.ResourceFlowRepository, matchRepo domain.MatchRepository, negotiationHistoryRepo domain.NegotiationHistoryRepository, sharedAssetRepo domain.SharedAssetRepository, proposalRepo domain.ProposalRepository, geoFeatureRepo domain.GeographicalFeatureRepository, orgGraphRepo *repository.GraphOrganizationRepository, siteGraphRepo *repository.GraphSiteRepository, addressGraphRepo *repository.GraphAddressRepository, flowGraphRepo *repository.GraphResourceFlowRepository, matchGraphRepo *repository.GraphMatchRepository, sharedAssetGraphRepo *repository.GraphSharedAssetRepository, db *gorm.DB, productRepo domain.ProductRepository, serviceRepo domain.ServiceRepository, communityListingRepo domain.CommunityListingRepository, ) (*service.OrganizationService, *service.SiteService, *service.ResourceFlowService, *service.GeospatialService, *service.SharedAssetService, *service.AnalyticsService, *service.AuthService, *matching.Service, service.CacheService, *service.WebSocketService, *service.AIService, *service.ProposalService, *service.SpatialResourceMatcher, *service.EnvironmentalImpactService, *service.FacilityLocationOptimizer, *service.EconomicService, *service.UserService, *service.AdminService, *service.I18nService, *service.SubscriptionService, *service.VerificationService, domain.ProductRepository, domain.ServiceRepository, domain.CommunityListingRepository, *service.PublicTransportService) { // Create event bus first eventBus, err := service.NewRedisEventBus("redis://localhost:6379") if err != nil { log.Printf("Warning: Failed to create Redis event bus, using in-memory: %v", err) eventBus = nil } // Create services orgService := service.NewOrganizationService(orgRepo, nil) // Graph repo set later if available siteService := service.NewSiteService(siteRepo) resourceFlowService := service.NewResourceFlowService(resourceFlowRepo, eventBus) authService := service.NewAuthService(userRepo, cfg.JWTSecret) sharedAssetService := service.NewSharedAssetService(sharedAssetRepo) geospatialService := service.NewGeospatialService(db, geoFeatureRepo) analyticsService := service.NewAnalyticsService(db, orgRepo, siteRepo, resourceFlowRepo, matchRepo, sharedAssetRepo) proposalService := service.NewProposalService(proposalRepo) // Initialize geospatial calculator geoCalc := geospatial.NewCalculatorWithDefaults() // Initialize geographical transportation service geoTransportSvc := service.NewTransportationService(geoCalc) // Initialize geographical services spatialMatcher := service.NewSpatialResourceMatcher(geoFeatureRepo, siteRepo, resourceFlowRepo, geospatialService, geoTransportSvc, geoCalc) environmentalSvc := service.NewEnvironmentalImpactService(geoFeatureRepo, siteRepo, geospatialService, geoCalc) facilityOptimizer := service.NewFacilityLocationOptimizer(geoFeatureRepo, siteRepo, geospatialService, spatialMatcher, environmentalSvc, geoTransportSvc) // Create cache and websocket services var cacheService service.CacheService redisCache, err := service.NewRedisCacheService(cfg.RedisURL) if err != nil { log.Printf("Warning: Failed to initialize Redis cache, using in-memory cache: %v", err) cacheService = service.NewMemoryCacheService() } else { log.Println("Successfully initialized Redis cache service") cacheService = redisCache } websocketService := service.NewWebSocketService() // Create analysis services for matching riskSvc := risk.NewService() transportSvc := transport.NewService() regulatorySvc := regulatory.NewService() // Create matching service matchingSvc := matching.NewService( matchRepo, negotiationHistoryRepo, resourceFlowRepo, siteRepo, orgRepo, productRepo, serviceRepo, communityListingRepo, riskSvc, transportSvc, regulatorySvc, eventBus, ) // Create financial calculator and economic service financialConfig := financial.DefaultConfig() financialCalculator := financial.NewCalculator(financialConfig) economicService := service.NewEconomicService(financialCalculator) aiService := service.NewAIService() // Create additional services needed by handlers userService := service.NewUserService(userRepo) // Create trust service trustRepo := repository.NewTrustMetricsRepository(db) verifiedRepo := repository.NewVerifiedDataRepository(db) historicalRepo := repository.NewHistoricalSuccessRepository(db) trustService := service.NewTrustService(trustRepo, verifiedRepo, historicalRepo) // Create i18n service - simplified initialization locRepo := repository.NewLocalizationRepository(db) i18nService := service.NewI18nService(nil, locRepo, nil, nil) // Simplified - can be enhanced later // Create subscription service subscriptionRepo := repository.NewSubscriptionRepository(db) // UsageTrackingRepository can be nil for now - create if needed later subscriptionService := service.NewSubscriptionService(subscriptionRepo, nil, userRepo) // Create verification service verificationService := service.NewVerificationService(trustService, orgService) // Create admin service adminService := service.NewAdminService(orgService, userService, verificationService, i18nService) // Initialize public transport service (reads data/bugulma_public_transport_enriched.json if available) publicTransportService, err := service.NewPublicTransportService("data") if err != nil { log.Printf("Warning: failed to initialize public transport service: %v", err) publicTransportService = nil } return orgService, siteService, resourceFlowService, geospatialService, sharedAssetService, analyticsService, authService, matchingSvc, cacheService, websocketService, aiService, proposalService, spatialMatcher, environmentalSvc, facilityOptimizer, economicService, userService, adminService, i18nService, subscriptionService, verificationService, productRepo, serviceRepo, communityListingRepo, publicTransportService } // initializeHandlers creates all HTTP handler instances func initializeHandlers( cfg *config.Config, orgService *service.OrganizationService, siteService *service.SiteService, resourceFlowService *service.ResourceFlowService, matchingSvc *matching.Service, authService *service.AuthService, sharedAssetService *service.SharedAssetService, proposalService *service.ProposalService, geospatialService *service.GeospatialService, analyticsService *service.AnalyticsService, aiService *service.AIService, cacheService service.CacheService, db *gorm.DB, userService *service.UserService, adminService *service.AdminService, i18nService *service.I18nService, subscriptionService *service.SubscriptionService, verificationService *service.VerificationService, publicTransportService *service.PublicTransportService, scheduleService *service.ScheduleService, settingsService *service.SettingsService, ) (*handler.OrganizationHandler, *handler.SiteHandler, *handler.ResourceFlowHandler, *handler.ProposalHandler, *handler.MatchingHandler, *handler.AuthHandler, *handler.SharedAssetHandler, *handler.GeospatialHandler, *handler.AnalyticsHandler, *handler.AIHandler, *handler.HeritageHandler, *handler.UserHandler, *handler.OrganizationAdminHandler, *handler.I18nHandler, *handler.ContentHandler, *handler.AdminHandler, *handler.SettingsAdminHandler, *handler.SubscriptionHandler, *handler.DiscoveryHandler, *handler.PublicTransportHandler) { imageService := service.NewImageService(cfg) orgHandler := handler.NewOrganizationHandler(orgService, imageService, resourceFlowService, matchingSvc, proposalService) siteHandler := handler.NewSiteHandler(siteService) resourceFlowHandler := handler.NewResourceFlowHandler(resourceFlowService) proposalHandler := handler.NewProposalHandler(proposalService) matchingHandler := handler.NewMatchingHandler(matchingSvc, nil) authHandler := handler.NewAuthHandler(authService) sharedAssetHandler := handler.NewSharedAssetHandler(sharedAssetService) geospatialHandler := handler.NewGeospatialHandler(geospatialService) analyticsHandler := handler.NewAnalyticsHandler(analyticsService) aiHandler := handler.NewAIHandler(aiService) heritageHandler := handler.NewHeritageHandler(repository.NewHeritageRepository(db)) // Additional handlers userHandler := handler.NewUserHandler(userService) orgAdminHandler := handler.NewOrganizationAdminHandler(orgHandler, verificationService, adminService) i18nHandler := handler.NewI18nHandler(i18nService) contentHandler := handler.NewContentHandler(service.NewContentService( repository.NewStaticPageRepository(db), repository.NewAnnouncementRepository(db), repository.NewMediaAssetRepository(db), )) adminHandler := handler.NewAdminHandler(adminService, analyticsService) settingsHandler := handler.NewSettingsAdminHandler(settingsService) subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService) discoveryHandler := handler.NewDiscoveryHandler(matchingSvc) publicTransportHandler := handler.NewPublicTransportHandler(publicTransportService, scheduleService) return orgHandler, siteHandler, resourceFlowHandler, proposalHandler, matchingHandler, authHandler, sharedAssetHandler, geospatialHandler, analyticsHandler, aiHandler, heritageHandler, userHandler, orgAdminHandler, i18nHandler, contentHandler, adminHandler, settingsHandler, subscriptionHandler, discoveryHandler, publicTransportHandler } // setupRouter configures the Gin router with middleware func setupRouter(authService *service.AuthService, settingsSvc *service.SettingsService) *gin.Engine { router := gin.Default() // Serve static files router.Static("/static", "./static") // CORS middleware router.Use(func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) // Context middleware for extracting user/org info from JWT or headers router.Use(middleware.ContextMiddleware(authService)) // Maintenance middleware: block non-whitelisted requests when maintenance mode is enabled router.Use(middleware.MaintenanceMiddleware(settingsSvc, []string{"/api/v1/admin", "/health", "/metrics"})) return router } // setupRoutes configures all API routes using the organized routes package func setupRoutes( router *gin.Engine, orgHandler *handler.OrganizationHandler, siteHandler *handler.SiteHandler, resourceHandler *handler.ResourceFlowHandler, proposalHandler *handler.ProposalHandler, matchingHandler *handler.MatchingHandler, authHandler *handler.AuthHandler, sharedAssetHandler *handler.SharedAssetHandler, geospatialHandler *handler.GeospatialHandler, analyticsHandler *handler.AnalyticsHandler, aiHandler *handler.AIHandler, heritageHandler *handler.HeritageHandler, graphHandler *handler.GraphHandler, graphTraversalHandler *handler.GraphTraversalHandler, websocketService *service.WebSocketService, orgService *service.OrganizationService, siteService *service.SiteService, geospatialService *service.GeospatialService, graphSyncService *service.GraphSyncService, userHandler *handler.UserHandler, orgAdminHandler *handler.OrganizationAdminHandler, i18nHandler *handler.I18nHandler, contentHandler *handler.ContentHandler, adminHandler *handler.AdminHandler, settingsHandler *handler.SettingsAdminHandler, subscriptionHandler *handler.SubscriptionHandler, authService *service.AuthService, discoveryHandler *handler.DiscoveryHandler, publicTransportHandler *handler.PublicTransportHandler, ) { routes.RegisterAllRoutes( router, orgHandler, siteHandler, resourceHandler, proposalHandler, matchingHandler, authHandler, sharedAssetHandler, geospatialHandler, analyticsHandler, aiHandler, heritageHandler, graphHandler, graphTraversalHandler, websocketService, userHandler, orgAdminHandler, i18nHandler, contentHandler, adminHandler, settingsHandler, subscriptionHandler, authService, discoveryHandler, publicTransportHandler, ) }