package game

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"strconv"
	"strings"
	"time"

	gamev1 "hyapp.local/api/proto/game/v1"
	"hyapp/pkg/xerr"
	gamedomain "hyapp/services/game-service/internal/domain/game"
)

const (
	// 灵仙接口使用 errorCode 作为业务结果，HTTP 成功不代表订单成功。
	leaderccCodeOK                  = 0
	leaderccCodeTimeout             = 4000
	leaderccCodeInsufficientBalance = 4004
	leaderccCodeInvalidArgument     = 4005
	leaderccCodeTokenInvalid        = 10001
	leaderccCodeRiskRejected        = 10002
	leaderccCodeDuplicatedOrder     = 10003
	leaderccCodeSignatureInvalid    = 10004

	leaderccDefaultTokenTTL = 24 * time.Hour
)

// leaderccAdapterConfig 对应后台平台配置里的 adapter_config_json。
// 目前只放跨环境可调整字段，避免每接一个灵仙游戏或切域名都发版。
type leaderccAdapterConfig struct {
	// uid_mode 必须和启动 URL、userinfo、change_coin 三处保持一致。
	UIDMode         string `json:"uid_mode"`
	DefaultLang     string `json:"default_lang"`
	TokenTTLSeconds int64  `json:"token_ttl_seconds"`
	// game_urls 由后台灵仙列表同步写入，解决同一平台下每款游戏 H5 地址不同的问题。
	GameURLs map[string]string `json:"game_urls"`
}

func leaderccConfigFromPlatform(value any) leaderccAdapterConfig {
	// Platform 用于回调，LaunchableGame 用于启动；两处共用同一套 JSON 解析规则。
	var raw string
	switch typed := value.(type) {
	case gamedomain.Platform:
		raw = typed.AdapterConfigJSON
	case gamedomain.LaunchableGame:
		raw = typed.AdapterConfigJSON
	}
	var config leaderccAdapterConfig
	_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
	config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
	config.DefaultLang = strings.TrimSpace(config.DefaultLang)
	config.GameURLs = normalizeStringMap(config.GameURLs)
	return config
}

func leaderccLaunchBaseURL(game gamedomain.LaunchableGame, config leaderccAdapterConfig) string {
	for _, key := range []string{game.ProviderGameID, game.GameID, strings.ToLower(game.GameID)} {
		if raw := strings.TrimSpace(config.GameURLs[strings.TrimSpace(key)]); raw != "" {
			return raw
		}
	}
	return ""
}

func (c leaderccAdapterConfig) TokenTTL() time.Duration {
	// 灵仙启动后会持续用 token 调后端接口，默认给 24 小时，后台可通过 JSON 覆盖。
	if c.TokenTTLSeconds > 0 {
		return time.Duration(c.TokenTTLSeconds) * time.Second
	}
	return leaderccDefaultTokenTTL
}

func (s *Service) handleLeaderCCOperation(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, operation string, requestHash string) ([]byte, string, string, bool, string, error) {
	// 灵仙不做 IP 白名单过滤，来源校验只依赖每个接口的 MD5 签名和 token/session 绑定。
	switch operation {
	// operation 由统一 callback 路由传入，兼容不同命名可以减少网关层特殊分支。
	case "userinfo", "user_info", "get_user_info":
		return s.handleLeaderCCUserInfo(ctx, app, platform, req)
	case "change_coin", "change_balance", "update_coin":
		return s.handleLeaderCCChangeCoin(ctx, app, platform, req, requestHash)
	case "repair_order", "repair", "repair_coin":
		return s.handleLeaderCCRepairOrder(ctx, app, platform, req)
	default:
		raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
		return raw, contentType, strconv.Itoa(leaderccCodeOK), false, "", nil
	}
}

