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 (
	// 热游文档只定义这几个业务码；HTTP 200 仅表示回调已被接收，游戏侧以 errorCode 判定业务结果。
	reyouCodeOK                  = 0
	reyouCodeTokenInvalid        = 1001
	reyouCodeInsufficientBalance = 2001
	reyouCodeDuplicatedOrder     = 3001

	reyouDefaultTokenTTL = 24 * time.Hour
)

type reyouAdapterConfig struct {
	// uid_mode 必须同时约束启动 URL 和回调校验，避免同一 token 被不同用户标识复用。
	UIDMode         string `json:"uid_mode"`
	DefaultLang     string `json:"default_lang"`
	TokenTTLSeconds int64  `json:"token_ttl_seconds"`
	// 热游文档没有提供游戏列表 API，后台用 game_urls 保存厂商给的逐游戏 H5 地址。
	GameURLs          map[string]string `json:"game_urls"`
	LaunchURLTemplate string            `json:"launch_url_template"`
}

func reyouConfigFromPlatform(value any) reyouAdapterConfig {
	// 启动链路拿 LaunchableGame，回调链路拿 Platform；两者共用一套 JSON 字段，避免配置漂移。
	var raw string
	switch typed := value.(type) {
	case gamedomain.Platform:
		raw = typed.AdapterConfigJSON
	case gamedomain.LaunchableGame:
		raw = typed.AdapterConfigJSON
	}
	var config reyouAdapterConfig
	_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
	config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
	config.DefaultLang = strings.TrimSpace(config.DefaultLang)
	config.LaunchURLTemplate = strings.TrimSpace(config.LaunchURLTemplate)
	config.GameURLs = normalizeStringMap(config.GameURLs)
	return config
}

func (c reyouAdapterConfig) TokenTTL() time.Duration {
	// 热游要求 token 在游戏中以及延迟结算后至少 3 分钟仍有效；默认 24 小时覆盖长局和重试场景。
	if c.TokenTTLSeconds > 0 {
		return time.Duration(c.TokenTTLSeconds) * time.Second
	}
	return reyouDefaultTokenTTL
}

func (c reyouAdapterConfig) LanguageValue() string {
	if c.DefaultLang != "" {
		return c.DefaultLang
	}
	return "en"
}

func reyouLaunchBaseURL(game gamedomain.LaunchableGame, config reyouAdapterConfig) 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
		}
	}
	if config.LaunchURLTemplate == "" {
		return ""
	}
	replacer := strings.NewReplacer(
		"{provider_game_id}", strings.TrimSpace(game.ProviderGameID),
		"{providerGameId}", strings.TrimSpace(game.ProviderGameID),
		"{game_id}", strings.TrimSpace(game.GameID),
		"{gameId}", strings.TrimSpace(game.GameID),
	)
	return strings.TrimSpace(replacer.Replace(config.LaunchURLTemplate))
}

func (s *Service) handleReyouOperation(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, operation string, requestHash string) ([]byte, string, string, bool, string, error) {
	// IP 白名单是热游正式环境侧的第一层来源校验；未配置时兼容测试环境任意来源。
	if !callbackIPAllowed(req, platform.CallbackIPWhitelist) {
		return reyouAdapterError(reyouCodeTokenInvalid, "ip restricted", false, "")
	}
	switch operation {
	case "getuserinfo", "get_user_info", "get-user-info", "userinfo", "user_info":
		return s.handleReyouUserInfo(ctx, app, platform, req)
	case "updatebalance", "update_balance", "update-balance", "change_balance":
		return s.handleReyouUpdateBalance(ctx, app, platform, req, requestHash)
	default:
		raw, contentType := reyouJSON(reyouCodeOK, "", map[string]any{})
		return raw, contentType, strconv.Itoa(reyouCodeOK), false, "", nil
	}
}

type reyouUserInfoBody struct {
	GameID json.RawMessage `json:"gameId"`
	UID    json.RawMessage `json:"uid"`
	Token  json.RawMessage `json:"token"`
	Sign   string          `json:"sign"`
}

func (body reyouUserInfoBody) gameIDText() string { return reyouJSONText(body.GameID) }
func (body reyouUserInfoBody) uidText() string    { return reyouJSONText(body.UID) }
func (body reyouUserInfoBody) tokenText() string  { return reyouJSONText(body.Token) }

type reyouUpdateBalanceBody struct {
	OrderID json.RawMessage `json:"orderId"`
	GameID  json.RawMessage `json:"gameId"`
	RoundID json.RawMessage `json:"roundId"`
	UID     json.RawMessage `json:"uid"`
	Coin    json.RawMessage `json:"coin"`
	Type    json.RawMessage `json:"type"`
	Token   json.RawMessage `json:"token"`
	Sign    string          `json:"sign"`
}

