package gamemanagement

import (
	"bytes"
	"context"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"hyapp-admin-server/internal/response"
	gamev1 "hyapp.local/api/proto/game/v1"

	"github.com/gin-gonic/gin"
)

const (
	adapterBaishunV1    = "baishun_v1"
	adapterLeaderCCV1   = "leadercc_v1"
	adapterZeeOneV1     = "zeeone_v1"
	adapterVivaGamesV1  = "vivagames_v1"
	adapterReyouV1      = "reyou_v1"
	adapterZGameV1      = "zgame_v1"
	adapterAMGV1        = "amg_v1"
	adapterYomiV4       = "yomi_v4"
	defaultGameStatus   = "disabled"
	defaultGameCategory = "casino"
	defaultLaunchMode   = "full_screen"
	defaultOrientation  = "portrait"
)

type syncGamesRequest struct {
	DryRun          bool     `json:"dryRun"`
	Status          string   `json:"status"`
	Category        string   `json:"category"`
	LaunchMode      string   `json:"launchMode"`
	MinCoin         int64    `json:"minCoin"`
	Tags            []string `json:"tags"`
	GameListType    int      `json:"gameListType"`
	ProviderGameIDs []string `json:"providerGameIds"`
}

type providerGameSyncPlan struct {
	Games    []catalogRequest
	GameURLs map[string]string
}

// SyncPlatformGames 从厂商后台 API 拉取游戏清单，格式化成本地 game_catalog，再由 game-service 落库并补默认展示规则。
func (h *Handler) SyncPlatformGames(c *gin.Context) {
	platformCode := strings.TrimSpace(c.Param("platform_code"))
	if platformCode == "" {
		response.BadRequest(c, "平台 Code 参数不正确")
		return
	}
	var req syncGamesRequest
	if c.Request.ContentLength > 0 {
		if err := c.ShouldBindJSON(&req); err != nil {
			response.BadRequest(c, "同步参数不正确")
			return
		}
	}
	platform, err := h.findGamePlatform(c.Request.Context(), c, platformCode)
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	plan, err := fetchProviderGameSyncPlan(c.Request.Context(), platform, req)
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}
	games := selectProviderGames(plan.Games, req.ProviderGameIDs)
	items := make([]catalogDTO, 0, len(games))
	if req.DryRun {
		for _, game := range games {
			item := catalogFromProto(game.toProto(""))
			item.LaunchURL = strings.TrimSpace(plan.GameURLs[game.ProviderGameID])
			items = append(items, item)
		}
		response.OK(c, gin.H{"platformCode": platformCode, "synced": len(items), "dryRun": true, "items": items})
		return
	}
	if len(games) == 0 {
		response.BadRequest(c, "请选择要添加的游戏")
		return
	}
	adapterConfigJSON := mergeAdapterGameURLs(platform.GetAdapterConfigJson(), selectedGameURLs(plan.GameURLs, games))
	if strings.TrimSpace(adapterConfigJSON) != "" && strings.TrimSpace(adapterConfigJSON) != strings.TrimSpace(platform.GetAdapterConfigJson()) {
		if _, err := h.game.UpsertPlatform(c.Request.Context(), &gamev1.UpsertPlatformRequest{
			Meta:     requestMeta(c),
			Platform: platformWithAdapterConfig(platform, adapterConfigJSON),
		}); err != nil {
			response.BadRequest(c, "同步游戏平台配置失败: "+err.Error())
			return
		}
	}
	for _, game := range games {
		resp, err := h.game.UpsertCatalog(c.Request.Context(), &gamev1.UpsertCatalogRequest{
			Meta: requestMeta(c),
			Game: game.toProto(""),
		})
		if err != nil {
			response.BadRequest(c, "同步游戏目录失败: "+err.Error())
			return
		}
		items = append(items, catalogFromProto(resp.GetGame()))
	}
	h.auditLog(c, "sync-provider-games", "game_platforms", platformCode, fmt.Sprintf("synced=%d", len(items)))
	response.OK(c, gin.H{"platformCode": platformCode, "synced": len(items), "dryRun": false, "items": items})
}

func (h *Handler) findGamePlatform(ctx context.Context, c *gin.Context, platformCode string) (*gamev1.GamePlatform, error) {
	resp, err := h.game.ListPlatforms(ctx, &gamev1.ListPlatformsRequest{Meta: requestMeta(c)})
	if err != nil {
		return nil, fmt.Errorf("获取游戏平台失败")
	}
	for _, platform := range resp.GetPlatforms() {
		if strings.EqualFold(strings.TrimSpace(platform.GetPlatformCode()), platformCode) {
			return platform, nil
		}
	}
	return nil, fmt.Errorf("游戏平台不存在")
}

