package game

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

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

const (
	// ZGame 文档的业务码会被游戏服务端直接消费；HTTP 200 只表示网关成功接收并处理回调。
	zgameCodeOK                  = 0
	zgameCodeInvalidSession      = 201
	zgameCodeInvalidSignature    = 400
	zgameCodeUnauthorized        = 401
	zgameCodeInvalidCode         = 403
	zgameCodeInsufficientBalance = 501
	zgameCodeInternalError       = 500

	zgameDefaultTokenTTL = 24 * time.Hour
)

type zgameAdapterConfig struct {
	// uid_mode 必须和启动链接、login open_id、后续签名 open_id 保持一致，避免同一 token 被不同用户标识复用。
	UIDMode         string `json:"uid_mode"`
	DefaultLang     string `json:"default_lang"`
	TokenTTLSeconds int64  `json:"token_ttl_seconds"`
	// game_urls 支持 ZGame 每款游戏独立 H5 地址；launch_url_template 用于批量上新时按 provider_game_id 生成地址。
	GameURLs          map[string]string `json:"game_urls"`
	LaunchURLTemplate string            `json:"launch_url_template"`
	// callback_base 可按环境覆盖为完整公网 HTTPS 地址，避免 H5 在第三方域名下把相对路径解析到厂商域名。
	CallbackBase      string `json:"callback_base"`
	CallbackBaseAlias string `json:"callbackBase"`
	// merchant_payload 是 ZGame consume 回调的透传字段来源；只参与厂商回传，不进入内部业务判断。
	MerchantPayload string `json:"merchant_payload"`
	// level/gender 是 ZGame 用户资料响应里的展示字段；真实等级接入前保持显式默认值，避免从其它体系误推。
	Level  int32 `json:"level"`
	Gender int32 `json:"gender"`
}

func zgameConfigFromPlatform(value any) zgameAdapterConfig {
	var raw string
	switch typed := value.(type) {
	case gamedomain.Platform:
		raw = typed.AdapterConfigJSON
	case gamedomain.LaunchableGame:
		raw = typed.AdapterConfigJSON
	}
	var config zgameAdapterConfig
	_ = json.Unmarshal([]byte(strings.TrimSpace(raw)), &config)
	config.UIDMode = strings.ToLower(strings.TrimSpace(config.UIDMode))
	if config.UIDMode == "" {
		// laluparty 游戏侧展示和签名都用短号作为 open_id；历史配置未写 uid_mode 时按文档默认走 display_user_id。
		config.UIDMode = "display_user_id"
	}
	config.DefaultLang = strings.TrimSpace(config.DefaultLang)
	config.LaunchURLTemplate = strings.TrimSpace(config.LaunchURLTemplate)
	config.CallbackBase = strings.TrimSpace(firstNonEmpty(config.CallbackBase, config.CallbackBaseAlias))
	config.MerchantPayload = strings.TrimSpace(config.MerchantPayload)
	config.GameURLs = normalizeStringMap(config.GameURLs)
	return config
}

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

func (c zgameAdapterConfig) CallbackBaseValue() string {
	if c.CallbackBase != "" {
		return c.CallbackBase
	}
	return "/api/v1/zgame/callback"
}

func zgameLaunchBaseURL(game gamedomain.LaunchableGame, config zgameAdapterConfig) 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) handleZGameOperation(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 zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", false, "")
	}
	config := zgameConfigFromPlatform(platform)
	switch zgameOperation(operation) {
	case "login":
		return s.handleZGameLogin(ctx, app, config, req)
	case "session_renew":
		return s.handleZGameSessionRenew(ctx, app, config, platform.CallbackSecretCiphertext, req)
	case "user_info_batch":
		return s.handleZGameUserInfoBatch(ctx, app, config, platform.CallbackSecretCiphertext, req)
	case "consume":
		return s.handleZGameConsume(ctx, app, config, platform.CallbackSecretCiphertext, req, requestHash)
	default:
		return zgameAdapterError(zgameCodeOK, "success", false, "")
	}
}