func (body reyouUpdateBalanceBody) orderIDText() string { return reyouJSONText(body.OrderID) }
func (body reyouUpdateBalanceBody) gameIDText() string  { return reyouJSONText(body.GameID) }
func (body reyouUpdateBalanceBody) roundIDText() string { return reyouJSONText(body.RoundID) }
func (body reyouUpdateBalanceBody) uidText() string     { return reyouJSONText(body.UID) }
func (body reyouUpdateBalanceBody) coinText() string    { return reyouJSONText(body.Coin) }
func (body reyouUpdateBalanceBody) typeText() string    { return reyouJSONText(body.Type) }
func (body reyouUpdateBalanceBody) tokenText() string   { return reyouJSONText(body.Token) }

func (s *Service) handleReyouUserInfo(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
	var body reyouUserInfoBody
	if err := decodeReyouJSON(req.GetRawBody(), &body); err != nil {
		return reyouAdapterError(reyouCodeTokenInvalid, "invalid payload", false, "")
	}
	gameID := body.gameIDText()
	uid := body.uidText()
	token := body.tokenText()
	// 文档签名顺序固定为 gameId + uid + token + key；不能排序，也不能插入分隔符。
	if !reyouSignValid(body.Sign, platform.CallbackSecretCiphertext, gameID, uid, token) {
		return reyouAdapterError(reyouCodeTokenInvalid, "signature invalid", false, "")
	}
	config := reyouConfigFromPlatform(platform)
	session, err := s.validReyouSession(ctx, app, token)
	// token 通过后继续绑定 uid/gameId，防止同一登录 token 被跨用户或跨游戏复用。
	if err != nil || !reyouSessionMatches(session, config, uid, gameID) {
		return reyouAdapterError(reyouCodeTokenInvalid, "token invalid", true, "")
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
	if err != nil {
		return reyouAdapterError(reyouCodeFromError(err), reyouMessageFromError(err), true, "")
	}
	raw, contentType := reyouJSON(reyouCodeOK, "", map[string]any{
		"uid":      uid,
		"nickname": snapshot.Nickname,
		"avatar":   snapshot.Avatar,
		"coin":     snapshot.Balance,
		"vipLevel": int64(0),
	})
	return raw, contentType, strconv.Itoa(reyouCodeOK), true, "", nil
}

func (s *Service) handleReyouUpdateBalance(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, requestHash string) ([]byte, string, string, bool, string, error) {
	var body reyouUpdateBalanceBody
	if err := decodeReyouJSON(req.GetRawBody(), &body); err != nil {
		return reyouAdapterError(reyouCodeTokenInvalid, "invalid payload", false, "")
	}
	providerOrderID := body.orderIDText()
	gameID := body.gameIDText()
	roundID := body.roundIDText()
	uid := body.uidText()
	coinText := body.coinText()
	typeText := body.typeText()
	token := body.tokenText()
	// 文档签名顺序固定为 orderId + gameId + roundId + uid + coin + type + token + key。
	if !reyouSignValid(body.Sign, platform.CallbackSecretCiphertext, providerOrderID, gameID, roundID, uid, coinText, typeText, token) {
		return reyouAdapterError(reyouCodeTokenInvalid, "signature invalid", false, providerOrderID)
	}
	coin, coinOK := reyouInt64(coinText)
	opValue, typeOK := reyouInt32(typeText)
	opType := reyouOpType(opValue)
	if providerOrderID == "" || gameID == "" || uid == "" || token == "" || !coinOK || !typeOK || opType == "" || coin < 0 {
		return reyouAdapterError(reyouCodeTokenInvalid, "invalid payload", true, providerOrderID)
	}
	config := reyouConfigFromPlatform(platform)
	session, err := s.validReyouSession(ctx, app, token)
	// 热游同时带 token、uid 和 gameId，三者必须落到同一个启动会话；JWT 直连时允许 gameId 后置补齐。
	if err != nil || !reyouSessionMatches(session, config, uid, gameID) {
		return reyouAdapterError(reyouCodeTokenInvalid, "token invalid", true, providerOrderID)
	}
	if session.ProviderGameID == "" {
		session.ProviderGameID = gameID
	}
	if session.GameID == "" {
		session.GameID = gameID
	}
	result, err := s.applyYomiCoinChange(ctx, app, req, session, providerOrderID, roundID, opType, coin, "", requestHash)
	if err != nil {
		code := reyouCodeFromError(err)
		balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID)
		raw, contentType := reyouJSON(code, reyouMessageFromError(err), map[string]any{"coin": balance, "responseId": ""})
		return raw, contentType, strconv.Itoa(code), true, providerOrderID, nil
	}
	if result.IdempotentReplay {
		// 热游文档要求重复 orderId 明确返回 3001；这里只回历史余额，不再次触发钱包改账。
		raw, contentType := reyouJSON(reyouCodeDuplicatedOrder, "duplicated order", map[string]any{"coin": result.BalanceAfter, "responseId": reyouResponseID(app, req.GetPlatformCode(), providerOrderID)})
		return raw, contentType, strconv.Itoa(reyouCodeDuplicatedOrder), true, providerOrderID, nil
	}
	raw, contentType := reyouJSON(reyouCodeOK, "", map[string]any{"coin": result.BalanceAfter, "responseId": reyouResponseID(app, req.GetPlatformCode(), providerOrderID)})
	return raw, contentType, strconv.Itoa(reyouCodeOK), true, providerOrderID, nil
}

