package luckygift

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/userclient"
	"hyapp-admin-server/internal/middleware"
	"hyapp-admin-server/internal/modules/shared"
	"hyapp-admin-server/internal/response"
	luckygiftv1 "hyapp.local/api/proto/luckygift/v1"

	"github.com/gin-gonic/gin"
	"google.golang.org/grpc/codes"
	grpcstatus "google.golang.org/grpc/status"
)

type luckyGiftInternalUserDTO struct {
	// UserID 使用字符串返回，避免浏览器把 64 位长号转成 JavaScript number 后丢失末位精度。
	UserID              string `json:"user_id"`
	DisplayUserID       string `json:"display_user_id"`
	DefaultDisplayID    string `json:"default_display_user_id"`
	PrettyID            string `json:"pretty_id"`
	PrettyDisplayUserID string `json:"pretty_display_user_id"`
	Name                string `json:"name"`
	Avatar              string `json:"avatar"`
	Status              string `json:"status"`
}

type luckyGiftUserProfileWindowDTO struct {
	Draws       int64 `json:"draws"`
	WagerCoins  int64 `json:"wager_coins"`
	PayoutCoins int64 `json:"payout_coins"`
	NetCoins    int64 `json:"net_coins"`
	RTPPPM      int64 `json:"rtp_ppm"`
	HasRTP      bool  `json:"has_rtp"`
}

type luckyGiftUserProfileDTO struct {
	AppCode        string                    `json:"app_code"`
	PoolID         string                    `json:"pool_id"`
	IdentityType   string                    `json:"identity_type"`
	InternalUserID string                    `json:"internal_user_id,omitempty"`
	ExternalUserID string                    `json:"external_user_id,omitempty"`
	InternalUser   *luckyGiftInternalUserDTO `json:"internal_user,omitempty"`

	RuleVersion               int64                         `json:"rule_version"`
	StrategyVersion           string                        `json:"strategy_version"`
	Stage                     string                        `json:"stage"`
	PaidDraws                 int64                         `json:"paid_draws"`
	EquivalentDraws           int64                         `json:"equivalent_draws"`
	LossStreak                int64                         `json:"loss_streak"`
	PendingSpendJackpotTokens int64                         `json:"pending_spend_jackpot_tokens"`
	GuaranteeDrawsRemaining   int64                         `json:"guarantee_draws_remaining"`
	DownweightActive          bool                          `json:"downweight_active"`
	DownweightFactorPPM       int64                         `json:"downweight_factor_ppm"`
	User24HourRTPThresholdPPM int64                         `json:"user_24h_rtp_threshold_ppm"`
	CurrentRTPPPM             int64                         `json:"current_rtp_ppm"`
	HasCurrentRTP             bool                          `json:"has_current_rtp"`
	OneHour                   luckyGiftUserProfileWindowDTO `json:"one_hour"`
	TwentyFourHours           luckyGiftUserProfileWindowDTO `json:"twenty_four_hours"`
	FortyEightHours           luckyGiftUserProfileWindowDTO `json:"forty_eight_hours"`
	Lifetime                  luckyGiftUserProfileWindowDTO `json:"lifetime"`

	BaseRewardCoins                int64 `json:"base_reward_coins"`
	OrdinaryWinCount               int64 `json:"ordinary_win_count"`
	HighMultiplierOrdinaryWinCount int64 `json:"high_multiplier_ordinary_win_count"`
	RTPCompensationJackpotCount    int64 `json:"rtp_compensation_jackpot_count"`
	CumulativeSpendJackpotCount    int64 `json:"cumulative_spend_jackpot_count"`
	JackpotWinCount                int64 `json:"jackpot_win_count"`
	GuaranteeHitCount              int64 `json:"guarantee_hit_count"`
	ZeroDrawCount                  int64 `json:"zero_draw_count"`
	GrantedDrawCount               int64 `json:"granted_draw_count"`
	PendingDrawCount               int64 `json:"pending_draw_count"`
	FailedDrawCount                int64 `json:"failed_draw_count"`
	MaxMultiplierPPM               int64 `json:"max_multiplier_ppm"`
	MaxRewardCoins                 int64 `json:"max_reward_coins"`
	FirstDrawAtMS                  int64 `json:"first_draw_at_ms"`
	LastDrawAtMS                   int64 `json:"last_draw_at_ms"`
	AggregatedAtMS                 int64 `json:"aggregated_at_ms"`
	DataLagMS                      int64 `json:"data_lag_ms"`
}

