package main

import (
	"baron-sso-backend/internal/bootstrap"
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/handler"
	"baron-sso-backend/internal/idp"
	"baron-sso-backend/internal/logger"
	"baron-sso-backend/internal/middleware"
	"baron-sso-backend/internal/repository"
	"baron-sso-backend/internal/service"
	"baron-sso-backend/internal/validator"
	"context"
	"fmt"
	"log"
	"log/slog"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/bwmarrin/snowflake"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/gofiber/fiber/v2/middleware/encryptcookie"
	"github.com/gofiber/fiber/v2/middleware/recover"
	"github.com/gofiber/fiber/v2/middleware/requestid"
	"github.com/joho/godotenv"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	gormLogger "gorm.io/gorm/logger"
)

func getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}

func getEnvFileOrValue(fileKey string, valueKey string, fallback string) (string, error) {
	if path := strings.TrimSpace(getEnv(fileKey, "")); path != "" {
		value, err := readEnvFileValue(path)
		if err != nil {
			return "", err
		}
		return value, nil
	}
	return getEnv(valueKey, fallback), nil
}

func readEnvFileValue(path string) (string, error) {
	candidates := []string{path}
	if !filepath.IsAbs(path) {
		candidates = append(candidates, filepath.Join("..", path), filepath.Join("..", "..", path))
	}

	var lastErr error
	for _, candidate := range candidates {
		data, err := os.ReadFile(candidate)
		if err == nil {
			return string(data), nil
		}
		lastErr = err
	}
	return "", fmt.Errorf("read secret file %q: %w", path, lastErr)
}

func normalizeDocsPrefix(prefix string) string {
	trimmed := strings.TrimSpace(prefix)
	if trimmed == "" || trimmed == "/" {
		return ""
	}
	if !strings.HasPrefix(trimmed, "/") {
		trimmed = "/" + trimmed
	}
	return strings.TrimRight(trimmed, "/")
}

func shouldEnableDocs(appEnv string) bool {
	env := strings.ToLower(strings.TrimSpace(appEnv))
	return env != "prod" && env != "production"
}

func registerDocsRoutes(app *fiber.App, prefix string) {
	base := normalizeDocsPrefix(prefix)
	docsPath := base + "/docs"
	redocPath := base + "/redoc"
	openapiPath := base + "/openapi.yaml"

	app.Get(docsPath, func(c *fiber.Ctx) error {
		return c.SendFile("./docs/swagger-ui/index.html")
	})
	app.Get(docsPath+"/", func(c *fiber.Ctx) error {
		return c.SendFile("./docs/swagger-ui/index.html")
	})
	app.Static(docsPath, "./docs/swagger-ui")

	app.Get(redocPath, func(c *fiber.Ctx) error {
		return c.SendFile("./docs/redoc/index.html")
	})
	app.Get(redocPath+"/", func(c *fiber.Ctx) error {
		return c.SendFile("./docs/redoc/index.html")
	})
	app.Static(redocPath, "./docs/redoc")

	app.Get(openapiPath, func(c *fiber.Ctx) error {
		c.Type("yaml")
		return c.SendFile("./docs/openapi.yaml")
	})
}

