package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/breakpilot/edu-search-service/internal/api/handlers" "github.com/breakpilot/edu-search-service/internal/config" "github.com/breakpilot/edu-search-service/internal/database" "github.com/breakpilot/edu-search-service/internal/indexer" "github.com/breakpilot/edu-search-service/internal/orchestrator" "github.com/breakpilot/edu-search-service/internal/search" "github.com/breakpilot/edu-search-service/internal/staff" "github.com/gin-gonic/gin" ) func main() { log.Println("Starting edu-search-service...") // Load configuration cfg := config.Load() log.Printf("Configuration loaded: Port=%s, OpenSearch=%s, Index=%s", cfg.Port, cfg.OpenSearchURL, cfg.IndexName) // Initialize OpenSearch indexer client indexClient, err := indexer.NewClient( cfg.OpenSearchURL, cfg.OpenSearchUsername, cfg.OpenSearchPassword, cfg.IndexName, ) if err != nil { log.Fatalf("Failed to create indexer client: %v", err) } // Create index if not exists ctx := context.Background() if err := indexClient.CreateIndex(ctx); err != nil { log.Printf("Warning: Could not create index (may already exist): %v", err) } // Initialize search service searchService, err := search.NewService( cfg.OpenSearchURL, cfg.OpenSearchUsername, cfg.OpenSearchPassword, cfg.IndexName, ) if err != nil { log.Fatalf("Failed to create search service: %v", err) } // Initialize seed store for admin API if err := handlers.InitSeedStore(cfg.SeedsDir); err != nil { log.Printf("Warning: Could not initialize seed store: %v", err) } // Create handler handler := handlers.NewHandler(cfg, searchService, indexClient) // Initialize PostgreSQL for Staff/Publications database dbCfg := &database.Config{ Host: cfg.DBHost, Port: cfg.DBPort, User: cfg.DBUser, Password: cfg.DBPassword, DBName: cfg.DBName, SSLMode: cfg.DBSSLMode, } db, err := database.New(ctx, dbCfg) if err != nil { log.Printf("Warning: Could not connect to PostgreSQL for staff database: %v", err) log.Println("Staff/Publications features will be disabled") } else { defer db.Close() log.Println("Connected to PostgreSQL for staff/publications database") // Run migrations if err := db.RunMigrations(ctx); err != nil { log.Printf("Warning: Could not run migrations: %v", err) } } // Create repository for Staff handlers (may be nil if DB connection failed) var repo *database.Repository if db != nil { repo = database.NewRepository(db) } // Setup Gin router gin.SetMode(gin.ReleaseMode) router := gin.New() router.Use(gin.Recovery()) router.Use(gin.Logger()) // CORS middleware router.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() }) // Setup routes handlers.SetupRoutes(router, handler, cfg.APIKey) // Setup Staff/Publications routes if database is available if repo != nil { staffHandlers := handlers.NewStaffHandlers(repo, cfg.StaffCrawlerEmail) apiV1 := router.Group("/api/v1") staffHandlers.RegisterRoutes(apiV1) log.Println("Staff/Publications API routes registered") // Setup AI Extraction routes for vast.ai integration aiHandlers := handlers.NewAIExtractionHandlers(repo) aiHandlers.RegisterRoutes(apiV1) log.Println("AI Extraction API routes registered") } // Setup Orchestrator routes if database is available if db != nil { orchRepo := orchestrator.NewPostgresRepository(db.Pool) // Create real crawlers with adapters for orchestrator interface staffCrawler := staff.NewStaffCrawler(repo) staffAdapter := staff.NewOrchestratorAdapter(staffCrawler, repo) pubAdapter := staff.NewPublicationOrchestratorAdapter(repo) orch := orchestrator.NewOrchestrator(orchRepo, staffAdapter, pubAdapter) orchHandler := handlers.NewOrchestratorHandler(orch, orchRepo) v1 := router.Group("/v1") v1.Use(handlers.AuthMiddleware(cfg.APIKey)) handlers.SetupOrchestratorRoutes(v1, orchHandler) log.Println("Orchestrator API routes registered") // Setup Audience routes (reuses orchRepo which implements AudienceRepository) audienceHandler := handlers.NewAudienceHandler(orchRepo) handlers.SetupAudienceRoutes(v1, audienceHandler) log.Println("Audience API routes registered") } // Create HTTP server srv := &http.Server{ Addr: ":" + cfg.Port, Handler: router, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } // Start server in goroutine go func() { log.Printf("Server listening on port %s", cfg.Port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } }() // Graceful shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v", err) } log.Println("Server exited") }