type luckyGiftUserProfilePageDTO struct {
	Items        []luckyGiftUserProfileDTO `json:"items"`
	Page         int                       `json:"page"`
	PageSize     int                       `json:"page_size"`
	Total        int64                     `json:"total"`
	SnapshotAtMS int64                     `json:"snapshot_at_ms"`
	NextCursor   string                    `json:"next_cursor,omitempty"`
	HasMore      bool                      `json:"has_more"`
}

type luckyGiftUserMultiplierStatDTO struct {
	HitType       string `json:"hit_type"`
	HitTypeLabel  string `json:"hit_type_label"`
	MultiplierPPM int64  `json:"multiplier_ppm"`
	DrawCount     int64  `json:"draw_count"`
	WagerCoins    int64  `json:"wager_coins"`
	PayoutCoins   int64  `json:"payout_coins"`
}

type luckyGiftUserJackpotConditionDTO struct {
	Name        string `json:"name"`
	Label       string `json:"label"`
	Actual      string `json:"actual"`
	Threshold   string `json:"threshold"`
	Operator    string `json:"operator"`
	Numerator   int64  `json:"numerator"`
	Denominator int64  `json:"denominator"`
	RatioPPM    int64  `json:"ratio_ppm"`
	LimitPPM    int64  `json:"limit_ppm"`
	FactorPPM   int64  `json:"factor_ppm"`
	Factor      string `json:"factor"`
	Passed      bool   `json:"passed"`
	Reason      string `json:"reason"`
}

type luckyGiftUserJackpotHitDTO struct {
	EventKey       string                             `json:"event_key"`
	DrawID         string                             `json:"draw_id"`
	RequestID      string                             `json:"request_id"`
	RuleVersion    int64                              `json:"rule_version"`
	Stage          string                             `json:"stage"`
	Mechanism      string                             `json:"mechanism"`
	MechanismLabel string                             `json:"mechanism_label"`
	ReasonCode     string                             `json:"reason_code"`
	ReasonSummary  string                             `json:"reason_summary"`
	TierID         string                             `json:"tier_id"`
	MultiplierPPM  int64                              `json:"multiplier_ppm"`
	WagerCoins     int64                              `json:"wager_coins"`
	PayoutCoins    int64                              `json:"payout_coins"`
	OccurredAtMS   int64                              `json:"occurred_at_ms"`
	Conditions     []luckyGiftUserJackpotConditionDTO `json:"conditions"`
}

type luckyGiftUserProfileDetailDTO struct {
	Profile                luckyGiftUserProfileDTO          `json:"profile"`
	MultiplierDistribution []luckyGiftUserMultiplierStatDTO `json:"multiplier_distribution"`
	JackpotHits            []luckyGiftUserJackpotHitDTO     `json:"jackpot_hits"`
	JackpotHitTotal        int64                            `json:"jackpot_hit_total"`
	JackpotPage            int32                            `json:"jackpot_page"`
	JackpotPageSize        int32                            `json:"jackpot_page_size"`
	NextJackpotCursor      string                           `json:"next_jackpot_cursor,omitempty"`
	JackpotHasMore         bool                             `json:"jackpot_has_more"`
	SnapshotAtMS           int64                            `json:"snapshot_at_ms"`
}

