package game

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

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

const (
	// VIVAGAMES 文档定义 code=0 成功；游戏侧按 JSON code 判断业务结果，HTTP 200 只表示回调已被接收。
	vivaGamesCodeOK                  = 0
	vivaGamesCodeAppNotFound         = 1001
	vivaGamesCodeInvalidArgument     = 1002
	vivaGamesCodeSignatureInvalid    = 1003
	vivaGamesCodeCodeInvalid         = 1004
	vivaGamesCodeTokenInvalid        = 1005
	vivaGamesCodeInternalError       = 1006
	vivaGamesCodeGameMaintenance     = 1007
	vivaGamesCodeInsufficientBalance = 1008
	vivaGamesCodeUserDisabled        = 1009
	vivaGamesCodeRequestExpired      = 1010

	vivaGamesDefaultTokenTTL      = 24 * time.Hour
	vivaGamesSignatureValidWindow = 5 * time.Minute
)

type vivaGamesAdapterConfig struct {
	// app_id 来自 VIVAGAMES 后台；callbackSecret 存放 PDF 签名算法使用的 AppKey。
	AppID       int64  `json:"app_id"`
	AppIDAlias  int64  `json:"appId"`
	AppSecret   string `json:"app_secret"`
	UIDMode     string `json:"uid_mode"`
	DefaultLang string `json:"default_lang"`
	// token_ttl_seconds 控制启动 session 至少保留多久，避免 H5 游戏中途回调时 token 已过期。
	TokenTTLSeconds int64 `json:"token_ttl_seconds"`

	// 以下字段直接对应 NativeBridge.getConfig，后台热改后下一次启动生效。
	Metadata     string `json:"metadata"`
	CoinType     int64  `json:"coin_type"`
	AmountRate   int64  `json:"amount_rate"`
	BGMSwitch    *int64 `json:"bgm_switch"`
	GSP          int64  `json:"gsp"`
	CurrencyIcon string `json:"currency_icon"`

	// VIVAGAMES 文档没有游戏列表 API，game_urls 用后台配置保存厂商提供的逐游戏 H5 URL。
	GameURLs          map[string]string `json:"game_urls"`
	LaunchURLTemplate string            `json:"launch_url_template"`
}

func vivaGamesConfigFromPlatform(value any) vivaGamesAdapterConfig {
	var raw string
	switch typed := value.(type) {
	case gamedomain.Platform:
		raw = typed.AdapterConfigJSON
	case gamedomain.LaunchableGame:
		raw = typed.AdapterConfigJSON
	}
	var config vivaGamesAdapterConfig
	_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
	if config.AppID == 0 {
		config.AppID = config.AppIDAlias
	}
	config.AppSecret = strings.TrimSpace(config.AppSecret)
	config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
	config.DefaultLang = strings.TrimSpace(config.DefaultLang)
	config.Metadata = strings.TrimSpace(config.Metadata)
	config.CurrencyIcon = strings.TrimSpace(config.CurrencyIcon)
	config.LaunchURLTemplate = strings.TrimSpace(config.LaunchURLTemplate)
	config.GameURLs = normalizeStringMap(config.GameURLs)
	return config
}

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

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

func (c vivaGamesAdapterConfig) AmountRateValue() int64 {
	if c.AmountRate > 0 {
		return c.AmountRate
	}
	return 1
}

func (c vivaGamesAdapterConfig) BGMSwitchValue() int64 {
	if c.BGMSwitch != nil {
		return *c.BGMSwitch
	}
	return 1
}

func (c vivaGamesAdapterConfig) GSPValue() int64 {
	if c.GSP > 0 {
		return c.GSP
	}
	return 101
}

func (c vivaGamesAdapterConfig) MetadataValue(session gamedomain.LaunchSession) string {
	if c.Metadata != "" {
		return c.Metadata
	}
	if strings.TrimSpace(session.RoomID) == "" {
		return ""
	}
	raw, err := json.Marshal(map[string]string{"room_id": strings.TrimSpace(session.RoomID)})
	if err != nil {
		return ""
	}
	return string(raw)
}

func vivaGamesLaunchBaseURL(game gamedomain.LaunchableGame, config vivaGamesAdapterConfig) 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 vivaGamesGameConfigQuery(currencyIcon string) string {
	raw, err := json.Marshal(map[string]string{"currencyIcon": strings.TrimSpace(currencyIcon)})
	if err != nil {
		return ""
	}
	return string(raw)
}

