package main

import (
	"context"
	"encoding/json"
	"expvar"
	"flag"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"github.com/ez-api/ez-api/docs"
	"github.com/ez-api/ez-api/internal/api"
	"github.com/ez-api/ez-api/internal/config"
	"github.com/ez-api/ez-api/internal/cron"
	"github.com/ez-api/ez-api/internal/middleware"
	"github.com/ez-api/ez-api/internal/migrate"
	"github.com/ez-api/ez-api/internal/model"
	"github.com/ez-api/ez-api/internal/service"
	"github.com/ez-api/foundation/logging"
	"github.com/ez-api/foundation/scheduler"
	"github.com/gin-gonic/gin"
	"github.com/redis/go-redis/v9"
	swaggerFiles "github.com/swaggo/files"
	ginSwagger "github.com/swaggo/gin-swagger"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

// @title           EZ-API Control Plane
// @version         0.0.1
// @description     Management API for EZ-API Gateway system.
// @termsOfService  http://swagger.io/terms/

// @contact.name    API Support
// @contact.url     http://www.swagger.io/support
// @contact.email   support@swagger.io

// @license.name    Apache 2.0
// @license.url     http://www.apache.org/licenses/LICENSE-2.0.html

// @host            localhost:8080
// @BasePath        /

// @securityDefinitions.apikey AdminAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and the admin token. Example: Bearer admin123

// @securityDefinitions.apikey MasterAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and the master key. Example: Bearer sk-xxx

func fatal(logger *slog.Logger, msg string, args ...any) {
	logger.Error(msg, args...)
	os.Exit(1)
}

func isOriginAllowed(allowed []string, origin string) bool {
	if len(allowed) == 0 {
		return false
	}
	for _, item := range allowed {
		if item == "*" {
			return true
		}
		if strings.EqualFold(strings.TrimSpace(item), strings.TrimSpace(origin)) {
			return true
		}
	}
	return false
}

func main() {
	// Handle --version flag before any initialization
	if len(os.Args) > 1 && (os.Args[1] == "--version" || os.Args[1] == "-v") {
		fmt.Printf("ez-api %s\n", api.Version)
		os.Exit(0)
	}

	logger, _ := logging.New(logging.Options{Service: "ez-api"})
	appCtx, appCancel := context.WithCancel(context.Background())
	defer appCancel()
	if len(os.Args) > 1 && os.Args[1] == "import" {
		code := runImport(logger, os.Args[2:])
		os.Exit(code)
	}

	// 1. Load Configuration
	cfg, err := config.Load()
	if err != nil {
		fatal(logger, "failed to load config", "err", err)
	}

	// 2. Initialize Redis Client
	rdb := redis.NewClient(&redis.Options{
		Addr:     cfg.Redis.Addr,
		Password: cfg.Redis.Password,
		DB:       cfg.Redis.DB,
	})

	// Verify Redis connection
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := rdb.Ping(ctx).Err(); err != nil {
		fatal(logger, "failed to connect to redis", "err", err)
	}
	logger.Info("connected to redis successfully")

	// 3. Initialize GORM (PostgreSQL)
	db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{})
	if err != nil {
		fatal(logger, "failed to connect to postgresql", "err", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		fatal(logger, "failed to get generic database object", "err", err)
	}
	// Verify DB connection
	if err := sqlDB.Ping(); err != nil {
		fatal(logger, "failed to ping postgresql", "err", err)
	}
	logger.Info("connected to postgresql successfully")

	logDB := db
	if cfg.Log.DSN != "" {
		logDB, err = gorm.Open(postgres.Open(cfg.Log.DSN), &gorm.Config{})
		if err != nil {
			fatal(logger, "failed to connect to log database", "err", err)
		}
		sqlLogDB, err := logDB.DB()
		if err != nil {
			fatal(logger, "failed to get log database object", "err", err)
		}
		if err := sqlLogDB.Ping(); err != nil {
			fatal(logger, "failed to ping log database", "err", err)
		}
		logger.Info("connected to log database successfully")
	}

	// Auto Migrate
	if logDB != db {
		if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.ProviderGroup{}, &model.APIKey{}, &model.Model{}, &model.Binding{}, &model.Namespace{}, &model.OperationLog{}, &model.SyncOutbox{}, &model.Alert{}, &model.AlertThresholdConfig{}); err != nil {
			fatal(logger, "failed to auto migrate", "err", err)
		}
		if err := logDB.AutoMigrate(&model.LogRecord{}); err != nil {
			fatal(logger, "failed to auto migrate log tables", "err", err)
		}
		if err := service.EnsureLogIndexes(logDB); err != nil {
			fatal(logger, "failed to ensure log indexes", "err", err)
		}
	} else {
		if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.ProviderGroup{}, &model.APIKey{}, &model.Model{}, &model.Binding{}, &model.Namespace{}, &model.OperationLog{}, &model.LogRecord{}, &model.SyncOutbox{}, &model.Alert{}, &model.AlertThresholdConfig{}); err != nil {
			fatal(logger, "failed to auto migrate", "err", err)
		}
		if err := service.EnsureLogIndexes(db); err != nil {
			fatal(logger, "failed to ensure log indexes", "err", err)
		}
	}

	// 4. Setup Services and Handlers
	syncService := service.NewSyncService(rdb)
	var outboxService *service.SyncOutboxService
	if cfg.SyncOutbox.Enabled {
		outboxCfg := service.SyncOutboxConfig{
			Enabled:    cfg.SyncOutbox.Enabled,
			Interval:   time.Duration(cfg.SyncOutbox.IntervalSeconds) * time.Second,
			BatchSize:  cfg.SyncOutbox.BatchSize,
			MaxRetries: cfg.SyncOutbox.MaxRetries,
		}
		outboxService = service.NewSyncOutboxService(db, syncService, outboxCfg, logger)
		syncService.SetOutbox(outboxService)
	}
	logPartitioner := service.NewLogPartitioner(logDB, cfg.Log.Partitioning)
	if logPartitioner.Enabled() {
		if _, err := logPartitioner.EnsurePartitionFor(time.Now().UTC()); err != nil {
			fatal(logger, "failed to ensure log partition", "err", err)
		}
	}
	logWriter := service.NewLogWriter(logDB, cfg.Log.QueueCapacity, cfg.Log.BatchSize, cfg.Log.FlushInterval, logPartitioner)
	logCtx, cancelLogs := context.WithCancel(context.Background())
	defer cancelLogs()
	logWriter.Start(logCtx)

	// Initialize cron jobs
	quotaResetter := cron.NewQuotaResetter(db, syncService)
	logCleaner := cron.NewLogCleaner(logDB, rdb, cfg.Log.RetentionDays, int64(cfg.Log.MaxRecords), logPartitioner)
	tokenRefresher := cron.NewTokenRefresher(
		db,
		rdb,
		syncService,
		time.Duration(cfg.TokenRefresh.RefreshSkewSeconds)*time.Second,
		cfg.TokenRefresh.BatchSize,
		cfg.TokenRefresh.MaxRetries,
	)
	alertDetectorConfig := cron.DefaultAlertDetectorConfig()
	alertDetector := cron.NewAlertDetector(db, logDB, rdb, service.NewStatsService(rdb), alertDetectorConfig, logger)

	// Setup scheduler (jobs are added incrementally, Start() called after all services initialized)
	sched := scheduler.New(
		scheduler.WithLogger(logger),
		scheduler.WithSkipIfRunning(),
		scheduler.WithBaseContext(appCtx),
	)
	sched.Every("quota-reset", time.Duration(cfg.Quota.ResetIntervalSeconds)*time.Second, quotaResetter.RunOnce)
	sched.Every("log-cleanup", time.Hour, logCleaner.RunOnce)
	sched.Every("token-refresh", time.Duration(cfg.TokenRefresh.IntervalSeconds)*time.Second, tokenRefresher.RunOnce)
	sched.Every("alert-detection", time.Minute, alertDetector.RunOnce)
	if outboxService != nil && outboxService.Enabled() {
		sched.Every("sync-outbox", outboxService.Interval(), outboxService.RunOnce)
	}

	adminService, err := service.NewAdminService()
	if err != nil {
		fatal(logger, "failed to create admin service", "err", err)
	}
	masterService := service.NewMasterService(db)
	statsService := service.NewStatsService(rdb)
	healthService := service.NewHealthCheckService(db, rdb)
	statusHandler := api.NewStatusHandler(healthService)

	handler := api.NewHandler(db, logDB, syncService, logWriter, rdb, logPartitioner)
	adminHandler := api.NewAdminHandler(db, logDB, masterService, syncService, statsService, logPartitioner)
	masterHandler := api.NewMasterHandler(db, logDB, masterService, syncService, statsService, logPartitioner)
	dashboardHandler := api.NewDashboardHandler(db, logDB, statsService, logPartitioner)
	alertHandler := api.NewAlertHandler(db)
	internalHandler := api.NewInternalHandler(db)
	featureHandler := api.NewFeatureHandler(rdb)
	authHandler := api.NewAuthHandler(db, rdb, adminService, masterService)
	modelRegistryService := service.NewModelRegistryService(db, rdb, service.ModelRegistryConfig{
		Enabled:             cfg.ModelRegistry.Enabled,
		RefreshEvery:        time.Duration(cfg.ModelRegistry.RefreshSeconds) * time.Second,
		ModelsDevBaseURL:    cfg.ModelRegistry.ModelsDevBaseURL,
		ModelsDevAPIBaseURL: cfg.ModelRegistry.ModelsDevAPIBaseURL,
		ModelsDevRef:        cfg.ModelRegistry.ModelsDevRef,
		CacheDir:            cfg.ModelRegistry.CacheDir,
		Timeout:             time.Duration(cfg.ModelRegistry.TimeoutSeconds) * time.Second,
	})
	modelRegistryHandler := api.NewModelRegistryHandler(modelRegistryService)

	// 4.1 Prime Redis snapshots so DP can start with data
	if err := syncService.SyncAll(db); err != nil {
		logger.Warn("initial sync warning", "err", err)
	}
	// Initial model registry refresh before scheduler starts
	if modelRegistryService.Enabled() {
		modelRegistryService.RunOnce(context.Background())
		sched.Every("model-registry-refresh", modelRegistryService.RefreshEvery(), modelRegistryService.RunOnce)
	}
	sched.Start()

	// 5. Setup Gin Router
	r := gin.Default()
	r.Use(middleware.RequestID())

	allowedOrigins := cfg.CORS.AllowOrigins
	allowAllOrigins := isOriginAllowed(allowedOrigins, "*")

	// CORS Middleware
	r.Use(func(c *gin.Context) {
		origin := c.Request.Header.Get("Origin")
		if allowAllOrigins {
			c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
		} else if origin != "" && isOriginAllowed(allowedOrigins, origin) {
			c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
			c.Writer.Header().Add("Vary", "Origin")
		}
		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")

		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}

		c.Next()
	})

	// 动态设置 Swagger Host
	if cfg.Server.SwaggerHost != "" {
		docs.SwaggerInfo.Host = cfg.Server.SwaggerHost
	} else {
		docs.SwaggerInfo.Host = "" // 使用相对路径
	}

	// Health Check Endpoint
	r.GET("/health", func(c *gin.Context) {
		status := healthService.Check(c.Request.Context())
		httpStatus := http.StatusOK
		if status.Status == "down" {
			httpStatus = http.StatusServiceUnavailable
		}
		c.JSON(httpStatus, status)
	})

	// Public Status Endpoints
	r.GET("/status", statusHandler.Status)
	r.GET("/about", statusHandler.About)

	// Swagger Documentation
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

	// API Routes
	// Internal Routes
	internalGroup := r.Group("/internal")
	internalGroup.Use(middleware.InternalAuthMiddleware(cfg.Internal.StatsToken))
	{
		internalGroup.POST("/stats/flush", internalHandler.FlushStats)
		internalGroup.POST("/apikey-stats/flush", internalHandler.FlushAPIKeyStats)
		internalGroup.POST("/alerts/report", internalHandler.ReportAlerts)
		internalGroup.GET("/metrics", gin.WrapH(expvar.Handler()))
	}

	// Admin Routes
	adminGroup := r.Group("/admin")
	adminGroup.Use(middleware.AdminAuthMiddleware(adminService))
	adminGroup.Use(middleware.OperationLogMiddleware(db))
	{
		adminGroup.POST("/masters", adminHandler.CreateMaster)
		adminGroup.GET("/masters", adminHandler.ListMasters)
		adminGroup.GET("/masters/:id", adminHandler.GetMaster)
		adminGroup.GET("/masters/:id/realtime", adminHandler.GetMasterRealtime)
		adminGroup.PUT("/masters/:id", adminHandler.UpdateMaster)
		adminGroup.DELETE("/masters/:id", adminHandler.DeleteMaster)
		adminGroup.POST("/masters/batch", adminHandler.BatchMasters)
		adminGroup.POST("/masters/:id/manage", adminHandler.ManageMaster)
		adminGroup.POST("/masters/:id/keys", adminHandler.IssueChildKeyForMaster)
		adminGroup.GET("/masters/:id/access", handler.GetMasterAccess)
		adminGroup.PUT("/masters/:id/access", handler.UpdateMasterAccess)
		adminGroup.GET("/keys/:id/access", handler.GetKeyAccess)
		adminGroup.PUT("/keys/:id/access", handler.UpdateKeyAccess)
		adminGroup.GET("/operation-logs", adminHandler.ListOperationLogs)
		adminGroup.POST("/namespaces", handler.CreateNamespace)
		adminGroup.GET("/namespaces", handler.ListNamespaces)
		adminGroup.GET("/namespaces/:id", handler.GetNamespace)
		adminGroup.PUT("/namespaces/:id", handler.UpdateNamespace)
		adminGroup.DELETE("/namespaces/:id", handler.DeleteNamespace)
		adminGroup.GET("/features", featureHandler.ListFeatures)
		adminGroup.PUT("/features", featureHandler.UpdateFeatures)
		adminGroup.GET("/model-registry/status", modelRegistryHandler.GetStatus)
		adminGroup.POST("/model-registry/check", modelRegistryHandler.Check)
		adminGroup.POST("/model-registry/refresh", modelRegistryHandler.Refresh)
		adminGroup.POST("/model-registry/rollback", modelRegistryHandler.Rollback)
		// Other admin routes for managing providers, models, etc.
		adminGroup.POST("/provider-groups", handler.CreateProviderGroup)
		adminGroup.GET("/provider-groups", handler.ListProviderGroups)
		adminGroup.GET("/provider-groups/:id", handler.GetProviderGroup)
		adminGroup.PUT("/provider-groups/:id", handler.UpdateProviderGroup)
		adminGroup.DELETE("/provider-groups/:id", handler.DeleteProviderGroup)
		adminGroup.POST("/api-keys", handler.CreateAPIKey)
		adminGroup.GET("/api-keys", handler.ListAPIKeys)
		adminGroup.GET("/api-keys/:id", handler.GetAPIKey)
		adminGroup.PUT("/api-keys/:id", handler.UpdateAPIKey)
		adminGroup.DELETE("/api-keys/:id", handler.DeleteAPIKey)
		adminGroup.POST("/api-keys/batch", handler.BatchAPIKeys)
		adminGroup.POST("/models", handler.CreateModel)
		adminGroup.GET("/models", handler.ListModels)
		adminGroup.PUT("/models/:id", handler.UpdateModel)
		adminGroup.DELETE("/models/:id", handler.DeleteModel)
		adminGroup.POST("/models/batch", handler.BatchModels)
		adminGroup.GET("/logs", handler.ListLogs)
		adminGroup.DELETE("/logs", handler.DeleteLogs)
		adminGroup.GET("/logs/stats", handler.LogStats)
		adminGroup.GET("/logs/stats/traffic-chart", handler.GetTrafficChart)
		adminGroup.GET("/logs/webhook", handler.GetLogWebhookConfig)
		adminGroup.PUT("/logs/webhook", handler.UpdateLogWebhookConfig)
		adminGroup.GET("/stats", adminHandler.GetAdminStats)
		adminGroup.GET("/realtime", adminHandler.GetAdminRealtime)
		adminGroup.GET("/dashboard/summary", dashboardHandler.GetSummary)
		adminGroup.GET("/apikey-stats/summary", adminHandler.GetAPIKeyStatsSummary)
		adminGroup.GET("/alerts", alertHandler.ListAlerts)
		adminGroup.POST("/alerts", alertHandler.CreateAlert)
		adminGroup.GET("/alerts/stats", alertHandler.GetAlertStats)
		adminGroup.GET("/alerts/thresholds", alertHandler.GetAlertThresholds)
		adminGroup.PUT("/alerts/thresholds", alertHandler.UpdateAlertThresholds)
		adminGroup.GET("/alerts/:id", alertHandler.GetAlert)
		adminGroup.POST("/alerts/:id/ack", alertHandler.AcknowledgeAlert)
		adminGroup.POST("/alerts/:id/resolve", alertHandler.ResolveAlert)
		adminGroup.DELETE("/alerts/:id", alertHandler.DismissAlert)
		adminGroup.POST("/bindings", handler.CreateBinding)
		adminGroup.GET("/bindings", handler.ListBindings)
		adminGroup.GET("/bindings/:id", handler.GetBinding)
		adminGroup.PUT("/bindings/:id", handler.UpdateBinding)
		adminGroup.DELETE("/bindings/:id", handler.DeleteBinding)
		adminGroup.POST("/bindings/batch", handler.BatchBindings)
		adminGroup.POST("/sync/snapshot", handler.SyncSnapshot)
	}

	// Master Routes
	masterGroup := r.Group("/v1")
	masterGroup.Use(middleware.MasterAuthMiddleware(masterService))
	{
		masterGroup.GET("/self", masterHandler.GetSelf)
		masterGroup.POST("/tokens", masterHandler.IssueChildKey)
		masterGroup.GET("/tokens", masterHandler.ListTokens)
		masterGroup.GET("/tokens/:id", masterHandler.GetToken)
		masterGroup.PUT("/tokens/:id", masterHandler.UpdateToken)
		masterGroup.DELETE("/tokens/:id", masterHandler.DeleteToken)
		masterGroup.GET("/logs", masterHandler.ListSelfLogs)
		masterGroup.GET("/logs/stats", masterHandler.GetSelfLogStats)
		masterGroup.GET("/realtime", masterHandler.GetSelfRealtime)
		masterGroup.GET("/stats", masterHandler.GetSelfStats)
	}

	// Auth Routes (public, no middleware - self-validates token)
	authGroup := r.Group("/auth")
	{
		authGroup.GET("/whoami", authHandler.Whoami)
	}

	// Public/General Routes (if any)
	r.POST("/logs", handler.IngestLog)

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

	// 6. Start Server with Graceful Shutdown
	go func() {
		logger.Info("starting ez-api", "port", cfg.Server.Port)
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fatal(logger, "server failed", "err", err)
		}
	}()

	// Wait for interrupt signal
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	logger.Info("shutting down server...")
	appCancel()
	sched.Stop()

	// Shutdown with timeout
	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(ctx); err != nil {
		fatal(logger, "server forced to shutdown", "err", err)
	}

	logger.Info("server exited properly")
}