// ListLuckyGiftUserProfiles 查询已经由 lucky-gift-service 增量物化的用户画像。列表只按 app+pool
// 读取聚合表；内部账号的短号/靓号解析和资料补全统一走 user-service，不在 HTTP 请求里扫描开奖事实表。
func (h *Handler) ListLuckyGiftUserProfiles(c *gin.Context) {
	appCode, poolID, ok := requiredLuckyGiftProfileScope(c)
	if !ok {
		return
	}
	identityType, ok := normalizeLuckyGiftProfileIdentity(firstNonEmpty(c.Query("identity_type"), c.Query("source")))
	if !ok {
		response.BadRequest(c, "identity_type must be all, internal or external")
		return
	}
	options := shared.ListOptions(c)
	if options.PageSize > 100 {
		options.PageSize = 100
	}
	ctx, cancel := h.luckyGiftContext(c)
	defer cancel()
	ctx = appctx.WithContext(ctx, appCode)

	internalUserID := parseOptionalInt64(firstNonEmpty(c.Query("internal_user_id"), c.Query("user_id")))
	externalUserID := strings.TrimSpace(c.Query("external_user_id"))
	keyword := strings.TrimSpace(c.Query("user_keyword"))
	if keyword != "" {
		resolvedInternalID, resolvedExternalID, internalMatched, err := h.resolveLuckyGiftProfileKeyword(ctx, appCode, identityType, keyword, middleware.CurrentRequestID(c))
		if err != nil {
			response.ServerError(c, "搜索用户失败")
			return
		}
		internalUserID, externalUserID = resolvedInternalID, resolvedExternalID
		if identityType == "internal" && !internalMatched {
			// 未解析的内部短号/长号/靓号必须返回空页，不能把“查无此人”退化为无条件全量列表。
			response.OK(c, luckyGiftUserProfilePageDTO{Items: []luckyGiftUserProfileDTO{}, Page: options.Page, PageSize: options.PageSize})
			return
		}
	}

	resp, err := h.luckyGift.ListLuckyGiftUserProfiles(ctx, &luckygiftv1.ListLuckyGiftUserProfilesRequest{
		Meta:           h.metaForApp(c, appCode),
		PoolId:         poolID,
		IdentityType:   identityType,
		InternalUserId: internalUserID,
		ExternalUserId: externalUserID,
		Stage:          strings.TrimSpace(c.Query("stage")),
		Status:         strings.TrimSpace(c.Query("status")),
		SortBy:         strings.TrimSpace(c.Query("sort_by")),
		SortDirection:  strings.TrimSpace(c.Query("sort_direction")),
		Page:           int32(options.Page),
		PageSize:       int32(options.PageSize),
		Cursor:         strings.TrimSpace(c.Query("cursor")),
	})
	if err != nil {
		h.writeLuckyGiftUserProfileError(c, err, "获取幸运礼物用户画像失败")
		return
	}

	users, err := h.loadLuckyGiftInternalUsers(ctx, resp.GetProfiles(), middleware.CurrentRequestID(c))
	if err != nil {
		response.ServerError(c, "获取用户资料失败")
		return
	}
	items := make([]luckyGiftUserProfileDTO, 0, len(resp.GetProfiles()))
	for _, profile := range resp.GetProfiles() {
		items = append(items, luckyGiftUserProfileFromProto(profile, users[profile.GetInternalUserId()]))
	}
	response.OK(c, luckyGiftUserProfilePageDTO{
		Items: items, Page: options.Page, PageSize: options.PageSize,
		Total: resp.GetTotal(), SnapshotAtMS: resp.GetSnapshotAtMs(),
		NextCursor: resp.GetNextCursor(), HasMore: resp.GetHasMore(),
	})
}