func fetchProviderGameSyncPlan(ctx context.Context, platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	switch strings.ToLower(strings.TrimSpace(platform.GetAdapterType())) {
	case adapterBaishunV1:
		return fetchBaishunGameSyncPlan(ctx, platform, req)
	case adapterLeaderCCV1:
		return fetchLeaderCCGameSyncPlan(ctx, platform, req)
	case adapterZeeOneV1:
		return fetchZeeOneGameSyncPlan(platform, req)
	case adapterVivaGamesV1:
		return fetchVivaGamesGameSyncPlan(platform, req)
	case adapterReyouV1:
		return fetchReyouGameSyncPlan(platform, req)
	case adapterZGameV1:
		return fetchZGameGameSyncPlan(platform, req)
	case adapterAMGV1:
		return fetchAMGGameSyncPlan(platform, req)
	case adapterYomiV4:
		return fetchYomiGameSyncPlan(platform, req)
	default:
		// 不在配置文件写死游戏：每家厂商新增“列表 API”时只加一个 adapter fetcher，后台入口保持不变。
		return providerGameSyncPlan{}, fmt.Errorf("当前适配器暂未接入厂商游戏列表 API: %s", platform.GetAdapterType())
	}
}

func platformWithAdapterConfig(platform *gamev1.GamePlatform, adapterConfigJSON string) *gamev1.GamePlatform {
	return &gamev1.GamePlatform{
		AppCode:             platform.GetAppCode(),
		PlatformCode:        platform.GetPlatformCode(),
		PlatformName:        platform.GetPlatformName(),
		Status:              platform.GetStatus(),
		ApiBaseUrl:          platform.GetApiBaseUrl(),
		AdapterType:         platform.GetAdapterType(),
		CallbackSecret:      platform.GetCallbackSecret(),
		CallbackSecretSet:   platform.GetCallbackSecretSet(),
		CallbackIpWhitelist: platform.GetCallbackIpWhitelist(),
		AdapterConfigJson:   adapterConfigJSON,
		SortOrder:           platform.GetSortOrder(),
		CreatedAtMs:         platform.GetCreatedAtMs(),
		UpdatedAtMs:         platform.GetUpdatedAtMs(),
	}
}

type baishunSyncConfig struct {
	AppID                 int64             `json:"app_id"`
	AppIDCamel            int64             `json:"appId"`
	AppChannel            string            `json:"app_channel"`
	AppChannelCamel       string            `json:"appChannel"`
	GameListType          int               `json:"game_list_type"`
	GameListTypeCamel     int               `json:"gameListType"`
	ProviderAPIBaseURL    string            `json:"provider_api_base_url"`
	ProviderAPIBaseURLAlt string            `json:"providerApiBaseUrl"`
	GameListURL           string            `json:"game_list_url"`
	GameListURLCamel      string            `json:"gameListUrl"`
	GameURLs              map[string]string `json:"game_urls"`
}

type leaderCCSyncConfig struct {
	// game_list_url 是灵仙按环境提供的公开 JSON 清单，game_urls 由同步操作自动回写。
	GameListURL      string `json:"game_list_url"`
	GameListURLCamel string `json:"gameListUrl"`
}

type leaderCCGameListItem struct {
	GameID  any    `json:"gameId"`
	Name    string `json:"name"`
	Title   string `json:"title"`
	Version any    `json:"ver"`
	FullURL string `json:"full_url"`
	HalfURL string `json:"half_url"`
}

type zeeoneSyncConfig struct {
	// ZeeOne 测试资料给的是逐游戏 H5 地址，不是列表 API；后台“获取游戏列表”直接把这个映射转换为候选目录。
	GameURLs   map[string]string `json:"game_urls"`
	GameNames  map[string]string `json:"game_names"`
	GameIcons  map[string]string `json:"game_icons"`
	GameCovers map[string]string `json:"game_covers"`
}

type vivaGamesSyncConfig struct {
	// VIVAGAMES 文档说明游戏 URL 由厂商提供，后台用这个映射把 URL 转成可勾选的本地目录候选项。
	GameURLs   map[string]string `json:"game_urls"`
	GameNames  map[string]string `json:"game_names"`
	GameIcons  map[string]string `json:"game_icons"`
	GameCovers map[string]string `json:"game_covers"`
}

type reyouSyncConfig struct {
	// 热游文档没有游戏列表 API；后台以 game_urls 作为唯一清单来源，和服务端启动配置保持同一份 JSON。
	GameURLs   map[string]string `json:"game_urls"`
	GameNames  map[string]string `json:"game_names"`
	GameIcons  map[string]string `json:"game_icons"`
	GameCovers map[string]string `json:"game_covers"`
}

