package server

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	redisclient "github.com/redis/go-redis/v9"
	"gorm.io/gorm"

	"security-center-backend/internal/config"
	agenthandler "security-center-backend/internal/handler/agent"
	"security-center-backend/internal/handler/audit"
	"security-center-backend/internal/handler/auth"
	"security-center-backend/internal/handler/health"
	openapistatic "security-center-backend/internal/handler/openapi"
	"security-center-backend/internal/handler/role"
	systemhandler "security-center-backend/internal/handler/system"
	"security-center-backend/internal/handler/user"
	"security-center-backend/internal/infra/mysql"
	"security-center-backend/internal/infra/redis"
	"security-center-backend/internal/middleware"
	"security-center-backend/internal/model"
	alertrepository "security-center-backend/internal/modules/alerts/repository"
	caasmrepository "security-center-backend/internal/modules/caasm/repository"
	caasmservice "security-center-backend/internal/modules/caasm/service"
	depository "security-center-backend/internal/modules/department/repository"
	scanrepository "security-center-backend/internal/modules/scan/repository"
	vulnrepository "security-center-backend/internal/modules/vuln/repository"
	"security-center-backend/internal/pkg/cache"
	"security-center-backend/internal/pkg/logger"
	"security-center-backend/internal/pkg/storage"
	"security-center-backend/internal/repository"
	"security-center-backend/internal/router"
	servermodules "security-center-backend/internal/server/modules"
	"security-center-backend/internal/service"
	auditservice "security-center-backend/internal/service/audit"
	"security-center-backend/internal/service/directory"
	jwtpkg "security-center-backend/internal/service/jwt"
	"security-center-backend/internal/service/oauth"
	rbacservice "security-center-backend/internal/service/rbac"
	settingsservice "security-center-backend/internal/service/settings"
	systemservice "security-center-backend/internal/service/system"
)

type Application struct {
	cfg        *config.Config
	logger     *logger.Logger
	engine     *gin.Engine
	httpServer *http.Server
	db         *gorm.DB
	redis      *redisclient.Client
	audit      *auditservice.Service
}

func configuredDevRoleNames(cfg *config.Config) []string {
	if cfg == nil || len(cfg.Auth.DevUser.Roles) == 0 {
		return []string{"admin"}
	}
	return cfg.Auth.DevUser.Roles
}

func ensureDevUserRoles(ctx context.Context, log *logger.Logger, cfg *config.Config, userRepo repository.UserRepository, roleRepo repository.RoleRepository, user *model.User) {
	if user == nil || userRepo == nil || roleRepo == nil {
		return
	}

	roleNames := configuredDevRoleNames(cfg)
	roleIDs := make([]uint64, 0, len(roleNames))
	missingRoles := make([]string, 0)
	for _, roleName := range roleNames {
		role, err := roleRepo.FindByName(ctx, roleName)
		if err != nil || role == nil {
			missingRoles = append(missingRoles, roleName)
			continue
		}
		roleIDs = append(roleIDs, role.ID)
	}

	if len(missingRoles) > 0 {
		log.Warn("failed to resolve one or more configured dev roles", "user_id", user.ID, "roles", missingRoles)
	}
	if len(roleIDs) == 0 {
		return
	}

	if err := userRepo.AssignRoles(ctx, user.ID, roleIDs); err != nil {
		log.Warn("failed to align dev user roles in database", "user_id", user.ID, "error", err)
		return
	}
	log.Info("aligned dev user roles in database", "user_id", user.ID, "roles", roleNames)
}

