package app

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"google.golang.org/grpc"
	"hyapp/pkg/dingtalkrobot"
	"hyapp/pkg/grpcclient"
	serviceapp "hyapp/pkg/servicekit/app"
	"hyapp/pkg/tencentcos"
	"hyapp/pkg/tencentim"
	"hyapp/pkg/userleaderboard"
	"hyapp/services/gateway-service/internal/appconfig"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/client"
	"hyapp/services/gateway-service/internal/config"
	"hyapp/services/gateway-service/internal/financewithdrawal"
	"hyapp/services/gateway-service/internal/healthcheck"
	httptransport "hyapp/services/gateway-service/internal/transport/http"
	"hyapp/services/gateway-service/internal/transport/http/giftlimit"
)

// App 装配 gateway 的 HTTP 入口。
type App struct {
	server        *http.Server
	listener      net.Listener
	roomConn      *grpc.ClientConn
	userConn      *grpc.ClientConn
	walletConn    *grpc.ClientConn
	activityConn  *grpc.ClientConn
	luckyGiftConn *grpc.ClientConn
	gameConn      *grpc.ClientConn
	appConfig     *appconfig.MySQLReader
	withdrawal    *financewithdrawal.MySQLWriter
	redisClose    func() error
	health        *healthcheck.State
	closeOnce     sync.Once
}

