package game

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

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

const (
	// BAISHUN 文档定义 code=0 成功；余额不足固定 1008，签名错误固定 1003。
	baishunCodeOK                  = 0
	baishunCodeInvalidData         = 1001
	baishunCodeReadDataFailed      = 1002
	baishunCodeSignatureInvalid    = 1003
	baishunCodeInsufficientBalance = 1008
	baishunCodeGameMaintenance     = 1019
	baishunCodeAPIServerError      = 1021
	baishunCodeIPRestricted        = 1023

	baishunDefaultTokenTTL       = 24 * time.Hour
	baishunSignatureValidWindow  = 15 * time.Second
	baishunDefaultSuccessMessage = "succeed"
)

type baishunAdapterConfig struct {
	// app_id/app_channel/app_key 均来自百顺后台；app_key 存在平台 callbackSecret 中。
	AppID           int64  `json:"app_id"`
	AppIDAlias      int64  `json:"appId"`
	AppChannel      string `json:"app_channel"`
	AppChannelAlias string `json:"appChannel"`
	UIDMode         string `json:"uid_mode"`
	DefaultLang     string `json:"default_lang"`
	TokenTTLSeconds int64  `json:"token_ttl_seconds"`

	// 以下字段是 NativeBridge.getConfig 需要返回给 H5 的配置，同时也会拼在启动 URL query 中给壳层复用。
	GameMode     baishunGameModeValue `json:"game_mode"`
	SceneMode    int64                `json:"scene_mode"`
	CurrencyIcon string               `json:"currency_icon"`
	GSP          int64                `json:"gsp"`

	// 可选用户管控字段；配置后会随 get_sstoken/get_user_info 一起返回给百顺。
	UserType    int    `json:"user_type"`
	ReleaseCond int    `json:"release_cond"`
	IsOldUser   *bool  `json:"is_old_user"`
	Extend      string `json:"extend"`

	// game_urls 支持同一个百顺商户下不同 game_id 使用不同 download_url。
	GameURLs          map[string]string `json:"game_urls"`
	LaunchURLTemplate string            `json:"launch_url_template"`
}

func baishunConfigFromPlatform(value any) baishunAdapterConfig {
	var raw string
	switch typed := value.(type) {
	case gamedomain.Platform:
		raw = typed.AdapterConfigJSON
	case gamedomain.LaunchableGame:
		raw = typed.AdapterConfigJSON
	}
	var config baishunAdapterConfig
	_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
	if config.AppID == 0 {
		config.AppID = config.AppIDAlias
	}
	if strings.TrimSpace(config.AppChannel) == "" {
		config.AppChannel = config.AppChannelAlias
	}
	config.AppChannel = strings.TrimSpace(config.AppChannel)
	config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
	config.DefaultLang = strings.TrimSpace(config.DefaultLang)
	config.GameMode = baishunGameModeValue(strings.TrimSpace(string(config.GameMode)))
	config.CurrencyIcon = strings.TrimSpace(config.CurrencyIcon)
	config.Extend = strings.TrimSpace(config.Extend)
	config.LaunchURLTemplate = strings.TrimSpace(config.LaunchURLTemplate)
	config.GameURLs = normalizeStringMap(config.GameURLs)
	return config
}

func (c baishunAdapterConfig) TokenTTL() time.Duration {
	if c.TokenTTLSeconds > 0 {
		return time.Duration(c.TokenTTLSeconds) * time.Second
	}
	return baishunDefaultTokenTTL
}

func (c baishunAdapterConfig) LanguageValue() string {
	if c.DefaultLang != "" {
		return c.DefaultLang
	}
	return "2"
}

func (c baishunAdapterConfig) GameModeValue() string {
	if c.GameMode != "" {
		return string(c.GameMode)
	}
	return "3"
}

type baishunGameModeValue string