// GetLuckyGiftUserProfile 返回单个用户的倍率分布和大奖解释。大奖明细只在展开行时请求，
// 避免列表页同时解码所有用户的条件 trace，保证高密度页面仍保持稳定响应时间。
func (h *Handler) GetLuckyGiftUserProfile(c *gin.Context) {
	appCode, poolID, ok := requiredLuckyGiftProfileScope(c)
	if !ok {
		return
	}
	identityType, ok := normalizeLuckyGiftProfileIdentity(firstNonEmpty(c.Query("identity_type"), c.Query("source")))
	if !ok || identityType == "" {
		response.BadRequest(c, "identity_type must be internal or external")
		return
	}
	ctx, cancel := h.luckyGiftContext(c)
	defer cancel()
	ctx = appctx.WithContext(ctx, appCode)

	internalUserID := parseOptionalInt64(firstNonEmpty(c.Query("internal_user_id"), c.Query("user_id")))
	externalUserID := strings.TrimSpace(c.Query("external_user_id"))
	if keyword := strings.TrimSpace(c.Query("user_keyword")); keyword != "" {
		resolvedInternalID, resolvedExternalID, internalMatched, err := h.resolveLuckyGiftProfileKeyword(ctx, appCode, identityType, keyword, middleware.CurrentRequestID(c))
		if err != nil {
			response.ServerError(c, "搜索用户失败")
			return
		}
		internalUserID, externalUserID = resolvedInternalID, resolvedExternalID
		if identityType == "internal" && !internalMatched {
			response.NotFound(c, "未找到该用户的幸运礼物画像")
			return
		}
	}
	if (identityType == "internal" && internalUserID <= 0) || (identityType == "external" && externalUserID == "") {
		response.BadRequest(c, "internal_user_id or external_user_id is required")
		return
	}
	jackpotPage := parseLuckyGiftProfilePositiveInt(c.Query("jackpot_page"), 1, 1_000_000)
	jackpotPageSize := parseLuckyGiftProfilePositiveInt(c.Query("jackpot_page_size"), 20, 50)

	resp, err := h.luckyGift.GetLuckyGiftUserProfile(ctx, &luckygiftv1.GetLuckyGiftUserProfileRequest{
		Meta: h.metaForApp(c, appCode), PoolId: poolID, IdentityType: identityType,
		InternalUserId: internalUserID, ExternalUserId: externalUserID,
		JackpotPage: int32(jackpotPage), JackpotPageSize: int32(jackpotPageSize),
		JackpotCursor: strings.TrimSpace(c.Query("jackpot_cursor")),
	})
	if err != nil {
		h.writeLuckyGiftUserProfileError(c, err, "获取幸运礼物用户画像详情失败")
		return
	}
	users, err := h.loadLuckyGiftInternalUsers(ctx, []*luckygiftv1.LuckyGiftUserProfile{resp.GetProfile()}, middleware.CurrentRequestID(c))
	if err != nil {
		response.ServerError(c, "获取用户资料失败")
		return
	}
	detail := luckyGiftUserProfileDetailDTO{
		Profile:                luckyGiftUserProfileFromProto(resp.GetProfile(), users[resp.GetProfile().GetInternalUserId()]),
		MultiplierDistribution: make([]luckyGiftUserMultiplierStatDTO, 0, len(resp.GetMultiplierDistribution())),
		JackpotHits:            make([]luckyGiftUserJackpotHitDTO, 0, len(resp.GetJackpotHits())),
		JackpotHitTotal:        resp.GetJackpotHitTotal(), JackpotPage: resp.GetJackpotPage(),
		JackpotPageSize: resp.GetJackpotPageSize(), NextJackpotCursor: resp.GetNextJackpotCursor(),
		JackpotHasMore: resp.GetJackpotHasMore(), SnapshotAtMS: resp.GetSnapshotAtMs(),
	}
	for _, stat := range resp.GetMultiplierDistribution() {
		detail.MultiplierDistribution = append(detail.MultiplierDistribution, luckyGiftUserMultiplierStatFromProto(stat))
	}
	for _, hit := range resp.GetJackpotHits() {
		detail.JackpotHits = append(detail.JackpotHits, luckyGiftUserJackpotHitFromProto(hit))
	}
	response.OK(c, detail)
}

func parseLuckyGiftProfilePositiveInt(value string, fallback, maximum int) int {
	parsed, err := strconv.Atoi(strings.TrimSpace(value))
	if err != nil || parsed <= 0 {
		return fallback
	}
	if parsed > maximum {
		return maximum
	}
	return parsed
}

func requiredLuckyGiftProfileScope(c *gin.Context) (string, string, bool) {
	appCode := strings.ToLower(strings.TrimSpace(c.Query("app_code")))
	poolID := strings.TrimSpace(c.Query("pool_id"))
	if appCode == "" || poolID == "" {
		response.BadRequest(c, "app_code and pool_id are required")
		return "", "", false
	}
	return appCode, poolID, true
}