func (s *Service) handleLeaderCCUserInfo(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
	// userinfo 签名顺序来自灵仙文档：gameId + uid + token + roomId + key。
	var body struct {
		GameID string `json:"gameId"`
		UID    string `json:"uid"`
		Token  string `json:"token"`
		RoomID string `json:"roomId"`
		Sign   string `json:"sign"`
	}
	if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil {
		return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "")
	}
	if leaderccTokenOnlyProbe(body.Token, body.GameID, body.UID, body.RoomID, body.Sign) {
		if _, err := s.validLeaderCCSession(ctx, app, body.Token); err != nil {
			return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, "")
		}
		raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
		return raw, contentType, strconv.Itoa(leaderccCodeOK), true, "", nil
	}
	// key 存在 callback_secret 字段里；后台会回显当前值，便于直接提供给灵仙排查签名。
	if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.GameID, body.UID, body.Token, body.RoomID) {
		return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, "")
	}
	config := leaderccConfigFromPlatform(platform)
	session, err := s.validLeaderCCSession(ctx, app, body.Token)
	// token 通过后仍要校验 uid/gameId，防止合法 token 被跨玩家或跨游戏复用。
	if err != nil || !leaderccSessionMatches(session, config, body.UID, body.GameID) {
		return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, "")
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
	if err != nil {
		return leaderccAdapterError(leaderccCodeFromError(err), leaderccMessageFromError(err), true, "")
	}
	raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{
		// 灵仙字段名是 coin；这里直接返回钱包可用 COIN，避免在适配器层引入换算。
		"uid":      body.UID,
		"nickname": snapshot.Nickname,
		"avatar":   snapshot.Avatar,
		"coin":     snapshot.Balance,
	})
	return raw, contentType, strconv.Itoa(leaderccCodeOK), true, "", nil
}

func (s *Service) handleLeaderCCChangeCoin(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, requestHash string) ([]byte, string, string, bool, string, error) {
	// change_coin 是灵仙唯一改账入口；orderId 是幂等键，roundId 用来关联同一轮投注/返奖。
	var body struct {
		OrderID    string `json:"orderId"`
		GameID     string `json:"gameId"`
		RoundID    string `json:"roundId"`
		UID        string `json:"uid"`
		Coin       int64  `json:"coin"`
		Type       int32  `json:"type"`
		RewardType int32  `json:"rewardType"`
		Token      string `json:"token"`
		WinID      string `json:"winId"`
		RoomID     string `json:"roomId"`
		Sign       string `json:"sign"`
	}
	if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil {
		return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "")
	}
	providerOrderID := strings.TrimSpace(body.OrderID)
	if leaderccTokenOnlyProbe(body.Token, body.OrderID, body.GameID, body.RoundID, body.UID, body.WinID, body.RoomID, body.Sign) && body.Coin == 0 && body.Type == 0 && body.RewardType == 0 {
		if _, err := s.validLeaderCCSession(ctx, app, body.Token); err != nil {
			return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID)
		}
		raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
		return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
	}
	// 文档签名顺序：orderId + gameId + roundId + uid + coin + type + rewardType + token + winId + roomId + key。
	if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.OrderID, body.GameID, body.RoundID, body.UID, strconv.FormatInt(body.Coin, 10), strconv.FormatInt(int64(body.Type), 10), strconv.FormatInt(int64(body.RewardType), 10), body.Token, body.WinID, body.RoomID) {
		return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, providerOrderID)
	}
	config := leaderccConfigFromPlatform(platform)
	session, err := s.validLeaderCCSession(ctx, app, body.Token)
	// 灵仙 change_coin 既带 token 又带 uid/gameId，三者都必须命中同一条启动会话。
	if err != nil || !leaderccSessionMatches(session, config, body.UID, body.GameID) {
		return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID)
	}
	if session.ProviderGameID == "" {
		session.ProviderGameID = strings.TrimSpace(body.GameID)
	}
	if session.GameID == "" {
		session.GameID = firstNonEmpty(strings.TrimSpace(body.GameID), "leadercc")
	}
	opType := leaderccOpType(body.Type)
	// type=1 扣款、type=2 加款；coin=0 没有实际改账，但仍会按幂等订单记成功。
	if providerOrderID == "" || opType == "" || body.Coin < 0 {
		return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", true, providerOrderID)
	}
	// 钱包改账和幂等逻辑与 Yomi 一致，厂商差异只停留在签名和字段映射层。
	result, err := s.applyYomiCoinChange(ctx, app, req, session, providerOrderID, body.RoundID, opType, body.Coin, body.RoomID, requestHash)
	if err != nil {
		// 灵仙余额不足需要返回 4004；其它内部错误在 leaderccCodeFromError 里统一映射。
		code := leaderccCodeFromError(err)
		message := leaderccMessageFromError(err)
		balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID)
		raw, contentType := leaderccJSON(code, message, map[string]any{"coin": balance})
		return raw, contentType, strconv.Itoa(code), true, providerOrderID, nil
	}
	if result.IdempotentReplay {
		// 灵仙要求“成功订单的重复通知”也显式返回 10003；这里不重复改账，只回历史余额。
		raw, contentType := leaderccJSON(leaderccCodeDuplicatedOrder, leaderccMessageFromError(xerr.New(xerr.IdempotencyConflict, "duplicated order")), map[string]any{"coin": result.BalanceAfter})
		return raw, contentType, strconv.Itoa(leaderccCodeDuplicatedOrder), true, providerOrderID, nil
	}
	raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{"coin": result.BalanceAfter})
	return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
}