func (v *baishunGameModeValue) UnmarshalJSON(raw []byte) error {
	// 后台适配器配置是自由 JSON，历史上既可能保存 "2"，也可能保存 2。
	// 百顺启动 query 最终只需要文本值；在配置入口归一，避免数字类型被 Go string 字段吞掉后误走默认 gameMode=3。
	decoder := json.NewDecoder(strings.NewReader(strings.TrimSpace(string(raw))))
	decoder.UseNumber()
	var value any
	if err := decoder.Decode(&value); err != nil {
		return err
	}
	switch typed := value.(type) {
	case nil:
		*v = ""
	case string:
		*v = baishunGameModeValue(strings.TrimSpace(typed))
	case json.Number:
		*v = baishunGameModeValue(strings.TrimSpace(typed.String()))
	default:
		// 复杂类型不是百顺协议值；按未配置处理，保留默认值兜底，同时不影响同一 JSON 中其它配置解析。
		*v = ""
	}
	return nil
}

func baishunLaunchBaseURL(game gamedomain.LaunchableGame, config baishunAdapterConfig) 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) handleBaishunOperation(ctx context.Context, app string, platform gamedomain.Platform, req *gamev1.CallbackRequest, operation string, requestHash string) ([]byte, string, string, bool, string, error) {
	if !callbackIPAllowed(req, platform.CallbackIPWhitelist) {
		raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeIPRestricted, "ip restricted", map[string]any{})
		return raw, contentType, strconv.Itoa(baishunCodeIPRestricted), false, "", nil
	}
	config := baishunConfigFromPlatform(platform)
	body, signed, decoded, providerRequestID := s.decodeBaishunBody(req, platform.CallbackSecretCiphertext)
	if !signed {
		raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeSignatureInvalid, "signature invalid", map[string]any{})
		return raw, contentType, strconv.Itoa(baishunCodeSignatureInvalid), false, providerRequestID, nil
	}
	if !decoded {
		raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "invalid data", map[string]any{})
		return raw, contentType, strconv.Itoa(baishunCodeInvalidData), signed, providerRequestID, nil
	}
	if err := validateBaishunAppID(config, body.AppIDInt64()); err != nil {
		raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "invalid app_id", map[string]any{})
		return raw, contentType, strconv.Itoa(baishunCodeInvalidData), signed, providerRequestID, nil
	}
	switch operation {
	case "get_sstoken", "getsstoken", "sstoken", "api/get_sstoken":
		return s.handleBaishunGetSSToken(ctx, app, config, req, body)
	case "get_user_info", "userinfo", "user_info", "api/get_user_info":
		return s.handleBaishunUserInfo(ctx, app, config, req, body)
	case "update_sstoken", "updatesstoken", "refresh_sstoken", "api/update_sstoken":
		return s.handleBaishunUpdateSSToken(ctx, app, config, req, body)
	case "change_balance", "changebalance", "api/change_balance":
		return s.handleBaishunChangeBalance(ctx, app, config, req, body, requestHash)
	default:
		raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeOK, baishunDefaultSuccessMessage, map[string]any{})
		return raw, contentType, strconv.Itoa(baishunCodeOK), signed, providerRequestID, nil
	}
}

type baishunBody struct {
	AppID          json.Number `json:"app_id"`
	UserID         string      `json:"user_id"`
	Code           string      `json:"code"`
	SSToken        string      `json:"ss_token"`
	ClientIP       string      `json:"client_ip"`
	GameID         json.Number `json:"game_id"`
	CurrencyDiff   int64       `json:"currency_diff"`
	DiffMsg        string      `json:"diff_msg"`
	GameRoundID    string      `json:"game_round_id"`
	RoomID         string      `json:"room_id"`
	ChangeTimeAt   int64       `json:"change_time_at"`
	OrderID        string      `json:"order_id"`
	Extend         string      `json:"extend"`
	MsgType        string      `json:"msg_type"`
	CurrencyType   json.Number `json:"currency_type"`
	SignatureNonce string      `json:"signature_nonce"`
	Timestamp      int64       `json:"timestamp"`
	Signature      string      `json:"signature"`
	ProviderName   string      `json:"provider_name"`
}