func runImport(logger *slog.Logger, args []string) int {
	fs := flag.NewFlagSet("import", flag.ContinueOnError)
	fs.SetOutput(os.Stderr)

	var filePath string
	var dryRun bool
	var conflictPolicy string
	var includeBindings bool

	fs.StringVar(&filePath, "file", "", "Path to export JSON")
	fs.BoolVar(&dryRun, "dry-run", false, "Validate only, do not write to database")
	fs.StringVar(&conflictPolicy, "conflict", migrate.ConflictSkip, "Conflict policy: skip or overwrite")
	fs.BoolVar(&includeBindings, "include-bindings", false, "Import bindings from payload")

	if err := fs.Parse(args); err != nil {
		logger.Error("failed to parse flags", "err", err)
		return 2
	}
	if strings.TrimSpace(filePath) == "" {
		fmt.Fprintln(os.Stderr, "missing --file")
		return 2
	}
	conflictPolicy = strings.ToLower(strings.TrimSpace(conflictPolicy))
	if conflictPolicy != migrate.ConflictSkip && conflictPolicy != migrate.ConflictOverwrite {
		fmt.Fprintf(os.Stderr, "invalid --conflict value: %s\n", conflictPolicy)
		return 2
	}

	cfg, err := config.Load()
	if err != nil {
		logger.Error("failed to load config", "err", err)
		return 1
	}

	db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{})
	if err != nil {
		logger.Error("failed to connect to postgresql", "err", err)
		return 1
	}

	if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.ProviderGroup{}, &model.APIKey{}, &model.Model{}, &model.Binding{}, &model.Namespace{}, &model.SyncOutbox{}); err != nil {
		logger.Error("failed to auto migrate", "err", err)
		return 1
	}

	importer := migrate.NewImporter(db, migrate.ImportOptions{
		DryRun:          dryRun,
		ConflictPolicy:  conflictPolicy,
		IncludeBindings: includeBindings,
	})
	summary, err := importer.ImportFile(filePath)
	if err != nil {
		logger.Error("import failed", "err", err)
		return 1
	}

	payload, err := json.MarshalIndent(summary, "", "  ")
	if err != nil {
		logger.Error("failed to render import summary", "err", err)
		return 1
	}
	fmt.Fprintln(os.Stdout, string(payload))
	if dryRun {
		fmt.Fprintln(os.Stdout, "dry-run only: no data written")
	}
	return 0
}