func (s *Service) handleLeaderCCRepairOrder(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
	// 补单接口必须提供给厂商，但当前阶段只记录，不自动加钱，避免绕过钱包订单幂等。
	var body struct {
		OrderID    string `json:"orderId"`
		GameID     string `json:"gameId"`
		RoundID    string `json:"roundId"`
		UID        string `json:"uid"`
		Coin       int64  `json:"coin"`
		RewardType int32  `json:"rewardType"`
		Token      string `json:"token"`
		WinID      string `json:"winId"`
		RoomID     string `json:"roomId"`
		Sign       string `json:"sign"`
	}
	if err := decodeLeaderCCJSON(req.GetRawBody(), &body); err != nil {
		return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", false, "")
	}
	providerOrderID := strings.TrimSpace(body.OrderID)
	if leaderccTokenOnlyProbe(body.Token, body.OrderID, body.GameID, body.RoundID, body.UID, body.WinID, body.RoomID, body.Sign) && body.Coin == 0 && body.RewardType == 0 {
		if _, err := s.validLeaderCCSession(ctx, app, body.Token); err != nil {
			return leaderccAdapterError(leaderccCodeTokenInvalid, "token invalid", true, providerOrderID)
		}
		raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
		return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
	}
	// 文档签名顺序：orderId + gameId + roundId + uid + coin + rewardType + winId + roomId + key。
	if !leaderccSignValid(body.Sign, platform.CallbackSecretCiphertext, body.OrderID, body.GameID, body.RoundID, body.UID, strconv.FormatInt(body.Coin, 10), strconv.FormatInt(int64(body.RewardType), 10), body.WinID, body.RoomID) {
		return leaderccAdapterError(leaderccCodeSignatureInvalid, "signature invalid", false, providerOrderID)
	}
	if providerOrderID == "" || strings.TrimSpace(body.GameID) == "" || strings.TrimSpace(body.UID) == "" || body.Coin < 0 {
		return leaderccAdapterError(leaderccCodeInvalidArgument, "invalid payload", true, providerOrderID)
	}
	// repair_id 稳定派生，厂商重复补单会命中唯一索引并保持幂等。
	_, _, err := s.repository.CreateRepairOrder(ctx, gamedomain.RepairOrder{
		AppCode:         app,
		RepairID:        "grep_" + stableHash(app+"|"+platform.PlatformCode+"|"+providerOrderID),
		PlatformCode:    platform.PlatformCode,
		ProviderOrderID: providerOrderID,
		Reason:          "leadercc_repair_order",
		Status:          gamedomain.RepairStatusLogged,
	})
	if err != nil {
		return leaderccAdapterError(leaderccCodeFromError(err), leaderccMessageFromError(err), true, providerOrderID)
	}
	raw, contentType := leaderccJSON(leaderccCodeOK, "", map[string]any{})
	return raw, contentType, strconv.Itoa(leaderccCodeOK), true, providerOrderID, nil
}

func (s *Service) validLeaderCCSession(ctx context.Context, app string, token string) (gamedomain.LaunchSession, error) {
	// 灵仙回调直接使用 App 登录 access token；无需预先进入游戏创建 launch session。
	if strings.Count(strings.TrimSpace(token), ".") == 2 {
		return s.appSessionFromAccessToken(app, token)
	}
	// 兼容历史联调 token：老链路会先 launch，再按 game_launch_sessions 校验。
	session, err := s.validYomiSession(ctx, app, token)
	if err != nil {
		return gamedomain.LaunchSession{}, err
	}
	return session, nil
}

