// Package app 装配 user-service 的 gRPC 入口、领域服务、repository 和健康检查。
package app

import (
	"context"
	"database/sql"
	"errors"
	"net"
	"sync"
	"time"

	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/grpcclient"
	"hyapp/pkg/grpchealth"
	"hyapp/pkg/grpcshutdown"
	"hyapp/pkg/healthhttp"
	"hyapp/pkg/idgen"
	"hyapp/pkg/logx"
	"hyapp/pkg/tencentim"
	"hyapp/services/user-service/internal/config"
	"hyapp/services/user-service/internal/integration/activityclient"
	"hyapp/services/user-service/internal/integration/roomclient"
	authservice "hyapp/services/user-service/internal/service/auth"
	hostservice "hyapp/services/user-service/internal/service/host"
	inviteservice "hyapp/services/user-service/internal/service/invite"
	mictimeservice "hyapp/services/user-service/internal/service/mictime"
	userservice "hyapp/services/user-service/internal/service/user"
	mysqlstorage "hyapp/services/user-service/internal/storage/mysql"
	invitestorage "hyapp/services/user-service/internal/storage/mysql/invite"
	mictimestorage "hyapp/services/user-service/internal/storage/mysql/mictime"
	grpcserver "hyapp/services/user-service/internal/transport/grpc"

	"google.golang.org/grpc"
	healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
)

// App 装配 user-service gRPC 入口和底座依赖。
type App struct {
	// server 承载 AuthService、UserService、UserIdentityService 和 gRPC health。
	server *grpc.Server
	// listener 是 user-service 的唯一网络入口。
	listener net.Listener
	// health 是标准 gRPC health 的 serving/draining 状态来源。
	health *grpchealth.ServingChecker
	// healthHTTP 给 CLB 和发布脚本提供 HTTP readiness，不承载业务请求。
	healthHTTP *healthhttp.Server
	// mysqlRepo 持有用户主数据、认证身份、session 和短号事务的唯一运行时存储。
	mysqlRepo *mysqlstorage.Repository
	// walletDB 只用于读取 wallet_outbox 充值事实，实际账务状态仍由 wallet-service owner。
	walletDB *sql.DB
	// roomDB 只用于读取 room_outbox 麦位事实，Room Cell 状态仍由 room-service owner。
	roomDB *sql.DB
	// roomConn 是低频平台治理调用 room-service 的 gRPC 连接。
	roomConn *grpc.ClientConn
	// activityConn 是注册完成后触发注册奖励的 gRPC 连接。
	activityConn *grpc.ClientConn
	// userSvc 持有用户主数据用例，cron RPC 复用它消费区域重算任务。
	userSvc *userservice.Service
	// authSvc 持有认证、登录 session 和登录 IP 风控用例。
	authSvc *authservice.Service
	// inviteSvc 消费 wallet recharge outbox，维护有效邀请 read model。
	inviteSvc *inviteservice.Service
	// micTimeSvc 消费 room mic outbox，维护用户维度麦上时长 read model。
	micTimeSvc *mictimeservice.Service
	// loginRiskRedisClose 关闭登录风险 Redis cache；cache 不作为启动硬依赖。
	loginRiskRedisClose func() error
	// cfg 保存 worker 运行参数，避免 Run 阶段重新读配置。
	cfg config.Config
	// workerCtx 统一控制后台 worker 生命周期。
	workerCtx context.Context
	// workerCancel 停止 invite recharge 和 room mic outbox worker。
	workerCancel context.CancelFunc
	// workerWG 等待后台 worker 当前批次安全退出。
	workerWG sync.WaitGroup
	// closeOnce 防止信号退出和 Serve 异常同时触发重复关闭。
	closeOnce sync.Once
}