func zgameOperation(operation string) string {
	normalized := strings.Trim(strings.ToLower(strings.TrimSpace(operation)), "/")
	normalized = strings.TrimPrefix(normalized, "api/")
	normalized = strings.TrimPrefix(normalized, "server/")
	normalized = strings.TrimPrefix(normalized, "client/")
	return strings.Trim(normalized, "/")
}

type zgameLoginBody struct {
	OpenID      string `json:"open_id"`
	OpenIDV2    string `json:"openId"`
	JSCode      string `json:"js_code"`
	JSCodeV2    string `json:"jsCode"`
	JSCodeV3    string `json:"code"`
	CallbackURL string `json:"callback_url"`
}

func (body zgameLoginBody) openID() string {
	return firstNonEmpty(body.OpenID, body.OpenIDV2)
}

func (body zgameLoginBody) jsCode() string {
	return firstNonEmpty(body.JSCode, body.JSCodeV2, body.JSCodeV3)
}

func (s *Service) handleZGameLogin(ctx context.Context, app string, config zgameAdapterConfig, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
	var body zgameLoginBody
	if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
		return zgameAdapterError(zgameCodeInvalidCode, "Invalid or Expired Code", false, "")
	}
	openID := body.openID()
	jsCode := body.jsCode()
	session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, jsCode, "")
	if err != nil || !zgameSessionMatches(session, config, openID, "") {
		return zgameAdapterError(zgameCodeInvalidCode, "Invalid or Expired Code", true, "")
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, session.UserID)
	if err != nil {
		return zgameAdapterError(zgameCodeFromError(err), zgameMessageFromError(err), true, "")
	}
	raw, contentType := zgameJSON(zgameCodeOK, "success", zgameLoginData(session, config, openID, jsCode, snapshot, s.now()))
	return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil
}

type zgameSignedSessionBody struct {
	OpenID       string `json:"open_id"`
	OpenIDV2     string `json:"openId"`
	Session      string `json:"session"`
	Signature    string `json:"signature"`
	RawGameID    any    `json:"game_id"`
	RawGameIDV2  any    `json:"gameId"`
	RawTimestamp any    `json:"timestamp"`
}

func (body zgameSignedSessionBody) openID() string {
	return firstNonEmpty(body.OpenID, body.OpenIDV2)
}

func (body zgameSignedSessionBody) gameIDText() string {
	return zgameAnyString(firstNonEmpty(zgameAnyString(body.RawGameID), zgameAnyString(body.RawGameIDV2)))
}

func (s *Service) handleZGameSessionRenew(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
	body, signed, session, err := s.zgameSignedSession(ctx, app, config, secret, req)
	if !signed {
		return zgameAdapterError(zgameCodeInvalidSignature, "Invalid Signature", false, "")
	}
	if err != nil || !zgameSessionMatches(session, config, body.openID(), body.gameIDText()) {
		return zgameAdapterError(zgameCodeInvalidSession, "Invalid Session", true, "")
	}
	raw, contentType := zgameJSON(zgameCodeOK, "success", map[string]any{
		"session":         body.Session,
		"session_expired": zgameSessionTTLMS(session, s.now()),
	})
	return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil
}

type zgameUserInfoBatchBody struct {
	OpenIDList   []string `json:"open_id_list"`
	OpenIDListV2 []string `json:"openIdList"`
	OpenID       string   `json:"open_id"`
	OpenIDV2     string   `json:"openId"`
	Session      string   `json:"session"`
	Signature    string   `json:"signature"`
}

func (body zgameUserInfoBatchBody) openID() string {
	return firstNonEmpty(body.OpenID, body.OpenIDV2)
}

func (body zgameUserInfoBatchBody) openIDList() []string {
	if len(body.OpenIDList) > 0 {
		return body.OpenIDList
	}
	return body.OpenIDListV2
}

