package grpc

import (
	"context"
	"strings"

	luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
	service "hyapp/services/lucky-gift-service/internal/service/luckygift"
)

// LuckyGiftServer 暴露房间送礼主链路调用的幸运礼物查询和抽奖入口。
type LuckyGiftServer struct {
	luckygiftv1.UnimplementedLuckyGiftServiceServer

	svc *service.Service
}

func NewLuckyGiftServer(svc *service.Service) *LuckyGiftServer {
	return &LuckyGiftServer{svc: svc}
}

func (s *LuckyGiftServer) CheckLuckyGift(ctx context.Context, req *luckygiftv1.CheckLuckyGiftRequest) (*luckygiftv1.CheckLuckyGiftResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	result, err := s.svc.Check(ctx, domain.CheckCommand{
		PoolID:   req.GetPoolId(),
		GiftID:   req.GetGiftId(),
		UserID:   req.GetUserId(),
		DeviceID: req.GetDeviceId(),
		RoomID:   req.GetRoomId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.CheckLuckyGiftResponse{
		Enabled:         result.Enabled,
		Reason:          result.Reason,
		PoolId:          result.PoolID,
		GiftId:          result.GiftID,
		GiftPrice:       result.GiftPrice,
		RuleVersion:     result.RuleVersion,
		StrategyVersion: result.StrategyVersion,
		TargetRtpPpm:    result.TargetRTPPPM,
		ExperiencePool:  result.ExperiencePool,
	}, nil
}

func (s *LuckyGiftServer) ExecuteLuckyGiftDraw(ctx context.Context, req *luckygiftv1.ExecuteLuckyGiftDrawRequest) (*luckygiftv1.ExecuteLuckyGiftDrawResponse, error) {
	meta := req.GetLuckyGift()
	ctx = appcode.WithContext(ctx, meta.GetMeta().GetAppCode())
	result, err := s.svc.Draw(ctx, luckyDrawCommandFromProto(meta))
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.ExecuteLuckyGiftDrawResponse{Result: luckyDrawResultToProto(result)}, nil
}

func (s *LuckyGiftServer) BatchExecuteLuckyGiftDraw(ctx context.Context, req *luckygiftv1.BatchExecuteLuckyGiftDrawRequest) (*luckygiftv1.BatchExecuteLuckyGiftDrawResponse, error) {
	if len(req.GetLuckyGifts()) == 0 {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch draw commands are required"))
	}
	appCode := strings.TrimSpace(req.GetLuckyGifts()[0].GetMeta().GetAppCode())
	if appCode == "" {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch app_code is required"))
	}
	ctx = appcode.WithContext(ctx, appCode)
	cmds := make([]domain.DrawCommand, 0, len(req.GetLuckyGifts()))
	for _, meta := range req.GetLuckyGifts() {
		// 一个批次共享同一条房间送礼命令和同一个 app 账本；混入其他 app_code 会污染第一条 app 的 RTP 和单池账本。
		if strings.TrimSpace(meta.GetMeta().GetAppCode()) != appCode {
			return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch app_code mismatch"))
		}
		cmds = append(cmds, luckyDrawCommandFromProto(meta))
	}
	results, err := s.svc.DrawBatch(ctx, cmds)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.BatchExecuteLuckyGiftDrawResponse{Results: make([]*luckygiftv1.LuckyGiftDrawResult, 0, len(results))}
	for _, result := range results {
		resp.Results = append(resp.Results, luckyDrawResultToProto(result))
	}
	return resp, nil
}

func (s *LuckyGiftServer) ExecuteExternalGiftDraw(ctx context.Context, req *luckygiftv1.ExternalGiftDrawRequest) (*luckygiftv1.ExternalGiftDrawResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetAppCode())
	result, err := s.svc.ExecuteExternalDraw(ctx, domain.ExternalDrawCommand{
		AppCode:            req.GetAppCode(),
		ExternalUserID:     req.GetExternalUserId(),
		DeviceID:           req.GetDeviceId(),
		RequestID:          req.GetRequestId(),
		GiftCount:          req.GetGiftCount(),
		UnitAmount:         req.GetUnitAmount(),
		TotalAmount:        req.GetTotalAmount(),
		Currency:           req.GetCurrency(),
		PaidAtMS:           req.GetPaidAtMs(),
		MetadataJSON:       req.GetMetadataJson(),
		PoolID:             req.GetPoolId(),
		UserRegisteredAtMS: req.GetUserRegisteredAtMs(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return externalDrawResultToProto(result), nil
}

func luckyDrawCommandFromProto(meta *luckygiftv1.LuckyGiftMeta) domain.DrawCommand {
	return domain.DrawCommand{
		CommandID:                 meta.GetCommandId(),
		PoolID:                    meta.GetPoolId(),
		UserID:                    meta.GetUserId(),
		TargetUserID:              meta.GetTargetUserId(),
		DeviceID:                  meta.GetDeviceId(),
		RoomID:                    meta.GetRoomId(),
		AnchorID:                  meta.GetAnchorId(),
		GiftID:                    meta.GetGiftId(),
		GiftCount:                 meta.GetGiftCount(),
		CoinSpent:                 meta.GetCoinSpent(),
		PaidAtMS:                  meta.GetPaidAtMs(),
		UserRegisteredAtMS:        meta.GetUserRegisteredAtMs(),
		VisibleRegionID:           meta.GetVisibleRegionId(),
		RoomLocked:                meta.GetRoomLocked(),
		OwnerUserID:               meta.GetOwnerUserId(),
		CountryID:                 meta.GetCountryId(),
		SenderName:                meta.GetSenderName(),
		SenderAvatar:              meta.GetSenderAvatar(),
		SenderDisplayUserID:       meta.GetSenderDisplayUserId(),
		SenderPrettyDisplayUserID: meta.GetSenderPrettyDisplayUserId(),
		// 充值分层、短时加权与主播风控只消费 wallet-service 已落账的事实，
		// lucky-gift-service 不跨 owner 数据库反查并重新解释同一笔交易。
		Recharge7DCoins:   meta.GetRecharge_7DCoins(),
		Recharge30DCoins:  meta.GetRecharge_30DCoins(),
		LastRechargedAtMS: meta.GetLastRechargedAtMs(),
		// protobuf 为滚动发布保留旧字段号和 getter 名；domain 内只暴露真实的 wallet owner 语义。
		HostPeriodDiamondAdded: meta.GetGiftIncomeCoins(),
	}
}

// AdminLuckyGiftServer 暴露后台幸运礼物规则配置和抽奖审计入口。
type AdminLuckyGiftServer struct {
	luckygiftv1.UnimplementedAdminLuckyGiftServiceServer

	svc *service.Service
}

func NewAdminLuckyGiftServer(svc *service.Service) *AdminLuckyGiftServer {
	return &AdminLuckyGiftServer{svc: svc}
}

func (s *AdminLuckyGiftServer) GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	config, err := s.svc.GetConfig(ctx, req.GetPoolId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.GetLuckyGiftConfigResponse{Config: luckyRuleConfigToProto(config)}, nil
}

func (s *AdminLuckyGiftServer) UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	config := luckyRuleConfigFromProto(req.GetConfig())
	config.CreatedByAdminID = req.GetOperatorAdminId()
	updated, err := s.svc.UpsertConfig(ctx, config)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.UpsertLuckyGiftConfigResponse{Config: luckyRuleConfigToProto(updated)}, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error) {
	rawAppCode := req.GetMeta().GetAppCode()
	if rawAppCode != "" {
		ctx = appcode.WithContext(ctx, rawAppCode)
	}
	configs, err := s.svc.ListConfigs(ctx, rawAppCode)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftConfigsResponse{Configs: make([]*luckygiftv1.LuckyGiftRuleConfig, 0, len(configs))}
	for _, config := range configs {
		resp.Configs = append(resp.Configs, luckyRuleConfigToProto(config))
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	configs, err := s.svc.ListConfigVersions(ctx, req.GetPoolId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftConfigVersionsResponse{Configs: make([]*luckygiftv1.LuckyGiftRuleConfig, 0, len(configs))}
	for _, config := range configs {
		resp.Configs = append(resp.Configs, luckyRuleConfigToProto(config))
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	config, err := s.svc.RollbackConfig(ctx, req.GetPoolId(), req.GetTargetRuleVersion(), req.GetOperatorAdminId())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.RollbackLuckyGiftConfigResponse{
		SourceRuleVersion: req.GetTargetRuleVersion(),
		Config:            luckyRuleConfigToProto(config),
	}, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	draws, total, err := s.svc.ListDraws(ctx, domain.DrawQuery{
		AppCode:        req.GetMeta().GetAppCode(),
		GiftID:         req.GetGiftId(),
		PoolID:         req.GetPoolId(),
		UserID:         req.GetUserId(),
		RoomID:         req.GetRoomId(),
		Status:         req.GetStatus(),
		Page:           req.GetPage(),
		PageSize:       req.GetPageSize(),
		ExternalUserID: req.GetExternalUserId(),
		ExternalOnly:   req.GetExternalOnly(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftDrawsResponse{Draws: make([]*luckygiftv1.LuckyGiftDrawResult, 0, len(draws)), Total: total}
	for _, draw := range draws {
		resp.Draws = append(resp.Draws, luckyDrawResultToProto(draw))
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	summary, err := s.svc.GetDrawSummary(ctx, domain.DrawQuery{
		AppCode:        req.GetMeta().GetAppCode(),
		GiftID:         req.GetGiftId(),
		PoolID:         req.GetPoolId(),
		UserID:         req.GetUserId(),
		RoomID:         req.GetRoomId(),
		Status:         req.GetStatus(),
		ExternalUserID: req.GetExternalUserId(),
		ExternalOnly:   req.GetExternalOnly(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.GetLuckyGiftDrawSummaryResponse{Summary: luckyDrawSummaryToProto(summary)}, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error) {
	appCode := strings.TrimSpace(req.GetMeta().GetAppCode())
	ctx = appcode.WithContext(ctx, appCode)
	profiles, total, nextCursor, hasMore, snapshotAtMS, err := s.svc.ListUserProfiles(ctx, domain.UserProfileQuery{
		AppCode:        appCode,
		PoolID:         req.GetPoolId(),
		IdentityType:   req.GetIdentityType(),
		InternalUserID: req.GetInternalUserId(),
		ExternalUserID: req.GetExternalUserId(),
		Stage:          req.GetStage(),
		Status:         req.GetStatus(),
		SortBy:         req.GetSortBy(),
		SortDirection:  req.GetSortDirection(),
		Cursor:         req.GetCursor(),
		Page:           req.GetPage(),
		PageSize:       req.GetPageSize(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftUserProfilesResponse{
		Profiles:     make([]*luckygiftv1.LuckyGiftUserProfile, 0, len(profiles)),
		Total:        total,
		SnapshotAtMs: snapshotAtMS,
		NextCursor:   nextCursor,
		HasMore:      hasMore,
	}
	for _, profile := range profiles {
		resp.Profiles = append(resp.Profiles, luckyUserProfileToProto(profile))
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) GetLuckyGiftUserProfile(ctx context.Context, req *luckygiftv1.GetLuckyGiftUserProfileRequest) (*luckygiftv1.GetLuckyGiftUserProfileResponse, error) {
	appCode := strings.TrimSpace(req.GetMeta().GetAppCode())
	ctx = appcode.WithContext(ctx, appCode)
	detail, snapshotAtMS, err := s.svc.GetUserProfile(ctx, domain.UserProfileQuery{
		AppCode:         appCode,
		PoolID:          req.GetPoolId(),
		IdentityType:    req.GetIdentityType(),
		InternalUserID:  req.GetInternalUserId(),
		ExternalUserID:  req.GetExternalUserId(),
		JackpotPage:     req.GetJackpotPage(),
		JackpotPageSize: req.GetJackpotPageSize(),
		JackpotCursor:   req.GetJackpotCursor(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.GetLuckyGiftUserProfileResponse{
		Profile:                luckyUserProfileToProto(detail.Profile),
		MultiplierDistribution: make([]*luckygiftv1.LuckyGiftUserMultiplierStat, 0, len(detail.MultiplierDistribution)),
		JackpotHits:            make([]*luckygiftv1.LuckyGiftUserJackpotHit, 0, len(detail.JackpotHits)),
		JackpotHitTotal:        detail.JackpotHitTotal,
		SnapshotAtMs:           snapshotAtMS,
		JackpotPage:            detail.JackpotPage,
		JackpotPageSize:        detail.JackpotPageSize,
		NextJackpotCursor:      detail.NextJackpotCursor,
		JackpotHasMore:         detail.JackpotHasMore,
	}
	for _, stat := range detail.MultiplierDistribution {
		resp.MultiplierDistribution = append(resp.MultiplierDistribution, &luckygiftv1.LuckyGiftUserMultiplierStat{
			HitType: stat.HitType, MultiplierPpm: stat.MultiplierPPM, DrawCount: stat.DrawCount,
			WagerCoins: stat.WagerCoins, PayoutCoins: stat.PayoutCoins,
		})
	}
	for _, hit := range detail.JackpotHits {
		item := &luckygiftv1.LuckyGiftUserJackpotHit{
			DrawId: hit.DrawID, RequestId: hit.RequestID, RuleVersion: hit.RuleVersion, Stage: hit.Stage,
			EventKey:  hit.SourceType + ":" + hit.DrawID,
			Mechanism: hit.Mechanism, ReasonCode: hit.ReasonCode, ReasonSummary: hit.ReasonSummary,
			TierId: hit.TierID, MultiplierPpm: hit.MultiplierPPM, WagerCoins: hit.WagerCoins,
			PayoutCoins: hit.PayoutCoins, OccurredAtMs: hit.OccurredAtMS,
			Conditions: make([]*luckygiftv1.LuckyGiftUserJackpotCondition, 0, len(hit.Conditions)),
		}
		for _, condition := range hit.Conditions {
			item.Conditions = append(item.Conditions, &luckygiftv1.LuckyGiftUserJackpotCondition{
				Name: condition.Name, Numerator: condition.Numerator, Denominator: condition.Denominator,
				RatioPpm: condition.RatioPPM, LimitPpm: condition.LimitPPM, FactorPpm: condition.FactorPPM,
				Passed: condition.Passed, Reason: condition.Reason,
			})
		}
		resp.JackpotHits = append(resp.JackpotHits, item)
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error) {
	appCode := req.GetMeta().GetAppCode()
	if appCode != "" {
		ctx = appcode.WithContext(ctx, appCode)
	}
	pools, err := s.svc.ListPoolBalances(ctx, domain.PoolBalanceQuery{
		AppCode:         appCode,
		PoolID:          req.GetPoolId(),
		StrategyVersion: req.GetStrategyVersion(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftPoolBalancesResponse{Pools: make([]*luckygiftv1.LuckyGiftPoolBalance, 0, len(pools))}
	for _, pool := range pools {
		resp.Pools = append(resp.Pools, luckyPoolBalanceToProto(pool))
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftDailyStats(ctx context.Context, req *luckygiftv1.ListLuckyGiftDailyStatsRequest) (*luckygiftv1.ListLuckyGiftDailyStatsResponse, error) {
	appCode := strings.TrimSpace(req.GetMeta().GetAppCode())
	if appCode == "" {
		// 按日汇总是单租户口径，缺失 app_code 不能静默落到默认 lalu 租户。
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift daily stats app_code is required"))
	}
	ctx = appcode.WithContext(ctx, appCode)
	stats, err := s.svc.ListDailyStats(ctx, domain.DailyStatsQuery{
		AppCode:         appCode,
		PoolID:          req.GetPoolId(),
		StrategyVersion: req.GetStrategyVersion(),
		StartMS:         req.GetStartMs(),
		EndMS:           req.GetEndMs(),
		RegionIDs:       req.GetRegionIds(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftDailyStatsResponse{Stats: make([]*luckygiftv1.LuckyGiftDailyStat, 0, len(stats))}
	for _, stat := range stats {
		resp.Stats = append(resp.Stats, &luckygiftv1.LuckyGiftDailyStat{
			StatDay:        stat.StatDay,
			DrawCount:      stat.DrawCount,
			TurnoverCoin:   stat.TurnoverCoin,
			PayoutCoin:     stat.PayoutCoin,
			BaseRewardCoin: stat.BaseRewardCoin,
		})
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) AdjustLuckyGiftPoolBalance(ctx context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) {
	if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
		// 资金 RPC 必须 fail-closed；缺失 meta 不能像普通兼容查询一样静默落到默认 lalu 租户。
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment app_code is required"))
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	result, err := s.svc.AdjustPoolBalance(ctx, domain.PoolAdjustmentCommand{
		PoolID:          req.GetPoolId(),
		StrategyVersion: req.GetStrategyVersion(),
		AdjustmentID:    req.GetAdjustmentId(),
		Direction:       req.GetDirection(),
		AmountCoins:     req.GetAmountCoins(),
		Reason:          req.GetReason(),
		OperatorAdminID: req.GetOperatorAdminId(),
		RequestID:       req.GetMeta().GetRequestId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{
		Adjustment:       luckyPoolAdjustmentToProto(result.Adjustment),
		Pool:             luckyPoolBalanceToProto(result.Pool),
		IdempotentReplay: result.IdempotentReplay,
	}, nil
}

func (s *AdminLuckyGiftServer) CreateLuckyGiftExperiment(ctx context.Context, req *luckygiftv1.CreateLuckyGiftExperimentRequest) (*luckygiftv1.CreateLuckyGiftExperimentResponse, error) {
	if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
		// 实验创建会发布新配置版本并改变线上分流；缺 meta 不能静默落到默认租户。
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required"))
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	experiment, treatment, err := s.svc.CreateExperiment(ctx, domain.CreateExperimentCommand{
		Name:                req.GetName(),
		TreatmentConfig:     luckyRuleConfigFromProto(req.GetTreatmentConfig()),
		TreatmentTrafficPPM: req.GetTreatmentTrafficPpm(),
		OperatorAdminID:     req.GetOperatorAdminId(),
		Overrides:           luckyExperimentOverridesFromProto(req.GetOverrides()),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.CreateLuckyGiftExperimentResponse{
		Experiment:      luckyExperimentToProto(experiment),
		TreatmentConfig: luckyRuleConfigToProto(treatment),
	}, nil
}

func (s *AdminLuckyGiftServer) ListLuckyGiftExperiments(ctx context.Context, req *luckygiftv1.ListLuckyGiftExperimentsRequest) (*luckygiftv1.ListLuckyGiftExperimentsResponse, error) {
	if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
		// 实验查询按单 app 组织；空 app_code 不能像兼容查询一样静默落到默认租户。
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required"))
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	experiments, err := s.svc.ListExperiments(ctx, domain.ExperimentQuery{
		AppCode:   req.GetMeta().GetAppCode(),
		PoolID:    req.GetPoolId(),
		Status:    req.GetStatus(),
		WithStats: req.GetWithStats(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &luckygiftv1.ListLuckyGiftExperimentsResponse{Experiments: make([]*luckygiftv1.LuckyGiftExperiment, 0, len(experiments))}
	for _, experiment := range experiments {
		resp.Experiments = append(resp.Experiments, luckyExperimentToProto(experiment))
	}
	return resp, nil
}

func (s *AdminLuckyGiftServer) UpdateLuckyGiftExperiment(ctx context.Context, req *luckygiftv1.UpdateLuckyGiftExperimentRequest) (*luckygiftv1.UpdateLuckyGiftExperimentResponse, error) {
	if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required"))
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	// 流量字段用 proto3 optional 的存在性表达"不修改"；缺省 0 绝不能被解释成放量清零。
	trafficPPM := int64(-1)
	if req.TreatmentTrafficPpm != nil {
		trafficPPM = req.GetTreatmentTrafficPpm()
	}
	experiment, err := s.svc.UpdateExperiment(ctx, domain.UpdateExperimentCommand{
		ExperimentID:        req.GetExperimentId(),
		TreatmentTrafficPPM: trafficPPM,
		Status:              req.GetStatus(),
		OperatorAdminID:     req.GetOperatorAdminId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.UpdateLuckyGiftExperimentResponse{Experiment: luckyExperimentToProto(experiment)}, nil
}

func (s *AdminLuckyGiftServer) SetLuckyGiftExperimentOverrides(ctx context.Context, req *luckygiftv1.SetLuckyGiftExperimentOverridesRequest) (*luckygiftv1.SetLuckyGiftExperimentOverridesResponse, error) {
	if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required"))
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	experiment, err := s.svc.SetExperimentOverrides(ctx, domain.SetExperimentOverridesCommand{
		ExperimentID:    req.GetExperimentId(),
		Upserts:         luckyExperimentOverridesFromProto(req.GetUpserts()),
		Removes:         req.GetRemoves(),
		OperatorAdminID: req.GetOperatorAdminId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.SetLuckyGiftExperimentOverridesResponse{Experiment: luckyExperimentToProto(experiment)}, nil
}

func (s *AdminLuckyGiftServer) EndLuckyGiftExperiment(ctx context.Context, req *luckygiftv1.EndLuckyGiftExperimentRequest) (*luckygiftv1.EndLuckyGiftExperimentResponse, error) {
	if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required"))
	}
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	experiment, finalConfig, err := s.svc.EndExperiment(ctx, domain.EndExperimentCommand{
		ExperimentID:    req.GetExperimentId(),
		WinnerArm:       req.GetWinnerArm(),
		OperatorAdminID: req.GetOperatorAdminId(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &luckygiftv1.EndLuckyGiftExperimentResponse{
		Experiment:  luckyExperimentToProto(experiment),
		FinalConfig: luckyRuleConfigToProto(finalConfig),
	}, nil
}

func luckyExperimentOverridesFromProto(overrides []*luckygiftv1.LuckyGiftExperimentOverride) []domain.ExperimentOverride {
	out := make([]domain.ExperimentOverride, 0, len(overrides))
	for _, override := range overrides {
		if override == nil {
			continue
		}
		out = append(out, domain.ExperimentOverride{UserKey: override.GetUserKey(), Arm: override.GetArm()})
	}
	return out
}

func luckyExperimentToProto(experiment domain.Experiment) *luckygiftv1.LuckyGiftExperiment {
	out := &luckygiftv1.LuckyGiftExperiment{
		AppCode:              experiment.AppCode,
		PoolId:               experiment.PoolID,
		ExperimentId:         experiment.ExperimentID,
		Name:                 experiment.Name,
		ControlRuleVersion:   experiment.ControlRuleVersion,
		TreatmentRuleVersion: experiment.TreatmentRuleVersion,
		TreatmentTrafficPpm:  experiment.TreatmentTrafficPPM,
		BucketSalt:           experiment.BucketSalt,
		Status:               experiment.Status,
		WinnerArm:            experiment.WinnerArm,
		WinnerRuleVersion:    experiment.WinnerRuleVersion,
		FinalRuleVersion:     experiment.FinalRuleVersion,
		CreatedByAdminId:     experiment.CreatedByAdminID,
		StartedAtMs:          experiment.StartedAtMS,
		EndedAtMs:            experiment.EndedAtMS,
		UpdatedAtMs:          experiment.UpdatedAtMS,
	}
	for _, override := range experiment.Overrides {
		out.Overrides = append(out.Overrides, &luckygiftv1.LuckyGiftExperimentOverride{UserKey: override.UserKey, Arm: override.Arm})
	}
	for _, stats := range experiment.ArmStats {
		out.ArmStats = append(out.ArmStats, &luckygiftv1.LuckyGiftExperimentArmStats{
			Arm:              stats.Arm,
			RuleVersion:      stats.RuleVersion,
			StrategyVersion:  stats.StrategyVersion,
			TotalDraws:       stats.TotalDraws,
			UniqueUsers:      stats.UniqueUsers,
			TotalSpentCoins:  stats.TotalSpentCoins,
			TotalRewardCoins: stats.TotalRewardCoins,
			ActualRtpPpm:     stats.ActualRTPPPM,
		})
	}
	return out
}

func luckyRuleConfigFromProto(config *luckygiftv1.LuckyGiftRuleConfig) domain.RuleConfig {
	if config == nil {
		return domain.RuleConfig{}
	}
	stages := make([]domain.RuleStage, 0, len(config.GetStages()))
	for _, stage := range config.GetStages() {
		tiers := make([]domain.RuleTier, 0, len(stage.GetTiers()))
		for _, tier := range stage.GetTiers() {
			tiers = append(tiers, domain.RuleTier{
				Stage:         tier.GetStage(),
				TierID:        tier.GetTierId(),
				MultiplierPPM: tier.GetMultiplierPpm(),
				BaseWeightPPM: tier.GetBaseWeightPpm(),
				HighWaterOnly: tier.GetHighWaterOnly(),
				Enabled:       tier.GetEnabled(),
			})
		}
		stages = append(stages, domain.RuleStage{
			Stage:               stage.GetStage(),
			MinRecharge7DCoins:  stage.GetMinRecharge_7DCoins(),
			MinRecharge30DCoins: stage.GetMinRecharge_30DCoins(),
			Tiers:               tiers,
		})
	}
	return domain.RuleConfig{
		AppCode:                        config.GetAppCode(),
		PoolID:                         config.GetPoolId(),
		RuleVersion:                    config.GetRuleVersion(),
		Enabled:                        config.GetEnabled(),
		TargetRTPPPM:                   config.GetTargetRtpPpm(),
		PoolRatePPM:                    config.GetPoolRatePpm(),
		SettlementWindowWager:          config.GetSettlementWindowWager(),
		ControlBandPPM:                 config.GetControlBandPpm(),
		GiftPriceReference:             config.GetGiftPriceReference(),
		NoviceMaxEquivalentDraws:       config.GetNoviceMaxEquivalentDraws(),
		NormalMaxEquivalentDraws:       config.GetNormalMaxEquivalentDraws(),
		EffectiveFromMS:                config.GetEffectiveFromMs(),
		CreatedByAdminID:               config.GetCreatedByAdminId(),
		CreatedAtMS:                    config.GetCreatedAtMs(),
		StrategyVersion:                config.GetStrategyVersion(),
		ProfitRatePPM:                  config.GetProfitRatePpm(),
		AnchorRatePPM:                  config.GetAnchorRatePpm(),
		InitialPoolCoins:               config.GetInitialPoolCoins(),
		LossStreakGuarantee:            config.GetLossStreakGuarantee(),
		LowWatermarkCoins:              config.GetLowWatermarkCoins(),
		LowWaterNonzeroFactorPPM:       config.GetLowWaterNonzeroFactorPpm(),
		HighWatermarkCoins:             config.GetHighWatermarkCoins(),
		HighWaterNonzeroFactorPPM:      config.GetHighWaterNonzeroFactorPpm(),
		V2HighWaterBoostThresholdCoins: config.GetV2HighWaterBoostThresholdCoins(),
		V2HighWaterBoostFactorPPM:      config.GetV2HighWaterBoostFactorPpm(),
		V2HighWaterBoostRecoverCoins:   config.GetV2HighWaterBoostRecoverCoins(),
		RechargeBoostWindowMS:          config.GetRechargeBoostWindowMs(),
		RechargeBoostFactorPPM:         config.GetRechargeBoostFactorPpm(),
		User24HourRTPThresholdPPM:      config.GetUser_24HRtpThresholdPpm(),
		User24HourOrdinaryWinFactorPPM: config.GetUser_24HOrdinaryWinFactorPpm(),
		JackpotMultiplierPPMs:          append([]int64(nil), config.GetJackpotMultiplierPpms()...),
		JackpotGlobalRTPMaxPPM:         config.GetJackpotGlobalRtpMaxPpm(),
		JackpotUserRoundRTPMaxPPM:      config.GetJackpotUserRoundRtpMaxPpm(),
		JackpotUser48hRTPMaxPPM:        config.GetJackpotUser_48HRtpMaxPpm(),
		JackpotSpendThresholdCoins:     config.GetJackpotSpendThresholdCoins(),
		MaxJackpotHitsPerUserDay:       config.GetMaxJackpotHitsPerUserDay(),
		JackpotRateLimitWindowMinutes:  config.GetJackpotRateLimitWindowMinutes(),
		MaxJackpotHitsPerWindow:        config.GetMaxJackpotHitsPerWindow(),
		MaxSinglePayout:                config.GetMaxSinglePayout(),
		UserHourlyPayoutCap:            config.GetUserHourlyPayoutCap(),
		UserDailyPayoutCap:             config.GetUserDailyPayoutCap(),
		DeviceDailyPayoutCap:           config.GetDeviceDailyPayoutCap(),
		RoomHourlyPayoutCap:            config.GetRoomHourlyPayoutCap(),
		AnchorDailyPayoutCap:           config.GetAnchorDailyPayoutCap(),
		Stages:                         stages,
	}
}

func luckyRuleConfigToProto(config domain.RuleConfig) *luckygiftv1.LuckyGiftRuleConfig {
	roundRTPMaxPPM := config.JackpotUserRoundRTPMaxPPM
	if roundRTPMaxPPM == 0 {
		roundRTPMaxPPM = config.JackpotUserDayRTPMaxPPM
	}
	rolling48hRTPMaxPPM := config.JackpotUser48hRTPMaxPPM
	if rolling48hRTPMaxPPM == 0 {
		rolling48hRTPMaxPPM = config.JackpotUser72hRTPMaxPPM
	}
	stages := make([]*luckygiftv1.LuckyGiftRuleStage, 0, len(config.Stages))
	for _, stage := range config.Stages {
		tiers := make([]*luckygiftv1.LuckyGiftRuleTier, 0, len(stage.Tiers))
		for _, tier := range stage.Tiers {
			tiers = append(tiers, &luckygiftv1.LuckyGiftRuleTier{
				Stage:         tier.Stage,
				TierId:        tier.TierID,
				MultiplierPpm: tier.MultiplierPPM,
				BaseWeightPpm: tier.BaseWeightPPM,
				HighWaterOnly: tier.HighWaterOnly,
				Enabled:       tier.Enabled,
			})
		}
		stages = append(stages, &luckygiftv1.LuckyGiftRuleStage{
			Stage:                stage.Stage,
			MinRecharge_7DCoins:  stage.MinRecharge7DCoins,
			MinRecharge_30DCoins: stage.MinRecharge30DCoins,
			Tiers:                tiers,
		})
	}
	return &luckygiftv1.LuckyGiftRuleConfig{
		AppCode:                        config.AppCode,
		PoolId:                         config.PoolID,
		RuleVersion:                    config.RuleVersion,
		Enabled:                        config.Enabled,
		TargetRtpPpm:                   config.TargetRTPPPM,
		PoolRatePpm:                    config.PoolRatePPM,
		SettlementWindowWager:          config.SettlementWindowWager,
		ControlBandPpm:                 config.ControlBandPPM,
		GiftPriceReference:             config.GiftPriceReference,
		NoviceMaxEquivalentDraws:       config.NoviceMaxEquivalentDraws,
		NormalMaxEquivalentDraws:       config.NormalMaxEquivalentDraws,
		EffectiveFromMs:                config.EffectiveFromMS,
		CreatedByAdminId:               config.CreatedByAdminID,
		CreatedAtMs:                    config.CreatedAtMS,
		StrategyVersion:                config.StrategyVersion,
		ProfitRatePpm:                  config.ProfitRatePPM,
		AnchorRatePpm:                  config.AnchorRatePPM,
		InitialPoolCoins:               config.InitialPoolCoins,
		LossStreakGuarantee:            config.LossStreakGuarantee,
		LowWatermarkCoins:              config.LowWatermarkCoins,
		LowWaterNonzeroFactorPpm:       config.LowWaterNonzeroFactorPPM,
		HighWatermarkCoins:             config.HighWatermarkCoins,
		HighWaterNonzeroFactorPpm:      config.HighWaterNonzeroFactorPPM,
		V2HighWaterBoostThresholdCoins: config.V2HighWaterBoostThresholdCoins,
		V2HighWaterBoostFactorPpm:      config.V2HighWaterBoostFactorPPM,
		V2HighWaterBoostRecoverCoins:   config.V2HighWaterBoostRecoverCoins,
		RechargeBoostWindowMs:          config.RechargeBoostWindowMS,
		RechargeBoostFactorPpm:         config.RechargeBoostFactorPPM,
		User_24HRtpThresholdPpm:        config.User24HourRTPThresholdPPM,
		User_24HOrdinaryWinFactorPpm:   config.User24HourOrdinaryWinFactorPPM,
		JackpotMultiplierPpms:          append([]int64(nil), config.JackpotMultiplierPPMs...),
		JackpotGlobalRtpMaxPpm:         config.JackpotGlobalRTPMaxPPM,
		JackpotUserRoundRtpMaxPpm:      roundRTPMaxPPM,
		JackpotUser_48HRtpMaxPpm:       rolling48hRTPMaxPPM,
		JackpotSpendThresholdCoins:     config.JackpotSpendThresholdCoins,
		MaxJackpotHitsPerUserDay:       config.MaxJackpotHitsPerUserDay,
		JackpotRateLimitWindowMinutes:  config.JackpotRateLimitWindowMinutes,
		MaxJackpotHitsPerWindow:        config.MaxJackpotHitsPerWindow,
		MaxSinglePayout:                config.MaxSinglePayout,
		UserHourlyPayoutCap:            config.UserHourlyPayoutCap,
		UserDailyPayoutCap:             config.UserDailyPayoutCap,
		DeviceDailyPayoutCap:           config.DeviceDailyPayoutCap,
		RoomHourlyPayoutCap:            config.RoomHourlyPayoutCap,
		AnchorDailyPayoutCap:           config.AnchorDailyPayoutCap,
		Stages:                         stages,
	}
}

func luckyDrawResultToProto(result domain.DrawResult) *luckygiftv1.LuckyGiftDrawResult {
	response := &luckygiftv1.LuckyGiftDrawResult{
		DrawId:               result.DrawID,
		CommandId:            result.CommandID,
		AppCode:              result.AppCode,
		UserId:               result.UserID,
		ExternalUserId:       result.ExternalUserID,
		PoolId:               result.PoolID,
		GiftId:               result.GiftID,
		RuleVersion:          result.RuleVersion,
		ExperiencePool:       result.ExperiencePool,
		SelectedTierId:       result.SelectedTierID,
		MultiplierPpm:        result.MultiplierPPM,
		BaseRewardCoins:      result.BaseRewardCoins,
		EffectiveRewardCoins: result.EffectiveRewardCoins,
		RewardStatus:         result.RewardStatus,
		RtpWindowIndex:       result.RTPWindowIndex,
		GiftRtpWindowIndex:   result.GiftRTPWindowIndex,
		GlobalBaseRtpPpm:     result.GlobalBaseRTPPPM,
		GiftBaseRtpPpm:       result.GiftBaseRTPPPM,
		StageFeedback:        result.StageFeedback,
		HighMultiplier:       result.HighMultiplier,
		CreatedAtMs:          result.CreatedAtMS,
		WalletTransactionId:  result.WalletTransactionID,
		CoinBalanceAfter:     result.CoinBalanceAfter,
	}
	for _, hit := range result.Hits {
		response.Hits = append(response.Hits, &luckygiftv1.LuckyGiftHit{
			GiftIndex: hit.GiftIndex, DrawId: hit.DrawID, SelectedTierId: hit.SelectedTierID,
			MultiplierPpm: hit.MultiplierPPM, RewardCoins: hit.RewardCoins,
		})
	}
	return response
}

func externalDrawResultToProto(result domain.ExternalDrawResult) *luckygiftv1.ExternalGiftDrawResponse {
	return &luckygiftv1.ExternalGiftDrawResponse{
		DrawId:         result.DrawID,
		RequestId:      result.RequestID,
		AppCode:        result.AppCode,
		ExternalUserId: result.ExternalUserID,
		GiftCount:      result.GiftCount,
		UnitAmount:     result.UnitAmount,
		TotalAmount:    result.TotalAmount,
		RewardAmount:   result.RewardAmount,
		MultiplierPpm:  result.MultiplierPPM,
		RewardStatus:   result.RewardStatus,
		RuleVersion:    result.RuleVersion,
		CreatedAtMs:    result.CreatedAtMS,
	}
}

func luckyDrawSummaryToProto(summary domain.DrawSummary) *luckygiftv1.LuckyGiftDrawSummary {
	return &luckygiftv1.LuckyGiftDrawSummary{
		PoolId:           summary.PoolID,
		TotalDraws:       summary.TotalDraws,
		UniqueUsers:      summary.UniqueUsers,
		UniqueRooms:      summary.UniqueRooms,
		TotalSpentCoins:  summary.TotalSpentCoins,
		TotalRewardCoins: summary.TotalRewardCoins,
		BaseRewardCoins:  summary.BaseRewardCoins,
		ActualRtpPpm:     summary.ActualRTPPPM,
		PendingDraws:     summary.PendingDraws,
		GrantedDraws:     summary.GrantedDraws,
		FailedDraws:      summary.FailedDraws,
	}
}

func luckyPoolBalanceToProto(pool domain.PoolBalance) *luckygiftv1.LuckyGiftPoolBalance {
	return &luckygiftv1.LuckyGiftPoolBalance{
		AppCode:           pool.AppCode,
		PoolId:            pool.PoolID,
		StrategyVersion:   pool.StrategyVersion,
		Balance:           pool.Balance,
		ReserveFloor:      pool.ReserveFloor,
		AvailableBalance:  pool.AvailableBalance,
		TotalIn:           pool.TotalIn,
		TotalOut:          pool.TotalOut,
		ManualCreditTotal: pool.ManualCreditTotal,
		ManualDebitTotal:  pool.ManualDebitTotal,
		Materialized:      pool.Materialized,
		UpdatedAtMs:       pool.UpdatedAtMS,
	}
}

func luckyUserProfileWindowToProto(window domain.UserProfileWindow) *luckygiftv1.LuckyGiftUserProfileWindow {
	return &luckygiftv1.LuckyGiftUserProfileWindow{
		Draws: window.Draws, WagerCoins: window.WagerCoins, PayoutCoins: window.PayoutCoins,
		NetCoins: window.NetCoins, RtpPpm: window.RTPPPM, HasRtp: window.HasRTP,
	}
}

func luckyUserProfileToProto(profile domain.UserProfile) *luckygiftv1.LuckyGiftUserProfile {
	// StrategyUserID 可能是 external_user_id 的稳定哈希，只允许仓储层关联 lucky_user_states；
	// transport 显式逐字段映射且没有该字段，避免未来通过反射或 JSON 误下发。
	return &luckygiftv1.LuckyGiftUserProfile{
		AppCode: profile.AppCode, PoolId: profile.PoolID, IdentityType: profile.IdentityType,
		InternalUserId: profile.InternalUserID, ExternalUserId: profile.ExternalUserID,
		RuleVersion: profile.RuleVersion, StrategyVersion: profile.StrategyVersion, Stage: profile.Stage,
		PaidDraws: profile.PaidDraws, EquivalentDraws: profile.EquivalentDraws, LossStreak: profile.LossStreak,
		PendingSpendJackpotTokens: profile.PendingSpendJackpotTokens,
		GuaranteeDrawsRemaining:   profile.GuaranteeDrawsRemaining,
		DownweightActive:          profile.DownweightActive, DownweightFactorPpm: profile.DownweightFactorPPM,
		User_24HRtpThresholdPpm: profile.User24HourRTPThresholdPPM,
		OneHour:                 luckyUserProfileWindowToProto(profile.OneHour),
		TwentyFourHours:         luckyUserProfileWindowToProto(profile.TwentyFourHour),
		FortyEightHours:         luckyUserProfileWindowToProto(profile.FortyEightHour),
		Lifetime:                luckyUserProfileWindowToProto(profile.Lifetime),
		BaseRewardCoins:         profile.BaseRewardCoins, OrdinaryWinCount: profile.OrdinaryWinCount,
		HighMultiplierOrdinaryWinCount: profile.HighMultiplierOrdinaryWinCount,
		RtpCompensationJackpotCount:    profile.RTPCompensationJackpotCount,
		CumulativeSpendJackpotCount:    profile.CumulativeSpendJackpotCount,
		GuaranteeHitCount:              profile.GuaranteeHitCount, ZeroDrawCount: profile.ZeroDrawCount,
		GrantedDrawCount: profile.GrantedDrawCount, PendingDrawCount: profile.PendingDrawCount,
		FailedDrawCount: profile.FailedDrawCount, MaxMultiplierPpm: profile.MaxMultiplierPPM,
		MaxRewardCoins: profile.MaxRewardCoins, FirstDrawAtMs: profile.FirstDrawAtMS,
		LastDrawAtMs: profile.LastDrawAtMS, AggregatedAtMs: profile.AggregatedAtMS, DataLagMs: profile.DataLagMS,
	}
}

func luckyPoolAdjustmentToProto(adjustment domain.PoolAdjustment) *luckygiftv1.LuckyGiftPoolAdjustment {
	return &luckygiftv1.LuckyGiftPoolAdjustment{
		AdjustmentId:    adjustment.AdjustmentID,
		AppCode:         adjustment.AppCode,
		PoolId:          adjustment.PoolID,
		StrategyVersion: adjustment.StrategyVersion,
		Direction:       adjustment.Direction,
		AmountCoins:     adjustment.AmountCoins,
		Reason:          adjustment.Reason,
		OperatorAdminId: adjustment.OperatorAdminID,
		BalanceBefore:   adjustment.BalanceBefore,
		BalanceAfter:    adjustment.BalanceAfter,
		CreatedAtMs:     adjustment.CreatedAtMS,
	}
}