func main() {
	// Load .env file from possible paths
	// 1. .env (Current Directory)
	// 2. ../.env (Project Root when running from backend/)
	// 3. ../../.env (Project Root when running from backend/cmd/server/)
	if err := godotenv.Load(".env"); err != nil {
		if err := godotenv.Load("../.env"); err != nil {
			godotenv.Load("../../.env")
		}
	}

	// 0. Initialize Logger
	appEnvForLogger := getEnv("APP_ENV", getEnv("GO_ENV", "dev"))
	logger.Init(logger.Config{
		ServiceName:   "baron-sso",
		Environment:   appEnvForLogger,
		LevelOverride: getEnv("BACKEND_LOG_LEVEL", ""),
	})
	// Initialize Snowflake Node (Node 2 for Baron)
	node, err := snowflake.NewNode(2)
	if err != nil {
		slog.Error("Failed to initialize snowflake node", "error", err)
		os.Exit(1)
	}

	// 1. Log Config on Startup
	fmt.Println("============================================================")
	fmt.Println(`
      |\__/,|   (\      
    _.|o o  |_   ) )    
   -(((---(((--------
    	`)
	fmt.Println("🚀 Baron SSO Backend Starting...")

	slog.Info("Service starting",
		"service", "baron-sso",
		"app_env", getEnv("APP_ENV", "dev"),
		"db_port", getEnv("DB_PORT", "5532"),
		"backend_port", getEnv("BACKEND_PORT", "3000"),
		"userfront_port", getEnv("USERFRONT_PORT", "5000"),
		"userfront_url", getEnv("USERFRONT_URL", "http://sso.hmac.kr"),
		"redis_addr", getEnv("REDIS_ADDR", "redis:6379"),
	)

	// --- Fail-Fast Schema Validation ---
	// 팩토리를 사용하여 IDP 공급자를 초기화합니다.
	idpProvider, err := idp.InitializeProvider()
	if err != nil {
		slog.Error("❌ [CRITICAL] Failed to initialize IDP Provider", "error", err)
		os.Exit(1)
	}

	if err := validator.ValidateIDPCompatibility(domain.BrokerUser{}, idpProvider); err != nil {
		slog.Error("❌ [CRITICAL] Broker Schema Mismatch",
			"idp", idpProvider.Name(),
			"error", err,
		)
		fmt.Printf("\n!!! CRITICAL ERROR: IDP Schema Mismatch !!!\n%v\n\n", err)
		os.Exit(1) // Break the build/deployment
	}
	slog.Info("✅ IDP Schema Validation Passed", "idp", idpProvider.Name())
	// -----------------------------------

	// 2. Initialize DB Connections
	// ClickHouse
	chHost := getEnv("CLICKHOUSE_HOST", "localhost")
	chPort, _ := strconv.Atoi(getEnv("CLICKHOUSE_PORT_NATIVE", "9000"))
	chUser := getEnv("CLICKHOUSE_USER", "baron")
	chPass := getEnv("CLICKHOUSE_PASSWORD", "password")
	chDB := getEnv("CLICKHOUSE_DB", "baron_sso")

	var auditRepo domain.AuditRepository
	var rpUsageProjectionRepo domain.RPUsageProjectionRepository
	var rpUsageQueryRepo domain.RPUsageQueryRepository
	if repo, err := repository.NewClickHouseRepository(chHost, chPort, chUser, chPass, chDB); err != nil {
		slog.Warn("Failed to connect to ClickHouse. Audit logs will fail.", "error", err)
		auditRepo = nil // Explicitly set to nil interface
	} else {
		auditRepo = repo
		rpUsageProjectionRepo = repo
		rpUsageQueryRepo = repo
		slog.Info("✅ Connected to ClickHouse")
	}

	var oathkeeperRepo domain.OathkeeperLogRepository
	oryCHHost := getEnv("ORY_CLICKHOUSE_HOST", "ory_clickhouse")
	oryCHPort, _ := strconv.Atoi(getEnv("ORY_CLICKHOUSE_PORT_NATIVE", "9000"))
	oryCHUser := getEnv("ORY_CLICKHOUSE_USER", "ory")
	oryCHPass := getEnv("ORY_CLICKHOUSE_PASSWORD", "orypass")
	oryCHDB := getEnv("ORY_CLICKHOUSE_DB", "ory")
	if repo, err := repository.NewOathkeeperClickHouseRepository(oryCHHost, oryCHPort, oryCHUser, oryCHPass, oryCHDB); err != nil {
		slog.Warn("Failed to connect to Ory ClickHouse. Oathkeeper logs will be skipped.", "error", err)
		oathkeeperRepo = nil
	} else {
		oathkeeperRepo = repo
		slog.Info("✅ Connected to Ory ClickHouse")
	}

	redisService, err := service.NewRedisService()
	if err != nil {
		slog.Warn("Failed to connect to Redis. Auth features may fail.", "error", err)
	}

	ketoService := service.NewKetoService()

	// PostgreSQL (Meta Store)
	pgHost := getEnv("DB_HOST", "localhost")
	pgPort := getEnv("DB_PORT", "5432")
	pgUser := getEnv("DB_USER", "baron")
	pgPass := getEnv("DB_PASSWORD", "password")
	pgName := getEnv("DB_NAME", "baron_sso")

	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Seoul",
		pgHost, pgUser, pgPass, pgName, pgPort)

	gormLog := gormLogger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		gormLogger.Config{
			SlowThreshold:             time.Second,
			LogLevel:                  gormLogger.Warn,
			IgnoreRecordNotFoundError: true,
			Colorful:                  true,
		},
	)

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
		Logger: gormLog,
	})
	if err != nil {
		slog.Error("❌ Failed to connect to PostgreSQL", "error", err)
		os.Exit(1)
	} else {
		slog.Info("✅ Connected to PostgreSQL")

		// Run Bootstrap (Migrations & Seeding)
		if err := bootstrap.Run(db); err != nil {
			slog.Error("❌ Bootstrap failed", "error", err)
		}

		// [New] Initialize Keto Outbox and Worker
		ketoOutboxRepo := repository.NewKetoOutboxRepository(db)
		ketoRelayWorker := service.NewKetoRelayWorker(ketoOutboxRepo, ketoService)
		go ketoRelayWorker.Start(context.Background())
		slog.Info("✅ Keto Relay Worker started")

		// [Moved & Enhanced] Seed Admin Identity & Sync Local Role
		if kratosID, err := bootstrap.SeedAdminIdentity(idpProvider); err != nil {
			slog.Error("❌ Admin identity seed failed", "error", err)
		} else {
			// Sync role to local DB
			if err := bootstrap.SyncAdminRole(db, kratosID); err != nil {
				slog.Error("❌ Admin role sync failed", "error", err)
			}
		}

		// [New] Sync existing data to Keto
		if ketoOutboxRepo != nil {
			if err := bootstrap.SyncKetoRelations(db, ketoOutboxRepo); err != nil {
				slog.Warn("⚠️ Keto synchronization queueing failed during startup", "error", err)
			}
		}
	}

	// Oathkeeper 상태를 주기적으로 확인해 다운을 감지합니다.
	var oathkeeperProbe *HTTPProbe
	if strings.ToLower(getEnv("OATHKEEPER_HEALTH_ENABLED", "true")) != "false" {
		intervalSec, err := strconv.Atoi(getEnv("OATHKEEPER_HEALTH_INTERVAL_SECONDS", "10"))
		if err != nil || intervalSec <= 0 {
			intervalSec = 10
		}
		timeoutSec, err := strconv.Atoi(getEnv("OATHKEEPER_HEALTH_TIMEOUT_SECONDS", "2"))
		if err != nil || timeoutSec <= 0 {
			timeoutSec = 2
		}
		oathkeeperProbe = NewHTTPProbe(
			"oathkeeper",
			getEnv("OATHKEEPER_HEALTH_URL", "http://oathkeeper:4456/health/ready"),
			time.Duration(intervalSec)*time.Second,
			time.Duration(timeoutSec)*time.Second,
		)
		oathkeeperProbe.Start()
	} else {
		slog.Info("Oathkeeper probe disabled")
	}

	// 2. Initialize Handlers
	tenantRepo := repository.NewTenantRepository(db)
	userGroupRepo := repository.NewUserGroupRepository(db)
	userRepo := repository.NewUserRepository(db)
	userProjectionRepo := repository.NewUserProjectionRepository(db)
	ketoOutboxRepo := repository.NewKetoOutboxRepository(db) // Reuse or re-init
	rpUsageOutboxRepo := repository.NewRPUsageOutboxRepository(db)
	worksmobileOutboxRepo := repository.NewWorksmobileOutboxRepository(db)
	sharedLinkRepo := repository.NewSharedLinkRepository(db)
	kratosAdminService := service.NewKratosAdminService()
	oryAdminProvider := service.NewOryProvider()

	userProjectionSyncer := service.NewUserProjectionSyncService(kratosAdminService, userProjectionRepo)
	if synced, err := userProjectionSyncer.Reconcile(context.Background()); err != nil {
		slog.Error("❌ Kratos user projection sync failed", "error", err)
	} else {
		slog.Info("✅ Kratos user projection synced", "users", synced)
	}

	tenantService := service.NewTenantService(tenantRepo, userRepo, userGroupRepo, ketoOutboxRepo)
	worksmobilePrivateKey, err := getEnvFileOrValue("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY_FILE", "WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY", "")
	if err != nil {
		slog.Error("Worksmobile private key file could not be loaded", "error", err)
		os.Exit(1)
	}
	worksmobileClient := service.NewWorksmobileHTTPClientWithAuth(
		getEnv("WORKS_ADMIN_ACCESS_TOKEN", getEnv("WORKS_ADMIN_OAUTH_ACCESS_TOKEN", "")),
		getEnv("SAMAN_SCIM_LONGLIVE_TOKEN", ""),
		service.WorksmobileOAuthConfig{
			ClientID:       getEnv("WORKS_ADMIN_OAUTH_CLIENT_ID", ""),
			ClientSecret:   getEnv("WORKS_ADMIN_OAUTH_CLIENT_SECRET", ""),
			ServiceAccount: getEnv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT", ""),
			PrivateKey:     worksmobilePrivateKey,
			Scope:          getEnv("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
			TokenURL:       getEnv("WORKS_ADMIN_OAUTH_TOKEN_URL", ""),
		},
	)
	configureWorksmobileClientFromEnv(worksmobileClient)
	worksmobileService := service.NewWorksmobileSyncService(tenantService, userRepo, worksmobileOutboxRepo, worksmobileClient)
	worksmobileRelayClient := *worksmobileClient
	worksmobileRelayClient.RateLimiter = service.NewWorksmobileAPIRateLimiter(240, time.Minute)
	worksmobileRelayWorker := service.NewWorksmobileRelayWorker(worksmobileOutboxRepo, &worksmobileRelayClient)
	if lock := service.NewWorksmobileRedisRelayLeaderLock(redisService); lock != nil {
		worksmobileRelayWorker.SetLeaderLock(lock)
	}
	go worksmobileRelayWorker.Start(context.Background())
	slog.Info("✅ Worksmobile Relay Worker started")
	rpUsageEmitter := service.NewRPUsageEventEmitter(rpUsageOutboxRepo)
	if rpUsageProjectionRepo != nil {
		rpUsageProjectorWorker := service.NewRPUsageProjectorWorker(rpUsageOutboxRepo, rpUsageProjectionRepo)
		go rpUsageProjectorWorker.Start(context.Background())
		slog.Info("✅ RP Usage Projector Worker started")
	} else {
		slog.Warn("RP Usage Projector Worker skipped because ClickHouse is unavailable")
	}
	sharedLinkService := service.NewSharedLinkService(sharedLinkRepo)
	userGroupService := service.NewUserGroupService(userGroupRepo, userRepo, tenantRepo, ketoService, ketoOutboxRepo, kratosAdminService)
	userGroupService.SetWorksmobileSyncer(worksmobileService)
	tenantService.SetKetoService(ketoService) // Keto 주입

	hydraService := service.NewHydraAdminService()
	headlessJWKSCache := service.NewHeadlessJWKSCacheService(redisService, nil)
	headlessJWKSWorker := service.NewHeadlessJWKSCacheWorker(hydraService, headlessJWKSCache)
	go headlessJWKSWorker.Start(context.Background())
	slog.Info("✅ Headless JWKS Cache Worker started")
	relyingPartyService := service.NewRelyingPartyService(hydraService, ketoService, ketoOutboxRepo)
	secretRepo := repository.NewClientSecretRepository(db)
	consentRepo := repository.NewClientConsentRepository(db)
	rpUserMetadataRepo := repository.NewRPUserMetadataRepository(db)
	developerService := service.NewDeveloperService(db)

	auditHandler := handler.NewAuditHandler(auditRepo)
	authHandler := handler.NewAuthHandler(redisService, idpProvider, auditRepo, oathkeeperRepo, tenantService, ketoService, ketoOutboxRepo, userRepo, consentRepo, kratosAdminService)
	authHandler.HeadlessJWKS = headlessJWKSCache
	authHandler.UserProjectionRepo = userProjectionRepo
	authHandler.RPUserMetadataRepo = rpUserMetadataRepo
	authHandler.RPUsageSink = rpUsageEmitter
	adminHandler := handler.NewAdminHandler(ketoService, ketoOutboxRepo)
	adminHandler.DB = db
	adminHandler.RPUsageQueries = rpUsageQueryRepo
	adminHandler.TenantRepo = tenantRepo
	adminHandler.Hydra = hydraService
	adminHandler.AuditRepo = auditRepo
	adminHandler.UserProjectionRepo = userProjectionRepo
	adminHandler.IdentityCache = redisService
	adminHandler.IntegrityChecker = repository.NewDataIntegrityChecker(db)
	devHandler := handler.NewDevHandler(redisService, secretRepo, consentRepo, relyingPartyService, ketoService, ketoOutboxRepo, tenantService, developerService, authHandler)
	devHandler.HeadlessJWKS = headlessJWKSCache
	devHandler.AuditRepo = auditRepo
	devHandler.RPUserMetadataRepo = rpUserMetadataRepo
	devHandler.RPUsageQueries = rpUsageQueryRepo
	tenantHandler := handler.NewTenantHandler(db, tenantService, userRepo, userProjectionRepo, ketoService, ketoOutboxRepo, kratosAdminService, sharedLinkService, hydraService, consentRepo)
	tenantHandler.OrgChartCache = redisService
	userGroupHandler := handler.NewUserGroupHandler(userGroupService)
	relyingPartyHandler := handler.NewRelyingPartyHandler(relyingPartyService, kratosAdminService)
	userHandler := handler.NewUserHandler(kratosAdminService, oryAdminProvider, tenantService, ketoService, ketoOutboxRepo, userRepo, userGroupRepo, auditRepo)
	userHandler.UserProjectionRepo = userProjectionRepo
	tenantHandler.SetWorksmobileSyncer(worksmobileService)
	userHandler.SetWorksmobileSyncer(worksmobileService)
	worksmobileHandler := handler.NewWorksmobileHandler(worksmobileService)
	apiKeyHandler := handler.NewApiKeyHandler(db)

	// 3. Initialize Fiber
	appEnv := getEnv("APP_ENV", "dev")
	clientLogDebugFlag := getEnv("CLIENT_LOG_DEBUG", "")
	clientDebugEnabled := logger.ClientDebugEnabled(appEnv, clientLogDebugFlag)
	app := fiber.New(fiber.Config{
		AppName:               "Baron SSO Backend",
		DisableStartupMessage: true,  // Clean logs
		ReadBufferSize:        32768, // 32KB로 증가 (긴 OIDC 챌린지 대응)
		ErrorHandler:          newErrorHandler(appEnv),
	})

	// Middleware
	app.Use(requestid.New(requestid.Config{
		Generator: func() string {
			return node.Generate().String()
		},
	}))

	// [Standardized] HTTP Request Logger Middleware using slog
	app.Use(func(c *fiber.Ctx) error {
		start := time.Now()

		// Handle request
		err := c.Next()

		// Log after request
		latency := time.Since(start)
		status := c.Response().StatusCode()
		path := c.Path()

		// Skip logging for all successful requests (status < 400)
		if status < 400 {
			return err
		}

		msg := "http_request"
		if err != nil {
			msg = "http_request_error"
		}

		slog.Info(msg,
			"status", status,
			"method", c.Method(),
			"path", path,
			"latency", latency.String(),
			"ip", c.IP(),
			"req_id", c.GetRespHeader(fiber.HeaderXRequestID),
		)
		return err
	})

	app.Use(recover.New(recover.Config{
		EnableStackTrace: true,
	}))

	// Backfill `code` on legacy JSON error responses during migration period.
	app.Use(middleware.ErrorCodeEnricher())

	allowedOrigins := getEnv("CORS_ALLOWED_ORIGINS", "http://localhost:5000")
	userfrontURL := getEnv("USERFRONT_URL", "http://sso.hmac.kr")
	baseDomain := ""
	if u, err := url.Parse(userfrontURL); err == nil {
		baseDomain = u.Hostname()
	}

	app.Use(cors.New(cors.Config{
		AllowOriginsFunc: func(origin string) bool {
			// 1. Check static allowed list
			for allowed := range strings.SplitSeq(allowedOrigins, ",") {
				if origin == strings.TrimSpace(allowed) {
					return true
				}
			}

			// Parse origin URL
			u, err := url.Parse(origin)
			if err != nil {
				return false
			}
			hostname := u.Hostname()

			// 2. Check subdomains of base domain
			if baseDomain != "" && (hostname == baseDomain || strings.HasSuffix(hostname, "."+baseDomain)) {
				return true
			}

			// 3. Check registered tenant domains
			// Use context.Background() as we don't have request context here easily
			allowed, _ := tenantService.IsDomainAllowed(context.Background(), hostname)
			return allowed
		},
		AllowHeaders:     "Origin, Content-Type, Accept, Authorization, X-Test-Role, X-Mock-Role, X-Tenant-ID",
		AllowMethods:     "GET, POST, HEAD, PUT, DELETE, PATCH, OPTIONS",
		AllowCredentials: true,
	}))

	// Ensure COOKIE_SECRET is exactly 32 bytes for AES-256
	cookieSecret := getEnv("COOKIE_SECRET", "secret-key-must-be-32-bytes-long!")
	if len(cookieSecret) != 32 {
		slog.Warn("COOKIE_SECRET length is not 32 bytes. Adjusting...", "original_length", len(cookieSecret))
		if len(cookieSecret) > 32 {
			cookieSecret = cookieSecret[:32]
		} else {
			// Pad with '0' if too short
			cookieSecret = fmt.Sprintf("%-32s", cookieSecret)
		}
	}

	app.Use(encryptcookie.New(encryptcookie.Config{
		Key: cookieSecret,
	}))

	// [Security] Disable Swagger/ReDoc in Production
	if shouldEnableDocs(appEnv) {
		docsPrefix := getEnv("DOCS_BASE_PATH", "/api")
		registerDocsRoutes(app, "")
		if normalized := normalizeDocsPrefix(docsPrefix); normalized != "" {
			registerDocsRoutes(app, normalized)
		}
		slog.Info("📚 API Docs enabled", "swagger", "/docs", "redoc", "/redoc", "docs_prefix", docsPrefix)
	} else {
		slog.Info("🔒 API Docs disabled in production-like environment", "app_env", appEnv)
	}
	slog.Info("Client log policy configured",
		"app_env", appEnv,
		"client_debug_enabled", clientDebugEnabled,
	)

	// Routes
	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Baron SSO Audit Backend Online")
	})

	app.Get("/health", func(c *fiber.Ctx) error {
		status := "ok"
		checks := make(map[string]string)

		// Check ClickHouse
		if auditRepo != nil {
			if err := auditRepo.Ping(c.Context()); err != nil {
				checks["clickhouse"] = "error: " + err.Error()
				status = "error"
			} else {
				checks["clickhouse"] = "ok"
			}
		} else {
			checks["clickhouse"] = "not_initialized"
			status = "degraded"
		}

		// Check Redis
		if redisService != nil {
			if err := redisService.Ping(c.Context()); err != nil {
				checks["redis"] = "error: " + err.Error()
				status = "error"
			} else {
				checks["redis"] = "ok"
			}
		} else {
			checks["redis"] = "not_initialized"
			status = "degraded"
		}

		// Check Oathkeeper
		if oathkeeperProbe != nil {
			snapshot := oathkeeperProbe.Snapshot()
			switch snapshot.Status {
			case "ok":
				checks["oathkeeper"] = "ok"
			case "":
				checks["oathkeeper"] = "unknown"
				if status != "error" {
					status = "degraded"
				}
			default:
				if snapshot.Error == "" {
					checks["oathkeeper"] = "error"
				} else {
					checks["oathkeeper"] = "error: " + snapshot.Error
				}
				status = "error"
			}
		} else {
			checks["oathkeeper"] = "disabled"
			if status != "error" {
				status = "degraded"
			}
		}

		if status == "error" {
			return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
				"status": status,
				"checks": checks,
			})
		}

		return c.JSON(fiber.Map{
			"status": status,
			"checks": checks,
		})
	})
	rpManifestHandler := handler.NewRPManifestHandler()
	app.Get("/.well-known/baron-rp-manifest", rpManifestHandler.GetHTML)
	app.Get("/.well-known/baron-rp-manifest.json", rpManifestHandler.GetJSON)
	app.Get("/.well-known/baron-rp-manifest.schema.json", rpManifestHandler.GetSchema)

	// API Group
	api := app.Group("/api/v1")

	workerCount, _ := strconv.Atoi(getEnv("AUDIT_WORKER_COUNT", "5"))
	queueSize, _ := strconv.Atoi(getEnv("AUDIT_QUEUE_SIZE", "2000"))

	api.Use(middleware.AuditMiddleware(middleware.AuditConfig{
		Repo: auditRepo,
		ExcludePaths: map[string]struct{}{
			"/api/v1/audit":               {},
			"/api/v1/audit/auth/timeline": {},
			"/api/v1/client-log":          {},
			"/api/v1/dev/audit-logs":      {},
		},
		BodyDump:    true,
		WorkerCount: workerCount,
		QueueSize:   queueSize,
	}))
	api.Post("/audit", auditHandler.CreateLog)
	api.Get("/audit", auditHandler.ListLogs)
	api.Get("/audit/auth/timeline", authHandler.GetAuthTimeline)

	// [New] Shared Link Public API (No Auth required)
	api.Get("/public/orgchart", tenantHandler.GetPublicOrgChart)

	// Public Tenant Registration
	api.Post("/tenants/registration", tenantHandler.RegisterTenantPublic)
	api.Get("/admin/worksmobile/oauth/callback", worksmobileHandler.OAuthCallback)

	integrationsAPI := api.Group("/integrations")
	integrationsAPI.Use(middleware.ApiKeyAuth(middleware.ApiKeyAuthConfig{DB: db}))
	integrationsAPI.Get("/org-context", tenantHandler.GetOrgContext)

	// Tenant Context Middleware (identifies tenant from Host header)
	api.Use(middleware.TenantContextMiddleware(middleware.TenantContextConfig{
		TenantService: tenantService,
	}))

	// Auth Proxy Routes
	auth := api.Group("/auth")
	auth.Post("/enchanted-link/init", authHandler.InitEnchantedLink)
	auth.Post("/enchanted-link/poll", authHandler.PollEnchantedLink)
	auth.Post("/magic-link/verify", authHandler.VerifyMagicLink)
	auth.Post("/login/code/verify", authHandler.VerifyLoginCode)
	auth.Post("/login/code/verify-short", authHandler.VerifyLoginShortCode)
	auth.Post("/password/login", authHandler.PasswordLogin)
	auth.Post("/headless/password/login", authHandler.HeadlessPasswordLogin)
	auth.Post("/headless/link/init", authHandler.HeadlessLinkInit)
	auth.Post("/headless/link/poll", authHandler.HeadlessLinkPoll)
	auth.Get("/tenant-info", authHandler.GetTenantInfo)
	auth.Get("/consent", authHandler.GetConsentRequest)
	auth.Post("/consent/accept", authHandler.AcceptConsentRequest)
	auth.Post("/consent/reject", authHandler.RejectConsentRequest)

	auth.Post("/oidc/login/accept", authHandler.AcceptOidcLoginRequest)

	auth.Post("/password/reset/initiate", authHandler.InitiatePasswordReset)
	// [Changed] Use Interstitial Page for GET to prevent Scanner consumption
	auth.Get("/password/reset/verify", authHandler.VerifyPasswordResetPage)
	auth.Get("/password/reset/v/:token", authHandler.VerifyPasswordResetPage)
	auth.Get("/password/reset/ve", authHandler.VerifyPasswordResetPage)
	// [Added] Use POST for actual verification triggered by the user
	auth.Post("/password/reset/verify", authHandler.ProcessPasswordResetToken)
	auth.Post("/password/reset/v/:token", authHandler.ProcessPasswordResetToken)
	auth.Post("/password/reset/ve", authHandler.ProcessPasswordResetToken)
	auth.Post("/password/reset/complete", authHandler.CompletePasswordReset)
	auth.Get("/password/policy", authHandler.GetPasswordPolicy)
	auth.Post("/sms", authHandler.SendSms)
	auth.Post("/verify-sms", authHandler.VerifySms)
	auth.Post("/qr/init", authHandler.InitQRLogin)
	auth.Post("/qr/poll", authHandler.PollQRLogin)
	auth.Post("/qr/approve", authHandler.ScanQRLogin)
	auth.Get("/backchannel/jwks.json", authHandler.GetBackchannelLogoutJWKS)

	// Signup Routes
	signup := auth.Group("/signup")
	signup.Get("/tenants", authHandler.GetActiveTenants)
	signup.Post("/check-email", authHandler.CheckEmail)
	signup.Post("/check-login-id", authHandler.CheckLoginID)
	signup.Post("/send-email-code", authHandler.SendSignupEmailCode)
	signup.Post("/send-sms-code", authHandler.SendSignupSmsCode)
	signup.Post("/verify-code", authHandler.VerifySignupCode)
	signup.Post("/", authHandler.Signup)

	// User Routes (My Page)
	user := api.Group("/user")
	user.Get("/me", authHandler.GetMe)
	user.Put("/me", authHandler.UpdateMe)
	user.Post("/me/password", authHandler.ChangeMyPassword)
	user.Post("/me/send-code", authHandler.SendUpdateCode)
	user.Post("/me/verify-code", authHandler.VerifyUpdateCode)
	user.Get("/sessions", authHandler.ListMySessions)
	user.Delete("/sessions/:id", authHandler.DeleteMySession)
	user.Get("/rp/linked", authHandler.ListLinkedRps)
	user.Get("/rp/history", authHandler.ListRpHistory)
	user.Delete("/rp/linked/:id", authHandler.RevokeLinkedRp)

	// Admin Routes
	admin := api.Group("/admin")
	admin.Use(middleware.ApiKeyAuth(middleware.ApiKeyAuthConfig{DB: db})) // API Key 인증 추가

	// RBAC Middleware Instances
	requireSuperAdmin := middleware.RequireRole(middleware.RBACConfig{
		AllowedRoles: []string{domain.RoleSuperAdmin},
		AuthHandler:  authHandler,
		KetoService:  ketoService,
	})
	requireAdmin := requireSuperAdmin // Simplified: only super_admin can access admin management routes
	requireAnyUser := middleware.RequireRole(middleware.RBACConfig{
		AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleUser},
		AuthHandler:  authHandler,
		KetoService:  ketoService,
	})

	admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능)
	admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats)
	admin.Get("/integrity", requireSuperAdmin, adminHandler.GetDataIntegrity)
	admin.Get("/integrity/orphan-user-login-ids", requireSuperAdmin, adminHandler.ListOrphanUserLoginIDs)
	admin.Delete("/integrity/orphan-user-login-ids", requireSuperAdmin, adminHandler.DeleteOrphanUserLoginIDs)
	admin.Get("/projections/users", requireSuperAdmin, adminHandler.GetUserProjectionStatus)
	admin.Get("/ory/ssot", requireSuperAdmin, adminHandler.GetOrySSOTSystemStatus)
	admin.Post("/ory/ssot/identity-cache/flush", requireSuperAdmin, adminHandler.FlushIdentityCache)
	admin.Get("/rp-usage/daily", requireAdmin, adminHandler.GetRPUsageDaily)
	admin.Get("/global-custom-claims", requireSuperAdmin, adminHandler.GetGlobalCustomClaimDefinitions)
	admin.Put("/global-custom-claims", requireSuperAdmin, adminHandler.UpdateGlobalCustomClaimDefinitions)

	// Tenant Management (Mixed roles, handler filters results)
	admin.Get("/tenants", requireAnyUser, tenantHandler.ListTenants)
	admin.Get("/orgchart/snapshot", requireAnyUser, tenantHandler.GetOrgChartSnapshot)
	admin.Get("/tenants/export", requireSuperAdmin, tenantHandler.ExportTenantsCSV)
	admin.Post("/tenants/import", requireSuperAdmin, tenantHandler.ImportTenantsCSV)
	admin.Post("/tenants", requireSuperAdmin, tenantHandler.CreateTenant)

	// [New] Shared Link Management
	admin.Post("/tenants/:id/share-links", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.CreateShareLink)
	admin.Get("/tenants/:id/share-links", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), tenantHandler.ListShareLinks)
	admin.Delete("/share-links/:id", requireAdmin, tenantHandler.DeleteShareLink)

	admin.Delete("/tenants/bulk", requireSuperAdmin, tenantHandler.DeleteTenantsBulk)
	admin.Post("/tenants/:id/approve", requireSuperAdmin, tenantHandler.ApproveTenant)
	admin.Get("/tenants/:id", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), tenantHandler.GetTenant)
	admin.Put("/tenants/:id", requireSuperAdmin, tenantHandler.UpdateTenant)
	admin.Delete("/tenants/:id", requireSuperAdmin, tenantHandler.DeleteTenant)
	admin.Get("/tenants/:id/admins", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.ListAdmins)
	admin.Post("/tenants/:id/admins/:userId", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.AddAdmin)
	admin.Delete("/tenants/:id/admins/:userId", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.RemoveAdmin)
	admin.Get("/tenants/:id/owners", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.ListOwners)
	admin.Post("/tenants/:id/owners/:userId", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.AddOwner)
	admin.Delete("/tenants/:id/owners/:userId", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), tenantHandler.RemoveOwner)

	admin.Get("/tenants/:tenantId/worksmobile", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.GetOverview)
	admin.Get("/tenants/:tenantId/worksmobile/comparison", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.GetComparison)
	admin.Get("/tenants/:tenantId/worksmobile/credential-batches", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.ListCredentialBatches)
	admin.Delete("/tenants/:tenantId/worksmobile/credential-batches/:batchId/passwords", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.DeleteCredentialBatchPasswords)
	admin.Get("/tenants/:tenantId/worksmobile/initial-passwords.csv", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.DownloadInitialPasswordsCSV)
	admin.Post("/tenants/:tenantId/worksmobile/backfill/dry-run", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.BackfillDryRun)
	admin.Post("/tenants/:tenantId/worksmobile/orgunits/:orgUnitId/sync", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.SyncOrgUnit)
	admin.Post("/tenants/:tenantId/worksmobile/orgunits/:orgUnitId/delete", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.DeleteOrgUnit)
	admin.Post("/tenants/:tenantId/worksmobile/users/:userId/sync", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.SyncUser)
	admin.Post("/tenants/:tenantId/worksmobile/users/:userId/password/reset", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.ResetUserPassword)
	admin.Post("/tenants/:tenantId/worksmobile/jobs/:jobId/retry", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.RetryJob)
	admin.Delete("/tenants/:tenantId/worksmobile/jobs/pending", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), worksmobileHandler.DeletePendingJobs)

	// Organization & Org-Chart Management (Tenant Admin/Super Admin)
	org := admin.Group("/tenants/:tenantId/organization")
	org.Get("/", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), userGroupHandler.List)
	org.Post("/", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.Create)
	org.Get("/:id", userGroupHandler.Get)
	org.Put("/:id", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.Update)
	org.Delete("/:id", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.Delete)
	org.Post("/:id/members", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.AddMember)
	org.Delete("/:id/members/:userId", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.RemoveMember)
	org.Get("/:id/roles", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), userGroupHandler.ListRoles)
	org.Post("/:id/roles", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.AssignRole)
	org.Delete("/:id/roles/:tenantId/:relation", middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "manage"), userGroupHandler.RemoveRole)

	// Relying Party Management (Global List)
	admin.Get("/relying-parties", requireAdmin, relyingPartyHandler.ListAll)

	// Relying Party Management (Tenant Context)
	admin.Post("/tenants/:tenantId/relying-parties",
		requireAdmin,
		middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "grant_dev_permissions"),
		relyingPartyHandler.Create)

	admin.Get("/tenants/:tenantId/relying-parties",
		requireAdmin,
		middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view_dev_console"),
		relyingPartyHandler.List)

	admin.Get("/relying-parties/:id",
		requireAdmin,
		middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "RelyingParty", "view"),
		relyingPartyHandler.Get)

	admin.Put("/relying-parties/:id",
		requireAdmin,
		middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "RelyingParty", "edit_config"),
		relyingPartyHandler.Update)

	admin.Delete("/relying-parties/:id",
		requireAdmin,
		middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "RelyingParty", "manage"),
		relyingPartyHandler.Delete)

	// Admin User Management
	admin.Get("/users", requireAnyUser, userHandler.ListUsers)
	admin.Get("/users/export", requireAdmin, userHandler.ExportUsersCSV)
	admin.Post("/users", requireAdmin, userHandler.CreateUser)
	admin.Post("/users/bulk", requireAdmin, userHandler.BulkCreateUsers)
	admin.Put("/users/bulk", requireAdmin, userHandler.BulkUpdateUsers)
	admin.Delete("/users/bulk", requireAdmin, userHandler.BulkDeleteUsers)
	admin.Get("/users/:id", requireAdmin, userHandler.GetUser)
	admin.Get("/users/:id/rp-history", requireAdmin, userHandler.GetUserRpHistory)
	admin.Put("/users/:id", requireAdmin, userHandler.UpdateUser)
	admin.Delete("/users/:id", requireAdmin, userHandler.DeleteUser)

	// API Key Management (M2M) - Super Admin Only
	admin.Get("/api-keys", requireSuperAdmin, apiKeyHandler.ListApiKeys)
	admin.Post("/api-keys", requireSuperAdmin, apiKeyHandler.CreateApiKey)
	admin.Patch("/api-keys/:id", requireSuperAdmin, apiKeyHandler.UpdateApiKey)
	admin.Post("/api-keys/:id/secret/rotate", requireSuperAdmin, apiKeyHandler.RotateApiKeySecret)
	admin.Delete("/api-keys/:id", requireSuperAdmin, apiKeyHandler.DeleteApiKey)

	// 개발자 포털 라우트 (RP/Consent 관리 및 IdP 설정)
	dev := api.Group("/dev")
	dev.Get("/stats", devHandler.GetStats)
	dev.Get("/my-tenants", devHandler.ListMyTenants)
	dev.Get("/users", devHandler.SearchUsers)
	dev.Get("/clients", devHandler.ListClients)
	dev.Post("/clients", devHandler.CreateClient)
	dev.Get("/clients/:id/users/:userId/metadata", devHandler.GetRPUserMetadata)
	dev.Put("/clients/:id/users/:userId/metadata", devHandler.UpsertRPUserMetadata)
	dev.Get("/clients/:id", devHandler.GetClient)
	dev.Get("/clients/:id/relations", devHandler.ListClientRelations)
	dev.Post("/clients/:id/relations", devHandler.AddClientRelation)
	dev.Delete("/clients/:id/relations", devHandler.RemoveClientRelation)
	dev.Put("/clients/:id", devHandler.UpdateClient)
	dev.Post("/clients/:id/headless-jwks/refresh", devHandler.RefreshHeadlessJWKSCache)
	dev.Delete("/clients/:id/headless-jwks/cache", devHandler.RevokeHeadlessJWKSCache)
	dev.Post("/clients/:id/secret/rotate", devHandler.RotateClientSecret)
	dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
	dev.Delete("/clients/:id", devHandler.DeleteClient)
	dev.Get("/consents", devHandler.ListConsents)
	dev.Delete("/consents", devHandler.RevokeConsents)
	dev.Get("/audit-logs", devHandler.ListAuditLogs)
	dev.Get("/rp-usage/daily", devHandler.GetRPUsageDaily)

	// [New] Developer Registration Flow
	dev.Post("/developer-request", devHandler.RequestDeveloperAccess)
	dev.Get("/developer-request", devHandler.GetDeveloperRequestStatus)
	dev.Get("/developer-request/status", devHandler.GetDeveloperRequestStatus)
	dev.Get("/developer-request/list", devHandler.ListDeveloperRequests)
	dev.Post("/developer-request/:id/approve", devHandler.ApproveDeveloperRequest)
	dev.Post("/developer-request/:id/reject", devHandler.RejectDeveloperRequest)
	dev.Post("/developer-request/:id/cancel-approval", devHandler.CancelDeveloperRequestApproval)

	// Webhook for Kratos courier (HTTP delivery)
	auth.Post("/webhooks/kratos-courier", authHandler.HandleKratosCourierRelay)

	// Client Logging Route (Standardized & Flattened)
	api.Post("/client-log", func(c *fiber.Ctx) error {
		type LogReq struct {
			Level   string         `json:"level"`
			Message string         `json:"message"`
			Data    map[string]any `json:"data,omitempty"`
		}
		var req LogReq
		if err := c.BodyParser(&req); err != nil {
			return c.SendStatus(fiber.StatusBadRequest)
		}
		if !logger.ShouldAcceptClientLog(appEnv, clientLogDebugFlag, req.Level) {
			return c.SendStatus(fiber.StatusOK)
		}
		level := logger.NormalizeClientLogLevel(req.Level)
		if level == slog.LevelInfo && logger.ShouldFilterNoisyClientInfo(appEnv, clientLogDebugFlag, req.Message) {
			return c.SendStatus(fiber.StatusOK)
		}

		// Prepare attributes for flattening
		attrs := []any{
			slog.String("source", "client"),
		}
		sanitizedData := logger.SanitizeClientLogData(req.Data)
		for k, v := range sanitizedData {
			// Skip svc if it's already set by the global logger to avoid confusion,
			// or keep it as client_svc
			if k == "svc" {
				attrs = append(attrs, slog.Any("client_svc", v))
			} else {
				attrs = append(attrs, slog.Any(k, v))
			}
		}
		slog.Log(c.Context(), level, logger.SanitizeClientLogMessage(req.Message), attrs...)
		return c.SendStatus(fiber.StatusOK)
	})

	// Start Server
	port := getEnv("BACKEND_PORT", "3000")
	slog.Info("Server listening", "port", port)
	fmt.Println("============================================================")
	if err := app.Listen(":" + port); err != nil {
		slog.Error("Server failed to start", "error", err)
		os.Exit(1)
	}
}

func configureWorksmobileClientFromEnv(client *service.WorksmobileHTTPClient) {
	if client == nil {
		return
	}
	client.BaseURL = strings.TrimSpace(getEnv("WORKS_ADMIN_API_BASE_URL", ""))
}