type zgameSyncConfig struct {
	// ZGame 接入只约束固定回调和 JS Bridge；游戏 H5 地址由运营配置，后台据此生成可同步目录。
	GameURLs   map[string]string `json:"game_urls"`
	GameNames  map[string]string `json:"game_names"`
	GameIcons  map[string]string `json:"game_icons"`
	GameCovers map[string]string `json:"game_covers"`
}

type amgSyncConfig struct {
	// AMG 文档未提供游戏列表 API；game_urls 的 key 必须使用 settlement 回调中的数值 gameId。
	GameURLs   map[string]string `json:"game_urls"`
	GameNames  map[string]string `json:"game_names"`
	GameIcons  map[string]string `json:"game_icons"`
	GameCovers map[string]string `json:"game_covers"`
}

type yomiSyncConfig struct {
	// Yomi V4 文档只提供启动鉴权地址，没有游戏列表 API；后台通过 game_names 维护截图里的 gameUid 清单。
	GameNames  map[string]string `json:"game_names"`
	GameURLs   map[string]string `json:"game_urls"`
	GameIcons  map[string]string `json:"game_icons"`
	GameCovers map[string]string `json:"game_covers"`
}

func fetchLeaderCCGameSyncPlan(ctx context.Context, platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeLeaderCCSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	endpoint := strings.TrimSpace(firstNonEmpty(config.GameListURL, config.GameListURLCamel))
	if endpoint == "" {
		return providerGameSyncPlan{}, fmt.Errorf("灵仙列表同步缺少配置: adapterConfigJson.game_list_url")
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("灵仙游戏列表 URL 不合法: %w", err)
	}
	resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(httpReq)
	if err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("请求灵仙游戏列表失败: %w", err)
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("读取灵仙游戏列表失败: %w", err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return providerGameSyncPlan{}, fmt.Errorf("灵仙游戏列表 HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(raw)))
	}
	var providerItems []leaderCCGameListItem
	if err := json.Unmarshal(raw, &providerItems); err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("解析灵仙游戏列表失败: %w", err)
	}
	games, gameURLs := leaderCCCatalogItems(platform.GetPlatformCode(), providerItems, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("灵仙游戏列表为空")
	}
	return providerGameSyncPlan{Games: games, GameURLs: gameURLs}, nil
}

func decodeLeaderCCSyncConfig(raw string) (leaderCCSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return leaderCCSyncConfig{}, nil
	}
	var config leaderCCSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return leaderCCSyncConfig{}, fmt.Errorf("灵仙适配器配置 JSON 不合法")
	}
	return config, nil
}

func leaderCCCatalogItems(platformCode string, providerItems []leaderCCGameListItem, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	items := make([]catalogRequest, 0, len(providerItems))
	gameURLs := make(map[string]string, len(providerItems))
	for index, providerItem := range providerItems {
		providerGameID := stringFromJSONValue(providerItem.GameID)
		if providerGameID == "" {
			continue
		}
		// 半屏地址优先匹配语音房内嵌场景；厂商未提供时才回退全屏地址。
		launchURL := firstNonEmpty(providerItem.HalfURL, providerItem.FullURL)
		if launchURL == "" {
			continue
		}
		launchMode := strings.TrimSpace(req.LaunchMode)
		if launchMode == "" {
			launchMode = "half_screen"
			if strings.TrimSpace(providerItem.HalfURL) == "" {
				launchMode = defaultLaunchMode
			}
		}
		gameURLs[providerGameID] = launchURL
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       firstNonEmpty(providerItem.Name, providerItem.Title, providerGameID),
			Category:       defaulted(req.Category, defaultGameCategory),
			LaunchMode:     launchMode,
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(providerItems), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterLeaderCCV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func fetchZeeOneGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeZeeOneSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	games, gameURLs := zeeoneCatalogItems(platform.GetPlatformCode(), config, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("ZeeOne 游戏列表为空：请在 adapterConfigJson.game_urls 配置游戏 ID 到 H5 URL 的映射")
	}
	return providerGameSyncPlan{
		Games:    games,
		GameURLs: gameURLs,
	}, nil
}

func decodeZeeOneSyncConfig(raw string) (zeeoneSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return zeeoneSyncConfig{}, nil
	}
	var config zeeoneSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return zeeoneSyncConfig{}, fmt.Errorf("ZeeOne 适配器配置 JSON 不合法")
	}
	return config, nil
}