func (s *Service) handleVivaGamesOperation(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 := vivaGamesJSON(vivaGamesCodeSignatureInvalid, "ip restricted", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeSignatureInvalid), false, "", nil
	}
	config := vivaGamesConfigFromPlatform(platform)
	body, signed, decoded, expired, providerRequestID := s.decodeVivaGamesBody(req, platform.CallbackSecretCiphertext)
	if !decoded {
		raw, contentType := vivaGamesJSON(vivaGamesCodeInvalidArgument, "invalid argument", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeInvalidArgument), false, providerRequestID, nil
	}
	if expired {
		raw, contentType := vivaGamesJSON(vivaGamesCodeRequestExpired, "request expired", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeRequestExpired), false, providerRequestID, nil
	}
	if !signed {
		raw, contentType := vivaGamesJSON(vivaGamesCodeSignatureInvalid, "sign error", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeSignatureInvalid), false, providerRequestID, nil
	}
	appID := body.AppIDInt64()
	if appID <= 0 {
		raw, contentType := vivaGamesJSON(vivaGamesCodeInvalidArgument, "invalid app_id", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeInvalidArgument), signed, providerRequestID, nil
	}
	if config.AppID > 0 && appID != config.AppID {
		raw, contentType := vivaGamesJSON(vivaGamesCodeAppNotFound, "appId not found", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeAppNotFound), signed, providerRequestID, nil
	}
	switch operation {
	case "get-token", "get_token", "gettoken", "api/get-token", "api/get_token", "v1/api/get-token":
		return s.handleVivaGamesGetToken(ctx, app, config, req, body)
	case "get-user-info", "get_user_info", "userinfo", "user_info", "api/get-user-info", "api/get_user_info", "v1/api/get-user-info":
		return s.handleVivaGamesUserInfo(ctx, app, config, req, body)
	case "update-token", "update_token", "updatetoken", "api/update-token", "api/update_token", "v1/api/update-token":
		return s.handleVivaGamesUpdateToken(ctx, app, config, body)
	case "change-balance", "change_balance", "changebalance", "api/change-balance", "api/change_balance", "v1/api/change-balance":
		return s.handleVivaGamesChangeBalance(ctx, app, config, req, body, requestHash)
	default:
		raw, contentType := vivaGamesJSON(vivaGamesCodeOK, "", map[string]any{})
		return raw, contentType, strconv.Itoa(vivaGamesCodeOK), signed, providerRequestID, nil
	}
}

type vivaGamesBody struct {
	AppID        json.Number `json:"app_id"`
	UserID       string      `json:"user_id"`
	Code         string      `json:"code"`
	Token        string      `json:"token"`
	ClientIP     string      `json:"client_ip"`
	GameID       json.Number `json:"game_id"`
	OrderID      string      `json:"order_id"`
	GameRoundID  string      `json:"game_round_id"`
	TotalBet     int64       `json:"total_bet"`
	ChangeAmount int64       `json:"change_amount"`
	ChangeReason string      `json:"change_reason"`
	ChangeTimeAt int64       `json:"change_time_at"`
	Metadata     string      `json:"metadata"`
	ExtendType   string      `json:"extend_type"`
	Extend       string      `json:"extend"`
	IgnoreExpire bool        `json:"ignore_expire"`
	Timestamp    int64       `json:"timestamp"`
	Signature    string      `json:"sign"`
}

func (s *Service) decodeVivaGamesBody(req *gamev1.CallbackRequest, secret string) (vivaGamesBody, bool, bool, bool, string) {
	var body vivaGamesBody
	decoder := json.NewDecoder(strings.NewReader(string(req.GetRawBody())))
	decoder.UseNumber()
	if err := decoder.Decode(&body); err != nil {
		return vivaGamesBody{}, false, false, false, ""
	}
	body.normalize()
	providerRequestID := strings.TrimSpace(body.OrderID)
	params, err := vivaGamesParamsForSign(req.GetRawBody())
	if err != nil {
		return vivaGamesBody{}, false, false, false, providerRequestID
	}
	signed, expired := s.vivaGamesSignatureValid(params, body.Signature, body.Timestamp, secret)
	return body, signed, true, expired, providerRequestID
}

func vivaGamesParamsForSign(raw []byte) (map[string]any, error) {
	var params map[string]any
	decoder := json.NewDecoder(strings.NewReader(string(raw)))
	decoder.UseNumber()
	if err := decoder.Decode(&params); err != nil {
		return nil, err
	}
	delete(params, "sign")
	return params, nil
}

func (s *Service) vivaGamesSignatureValid(params map[string]any, signature string, timestamp int64, secret string) (bool, bool) {
	secret = strings.TrimSpace(secret)
	if secret == "" || strings.TrimSpace(signature) == "" || timestamp <= 0 {
		return false, false
	}
	requestAt := time.Unix(timestamp, 0)
	now := s.now()
	if requestAt.After(now.Add(vivaGamesSignatureValidWindow)) || now.Sub(requestAt) > vivaGamesSignatureValidWindow {
		return false, true
	}
	expected := vivaGamesSign(params, secret)
	return strings.EqualFold(strings.TrimSpace(signature), expected), false
}

