package main

import (
	"context"
	"database/sql"
	"errors"
	"flag"
	"log"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"hyapp-admin-server/internal/cache"
	"hyapp-admin-server/internal/config"
	"hyapp-admin-server/internal/integration/activityclient"
	"hyapp-admin-server/internal/integration/gameclient"
	"hyapp-admin-server/internal/integration/userclient"
	"hyapp-admin-server/internal/integration/walletclient"
	jobrunner "hyapp-admin-server/internal/job"
	"hyapp-admin-server/internal/migration"
	adminusermodule "hyapp-admin-server/internal/modules/adminuser"
	appconfigmodule "hyapp-admin-server/internal/modules/appconfig"
	appregistrymodule "hyapp-admin-server/internal/modules/appregistry"
	appusermodule "hyapp-admin-server/internal/modules/appuser"
	auditmodule "hyapp-admin-server/internal/modules/audit"
	authmodule "hyapp-admin-server/internal/modules/auth"
	coinledgermodule "hyapp-admin-server/internal/modules/coinledger"
	countryregionmodule "hyapp-admin-server/internal/modules/countryregion"
	dailytaskmodule "hyapp-admin-server/internal/modules/dailytask"
	dashboardmodule "hyapp-admin-server/internal/modules/dashboard"
	gamemanagementmodule "hyapp-admin-server/internal/modules/gamemanagement"
	healthmodule "hyapp-admin-server/internal/modules/health"
	hostorgmodule "hyapp-admin-server/internal/modules/hostorg"
	jobmodule "hyapp-admin-server/internal/modules/job"
	menumodule "hyapp-admin-server/internal/modules/menu"
	notificationmodule "hyapp-admin-server/internal/modules/notification"
	paymentmodule "hyapp-admin-server/internal/modules/payment"
	rbacmodule "hyapp-admin-server/internal/modules/rbac"
	registrationrewardmodule "hyapp-admin-server/internal/modules/registrationreward"
	resourcemodule "hyapp-admin-server/internal/modules/resource"
	roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
	searchmodule "hyapp-admin-server/internal/modules/search"
	uploadmodule "hyapp-admin-server/internal/modules/upload"
	"hyapp-admin-server/internal/platform/logging"
	"hyapp-admin-server/internal/platform/tencentcos"
	"hyapp-admin-server/internal/repository"
	"hyapp-admin-server/internal/router"
	"hyapp-admin-server/internal/service"

	mysqlDriver "github.com/go-sql-driver/mysql"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func main() {
	configPath := flag.String("config", "configs/config.yaml", "path to admin-server config")
	runBootstrap := flag.Bool("bootstrap", false, "run configured bootstrap seed before starting")
	flag.Parse()

	cfg, err := config.Load(*configPath)
	if err != nil {
		log.Fatal(err)
	}
	if err := logging.Init(cfg.ServiceName, cfg.Environment, cfg.NodeID, cfg.Log); err != nil {
		log.Fatal(err)
	}

	if err := ensureDatabase(cfg.MySQLDSN); err != nil {
		fatalRuntime("ensure_mysql_database_failed", err)
	}
	if err := ensureDatabase(cfg.WalletMySQLDSN); err != nil {
		fatalRuntime("ensure_wallet_mysql_database_failed", err)
	}
	if err := ensureDatabase(cfg.RoomMySQLDSN); err != nil {
		fatalRuntime("ensure_room_mysql_database_failed", err)
	}

	dbLogger := logger.New(log.New(logging.StdWriter(), "", 0), logger.Config{
		LogLevel:                  logger.Warn,
		IgnoreRecordNotFoundError: true,
	})
	db, err := gorm.Open(mysql.Open(cfg.MySQLDSN), &gorm.Config{
		Logger: dbLogger,
		NowFunc: func() time.Time {
			return time.Now().UTC()
		},
	})
	if err != nil {
		fatalRuntime("connect_mysql_failed", err)
	}
	sqlDB, err := db.DB()
	if err != nil {
		fatalRuntime("mysql_db_handle_failed", err)
	}
	if cfg.Migrations.Enabled {
		if err := migration.ApplyWithOptions(context.Background(), sqlDB, cfg.Migrations.Dir, migration.Options{
			AllowChecksumRepair: cfg.Migrations.AllowChecksumRepair,
		}); err != nil {
			fatalRuntime("apply_migrations_failed", err)
		}
	}
	redisClient, err := cache.NewRedis(context.Background(), cfg.Redis)
	if err != nil {
		fatalRuntime("connect_redis_failed", err)
	}
	if redisClient != nil {
		defer redisClient.Close()
	}

	userDB, err := sql.Open("mysql", cfg.UserMySQLDSN)
	if err != nil {
		fatalRuntime("connect_user_mysql_failed", err)
	}
	if err := userDB.PingContext(context.Background()); err != nil {
		fatalRuntime("ping_user_mysql_failed", err)
	}
	defer userDB.Close()

	walletDB, err := sql.Open("mysql", cfg.WalletMySQLDSN)
	if err != nil {
		fatalRuntime("connect_wallet_mysql_failed", err)
	}
	if err := walletDB.PingContext(context.Background()); err != nil {
		fatalRuntime("ping_wallet_mysql_failed", err)
	}
	defer walletDB.Close()

	roomDB, err := sql.Open("mysql", cfg.RoomMySQLDSN)
	if err != nil {
		fatalRuntime("connect_room_mysql_failed", err)
	}
	if err := roomDB.PingContext(context.Background()); err != nil {
		fatalRuntime("ping_room_mysql_failed", err)
	}
	defer roomDB.Close()

	store := repository.New(db)
	if cfg.MySQLAutoMigrate {
		if err := store.AutoMigrate(); err != nil {
			fatalRuntime("auto_migrate_failed", err)
		}
	}
	if *runBootstrap || cfg.Bootstrap.Enabled {
		if err := store.Seed(cfg); err != nil {
			fatalRuntime("seed_database_failed", err)
		}
	}

	userConn, err := grpc.Dial(cfg.UserService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fatalRuntime("connect_user_service_failed", err)
	}
	defer userConn.Close()

	walletConn, err := grpc.Dial(cfg.WalletService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fatalRuntime("connect_wallet_service_failed", err)
	}
	defer walletConn.Close()

	activityConn, err := grpc.Dial(cfg.ActivityService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fatalRuntime("connect_activity_service_failed", err)
	}
	defer activityConn.Close()

	gameConn, err := grpc.Dial(cfg.GameService.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fatalRuntime("connect_game_service_failed", err)
	}
	defer gameConn.Close()

	auth := service.NewAuthService(cfg.JWTSecret, cfg.AccessTokenTTL)
	var runner *jobrunner.Runner
	var jobStatus healthmodule.JobStatusProvider
	if cfg.Jobs.Enabled {
		runner = jobrunner.NewRunner(store, redisClient, cfg)
		runner.Start()
		defer runner.Close()
		jobStatus = runner
	}

	auditHandler := auditmodule.New(store)
	var objectUploader uploadmodule.ObjectUploader
	if cfg.TencentCOS.Enabled {
		uploader, err := tencentcos.NewUploader(tencentcos.Config{
			SecretID:  cfg.TencentCOS.SecretID,
			SecretKey: cfg.TencentCOS.SecretKey,
			Bucket:    cfg.TencentCOS.BucketName,
			Region:    cfg.TencentCOS.Region,
			AccessURL: cfg.TencentCOS.AccessURL,
		}, nil)
		if err != nil {
			fatalRuntime("create_tencent_cos_uploader_failed", err)
		}
		objectUploader = uploader
	}
	handlers := router.Handlers{
		Audit:              auditHandler,
		Auth:               authmodule.New(store, auth, auditHandler, cfg),
		AdminUser:          adminusermodule.New(store, cfg, auditHandler),
		AppConfig:          appconfigmodule.New(store, auditHandler),
		AppRegistry:        appregistrymodule.New(userDB),
		AppUser:            appusermodule.New(userclient.NewGRPC(userConn), activityclient.NewGRPC(activityConn), sqlDB, userDB, walletDB, cfg, auditHandler),
		CoinLedger:         coinledgermodule.New(userDB, walletDB),
		CountryRegion:      countryregionmodule.New(userclient.NewGRPC(userConn), userDB, cfg, auditHandler),
		DailyTask:          dailytaskmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
		Dashboard:          dashboardmodule.New(store),
		Game:               gamemanagementmodule.New(gameclient.NewGRPC(gameConn), auditHandler),
		Health:             healthmodule.New(store, redisClient, jobStatus),
		HostOrg:            hostorgmodule.New(userclient.NewGRPC(userConn), walletclient.NewGRPC(walletConn), userDB, walletDB, sqlDB, auditHandler),
		Job:                jobmodule.New(store, cfg, auditHandler),
		Menu:               menumodule.New(store, auditHandler),
		Notification:       notificationmodule.New(store, auditHandler),
		Payment:            paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
		RBAC:               rbacmodule.New(store, auditHandler),
		RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
		Resource:           resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, auditHandler),
		RoomAdmin:          roomadminmodule.New(roomDB, userDB, auditHandler),
		Search:             searchmodule.New(store),
		Upload:             uploadmodule.New(objectUploader, cfg.TencentCOS.ObjectPrefix, auditHandler),
	}
	engine := router.New(cfg, auth, handlers)

	server := &http.Server{
		Addr:              cfg.HTTPAddr,
		Handler:           engine,
		ReadHeaderTimeout: 5 * time.Second,
	}

	go func() {
		slog.Info("service_listening", "addr", cfg.HTTPAddr)
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			fatalRuntime("listen_failed", err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := server.Shutdown(ctx); err != nil {
		slog.Error("shutdown_failed", "error", err.Error())
	}
}

func fatalRuntime(msg string, err error) {
	slog.Error(msg, "error", err.Error())
	os.Exit(1)
}

func ensureDatabase(dsn string) error {
	parsed, err := mysqlDriver.ParseDSN(dsn)
	if err != nil {
		return err
	}
	if parsed.DBName == "" {
		return nil
	}

	dbName := parsed.DBName
	parsed.DBName = ""

	db, err := sql.Open("mysql", parsed.FormatDSN())
	if err != nil {
		return err
	}
	defer db.Close()

	escaped := strings.ReplaceAll(dbName, "`", "``")
	_, err = db.Exec("CREATE DATABASE IF NOT EXISTS `" + escaped + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
	return err
}
