package game

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	gamev1 "hyapp.local/api/proto/game/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	gamedomain "hyapp/services/game-service/internal/domain/game"
)

const defaultScene = gamedomain.SceneVoiceRoom

// Repository 隔离 game-service 持久化事实，订单和 session 不能只放内存。
type Repository interface {
	Ping(ctx context.Context) error
	ListGames(ctx context.Context, query ListGamesQuery) ([]gamedomain.AppGame, error)
	GetLaunchableGame(ctx context.Context, appCode string, gameID string) (gamedomain.LaunchableGame, error)
	CreateLaunchSession(ctx context.Context, session gamedomain.LaunchSession) error
	InsertCallbackLog(ctx context.Context, log gamedomain.CallbackLog) error
	CreateGameOrder(ctx context.Context, order gamedomain.GameOrder) (gamedomain.GameOrder, bool, error)
	MarkOrderSucceeded(ctx context.Context, appCode string, orderID string, walletTransactionID string, balanceAfter int64, nowMs int64) error
	MarkOrderFailed(ctx context.Context, appCode string, orderID string, status string, code string, message string, nowMs int64) error
	ListPlatforms(ctx context.Context, appCode string, status string) ([]gamedomain.Platform, error)
	UpsertPlatform(ctx context.Context, platform gamedomain.Platform) (gamedomain.Platform, error)
	ListCatalog(ctx context.Context, query ListCatalogQuery) ([]gamedomain.CatalogItem, string, error)
	UpsertCatalog(ctx context.Context, item gamedomain.CatalogItem) (gamedomain.CatalogItem, error)
	SetGameStatus(ctx context.Context, appCode string, gameID string, status string, nowMs int64) (gamedomain.CatalogItem, error)
}

// WalletClient 是 game-service 调 wallet-service 的最小改账依赖。
type WalletClient interface {
	ApplyGameCoinChange(ctx context.Context, req *walletv1.ApplyGameCoinChangeRequest) (*walletv1.ApplyGameCoinChangeResponse, error)
	GetBalances(ctx context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error)
}

// UserClient 只暴露平台回调需要的用户展示资料查询能力。
type UserClient interface {
	GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error)
}

type Config struct {
	LaunchSessionTTL time.Duration
}

type Service struct {
	config     Config
	repository Repository
	wallet     WalletClient
	user       UserClient
	now        func() time.Time
}

type ListGamesQuery struct {
	AppCode        string
	UserID         int64
	Scene          string
	RoomID         string
	RegionID       int64
	Language       string
	ClientPlatform string
}

type ListCatalogQuery struct {
	AppCode      string
	PlatformCode string
	Status       string
	PageSize     int32
	Cursor       string
}

type LaunchCommand struct {
	AppCode       string
	RequestID     string
	UserID        int64
	DisplayUserID string
	GameID        string
	Scene         string
	RoomID        string
}

type LaunchResult struct {
	SessionID    string
	LaunchURL    string
	ExpiresAtMS  int64
	Orientation  string
	ServerTimeMS int64
}

// New 创建 game-service 用例层。
func New(config Config, repository Repository, wallet WalletClient, user UserClient) *Service {
	if config.LaunchSessionTTL <= 0 {
		config.LaunchSessionTTL = 15 * time.Minute
	}
	return &Service{
		config:     config,
		repository: repository,
		wallet:     wallet,
		user:       user,
		now:        time.Now,
	}
}

// Repository 只给 transport/admin 薄封装复用；业务主链路仍通过 Service 方法表达。
func (s *Service) Repository() Repository {
	if s == nil {
		return nil
	}
	return s.repository
}