func (s *Service) handleZGameUserInfoBatch(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest) ([]byte, string, string, bool, string, error) {
	var body zgameUserInfoBatchBody
	if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
		return zgameAdapterError(zgameCodeInvalidSession, "Invalid Session", false, "")
	}
	if !zgameSignatureValid(body.openID(), body.Session, body.Signature, secret) {
		return zgameAdapterError(zgameCodeInvalidSignature, "Invalid Signature", false, "")
	}
	session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, body.Session, "")
	if err != nil || !zgameSessionMatches(session, config, body.openID(), "") {
		return zgameAdapterError(zgameCodeInvalidSession, "Invalid Session", true, "")
	}
	userInfos := make(map[string]any, len(body.openIDList()))
	for _, openID := range body.openIDList() {
		openID = strings.TrimSpace(openID)
		if openID == "" {
			continue
		}
		data, ok := s.zgameUserInfoForOpenID(ctx, app, config, req, session, openID)
		if ok {
			userInfos[openID] = data
		}
	}
	raw, contentType := zgameJSON(zgameCodeOK, "success", map[string]any{"user_infos": userInfos})
	return raw, contentType, strconv.Itoa(zgameCodeOK), true, "", nil
}

type zgameConsumeBody struct {
	OpenID            string      `json:"open_id"`
	OpenIDV2          string      `json:"openId"`
	Session           string      `json:"session"`
	Amount            json.Number `json:"amount"`
	OrderID           string      `json:"order_id"`
	OrderIDV2         string      `json:"orderId"`
	GameID            string      `json:"game_id"`
	GameIDV2          string      `json:"gameId"`
	Timestamp         int64       `json:"timestamp"`
	RoundID           string      `json:"round_id"`
	RoundIDV2         string      `json:"roundId"`
	Type              int32       `json:"type"`
	MerchantPayload   string      `json:"merchant_payload"`
	MerchantPayloadV2 string      `json:"merchantPayload"`
	Signature         string      `json:"signature"`
}

func (body zgameConsumeBody) openID() string {
	return firstNonEmpty(body.OpenID, body.OpenIDV2)
}

func (body zgameConsumeBody) orderID() string {
	return firstNonEmpty(body.OrderID, body.OrderIDV2)
}

func (body zgameConsumeBody) gameID() string {
	return firstNonEmpty(body.GameID, body.GameIDV2)
}

func (body zgameConsumeBody) roundID() string {
	return firstNonEmpty(body.RoundID, body.RoundIDV2)
}

func (body zgameConsumeBody) merchantPayload() string {
	return firstNonEmpty(body.MerchantPayload, body.MerchantPayloadV2)
}

func (s *Service) handleZGameConsume(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest, requestHash string) ([]byte, string, string, bool, string, error) {
	var body zgameConsumeBody
	if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
		return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", false, "")
	}
	if !zgameSignatureValid(body.openID(), body.Session, body.Signature, secret) {
		return zgameAdapterError(zgameCodeInvalidSignature, "Invalid Signature", false, body.orderID())
	}
	session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, body.Session, body.gameID())
	if err != nil || !zgameSessionMatches(session, config, body.openID(), body.gameID()) {
		return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", true, body.orderID())
	}
	amount, amountOK := zgameAmount(body.Amount)
	opType, coinAmount := zgameOpType(body.Type, amount)
	if body.orderID() == "" || body.gameID() == "" || !amountOK || coinAmount < 0 || opType == "" {
		return zgameAdapterError(zgameCodeUnauthorized, "Unauthorized", true, body.orderID())
	}
	if session.GameID == "" {
		session.GameID = body.gameID()
	}
	if session.ProviderGameID == "" {
		session.ProviderGameID = body.gameID()
	}
	providerOrderID := zgameProviderOrderID(body.orderID(), opType)
	result, err := s.applyYomiCoinChange(ctx, app, req, session, providerOrderID, body.roundID(), opType, coinAmount, "", requestHash)
	if err != nil {
		code := zgameCodeFromError(err)
		balance := s.bestEffortCoinBalance(ctx, app, req.GetMeta().GetRequestId(), session.UserID)
		raw, contentType := zgameBalanceJSON(code, zgameMessageFromError(err), balance)
		return raw, contentType, strconv.Itoa(code), true, body.orderID(), nil
	}
	raw, contentType := zgameBalanceJSON(zgameCodeOK, "success", result.BalanceAfter)
	return raw, contentType, strconv.Itoa(zgameCodeOK), true, body.orderID(), nil
}