func vivaGamesSign(params map[string]any, secret string) string {
	keys := make([]string, 0, len(params))
	for key, value := range params {
		if value == nil || strings.TrimSpace(key) == "" || strings.EqualFold(key, "sign") {
			continue
		}
		keys = append(keys, key)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys)+1)
	for _, key := range keys {
		parts = append(parts, fmt.Sprintf("%s=%s", key, vivaGamesSignValue(params[key])))
	}
	parts = append(parts, "key="+strings.TrimSpace(secret))
	sum := sha256.Sum256([]byte(strings.Join(parts, "&")))
	return hex.EncodeToString(sum[:])
}

func vivaGamesSignValue(value any) string {
	switch typed := value.(type) {
	case json.Number:
		return typed.String()
	case string:
		return typed
	case bool:
		if typed {
			return "true"
		}
		return "false"
	default:
		return strings.TrimSpace(fmt.Sprint(typed))
	}
}

func (body *vivaGamesBody) normalize() {
	body.UserID = strings.TrimSpace(body.UserID)
	body.Code = strings.TrimSpace(body.Code)
	body.Token = strings.TrimSpace(body.Token)
	body.ClientIP = strings.TrimSpace(body.ClientIP)
	body.OrderID = strings.TrimSpace(body.OrderID)
	body.GameRoundID = strings.TrimSpace(body.GameRoundID)
	body.ChangeReason = strings.ToLower(strings.TrimSpace(body.ChangeReason))
	body.Metadata = strings.TrimSpace(body.Metadata)
	body.ExtendType = strings.TrimSpace(body.ExtendType)
	body.Extend = strings.TrimSpace(body.Extend)
	body.Signature = strings.TrimSpace(body.Signature)
}

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

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

func (s *Service) handleVivaGamesGetToken(ctx context.Context, app string, config vivaGamesAdapterConfig, req *gamev1.CallbackRequest, body vivaGamesBody) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.Code == "" {
		return vivaGamesAdapterError(vivaGamesCodeInvalidArgument, "invalid argument", true, "")
	}
	session, err := s.validVivaGamesSession(ctx, app, body.Code, false)
	if err != nil || !vivaGamesSessionMatches(session, config, body.UserID, 0) {
		return vivaGamesAdapterError(vivaGamesCodeCodeInvalid, "code error", true, "")
	}
	raw, contentType := vivaGamesJSON(vivaGamesCodeOK, "", map[string]any{
		"token":     body.Code,
		"expire_at": session.ExpiresAtMS,
	})
	return raw, contentType, strconv.Itoa(vivaGamesCodeOK), true, "", nil
}

func (s *Service) handleVivaGamesUserInfo(ctx context.Context, app string, config vivaGamesAdapterConfig, req *gamev1.CallbackRequest, body vivaGamesBody) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.Token == "" {
		return vivaGamesAdapterError(vivaGamesCodeInvalidArgument, "invalid argument", true, "")
	}
	session, err := s.validVivaGamesSession(ctx, app, body.Token, false)
	if err != nil || !vivaGamesSessionMatches(session, config, body.UserID, body.GameIDInt64()) {
		return vivaGamesAdapterError(vivaGamesCodeTokenInvalid, "token error", true, "")
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
	if err != nil {
		return vivaGamesAdapterError(vivaGamesCodeFromError(err), vivaGamesMessageFromError(err), true, "")
	}
	raw, contentType := vivaGamesJSON(vivaGamesCodeOK, "", map[string]any{
		"user_id":    externalUserID(session, config.UIDMode),
		"nickname":   snapshot.Nickname,
		"avatar_url": snapshot.Avatar,
		"balance":    snapshot.Balance,
		"status":     0,
	})
	return raw, contentType, strconv.Itoa(vivaGamesCodeOK), true, "", nil
}

func (s *Service) handleVivaGamesUpdateToken(ctx context.Context, app string, config vivaGamesAdapterConfig, body vivaGamesBody) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.Token == "" {
		return vivaGamesAdapterError(vivaGamesCodeInvalidArgument, "invalid argument", true, "")
	}
	session, err := s.validVivaGamesSession(ctx, app, body.Token, false)
	if err != nil || !vivaGamesSessionMatches(session, config, body.UserID, 0) {
		return vivaGamesAdapterError(vivaGamesCodeTokenInvalid, "token error", true, "")
	}
	raw, contentType := vivaGamesJSON(vivaGamesCodeOK, "", map[string]any{
		"token":     body.Token,
		"expire_at": session.ExpiresAtMS,
	})
	return raw, contentType, strconv.Itoa(vivaGamesCodeOK), true, "", nil
}