func normalizeLuckyGiftProfileIdentity(value string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "", "all":
		// owner 服务用空值表达“内部与外部”，避免引入第三种持久身份类型。
		return "", true
	case "internal", "external":
		return strings.ToLower(strings.TrimSpace(value)), true
	default:
		return "", false
	}
}

func (h *Handler) resolveLuckyGiftProfileKeyword(ctx context.Context, appCode, identityType, keyword, requestID string) (int64, string, bool, error) {
	var externalUserID string
	if identityType == "" || identityType == "external" {
		// 外部接入没有 HyApp 用户主数据，原样按 external_user_id 精确匹配；稳定策略哈希永不出 owner 服务。
		externalUserID = keyword
	}
	if identityType == "external" {
		return 0, externalUserID, false, nil
	}
	if h.users == nil {
		return 0, externalUserID, false, fmt.Errorf("lucky gift user profile resolver is not configured")
	}
	identity, err := h.users.ResolveAdminUserIdentifier(appctx.WithContext(ctx, appCode), userclient.ResolveAdminUserIdentifierRequest{
		RequestID: requestID, Caller: "admin-server", UserIdentifier: keyword,
	})
	if err == nil && identity != nil && identity.UserID > 0 {
		return identity.UserID, externalUserID, true, nil
	}
	if err != nil {
		switch grpcstatus.Code(err) {
		case codes.NotFound, codes.InvalidArgument:
			// all 模式下 keyword 也可能是任意 external_user_id；内部展示号格式不合法只代表内部未命中，不能阻断外部精确搜索。
			return 0, externalUserID, false, nil
		default:
			return 0, externalUserID, false, err
		}
	}
	return 0, externalUserID, false, nil
}

func (h *Handler) loadLuckyGiftInternalUsers(ctx context.Context, profiles []*luckygiftv1.LuckyGiftUserProfile, requestID string) (map[int64]*userclient.User, error) {
	ids := make([]int64, 0, len(profiles))
	seen := make(map[int64]struct{}, len(profiles))
	for _, profile := range profiles {
		if profile == nil || profile.GetIdentityType() != "internal" || profile.GetInternalUserId() <= 0 {
			continue
		}
		if _, exists := seen[profile.GetInternalUserId()]; exists {
			continue
		}
		seen[profile.GetInternalUserId()] = struct{}{}
		ids = append(ids, profile.GetInternalUserId())
	}
	if len(ids) == 0 {
		return map[int64]*userclient.User{}, nil
	}
	if h.users == nil {
		return nil, fmt.Errorf("lucky gift user profile user client is not configured")
	}
	return h.users.BatchGetUsers(ctx, userclient.BatchGetUsersRequest{RequestID: requestID, Caller: "admin-server", UserIDs: ids})
}