// New 初始化 gateway 应用。
func New(cfg config.Config) (*App, error) {
	if err := cfg.Normalize(); err != nil {
		return nil, err
	}

	grpcConfig := cfg.GRPCClient.Runtime()
	roomConn, err := grpcclient.Dial(cfg.RoomServiceAddr, grpcConfig)
	if err != nil {
		return nil, err
	}

	userConn, err := grpcclient.Dial(cfg.UserServiceAddr, grpcConfig)
	if err != nil {
		_ = roomConn.Close()
		return nil, err
	}

	walletConn, err := grpcclient.Dial(cfg.WalletServiceAddr, grpcConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		return nil, err
	}
	activityConn, err := grpcclient.Dial(cfg.ActivityServiceAddr, grpcConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		return nil, err
	}
	luckyGiftConn, err := grpcclient.Dial(cfg.LuckyGiftServiceAddr, grpcConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		return nil, err
	}
	gameConn, err := grpcclient.Dial(cfg.GameServiceAddr, grpcConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = luckyGiftConn.Close()
		return nil, err
	}

	var roomClient client.RoomClient = client.NewGRPCRoomClient(roomConn)
	var roomGuardClient client.RoomGuardClient = client.NewGRPCRoomGuardClient(roomConn)
	var roomQueryClient client.RoomQueryClient = client.NewGRPCRoomQueryClient(roomConn)
	var userClient client.UserAuthClient = client.NewGRPCUserAuthClient(userConn)
	var userIdentityClient client.UserIdentityClient = client.NewGRPCUserIdentityClient(userConn)
	var userProfileClient client.UserProfileClient = client.NewGRPCUserProfileClient(userConn)
	var userInviteClient client.UserInviteClient = client.NewGRPCUserInviteClient(userConn)
	var userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn)
	var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
	var userRegionClient client.UserRegionClient = client.NewGRPCUserRegionClient(userConn)
	var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
	var userHostAdminClient client.UserHostAdminClient = client.NewGRPCUserHostAdminClient(userConn)
	var userSocialClient client.UserSocialClient = client.NewGRPCUserSocialClient(userConn)
	var userCPClient client.UserCPClient = client.NewGRPCUserCPClient(userConn)
	var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
	var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
	var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
	var messageActionClient client.MessageActionConfirmClient = client.NewGRPCMessageActionConfirmClient(activityConn)
	var taskClient client.TaskClient = client.NewGRPCTaskClient(activityConn)
	var growthLevelClient client.GrowthLevelClient = client.NewGRPCGrowthLevelClient(activityConn)
	var achievementClient client.AchievementClient = client.NewGRPCAchievementClient(activityConn)
	var registrationRewardClient client.RegistrationRewardClient = client.NewGRPCRegistrationRewardClient(activityConn)
	var firstRechargeRewardClient client.FirstRechargeRewardClient = client.NewGRPCFirstRechargeRewardClient(activityConn)
	var cumulativeRechargeRewardClient client.CumulativeRechargeRewardClient = client.NewGRPCCumulativeRechargeRewardClient(activityConn)
	var inviteActivityRewardClient client.InviteActivityRewardClient = client.NewGRPCInviteActivityRewardClient(activityConn)
	var sevenDayCheckInClient client.SevenDayCheckInClient = client.NewGRPCSevenDayCheckInClient(activityConn)
	var luckyGiftClient client.LuckyGiftClient = client.NewGRPCLuckyGiftClient(luckyGiftConn)
	var roomTurnoverRewardClient client.RoomTurnoverRewardClient = client.NewGRPCRoomTurnoverRewardClient(activityConn)
	var weeklyStarClient client.WeeklyStarClient = client.NewGRPCWeeklyStarClient(activityConn)
	var cpWeeklyRankClient client.CPWeeklyRankClient = client.NewGRPCCPWeeklyRankClient(activityConn)
	var agencyOpeningClient client.AgencyOpeningClient = client.NewGRPCAgencyOpeningClient(activityConn)
	var activityTemplateRuntimeClient client.ActivityTemplateRuntimeClient = client.NewGRPCActivityTemplateRuntimeClient(activityConn)
	var wheelClient client.WheelClient = client.NewGRPCWheelClient(activityConn)
	var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
	var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
	var statisticsClient client.StatisticsClient = client.NewHTTPStatisticsClient(cfg.Statistics.BaseURL, cfg.Statistics.Timeout)
	appConfigReader, err := openAppConfigReader(cfg.AppConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = luckyGiftConn.Close()
		_ = gameConn.Close()
		return nil, err
	}
	withdrawalWriter, err := openWithdrawalApplicationWriter(cfg.AppConfig)
	if err != nil {
		if appConfigReader != nil {
			_ = appConfigReader.Close()
		}
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = luckyGiftConn.Close()
		_ = gameConn.Close()
		return nil, err
	}
	withdrawalNotifier, err := openWithdrawalApplicationNotifier(cfg.FinanceNotifications)
	if err != nil {
		if withdrawalWriter != nil {
			_ = withdrawalWriter.Close()
		}
		if appConfigReader != nil {
			_ = appConfigReader.Close()
		}
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = luckyGiftConn.Close()
		_ = gameConn.Close()
		return nil, err
	}
	closeGatewayDeps := func(redisClose func() error) {
		if withdrawalWriter != nil {
			_ = withdrawalWriter.Close()
		}
		if appConfigReader != nil {
			_ = appConfigReader.Close()
		}
		if redisClose != nil {
			_ = redisClose()
		}
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = luckyGiftConn.Close()
		_ = gameConn.Close()
	}
	handler := httptransport.NewHandlerWithConfig(roomClient, roomGuardClient, userClient, userIdentityClient, httptransport.TencentIMConfig{
		SDKAppID:          cfg.TencentIM.SDKAppID,
		SecretKey:         cfg.TencentIM.SecretKey,
		UserSigTTL:        cfg.TencentIM.UserSigTTL,
		AdminIdentifier:   cfg.TencentIM.AdminIdentifier,
		CallbackAuthToken: cfg.TencentIM.CallbackAuthToken,
		GroupIDPrefix:     cfg.TencentIM.GroupIDPrefix,
	}, userProfileClient)
	if cfg.TencentIM.SDKAppID > 0 && strings.TrimSpace(cfg.TencentIM.SecretKey) != "" {
		imClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
		if err != nil {
			closeGatewayDeps(nil)
			return nil, err
		}
		handler.SetTencentIMAccountImporter(gatewayTencentIMAccountImporter{client: imClient})
		handler.SetTencentIMUserMessagePublisher(imClient)
	}
	handler.SetRoomQueryClient(roomQueryClient)
	handler.SetUserDeviceClient(userDeviceClient)
	handler.SetUserInviteClient(userInviteClient)
	handler.SetUserCountryQueryClient(userCountryQueryClient)
	handler.SetUserRegionClient(userRegionClient)
	handler.SetUserHostClient(userHostClient)
	handler.SetUserHostAdminClient(userHostAdminClient)
	handler.SetUserSocialClient(userSocialClient)
	handler.SetUserCPClient(userCPClient)
	handler.SetAppRegistryClient(appRegistryClient)
	handler.SetWalletClient(walletClient)
	handler.SetMessageInboxClient(messageClient)
	handler.SetMessageActionConfirmClient(messageActionClient)
	handler.SetTaskClient(taskClient)
	handler.SetGrowthLevelClient(growthLevelClient)
	handler.SetAchievementClient(achievementClient)
	handler.SetRegistrationRewardClient(registrationRewardClient)
	handler.SetFirstRechargeRewardClient(firstRechargeRewardClient)
	handler.SetCumulativeRechargeRewardClient(cumulativeRechargeRewardClient)
	handler.SetInviteActivityRewardClient(inviteActivityRewardClient)
	handler.SetSevenDayCheckInClient(sevenDayCheckInClient)
	handler.SetLuckyGiftClient(luckyGiftClient)
	handler.SetRoomTurnoverRewardClient(roomTurnoverRewardClient)
	handler.SetWeeklyStarClient(weeklyStarClient)
	handler.SetCPWeeklyRankClient(cpWeeklyRankClient)
	handler.SetAgencyOpeningClient(agencyOpeningClient)
	handler.SetActivityTemplateRuntimeClient(activityTemplateRuntimeClient)
	handler.SetWheelClient(wheelClient)
	handler.SetBroadcastClient(broadcastClient)
	handler.SetGameClient(gameClient)
	handler.SetStatisticsClient(statisticsClient)
	handler.SetWithdrawalApplicationWriter(withdrawalWriter)
	handler.SetWithdrawalApplicationNotifier(withdrawalNotifier)
	leaderboardRedisClose, err := configureUserLeaderboard(handler, cfg.Leaderboard)
	if err != nil {
		closeGatewayDeps(nil)
		return nil, err
	}
	redisClose := leaderboardRedisClose
	if appConfigReader != nil {
		handler.SetAppConfigReader(appConfigReader)
	}
	handler.SetTencentRTC(httptransport.TencentRTCConfig{
		Enabled:         cfg.TencentRTC.Enabled,
		SDKAppID:        cfg.TencentRTC.SDKAppID,
		SecretKey:       cfg.TencentRTC.SecretKey,
		UserSigTTL:      cfg.TencentRTC.UserSigTTL,
		RoomIDType:      cfg.TencentRTC.RoomIDType,
		AppScene:        cfg.TencentRTC.AppScene,
		CallbackSignKey: cfg.TencentRTC.CallbackSignKey,
	})
	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 {
			closeGatewayDeps(redisClose)
			return nil, err
		}
		handler.SetObjectUploader(uploader)
	}
	rateLimitConfig := httptransport.AuthRateLimitConfig{
		Enabled:              cfg.AuthRateLimit.Enabled,
		KeyPrefix:            cfg.AuthRateLimit.KeyPrefix,
		Window:               time.Duration(cfg.AuthRateLimit.WindowSec) * time.Second,
		IPLimit:              cfg.AuthRateLimit.IPLimit,
		DeviceIDLimit:        cfg.AuthRateLimit.DeviceIDLimit,
		ProviderIPLimit:      cfg.AuthRateLimit.ProviderIPLimit,
		DisplayUserIDIPLimit: cfg.AuthRateLimit.DisplayUserIDIPLimit,
		DisplayUserIDLimit:   cfg.AuthRateLimit.DisplayUserIDLimit,
		SessionIDIPLimit:     cfg.AuthRateLimit.SessionIDIPLimit,
	}
	loginRiskConfig := httptransport.LoginRiskConfig{
		Enabled:   cfg.LoginRisk.Enabled,
		KeyPrefix: cfg.LoginRisk.KeyPrefix,
		FastGuard: httptransport.LoginRiskFastGuardConfig{
			BlockedLanguages:           cfg.LoginRisk.FastGuard.BlockedLanguages,
			BlockedLanguagePrimaryTags: cfg.LoginRisk.FastGuard.BlockedLanguagePrimaryTags,
			BlockedTimezones:           cfg.LoginRisk.FastGuard.BlockedTimezones,
			BlockedTimezonePrefixes:    cfg.LoginRisk.FastGuard.BlockedTimezonePrefixes,
		},
	}
	handler.SetLoginRisk(loginRiskConfig)
	authRedisClose, err := configureAuthRateLimit(handler, cfg.AuthRateLimit, rateLimitConfig)
	if err != nil {
		closeGatewayDeps(redisClose)
		return nil, err
	}
	redisClose = joinClose(redisClose, authRedisClose)
	giftCapacityRedisClose, err := configureGiftCapacityLimit(handler, cfg.GiftCapacityLimit)
	if err != nil {
		closeGatewayDeps(redisClose)
		return nil, err
	}
	redisClose = joinClose(redisClose, giftCapacityRedisClose)
	loginRiskRedisClose, err := configureLoginRisk(handler, cfg.LoginRisk, loginRiskConfig)
	if err != nil {
		closeGatewayDeps(redisClose)
		return nil, err
	}
	redisClose = joinClose(redisClose, loginRiskRedisClose)
	verifier := auth.NewVerifier(cfg.JWTSecret)
	healthState := healthcheck.NewState(nodeID(), cfg.JWTSecret, roomConn,
		healthcheck.GRPCDependency{Name: "user_grpc", Conn: userConn},
		healthcheck.GRPCDependency{Name: "wallet_grpc", Conn: walletConn},
		healthcheck.GRPCDependency{Name: "activity_grpc", Conn: activityConn},
		healthcheck.GRPCDependency{Name: "lucky_gift_grpc", Conn: luckyGiftConn},
		healthcheck.GRPCDependency{Name: "game_grpc", Conn: gameConn},
	)

	mux := http.NewServeMux()
	healthHandler := healthcheck.NewHTTPHandler(healthState)
	mux.HandleFunc("/healthz/live", healthHandler.Live)
	mux.HandleFunc("/healthz/ready", healthHandler.Ready)
	mux.Handle("/", handler.Routes(verifier))

	listener, err := net.Listen("tcp", cfg.HTTPAddr)
	if err != nil {
		closeGatewayDeps(redisClose)
		return nil, err
	}

	server := &http.Server{
		Handler: httptransport.WithCORS(httptransport.CORSConfig{
			Enabled:          cfg.CORS.Enabled,
			AllowedOrigins:   cfg.CORS.AllowedOrigins,
			AllowedMethods:   cfg.CORS.AllowedMethods,
			AllowedHeaders:   cfg.CORS.AllowedHeaders,
			ExposeHeaders:    cfg.CORS.ExposeHeaders,
			AllowCredentials: cfg.CORS.AllowCredentials,
			MaxAgeSec:        cfg.CORS.MaxAgeSec,
		}, mux),
	}

	return &App{
		server:        server,
		listener:      listener,
		roomConn:      roomConn,
		userConn:      userConn,
		walletConn:    walletConn,
		activityConn:  activityConn,
		luckyGiftConn: luckyGiftConn,
		gameConn:      gameConn,
		appConfig:     appConfigReader,
		withdrawal:    withdrawalWriter,
		redisClose:    redisClose,
		health:        healthState,
	}, nil
}