func (s *Service) zgameSignedSession(ctx context.Context, app string, config zgameAdapterConfig, secret string, req *gamev1.CallbackRequest) (zgameSignedSessionBody, bool, gamedomain.LaunchSession, error) {
	var body zgameSignedSessionBody
	if err := decodeYomiJSON(req.GetRawBody(), &body); err != nil {
		return zgameSignedSessionBody{}, false, gamedomain.LaunchSession{}, xerr.New(xerr.InvalidArgument, "invalid payload")
	}
	if !zgameSignatureValid(body.openID(), body.Session, body.Signature, secret) {
		return body, false, gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "signature invalid")
	}
	session, err := s.validZGameSession(ctx, app, gamedomain.PlatformCodeZGame, body.Session, body.gameIDText())
	if err != nil {
		return body, true, gamedomain.LaunchSession{}, err
	}
	if !zgameSessionMatches(session, config, body.openID(), body.gameIDText()) {
		return body, true, gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session mismatch")
	}
	return body, true, session, nil
}

func (s *Service) validZGameSession(ctx context.Context, app string, platformCode string, token string, providerGameID string) (gamedomain.LaunchSession, error) {
	token = strings.TrimSpace(token)
	if token == "" {
		return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session invalid")
	}
	// 新版启动链路使用服务端短期 js_code；保留 JWT 兼容旧测试链接，避免已打开的 H5 立即失效。
	if strings.Count(token, ".") == 2 {
		if session, err := s.appSessionFromAccessToken(app, token); err == nil {
			return session, nil
		}
	}
	if session, err := s.validScopedYomiSession(ctx, app, platformCode, token, providerGameID); err == nil {
		return session, nil
	}
	if strings.Count(token, ".") == 2 {
		return s.appSessionFromAccessToken(app, token)
	}
	return gamedomain.LaunchSession{}, xerr.New(xerr.Unauthorized, "session invalid")
}

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

func zgameSignatureValid(openID string, session string, signature string, secret string) bool {
	openID = strings.TrimSpace(openID)
	session = strings.TrimSpace(session)
	secret = strings.TrimSpace(secret)
	signature = strings.TrimSpace(signature)
	if openID == "" || session == "" || secret == "" || signature == "" {
		return false
	}
	sum := sha256.Sum256([]byte(openID + session + secret))
	expected := hex.EncodeToString(sum[:])
	return subtle.ConstantTimeCompare([]byte(strings.ToLower(signature)), []byte(expected)) == 1
}

func zgameLoginData(session gamedomain.LaunchSession, config zgameAdapterConfig, requestedOpenID string, token string, snapshot yomiUserSnapshot, now time.Time) map[string]any {
	openID := strings.TrimSpace(requestedOpenID)
	if openID == "" {
		openID = externalUserID(session, config.UIDMode)
	}
	return map[string]any{
		"session":         token,
		"session_expired": zgameSessionTTLMS(session, now),
		"openId":          openID,
		"userName":        snapshot.Nickname,
		"nickname":        snapshot.Nickname,
		"avatar":          snapshot.Avatar,
		"balance":         snapshot.Balance,
		"level":           config.Level,
		"country":         "",
		"gender":          config.Gender,
		"sex":             config.Gender,
	}
}

func (s *Service) zgameUserInfoForOpenID(ctx context.Context, app string, config zgameAdapterConfig, req *gamev1.CallbackRequest, session gamedomain.LaunchSession, openID string) (map[string]any, bool) {
	userID := int64(0)
	if openID == externalUserID(session, config.UIDMode) || openID == strings.TrimSpace(session.DisplayUserID) {
		userID = session.UserID
	} else if !strings.EqualFold(config.UIDMode, "display_user_id") {
		// 批量查询没有 display_id -> user_id 解析依赖；默认 user_id 模式可直接按 open_id 查用户资料。
		parsed, err := strconv.ParseInt(openID, 10, 64)
		if err == nil && parsed > 0 {
			userID = parsed
		}
	}
	if userID <= 0 {
		return nil, false
	}
	snapshot, err := s.yomiUserSnapshot(ctx, app, req, userID)
	if err != nil {
		return nil, false
	}
	return zgameUserInfoData(config, snapshot), true
}