func luckyGiftUserProfileFromProto(profile *luckygiftv1.LuckyGiftUserProfile, user *userclient.User) luckyGiftUserProfileDTO {
	if profile == nil {
		return luckyGiftUserProfileDTO{}
	}
	jackpotCount := profile.GetRtpCompensationJackpotCount() + profile.GetCumulativeSpendJackpotCount()
	dto := luckyGiftUserProfileDTO{
		AppCode: profile.GetAppCode(), PoolID: profile.GetPoolId(), IdentityType: profile.GetIdentityType(),
		RuleVersion:     profile.GetRuleVersion(),
		StrategyVersion: profile.GetStrategyVersion(), Stage: profile.GetStage(), PaidDraws: profile.GetPaidDraws(),
		EquivalentDraws: profile.GetEquivalentDraws(), LossStreak: profile.GetLossStreak(),
		PendingSpendJackpotTokens: profile.GetPendingSpendJackpotTokens(), GuaranteeDrawsRemaining: profile.GetGuaranteeDrawsRemaining(),
		DownweightActive: profile.GetDownweightActive(), DownweightFactorPPM: profile.GetDownweightFactorPpm(),
		User24HourRTPThresholdPPM: profile.GetUser_24HRtpThresholdPpm(),
		OneHour:                   luckyGiftUserProfileWindowFromProto(profile.GetOneHour()),
		TwentyFourHours:           luckyGiftUserProfileWindowFromProto(profile.GetTwentyFourHours()),
		FortyEightHours:           luckyGiftUserProfileWindowFromProto(profile.GetFortyEightHours()),
		Lifetime:                  luckyGiftUserProfileWindowFromProto(profile.GetLifetime()),
		BaseRewardCoins:           profile.GetBaseRewardCoins(), OrdinaryWinCount: profile.GetOrdinaryWinCount(),
		HighMultiplierOrdinaryWinCount: profile.GetHighMultiplierOrdinaryWinCount(),
		RTPCompensationJackpotCount:    profile.GetRtpCompensationJackpotCount(),
		CumulativeSpendJackpotCount:    profile.GetCumulativeSpendJackpotCount(), JackpotWinCount: jackpotCount,
		GuaranteeHitCount: profile.GetGuaranteeHitCount(), ZeroDrawCount: profile.GetZeroDrawCount(),
		GrantedDrawCount: profile.GetGrantedDrawCount(), PendingDrawCount: profile.GetPendingDrawCount(), FailedDrawCount: profile.GetFailedDrawCount(),
		MaxMultiplierPPM: profile.GetMaxMultiplierPpm(), MaxRewardCoins: profile.GetMaxRewardCoins(),
		FirstDrawAtMS: profile.GetFirstDrawAtMs(), LastDrawAtMS: profile.GetLastDrawAtMs(),
		AggregatedAtMS: profile.GetAggregatedAtMs(), DataLagMS: profile.GetDataLagMs(),
		CurrentRTPPPM: profile.GetTwentyFourHours().GetRtpPpm(), HasCurrentRTP: profile.GetTwentyFourHours().GetHasRtp(),
	}
	if profile.GetIdentityType() == "internal" && profile.GetInternalUserId() > 0 {
		// 外部画像即使 owner 发生协议回归误填了 internal_user_id，也不得在 admin HTTP 边界透出策略哈希。
		dto.InternalUserID = strconv.FormatInt(profile.GetInternalUserId(), 10)
		dto.InternalUser = luckyGiftInternalUserFromClient(profile.GetInternalUserId(), user)
	} else if profile.GetIdentityType() == "external" {
		dto.ExternalUserID = profile.GetExternalUserId()
	}
	return dto
}

func luckyGiftInternalUserFromClient(userID int64, user *userclient.User) *luckyGiftInternalUserDTO {
	dto := &luckyGiftInternalUserDTO{UserID: strconv.FormatInt(userID, 10)}
	if user == nil {
		return dto
	}
	dto.DisplayUserID = user.DisplayUserID
	dto.DefaultDisplayID = user.DefaultDisplayUserID
	dto.PrettyID = user.PrettyID
	dto.PrettyDisplayUserID = user.PrettyDisplayUserID
	dto.Name = user.Username
	dto.Avatar = user.Avatar
	dto.Status = user.Status
	return dto
}

func luckyGiftUserProfileWindowFromProto(window *luckygiftv1.LuckyGiftUserProfileWindow) luckyGiftUserProfileWindowDTO {
	if window == nil {
		return luckyGiftUserProfileWindowDTO{}
	}
	return luckyGiftUserProfileWindowDTO{
		Draws: window.GetDraws(), WagerCoins: window.GetWagerCoins(), PayoutCoins: window.GetPayoutCoins(),
		NetCoins: window.GetNetCoins(), RTPPPM: window.GetRtpPpm(), HasRTP: window.GetHasRtp(),
	}
}

func luckyGiftUserMultiplierStatFromProto(stat *luckygiftv1.LuckyGiftUserMultiplierStat) luckyGiftUserMultiplierStatDTO {
	if stat == nil {
		return luckyGiftUserMultiplierStatDTO{}
	}
	return luckyGiftUserMultiplierStatDTO{
		HitType: stat.GetHitType(), HitTypeLabel: luckyGiftHitTypeLabel(stat.GetHitType()),
		MultiplierPPM: stat.GetMultiplierPpm(), DrawCount: stat.GetDrawCount(),
		WagerCoins: stat.GetWagerCoins(), PayoutCoins: stat.GetPayoutCoins(),
	}
}

