package broadcast

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"hyapp/pkg/appcode"
	"hyapp/pkg/gamemq"
	"hyapp/pkg/xerr"
	broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
)

const maxGameWinMinCoins = int64(1_000_000_000_000)

type cachedGameWinConfig struct {
	config      broadcastdomain.GameWinConfig
	expiresAtMS int64
}

// HandleGameEvent 把 game-service 已提交的返奖事实投影成区域 game_win 飘屏。
// 游戏结算、阈值策略、房间锁状态和 IM 投递分别由 owner 服务持有；这里仅在消费时组装稳定展示快照。
func (s *Service) HandleGameEvent(ctx context.Context, message gamemq.GameOutboxMessage) (broadcastdomain.ConsumeGameEventResult, error) {
	result := broadcastdomain.ConsumeGameEventResult{EventID: strings.TrimSpace(message.EventID), Status: broadcastdomain.StatusSkipped}
	if strings.TrimSpace(message.EventType) != gamemq.EventTypeGameOrderSettled {
		return result, nil
	}
	opType := strings.ToLower(strings.TrimSpace(message.OpType))
	if opType != "credit" && opType != "payout" {
		// debit/bet/refund 不是中奖到账，确认 MQ 位点但绝不能生成正向中奖飘屏。
		return result, nil
	}
	if strings.TrimSpace(message.AppCode) == "" || strings.TrimSpace(message.EventID) == "" || strings.TrimSpace(message.OrderID) == "" || message.UserID <= 0 || message.CoinAmount < 0 {
		return broadcastdomain.ConsumeGameEventResult{}, xerr.New(xerr.InvalidArgument, "game order settled fact is incomplete")
	}

	eventCtx := appcode.WithContext(ctx, message.AppCode)
	config, err := s.cachedGameWinBroadcastConfig(eventCtx)
	if err != nil {
		return broadcastdomain.ConsumeGameEventResult{}, err
	}
	if !config.Enabled || message.CoinAmount < config.MinWinCoins {
		return result, nil
	}

	payload, err := decodeGameWinSourcePayload(message.PayloadJSON)
	if err != nil {
		return broadcastdomain.ConsumeGameEventResult{}, err
	}
	regionID := firstGamePayloadInt64(payload, "visible_region_id", "region_id")
	if regionID <= 0 {
		// 区域未知时不能扩大成全局播报；来源事实仍被合法确认，避免坏数据反复进入 DLQ。
		return result, nil
	}

	profile, err := s.broadcastUserProfile(eventCtx, message.UserID)
	if err != nil {
		return broadcastdomain.ConsumeGameEventResult{}, fmt.Errorf("resolve game win user profile: %w", err)
	}
	roomID := firstGamePayloadString(payload, "room_id")
	roomSnapshot := RoomBroadcastSnapshot{}
	if roomID != "" {
		if s.roomSnapshotSource == nil {
			return broadcastdomain.ConsumeGameEventResult{}, xerr.New(xerr.Unavailable, "room snapshot source is not configured")
		}
		roomSnapshot, err = s.roomSnapshotSource.GetRoomBroadcastSnapshot(eventCtx, roomID)
		if err != nil {
			if status.Code(err) == codes.NotFound {
				// 房间已删除时仍可展示中奖，但必须移除失效入口，不能把未知 locked=false 发给客户端。
				roomID = ""
				roomSnapshot = RoomBroadcastSnapshot{}
			} else {
				return broadcastdomain.ConsumeGameEventResult{}, fmt.Errorf("resolve game win room snapshot: %w", err)
			}
		}
	}

	broadcastEventID := "game_win:" + strings.TrimSpace(message.OrderID)
	payloadJSON, err := gameWinPayload(message, payload, broadcastEventID, regionID, profile, roomID, roomSnapshot, s.now().UTC().UnixMilli())
	if err != nil {
		return broadcastdomain.ConsumeGameEventResult{}, err
	}
	published, err := s.PublishRegionBroadcast(eventCtx, PublishInput{
		EventID:       broadcastEventID,
		BroadcastType: broadcastdomain.TypeGameWin,
		RegionID:      regionID,
		PayloadJSON:   payloadJSON,
	})
	if err != nil {
		return broadcastdomain.ConsumeGameEventResult{}, err
	}
	return broadcastdomain.ConsumeGameEventResult{
		EventID:          message.EventID,
		Status:           published.Status,
		BroadcastEventID: published.EventID,
		BroadcastCreated: published.Created,
	}, nil
}