func (s *Service) handleVivaGamesChangeBalance(ctx context.Context, app string, config vivaGamesAdapterConfig, req *gamev1.CallbackRequest, body vivaGamesBody, requestHash string) ([]byte, string, string, bool, string, error) {
	if body.UserID == "" || body.Token == "" || body.OrderID == "" || body.GameIDInt64() <= 0 || body.ChangeReason == "" {
		return vivaGamesAdapterError(vivaGamesCodeInvalidArgument, "invalid argument", true, body.OrderID)
	}
	session, err := s.validVivaGamesSession(ctx, app, body.Token, body.IgnoreExpire)
	if err != nil || !vivaGamesSessionMatches(session, config, body.UserID, body.GameIDInt64()) {
		return vivaGamesAdapterError(vivaGamesCodeTokenInvalid, "token error", true, body.OrderID)
	}
	if session.ProviderGameID == "" {
		session.ProviderGameID = strconv.FormatInt(body.GameIDInt64(), 10)
	}
	if session.GameID == "" {
		session.GameID = session.ProviderGameID
	}
	opType, amount := vivaGamesOpType(body.ChangeAmount)
	result, err := s.applyYomiCoinChange(ctx, app, req, session, body.OrderID, body.GameRoundID, opType, amount, "", requestHash)
	if err != nil {
		code := vivaGamesCodeFromError(err)
		balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID)
		raw, contentType := vivaGamesJSON(code, vivaGamesMessageFromError(err), map[string]any{"balance": balance})
		return raw, contentType, strconv.Itoa(code), true, body.OrderID, nil
	}
	raw, contentType := vivaGamesJSON(vivaGamesCodeOK, "", map[string]any{"balance": result.BalanceAfter})
	return raw, contentType, strconv.Itoa(vivaGamesCodeOK), true, body.OrderID, nil
}

func (s *Service) validVivaGamesSession(ctx context.Context, app string, token string, ignoreExpire bool) (gamedomain.LaunchSession, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
	}
	// VIVAGAMES token 直接复用 App access token；先解析 JWT 可以避免同一登录 token 多游戏启动时命中过期旧 session。
	if strings.Count(token, ".") == 2 && !ignoreExpire {
		if session, err := s.appSessionFromAccessToken(app, token); err == nil {
			return session, nil
		}
	}
	if session, err := s.repository.GetLaunchSessionByToken(ctx, app, token); err == nil {
		if session.Status != gamedomain.SessionActive {
			return gamedomain.LaunchSession{}, xerr.New(xerr.SessionExpired, "token expired")
		}
		if !ignoreExpire && session.ExpiresAtMS <= s.now().UnixMilli() {
			return gamedomain.LaunchSession{}, xerr.New(xerr.SessionExpired, "token expired")
		}
		return session, nil
	}
	if strings.Count(token, ".") == 2 {
		return s.appSessionFromAccessToken(app, token)
	}
	return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "token invalid")
}

func vivaGamesSessionMatches(session gamedomain.LaunchSession, config vivaGamesAdapterConfig, 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 vivaGamesOpType(changeAmount int64) (string, int64) {
	if changeAmount < 0 {
		return "debit", -changeAmount
	}
	return "credit", changeAmount
}

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

func vivaGamesJSON(code int, message string, data any) ([]byte, string) {
	if data == nil {
		data = map[string]any{}
	}
	return jsonResponse(map[string]any{
		"code": code,
		"msg":  strings.TrimSpace(message),
		"data": data,
	})
}

func vivaGamesCodeFromError(err error) int {
	switch {
	case err == nil:
		return vivaGamesCodeOK
	case xerr.IsCode(err, xerr.InsufficientBalance):
		return vivaGamesCodeInsufficientBalance
	case xerr.IsCode(err, xerr.SessionExpired), xerr.IsCode(err, xerr.Unauthorized):
		return vivaGamesCodeTokenInvalid
	case xerr.IsCode(err, xerr.InvalidArgument):
		return vivaGamesCodeInvalidArgument
	case xerr.IsCode(err, xerr.Conflict):
		return vivaGamesCodeGameMaintenance
	case xerr.IsCode(err, xerr.NotFound):
		return vivaGamesCodeTokenInvalid
	default:
		return vivaGamesCodeInternalError
	}
}

func vivaGamesMessageFromError(err error) string {
	switch vivaGamesCodeFromError(err) {
	case vivaGamesCodeInsufficientBalance:
		return "balance not enough"
	case vivaGamesCodeTokenInvalid:
		return "token error"
	case vivaGamesCodeInvalidArgument:
		return "invalid argument"
	case vivaGamesCodeGameMaintenance:
		return "game maintenance"
	default:
		if err != nil && strings.TrimSpace(xerr.MessageOf(err)) != "" {
			return xerr.MessageOf(err)
		}
		return "internal error"
	}
}