// New 初始化 user-service 应用。
func New(cfg config.Config) (*App, error) {
	// userRepository 只表达用户主数据用例，authRepository 只表达认证用例；MySQL 实现同时满足两者。
	startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// user-service 的登录、注册、短号和 session 都必须落 MySQL；空 DSN 直接启动失败。
	mysqlRepo, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN)
	if err != nil {
		return nil, err
	}
	// listener 在 New 阶段创建，端口占用会作为启动失败返回。
	listener, err := net.Listen("tcp", cfg.GRPCAddr)
	if err != nil {
		if mysqlRepo != nil {
			_ = mysqlRepo.Close()
		}
		return nil, err
	}

	server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("user-service")))
	thirdPartyVerifier := authservice.NewRoutedThirdPartyVerifier(
		authservice.NewFirebaseThirdPartyVerifier(authservice.FirebaseVerifierConfig{
			ProjectID:              cfg.ThirdParty.Firebase.ProjectID,
			AllowedSignInProviders: cfg.ThirdParty.Firebase.AllowedSignInProviders,
			CertsURL:               cfg.ThirdParty.Firebase.CertsURL,
		}),
		authservice.NewStaticThirdPartyVerifier(cfg.ThirdParty.AllowedProviders),
	)
	authRepo := mysqlRepo.AuthRepository()
	appRepo := mysqlRepo.AppRepository()
	userRepo := mysqlRepo.UserRepository()
	identityRepo := mysqlRepo.IdentityRepository()
	regionRepo := mysqlRepo.RegionRepository()
	deviceRepo := mysqlRepo.DeviceRepository()
	hostRepo := mysqlRepo.HostRepository()
	inviteRepo := mysqlRepo.InviteRepository()
	micTimeRepo := mysqlRepo.MicTimeRepository()
	var ipDecisionCache authservice.IPDecisionCache
	var loginRiskRedisClose func() error
	if cfg.LoginRisk.Enabled && cfg.LoginRisk.RedisAddr != "" {
		redisClient, redisErr := authservice.NewLoginRiskRedisClient(startupCtx, cfg.LoginRisk.RedisAddr, cfg.LoginRisk.RedisPassword, cfg.LoginRisk.RedisDB)
		if redisErr == nil {
			ipDecisionCache = authservice.NewRedisIPDecisionCache(redisClient)
			loginRiskRedisClose = redisClient.Close
		} else {
			// IP 风控 Redis 缓存按文档 fail-open；启动继续，worker 仍会写 MySQL 审计。
			logx.Warn(startupCtx, "login_risk_redis_unavailable")
		}
	}
	var imLoginKicker userservice.IMLoginKicker
	if cfg.TencentIM.Enabled {
		imClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
		if err != nil {
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		imLoginKicker = imClient
	}
	var roomConn *grpc.ClientConn
	var roomEvictor userservice.RoomEvictor
	if cfg.RoomService.Enabled {
		roomConn, err = grpcclient.Dial(cfg.RoomService.Addr, grpcclient.Config{
			DefaultTimeout: time.Duration(cfg.RoomService.RequestTimeoutMs) * time.Millisecond,
		})
		if err != nil {
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		roomEvictor = roomclient.NewGRPC(roomConn)
	}
	var activityConn *grpc.ClientConn
	var registrationRewardIssuer authservice.RegistrationRewardIssuer
	if cfg.ActivityService.Enabled {
		activityConn, err = grpcclient.Dial(cfg.ActivityService.Addr, grpcclient.Config{
			DefaultTimeout: time.Duration(cfg.ActivityService.RequestTimeoutMs) * time.Millisecond,
		})
		if err != nil {
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			if roomConn != nil {
				_ = roomConn.Close()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		registrationRewardIssuer = activityclient.NewGRPC(activityConn)
	}

	// auth service 负责登录、session 和 token；用户主数据和短号事务仍通过各自领域存储完成。
	authSvc := authservice.New(authservice.Config{
		Issuer:             cfg.JWT.Issuer,
		AccessTokenTTLSec:  cfg.JWT.AccessTokenTTLSec,
		RefreshTokenTTLSec: cfg.JWT.RefreshTokenTTLSec,
		SigningAlg:         cfg.JWT.SigningAlg,
		SigningSecret:      cfg.JWT.SigningSecret,
	},
		authservice.WithAuthRepository(authRepo),
		authservice.WithUserRepository(userRepo),
		authservice.WithIdentityRepository(identityRepo),
		authservice.WithCountryRegionRepository(regionRepo),
		authservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)),
		authservice.WithDisplayUserIDAllocateMaxAttempts(cfg.DisplayUserID.AllocateMaxAttempts),
		authservice.WithThirdPartyVerifier(thirdPartyVerifier),
		authservice.WithLoginRiskPolicy(authservice.LoginRiskPolicy{
			Enabled:              cfg.LoginRisk.Enabled,
			UnsupportedCountries: cfg.LoginRisk.UnsupportedCountries,
			BlockedTTL:           time.Duration(cfg.LoginRisk.BlockedTTLSec) * time.Second,
			AllowedTTL:           time.Duration(cfg.LoginRisk.AllowedTTLSec) * time.Second,
			UnknownTTL:           time.Duration(cfg.LoginRisk.UnknownTTLSec) * time.Second,
			ProviderTimeout:      time.Duration(cfg.LoginRisk.ProviderTimeoutMs) * time.Millisecond,
			ProviderNames:        cfg.LoginRisk.Providers,
			DenylistTTL:          time.Duration(cfg.JWT.AccessTokenTTLSec+60) * time.Second,
		}),
		authservice.WithIPDecisionCache(ipDecisionCache),
		authservice.WithIPGeoProviders(authservice.BuildIPGeoProviders(cfg.LoginRisk.Providers)),
		authservice.WithRegistrationRewardIssuer(registrationRewardIssuer),
	)
	// user service 负责用户主状态和 display_user_id/靓号用例，不进入房间高频流程。
	userSvc := userservice.New(userRepo,
		userservice.WithAppRegistryRepository(appRepo),
		userservice.WithIdentityRepository(identityRepo),
		userservice.WithCountryRegionRepository(regionRepo),
		userservice.WithCountryAdminRepository(regionRepo),
		userservice.WithRegionAdminRepository(regionRepo),
		userservice.WithRegionRebuildRepository(regionRepo),
		userservice.WithDeviceRepository(deviceRepo),
		userservice.WithModerationRepository(userRepo),
		userservice.WithSessionDenylist(ipDecisionCache, time.Duration(cfg.JWT.AccessTokenTTLSec+60)*time.Second),
		userservice.WithIMLoginKicker(imLoginKicker),
		userservice.WithRoomEvictor(roomEvictor),
		userservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)),
		userservice.WithDisplayUserIDPolicy(cfg.DisplayUserID.AllocateMaxAttempts, time.Duration(cfg.DisplayUserID.ChangeCooldownSec)*time.Second),
	)
	hostSvc := hostservice.New(hostRepo,
		hostservice.WithIDGenerator(idgen.NewInt64Generator(cfg.IDGenerator.NodeID)),
	)
	var walletDB *sql.DB
	var inviteSvc *inviteservice.Service
	if cfg.InviteRechargeWorker.Enabled {
		walletDB, err = sql.Open("mysql", cfg.InviteRechargeWorker.WalletMySQLDSN)
		if err != nil {
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			if roomConn != nil {
				_ = roomConn.Close()
			}
			if activityConn != nil {
				_ = activityConn.Close()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		if err := walletDB.PingContext(startupCtx); err != nil {
			_ = walletDB.Close()
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			if roomConn != nil {
				_ = roomConn.Close()
			}
			if activityConn != nil {
				_ = activityConn.Close()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		inviteSvc = inviteservice.New(inviteRepo, invitestorage.NewWalletOutboxReader(walletDB))
	}
	var roomDB *sql.DB
	micTimeSvc := mictimeservice.New(micTimeRepo, nil)
	if cfg.MicTimeWorker.Enabled {
		roomDB, err = sql.Open("mysql", cfg.MicTimeWorker.RoomMySQLDSN)
		if err != nil {
			if walletDB != nil {
				_ = walletDB.Close()
			}
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			if roomConn != nil {
				_ = roomConn.Close()
			}
			if activityConn != nil {
				_ = activityConn.Close()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		if err := roomDB.PingContext(startupCtx); err != nil {
			_ = roomDB.Close()
			if walletDB != nil {
				_ = walletDB.Close()
			}
			if loginRiskRedisClose != nil {
				_ = loginRiskRedisClose()
			}
			if roomConn != nil {
				_ = roomConn.Close()
			}
			if activityConn != nil {
				_ = activityConn.Close()
			}
			_ = listener.Close()
			_ = mysqlRepo.Close()
			return nil, err
		}
		micTimeSvc = mictimeservice.New(micTimeRepo, mictimestorage.NewRoomOutboxReader(roomDB))
	}
	userServer := grpcserver.NewServer(authSvc, userSvc, hostSvc)
	userServer.SetMicTimeService(micTimeSvc)
	// 多个 protobuf service 共用一个 Server 适配器，领域逻辑仍拆在 auth/user/host service。
	userv1.RegisterAuthServiceServer(server, userServer)
	userv1.RegisterUserServiceServer(server, userServer)
	userv1.RegisterUserSocialServiceServer(server, userServer)
	userv1.RegisterUserDeviceServiceServer(server, userServer)
	userv1.RegisterAppRegistryServiceServer(server, userServer)
	userv1.RegisterUserIdentityServiceServer(server, userServer)
	userv1.RegisterCountryAdminServiceServer(server, userServer)
	userv1.RegisterCountryQueryServiceServer(server, userServer)
	userv1.RegisterRegionAdminServiceServer(server, userServer)
	userv1.RegisterUserHostServiceServer(server, userServer)
	userv1.RegisterUserHostAdminServiceServer(server, userServer)
	userv1.RegisterUserCronServiceServer(server, userServer)

	healthDependencies := []grpchealth.Dependency{{
		Name:  "mysql",
		Check: mysqlRepo.Ping,
	}}
	if walletDB != nil {
		healthDependencies = append(healthDependencies, grpchealth.Dependency{
			Name:  "wallet_mysql",
			Check: walletDB.PingContext,
		})
	}
	if roomDB != nil {
		healthDependencies = append(healthDependencies, grpchealth.Dependency{
			Name:  "room_mysql",
			Check: roomDB.PingContext,
		})
	}
	health := grpchealth.NewServingChecker("user-service", healthDependencies...)
	// user-service 目前使用简单 serving checker，存储探测由后续专用 health 可扩展。
	healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health))
	healthHTTP, err := healthhttp.New(cfg.HealthHTTPAddr, cfg.NodeID, health)
	if err != nil {
		if roomDB != nil {
			_ = roomDB.Close()
		}
		if walletDB != nil {
			_ = walletDB.Close()
		}
		if loginRiskRedisClose != nil {
			_ = loginRiskRedisClose()
		}
		if roomConn != nil {
			_ = roomConn.Close()
		}
		if activityConn != nil {
			_ = activityConn.Close()
		}
		_ = listener.Close()
		_ = mysqlRepo.Close()
		return nil, err
	}
	workerCtx, workerCancel := context.WithCancel(context.Background())

	return &App{
		server:              server,
		listener:            listener,
		health:              health,
		healthHTTP:          healthHTTP,
		mysqlRepo:           mysqlRepo,
		walletDB:            walletDB,
		roomDB:              roomDB,
		roomConn:            roomConn,
		activityConn:        activityConn,
		authSvc:             authSvc,
		userSvc:             userSvc,
		inviteSvc:           inviteSvc,
		micTimeSvc:          micTimeSvc,
		loginRiskRedisClose: loginRiskRedisClose,
		cfg:                 cfg,
		workerCtx:           workerCtx,
		workerCancel:        workerCancel,
	}, nil
}

// Run 启动 gRPC 服务。
func (a *App) Run() error {
	a.runHealthHTTP()
	// 只有 listener 进入 Serve 后才标记 serving，避免健康检查提前放行。
	a.health.MarkServing()
	defer func() {
		if a.workerCancel != nil {
			a.workerCancel()
		}
		a.workerWG.Wait()
	}()
	if a.cfg.InviteRechargeWorker.Enabled && a.inviteSvc != nil {
		a.workerWG.Go(func() {
			a.inviteSvc.RunWalletRechargeWorker(a.workerCtx, inviteservice.WorkerOptions{
				WorkerID:     a.cfg.NodeID,
				PollInterval: time.Duration(a.cfg.InviteRechargeWorker.PollIntervalSec) * time.Second,
				BatchSize:    a.cfg.InviteRechargeWorker.BatchSize,
			})
		})
	}
	if a.cfg.MicTimeWorker.Enabled && a.micTimeSvc != nil {
		a.workerWG.Go(func() {
			a.micTimeSvc.RunRoomMicWorker(a.workerCtx, mictimeservice.WorkerOptions{
				WorkerID:     a.cfg.NodeID,
				PollInterval: time.Duration(a.cfg.MicTimeWorker.PollIntervalSec) * time.Second,
				BatchSize:    a.cfg.MicTimeWorker.BatchSize,
			})
		})
	}
	err := a.server.Serve(a.listener)
	a.health.MarkStopped()
	if errors.Is(err, grpc.ErrServerStopped) {
		// GracefulStop 触发的退出不是故障。
		return nil
	}

	return err
}

// Close 关闭 gRPC 服务和底层持久化连接。
func (a *App) Close() {
	a.closeOnce.Do(func() {
		// draining 会让 health 立即失败，避免下线进程继续接新 RPC。
		a.health.MarkDraining()
		if a.workerCancel != nil {
			a.workerCancel()
		}
		a.workerWG.Wait()
		// GracefulStop 等待已进入的 RPC 结束，避免 token/session 写入被硬切。
		grpcshutdown.GracefulStop(a.server, 15*time.Second)
		a.closeHealthHTTP()
		if a.mysqlRepo != nil {
			// MySQL 连接池最后关闭，保证 GracefulStop 期间 repository 仍可用。
			_ = a.mysqlRepo.Close()
		}
		if a.walletDB != nil {
			_ = a.walletDB.Close()
		}
		if a.roomDB != nil {
			_ = a.roomDB.Close()
		}
		if a.roomConn != nil {
			_ = a.roomConn.Close()
		}
		if a.activityConn != nil {
			_ = a.activityConn.Close()
		}
		if a.loginRiskRedisClose != nil {
			_ = a.loginRiskRedisClose()
		}
	})
}

func (a *App) runHealthHTTP() {
	if a.healthHTTP == nil {
		return
	}
	go func() {
		if err := a.healthHTTP.Run(); err != nil {
			logx.Error(context.Background(), "health_http_run_failed", err)
		}
	}()
}

func (a *App) closeHealthHTTP() {
	if a.healthHTTP == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	_ = a.healthHTTP.Close(ctx)
}