func luckyGiftUserJackpotHitFromProto(hit *luckygiftv1.LuckyGiftUserJackpotHit) luckyGiftUserJackpotHitDTO {
	if hit == nil {
		return luckyGiftUserJackpotHitDTO{}
	}
	conditions := make([]luckyGiftUserJackpotConditionDTO, 0, len(hit.GetConditions()))
	for _, condition := range hit.GetConditions() {
		conditions = append(conditions, luckyGiftUserJackpotConditionFromProto(condition))
	}
	return luckyGiftUserJackpotHitDTO{
		EventKey: hit.GetEventKey(), DrawID: hit.GetDrawId(), RequestID: hit.GetRequestId(), RuleVersion: hit.GetRuleVersion(), Stage: hit.GetStage(),
		Mechanism: hit.GetMechanism(), MechanismLabel: luckyGiftJackpotMechanismLabel(hit.GetMechanism()),
		ReasonCode: hit.GetReasonCode(), ReasonSummary: luckyGiftJackpotReasonSummary(hit), TierID: hit.GetTierId(),
		MultiplierPPM: hit.GetMultiplierPpm(), WagerCoins: hit.GetWagerCoins(), PayoutCoins: hit.GetPayoutCoins(),
		OccurredAtMS: hit.GetOccurredAtMs(), Conditions: conditions,
	}
}

func luckyGiftUserJackpotConditionFromProto(condition *luckygiftv1.LuckyGiftUserJackpotCondition) luckyGiftUserJackpotConditionDTO {
	if condition == nil {
		return luckyGiftUserJackpotConditionDTO{}
	}
	actual, threshold, operator := luckyGiftJackpotConditionValues(condition)
	return luckyGiftUserJackpotConditionDTO{
		Name: condition.GetName(), Label: luckyGiftJackpotConditionLabel(condition.GetName()),
		Actual: actual, Threshold: threshold, Operator: operator,
		Numerator: condition.GetNumerator(), Denominator: condition.GetDenominator(),
		RatioPPM: condition.GetRatioPpm(), LimitPPM: condition.GetLimitPpm(), FactorPPM: condition.GetFactorPpm(),
		Factor: luckyGiftPercent(condition.GetFactorPpm()), Passed: condition.GetPassed(), Reason: condition.GetReason(),
	}
}

func luckyGiftJackpotConditionValues(condition *luckygiftv1.LuckyGiftUserJackpotCondition) (string, string, string) {
	name := condition.GetName()
	switch name {
	case "global_rtp", "spend_global_rtp", "user_round_rtp", "user_48h_rtp", "user_day_rtp", "user_72h_rtp":
		return fmt.Sprintf("%s（返奖 %d / 消耗 %d）", luckyGiftPercent(condition.GetRatioPpm()), condition.GetNumerator(), condition.GetDenominator()), luckyGiftPercent(condition.GetLimitPpm()), "≤"
	case "user_24h_rtp_downweight":
		return fmt.Sprintf("%s（返奖 %d / 消耗 %d）", luckyGiftPercent(condition.GetRatioPpm()), condition.GetNumerator(), condition.GetDenominator()), luckyGiftPercent(condition.GetLimitPpm()), ">"
	case "user_registered_48h":
		return luckyGiftDurationHours(condition.GetNumerator()), luckyGiftDurationHours(condition.GetDenominator()), "≥"
	case "daily_jackpot_limit", "spend_daily_jackpot_limit":
		return fmt.Sprintf("%d 次", condition.GetNumerator()), fmt.Sprintf("%d 次", condition.GetLimitPpm()), "<"
	case "jackpot_rate_limit":
		return fmt.Sprintf("%d 次（近 %s）", condition.GetNumerator(), luckyGiftDurationMinutes(condition.GetDenominator())), fmt.Sprintf("%d 次", condition.GetLimitPpm()), "<"
	case "settled_round_qualification", "payable_jackpot_candidate":
		if condition.GetPassed() {
			return "已满足", "必须满足", "="
		}
		return "未满足", "必须满足", "="
	default:
		if condition.GetLimitPpm() != 0 || condition.GetRatioPpm() != 0 {
			return luckyGiftPercent(condition.GetRatioPpm()), luckyGiftPercent(condition.GetLimitPpm()), "≤"
		}
		if condition.GetPassed() {
			return "已满足", "必须满足", "="
		}
		return "未满足", "必须满足", "="
	}
}