// ListGames 返回 App 语音房里的跨平台合并游戏列表。
func (s *Service) ListGames(ctx context.Context, query ListGamesQuery) ([]gamedomain.AppGame, int64, error) {
	if query.UserID <= 0 {
		return nil, 0, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	if s.repository == nil {
		return nil, 0, xerr.New(xerr.Unavailable, "game repository is not configured")
	}
	query.AppCode = appcode.Normalize(query.AppCode)
	query.Scene = normalizeScene(query.Scene)
	query.Language = strings.ToLower(strings.TrimSpace(query.Language))
	query.ClientPlatform = strings.ToLower(strings.TrimSpace(query.ClientPlatform))
	ctx = appcode.WithContext(ctx, query.AppCode)

	games, err := s.repository.ListGames(ctx, query)
	return games, s.now().UnixMilli(), err
}

// LaunchGame 创建短期 H5 启动会话，并把原始钱包能力限制在服务端。
func (s *Service) LaunchGame(ctx context.Context, command LaunchCommand) (LaunchResult, error) {
	if command.UserID <= 0 || strings.TrimSpace(command.GameID) == "" {
		return LaunchResult{}, xerr.New(xerr.InvalidArgument, "launch command is incomplete")
	}
	if s.repository == nil {
		return LaunchResult{}, xerr.New(xerr.Unavailable, "game repository is not configured")
	}
	command.AppCode = appcode.Normalize(command.AppCode)
	command.Scene = normalizeScene(command.Scene)
	command.DisplayUserID = strings.TrimSpace(command.DisplayUserID)
	if command.DisplayUserID == "" {
		command.DisplayUserID = strconv.FormatInt(command.UserID, 10)
	}
	ctx = appcode.WithContext(ctx, command.AppCode)

	game, err := s.repository.GetLaunchableGame(ctx, command.AppCode, strings.TrimSpace(command.GameID))
	if err != nil {
		return LaunchResult{}, err
	}
	if game.PlatformStatus != gamedomain.StatusActive || game.Status != gamedomain.StatusActive {
		return LaunchResult{}, xerr.New(xerr.Conflict, "game is not launchable")
	}

	now := s.now()
	sessionID := "game_sess_" + strconv.FormatInt(now.UnixMilli(), 10) + "_" + randomHex(6)
	token := "gt_" + randomHex(24)
	expiresAt := now.Add(s.config.LaunchSessionTTL).UnixMilli()
	session := gamedomain.LaunchSession{
		AppCode:         command.AppCode,
		SessionID:       sessionID,
		UserID:          command.UserID,
		DisplayUserID:   command.DisplayUserID,
		RoomID:          strings.TrimSpace(command.RoomID),
		Scene:           command.Scene,
		PlatformCode:    game.PlatformCode,
		GameID:          game.GameID,
		ProviderGameID:  game.ProviderGameID,
		LaunchTokenHash: stableHash(token),
		Status:          gamedomain.SessionActive,
		ExpiresAtMS:     expiresAt,
		CreatedAtMS:     now.UnixMilli(),
		UpdatedAtMS:     now.UnixMilli(),
	}
	if err := s.repository.CreateLaunchSession(ctx, session); err != nil {
		return LaunchResult{}, err
	}

	launchURL, err := buildLaunchURL(game, session, token)
	if err != nil {
		return LaunchResult{}, err
	}
	return LaunchResult{
		SessionID:    sessionID,
		LaunchURL:    launchURL,
		ExpiresAtMS:  expiresAt,
		Orientation:  game.Orientation,
		ServerTimeMS: now.UnixMilli(),
	}, nil
}

// HandleCallback 保留 provider 原始请求摘要，并对 demo adapter 支持余额查询和金币改账。
func (s *Service) HandleCallback(ctx context.Context, req *gamev1.CallbackRequest) ([]byte, string, error) {
	if req == nil || strings.TrimSpace(req.GetPlatformCode()) == "" || strings.TrimSpace(req.GetOperation()) == "" {
		return nil, "", xerr.New(xerr.InvalidArgument, "callback request is incomplete")
	}
	if s.repository == nil {
		return nil, "", xerr.New(xerr.Unavailable, "game repository is not configured")
	}
	app := appcode.Normalize(req.GetMeta().GetAppCode())
	ctx = appcode.WithContext(ctx, app)
	operation := strings.ToLower(strings.TrimSpace(req.GetOperation()))
	requestHash := stableHashBytes(req.GetRawBody())
	callbackID := "gcb_" + stableHash(fmt.Sprintf("%s|%s|%s|%d", app, req.GetPlatformCode(), requestHash, s.now().UnixNano()))

	responseBody, contentType, statusCode, err := s.handleDemoOperation(ctx, app, req, operation, requestHash)
	responseHash := stableHashBytes(responseBody)
	logStatus := "succeeded"
	if err != nil {
		logStatus = "failed"
		responseBody, contentType = jsonResponse(map[string]any{"code": string(xerr.CodeOf(err)), "message": xerr.MessageOf(err)})
		statusCode = string(xerr.CodeOf(err))
		responseHash = stableHashBytes(responseBody)
	}
	_ = s.repository.InsertCallbackLog(ctx, gamedomain.CallbackLog{
		AppCode:          app,
		CallbackID:       callbackID,
		PlatformCode:     strings.TrimSpace(req.GetPlatformCode()),
		Operation:        operation,
		RequestID:        req.GetMeta().GetRequestId(),
		SignatureValid:   false,
		RequestHash:      requestHash,
		ResponseCode:     statusCode,
		ResponseBodyHash: responseHash,
		Status:           logStatus,
		CreatedAtMS:      s.now().UnixMilli(),
	})
	return responseBody, contentType, err
}

func (s *Service) handleDemoOperation(ctx context.Context, app string, req *gamev1.CallbackRequest, operation string, requestHash string) ([]byte, string, string, error) {
	switch operation {
	case "get_user_info":
		result, err := s.getCallbackUserInfo(ctx, app, req)
		if err != nil {
			return nil, "", "", err
		}
		raw, contentType := jsonResponse(result)
		return raw, contentType, "OK", nil
	case "get_balance":
		if s.wallet == nil {
			return nil, "", "", xerr.New(xerr.Unavailable, "wallet client is not configured")
		}
		var body struct {
			UserID int64 `json:"user_id"`
		}
		if err := json.Unmarshal(req.GetRawBody(), &body); err != nil || body.UserID <= 0 {
			return nil, "", "", xerr.New(xerr.InvalidArgument, "invalid get_balance payload")
		}
		resp, err := s.wallet.GetBalances(ctx, &walletv1.GetBalancesRequest{
			RequestId:  req.GetMeta().GetRequestId(),
			AppCode:    app,
			UserId:     body.UserID,
			AssetTypes: []string{"COIN"},
		})
		if err != nil {
			return nil, "", "", err
		}
		var balance int64
		for _, item := range resp.GetBalances() {
			if item.GetAssetType() == "COIN" {
				balance = item.GetAvailableAmount()
			}
		}
		raw, contentType := jsonResponse(map[string]any{"code": 0, "balance": balance})
		return raw, contentType, "OK", nil
	case "change_coin":
		result, err := s.applyCallbackCoinChange(ctx, app, req, requestHash)
		if err != nil {
			return nil, "", "", err
		}
		raw, contentType := jsonResponse(result)
		return raw, contentType, "OK", nil
	default:
		raw, contentType := jsonResponse(map[string]any{"code": 0, "message": "ok"})
		return raw, contentType, "OK", nil
	}
}

func (s *Service) getCallbackUserInfo(ctx context.Context, app string, req *gamev1.CallbackRequest) (map[string]any, error) {
	if s.user == nil || s.wallet == nil {
		return nil, xerr.New(xerr.Unavailable, "user or wallet client is not configured")
	}
	var body struct {
		UserID int64 `json:"user_id"`
	}
	if err := json.Unmarshal(req.GetRawBody(), &body); err != nil || body.UserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "invalid get_user_info payload")
	}
	userResp, err := s.user.GetUser(ctx, &userv1.GetUserRequest{
		Meta: &userv1.RequestMeta{
			RequestId: req.GetMeta().GetRequestId(),
			Caller:    "game-service",
			SentAtMs:  s.now().UnixMilli(),
			AppCode:   app,
		},
		UserId: body.UserID,
	})
	if err != nil {
		return nil, err
	}
	balanceResp, err := s.wallet.GetBalances(ctx, &walletv1.GetBalancesRequest{
		RequestId:  req.GetMeta().GetRequestId(),
		AppCode:    app,
		UserId:     body.UserID,
		AssetTypes: []string{"COIN"},
	})
	if err != nil {
		return nil, err
	}
	var balance int64
	for _, item := range balanceResp.GetBalances() {
		if item.GetAssetType() == "COIN" {
			balance = item.GetAvailableAmount()
			break
		}
	}
	user := userResp.GetUser()
	return map[string]any{
		"code":            0,
		"user_id":         body.UserID,
		"display_user_id": user.GetDisplayUserId(),
		"nickname":        user.GetUsername(),
		"avatar":          user.GetAvatar(),
		"country":         user.GetCountry(),
		"region_id":       user.GetRegionId(),
		"balance":         balance,
	}, nil
}