func leaderccSessionMatches(session gamedomain.LaunchSession, config leaderccAdapterConfig, uid string, gameID string) bool {
	// 使用 App token 时 session.ProviderGameID 可能为空，此时不强制要求先配置/启动游戏目录。
	if strings.TrimSpace(gameID) != "" && strings.TrimSpace(session.ProviderGameID) != "" && strings.TrimSpace(gameID) != strings.TrimSpace(session.ProviderGameID) {
		return false
	}
	normalizedUID := strings.TrimSpace(uid)
	return normalizedUID == externalUserID(session, config.UIDMode) ||
		normalizedUID == strings.TrimSpace(session.DisplayUserID) ||
		normalizedUID == strconv.FormatInt(session.UserID, 10)
}

func leaderccOpType(value int32) string {
	// 厂商枚举在适配器内结束，下游钱包只认识 debit/credit。
	switch value {
	case 1:
		return "debit"
	case 2:
		return "credit"
	default:
		return ""
	}
}

func decodeLeaderCCJSON(raw []byte, out any) error {
	// UseNumber 保持大订单号/局号不被 JSON float64 解析损坏，虽然当前结构体多数字段是 string。
	decoder := json.NewDecoder(strings.NewReader(string(raw)))
	decoder.UseNumber()
	return decoder.Decode(out)
}

func leaderccSignValid(actual string, key string, parts ...string) bool {
	// 灵仙签名是“按文档字段顺序拼接所有字段 + key”后取 MD5 小写 hex。
	key = strings.TrimSpace(key)
	if key == "" {
		return false
	}
	joined := strings.Builder{}
	for _, part := range parts {
		// 厂商样例没有分隔符；这里 trim 只清理传输空白，不改变字段顺序。
		joined.WriteString(strings.TrimSpace(part))
	}
	joined.WriteString(key)
	sum := md5.Sum([]byte(joined.String()))
	expected := hex.EncodeToString(sum[:])
	return strings.EqualFold(strings.TrimSpace(actual), expected)
}

func leaderccTokenOnlyProbe(token string, parts ...string) bool {
	if strings.TrimSpace(token) == "" {
		return false
	}
	for _, part := range parts {
		if strings.TrimSpace(part) != "" {
			return false
		}
	}
	return true
}

func leaderccAdapterError(code int, message string, signatureValid bool, providerRequestID string) ([]byte, string, string, bool, string, error) {
	// signatureValid/providerRequestID 会写入 callback log，帮助后续排查签名和重复订单问题。
	raw, contentType := leaderccJSON(code, message, map[string]any{})
	return raw, contentType, strconv.Itoa(code), signatureValid, providerRequestID, nil
}

func leaderccJSON(errorCode int, message string, data any) ([]byte, string) {
	// 灵仙成功可以不带 message；失败时带 message 方便联调定位。
	payload := map[string]any{"code": errorCode, "errorCode": errorCode}
	if data != nil {
		payload["data"] = data
	}
	if errorCode == leaderccCodeOK && strings.TrimSpace(message) == "" {
		message = "ok"
	}
	if strings.TrimSpace(message) != "" {
		payload["message"] = message
	}
	return jsonResponse(payload)
}

func leaderccCodeFromError(err error) int {
	// 内部错误码只在适配器边界映射为灵仙错误码，业务层不依赖厂商语义。
	switch {
	case err == nil:
		return leaderccCodeOK
	case xerr.IsCode(err, xerr.InsufficientBalance):
		return leaderccCodeInsufficientBalance
	case xerr.IsCode(err, xerr.SessionExpired), xerr.IsCode(err, xerr.Unauthorized):
		return leaderccCodeTokenInvalid
	case xerr.IsCode(err, xerr.IdempotencyConflict):
		return leaderccCodeDuplicatedOrder
	case xerr.IsCode(err, xerr.Unavailable):
		return leaderccCodeTimeout
	default:
		return leaderccCodeInvalidArgument
	}
}

func leaderccMessageFromError(err error) string {
	switch leaderccCodeFromError(err) {
	case leaderccCodeInsufficientBalance:
		return "coin not enough"
	case leaderccCodeTokenInvalid:
		return "token invalid"
	case leaderccCodeDuplicatedOrder:
		return "duplicated order"
	case leaderccCodeTimeout:
		return "network timeout"
	default:
		if err != nil && strings.TrimSpace(xerr.MessageOf(err)) != "" {
			return xerr.MessageOf(err)
		}
		return "invalid argument"
	}
}