func zgameUserInfoData(config zgameAdapterConfig, snapshot yomiUserSnapshot) map[string]any {
	return map[string]any{
		"userName": snapshot.Nickname,
		"nickname": snapshot.Nickname,
		"gender":   config.Gender,
		"level":    config.Level,
		"avatar":   snapshot.Avatar,
		"balance":  snapshot.Balance,
	}
}

func zgameSessionTTLMS(session gamedomain.LaunchSession, now time.Time) int64 {
	if session.ExpiresAtMS <= 0 {
		return int64(zgameDefaultTokenTTL / time.Millisecond)
	}
	ttl := session.ExpiresAtMS - now.UnixMilli()
	if ttl < 0 {
		return 0
	}
	return ttl
}

func zgameAmount(value json.Number) (int64, bool) {
	raw := strings.TrimSpace(value.String())
	if raw == "" {
		return 0, false
	}
	amount, err := strconv.ParseInt(raw, 10, 64)
	if err != nil {
		return 0, false
	}
	return amount, true
}

func zgameOpType(op int32, amount int64) (string, int64) {
	if amount == -1<<63 {
		return "", -1
	}
	coinAmount := amount
	if coinAmount < 0 {
		coinAmount = -coinAmount
	}
	if amount < 0 || op == 1 {
		return "credit", coinAmount
	}
	if op == 0 {
		return "debit", coinAmount
	}
	return "", coinAmount
}

func zgameProviderOrderID(orderID string, opType string) string {
	orderID = strings.TrimSpace(orderID)
	opType = strings.TrimSpace(opType)
	if orderID == "" || opType == "" {
		return orderID
	}
	return orderID + ":" + opType
}

func zgameAnyString(value any) string {
	switch typed := value.(type) {
	case nil:
		return ""
	case string:
		return strings.TrimSpace(typed)
	case json.Number:
		return strings.TrimSpace(typed.String())
	case float64:
		if typed == math.Trunc(typed) {
			return strconv.FormatInt(int64(typed), 10)
		}
		return strconv.FormatFloat(typed, 'f', -1, 64)
	case float32:
		return strconv.FormatFloat(float64(typed), 'f', -1, 32)
	case int:
		return strconv.Itoa(typed)
	case int64:
		return strconv.FormatInt(typed, 10)
	case int32:
		return strconv.FormatInt(int64(typed), 10)
	case uint64:
		return strconv.FormatUint(typed, 10)
	default:
		return strings.TrimSpace(fmt.Sprint(value))
	}
}

func zgameAdapterError(code int, message string, signatureValid bool, providerRequestID string) ([]byte, string, string, bool, string, error) {
	raw, contentType := zgameJSON(code, message, nil)
	return raw, contentType, strconv.Itoa(code), signatureValid, strings.TrimSpace(providerRequestID), nil
}

func zgameJSON(code int, message string, data any) ([]byte, string) {
	if strings.TrimSpace(message) == "" && code == zgameCodeOK {
		message = "success"
	}
	return jsonResponse(map[string]any{
		"code":   code,
		"errmsg": strings.TrimSpace(message),
		"data":   data,
	})
}

func zgameBalanceJSON(code int, message string, balance int64) ([]byte, string) {
	if strings.TrimSpace(message) == "" && code == zgameCodeOK {
		message = "success"
	}
	return jsonResponse(map[string]any{
		"code":    code,
		"errmsg":  strings.TrimSpace(message),
		"balance": balance,
		"data": map[string]any{
			"balance": balance,
		},
	})
}

func zgameCodeFromError(err error) int {
	switch {
	case err == nil:
		return zgameCodeOK
	case xerr.IsCode(err, xerr.InsufficientBalance):
		return zgameCodeInsufficientBalance
	case xerr.IsCode(err, xerr.SessionExpired), xerr.IsCode(err, xerr.Unauthorized):
		return zgameCodeUnauthorized
	default:
		return zgameCodeInternalError
	}
}

func zgameMessageFromError(err error) string {
	switch zgameCodeFromError(err) {
	case zgameCodeInsufficientBalance:
		return "Insufficient Funds"
	case zgameCodeUnauthorized:
		return "Unauthorized"
	case zgameCodeOK:
		return "success"
	default:
		return "Internal Error"
	}
}
