package app

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

	"google.golang.org/grpc"
	"hyapp/pkg/grpcclient"
	"hyapp/pkg/tencentcos"
	"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/healthcheck"
	httptransport "hyapp/services/gateway-service/internal/transport/http"
)

// App 装配 gateway 的 HTTP 入口。
type App struct {
	server       *http.Server
	listener     net.Listener
	roomConn     *grpc.ClientConn
	userConn     *grpc.ClientConn
	walletConn   *grpc.ClientConn
	activityConn *grpc.ClientConn
	gameConn     *grpc.ClientConn
	appConfig    *appconfig.MySQLReader
	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
	}
	gameConn, err := grpcclient.Dial(cfg.GameServiceAddr, grpcConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.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 userDeviceClient client.UserDeviceClient = client.NewGRPCUserDeviceClient(userConn)
	var userCountryQueryClient client.UserCountryQueryClient = client.NewGRPCUserCountryQueryClient(userConn)
	var userHostClient client.UserHostClient = client.NewGRPCUserHostClient(userConn)
	var userSocialClient client.UserSocialClient = client.NewGRPCUserSocialClient(userConn)
	var appRegistryClient client.AppRegistryClient = client.NewGRPCAppRegistryClient(userConn)
	var walletClient client.WalletClient = client.NewGRPCWalletClient(walletConn)
	var messageClient client.MessageInboxClient = client.NewGRPCMessageInboxClient(activityConn)
	var taskClient client.TaskClient = client.NewGRPCTaskClient(activityConn)
	var registrationRewardClient client.RegistrationRewardClient = client.NewGRPCRegistrationRewardClient(activityConn)
	var broadcastClient client.BroadcastClient = client.NewGRPCBroadcastClient(activityConn)
	var gameClient client.GameClient = client.NewGRPCGameClient(gameConn)
	appConfigReader, err := openAppConfigReader(cfg.AppConfig)
	if err != nil {
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = gameConn.Close()
		return nil, err
	}
	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,
	}, userProfileClient)
	handler.SetRoomQueryClient(roomQueryClient)
	handler.SetUserDeviceClient(userDeviceClient)
	handler.SetUserCountryQueryClient(userCountryQueryClient)
	handler.SetUserHostClient(userHostClient)
	handler.SetUserSocialClient(userSocialClient)
	handler.SetAppRegistryClient(appRegistryClient)
	handler.SetWalletClient(walletClient)
	handler.SetMessageInboxClient(messageClient)
	handler.SetTaskClient(taskClient)
	handler.SetRegistrationRewardClient(registrationRewardClient)
	handler.SetBroadcastClient(broadcastClient)
	handler.SetGameClient(gameClient)
	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 {
			if appConfigReader != nil {
				_ = appConfigReader.Close()
			}
			_ = roomConn.Close()
			_ = userConn.Close()
			_ = walletConn.Close()
			_ = activityConn.Close()
			_ = gameConn.Close()
			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)
	redisClose, err := configureAuthRateLimit(handler, cfg.AuthRateLimit, rateLimitConfig)
	if err != nil {
		if appConfigReader != nil {
			_ = appConfigReader.Close()
		}
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = gameConn.Close()
		return nil, err
	}
	loginRiskRedisClose, err := configureLoginRisk(handler, cfg.LoginRisk, loginRiskConfig)
	if err != nil {
		if redisClose != nil {
			_ = redisClose()
		}
		if appConfigReader != nil {
			_ = appConfigReader.Close()
		}
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = gameConn.Close()
		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: "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 {
		if redisClose != nil {
			_ = redisClose()
		}
		if appConfigReader != nil {
			_ = appConfigReader.Close()
		}
		_ = roomConn.Close()
		_ = userConn.Close()
		_ = walletConn.Close()
		_ = activityConn.Close()
		_ = gameConn.Close()
		return nil, err
	}

	server := &http.Server{
		Handler: mux,
	}

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

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

	return appconfig.OpenMySQLReader(cfg.MySQLDSN)
}

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 configureLoginRisk(handler *httptransport.Handler, cfg config.LoginRiskConfig, riskConfig httptransport.LoginRiskConfig) (func() error, error) {
	if !riskConfig.Enabled {
		handler.SetLoginRisk(riskConfig)
		return nil, nil
	}
	redisAddr := strings.TrimSpace(cfg.RedisAddr)
	if redisAddr == "" {
		handler.SetLoginRisk(riskConfig)
		return nil, nil
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	redisClient, err := httptransport.NewLoginRiskRedisClient(ctx, redisAddr, cfg.RedisPassword, cfg.RedisDB)
	if err != nil {
		// IP 风控缓存按文档 fail-open：Redis 不可用不能阻断 gateway 启动或登录。
		handler.SetLoginRisk(riskConfig)
		return nil, nil
	}
	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())
	}
}

// Run 启动 HTTP 服务。
func (a *App) Run() error {
	a.health.MarkHTTPServing()
	err := a.server.Serve(a.listener)
	a.health.MarkHTTPStopped()
	if errors.Is(err, http.ErrServerClosed) {
		return nil
	}

	return err
}

// Close 关闭 HTTP 服务。
func (a *App) Close() error {
	var err error
	a.closeOnce.Do(func() {
		a.health.MarkDraining()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
		defer cancel()
		// HTTP Shutdown drains accepted requests before client connections close,
		// so in-flight handlers can finish their internal gRPC calls.
		if shutdownErr := a.server.Shutdown(shutdownCtx); shutdownErr != nil {
			// 超过 drain 预算后强制关闭，避免发布脚本被异常长连接卡住。
			err = errors.Join(err, shutdownErr, a.server.Close())
		}
		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.gameConn != nil {
			err = errors.Join(err, a.gameConn.Close())
		}
		if a.appConfig != nil {
			err = errors.Join(err, a.appConfig.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
}