func (s *Service) decodeBaishunBody(req *gamev1.CallbackRequest, secret string) (baishunBody, bool, bool, string) {
	var body baishunBody
	decoder := json.NewDecoder(strings.NewReader(string(req.GetRawBody())))
	decoder.UseNumber()
	if err := decoder.Decode(&body); err != nil {
		return baishunBody{}, false, false, ""
	}
	body.normalize()
	providerRequestID := strings.TrimSpace(body.OrderID)
	if !s.baishunSignatureValid(body, secret) {
		return baishunBody{}, false, true, providerRequestID
	}
	return body, true, true, providerRequestID
}

func (s *Service) baishunSignatureValid(body baishunBody, secret string) bool {
	secret = strings.TrimSpace(secret)
	if secret == "" || body.SignatureNonce == "" || body.Timestamp <= 0 {
		return false
	}
	requestAt := time.Unix(body.Timestamp, 0)
	now := s.now()
	if requestAt.After(now.Add(baishunSignatureValidWindow)) || now.Sub(requestAt) > baishunSignatureValidWindow {
		return false
	}
	raw := fmt.Sprintf("%s%s%d", body.SignatureNonce, secret, body.Timestamp)
	sum := md5.Sum([]byte(raw))
	if !strings.EqualFold(body.Signature, hex.EncodeToString(sum[:])) {
		return false
	}
	// 文档要求 signature_nonce 在 15 秒窗口内不可重复；这里做单进程防重放，多节点严格一致需接共享缓存。
	if !s.rememberSignatureNonce(gamedomain.AdapterBaishunV1, body.SignatureNonce, requestAt, baishunSignatureValidWindow) {
		return false
	}
	return true
}

func (body *baishunBody) normalize() {
	body.UserID = strings.TrimSpace(body.UserID)
	body.Code = strings.TrimSpace(body.Code)
	body.SSToken = strings.TrimSpace(body.SSToken)
	body.ClientIP = strings.TrimSpace(body.ClientIP)
	body.DiffMsg = strings.ToLower(strings.TrimSpace(body.DiffMsg))
	body.GameRoundID = strings.TrimSpace(body.GameRoundID)
	body.RoomID = strings.TrimSpace(body.RoomID)
	body.OrderID = strings.TrimSpace(body.OrderID)
	body.Extend = strings.TrimSpace(body.Extend)
	body.MsgType = strings.TrimSpace(body.MsgType)
	body.SignatureNonce = strings.TrimSpace(body.SignatureNonce)
	body.Signature = strings.TrimSpace(body.Signature)
	body.ProviderName = strings.TrimSpace(body.ProviderName)
}

func (body baishunBody) AppIDInt64() int64 {
	value, _ := strconv.ParseInt(strings.TrimSpace(body.AppID.String()), 10, 64)
	return value
}

func (body baishunBody) GameIDInt64() int64 {
	value, _ := strconv.ParseInt(strings.TrimSpace(body.GameID.String()), 10, 64)
	return value
}

func validateBaishunAppID(config baishunAdapterConfig, appID int64) error {
	if appID <= 0 {
		return xerr.New(xerr.InvalidArgument, "app_id required")
	}
	if config.AppID > 0 && appID != config.AppID {
		return xerr.New(xerr.InvalidArgument, "app_id mismatch")
	}
	return nil
}

func (s *Service) handleBaishunGetSSToken(ctx context.Context, app string, config baishunAdapterConfig, req *gamev1.CallbackRequest, body baishunBody) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.Code == "" {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "invalid data", true, "")
	}
	session, err := s.validBaishunSession(ctx, app, body.Code)
	if err != nil || !baishunSessionMatches(session, config, body.UserID, 0) {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "token invalid", true, "")
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
	if err != nil {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunUserSnapshotCodeFromError(err), baishunUserSnapshotMessageFromError(err), true, "")
	}
	response := map[string]any{
		"code":      baishunCodeOK,
		"message":   baishunDefaultSuccessMessage,
		"unique_id": baishunUniqueID(req.GetMeta().GetRequestId()),
		"data": map[string]any{
			"ss_token":    body.Code,
			"expire_date": session.ExpiresAtMS,
		},
		"user_info": baishunUserInfoData(session, config, snapshot),
	}
	raw, contentType := jsonResponse(response)
	return raw, contentType, strconv.Itoa(baishunCodeOK), true, "", nil
}