func luckyGiftJackpotConditionLabel(name string) string {
	labels := map[string]string{
		"global_rtp":                  "全奖池当前返奖率",
		"spend_global_rtp":            "累计消费大奖检查时的全奖池返奖率",
		"settled_round_qualification": "上一结算轮次是否达到补偿条件",
		"user_round_rtp":              "用户当前结算轮次返奖率",
		"user_48h_rtp":                "用户近 48 小时返奖率",
		"user_registered_48h":         "用户注册是否已满 48 小时",
		"user_24h_rtp_downweight":     "用户近 24 小时普通中奖降权",
		"user_day_rtp":                "用户当天返奖率",
		"user_72h_rtp":                "用户近 72 小时返奖率",
		"daily_jackpot_limit":         "用户当天已中大奖次数",
		"spend_daily_jackpot_limit":   "累计消费大奖的当天次数限制",
		"jackpot_rate_limit":          "奖池滚动窗口内已爆大奖次数",
		"payable_jackpot_candidate":   "奖池余额是否足够支付所选大奖",
	}
	if label := labels[name]; label != "" {
		return label
	}
	return strings.ReplaceAll(strings.TrimSpace(name), "_", " ")
}

func luckyGiftJackpotMechanismLabel(mechanism string) string {
	switch mechanism {
	case "rtp_compensation", "jackpot_mechanism_1":
		return "RTP 补偿大奖"
	case "spend_milestone", "jackpot_mechanism_2":
		return "累计消费大奖"
	default:
		return "大奖"
	}
}

func luckyGiftJackpotReasonSummary(hit *luckygiftv1.LuckyGiftUserJackpotHit) string {
	switch hit.GetReasonCode() {
	case "rtp_compensation_jackpot":
		return "用户与奖池均满足补偿条件，且奖池余额足够支付，因此触发 RTP 补偿大奖。"
	case "milestone_token_jackpot":
		return "用户累计消费已获得一次大奖机会，且奖池余额足够支付，因此触发累计消费大奖。"
	}
	if summary := strings.TrimSpace(hit.GetReasonSummary()); summary != "" {
		return summary
	}
	return "本次开奖满足下列全部条件，因此命中该大奖。"
}

func luckyGiftHitTypeLabel(hitType string) string {
	switch hitType {
	case "zero":
		return "未中奖"
	case "ordinary":
		return "普通奖"
	case "rtp_compensation":
		return "RTP 补偿大奖"
	case "spend_milestone":
		return "累计消费大奖"
	default:
		return hitType
	}
}

func luckyGiftPercent(ppm int64) string {
	value := strconv.FormatFloat(float64(ppm)/10_000, 'f', 2, 64)
	value = strings.TrimRight(strings.TrimRight(value, "0"), ".")
	if value == "" {
		value = "0"
	}
	return value + "%"
}

func luckyGiftDurationHours(milliseconds int64) string {
	value := strconv.FormatFloat(float64(milliseconds)/3_600_000, 'f', 1, 64)
	value = strings.TrimRight(strings.TrimRight(value, "0"), ".")
	return value + " 小时"
}

func luckyGiftDurationMinutes(milliseconds int64) string {
	value := strconv.FormatFloat(float64(milliseconds)/60_000, 'f', 1, 64)
	value = strings.TrimRight(strings.TrimRight(value, "0"), ".")
	return value + " 分钟"
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func (h *Handler) writeLuckyGiftUserProfileError(c *gin.Context, err error, fallback string) {
	status := grpcstatus.Convert(err)
	switch status.Code() {
	case codes.InvalidArgument:
		response.BadRequest(c, status.Message())
	case codes.NotFound:
		response.NotFound(c, "未找到该用户的幸运礼物画像")
	default:
		response.ServerError(c, fallback)
	}
}
