package grpc

import (
	"context"
	"strings"
	"time"

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

// Server 把 game-service 用例层适配为 gRPC 契约。
type Server struct {
	gamev1.UnimplementedGameAppServiceServer
	gamev1.UnimplementedGameCallbackServiceServer
	gamev1.UnimplementedGameAdminServiceServer
	gamev1.UnimplementedGameCronServiceServer

	svc *gameservice.Service
}

func NewServer(svc *gameservice.Service) *Server {
	return &Server{svc: svc}
}

func (s *Server) ListGames(ctx context.Context, req *gamev1.ListGamesRequest) (*gamev1.ListGamesResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	items, serverTimeMS, err := s.svc.ListGames(ctx, gameservice.ListGamesQuery{
		AppCode:        req.GetMeta().GetAppCode(),
		UserID:         req.GetUserId(),
		Scene:          req.GetScene(),
		RoomID:         req.GetRoomId(),
		RegionID:       req.GetRegionId(),
		Language:       req.GetLanguage(),
		ClientPlatform: req.GetClientPlatform(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &gamev1.ListGamesResponse{ServerTimeMs: serverTimeMS, Games: make([]*gamev1.AppGame, 0, len(items))}
	for _, item := range items {
		resp.Games = append(resp.Games, appGameToProto(item))
	}
	return resp, nil
}

func (s *Server) ListRecentGames(ctx context.Context, req *gamev1.ListRecentGamesRequest) (*gamev1.ListGamesResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	items, serverTimeMS, err := s.svc.ListRecentGames(ctx, gameservice.ListRecentGamesQuery{
		AppCode:        req.GetMeta().GetAppCode(),
		UserID:         req.GetUserId(),
		Scene:          req.GetScene(),
		RoomID:         req.GetRoomId(),
		RegionID:       req.GetRegionId(),
		Language:       req.GetLanguage(),
		ClientPlatform: req.GetClientPlatform(),
		PageSize:       req.GetPageSize(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &gamev1.ListGamesResponse{ServerTimeMs: serverTimeMS, Games: make([]*gamev1.AppGame, 0, len(items))}
	for _, item := range items {
		resp.Games = append(resp.Games, appGameToProto(item))
	}
	return resp, nil
}

func (s *Server) GetBridgeScript(ctx context.Context, req *gamev1.GetBridgeScriptRequest) (*gamev1.GetBridgeScriptResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	result, err := s.svc.GetBridgeScript(ctx, req.GetMeta().GetAppCode())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.GetBridgeScriptResponse{
		Config: &gamev1.GameBridgeScriptConfig{
			BridgeScriptUrl:     result.URL,
			BridgeScriptVersion: result.Version,
			BridgeScriptSha256:  result.SHA256,
		},
		ServerTimeMs: result.ServerTimeMS,
	}, nil
}

func (s *Server) LaunchGame(ctx context.Context, req *gamev1.LaunchGameRequest) (*gamev1.LaunchGameResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	result, err := s.svc.LaunchGame(ctx, gameservice.LaunchCommand{
		AppCode:       req.GetMeta().GetAppCode(),
		RequestID:     req.GetMeta().GetRequestId(),
		UserID:        req.GetUserId(),
		DisplayUserID: req.GetDisplayUserId(),
		GameID:        req.GetGameId(),
		Scene:         req.GetScene(),
		RoomID:        req.GetRoomId(),
		AccessToken:   req.GetAccessToken(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.LaunchGameResponse{
		SessionId:    result.SessionID,
		LaunchUrl:    result.LaunchURL,
		ExpiresAtMs:  result.ExpiresAtMS,
		Orientation:  result.Orientation,
		SafeHeight:   result.SafeHeight,
		ServerTimeMs: result.ServerTimeMS,
	}, nil
}

func (s *Server) HandleCallback(ctx context.Context, req *gamev1.CallbackRequest) (*gamev1.CallbackResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	raw, contentType, err := s.svc.HandleCallback(ctx, req)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.CallbackResponse{RawBody: raw, ContentType: contentType}, nil
}

// ProcessLevelEventOutboxBatch 只由 cron-service 调用，批量接管并投递游戏消耗等级事件。
func (s *Server) ProcessLevelEventOutboxBatch(ctx context.Context, req *gamev1.CronBatchRequest) (*gamev1.CronBatchResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	claimed, processed, success, failure, hasMore, err := s.svc.ProcessLevelEventOutboxBatch(ctx, req.GetRunId(), req.GetWorkerId(), int(req.GetBatchSize()), durationFromMillis(req.GetLockTtlMs()))
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.CronBatchResponse{
		ClaimedCount:   claimed,
		ProcessedCount: processed,
		SuccessCount:   success,
		FailureCount:   failure,
		HasMore:        hasMore,
	}, nil
}

func (s *Server) ListPlatforms(ctx context.Context, req *gamev1.ListPlatformsRequest) (*gamev1.ListPlatformsResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	items, err := s.svcRepository().ListPlatforms(ctx, req.GetMeta().GetAppCode(), req.GetStatus())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &gamev1.ListPlatformsResponse{ServerTimeMs: time.Now().UnixMilli(), Platforms: make([]*gamev1.GamePlatform, 0, len(items))}
	for _, item := range items {
		resp.Platforms = append(resp.Platforms, platformToProto(item))
	}
	return resp, nil
}

func (s *Server) UpsertPlatform(ctx context.Context, req *gamev1.UpsertPlatformRequest) (*gamev1.PlatformResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	platform := platformFromProto(req.GetPlatform())
	platform.AppCode = req.GetMeta().GetAppCode()
	normalized, err := normalizePlatform(platform)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	saved, err := s.svcRepository().UpsertPlatform(ctx, normalized)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.PlatformResponse{Platform: platformToProto(saved), ServerTimeMs: time.Now().UnixMilli()}, nil
}

func (s *Server) ListCatalog(ctx context.Context, req *gamev1.ListCatalogRequest) (*gamev1.ListCatalogResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	items, next, err := s.svcRepository().ListCatalog(ctx, gameservice.ListCatalogQuery{
		AppCode:      req.GetMeta().GetAppCode(),
		PlatformCode: req.GetPlatformCode(),
		Status:       req.GetStatus(),
		PageSize:     req.GetPageSize(),
		Cursor:       req.GetCursor(),
	})
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	resp := &gamev1.ListCatalogResponse{NextCursor: next, ServerTimeMs: time.Now().UnixMilli(), Games: make([]*gamev1.GameCatalogItem, 0, len(items))}
	for _, item := range items {
		resp.Games = append(resp.Games, catalogToProto(item))
	}
	return resp, nil
}

func (s *Server) UpsertCatalog(ctx context.Context, req *gamev1.UpsertCatalogRequest) (*gamev1.CatalogResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	item := catalogFromProto(req.GetGame())
	item.AppCode = req.GetMeta().GetAppCode()
	normalized, err := normalizeCatalog(item)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	saved, err := s.svcRepository().UpsertCatalog(ctx, normalized)
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.CatalogResponse{Game: catalogToProto(saved), ServerTimeMs: time.Now().UnixMilli()}, nil
}

func (s *Server) SetGameStatus(ctx context.Context, req *gamev1.SetGameStatusRequest) (*gamev1.CatalogResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	status := normalizeStatus(req.GetStatus())
	if status == "" {
		return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "status is invalid"))
	}
	item, err := s.svcRepository().SetGameStatus(ctx, req.GetMeta().GetAppCode(), req.GetGameId(), status, time.Now().UnixMilli())
	if err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.CatalogResponse{Game: catalogToProto(item), ServerTimeMs: time.Now().UnixMilli()}, nil
}

func (s *Server) DeleteCatalog(ctx context.Context, req *gamev1.DeleteCatalogRequest) (*gamev1.DeleteCatalogResponse, error) {
	ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
	if err := s.svcRepository().DeleteCatalog(ctx, req.GetMeta().GetAppCode(), req.GetGameId()); err != nil {
		return nil, xerr.ToGRPCError(err)
	}
	return &gamev1.DeleteCatalogResponse{ServerTimeMs: time.Now().UnixMilli()}, nil
}

func (s *Server) svcRepository() gameservice.Repository {
	if s == nil || s.svc == nil || s.svc.Repository() == nil {
		return unavailableRepository{}
	}
	return s.svc.Repository()
}

type unavailableRepository struct{}

func (unavailableRepository) Ping(context.Context) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListGames(context.Context, gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
	return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListRecentGames(context.Context, gameservice.ListRecentGamesQuery) ([]gamedomain.AppGame, error) {
	return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetLaunchableGame(context.Context, string, string) (gamedomain.LaunchableGame, error) {
	return gamedomain.LaunchableGame{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetPlatform(context.Context, string, string) (gamedomain.Platform, error) {
	return gamedomain.Platform{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) CreateLaunchSession(context.Context, gamedomain.LaunchSession) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetLaunchSessionByToken(context.Context, string, string) (gamedomain.LaunchSession, error) {
	return gamedomain.LaunchSession{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) GetLaunchSessionByTokenScope(context.Context, string, string, string, string) (gamedomain.LaunchSession, error) {
	return gamedomain.LaunchSession{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) InsertCallbackLog(context.Context, gamedomain.CallbackLog) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) CreateGameOrder(context.Context, gamedomain.GameOrder) (gamedomain.GameOrder, bool, error) {
	return gamedomain.GameOrder{}, false, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkOrderSucceeded(context.Context, string, string, string, int64, int64) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkOrderFailed(context.Context, string, string, string, string, string, int64) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) CreateRepairOrder(context.Context, gamedomain.RepairOrder) (gamedomain.RepairOrder, bool, error) {
	return gamedomain.RepairOrder{}, false, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ClaimPendingLevelEvents(context.Context, string, int64, time.Duration, int) ([]gamedomain.LevelEventOutbox, error) {
	return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkLevelEventDelivered(context.Context, string, int64) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) MarkLevelEventFailed(context.Context, string, string, int64, int64) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListPlatforms(context.Context, string, string) ([]gamedomain.Platform, error) {
	return nil, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) UpsertPlatform(context.Context, gamedomain.Platform) (gamedomain.Platform, error) {
	return gamedomain.Platform{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) ListCatalog(context.Context, gameservice.ListCatalogQuery) ([]gamedomain.CatalogItem, string, error) {
	return nil, "", xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) UpsertCatalog(context.Context, gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
	return gamedomain.CatalogItem{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) SetGameStatus(context.Context, string, string, string, int64) (gamedomain.CatalogItem, error) {
	return gamedomain.CatalogItem{}, xerr.New(xerr.Unavailable, "game repository is not configured")
}
func (unavailableRepository) DeleteCatalog(context.Context, string, string) error {
	return xerr.New(xerr.Unavailable, "game repository is not configured")
}

func appGameToProto(item gamedomain.AppGame) *gamev1.AppGame {
	return &gamev1.AppGame{
		GameId:       item.GameID,
		PlatformCode: item.PlatformCode,
		NameKey:      item.NameKey,
		Name:         item.Name,
		IconUrl:      item.IconURL,
		CoverUrl:     item.CoverURL,
		Category:     item.Category,
		LaunchMode:   item.LaunchMode,
		Orientation:  item.Orientation,
		SafeHeight:   item.SafeHeight,
		MinCoin:      item.MinCoin,
		Enabled:      item.Enabled,
		Maintenance:  item.Maintenance,
		SortOrder:    item.SortOrder,
	}
}

func platformToProto(item gamedomain.Platform) *gamev1.GamePlatform {
	return &gamev1.GamePlatform{
		AppCode:      item.AppCode,
		PlatformCode: item.PlatformCode,
		PlatformName: item.PlatformName,
		Status:       item.Status,
		ApiBaseUrl:   item.APIBaseURL,
		SortOrder:    item.SortOrder,
		CreatedAtMs:  item.CreatedAtMS,
		UpdatedAtMs:  item.UpdatedAtMS,
		AdapterType:  item.AdapterType,
		// admin 后台要把当前厂商 key 提供给运营/对接人员核对；更新接口仍以空值表示保留旧密钥。
		CallbackSecret:      item.CallbackSecretCiphertext,
		CallbackSecretSet:   item.CallbackSecretConfigured,
		CallbackIpWhitelist: item.CallbackIPWhitelist,
		AdapterConfigJson:   item.AdapterConfigJSON,
	}
}

func platformFromProto(item *gamev1.GamePlatform) gamedomain.Platform {
	if item == nil {
		return gamedomain.Platform{}
	}
	return gamedomain.Platform{
		AppCode:      item.GetAppCode(),
		PlatformCode: item.GetPlatformCode(),
		PlatformName: item.GetPlatformName(),
		Status:       item.GetStatus(),
		APIBaseURL:   item.GetApiBaseUrl(),
		AdapterType:  item.GetAdapterType(),
		// 更新接口只有在 callback_secret 非空时才会覆盖旧密钥，空值表示保留原密钥。
		CallbackSecretCiphertext: item.GetCallbackSecret(),
		CallbackSecretConfigured: item.GetCallbackSecretSet(),
		CallbackIPWhitelist:      item.GetCallbackIpWhitelist(),
		AdapterConfigJSON:        item.GetAdapterConfigJson(),
		SortOrder:                item.GetSortOrder(),
	}
}

func catalogToProto(item gamedomain.CatalogItem) *gamev1.GameCatalogItem {
	return &gamev1.GameCatalogItem{
		AppCode:        item.AppCode,
		GameId:         item.GameID,
		PlatformCode:   item.PlatformCode,
		ProviderGameId: item.ProviderGameID,
		GameName:       item.GameName,
		Category:       item.Category,
		IconUrl:        item.IconURL,
		CoverUrl:       item.CoverURL,
		LaunchMode:     item.LaunchMode,
		Orientation:    item.Orientation,
		SafeHeight:     item.SafeHeight,
		MinCoin:        item.MinCoin,
		Status:         item.Status,
		SortOrder:      item.SortOrder,
		Tags:           item.Tags,
		CreatedAtMs:    item.CreatedAtMS,
		UpdatedAtMs:    item.UpdatedAtMS,
	}
}

func catalogFromProto(item *gamev1.GameCatalogItem) gamedomain.CatalogItem {
	if item == nil {
		return gamedomain.CatalogItem{}
	}
	return gamedomain.CatalogItem{
		AppCode:        item.GetAppCode(),
		GameID:         item.GetGameId(),
		PlatformCode:   item.GetPlatformCode(),
		ProviderGameID: item.GetProviderGameId(),
		GameName:       item.GetGameName(),
		Category:       item.GetCategory(),
		IconURL:        item.GetIconUrl(),
		CoverURL:       item.GetCoverUrl(),
		LaunchMode:     item.GetLaunchMode(),
		Orientation:    item.GetOrientation(),
		SafeHeight:     item.GetSafeHeight(),
		MinCoin:        item.GetMinCoin(),
		Status:         item.GetStatus(),
		SortOrder:      item.GetSortOrder(),
		Tags:           item.GetTags(),
	}
}

func normalizePlatform(item gamedomain.Platform) (gamedomain.Platform, error) {
	// gRPC 边界只做通用字段清洗，厂商 JSON 的具体语义留给 adapter 解析。
	item.PlatformCode = strings.TrimSpace(item.PlatformCode)
	item.PlatformName = strings.TrimSpace(item.PlatformName)
	item.Status = normalizeStatus(item.Status)
	item.APIBaseURL = strings.TrimSpace(item.APIBaseURL)
	item.AdapterType = normalizeAdapterType(item.AdapterType)
	item.CallbackSecretCiphertext = strings.TrimSpace(item.CallbackSecretCiphertext)
	item.CallbackIPWhitelist = compactStrings(item.CallbackIPWhitelist)
	item.AdapterConfigJSON = strings.TrimSpace(item.AdapterConfigJSON)
	if item.AdapterConfigJSON == "" {
		item.AdapterConfigJSON = "{}"
	}
	if item.PlatformCode == "" || item.PlatformName == "" || item.Status == "" {
		return gamedomain.Platform{}, xerr.New(xerr.InvalidArgument, "platform is incomplete")
	}
	if item.AdapterType == "" {
		return gamedomain.Platform{}, xerr.New(xerr.InvalidArgument, "platform adapter_type is invalid")
	}
	return item, nil
}

func normalizeCatalog(item gamedomain.CatalogItem) (gamedomain.CatalogItem, error) {
	item.GameID = strings.TrimSpace(item.GameID)
	item.PlatformCode = strings.TrimSpace(item.PlatformCode)
	item.ProviderGameID = strings.TrimSpace(item.ProviderGameID)
	item.GameName = strings.TrimSpace(item.GameName)
	item.Status = normalizeStatus(item.Status)
	if item.LaunchMode == "" {
		item.LaunchMode = gamedomain.LaunchModeFullScreen
	}
	if item.Orientation == "" {
		item.Orientation = "portrait"
	}
	if item.SafeHeight < 0 {
		item.SafeHeight = 0
	}
	if item.GameID == "" || item.PlatformCode == "" || item.ProviderGameID == "" || item.GameName == "" || item.Status == "" {
		return gamedomain.CatalogItem{}, xerr.New(xerr.InvalidArgument, "game is incomplete")
	}
	return item, nil
}

func normalizeStatus(status string) string {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case gamedomain.StatusActive:
		return gamedomain.StatusActive
	case gamedomain.StatusMaintenance:
		return gamedomain.StatusMaintenance
	case gamedomain.StatusDisabled:
		return gamedomain.StatusDisabled
	default:
		return ""
	}
}

func normalizeAdapterType(adapterType string) string {
	// 允许的 adapter 必须在这里登记，后台传错值时直接拒绝，避免落库后回调走错协议。
	switch strings.ToLower(strings.TrimSpace(adapterType)) {
	case gamedomain.AdapterYomiV4:
		return gamedomain.AdapterYomiV4
	case gamedomain.AdapterLeaderCCV1:
		return gamedomain.AdapterLeaderCCV1
	case gamedomain.AdapterZeeOneV1:
		return gamedomain.AdapterZeeOneV1
	case gamedomain.AdapterBaishunV1:
		return gamedomain.AdapterBaishunV1
	case gamedomain.AdapterVivaGamesV1:
		return gamedomain.AdapterVivaGamesV1
	case gamedomain.AdapterReyouV1:
		return gamedomain.AdapterReyouV1
	default:
		return ""
	}
}

func compactStrings(values []string) []string {
	out := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	return out
}

func durationFromMillis(value int64) time.Duration {
	if value <= 0 {
		return 0
	}
	return time.Duration(value) * time.Millisecond
}