func (s *Service) handleBaishunUserInfo(ctx context.Context, app string, config baishunAdapterConfig, req *gamev1.CallbackRequest, body baishunBody) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.SSToken == "" {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "invalid data", true, "")
	}
	session, err := s.validBaishunSession(ctx, app, body.SSToken)
	if err != nil || !baishunSessionMatches(session, config, body.UserID, body.GameIDInt64()) {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "token invalid", true, "")
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
	if err != nil {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunUserSnapshotCodeFromError(err), baishunUserSnapshotMessageFromError(err), true, "")
	}
	raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeOK, baishunDefaultSuccessMessage, baishunUserInfoData(session, config, snapshot))
	return raw, contentType, strconv.Itoa(baishunCodeOK), true, "", nil
}

func (s *Service) handleBaishunUpdateSSToken(ctx context.Context, app string, config baishunAdapterConfig, req *gamev1.CallbackRequest, body baishunBody) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.SSToken == "" {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "invalid data", true, "")
	}
	session, err := s.validBaishunSession(ctx, app, body.SSToken)
	if err != nil || !baishunSessionMatches(session, config, body.UserID, body.GameIDInt64()) {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "token invalid", true, "")
	}
	raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeOK, baishunDefaultSuccessMessage, map[string]any{
		"ss_token":    body.SSToken,
		"expire_date": session.ExpiresAtMS,
	})
	return raw, contentType, strconv.Itoa(baishunCodeOK), true, "", nil
}

func (s *Service) handleBaishunChangeBalance(ctx context.Context, app string, config baishunAdapterConfig, req *gamev1.CallbackRequest, body baishunBody, requestHash string) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.SSToken == "" || body.OrderID == "" || body.GameIDInt64() <= 0 || body.DiffMsg == "" {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "invalid data", true, body.OrderID)
	}
	session, err := s.validBaishunSession(ctx, app, body.SSToken)
	if err != nil || !baishunSessionMatches(session, config, body.UserID, body.GameIDInt64()) {
		return baishunAdapterError(req.GetMeta().GetRequestId(), baishunCodeInvalidData, "token invalid", true, body.OrderID)
	}
	if session.ProviderGameID == "" {
		session.ProviderGameID = strconv.FormatInt(body.GameIDInt64(), 10)
	}
	if session.GameID == "" {
		session.GameID = session.ProviderGameID
	}
	opType, amount := baishunOpType(body.CurrencyDiff)
	result, err := s.applyYomiCoinChange(ctx, app, req, session, body.OrderID, body.GameRoundID, opType, amount, body.RoomID, requestHash)
	if err != nil {
		code := baishunCodeFromError(err)
		balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID)
		raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), code, baishunMessageFromError(err), baishunChangeBalanceData(balance, body.Extend))
		return raw, contentType, strconv.Itoa(code), true, body.OrderID, nil
	}
	raw, contentType := baishunJSON(req.GetMeta().GetRequestId(), baishunCodeOK, baishunDefaultSuccessMessage, baishunChangeBalanceData(result.BalanceAfter, body.Extend))
	return raw, contentType, strconv.Itoa(baishunCodeOK), true, body.OrderID, nil
}

