package main import ( "context" "fmt" "net/http" "os" "os/signal" "strings" "syscall" "time" "seo-optimizer/internal/agent" "seo-optimizer/internal/api" "seo-optimizer/internal/brokenlinks" "seo-optimizer/internal/config" "seo-optimizer/internal/ideas" "seo-optimizer/internal/imageaudit" "seo-optimizer/internal/logger" "seo-optimizer/internal/newsletter" "seo-optimizer/internal/scheduler" "seo-optimizer/internal/seo" "seo-optimizer/internal/storage" "seo-optimizer/internal/translation" "seo-optimizer/internal/wordpress" ) const version = "1.0.0" func main() { // Load configuration configPath := os.Getenv("CONFIG_PATH") if configPath == "" { configPath = "configs/config.yaml" } cfg, err := config.LoadConfig(configPath) if err != nil { fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err) os.Exit(1) } // Initialize logger log := logger.NewLogger(cfg.Logging.Level, cfg.Logging.Format) log.Info("SEO Optimizer starting", "version", version, "config", configPath, ) // Initialize WordPress client wpClient := wordpress.NewClient( cfg.WordPress.BaseURL, cfg.WordPress.Username, cfg.WordPress.AppPassword, cfg.WordPress.Timeout, log, ) log.Info("WordPress client initialized", "base_url", cfg.WordPress.BaseURL, "username", cfg.WordPress.Username, ) // Initialize LLM client var llmClient agent.LLMClient switch cfg.LLM.Provider { case "claude": llmClient = agent.NewClaudeClient( cfg.Claude.APIKey, cfg.Claude.Model, cfg.Claude.MaxTokens, cfg.Claude.Temperature, log, ) log.Info("Claude client initialized", "model", cfg.Claude.Model, "max_tokens", cfg.Claude.MaxTokens, ) case "deepseek": llmClient = agent.NewDeepSeekClient( cfg.DeepSeek.APIKey, cfg.DeepSeek.BaseURL, cfg.DeepSeek.Model, cfg.DeepSeek.MaxTokens, cfg.DeepSeek.Temperature, log, ) log.Info("DeepSeek client initialized", "model", cfg.DeepSeek.Model, "max_tokens", cfg.DeepSeek.MaxTokens, "base_url", cfg.DeepSeek.BaseURL, ) default: log.Error("Unsupported LLM provider", "provider", cfg.LLM.Provider) os.Exit(1) } // Initialize DeepSeek client for translations (optional) var translationClient *agent.DeepSeekClient if strings.TrimSpace(cfg.DeepSeek.APIKey) != "" { translationClient = agent.NewDeepSeekClient( cfg.DeepSeek.APIKey, cfg.DeepSeek.BaseURL, cfg.DeepSeek.Model, cfg.DeepSeek.MaxTokens, cfg.DeepSeek.Temperature, log, ) log.Info("DeepSeek translation client initialized", "model", cfg.DeepSeek.Model) } else { log.Warn("DeepSeek API key missing; translation feature will be unavailable") } // Initialize storage store := storage.NewOptimizationStorage(cfg.Storage.BasePath, log) if err := store.Initialize(); err != nil { log.Error("Failed to initialize storage", "error", err) os.Exit(1) } log.Info("Storage initialized", "base_path", cfg.Storage.BasePath) // Initialize audit storage auditStore := storage.NewAuditStorage(cfg.Audit.BasePath, log) if err := auditStore.Initialize(); err != nil { log.Error("Failed to initialize audit storage", "error", err) os.Exit(1) } log.Info("Audit storage initialized", "base_path", cfg.Audit.BasePath) // Initialize idea storage ideaStore := storage.NewIdeaStorage(cfg.Ideas.BasePath, log) if err := ideaStore.Initialize(); err != nil { log.Error("Failed to initialize idea storage", "error", err) os.Exit(1) } log.Info("Idea storage initialized", "base_path", cfg.Ideas.BasePath) // Initialize outreach storage outreachStore := storage.NewOutreachStorage(cfg.Outreach.BasePath, log) if err := outreachStore.Initialize(); err != nil { log.Error("Failed to initialize outreach storage", "error", err) os.Exit(1) } log.Info("Outreach storage initialized", "base_path", cfg.Outreach.BasePath) // Initialize newsletter storage newsStore := storage.NewNewsletterStorage(cfg.Newsletter.BasePath, log) if err := newsStore.Initialize(); err != nil { log.Error("Failed to initialize newsletter storage", "error", err) os.Exit(1) } log.Info("Newsletter storage initialized", "base_path", cfg.Newsletter.BasePath) // Initialize broken link storage linkStore := storage.NewBrokenLinkStorage(cfg.LinkChecker.BasePath, log) if err := linkStore.Initialize(); err != nil { log.Error("Failed to initialize broken link storage", "error", err) os.Exit(1) } log.Info("Broken link storage initialized", "base_path", cfg.LinkChecker.BasePath) // Initialize image audit storage imageAuditStore := storage.NewImageAuditStorage(cfg.ImageAudit.BasePath, log) if err := imageAuditStore.Initialize(); err != nil { log.Error("Failed to initialize image audit storage", "error", err) os.Exit(1) } log.Info("Image audit storage initialized", "base_path", cfg.ImageAudit.BasePath) // Initialize idea service ideaService := ideas.NewService(wpClient, llmClient, nil, ideaStore, cfg.Ideas, outreachStore, cfg.Outreach, cfg.Runtime.DryRun, log) // Initialize newsletter service newsService := newsletter.NewService(llmClient, nil, newsStore, cfg.Newsletter, log) // Initialize broken link service linkService, err := brokenlinks.NewService(wpClient, linkStore, cfg.LinkChecker, log) if err != nil { log.Error("Failed to initialize broken link service", "error", err) os.Exit(1) } imageAuditService, err := imageaudit.NewService(wpClient, imageAuditStore, cfg.ImageAudit, log) if err != nil { log.Error("Failed to initialize image audit service", "error", err) os.Exit(1) } translationService := translation.NewService(wpClient, translationClient, log) // Initialize optimizer optimizer := seo.NewOptimizer(wpClient, llmClient, cfg.LLM.Provider, store, cfg, log) log.Info("Optimizer initialized") // Context with cancellation for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start API server if enabled var apiServer *api.Server if cfg.API.Enabled { apiServer = api.NewServer(optimizer, wpClient, store, auditStore, ideaService, newsService, linkService, imageAuditService, translationService, version, cfg.API.Port, log) // Start server in goroutine go func() { if err := apiServer.Start(); err != nil && err != http.ErrServerClosed { log.Error("API server failed", "error", err) } }() // Start periodic job cleanup go apiServer.StartPeriodicCleanup(ctx, 1*time.Hour, 24*time.Hour) log.Info("API server started", "port", cfg.API.Port) } // Start scheduler if enabled var sched *scheduler.Scheduler if cfg.Scheduler.Enabled { sched = scheduler.NewScheduler( optimizer, cfg.Scheduler.IntervalHours, cfg.Scheduler.Languages, log, ) go func() { if err := sched.Start(ctx); err != nil && err != context.Canceled { log.Error("Scheduler failed", "error", err) } }() log.Info("Scheduler started", "interval", fmt.Sprintf("%.1fh", cfg.Scheduler.IntervalHours), "languages", cfg.Scheduler.Languages, ) } // Start idea scheduler if enabled var ideaSched *scheduler.IdeaScheduler if cfg.Ideas.Enabled || cfg.Outreach.Enabled { ideaSched = scheduler.NewIdeaScheduler(ideaService, cfg.Ideas.IntervalHours, cfg.Ideas.Enabled, cfg.Outreach.Enabled, auditStore, log) go func() { if err := ideaSched.Start(ctx); err != nil && err != context.Canceled { log.Error("Idea scheduler failed", "error", err) } }() log.Info("Idea scheduler started", "interval", cfg.Ideas.IntervalHours) } // Start newsletter scheduler if enabled var newsSched *scheduler.NewsletterScheduler if cfg.Newsletter.Enabled { newsSched = scheduler.NewNewsletterScheduler(newsService, cfg.Newsletter.IntervalHours, auditStore, log) go func() { if err := newsSched.Start(ctx); err != nil && err != context.Canceled { log.Error("Newsletter scheduler failed", "error", err) } }() log.Info("Newsletter scheduler started", "interval", cfg.Newsletter.IntervalHours) } // Start broken link scheduler if enabled var linkSched *scheduler.BrokenLinkScheduler if cfg.LinkChecker.Enabled { linkSched = scheduler.NewBrokenLinkScheduler(linkService, cfg.LinkChecker.IntervalHours, auditStore, log) go func() { if err := linkSched.Start(ctx); err != nil && err != context.Canceled { log.Error("Broken link scheduler failed", "error", err) } }() log.Info("Broken link scheduler started", "interval", cfg.LinkChecker.IntervalHours) } // Start image audit scheduler if enabled var imageAuditSched *scheduler.ImageAuditScheduler if cfg.ImageAudit.Enabled { imageAuditSched = scheduler.NewImageAuditScheduler(imageAuditService, cfg.ImageAudit.IntervalHours, auditStore, log) go func() { if err := imageAuditSched.Start(ctx); err != nil && err != context.Canceled { log.Error("Image audit scheduler failed", "error", err) } }() log.Info("Image audit scheduler started", "interval", cfg.ImageAudit.IntervalHours) } // Wait for interrupt signal sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) sig := <-sigCh log.Info("Shutdown signal received", "signal", sig.String()) // Graceful shutdown log.Info("Initiating graceful shutdown...") // Cancel context to stop scheduler cancel() // Shutdown API server if running if apiServer != nil { shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() if err := apiServer.Shutdown(shutdownCtx); err != nil { log.Error("API server shutdown error", "error", err) } else { log.Info("API server shut down successfully") } } // Stop scheduler if sched != nil { sched.Stop() log.Info("Scheduler stopped") } if ideaSched != nil { ideaSched.Stop() log.Info("Idea scheduler stopped") } if newsSched != nil { newsSched.Stop() log.Info("Newsletter scheduler stopped") } if linkSched != nil { linkSched.Stop() log.Info("Broken link scheduler stopped") } if imageAuditSched != nil { imageAuditSched.Stop() log.Info("Image audit scheduler stopped") } log.Info("SEO Optimizer stopped gracefully") }