package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/calendarapi/internal/api" "github.com/calendarapi/internal/api/handlers" "github.com/calendarapi/internal/auth" "github.com/calendarapi/internal/config" "github.com/calendarapi/internal/middleware" "github.com/calendarapi/internal/repository" "github.com/calendarapi/internal/scheduler" "github.com/calendarapi/internal/service" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/jackc/pgx/v5/pgxpool" ) func main() { cfg := config.Load() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() pool, err := pgxpool.New(ctx, cfg.DatabaseURL) if err != nil { log.Fatalf("connect to database: %v", err) } defer pool.Close() if err := pool.Ping(ctx); err != nil { log.Fatalf("ping database: %v", err) } log.Println("connected to PostgreSQL") runMigrations(cfg.DatabaseURL) queries := repository.New(pool) jwtManager := auth.NewJWTManager(cfg.JWTSecret) auditSvc := service.NewAuditService(queries) calSvc := service.NewCalendarService(pool, queries, auditSvc, cfg.BaseURL) var sched service.ReminderScheduler if cfg.RedisAddr != "" { sched = scheduler.NewScheduler(cfg.RedisAddr) } else { sched = scheduler.NoopScheduler{} } if realSched, ok := sched.(*scheduler.Scheduler); ok { defer realSched.Close() } eventSvc := service.NewEventService(pool, queries, calSvc, auditSvc, sched) if cfg.RedisAddr != "" { realSched := sched.(*scheduler.Scheduler) icsHandler := handlers.NewICSHandler(calSvc, eventSvc, queries) subSyncWorker := scheduler.NewSubscriptionSyncWorker(icsHandler) worker := scheduler.NewReminderWorker(queries) asynqSrv := scheduler.StartWorker(cfg.RedisAddr, worker, subSyncWorker) defer asynqSrv.Shutdown() enqueuerCtx, enqueuerCancel := context.WithCancel(context.Background()) defer enqueuerCancel() go scheduler.StartSubscriptionEnqueuer(enqueuerCtx, queries, realSched, 15*time.Minute) log.Println("Redis connected, background jobs enabled") } else { log.Println("Redis not configured, background jobs disabled") } authSvc := service.NewAuthService(pool, queries, jwtManager, auditSvc) userSvc := service.NewUserService(pool, queries, auditSvc) contactSvc := service.NewContactService(queries, auditSvc) availSvc := service.NewAvailabilityService(queries, calSvc, eventSvc) bookingSvc := service.NewBookingService(pool, queries, calSvc, eventSvc) apiKeySvc := service.NewAPIKeyService(queries) reminderSvc := service.NewReminderService(queries, calSvc, sched) attendeeSvc := service.NewAttendeeService(queries, calSvc) h := api.Handlers{ Auth: handlers.NewAuthHandler(authSvc, userSvc), User: handlers.NewUserHandler(userSvc), Calendar: handlers.NewCalendarHandler(calSvc), Sharing: handlers.NewSharingHandler(calSvc), Event: handlers.NewEventHandler(eventSvc), Reminder: handlers.NewReminderHandler(reminderSvc), Attendee: handlers.NewAttendeeHandler(attendeeSvc), Contact: handlers.NewContactHandler(contactSvc), Availability: handlers.NewAvailabilityHandler(availSvc), Booking: handlers.NewBookingHandler(bookingSvc), APIKey: handlers.NewAPIKeyHandler(apiKeySvc), ICS: handlers.NewICSHandler(calSvc, eventSvc, queries), } authMW := middleware.NewAuthMiddleware(jwtManager, queries) rateLimiter := middleware.NewRateLimiter(cfg.RateLimitRPS, cfg.RateLimitBurst) router := api.NewRouter(h, authMW, rateLimiter, cfg) srv := &http.Server{ Addr: ":" + cfg.ServerPort, Handler: router, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } go func() { log.Printf("server starting on port %s (env=%s)", cfg.ServerPort, cfg.Env) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server error: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("shutting down server...") shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Fatalf("server shutdown error: %v", err) } log.Println("server stopped") } func runMigrations(databaseURL string) { m, err := migrate.New("file://migrations", databaseURL) if err != nil { log.Printf("migration init: %v (skipping)", err) return } defer m.Close() if err := m.Up(); err != nil && err != migrate.ErrNoChange { log.Fatalf("migration error: %v", err) } v, dirty, _ := m.Version() log.Printf("migrations applied (version=%d, dirty=%v)", v, dirty) }