func openAppConfigReader(cfg config.AppConfigConfig) (*appconfig.MySQLReader, error) {
	if strings.TrimSpace(cfg.MySQLDSN) == "" {
		return nil, nil
	}

	reader, err := appconfig.OpenMySQLReader(cfg.MySQLDSN)
	if err != nil {
		return nil, err
	}
	reader.SetRoomDiscoveryCacheTTL(cfg.RoomDiscoveryCacheTTL)
	if strings.TrimSpace(cfg.RedisAddr) == "" {
		return reader, nil
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	cacheClient, err := appconfig.OpenRedisCache(ctx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB)
	if err != nil {
		_ = reader.Close()
		return nil, fmt.Errorf("connect app config redis: %w", err)
	}
	reader.SetRedisCache(cacheClient, cfg.KeyPrefix, cfg.CacheTTL)
	return reader, nil
}

func openWithdrawalApplicationWriter(cfg config.AppConfigConfig) (*financewithdrawal.MySQLWriter, error) {
	// H5 提现申请进入后台 finance 审核表，表在 hyapp_admin；这里复用后台库 DSN，但只开放单表写入器。
	return financewithdrawal.OpenMySQLWriter(cfg.MySQLDSN)
}

func openWithdrawalApplicationNotifier(cfg config.FinanceNotificationsConfig) (financewithdrawal.Notifier, error) {
	if !cfg.DingTalk.Enabled || strings.TrimSpace(cfg.DingTalk.WebhookURL) == "" {
		return nil, nil
	}
	client, err := dingtalkrobot.New(dingtalkrobot.Config{
		WebhookURL:     cfg.DingTalk.WebhookURL,
		Secret:         cfg.DingTalk.Secret,
		AtMobiles:      cfg.DingTalk.AtMobiles,
		AtAll:          cfg.DingTalk.AtAll,
		RequestTimeout: cfg.DingTalk.RequestTimeout,
	}, nil)
	if err != nil {
		return nil, err
	}
	return financewithdrawal.NewDingTalkNotifier(client), nil
}

func configureAuthRateLimit(handler *httptransport.Handler, cfg config.AuthRateLimitConfig, rateLimitConfig httptransport.AuthRateLimitConfig) (func() error, error) {
	if !rateLimitConfig.Enabled {
		// 显式关闭频控时仍记录配置，避免 handler 使用默认开启策略。
		handler.SetAuthRateLimit(rateLimitConfig)
		return nil, nil
	}

	redisAddr := strings.TrimSpace(cfg.RedisAddr)
	if redisAddr == "" {
		return nil, errors.New("auth rate limit redis_addr is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	redisClient, err := httptransport.NewAuthRateLimitRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
	if err != nil {
		return nil, fmt.Errorf("connect auth rate limit redis: %w", err)
	}

	// 生产入口必须走 Redis 共享计数；内存 backend 只保留给单元测试和显式禁用频控。
	handler.SetAuthRateLimitBackend(rateLimitConfig, httptransport.NewRedisAuthRateLimiter(redisClient, rateLimitConfig))
	return redisClient.Close, nil
}

func configureGiftCapacityLimit(handler *httptransport.Handler, cfg config.GiftCapacityLimitConfig) (func() error, error) {
	handler.SetGiftRequestLimits(cfg.MaxGiftCountPerRequest, cfg.MaxGiftUnitsPerRequest)
	if !cfg.Enabled {
		handler.SetGiftCapacityProtection(false, cfg.RequestTimeout, cfg.MaxTargetsPerRequest, nil)
		return nil, nil
	}
	redisAddr := strings.TrimSpace(cfg.RedisAddr)
	if redisAddr == "" {
		return nil, errors.New("gift capacity limit redis_addr is required")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	redisClient, err := giftlimit.NewRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
	if err != nil {
		return nil, fmt.Errorf("connect gift capacity limit redis: %w", err)
	}
	runtimeConfig := giftlimit.Config{
		Enabled:                   true,
		KeyPrefix:                 cfg.KeyPrefix,
		UserBucketCapacity:        cfg.UserBucketCapacity,
		UserRefillTokensPerSecond: cfg.UserRefillTokensPerSecond,
		RoomBucketCapacity:        cfg.RoomBucketCapacity,
		RoomRefillTokensPerSecond: cfg.RoomRefillTokensPerSecond,
		AppBucketCapacity:         cfg.AppBucketCapacity,
		AppRefillTokensPerSecond:  cfg.AppRefillTokensPerSecond,
		MaxInFlight:               cfg.MaxInFlight,
		InFlightLease:             cfg.InFlightLease,
		RedisOperationTimeout:     cfg.RedisOperationTimeout,
	}
	// 与 public auth 频控相同，生产送礼保护必须使用共享 Redis；进程内计数无法约束多副本总流量。
	handler.SetGiftCapacityProtection(true, cfg.RequestTimeout, cfg.MaxTargetsPerRequest, giftlimit.NewRedisLimiter(redisClient, runtimeConfig))
	return redisClient.Close, nil
}

func configureUserLeaderboard(handler *httptransport.Handler, cfg config.LeaderboardConfig) (func() error, error) {
	redisAddr := strings.TrimSpace(cfg.RedisAddr)
	if redisAddr == "" {
		return nil, nil
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	redisClient, err := userleaderboard.NewRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
	if err != nil {
		return nil, fmt.Errorf("connect user leaderboard redis: %w", err)
	}
	// gateway 只读取 activity-service 维护的 Redis 聚合结果，不再在 HTTP 请求中扫描 wallet_transactions。
	handler.SetUserLeaderboardStore(userleaderboard.NewStore(redisClient, cfg.KeyPrefix))
	return redisClient.Close, nil
}

func configureLoginRisk(handler *httptransport.Handler, cfg config.LoginRiskConfig, riskConfig httptransport.LoginRiskConfig) (func() error, error) {
	redisAddr := strings.TrimSpace(cfg.RedisAddr)
	if redisAddr == "" {
		return nil, errors.New("session denylist redis_addr is required")
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	redisClient, err := httptransport.NewLoginRiskRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
	if err != nil {
		// FastGuard 可 fail-open，但 sid denylist 是 access JWT 撤销的唯一入口事实；连接失败必须阻断启动。
		return nil, fmt.Errorf("connect session denylist redis: %w", err)
	}
	handler.SetLoginRiskCache(riskConfig, httptransport.NewRedisLoginRiskCache(redisClient, riskConfig))
	return redisClient.Close, nil
}

func joinClose(left func() error, right func() error) func() error {
	if left == nil {
		return right
	}
	if right == nil {
		return left
	}
	return func() error {
		return errors.Join(left(), right())
	}
}

type gatewayTencentIMAccountImporter struct {
	client *tencentim.RESTClient
}

func (i gatewayTencentIMAccountImporter) ImportAccount(ctx context.Context, userID int64, nickname string, faceURL string) error {
	return i.client.ImportAccount(ctx, tencentim.AccountProfile{
		UserID:  userID,
		Nick:    nickname,
		FaceURL: faceURL,
	})
}

// Run 启动 HTTP 服务。
func (a *App) Run() error {
	a.health.MarkHTTPServing()
	err := serviceapp.ServeHTTP(a.server, a.listener)
	a.health.MarkHTTPStopped()
	return err
}

// Close 关闭 HTTP 服务。
func (a *App) Close() error {
	var err error
	a.closeOnce.Do(func() {
		a.health.MarkDraining()
		// HTTP drain 要先于内部 gRPC 连接关闭，保证已进入 handler 的编排请求能自然完成。
		err = errors.Join(err, serviceapp.ShutdownHTTP(a.server, 15*time.Second))
		if a.roomConn != nil {
			err = errors.Join(err, a.roomConn.Close())
		}
		if a.userConn != nil {
			err = errors.Join(err, a.userConn.Close())
		}
		if a.walletConn != nil {
			err = errors.Join(err, a.walletConn.Close())
		}
		if a.activityConn != nil {
			err = errors.Join(err, a.activityConn.Close())
		}
		if a.luckyGiftConn != nil {
			err = errors.Join(err, a.luckyGiftConn.Close())
		}
		if a.gameConn != nil {
			err = errors.Join(err, a.gameConn.Close())
		}
		if a.appConfig != nil {
			err = errors.Join(err, a.appConfig.Close())
		}
		if a.withdrawal != nil {
			err = errors.Join(err, a.withdrawal.Close())
		}
		if a.redisClose != nil {
			err = errors.Join(err, a.redisClose())
		}
	})

	return err
}

func nodeID() string {
	hostname, err := os.Hostname()
	if err != nil || hostname == "" {
		return "gateway-service"
	}

	return hostname
}