func (s *Service) validReyouSession(ctx context.Context, app string, token string) (gamedomain.LaunchSession, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
	}
	// App 当前把 access token 作为三方 token；JWT 要优先解析，避免旧 launch session 同 hash 但旧 gameId 误判。
	if strings.Count(token, ".") == 2 {
		if session, err := s.appSessionFromAccessToken(app, token); err == nil {
			return session, nil
		}
	}
	if session, err := s.validYomiSession(ctx, app, token); err == nil {
		return session, nil
	}
	if strings.Count(token, ".") == 2 {
		return s.appSessionFromAccessToken(app, token)
	}
	return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
}

func reyouSessionMatches(session gamedomain.LaunchSession, config reyouAdapterConfig, uid string, gameID string) bool {
	if strings.TrimSpace(gameID) != "" && strings.TrimSpace(session.ProviderGameID) != "" && strings.TrimSpace(gameID) != strings.TrimSpace(session.ProviderGameID) {
		return false
	}
	uid = strings.TrimSpace(uid)
	return uid == externalUserID(session, config.UIDMode) ||
		uid == strings.TrimSpace(session.DisplayUserID) ||
		uid == strconv.FormatInt(session.UserID, 10)
}

func reyouOpType(value int32) string {
	// 热游 type=1 表示扣减，type=2 表示增加；下游钱包只接受 debit/credit。
	switch value {
	case 1:
		return "debit"
	case 2:
		return "credit"
	default:
		return ""
	}
}

func decodeReyouJSON(raw []byte, out any) error {
	decoder := json.NewDecoder(strings.NewReader(string(raw)))
	decoder.UseNumber()
	return decoder.Decode(out)
}

func reyouJSONText(raw json.RawMessage) string {
	text := strings.TrimSpace(string(raw))
	if text == "" || text == "null" {
		return ""
	}
	var decoded string
	if err := json.Unmarshal(raw, &decoded); err == nil {
		return strings.TrimSpace(decoded)
	}
	return text
}

func reyouInt64(value string) (int64, bool) {
	parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
	return parsed, err == nil
}

func reyouInt32(value string) (int32, bool) {
	parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 32)
	return int32(parsed), err == nil
}

func reyouSignValid(actual string, key string, parts ...string) bool {
	key = strings.TrimSpace(key)
	if key == "" || strings.TrimSpace(actual) == "" {
		return false
	}
	builder := strings.Builder{}
	for _, part := range parts {
		builder.WriteString(strings.TrimSpace(part))
	}
	builder.WriteString(key)
	sum := md5.Sum([]byte(builder.String()))
	expected := strings.ToUpper(hex.EncodeToString(sum[:]))
	return strings.EqualFold(strings.TrimSpace(actual), expected)
}

func reyouResponseID(app string, platformCode string, providerOrderID string) string {
	return "reyou_" + stableHash(app+"|"+platformCode+"|"+providerOrderID)
}

func reyouAdapterError(code int, message string, signatureValid bool, providerRequestID string) ([]byte, string, string, bool, string, error) {
	raw, contentType := reyouJSON(code, message, map[string]any{})
	return raw, contentType, strconv.Itoa(code), signatureValid, providerRequestID, nil
}

func reyouJSON(errorCode int, message string, data any) ([]byte, string) {
	payload := map[string]any{"errorCode": errorCode}
	if data != nil {
		payload["data"] = data
	}
	if strings.TrimSpace(message) != "" {
		payload["message"] = strings.TrimSpace(message)
	}
	return jsonResponse(payload)
}

func reyouCodeFromError(err error) int {
	switch {
	case err == nil:
		return reyouCodeOK
	case xerr.IsCode(err, xerr.InsufficientBalance):
		return reyouCodeInsufficientBalance
	case xerr.IsCode(err, xerr.IdempotencyConflict):
		return reyouCodeDuplicatedOrder
	default:
		return reyouCodeTokenInvalid
	}
}

func reyouMessageFromError(err error) string {
	switch reyouCodeFromError(err) {
	case reyouCodeInsufficientBalance:
		return "coin not enough"
	case reyouCodeDuplicatedOrder:
		return "duplicated order"
	default:
		if err != nil && strings.TrimSpace(xerr.MessageOf(err)) != "" {
			return xerr.MessageOf(err)
		}
		return "token invalid"
	}
}