func (s *Service) applyCallbackCoinChange(ctx context.Context, app string, req *gamev1.CallbackRequest, requestHash string) (map[string]any, error) {
	if s.wallet == nil {
		return nil, xerr.New(xerr.Unavailable, "wallet client is not configured")
	}
	var body struct {
		UserID          int64  `json:"user_id"`
		GameID          string `json:"game_id"`
		ProviderGameID  string `json:"provider_game_id"`
		ProviderOrderID string `json:"provider_order_id"`
		ProviderRoundID string `json:"provider_round_id"`
		OpType          string `json:"op_type"`
		CoinAmount      int64  `json:"coin_amount"`
		RoomID          string `json:"room_id"`
	}
	if err := json.Unmarshal(req.GetRawBody(), &body); err != nil {
		return nil, xerr.New(xerr.InvalidArgument, "invalid change_coin payload")
	}
	body.GameID = strings.TrimSpace(body.GameID)
	body.ProviderOrderID = strings.TrimSpace(body.ProviderOrderID)
	body.OpType = strings.TrimSpace(body.OpType)
	if body.UserID <= 0 || body.GameID == "" || body.ProviderOrderID == "" || body.OpType == "" || body.CoinAmount <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "change_coin payload is incomplete")
	}
	if body.ProviderGameID == "" {
		game, err := s.repository.GetLaunchableGame(ctx, app, body.GameID)
		if err != nil {
			return nil, err
		}
		body.ProviderGameID = game.ProviderGameID
	}
	order := gamedomain.GameOrder{
		AppCode:         app,
		OrderID:         "gord_" + stableHash(app+"|"+req.GetPlatformCode()+"|"+body.ProviderOrderID),
		PlatformCode:    strings.TrimSpace(req.GetPlatformCode()),
		ProviderOrderID: body.ProviderOrderID,
		ProviderRoundID: body.ProviderRoundID,
		GameID:          body.GameID,
		ProviderGameID:  body.ProviderGameID,
		UserID:          body.UserID,
		RoomID:          strings.TrimSpace(body.RoomID),
		OpType:          body.OpType,
		CoinAmount:      body.CoinAmount,
		Status:          gamedomain.OrderStatusWalletApplying,
		RequestHash:     requestHash,
	}
	order, exists, err := s.repository.CreateGameOrder(ctx, order)
	if err != nil {
		return nil, err
	}
	if exists && order.Status == gamedomain.OrderStatusSucceeded {
		return map[string]any{"code": 0, "order_id": order.OrderID, "wallet_transaction_id": order.WalletTransactionID, "balance_after": order.WalletBalanceAfter, "idempotent_replay": true}, nil
	}

	walletResp, err := s.wallet.ApplyGameCoinChange(ctx, &walletv1.ApplyGameCoinChangeRequest{
		RequestId:       req.GetMeta().GetRequestId(),
		AppCode:         app,
		CommandId:       "game:" + strings.TrimSpace(req.GetPlatformCode()) + ":" + body.ProviderOrderID,
		UserId:          body.UserID,
		PlatformCode:    strings.TrimSpace(req.GetPlatformCode()),
		GameId:          body.GameID,
		ProviderOrderId: body.ProviderOrderID,
		ProviderRoundId: body.ProviderRoundID,
		OpType:          body.OpType,
		CoinAmount:      body.CoinAmount,
		RoomId:          body.RoomID,
		RequestHash:     requestHash,
	})
	nowMs := s.now().UnixMilli()
	if err != nil {
		status := gamedomain.OrderStatusFailed
		if xerr.IsCode(err, xerr.IdempotencyConflict) {
			status = gamedomain.OrderStatusConflict
		}
		_ = s.repository.MarkOrderFailed(ctx, app, order.OrderID, status, string(xerr.ReasonFromGRPC(err)), err.Error(), nowMs)
		return nil, err
	}
	if err := s.repository.MarkOrderSucceeded(ctx, app, order.OrderID, walletResp.GetWalletTransactionId(), walletResp.GetBalanceAfter(), nowMs); err != nil {
		return nil, err
	}
	return map[string]any{"code": 0, "order_id": order.OrderID, "wallet_transaction_id": walletResp.GetWalletTransactionId(), "balance_after": walletResp.GetBalanceAfter(), "idempotent_replay": walletResp.GetIdempotentReplay()}, nil
}