func zeeoneCatalogItems(platformCode string, config zeeoneSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	keys := make([]string, 0, len(config.GameURLs))
	for providerGameID, launchURL := range config.GameURLs {
		if strings.TrimSpace(providerGameID) != "" && strings.TrimSpace(launchURL) != "" {
			keys = append(keys, strings.TrimSpace(providerGameID))
		}
	}
	sort.Strings(keys)
	items := make([]catalogRequest, 0, len(keys))
	gameURLs := make(map[string]string, len(keys))
	for index, providerGameID := range keys {
		launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
		gameURLs[providerGameID] = launchURL
		name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
		iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
		coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(keys), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterZeeOneV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func zeeoneGameNameFromURL(raw string) string {
	parsed, err := url.Parse(strings.TrimSpace(raw))
	pathValue := strings.TrimSpace(raw)
	if err == nil && parsed.Path != "" {
		pathValue = parsed.Path
	}
	parts := strings.Split(strings.Trim(pathValue, "/"), "/")
	candidate := ""
	for index := len(parts) - 1; index >= 0; index-- {
		part := strings.TrimSpace(parts[index])
		if part == "" || strings.EqualFold(part, "index.html") || strings.EqualFold(part, "index.htm") {
			continue
		}
		candidate = strings.TrimSuffix(strings.TrimSuffix(part, ".html"), ".htm")
		break
	}
	return titleGameName(candidate)
}

func fetchVivaGamesGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeVivaGamesSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	games, gameURLs := vivaGamesCatalogItems(platform.GetPlatformCode(), config, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("VIVAGAMES 游戏列表为空：请在 adapterConfigJson.game_urls 配置游戏 ID 到 H5 URL 的映射")
	}
	return providerGameSyncPlan{
		Games:    games,
		GameURLs: gameURLs,
	}, nil
}

func decodeVivaGamesSyncConfig(raw string) (vivaGamesSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return vivaGamesSyncConfig{}, nil
	}
	var config vivaGamesSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return vivaGamesSyncConfig{}, fmt.Errorf("VIVAGAMES 适配器配置 JSON 不合法")
	}
	return config, nil
}

func vivaGamesCatalogItems(platformCode string, config vivaGamesSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	keys := make([]string, 0, len(config.GameURLs))
	for providerGameID, launchURL := range config.GameURLs {
		if strings.TrimSpace(providerGameID) != "" && strings.TrimSpace(launchURL) != "" {
			keys = append(keys, strings.TrimSpace(providerGameID))
		}
	}
	sort.Strings(keys)
	items := make([]catalogRequest, 0, len(keys))
	gameURLs := make(map[string]string, len(keys))
	for index, providerGameID := range keys {
		launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
		gameURLs[providerGameID] = launchURL
		name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
		iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
		coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(keys), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterVivaGamesV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func fetchReyouGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeReyouSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	games, gameURLs := reyouCatalogItems(platform.GetPlatformCode(), config, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("热游游戏列表为空：请在 adapterConfigJson.game_urls 配置游戏 ID 到 H5 URL 的映射")
	}
	return providerGameSyncPlan{
		Games:    games,
		GameURLs: gameURLs,
	}, nil
}

func fetchZGameGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeZGameSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	games, gameURLs := zgameCatalogItems(platform.GetPlatformCode(), config, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("ZGame 游戏列表为空：请在 adapterConfigJson.game_urls 配置游戏 ID 到 H5 URL 的映射")
	}
	return providerGameSyncPlan{
		Games:    games,
		GameURLs: gameURLs,
	}, nil
}

func fetchAMGGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeAMGSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	games, gameURLs := amgCatalogItems(platform.GetPlatformCode(), config, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("AMG 游戏列表为空：请在 adapterConfigJson.game_urls 配置游戏 ID 到 H5 URL 的映射")
	}
	return providerGameSyncPlan{Games: games, GameURLs: gameURLs}, nil
}

func fetchYomiGameSyncPlan(platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeYomiSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	games, gameURLs := yomiCatalogItems(platform.GetPlatformCode(), config, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("小游游戏列表为空：请在 adapterConfigJson.game_names 配置游戏 ID 到名称的映射")
	}
	return providerGameSyncPlan{
		Games:    games,
		GameURLs: gameURLs,
	}, nil
}

func decodeReyouSyncConfig(raw string) (reyouSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return reyouSyncConfig{}, nil
	}
	var config reyouSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return reyouSyncConfig{}, fmt.Errorf("热游适配器配置 JSON 不合法")
	}
	return config, nil
}

func decodeZGameSyncConfig(raw string) (zgameSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return zgameSyncConfig{}, nil
	}
	var config zgameSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return zgameSyncConfig{}, fmt.Errorf("ZGame 适配器配置 JSON 不合法")
	}
	return config, nil
}