func (s *Service) validBaishunSession(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")
	}
	// 百顺的 ss_token 直接复用 App access token；同一个登录 token 可能打开多款百顺游戏。
	// JWT 必须优先解析，否则会先命中旧启动会话的 hash，并被旧 provider_game_id 误判为跨游戏复用。
	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 baishunSessionMatches(session gamedomain.LaunchSession, config baishunAdapterConfig, userID string, gameID int64) bool {
	if gameID > 0 && strings.TrimSpace(session.ProviderGameID) != "" && strconv.FormatInt(gameID, 10) != strings.TrimSpace(session.ProviderGameID) {
		return false
	}
	userID = strings.TrimSpace(userID)
	return userID == externalUserID(session, config.UIDMode) ||
		userID == strings.TrimSpace(session.DisplayUserID) ||
		userID == strconv.FormatInt(session.UserID, 10)
}

func baishunOpType(diff int64) (string, int64) {
	if diff < 0 {
		return "debit", -diff
	}
	return "credit", diff
}

func baishunUserInfoData(session gamedomain.LaunchSession, config baishunAdapterConfig, snapshot yomiUserSnapshot) map[string]any {
	data := map[string]any{
		"user_id":     externalUserID(session, config.UIDMode),
		"user_name":   snapshot.Nickname,
		"user_avatar": snapshot.Avatar,
		"balance":     snapshot.Balance,
	}
	if config.UserType > 0 {
		data["user_type"] = config.UserType
	}
	if config.ReleaseCond >= 0 && config.UserType > 0 {
		data["release_cond"] = config.ReleaseCond
	}
	if config.IsOldUser != nil {
		data["is_old_user"] = *config.IsOldUser
	}
	if config.Extend != "" {
		data["extend"] = config.Extend
	}
	return data
}

func baishunChangeBalanceData(balance int64, extend string) map[string]any {
	data := map[string]any{"currency_balance": balance}
	if strings.TrimSpace(extend) != "" {
		data["extend"] = strings.TrimSpace(extend)
	}
	return data
}

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

func baishunJSON(uniqueID string, code int, message string, data any) ([]byte, string) {
	if strings.TrimSpace(message) == "" && code == baishunCodeOK {
		message = baishunDefaultSuccessMessage
	}
	if data == nil {
		data = map[string]any{}
	}
	return jsonResponse(map[string]any{
		"code":      code,
		"message":   strings.TrimSpace(message),
		"unique_id": baishunUniqueID(uniqueID),
		"data":      data,
	})
}

func baishunUniqueID(value string) string {
	value = strings.TrimSpace(value)
	if value != "" {
		return value
	}
	return strconv.FormatInt(time.Now().UnixNano(), 10)
}

func baishunCodeFromError(err error) int {
	switch {
	case err == nil:
		return baishunCodeOK
	case xerr.IsCode(err, xerr.InsufficientBalance):
		return baishunCodeInsufficientBalance
	case xerr.IsCode(err, xerr.SessionExpired), xerr.IsCode(err, xerr.Unauthorized), xerr.IsCode(err, xerr.InvalidArgument):
		return baishunCodeInvalidData
	case xerr.IsCode(err, xerr.NotFound):
		return baishunCodeReadDataFailed
	case xerr.IsCode(err, xerr.Conflict):
		return baishunCodeGameMaintenance
	default:
		return baishunCodeAPIServerError
	}
}

func baishunMessageFromError(err error) string {
	switch baishunCodeFromError(err) {
	case baishunCodeInsufficientBalance:
		return "coin not enough"
	case baishunCodeInvalidData:
		return "invalid data"
	case baishunCodeReadDataFailed:
		return "read data failed"
	case baishunCodeGameMaintenance:
		return "game maintenance"
	default:
		if err != nil && strings.TrimSpace(xerr.MessageOf(err)) != "" {
			return xerr.MessageOf(err)
		}
		return "api server error"
	}
}

func baishunUserSnapshotCodeFromError(err error) int {
	if xerr.IsCode(err, xerr.NotFound) {
		return baishunCodeInvalidData
	}
	return baishunCodeFromError(err)
}

func baishunUserSnapshotMessageFromError(err error) string {
	if xerr.IsCode(err, xerr.NotFound) {
		return "token invalid"
	}
	return baishunMessageFromError(err)
}