func NewApplication(cfg *config.Config, log *logger.Logger) (*Application, func(), error) {
	startedAt := time.Now()
	setGinMode(cfg.App.Environment)

	engine := gin.New()
	engine.Use(
		middleware.Recovery(log),
		middleware.RequestLogger(log),
		middleware.Gzip(cfg.HTTP),
		middleware.CORS(cfg.HTTP),
	)

	db, err := mysql.New(cfg.Database)
	if err != nil {
		// If bypass_sso is enabled, allow the app to run without database
		if cfg.Auth.BypassSSO {
			log.Warn("failed to initialize mysql, but continuing with bypass_sso enabled", "error", err)
			db = nil
		} else {
			return nil, nil, fmt.Errorf("initialize mysql: %w", err)
		}
	}
	if db != nil {
		if mysql.RuntimeAutoMigrateEnabled(cfg.App.Environment) {
			if mysql.IsProductionEnvironment(cfg.App.Environment) {
				log.Warn("runtime auto migrate enabled for production environment via explicit override", "environment", cfg.App.Environment)
			}
			if err := mysql.AutoMigrate(db); err != nil {
				// Log the migration error but don't fail if bypass_sso is enabled
				if cfg.Auth.BypassSSO {
					log.Warn("failed to auto migrate database, but continuing with bypass_sso enabled", "error", err)
				} else {
					return nil, nil, fmt.Errorf("auto migrate: %w", err)
				}
			}
		} else {
			log.Info("skipping runtime auto migrate; production deploys are expected to apply explicit migrations before startup", "environment", cfg.App.Environment)
		}
	}

	redisClient, err := redis.New(cfg.Redis)
	if err != nil {
		// If bypass_sso is enabled, allow the app to run without redis
		if cfg.Auth.BypassSSO {
			log.Warn("failed to initialize redis, but continuing with bypass_sso enabled", "error", err)
			redisClient = nil
		} else {
			return nil, nil, fmt.Errorf("initialize redis: %w", err)
		}
	}

	cacheLayer := cache.New(redisClient, 45*time.Second)

	jwtManager := jwtpkg.NewManager(cfg.JWT)
	googleClient := oauth.NewGoogleClient(cfg.OAuth)
	auth0Client := oauth.NewAuth0Client(cfg.OAuth)

	var fileUploader storage.FileUploader
	if cfg.Storage.Provider == "s3" && cfg.Storage.S3.Enabled {
		log.Info(
			"initializing s3 uploader for vulnerability image uploads",
			"bucket", cfg.Storage.S3.Bucket,
			"region", cfg.Storage.S3.Region,
			"prefix", cfg.Storage.S3.UploadPrefix,
			"endpoint", cfg.Storage.S3.Endpoint,
			"use_path_style", cfg.Storage.S3.UsePathStyle,
			"has_base_url", cfg.Storage.S3.BaseURL != "",
		)
		if uploader, err := storage.NewS3Uploader(context.Background(), cfg.Storage.S3); err != nil {
			log.Warn(
				"failed to initialize s3 uploader",
				"error", err,
				"bucket", cfg.Storage.S3.Bucket,
				"region", cfg.Storage.S3.Region,
				"prefix", cfg.Storage.S3.UploadPrefix,
				"endpoint", cfg.Storage.S3.Endpoint,
				"use_path_style", cfg.Storage.S3.UsePathStyle,
				"has_base_url", cfg.Storage.S3.BaseURL != "",
			)
		} else {
			fileUploader = uploader
			log.Info(
				"s3 uploader initialized",
				"bucket", cfg.Storage.S3.Bucket,
				"region", cfg.Storage.S3.Region,
				"prefix", cfg.Storage.S3.UploadPrefix,
			)
		}
	} else {
		log.Info(
			"vulnerability image uploads disabled by runtime config",
			"provider", cfg.Storage.Provider,
			"s3_enabled", cfg.Storage.S3.Enabled,
		)
	}

	var (
		userRepo                  repository.UserRepository
		roleRepo                  repository.RoleRepository
		permissionRepo            repository.PermissionRepository
		settingsRepo              repository.SystemSettingsRepository
		auditRepo                 repository.AuditRepository
		caasmAssetRepo            caasmrepository.AssetRepository
		caasmPortRepo             caasmrepository.IPPortIndexRepository
		caasmDomainRepo           caasmrepository.DomainRepository
		caasmDNSRecordRepo        caasmrepository.DNSRecordRepository
		caasmDNSRecordCommentRepo caasmrepository.DNSRecordCommentRepository
		caasmCloudAcctRepo        caasmrepository.CloudAccountRepository
		caasmSyncJobRepo          caasmrepository.SyncJobRepository
		caasmSyncJobEventRepo     caasmrepository.SyncJobEventRepository
		caasmConfigRepo           caasmrepository.ConfigRepository
		caasmHistoryRepo          caasmrepository.AssetHistoryRepository
		caasmTagRepo              caasmrepository.AssetTagRepository
		alertRuleRepo             alertrepository.RuleRepository
		alertRunRepo              alertrepository.RunRepository
		alertDeliveryRepo         alertrepository.DeliveryRepository
		scanRepo                  scanrepository.ScanRepository
		vulnRepo                  vulnrepository.VulnerabilityRepository
		vulnEventRepo             vulnrepository.VulnerabilityEventRepository
		vulnCommentRepo           vulnrepository.VulnerabilityCommentRepository
		vulnAttachmentRepo        vulnrepository.VulnerabilityAttachmentRepository
		vulnSLARepo               vulnrepository.SLARuleRepository
		vulnRequiredFieldRepo     vulnrepository.SubmissionRequiredFieldRepository
		vulnTypeRepo              vulnrepository.VulnerabilityTypeRepository
		vulnSourceRepo            vulnrepository.SourceRepository
		tagRepo                   vulnrepository.TagRepository
		channelRepo               vulnrepository.DiscoveryChannelRepository
		deptRepo                  depository.DepartmentRepository
		empRepo                   depository.EmployeeRepository
		orgSyncRunRepo            depository.OrgSyncRunRepository
		apiKeyRepo                repository.APIKeyRepository
	)
	if db != nil {
		userRepo = repository.NewUserRepository(db)
		roleRepo = repository.NewRoleRepository(db)
		permissionRepo = repository.NewPermissionRepository(db)
		settingsRepo = repository.NewSystemSettingsRepository(db)
		auditRepo = repository.NewAuditRepository(db)
		caasmAssetRepo = caasmrepository.NewAssetRepository(db)
		caasmPortRepo = caasmrepository.NewIPPortIndexRepository(db)
		caasmDomainRepo = caasmrepository.NewDomainRepository(db)
		caasmDNSRecordRepo = caasmrepository.NewDNSRecordRepository(db)
		caasmDNSRecordCommentRepo = caasmrepository.NewDNSRecordCommentRepository(db)
		caasmCloudAcctRepo = caasmrepository.NewCloudAccountRepository(db)
		caasmSyncJobRepo = caasmrepository.NewSyncJobRepository(db)
		caasmSyncJobEventRepo = caasmrepository.NewSyncJobEventRepository(db)
		caasmConfigRepo = caasmrepository.NewConfigRepository(db)
		caasmHistoryRepo = caasmrepository.NewAssetHistoryRepository(db)
		caasmTagRepo = caasmrepository.NewAssetTagRepository(db)
		alertRuleRepo = alertrepository.NewRuleRepository(db)
		alertRunRepo = alertrepository.NewRunRepository(db)
		alertDeliveryRepo = alertrepository.NewDeliveryRepository(db)
		scanRepo = scanrepository.NewScanRepository(db)
		vulnRepo = vulnrepository.NewVulnerabilityRepository(db)
		vulnEventRepo = vulnrepository.NewVulnerabilityEventRepository(db)
		vulnCommentRepo = vulnrepository.NewVulnerabilityCommentRepository(db)
		vulnAttachmentRepo = vulnrepository.NewVulnerabilityAttachmentRepository(db)
		vulnSLARepo = vulnrepository.NewSLARuleRepository(db)
		vulnRequiredFieldRepo = vulnrepository.NewSubmissionRequiredFieldRepository(db)
		vulnTypeRepo = vulnrepository.NewVulnerabilityTypeRepository(db)
		vulnSourceRepo = vulnrepository.NewSourceRepository(db)
		tagRepo = vulnrepository.NewTagRepository(db)
		channelRepo = vulnrepository.NewDiscoveryChannelRepository(db)
		deptRepo = depository.NewDepartmentRepository(db)
		empRepo = depository.NewEmployeeRepository(db)
		orgSyncRunRepo = depository.NewOrgSyncRunRepository(db)
		apiKeyRepo = repository.NewAPIKeyRepository(db)
	}

	directoryAdapter := directory.NewRepositoryAdapter(empRepo, deptRepo)

	auditSvc := auditservice.NewService(auditRepo, userRepo, directoryAdapter, 512, log)
	if auditRepo == nil && auditSvc != nil {
		log.Warn("audit repository is nil - audit logging will be disabled", "bypass_sso", cfg.Auth.BypassSSO)
	}

	var (
		rbacSvc       *rbacservice.Service
		userHandler   *user.Handler
		roleHandler   *role.Handler
		settingsSvc   *settingsservice.Service
		systemSvc     *systemservice.Service
		systemHandler *systemhandler.Handler
	)
	if userRepo != nil && roleRepo != nil {
		rbacSvc = rbacservice.NewService(userRepo, roleRepo, permissionRepo, directoryAdapter, directoryAdapter)
		userHandler = user.NewHandler(rbacSvc, auditSvc)
		roleHandler = role.NewHandler(rbacSvc, auditSvc)
		// Seed permission catalog and keep admin aligned with full permissions
		if err := rbacservice.SeedPermissions(context.Background(), permissionRepo, roleRepo); err != nil {
			log.Warn("failed to seed permissions", "error", err)
		}

		// Create dev user in database when bypass_sso is enabled
		// This prevents foreign key constraint failures in audit logs
		if cfg.Auth.BypassSSO {
			devUserEmail := cfg.Auth.DevUser.Email
			if devUserEmail == "" {
				devUserEmail = "dev.user@example.com"
			}
			devUserName := cfg.Auth.DevUser.Name
			if devUserName == "" {
				devUserName = "Development User"
			}

			// Check if dev user already exists by email
			existingUser, err := userRepo.FindByEmail(context.Background(), devUserEmail)
			if err == nil && existingUser != nil {
				log.Info("dev user already exists in database", "email", devUserEmail, "id", existingUser.ID)
				ensureDevUserRoles(context.Background(), log, cfg, userRepo, roleRepo, existingUser)
			} else {
				// Create the dev user
				devUser := &model.User{
					ExternalID: cfg.Auth.DevUser.ID,
					Email:      devUserEmail,
					Name:       devUserName,
					Status:     model.UserStatusActive,
				}
				if err := userRepo.CreateOrUpdate(context.Background(), devUser); err != nil {
					log.Warn("failed to create dev user in database", "error", err, "email", devUserEmail)
				} else {
					log.Info("created dev user in database", "email", devUserEmail, "id", devUser.ID)
					ensureDevUserRoles(context.Background(), log, cfg, userRepo, roleRepo, devUser)
				}
			}
		}
	}

	if settingsRepo != nil {
		settingsSvc = settingsservice.New(settingsRepo, auditSvc, cacheLayer)
		systemSvc = systemservice.NewService(cfg, settingsSvc, db, redisClient, startedAt)
		systemHandler = systemhandler.NewHandler(systemSvc)
	}

	caasmModule := servermodules.BuildCAASM(servermodules.CAASMBuildInput{
		DB:                   db,
		Redis:                redisClient,
		UserRepo:             userRepo,
		AssetRepo:            caasmAssetRepo,
		PortRepo:             caasmPortRepo,
		DomainRepo:           caasmDomainRepo,
		DNSRecordRepo:        caasmDNSRecordRepo,
		DNSRecordCommentRepo: caasmDNSRecordCommentRepo,
		CloudAccountRepo:     caasmCloudAcctRepo,
		SyncJobRepo:          caasmSyncJobRepo,
		SyncJobEventRepo:     caasmSyncJobEventRepo,
		ConfigRepo:           caasmConfigRepo,
		HistoryRepo:          caasmHistoryRepo,
		TagRepo:              caasmTagRepo,
		Cache:                cacheLayer,
		Logger:               log,
		Audit:                auditSvc,
	})
	departmentModule := servermodules.BuildDepartment(servermodules.DepartmentBuildInput{
		DepartmentRepo: deptRepo,
		EmployeeRepo:   empRepo,
		OrgSyncRunRepo: orgSyncRunRepo,
		UserRepo:       userRepo,
		Settings:       settingsSvc,
		Logger:         log,
	})
	if departmentModule.Configure != nil {
		departmentModule.Configure(settingsSvc)
	}
	pickerReadsBlocked := func() bool {
		if departmentModule.RuntimeStatusProvider == nil {
			return false
		}
		status := departmentModule.RuntimeStatusProvider()
		for _, scope := range status.BlockingScopes {
			if scope == "picker_reads" {
				return true
			}
		}
		return false
	}
	vulnModule := servermodules.BuildVuln(servermodules.VulnBuildInput{
		VulnRepo:              vulnRepo,
		VulnEventRepo:         vulnEventRepo,
		VulnCommentRepo:       vulnCommentRepo,
		VulnAttachmentRepo:    vulnAttachmentRepo,
		VulnSLARepo:           vulnSLARepo,
		VulnRequiredFieldRepo: vulnRequiredFieldRepo,
		VulnTypeRepo:          vulnTypeRepo,
		VulnSourceRepo:        vulnSourceRepo,
		TagRepo:               tagRepo,
		ChannelRepo:           channelRepo,
		UserRepo:              userRepo,
		EmployeeDir:           directoryAdapter,
		PickerReadsBlocked:    pickerReadsBlocked,
		FileUploader:          fileUploader,
		Cache:                 cacheLayer,
	})
	alertsModule := servermodules.BuildAlerts(servermodules.AlertsBuildInput{
		DB:           db,
		RuleRepo:     alertRuleRepo,
		RunRepo:      alertRunRepo,
		DeliveryRepo: alertDeliveryRepo,
		Settings:     settingsSvc,
		Cache:        cacheLayer,
		Logger:       log,
	})
	scanModule := servermodules.BuildScan(servermodules.ScanBuildInput{
		DB:     db,
		Repo:   scanRepo,
		Config: cfg.Scan.Scanner,
		Logger: log,
	})
	if alertsModule.Configure != nil && departmentModule.RuntimeStatusProvider != nil {
		alertsModule.Configure(settingsSvc, departmentModule.RuntimeStatusProvider)
	}

	authService := service.NewAuthService(cfg.Auth, cfg.OAuth, jwtManager, userRepo, roleRepo, directoryAdapter, cacheLayer, auditSvc, googleClient, auth0Client)

	authHandler := auth.NewHandler(authService, jwtManager, auditSvc, log)
	auditHandler := audit.NewHandler(auditSvc)
	healthHandler := health.NewHandler()
	openAPIHandler := openapistatic.NewHandler()

	if caasmModule.QueueMetricsProvider != nil {
		healthHandler.SetQueueMetricsProvider(caasmModule.QueueMetricsProvider)
	}

	if caasmModule.SchedulerStatusProvider != nil {
		healthHandler.SetSchedulerStatusProvider(caasmModule.SchedulerStatusProvider)
	}

	var authMiddleware gin.HandlerFunc
	// Always apply auth middleware so user context is present for audit logs and permission checks,
	// even in bypass_sso mode (dev tokens are still validated).
	authMiddleware = middleware.Auth(jwtManager)

	// Agent auth (X-API-Key) requires a repository; without a database the CombinedAuth
	// fallback in the router keeps the JWT-only chain.
	var agentAuth gin.HandlerFunc
	if apiKeyRepo != nil {
		agentAuth = middleware.AgentAuth(apiKeyRepo, cfg.Agent)
	}

	// Self-service API key management handler (Phase 2); requires the repository,
	// so it stays nil when the app runs without a database.
	var agentKeysHandler *agenthandler.Handler
	if apiKeyRepo != nil {
		agentKeysHandler = agenthandler.New(apiKeyRepo, cfg.Agent, db)
	}

	var requirePermission func(string) gin.HandlerFunc
	requirePermission = func(permission string) gin.HandlerFunc {
		return middleware.Permission(permission)
	}

	var auditTrailMiddleware gin.HandlerFunc
	if auditSvc != nil && auditRepo != nil {
		auditTrailMiddleware = middleware.AuditTrail(auditSvc)
	}

	// Agent 配额中间件; Addr 为空时 redis.New 返回 nil, 此时不装配(nil 安全)。
	var quotaMiddleware gin.HandlerFunc
	if redisClient != nil {
		quotaMiddleware = middleware.Quota(redisClient, cfg.Agent.QuotaEnabled, cfg.Agent.DefaultQuotaRPM)
	}

	router.RegisterRoutes(engine, router.Dependencies{
		Auth:                 authHandler,
		Health:               healthHandler,
		Audit:                auditHandler,
		Users:                userHandler,
		Roles:                roleHandler,
		System:               systemHandler,
		Alerts:               alertsModule.Handler,
		CAASM:                caasmModule.Handler,
		Scan:                 scanModule.Handler,
		VulnModule:           vulnModule.Module,
		Department:           departmentModule.Handler,
		Employees:            departmentModule.EmployeeHandler,
		AgentKeys:            agentKeysHandler,
		OpenAPI:              openAPIHandler,
		AuthMiddleware:       authMiddleware,
		AgentAuth:            agentAuth,
		Quota:                quotaMiddleware,
		RequirePermission:    requirePermission,
		AuditTrailMiddleware: auditTrailMiddleware,
	})

	httpSrv := &http.Server{
		Addr:         fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port),
		Handler:      engine,
		ReadTimeout:  cfg.HTTP.ReadTimeout,
		WriteTimeout: cfg.HTTP.WriteTimeout,
	}

	app := &Application{
		cfg:        cfg,
		logger:     log,
		engine:     engine,
		httpServer: httpSrv,
		db:         db,
		redis:      redisClient,
		audit:      auditSvc,
	}

	// Start Zombie Reaper
	reaperCtx, reaperCancel := context.WithCancel(context.Background())
	if caasmSyncJobRepo != nil {
		zombieReaper := caasmservice.NewZombieReaper(caasmSyncJobRepo, caasmModule.SyncJobTimeoutProvider)
		zombieReaper.SetLogger(log)
		go zombieReaper.Start(reaperCtx)
	}

	// Start audit_logs retention cleanup (in-process daily ticker, mirrors scan dispatcher pattern)
	retentionCtx, retentionCancel := context.WithCancel(context.Background())
	if db != nil {
		go StartAuditRetention(retentionCtx, db, cfg.Audit.RetentionDays, log.Logger)
	}

	cleanup := func() {
		app.logger.Info("cleaning up resources")
		reaperCancel()    // Stop Zombie Reaper
		retentionCancel() // Stop audit retention cleanup
		stopHooks := []servermodules.StopHook{}
		stopHooks = append(stopHooks, caasmModule.StopHooks...)
		stopHooks = append(stopHooks, alertsModule.StopHooks...)
		stopHooks = append(stopHooks, departmentModule.StopHooks...)
		stopHooks = append(stopHooks, scanModule.StopHooks...)
		for _, stop := range stopHooks {
			if stop == nil {
				continue
			}
			_ = stop(context.Background())
		}
		if app.db != nil {
			if sqlDB, err := app.db.DB(); err == nil {
				_ = sqlDB.Close()
			}
		}
		if app.redis != nil {
			_ = app.redis.Close()
		}
		if app.audit != nil {
			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			_ = app.audit.Shutdown(ctx)
			cancel()
		}
	}

	return app, cleanup, nil
}

func (a *Application) Run() error {
	a.logger.Info("http server listening", "addr", a.httpServer.Addr)
	if err := a.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		return err
	}
	return nil
}

func (a *Application) Stop(ctx context.Context) error {
	return a.httpServer.Shutdown(ctx)
}

func setGinMode(env string) {
	switch env {
	case "production":
		gin.SetMode(gin.ReleaseMode)
	case "test":
		gin.SetMode(gin.TestMode)
	default:
		gin.SetMode(gin.DebugMode)
	}
}