// GetGameWinBroadcastConfig 直读当前 App 配置并刷新本实例缓存，后台读取不会拿到另一个副本尚未过期的旧值。
func (s *Service) GetGameWinBroadcastConfig(ctx context.Context) (broadcastdomain.GameWinConfig, error) {
	if s == nil || s.gameWinConfigRepo == nil {
		return broadcastdomain.GameWinConfig{}, xerr.New(xerr.Unavailable, "game win broadcast config repository is not configured")
	}
	config, exists, err := s.gameWinConfigRepo.GetGameWinBroadcastConfig(ctx)
	if err != nil {
		return broadcastdomain.GameWinConfig{}, err
	}
	if !exists {
		config = broadcastdomain.GameWinConfig{Enabled: true, MinWinCoins: s.cfg.GameWinMinCoins}
	}
	s.storeCachedGameWinConfig(appcode.FromContext(ctx), config)
	return config, nil
}

// UpdateGameWinBroadcastConfig 保存完整替换策略；enabled=false 是显式停发，不能被省略值自动恢复为 true。
func (s *Service) UpdateGameWinBroadcastConfig(ctx context.Context, enabled bool, minWinCoins int64, operatorAdminID int64) (broadcastdomain.GameWinConfig, error) {
	if s == nil || s.gameWinConfigRepo == nil {
		return broadcastdomain.GameWinConfig{}, xerr.New(xerr.Unavailable, "game win broadcast config repository is not configured")
	}
	if minWinCoins <= 0 || minWinCoins > maxGameWinMinCoins {
		return broadcastdomain.GameWinConfig{}, xerr.New(xerr.InvalidArgument, "min_win_coins must be between 1 and 1000000000000")
	}
	if operatorAdminID < 0 {
		return broadcastdomain.GameWinConfig{}, xerr.New(xerr.InvalidArgument, "operator_admin_id is invalid")
	}
	nowMS := s.now().UTC().UnixMilli()
	stored, err := s.gameWinConfigRepo.UpsertGameWinBroadcastConfig(ctx, broadcastdomain.GameWinConfig{
		Enabled:          enabled,
		MinWinCoins:      minWinCoins,
		UpdatedByAdminID: operatorAdminID,
		CreatedAtMS:      nowMS,
		UpdatedAtMS:      nowMS,
	})
	if err != nil {
		return broadcastdomain.GameWinConfig{}, err
	}
	s.storeCachedGameWinConfig(appcode.FromContext(ctx), stored)
	return stored, nil
}

func (s *Service) cachedGameWinBroadcastConfig(ctx context.Context) (broadcastdomain.GameWinConfig, error) {
	app := appcode.FromContext(ctx)
	nowMS := s.now().UTC().UnixMilli()
	s.gameWinConfigMu.RLock()
	cached, ok := s.gameWinConfigCache[app]
	s.gameWinConfigMu.RUnlock()
	if ok && cached.expiresAtMS > nowMS {
		return cached.config, nil
	}
	return s.GetGameWinBroadcastConfig(ctx)
}

func (s *Service) storeCachedGameWinConfig(app string, config broadcastdomain.GameWinConfig) {
	if s == nil {
		return
	}
	app = appcode.Normalize(app)
	s.gameWinConfigMu.Lock()
	s.gameWinConfigCache[app] = cachedGameWinConfig{
		config:      config,
		expiresAtMS: s.now().UTC().Add(s.cfg.GameWinConfigCacheTTL).UnixMilli(),
	}
	s.gameWinConfigMu.Unlock()
}