func decodeAMGSyncConfig(raw string) (amgSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return amgSyncConfig{}, nil
	}
	var config amgSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return amgSyncConfig{}, fmt.Errorf("AMG 适配器配置 JSON 不合法")
	}
	return config, nil
}

func decodeYomiSyncConfig(raw string) (yomiSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return yomiSyncConfig{}, nil
	}
	var config yomiSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return yomiSyncConfig{}, fmt.Errorf("小游适配器配置 JSON 不合法")
	}
	return config, nil
}

func reyouCatalogItems(platformCode string, config reyouSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	keys := make([]string, 0, len(config.GameURLs))
	for providerGameID, launchURL := range config.GameURLs {
		if strings.TrimSpace(providerGameID) != "" && strings.TrimSpace(launchURL) != "" {
			keys = append(keys, strings.TrimSpace(providerGameID))
		}
	}
	sort.Strings(keys)
	items := make([]catalogRequest, 0, len(keys))
	gameURLs := make(map[string]string, len(keys))
	for index, providerGameID := range keys {
		launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
		gameURLs[providerGameID] = launchURL
		name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
		iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
		coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(keys), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterReyouV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func zgameCatalogItems(platformCode string, config zgameSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	keys := make([]string, 0, len(config.GameURLs))
	for providerGameID, launchURL := range config.GameURLs {
		if strings.TrimSpace(providerGameID) != "" && strings.TrimSpace(launchURL) != "" {
			keys = append(keys, strings.TrimSpace(providerGameID))
		}
	}
	sort.Strings(keys)
	items := make([]catalogRequest, 0, len(keys))
	gameURLs := make(map[string]string, len(keys))
	for index, providerGameID := range keys {
		launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
		gameURLs[providerGameID] = launchURL
		// ZGame 没有厂商列表 API 返回展示名，优先使用后台维护名称，再从 URL 尽量提取可读标题。
		name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
		iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
		coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(keys), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterZGameV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func amgCatalogItems(platformCode string, config amgSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	keys := make([]string, 0, len(config.GameURLs))
	for providerGameID, launchURL := range config.GameURLs {
		// settlement.gameId 是数值；同步阶段就拒绝非数值 key，避免启动成功后回调永远匹配不到 session。
		if value, err := strconv.ParseInt(strings.TrimSpace(providerGameID), 10, 64); err == nil && value > 0 && strings.TrimSpace(launchURL) != "" {
			keys = append(keys, strings.TrimSpace(providerGameID))
		}
	}
	sort.Strings(keys)
	items := make([]catalogRequest, 0, len(keys))
	gameURLs := make(map[string]string, len(keys))
	for index, providerGameID := range keys {
		launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
		gameURLs[providerGameID] = launchURL
		name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
		iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
		coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(keys), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterAMGV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func yomiCatalogItems(platformCode string, config yomiSyncConfig, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	keys := make([]string, 0, len(config.GameNames)+len(config.GameURLs))
	seen := make(map[string]struct{}, len(config.GameNames)+len(config.GameURLs))
	// 小游没有列表 API，game_names 是本地候选游戏的事实来源；game_urls 只用于后台预览，不参与启动拼参。
	for providerGameID := range config.GameNames {
		key := strings.TrimSpace(providerGameID)
		if key == "" {
			continue
		}
		seen[key] = struct{}{}
		keys = append(keys, key)
	}
	for providerGameID, launchURL := range config.GameURLs {
		key := strings.TrimSpace(providerGameID)
		if key == "" || strings.TrimSpace(launchURL) == "" {
			continue
		}
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		keys = append(keys, key)
	}
	sort.Strings(keys)
	items := make([]catalogRequest, 0, len(keys))
	gameURLs := make(map[string]string, len(keys))
	for index, providerGameID := range keys {
		launchURL := strings.TrimSpace(config.GameURLs[providerGameID])
		if launchURL != "" {
			gameURLs[providerGameID] = launchURL
		}
		name := firstNonEmpty(config.GameNames[providerGameID], zeeoneGameNameFromURL(launchURL), providerGameID)
		iconURL := strings.TrimSpace(config.GameIcons[providerGameID])
		coverURL := firstNonEmpty(config.GameCovers[providerGameID], iconURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    defaultOrientation,
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(keys), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterYomiV4}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func titleGameName(value string) string {
	tokens := splitGameNameTokens(value)
	if len(tokens) == 0 {
		return ""
	}
	smallWords := map[string]struct{}{"and": {}, "of": {}, "the": {}}
	words := make([]string, 0, len(tokens))
	for _, token := range tokens {
		lower := strings.ToLower(token)
		if _, err := strconv.Atoi(lower); err == nil {
			continue
		}
		if len(words) > 0 {
			if _, ok := smallWords[lower]; ok {
				words = append(words, lower)
				continue
			}
		}
		words = append(words, strings.ToUpper(lower[:1])+lower[1:])
	}
	return strings.Join(words, " ")
}

func splitGameNameTokens(value string) []string {
	var tokens []string
	var builder strings.Builder
	var previous rune
	flush := func() {
		if builder.Len() > 0 {
			tokens = append(tokens, builder.String())
			builder.Reset()
		}
	}
	for _, r := range strings.TrimSpace(value) {
		if !asciiAlphaNumeric(r) {
			flush()
			previous = 0
			continue
		}
		if builder.Len() > 0 && asciiUpper(r) && (asciiLower(previous) || asciiDigit(previous)) {
			flush()
		}
		builder.WriteRune(r)
		previous = r
	}
	flush()
	return tokens
}

func asciiAlphaNumeric(r rune) bool {
	return asciiLower(r) || asciiUpper(r) || asciiDigit(r)
}

func asciiLower(r rune) bool {
	return r >= 'a' && r <= 'z'
}

func asciiUpper(r rune) bool {
	return r >= 'A' && r <= 'Z'
}

func asciiDigit(r rune) bool {
	return r >= '0' && r <= '9'
}

func fetchBaishunGameSyncPlan(ctx context.Context, platform *gamev1.GamePlatform, req syncGamesRequest) (providerGameSyncPlan, error) {
	config, err := decodeBaishunSyncConfig(platform.GetAdapterConfigJson())
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	appID := config.appID()
	appChannel := config.appChannel()
	appKey := strings.TrimSpace(platform.GetCallbackSecret())
	endpoint := baishunGameListEndpoint(platform.GetApiBaseUrl(), config)
	if missing := missingBaishunSyncFields(appID, appChannel, appKey, endpoint); len(missing) > 0 {
		return providerGameSyncPlan{}, fmt.Errorf("百顺列表同步缺少配置: %s", strings.Join(missing, "、"))
	}
	body := baishunSignedListRequest(appID, appChannel, baishunGameListType(config, req), appKey)
	raw, err := json.Marshal(body)
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(raw))
	if err != nil {
		return providerGameSyncPlan{}, err
	}
	httpReq.Header.Set("Content-Type", "application/json")
	resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(httpReq)
	if err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("请求百顺游戏列表失败: %w", err)
	}
	defer resp.Body.Close()
	respRaw, err := io.ReadAll(resp.Body)
	if err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("读取百顺游戏列表失败: %w", err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return providerGameSyncPlan{}, fmt.Errorf("百顺游戏列表 HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respRaw)))
	}
	var list baishunGameListResponse
	if err := json.Unmarshal(respRaw, &list); err != nil {
		return providerGameSyncPlan{}, fmt.Errorf("解析百顺游戏列表失败: %w", err)
	}
	if list.Code != 0 {
		return providerGameSyncPlan{}, fmt.Errorf("百顺游戏列表返回失败: code=%d message=%s", list.Code, firstNonEmpty(list.Message, list.Msg))
	}
	games, gameURLs := baishunCatalogItems(platform.GetPlatformCode(), list.Data, req)
	if len(games) == 0 {
		return providerGameSyncPlan{}, fmt.Errorf("百顺游戏列表为空")
	}
	return providerGameSyncPlan{
		Games:    games,
		GameURLs: gameURLs,
	}, nil
}

func decodeBaishunSyncConfig(raw string) (baishunSyncConfig, error) {
	if strings.TrimSpace(raw) == "" {
		return baishunSyncConfig{}, nil
	}
	var config baishunSyncConfig
	if err := json.Unmarshal([]byte(raw), &config); err != nil {
		return baishunSyncConfig{}, fmt.Errorf("百顺适配器配置 JSON 不合法")
	}
	return config, nil
}

func missingBaishunSyncFields(appID int64, appChannel string, appKey string, endpoint string) []string {
	missing := make([]string, 0, 4)
	if appID == 0 {
		missing = append(missing, "adapterConfigJson.app_id")
	}
	if strings.TrimSpace(appChannel) == "" {
		missing = append(missing, "adapterConfigJson.app_channel")
	}
	if strings.TrimSpace(appKey) == "" {
		missing = append(missing, "回调密钥")
	}
	if strings.TrimSpace(endpoint) == "" {
		missing = append(missing, "adapterConfigJson.game_list_url 或 provider_api_base_url")
	}
	return missing
}

func (c baishunSyncConfig) appID() int64 {
	if c.AppID != 0 {
		return c.AppID
	}
	return c.AppIDCamel
}

func (c baishunSyncConfig) appChannel() string {
	return firstNonEmpty(c.AppChannel, c.AppChannelCamel)
}

func baishunGameListType(config baishunSyncConfig, req syncGamesRequest) int {
	if req.GameListType != 0 {
		return req.GameListType
	}
	if config.GameListType != 0 {
		return config.GameListType
	}
	if config.GameListTypeCamel != 0 {
		return config.GameListTypeCamel
	}
	return 1
}

func baishunGameListEndpoint(apiBaseURL string, config baishunSyncConfig) string {
	if endpoint := strings.TrimSpace(firstNonEmpty(config.GameListURL, config.GameListURLCamel)); endpoint != "" {
		return endpoint
	}
	base := strings.TrimSpace(firstNonEmpty(config.ProviderAPIBaseURL, config.ProviderAPIBaseURLAlt, apiBaseURL))
	if base == "" {
		return ""
	}
	if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") {
		base = "https://" + base
	}
	if strings.Contains(base, "/v1/api/gamelist") {
		return base
	}
	parsed, err := url.Parse(base)
	if err != nil || parsed.Host == "" {
		return ""
	}
	parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/api/gamelist"
	parsed.RawQuery = ""
	return parsed.String()
}

func baishunSignedListRequest(appID int64, appChannel string, gameListType int, appKey string) map[string]any {
	timestamp := time.Now().Unix()
	nonce := randomHex(8)
	signature := baishunMD5(nonce + appKey + strconv.FormatInt(timestamp, 10))
	return map[string]any{
		"game_list_type":  gameListType,
		"app_channel":     appChannel,
		"app_id":          appID,
		"signature":       signature,
		"signature_nonce": nonce,
		"timestamp":       timestamp,
	}
}

type baishunGameListResponse struct {
	Code    int                   `json:"code"`
	Message string                `json:"message"`
	Msg     string                `json:"msg"`
	Data    []baishunGameListItem `json:"data"`
}

type baishunGameListItem struct {
	GameID          any    `json:"game_id"`
	Name            string `json:"name"`
	GameName        string `json:"game_name"`
	PreviewURL      string `json:"preview_url"`
	IconURL         string `json:"icon_url"`
	CoverURL        string `json:"cover_url"`
	DownloadURL     string `json:"download_url"`
	GameOrientation any    `json:"game_orientation"`
	SafeHeight      any    `json:"safe_height"`
}

func baishunCatalogItems(platformCode string, providerItems []baishunGameListItem, req syncGamesRequest) ([]catalogRequest, map[string]string) {
	items := make([]catalogRequest, 0, len(providerItems))
	gameURLs := make(map[string]string, len(providerItems))
	for index, providerItem := range providerItems {
		providerGameID := stringFromJSONValue(providerItem.GameID)
		if providerGameID == "" {
			continue
		}
		name := firstNonEmpty(providerItem.Name, providerItem.GameName, providerGameID)
		iconURL := firstNonEmpty(providerItem.IconURL, providerItem.PreviewURL)
		coverURL := firstNonEmpty(providerItem.CoverURL, providerItem.PreviewURL, iconURL)
		gameURLs[providerGameID] = strings.TrimSpace(providerItem.DownloadURL)
		items = append(items, catalogRequest{
			GameID:         stableGameID(platformCode, providerGameID),
			PlatformCode:   strings.TrimSpace(platformCode),
			ProviderGameID: providerGameID,
			GameName:       name,
			Category:       defaulted(req.Category, defaultGameCategory),
			IconURL:        iconURL,
			CoverURL:       coverURL,
			LaunchMode:     defaulted(req.LaunchMode, defaultLaunchMode),
			Orientation:    baishunOrientation(providerItem.GameOrientation),
			SafeHeight:     int32FromJSONValue(providerItem.SafeHeight),
			MinCoin:        req.MinCoin,
			Status:         defaulted(req.Status, defaultGameStatus),
			SortOrder:      defaultCatalogSortOrder(len(providerItems), index),
			Tags:           compactTags(append([]string{strings.TrimSpace(platformCode), adapterBaishunV1}, req.Tags...)),
		})
	}
	return items, gameURLs
}

func selectProviderGames(games []catalogRequest, providerGameIDs []string) []catalogRequest {
	if len(providerGameIDs) == 0 {
		return games
	}
	allowed := make(map[string]struct{}, len(providerGameIDs))
	for _, id := range providerGameIDs {
		if strings.TrimSpace(id) != "" {
			allowed[strings.TrimSpace(id)] = struct{}{}
		}
	}
	out := make([]catalogRequest, 0, len(games))
	for _, game := range games {
		if _, ok := allowed[game.ProviderGameID]; ok {
			out = append(out, game)
		}
	}
	return out
}

func selectedGameURLs(all map[string]string, games []catalogRequest) map[string]string {
	out := make(map[string]string, len(games))
	for _, game := range games {
		if launchURL := strings.TrimSpace(all[game.ProviderGameID]); launchURL != "" {
			out[game.ProviderGameID] = launchURL
		}
	}
	return out
}

func mergeAdapterGameURLs(raw string, gameURLs map[string]string) string {
	config := map[string]any{}
	if strings.TrimSpace(raw) != "" {
		_ = json.Unmarshal([]byte(raw), &config)
	}
	current := map[string]any{}
	if existing, ok := config["game_urls"].(map[string]any); ok {
		for key, value := range existing {
			current[key] = value
		}
	}
	for providerGameID, launchURL := range gameURLs {
		if strings.TrimSpace(providerGameID) != "" && strings.TrimSpace(launchURL) != "" {
			current[providerGameID] = strings.TrimSpace(launchURL)
		}
	}
	config["game_urls"] = current
	out, err := json.Marshal(config)
	if err != nil {
		return strings.TrimSpace(raw)
	}
	return string(out)
}

func stableGameID(platformCode string, providerGameID string) string {
	prefix := asciiIDPart(platformCode)
	suffix := asciiIDPart(providerGameID)
	if prefix == "" {
		prefix = "game"
	}
	if suffix == "" {
		suffix = "unknown"
	}
	value := prefix + "_" + suffix
	if len(value) <= 96 {
		return value
	}
	sum := sha256.Sum256([]byte(platformCode + ":" + providerGameID))
	return prefix[:min(len(prefix), 32)] + "_" + hex.EncodeToString(sum[:16])
}

func asciiIDPart(value string) string {
	var builder strings.Builder
	lastUnderscore := false
	for _, r := range strings.ToLower(strings.TrimSpace(value)) {
		keep := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
		if keep {
			builder.WriteRune(r)
			lastUnderscore = false
			continue
		}
		if !lastUnderscore {
			builder.WriteByte('_')
			lastUnderscore = true
		}
	}
	return strings.Trim(builder.String(), "_")
}

func baishunOrientation(value any) string {
	switch strings.TrimSpace(stringFromJSONValue(value)) {
	case "2", "landscape":
		return "landscape"
	default:
		return defaultOrientation
	}
}

func stringFromJSONValue(value any) string {
	switch typed := value.(type) {
	case nil:
		return ""
	case string:
		return strings.TrimSpace(typed)
	case float64:
		if typed == float64(int64(typed)) {
			return strconv.FormatInt(int64(typed), 10)
		}
		return strconv.FormatFloat(typed, 'f', -1, 64)
	case json.Number:
		return typed.String()
	default:
		return strings.TrimSpace(fmt.Sprint(typed))
	}
}

func int32FromJSONValue(value any) int32 {
	switch typed := value.(type) {
	case nil:
		return 0
	case int:
		if typed > 0 {
			return int32(typed)
		}
	case int32:
		if typed > 0 {
			return typed
		}
	case int64:
		if typed > 0 {
			return int32(typed)
		}
	case float64:
		if typed > 0 {
			return int32(typed)
		}
	case json.Number:
		parsed, err := strconv.ParseInt(typed.String(), 10, 32)
		if err == nil && parsed > 0 {
			return int32(parsed)
		}
	case string:
		parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 32)
		if err == nil && parsed > 0 {
			return int32(parsed)
		}
	default:
		parsed, err := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 32)
		if err == nil && parsed > 0 {
			return int32(parsed)
		}
	}
	return 0
}

func baishunMD5(value string) string {
	sum := md5.Sum([]byte(value))
	return hex.EncodeToString(sum[:])
}

func randomHex(length int) string {
	buf := make([]byte, length)
	if _, err := rand.Read(buf); err != nil {
		return strconv.FormatInt(time.Now().UnixNano(), 16)
	}
	return hex.EncodeToString(buf)
}

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

func defaulted(value string, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return strings.TrimSpace(value)
}

func defaultCatalogSortOrder(total int, index int) int32 {
	// 厂商列表或稳定 key 列表已经给出默认展示顺序；大值优先后必须从高到低赋权，避免每次同步都把游戏倒序。
	if total <= 0 || index < 0 || index >= total {
		return 0
	}
	return int32((total - index) * 10)
}
