package main

import (
	"context"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	aws "github.com/aws/aws-sdk-go-v2/aws"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/config"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/handlers"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/middleware"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/monitoring"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/realtime"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/repository"
	"gitlab.com/patrickbritton3/sojorn/go-backend/internal/services"
)

func main() {
	cfg := config.LoadConfig()

	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})

	if cfg.DatabaseURL == "" {
		log.Fatal().Msg("DATABASE_URL is not set")
	}

	pgxConfig, err := pgxpool.ParseConfig(cfg.DatabaseURL)
	if err != nil {
		log.Fatal().Err(err).Msg("Unable to parse database config")
	}

	dbPool, err := pgxpool.NewWithConfig(context.Background(), pgxConfig)
	if err != nil {
		log.Fatal().Err(err).Msg("Unable to connect to database")
	}
	defer dbPool.Close()

	if err := dbPool.Ping(context.Background()); err != nil {
		log.Fatal().Err(err).Msg("Unable to ping database")
	}

	r := gin.Default()

	allowedOrigins := strings.Split(cfg.CORSOrigins, ",")
	allowAllOrigins := false
	allowedOriginSet := make(map[string]struct{}, len(allowedOrigins))
	for _, origin := range allowedOrigins {
		trimmed := strings.TrimSpace(origin)
		if trimmed == "" {
			continue
		}
		if trimmed == "*" {
			allowAllOrigins = true
			break
		}
		allowedOriginSet[trimmed] = struct{}{}
	}

	r.Use(cors.New(cors.Config{
		AllowOriginFunc: func(origin string) bool {
			log.Debug().Msgf("CORS origin: %s", origin)
			if allowAllOrigins {
				return true
			}
			if strings.HasPrefix(origin, "http://localhost") ||
				strings.HasPrefix(origin, "https://localhost") ||
				strings.HasPrefix(origin, "http://127.0.0.1") ||
				strings.HasPrefix(origin, "https://127.0.0.1") {
				return true
			}
			_, ok := allowedOriginSet[origin]
			return ok
		},
		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
		AllowHeaders:     []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID", "X-Timestamp", "X-Signature", "X-Algorithm"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
		MaxAge:           12 * time.Hour,
	}))

	r.NoRoute(func(c *gin.Context) {
		log.Debug().Msgf("No route found for %s %s", c.Request.Method, c.Request.URL.Path)
		c.JSON(404, gin.H{"error": "route not found", "path": c.Request.URL.Path, "method": c.Request.Method})
	})

	userRepo := repository.NewUserRepository(dbPool)
	postRepo := repository.NewPostRepository(dbPool)
	chatRepo := repository.NewChatRepository(dbPool)
	categoryRepo := repository.NewCategoryRepository(dbPool)
	notifRepo := repository.NewNotificationRepository(dbPool)
	tagRepo := repository.NewTagRepository(dbPool)

	assetService := services.NewAssetService(cfg.R2SigningSecret, cfg.R2PublicBaseURL, cfg.R2ImgDomain, cfg.R2VidDomain)
	feedService := services.NewFeedService(postRepo, assetService)

	pushService, err := services.NewPushService(userRepo, cfg.FirebaseCredentialsFile)
	if err != nil {
		log.Warn().Err(err).Msg("Failed to initialize PushService")
	}

	notificationService := services.NewNotificationService(notifRepo, pushService, userRepo)

	emailService := services.NewEmailService(cfg, dbPool)
	sendPulseService := services.NewSendPulseService(cfg.SendPulseID, cfg.SendPulseSecret)

	// Load moderation configuration
	moderationConfig := config.NewModerationConfig()
	moderationService := services.NewModerationService(dbPool, moderationConfig.OpenAIKey, moderationConfig.GoogleKey, moderationConfig.GoogleCredsFile)

	// Initialize appeal service
	appealService := services.NewAppealService(dbPool)

	// Initialize OpenRouter service
	openRouterService := services.NewOpenRouterService(dbPool, cfg.OpenRouterAPIKey)

	// Initialize Azure OpenAI service
	var azureOpenAIService *services.AzureOpenAIService
	if cfg.AzureOpenAIAPIKey != "" && cfg.AzureOpenAIEndpoint != "" {
		azureOpenAIService = services.NewAzureOpenAIService(dbPool, cfg.AzureOpenAIAPIKey, cfg.AzureOpenAIEndpoint, cfg.AzureOpenAIAPIVersion)
		log.Info().Msg("Azure OpenAI service initialized")
	} else {
		log.Warn().Msg("Azure OpenAI credentials not provided, Azure OpenAI service disabled")
	}

	// Initialize content filter (hard blocklist + strike system)
	contentFilter := services.NewContentFilter(dbPool)

	// Initialize local AI gateway service (on-server Ollama via localhost:8099)
	localAIService := services.NewLocalAIService(cfg.AIGatewayURL, cfg.AIGatewayToken)
	if localAIService != nil {
		log.Info().Str("url", cfg.AIGatewayURL).Msg("Local AI gateway configured")
	} else {
		log.Info().Msg("Local AI gateway not configured (AI_GATEWAY_URL not set)")
	}

	hub := realtime.NewHub()
	wsHandler := handlers.NewWSHandler(hub, cfg.JWTSecret)

	var s3Client *s3.Client
	if cfg.R2AccessKey != "" && cfg.R2SecretKey != "" && cfg.R2Endpoint != "" {
		resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
			return aws.Endpoint{URL: cfg.R2Endpoint, PartitionID: "aws", SigningRegion: "auto"}, nil
		})
		awsCfg, err := awsconfig.LoadDefaultConfig(
			context.Background(),
			awsconfig.WithRegion("auto"),
			awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.R2AccessKey, cfg.R2SecretKey, "")),
			awsconfig.WithEndpointResolverWithOptions(resolver),
		)
		if err != nil {
			log.Warn().Err(err).Msg("Failed to load AWS/R2 config, falling back to R2 API token flow")
		} else {
			s3Client = s3.NewFromConfig(awsCfg)
		}
	}

	// Initialize link preview service (after S3 client setup)
	linkPreviewService := services.NewLinkPreviewService(dbPool, s3Client, cfg.R2MediaBucket, cfg.R2ImgDomain)

	userHandler := handlers.NewUserHandler(userRepo, postRepo, notificationService, assetService)
	postHandler := handlers.NewPostHandler(postRepo, userRepo, feedService, assetService, notificationService, moderationService, contentFilter, openRouterService, linkPreviewService, localAIService, s3Client, cfg.R2VideoBucket, cfg.R2VidDomain)
	chatHandler := handlers.NewChatHandler(chatRepo, notificationService, hub)
	authHandler := handlers.NewAuthHandler(userRepo, cfg, emailService, sendPulseService)
	categoryHandler := handlers.NewCategoryHandler(categoryRepo)
	keyHandler := handlers.NewKeyHandler(userRepo)
	backupHandler := handlers.NewBackupHandler(repository.NewBackupRepository(dbPool))
	settingsHandler := handlers.NewSettingsHandler(userRepo, notifRepo)
	analysisHandler := handlers.NewAnalysisHandler()
	appealHandler := handlers.NewAppealHandler(appealService)

	// Initialize official accounts service
	officialAccountsService := services.NewOfficialAccountsService(dbPool, openRouterService, localAIService, linkPreviewService, moderationConfig.OpenAIKey)
	officialAccountsService.StartScheduler()
	defer officialAccountsService.StopScheduler()

	moderationHandler := handlers.NewModerationHandler(moderationService, openRouterService, localAIService)

	adminHandler := handlers.NewAdminHandler(dbPool, moderationService, appealService, emailService, openRouterService, azureOpenAIService, officialAccountsService, linkPreviewService, localAIService, cfg.JWTSecret, s3Client, cfg.R2MediaBucket, cfg.R2VideoBucket, cfg.R2ImgDomain, cfg.R2VidDomain)

	accountHandler := handlers.NewAccountHandler(userRepo, emailService, cfg)

	// Capsule system handlers (E2EE groups)
	capsuleHandler := handlers.NewCapsuleHandler(dbPool)
	capsuleEscrowHandler := handlers.NewCapsuleEscrowHandler(dbPool)

	// Group feature handler (posts, chat, forum, members)
	groupHandler := handlers.NewGroupHandler(dbPool)

	// Neighborhood board handler (standalone message board)
	boardHandler := handlers.NewBoardHandler(dbPool, contentFilter, moderationService)

	// Beacon search handler (search beacons, board, public groups)
	beaconSearchHandler := handlers.NewBeaconSearchHandler(dbPool)

	mediaHandler := handlers.NewMediaHandler(
		s3Client,
		cfg.R2AccountID,
		cfg.R2APIToken,
		cfg.R2MediaBucket,
		cfg.R2VideoBucket,
		cfg.R2ImgDomain,
		cfg.R2VidDomain,
	)

	// Feed algorithm service (scores posts for ranked feed)
	feedAlgorithmService := services.NewFeedAlgorithmService(dbPool)

	// Health check service
	hcService := monitoring.NewHealthCheckService(dbPool)

	// Repost & profile layout handlers
	repostHandler := handlers.NewRepostHandler(dbPool)
	profileLayoutHandler := handlers.NewProfileLayoutHandler(dbPool)

	// Audio library proxy (Funkwhale — gracefully returns 503 until FUNKWHALE_BASE is set)
	audioHandler := handlers.NewAudioHandler(cfg.FunkwhaleBase)

	r.GET("/ws", wsHandler.ServeWS)

	r.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{"status": "ok"})
	})
	r.HEAD("/health", func(c *gin.Context) {
		c.Status(200)
	})
	r.GET("/health/detailed", gin.WrapF(hcService.HealthCheckHandler))
	r.GET("/health/ready", gin.WrapF(hcService.ReadinessHandler))
	r.GET("/health/live", gin.WrapF(hcService.LivenessHandler))

	// ALTCHA challenge endpoints (direct to main router for testing)
	r.GET("/api/v1/auth/altcha-challenge", authHandler.GetAltchaChallenge)
	r.GET("/api/v1/admin/altcha-challenge", adminHandler.GetAltchaChallenge)
	r.GET("/api/v1/test", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "Route test successful"})
	})

	v1 := r.Group("/api/v1")
	{
		// Public waitlist signup (no auth required)
		waitlist := v1.Group("/waitlist")
		waitlist.Use(middleware.RateLimit(0.2, 3))
		{
			waitlist.POST("", func(c *gin.Context) {
				var req struct {
					Email string `json:"email" binding:"required,email"`
				}
				if err := c.ShouldBindJSON(&req); err != nil {
					c.JSON(400, gin.H{"error": "Valid email required"})
					return
				}
				// Store locally
				_, err := dbPool.Exec(c.Request.Context(),
					`INSERT INTO public.waitlist (email) VALUES ($1) ON CONFLICT (email) DO NOTHING`, req.Email)
				if err != nil {
					c.JSON(500, gin.H{"error": "Failed to join waitlist"})
					return
				}
				// Add to SendPulse waitlist in background
				go sendPulseService.AddToWaitlist(req.Email)
				c.JSON(200, gin.H{"message": "You're on the list!"})
			})
		}

		auth := v1.Group("/auth")
		{
			auth.POST("/register", authHandler.Register)
			auth.POST("/signup", authHandler.Register)
			auth.POST("/login", authHandler.Login)
			auth.POST("/refresh", authHandler.RefreshSession)
			auth.POST("/resend-verification", authHandler.ResendVerificationEmail)
			auth.GET("/verify", authHandler.VerifyEmail)
			auth.POST("/forgot-password", authHandler.ForgotPassword)
			auth.POST("/reset-password", authHandler.ResetPassword)
		}

		authorized := v1.Group("")
		authorized.Use(middleware.AuthMiddleware(cfg.JWTSecret, dbPool))
		{
			authorized.GET("/profiles/:id", userHandler.GetProfile)
			authorized.GET("/profile", userHandler.GetProfile)
			authorized.PATCH("/profile", userHandler.UpdateProfile)
			authorized.POST("/complete-onboarding", authHandler.CompleteOnboarding)
			authorized.GET("/profile/trust-state", userHandler.GetTrustState)

			settings := authorized.Group("/settings")
			{
				settings.GET("/privacy", settingsHandler.GetPrivacySettings)
				settings.PATCH("/privacy", settingsHandler.UpdatePrivacySettings)
				settings.GET("/user", settingsHandler.GetUserSettings)
				settings.PATCH("/user", settingsHandler.UpdateUserSettings)
			}

			users := authorized.Group("/users")
			{
				users.POST("/:id/follow", userHandler.Follow)
				users.DELETE("/:id/follow", userHandler.Unfollow)
				users.POST("/:id/accept", userHandler.AcceptFollowRequest)
				users.DELETE("/:id/reject", userHandler.RejectFollowRequest)
				users.GET("/requests", userHandler.GetPendingFollowRequests)
				users.POST("/requests", userHandler.GetPendingFollowRequests)
				users.GET("/:id/posts", postHandler.GetProfilePosts)
				users.GET("/:id/saved", userHandler.GetSavedPosts)
				users.GET("/me/liked", userHandler.GetLikedPosts)
				users.POST("/:id/block", userHandler.BlockUser)
				users.DELETE("/:id/block", userHandler.UnblockUser)
				users.GET("/blocked", userHandler.GetBlockedUsers)
				users.POST("/report", userHandler.ReportUser)
				users.POST("/block_by_handle", userHandler.BlockUserByHandle)
				users.POST("/me/blocks/bulk", userHandler.BulkBlockUsers)

				// Social Graph: Followers & Following
				users.GET("/:id/followers", userHandler.GetFollowers)
				users.GET("/:id/following", userHandler.GetFollowing)

				// Circle Management
				users.POST("/circle/:id", userHandler.AddToCircle)
				users.DELETE("/circle/:id", userHandler.RemoveFromCircle)
				users.GET("/circle/members", userHandler.GetCircleMembers)

				// Data Export
				users.GET("/me/export", userHandler.ExportData)

			}

			authorized.POST("/posts", postHandler.CreatePost)
			authorized.GET("/posts/:id", postHandler.GetPost)
			authorized.GET("/posts/:id/chain", postHandler.GetPostChain)
			authorized.GET("/posts/:id/thread", postHandler.GetPostChain)
			authorized.GET("/posts/:id/focus-context", postHandler.GetPostFocusContext)
			authorized.PATCH("/posts/:id", postHandler.UpdatePost)
			authorized.DELETE("/posts/:id", postHandler.DeletePost)
			authorized.POST("/posts/:id/pin", postHandler.PinPost)
			authorized.PATCH("/posts/:id/visibility", postHandler.UpdateVisibility)
			authorized.POST("/posts/:id/hide", postHandler.HidePost)
			authorized.POST("/posts/:id/like", postHandler.LikePost)
			authorized.DELETE("/posts/:id/like", postHandler.UnlikePost)
			authorized.POST("/posts/:id/save", postHandler.SavePost)
			authorized.DELETE("/posts/:id/save", postHandler.UnsavePost)
			authorized.POST("/posts/:id/reactions/toggle", postHandler.ToggleReaction)
			authorized.POST("/posts/:id/comments", postHandler.CreateComment)
			authorized.GET("/feed", postHandler.GetFeed)
			authorized.GET("/feed/personal", postHandler.GetFeed)
			authorized.POST("/beacons", postHandler.CreateBeacon)
			authorized.GET("/beacons/nearby", postHandler.GetNearbyBeacons)
			authorized.POST("/beacons/:id/vouch", postHandler.VouchBeacon)
			authorized.POST("/beacons/:id/report", postHandler.ReportBeacon)
			authorized.DELETE("/beacons/:id/vouch", postHandler.RemoveBeaconVote)
			authorized.GET("/categories", categoryHandler.GetCategories)
			authorized.POST("/categories/settings", categoryHandler.SetUserCategorySettings)
			authorized.GET("/categories/settings", categoryHandler.GetUserCategorySettings)
			authorized.POST("/analysis/tone", analysisHandler.CheckTone)
			authorized.POST("/moderate", moderationHandler.CheckContent)

			// Chat routes
			authorized.GET("/conversations", chatHandler.GetConversations)
			authorized.GET("/conversation", chatHandler.GetOrCreateConversation)
			authorized.POST("/messages", chatHandler.SendMessage)
			authorized.GET("/conversations/:id/messages", chatHandler.GetMessages)
			authorized.DELETE("/conversations/:id", chatHandler.DeleteConversation)
			authorized.DELETE("/messages/:id", chatHandler.DeleteMessage)
			authorized.GET("/mutual-follows", chatHandler.GetMutualFollows)

			// Key routes
			authorized.POST("/keys", keyHandler.PublishKeys)
			authorized.GET("/keys/:id", keyHandler.GetKeyBundle)
			authorized.DELETE("/keys/otk/:keyId", keyHandler.DeleteUsedOTK)

			backupGroup := authorized.Group("/backup")
			{
				backupGroup.POST("/sync/generate-code", backupHandler.GenerateSyncCode)
				backupGroup.POST("/sync/verify-code", backupHandler.VerifySyncCode)
				backupGroup.POST("/upload", backupHandler.UploadBackup)
				backupGroup.GET("/download", backupHandler.DownloadBackup)
				backupGroup.GET("/download/:backupId", backupHandler.DownloadBackup)
				backupGroup.GET("/list", backupHandler.ListBackups)
				backupGroup.DELETE("/:backupId", backupHandler.DeleteBackup)
				backupGroup.GET("/preferences", backupHandler.GetBackupPreferences)
				backupGroup.PUT("/preferences", backupHandler.UpdateBackupPreferences)
			}

			recoveryGroup := authorized.Group("/recovery")
			{
				recoveryGroup.POST("/social/setup", backupHandler.SetupSocialRecovery)
				recoveryGroup.POST("/initiate", backupHandler.InitiateRecovery)
				recoveryGroup.POST("/submit-shard", backupHandler.SubmitShard)
				recoveryGroup.POST("/complete/:sessionId", backupHandler.CompleteRecovery)
			}

			// Device management routes
			authorized.GET("/devices", backupHandler.GetUserDevices)

			// Media routes
			authorized.POST("/upload", mediaHandler.Upload)
			authorized.GET("/media/sign", mediaHandler.GetSignedMediaURL)

			// Search & Discover routes
			discoverHandler := handlers.NewDiscoverHandler(userRepo, postRepo, tagRepo, categoryRepo, assetService)
			authorized.GET("/search", discoverHandler.Search)
			authorized.GET("/discover", discoverHandler.GetDiscover)
			authorized.GET("/hashtags/trending", discoverHandler.GetTrendingHashtags)
			authorized.GET("/hashtags/following", discoverHandler.GetFollowedHashtags)
			authorized.GET("/hashtags/:name", discoverHandler.GetHashtagPage)
			authorized.POST("/hashtags/:name/follow", discoverHandler.FollowHashtag)
			authorized.DELETE("/hashtags/:name/follow", discoverHandler.UnfollowHashtag)

			// User by-handle lookup (used by capsule invite to resolve public keys)
			authorized.GET("/users/by-handle/:handle", userHandler.GetUserByHandle)

			// Follow System (unique routes only — followers/following covered by users group above)
			followHandler := handlers.NewFollowHandler(dbPool)
			authorized.POST("/users/:id/unfollow", followHandler.UnfollowUser)
			authorized.GET("/users/:id/is-following", followHandler.IsFollowing)
			authorized.GET("/users/:id/mutual-followers", followHandler.GetMutualFollowers)
			authorized.GET("/users/suggested", followHandler.GetSuggestedUsers)

			// Notifications
			notificationHandler := handlers.NewNotificationHandler(notifRepo, notificationService)
			authorized.GET("/notifications", notificationHandler.GetNotifications)
			authorized.GET("/notifications/unread", notificationHandler.GetUnreadCount)
			authorized.GET("/notifications/badge", notificationHandler.GetBadgeCount)
			authorized.PUT("/notifications/:id/read", notificationHandler.MarkAsRead)
			authorized.POST("/notifications/read", notificationHandler.BulkMarkAsRead)
			authorized.PUT("/notifications/read-all", notificationHandler.MarkAllAsRead)
			authorized.POST("/notifications/archive", notificationHandler.Archive)
			authorized.POST("/notifications/archive-all", notificationHandler.ArchiveAll)
			authorized.DELETE("/notifications/:id", notificationHandler.DeleteNotification)
			authorized.GET("/notifications/preferences", notificationHandler.GetNotificationPreferences)
			authorized.PUT("/notifications/preferences", notificationHandler.UpdateNotificationPreferences)
			authorized.POST("/notifications/device", notificationHandler.RegisterDevice)
			authorized.DELETE("/notifications/device", notificationHandler.UnregisterDevice)
			authorized.DELETE("/notifications/devices", notificationHandler.UnregisterAllDevices)

			// Safe domains (for external link warnings in app)
			authorized.GET("/safe-domains", postHandler.GetSafeDomains)
			authorized.GET("/safe-domains/check", postHandler.CheckURLSafety)

			// Account Lifecycle routes
			account := authorized.Group("/account")
			{
				account.GET("/status", accountHandler.GetAccountStatus)
				account.POST("/deactivate", accountHandler.DeactivateAccount)
				account.DELETE("", accountHandler.DeleteAccount)
				account.POST("/cancel-deletion", accountHandler.CancelDeletion)
				account.POST("/destroy", accountHandler.RequestImmediateDestroy)
			}

			// Appeal System routes
			appeals := authorized.Group("/appeals")
			{
				appeals.GET("", appealHandler.GetUserViolations)
				appeals.GET("/summary", appealHandler.GetUserViolationSummary)
				appeals.POST("", appealHandler.CreateAppeal)
				appeals.GET("/:id", appealHandler.GetAppeal)
			}

			// Neighborhood board (standalone message board — NOT posts)
			board := authorized.Group("/board")
			{
				board.GET("/nearby", boardHandler.ListNearby)
				board.POST("", boardHandler.CreateEntry)
				board.GET("/:id", boardHandler.GetEntry)
				board.POST("/:id/replies", boardHandler.CreateReply)
				board.POST("/vote", boardHandler.ToggleVote)
				board.POST("/:id/remove", boardHandler.RemoveEntry)
				board.POST("/:id/flag", boardHandler.FlagEntry)
			}

			// Beacon ecosystem search (beacons, board entries, public groups — never private)
			authorized.GET("/beacon/search", beaconSearchHandler.Search)

			// Neighborhood system (on-demand OSM detection + auto-join)
			neighborhoodHandler := handlers.NewNeighborhoodHandler(dbPool)
			neighborhoods := authorized.Group("/neighborhoods")
			{
				neighborhoods.GET("/detect", neighborhoodHandler.Detect)
				neighborhoods.GET("/current", neighborhoodHandler.GetCurrent)
				neighborhoods.GET("/search", neighborhoodHandler.SearchByZip)
				neighborhoods.POST("/choose", neighborhoodHandler.Choose)
				neighborhoods.GET("/mine", neighborhoodHandler.GetMyNeighborhood)
			}

			// Groups system (community groups with discovery and membership)
			groupsHandler := handlers.NewGroupsHandler(dbPool)
			groups := authorized.Group("/groups")
			{
				groups.GET("", groupsHandler.ListGroups)                                          // List all groups with optional category filter
				groups.GET("/mine", groupsHandler.GetMyGroups)                                    // Get user's joined groups
				groups.GET("/suggested", groupsHandler.GetSuggestedGroups)                        // Get suggested groups
				groups.POST("", groupsHandler.CreateGroup)                                        // Create new group
				groups.GET("/:id", groupsHandler.GetGroup)                                        // Get group details
				groups.POST("/:id/join", groupsHandler.JoinGroup)                                 // Join group or request to join
				groups.POST("/:id/leave", groupsHandler.LeaveGroup)                               // Leave group
				groups.GET("/:id/members", groupsHandler.GetGroupMembers)                         // Get group members
				groups.GET("/:id/requests", groupsHandler.GetPendingRequests)                     // Get pending join requests (admin)
				groups.POST("/:id/requests/:requestId/approve", groupsHandler.ApproveJoinRequest) // Approve join request
				groups.POST("/:id/requests/:requestId/reject", groupsHandler.RejectJoinRequest)   // Reject join request
				groups.GET("/:id/feed", groupsHandler.GetGroupFeed)
				groups.GET("/:id/key-status", groupsHandler.GetGroupKeyStatus)
				groups.POST("/:id/keys", groupsHandler.DistributeGroupKeys)
				groups.GET("/:id/members/public-keys", groupsHandler.GetGroupMemberPublicKeys)
				groups.POST("/:id/invite-member", groupsHandler.InviteMember)
				groups.DELETE("/:id/members/:userId", groupsHandler.RemoveMember)
				groups.PATCH("/:id/settings", groupsHandler.UpdateGroupSettings)
			}

			// Capsule system (E2EE groups + clusters)
			capsules := authorized.Group("/capsules")
			{
				capsules.GET("/mine", capsuleHandler.ListMyGroups)
				capsules.GET("/public", capsuleHandler.ListPublicClusters)
				capsules.GET("/discover", capsuleHandler.DiscoverGroups)
				capsules.POST("", capsuleHandler.CreateCapsule)
				capsules.POST("/group", capsuleHandler.CreateGroup)
				capsules.POST("/:id/join", capsuleHandler.JoinGroup)
				capsules.GET("/:id", capsuleHandler.GetCapsule)
				capsules.POST("/:id/entries", capsuleHandler.PostCapsuleEntry)
				capsules.GET("/:id/entries", capsuleHandler.GetCapsuleEntries)
				capsules.POST("/:id/invite", capsuleHandler.InviteToCapsule)
				capsules.POST("/:id/rotate-keys", capsuleHandler.RotateKeys)

				// Group features (posts, chat, forum, members)
				capsules.GET("/:id/posts", groupHandler.ListGroupPosts)
				capsules.POST("/:id/posts", groupHandler.CreateGroupPost)
				capsules.POST("/:id/posts/:postId/like", groupHandler.ToggleGroupPostLike)
				capsules.GET("/:id/posts/:postId/comments", groupHandler.ListGroupPostComments)
				capsules.POST("/:id/posts/:postId/comments", groupHandler.CreateGroupPostComment)
				capsules.GET("/:id/messages", groupHandler.ListGroupMessages)
				capsules.POST("/:id/messages", groupHandler.SendGroupMessage)
				capsules.GET("/:id/threads", groupHandler.ListGroupThreads)
				capsules.POST("/:id/threads", groupHandler.CreateGroupThread)
				capsules.GET("/:id/threads/:threadId", groupHandler.GetGroupThread)
				capsules.POST("/:id/threads/:threadId/replies", groupHandler.CreateGroupThreadReply)
				capsules.GET("/:id/members", groupHandler.ListGroupMembers)
				capsules.DELETE("/:id/members/:memberId", groupHandler.RemoveGroupMember)
				capsules.PATCH("/:id/members/:memberId", groupHandler.UpdateMemberRole)
				capsules.POST("/:id/leave", groupHandler.LeaveGroup)
				capsules.PATCH("/:id", groupHandler.UpdateGroup)
				capsules.DELETE("/:id", groupHandler.DeleteGroup)
				capsules.POST("/:id/invite-member", groupHandler.InviteToGroup)
				capsules.GET("/:id/search-users", groupHandler.SearchUsersForInvite)
			}

			// Capsule key management (per-user encrypted key store)
			capsuleKeys := authorized.Group("/capsule-keys")
			{
				capsuleKeys.GET("", capsuleEscrowHandler.GetMyKeys)
				capsuleKeys.POST("", capsuleEscrowHandler.StoreKey)
				capsuleKeys.GET("/:id", capsuleEscrowHandler.GetMyKeyForGroup)
				capsuleKeys.DELETE("/:id", capsuleEscrowHandler.DeleteKey)
			}

			// Capsule escrow backup (PIN-encrypted private key recovery)
			escrow := authorized.Group("/capsule/escrow")
			{
				escrow.GET("/status", capsuleEscrowHandler.GetBackupStatus)
				escrow.POST("/backup", capsuleEscrowHandler.UploadBackup)
				escrow.GET("/backup", capsuleEscrowHandler.GetBackup)
				escrow.DELETE("/backup", capsuleEscrowHandler.DeleteBackup)
			}

			// Repost & amplification system
			authorized.POST("/posts/repost", repostHandler.CreateRepost)
			authorized.POST("/posts/boost", repostHandler.BoostPost)
			authorized.GET("/posts/trending", repostHandler.GetTrendingPosts)
			authorized.GET("/posts/:id/reposts", repostHandler.GetRepostsForPost)
			authorized.GET("/posts/:id/amplification", repostHandler.GetAmplificationAnalytics)
			authorized.POST("/posts/:id/calculate-score", repostHandler.CalculateAmplificationScore)
			authorized.DELETE("/reposts/:id", repostHandler.DeleteRepost)
			authorized.POST("/reposts/:id/report", repostHandler.ReportRepost)
			authorized.GET("/amplification/rules", repostHandler.GetAmplificationRules)
			authorized.GET("/users/:id/reposts", repostHandler.GetUserReposts)
			authorized.GET("/users/:id/can-boost/:postId", repostHandler.CanBoostPost)
			authorized.GET("/users/:id/daily-boosts", repostHandler.GetDailyBoostCount)

			// Profile widget layout
			authorized.GET("/profile/layout", profileLayoutHandler.GetProfileLayout)
			authorized.PUT("/profile/layout", profileLayoutHandler.SaveProfileLayout)

			// Audio library (Funkwhale proxy — returns 503 until FUNKWHALE_BASE is set in env)
			authorized.GET("/audio/library", audioHandler.SearchAudioLibrary)
			authorized.GET("/audio/library/:trackId/listen", audioHandler.GetAudioTrackListen)

		}
	}

	// Admin login (no auth middleware - this IS the auth step)
	r.POST("/api/v1/admin/login", adminHandler.AdminLogin)

	// ──────────────────────────────────────────────
	// Admin Panel API (requires auth + admin role)
	// ──────────────────────────────────────────────
	admin := r.Group("/api/v1/admin")
	admin.Use(middleware.AuthMiddleware(cfg.JWTSecret, dbPool))
	admin.Use(middleware.AdminMiddleware(dbPool))
	{
		// Dashboard
		admin.GET("/dashboard", adminHandler.GetDashboardStats)
		admin.GET("/growth", adminHandler.GetGrowthStats)

		// User Management
		admin.GET("/users", adminHandler.ListUsers)
		admin.GET("/users/:id", adminHandler.GetUser)
		admin.PATCH("/users/:id/status", adminHandler.UpdateUserStatus)
		admin.PATCH("/users/:id/role", adminHandler.UpdateUserRole)
		admin.PATCH("/users/:id/verification", adminHandler.UpdateUserVerification)
		admin.POST("/users/:id/reset-strikes", adminHandler.ResetUserStrikes)
		admin.PATCH("/users/:id/profile", adminHandler.AdminUpdateProfile)
		admin.POST("/users/:id/follows", adminHandler.AdminManageFollow)
		admin.GET("/users/:id/follows", adminHandler.AdminListFollows)

		// Post Management
		admin.GET("/posts", adminHandler.ListPosts)
		admin.GET("/posts/:id", adminHandler.GetPost)
		admin.PATCH("/posts/:id/status", adminHandler.UpdatePostStatus)
		admin.DELETE("/posts/:id", adminHandler.DeletePost)
		admin.POST("/posts/bulk", adminHandler.BulkUpdatePosts)

		// User Bulk
		admin.POST("/users/bulk", adminHandler.BulkUpdateUsers)

		// Moderation Queue
		admin.GET("/moderation", adminHandler.GetModerationQueue)
		admin.PATCH("/moderation/:id/review", adminHandler.ReviewModerationFlag)
		admin.POST("/moderation/bulk", adminHandler.BulkReviewModeration)

		// Appeals
		admin.GET("/appeals", adminHandler.ListAppeals)
		admin.PATCH("/appeals/:id/review", adminHandler.ReviewAppeal)

		// Reports
		admin.GET("/reports", adminHandler.ListReports)
		admin.PATCH("/reports/:id", adminHandler.UpdateReportStatus)
		admin.POST("/reports/bulk", adminHandler.BulkUpdateReports)

		// Algorithm / Feed Config
		admin.GET("/algorithm", adminHandler.GetAlgorithmConfig)
		admin.PUT("/algorithm", adminHandler.UpdateAlgorithmConfig)

		// Categories
		admin.GET("/categories", adminHandler.ListCategories)
		admin.POST("/categories", adminHandler.CreateCategory)
		admin.PATCH("/categories/:id", adminHandler.UpdateCategory)

		// Neighborhoods
		admin.GET("/neighborhoods", adminHandler.ListNeighborhoods)
		admin.POST("/neighborhoods/:id/admins", adminHandler.SetNeighborhoodAdmin)
		admin.GET("/neighborhoods/:id/admins", adminHandler.ListNeighborhoodAdmins)
		admin.GET("/neighborhoods/:id/board", adminHandler.ListNeighborhoodBoardEntries)
		admin.PATCH("/neighborhoods/:id/board/:entryId", adminHandler.UpdateNeighborhoodBoardEntry)

		// System
		admin.GET("/health", adminHandler.GetSystemHealth)
		admin.GET("/audit-log", adminHandler.GetAuditLog)

		// R2 Storage
		admin.GET("/storage/stats", adminHandler.GetStorageStats)
		admin.GET("/storage/objects", adminHandler.ListStorageObjects)
		admin.GET("/storage/object", adminHandler.GetStorageObject)
		admin.DELETE("/storage/object", adminHandler.DeleteStorageObject)

		// Reserved Usernames
		admin.GET("/usernames/reserved", adminHandler.ListReservedUsernames)
		admin.POST("/usernames/reserved", adminHandler.AddReservedUsername)
		admin.POST("/usernames/reserved/bulk", adminHandler.BulkAddReservedUsernames)
		admin.DELETE("/usernames/reserved/:id", adminHandler.RemoveReservedUsername)

		// Username Claim Requests
		admin.GET("/usernames/claims", adminHandler.ListClaimRequests)
		admin.PATCH("/usernames/claims/:id", adminHandler.ReviewClaimRequest)

		// AI Moderation Config
		admin.GET("/ai/models", adminHandler.ListOpenRouterModels)
		admin.GET("/ai/models/local", adminHandler.ListLocalModels)
		admin.GET("/ai/config", adminHandler.GetAIModerationConfigs)
		admin.PUT("/ai/config", adminHandler.SetAIModerationConfig)
		admin.POST("/ai/test", adminHandler.TestAIModeration)

		// AI Moderation Audit Log
		admin.GET("/ai/moderation-log", adminHandler.GetAIModerationLog)
		admin.POST("/ai/moderation-log/:id/feedback", adminHandler.SubmitAIModerationFeedback)
		admin.GET("/ai/training-data", adminHandler.ExportAITrainingData)

		// Admin Content Creation & Import
		admin.POST("/users/create", adminHandler.AdminCreateUser)
		admin.POST("/content/import", adminHandler.AdminImportContent)

		// Official Accounts Management
		admin.GET("/official-profiles", adminHandler.ListOfficialProfiles)
		admin.GET("/official-accounts", adminHandler.ListOfficialAccounts)
		admin.GET("/official-accounts/:id", adminHandler.GetOfficialAccount)
		admin.POST("/official-accounts", adminHandler.UpsertOfficialAccount)
		admin.DELETE("/official-accounts/:id", adminHandler.DeleteOfficialAccount)
		admin.PATCH("/official-accounts/:id/toggle", adminHandler.ToggleOfficialAccount)
		admin.POST("/official-accounts/:id/trigger", adminHandler.TriggerOfficialPost)
		admin.POST("/official-accounts/:id/preview", adminHandler.PreviewOfficialPost)
		admin.GET("/official-accounts/:id/articles", adminHandler.FetchNewsArticles)
		admin.GET("/official-accounts/:id/posted", adminHandler.GetPostedArticles)
		admin.POST("/official-accounts/:id/articles/cleanup", adminHandler.CleanupPendingArticles)
		admin.POST("/official-accounts/articles/:article_id/skip", adminHandler.SkipArticle)
		admin.POST("/official-accounts/articles/:article_id/post", adminHandler.PostSpecificArticle)
		admin.DELETE("/official-accounts/articles/:article_id", adminHandler.DeleteArticle)

		// AI Engines Status
		admin.GET("/ai-engines", adminHandler.GetAIEngines)
		admin.POST("/upload-test-image", adminHandler.UploadTestImage)

		// Safe Domains Management
		admin.GET("/safe-domains", adminHandler.ListSafeDomains)
		admin.POST("/safe-domains", adminHandler.UpsertSafeDomain)
		admin.DELETE("/safe-domains/:id", adminHandler.DeleteSafeDomain)
		admin.GET("/safe-domains/check", adminHandler.CheckURLSafety)

		// Email Templates
		admin.GET("/email-templates", adminHandler.ListEmailTemplates)
		admin.GET("/email-templates/:id", adminHandler.GetEmailTemplate)
		admin.PATCH("/email-templates/:id", adminHandler.UpdateEmailTemplate)
		admin.POST("/email-templates/test", adminHandler.SendTestEmail)

		// Groups admin
		admin.GET("/groups", adminHandler.AdminListGroups)
		admin.GET("/groups/:id", adminHandler.AdminGetGroup)
		admin.DELETE("/groups/:id", adminHandler.AdminDeleteGroup)
		admin.GET("/groups/:id/members", adminHandler.AdminListGroupMembers)
		admin.DELETE("/groups/:id/members/:userId", adminHandler.AdminRemoveGroupMember)

		// Quip repair
		admin.GET("/quips/broken", adminHandler.GetBrokenQuips)
		admin.PATCH("/posts/:id/thumbnail", adminHandler.SetPostThumbnail)
		admin.POST("/quips/:id/repair", adminHandler.RepairQuip)

		// Feed scores viewer
		admin.GET("/feed-scores", adminHandler.AdminGetFeedScores)

		// Waitlist management
		admin.GET("/waitlist", adminHandler.AdminListWaitlist)
		admin.PATCH("/waitlist/:id", adminHandler.AdminUpdateWaitlist)
		admin.DELETE("/waitlist/:id", adminHandler.AdminDeleteWaitlist)

		// Feed impression reset
		admin.DELETE("/users/:id/feed-impressions", adminHandler.AdminResetFeedImpressions)
	}

	// Public claim request endpoint (no auth)
	r.POST("/api/v1/username-claim", adminHandler.SubmitClaimRequest)

	// Account destroy confirmation (accessed via email link, no auth)
	r.GET("/api/v1/account/destroy/confirm", accountHandler.ConfirmImmediateDestroy)

	srv := &http.Server{
		Addr:    ":" + cfg.Port,
		Handler: r,
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal().Err(err).Msg("Failed to start server")
		}
	}()

	// Background job: update feed algorithm scores every 15 minutes
	go func() {
		ticker := time.NewTicker(15 * time.Minute)
		defer ticker.Stop()
		for range ticker.C {
			log.Debug().Msg("[FeedAlgorithm] Refreshing feed scores")
			if err := feedAlgorithmService.UpdateFeedScores(context.Background(), []string{}, ""); err != nil {
				log.Error().Err(err).Msg("[FeedAlgorithm] Failed to refresh feed scores")
			}
		}
	}()

	// Background job: purge accounts past 14-day deletion window (runs every hour)
	go func() {
		ticker := time.NewTicker(1 * time.Hour)
		defer ticker.Stop()
		for range ticker.C {
			ids, err := userRepo.GetAccountsPendingPurge(context.Background())
			if err != nil {
				log.Error().Err(err).Msg("[Purge] Failed to fetch accounts pending purge")
				continue
			}
			for _, id := range ids {
				log.Warn().Str("user_id", id).Msg("[Purge] Auto-purging account past 14-day grace period")
				if err := userRepo.CascadePurgeUser(context.Background(), id); err != nil {
					log.Error().Err(err).Str("user_id", id).Msg("[Purge] FAILED to purge account")
				} else {
					log.Info().Str("user_id", id).Msg("[Purge] Account permanently destroyed")
				}
			}
		}
	}()

	log.Info().Msgf("Server started on port %s", cfg.Port)

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	log.Info().Msg("Shutting down server...")

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal().Err(err).Msg("Server forced to shutdown")
	}

	log.Info().Msg("Server exiting")
}