func normalizeScene(scene string) string {
	scene = strings.ToLower(strings.TrimSpace(scene))
	if scene == "" {
		return defaultScene
	}
	return scene
}

func buildLaunchURL(game gamedomain.LaunchableGame, session gamedomain.LaunchSession, token string) (string, error) {
	base := strings.TrimSpace(game.APIBaseURL)
	if base == "" {
		base = "https://game.example.local/h5"
	}
	parsed, err := url.Parse(base)
	if err != nil {
		return "", xerr.New(xerr.InvalidArgument, "platform launch url is invalid")
	}
	query := parsed.Query()
	query.Set("session_id", session.SessionID)
	query.Set("session_token", token)
	query.Set("game_id", session.GameID)
	query.Set("provider_game_id", session.ProviderGameID)
	query.Set("platform_code", session.PlatformCode)
	query.Set("user_id", strconv.FormatInt(session.UserID, 10))
	query.Set("display_user_id", session.DisplayUserID)
	query.Set("scene", session.Scene)
	query.Set("room_id", session.RoomID)
	parsed.RawQuery = query.Encode()
	return parsed.String(), nil
}

func jsonResponse(value any) ([]byte, string) {
	body, err := json.Marshal(value)
	if err != nil {
		return []byte(`{"code":"INTERNAL_ERROR","message":"internal error"}`), "application/json"
	}
	return body, "application/json"
}

func randomHex(size int) string {
	bytes := make([]byte, size)
	if _, err := rand.Read(bytes); err != nil {
		return strconv.FormatInt(time.Now().UnixNano(), 16)
	}
	return hex.EncodeToString(bytes)
}

func stableHash(value string) string {
	sum := sha256.Sum256([]byte(value))
	return hex.EncodeToString(sum[:])
}

func stableHashBytes(value []byte) string {
	sum := sha256.Sum256(value)
	return hex.EncodeToString(sum[:])
}