func gameWinPayload(message gamemq.GameOutboxMessage, source map[string]any, eventID string, regionID int64, profile SenderProfile, roomID string, room RoomBroadcastSnapshot, sentAtMS int64) (string, error) {
	gameID := strings.TrimSpace(message.GameID)
	providerGameID := firstGamePayloadString(source, "provider_game_id")
	gameName := firstGamePayloadString(source, "game_name")
	gameIconURL := firstGamePayloadString(source, "game_icon_url", "game_cover_url")
	roundID := firstGamePayloadString(source, "provider_round_id", "round_id")
	payload := map[string]any{
		"event_id":              eventID,
		"source_event_id":       message.EventID,
		"broadcast_type":        broadcastdomain.TypeGameWin,
		"scope":                 broadcastdomain.ScopeRegion,
		"app_code":              message.AppCode,
		"region_id":             regionID,
		"visible_region_id":     regionID,
		"order_id":              message.OrderID,
		"platform_code":         message.PlatformCode,
		"game_id":               gameID,
		"provider_game_id":      providerGameID,
		"game_name":             gameName,
		"game_icon_url":         gameIconURL,
		"game_round_id":         roundID,
		"round_id":              roundID,
		"room_id":               roomID,
		"room_locked":           room.Locked,
		"room_name":             room.Title,
		"user_id":               message.UserID,
		"sender_user_id":        message.UserID,
		"account":               profile.Account,
		"actual_account":        profile.Account,
		"user_name":             profile.Nickname,
		"user_nickname":         profile.Nickname,
		"user_avatar_url":       profile.Avatar,
		"coins":                 message.CoinAmount,
		"win_amount":            message.CoinAmount,
		"award_amount":          message.CoinAmount,
		"currency_diff":         message.CoinAmount,
		"wallet_transaction_id": firstGamePayloadString(source, "wallet_transaction_id"),
		"occurred_at_ms":        message.OccurredAtMS,
		"sent_at_ms":            sentAtMS,
		"user": map[string]any{
			"user_id":         profile.UserID,
			"display_user_id": profile.Account,
			"nickname":        profile.Nickname,
			"avatar":          profile.Avatar,
		},
	}
	if roomID != "" {
		payload["jump_type"] = "room_window"
		jumpPayload := map[string]any{
			"room_id":     roomID,
			"room_locked": room.Locked,
			"window":      "game_window",
			"game_id":     gameID,
		}
		action := map[string]any{
			"type":        "room_window",
			"room_id":     roomID,
			"room_locked": room.Locked,
			"window":      "game_window",
			"game_id":     gameID,
		}
		setRoomOwnerSnapshot(payload, room.OwnerUserID)
		setRoomOwnerSnapshot(jumpPayload, room.OwnerUserID)
		setRoomOwnerSnapshot(action, room.OwnerUserID)
		payload["jump_payload"] = jumpPayload
		payload["action"] = action
	}
	encoded, err := json.Marshal(payload)
	return string(encoded), err
}

func decodeGameWinSourcePayload(raw string) (map[string]any, error) {
	if strings.TrimSpace(raw) == "" {
		return map[string]any{}, nil
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return nil, fmt.Errorf("decode game order payload: %w", err)
	}
	if payload == nil {
		payload = map[string]any{}
	}
	return payload, nil
}

func firstGamePayloadString(payload map[string]any, keys ...string) string {
	for _, key := range keys {
		value, exists := payload[key]
		if !exists || value == nil {
			continue
		}
		if normalized := strings.TrimSpace(fmt.Sprint(value)); normalized != "" && normalized != "<nil>" {
			return normalized
		}
	}
	return ""
}

func firstGamePayloadInt64(payload map[string]any, keys ...string) int64 {
	for _, key := range keys {
		value, exists := payload[key]
		if !exists {
			continue
		}
		switch typed := value.(type) {
		case float64:
			return int64(typed)
		case float32:
			return int64(typed)
		case int64:
			return typed
		case int:
			return int64(typed)
		case json.Number:
			if parsed, err := typed.Int64(); err == nil {
				return parsed
			}
		case string:
			if parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64); err == nil {
				return parsed
			}
		}
	}
	return 0
}
