package userclient

import (
	"context"
	"fmt"
	"time"

	"hyapp-admin-server/internal/appctx"
	userv1 "hyapp.local/api/proto/user/v1"

	"google.golang.org/grpc"
)

type Client interface {
	GetUser(ctx context.Context, req GetUserRequest) (*User, error)
	BatchGetUsers(ctx context.Context, req BatchGetUsersRequest) (map[int64]*User, error)
	AdminChangeUserCountry(ctx context.Context, req AdminChangeUserCountryRequest) (*ChangeUserCountryResult, error)
	SetUserStatus(ctx context.Context, req SetUserStatusRequest) (*SetUserStatusResult, error)
	GetRegisterRiskConfig(ctx context.Context, req GetRegisterRiskConfigRequest) (*RegisterRiskConfig, error)
	UpdateRegisterRiskConfig(ctx context.Context, req UpdateRegisterRiskConfigRequest) (*RegisterRiskConfig, error)
	RefreshLoginRiskWhitelistCache(ctx context.Context, req RefreshLoginRiskWhitelistCacheRequest) (*LoginRiskWhitelistCacheSnapshot, error)
	ResolveDisplayUserID(ctx context.Context, req ResolveDisplayUserIDRequest) (*UserIdentity, error)
	ListPrettyDisplayIDPools(ctx context.Context, req ListPrettyDisplayIDPoolsRequest) ([]*PrettyDisplayIDPool, int64, error)
	CreatePrettyDisplayIDPool(ctx context.Context, req PrettyDisplayIDPoolRequest) (*PrettyDisplayIDPool, error)
	UpdatePrettyDisplayIDPool(ctx context.Context, req PrettyDisplayIDPoolRequest) (*PrettyDisplayIDPool, error)
	GeneratePrettyDisplayIDs(ctx context.Context, req GeneratePrettyDisplayIDsRequest) (*PrettyDisplayIDGenerationBatch, error)
	ListPrettyDisplayIDs(ctx context.Context, req ListPrettyDisplayIDsRequest) ([]*PrettyDisplayID, int64, error)
	SetPrettyDisplayIDStatus(ctx context.Context, req SetPrettyDisplayIDStatusRequest) (*PrettyDisplayID, error)
	RecyclePrettyDisplayID(ctx context.Context, req RecyclePrettyDisplayIDRequest) (*PrettyDisplayID, error)
	AdminGrantPrettyDisplayID(ctx context.Context, req AdminGrantPrettyDisplayIDRequest) (*AdminGrantPrettyDisplayIDResult, error)
	ListCountries(ctx context.Context, req ListCountriesRequest) ([]*Country, error)
	UpdateCountry(ctx context.Context, req UpdateCountryRequest) (*Country, error)
	ListRegions(ctx context.Context, req ListRegionsRequest) ([]*Region, error)
	GetRegion(ctx context.Context, req GetRegionRequest) (*Region, error)
	UpdateRegion(ctx context.Context, req UpdateRegionRequest) (*Region, error)
	ReplaceRegionCountries(ctx context.Context, req ReplaceRegionCountriesRequest) (*Region, error)
	CreateBDLeader(ctx context.Context, req CreateBDLeaderRequest) (*BDProfile, error)
	CreateBD(ctx context.Context, req CreateBDRequest) (*BDProfile, error)
	SetBDStatus(ctx context.Context, req SetBDStatusRequest) (*BDProfile, error)
	CreateCoinSeller(ctx context.Context, req CreateCoinSellerRequest) (*CoinSellerProfile, error)
	SetCoinSellerStatus(ctx context.Context, req SetCoinSellerStatusRequest) (*CoinSellerProfile, error)
	ReviewCoinSellerSubApplication(ctx context.Context, req ReviewCoinSellerSubApplicationRequest) (*CoinSellerSubApplicationReviewResult, error)
	GetCoinSellerProfile(ctx context.Context, req GetCoinSellerProfileRequest) (*CoinSellerProfile, error)
	GetUserRoleSummary(ctx context.Context, req GetUserRoleSummaryRequest) (*UserRoleSummary, error)
	GetRoleScopePolicy(ctx context.Context, req GetRoleScopePolicyRequest) (*RoleScopePolicy, error)
	UpdateRoleScopePolicies(ctx context.Context, req UpdateRoleScopePoliciesRequest) ([]RoleScopePolicy, error)
	CreateAgency(ctx context.Context, req CreateAgencyRequest) (*CreateAgencyResult, error)
	AdminAddAgencyHost(ctx context.Context, req AdminAddAgencyHostRequest) (*CreateAgencyResult, error)
	CloseAgency(ctx context.Context, req CloseAgencyRequest) (*Agency, error)
	DeleteAgency(ctx context.Context, req DeleteAgencyRequest) (*Agency, error)
	SetAgencyStatus(ctx context.Context, req SetAgencyStatusRequest) (*Agency, error)
	SetAgencyJoinEnabled(ctx context.Context, req SetAgencyJoinEnabledRequest) (*Agency, error)
	QuickCreateAccount(ctx context.Context, req QuickCreateAccountRequest) (*QuickCreateAccountResult, error)
}

// ExternalTeamClient is intentionally separate from Client so existing modules and
// test doubles do not accidentally gain access to portal-only hierarchy commands.
type ExternalTeamClient interface {
	ExternalInviteBD(ctx context.Context, req ExternalInviteBDRequest) (*ExternalRoleInvitation, error)
	ExternalInviteAgency(ctx context.Context, req ExternalInviteAgencyRequest) (*ExternalRoleInvitation, error)
	ExternalInviteHost(ctx context.Context, req ExternalInviteHostRequest) (*ExternalRoleInvitation, error)
	ListExternalTeamUsers(ctx context.Context, req ListExternalTeamUsersRequest) (*ListExternalTeamUsersResult, error)
	SearchExternalInvitationCandidate(ctx context.Context, req SearchExternalInvitationCandidateRequest) (*ExternalTeamUser, error)
}

type GetUserRequest struct {
	RequestID string
	Caller    string
	UserID    int64
}

// BatchGetUsersRequest 只承载后台展示需要的一组真实 user_id，不在 admin-server 里回查用户库。
type BatchGetUsersRequest struct {
	RequestID string
	Caller    string
	UserIDs   []int64
}

type QuickCreateAccountRequest struct {
	RequestID      string
	Caller         string
	Password       string
	Username       string
	Avatar         string
	Gender         string
	Country        string
	DeviceID       string
	Source         string
	InstallChannel string
	Platform       string
	Language       string
	Timezone       string
}

type QuickCreateAccountResult struct {
	UserID        int64
	DisplayUserID string
	AccessToken   string
}

type SetUserStatusRequest struct {
	RequestID      string
	Caller         string
	TargetUserID   int64
	Status         string
	OperatorType   string
	OperatorUserID int64
	Reason         string
}

type AdminChangeUserCountryRequest struct {
	RequestID    string
	Caller       string
	TargetUserID int64
	Country      string
	AdminUserID  int64
	Reason       string
}

type ChangeUserCountryResult struct {
	User          *User `json:"user"`
	OldRegionID   int64 `json:"oldRegionId"`
	NewRegionID   int64 `json:"newRegionId"`
	RegionChanged bool  `json:"regionChanged"`
}

type GetRoleScopePolicyRequest struct {
	RequestID string
	Caller    string
	Scene     string
}

type RoleScopePolicy struct {
	Scene                       string `json:"scene"`
	BaseScope                   string `json:"baseScope"`
	ExpandedScope               string `json:"expandedScope"`
	RegionExpansionConfigurable bool   `json:"regionExpansionConfigurable"`
	UpdatedAtMS                 int64  `json:"updatedAtMs"`
}

type UpdateRoleScopePoliciesRequest struct {
	RequestID string
	Caller    string
	Policies  []RoleScopePolicy
}

type SetUserStatusResult struct {
	User                *User  `json:"user"`
	RevokedSessionCount int64  `json:"revokedSessionCount"`
	AccessTokenRevoked  bool   `json:"accessTokenRevoked"`
	AccessTokenError    string `json:"accessTokenRevokeError,omitempty"`
	IMKicked            bool   `json:"imKicked"`
	IMKickError         string `json:"imKickError,omitempty"`
	RoomEvicted         bool   `json:"roomEvicted"`
	RoomID              string `json:"roomId,omitempty"`
	RTCKicked           bool   `json:"rtcKicked"`
	RTCKickError        string `json:"rtcKickError,omitempty"`
	RoomEvictError      string `json:"roomEvictError,omitempty"`
}

type GetRegisterRiskConfigRequest struct {
	RequestID string
	Caller    string
}

type UpdateRegisterRiskConfigRequest struct {
	RequestID            string
	Caller               string
	MaxAccountsPerDevice int32
	OperatorAdminID      int64
}

type RegisterRiskConfig struct {
	AppCode              string `json:"appCode"`
	MaxAccountsPerDevice int32  `json:"maxAccountsPerDevice"`
	UpdatedAtMs          int64  `json:"updatedAtMs"`
	UpdatedByAdminID     int64  `json:"updatedByAdminId"`
}

type RefreshLoginRiskWhitelistCacheRequest struct {
	RequestID string
	Caller    string
}

type LoginRiskWhitelistCacheSnapshot struct {
	Refreshed            bool  `json:"refreshed"`
	IPWhitelistCount     int32 `json:"ipWhitelistCount"`
	UserWhitelistCount   int32 `json:"userWhitelistCount"`
	UserCacheExpiresAtMs int64 `json:"userCacheExpiresAtMs"`
}

type ResolveDisplayUserIDRequest struct {
	RequestID     string
	Caller        string
	DisplayUserID string
}

// ResolveAdminUserIdentifierRequest 仅供后台精确搜索永久默认短号、当前有效展示号或系统长 user_id。
type ResolveAdminUserIdentifierRequest struct {
	RequestID      string
	Caller         string
	UserIdentifier string
}

type UserIdentity struct {
	UserID                   int64  `json:"userId,string"`
	DisplayUserID            string `json:"displayUserId"`
	Status                   string `json:"status"`
	DefaultDisplayUserID     string `json:"defaultDisplayUserId"`
	DisplayUserIDKind        string `json:"displayUserIdKind"`
	DisplayUserIDExpiresAtMs int64  `json:"displayUserIdExpiresAtMs"`
	PrettyID                 string `json:"prettyId,omitempty"`
	PrettyDisplayUserID      string `json:"prettyDisplayUserId,omitempty"`
	AppCode                  string `json:"appCode"`
}

type User struct {
	UserID                   int64  `json:"userId,string"`
	Status                   string `json:"status"`
	ContentBlacklisted       bool   `json:"contentBlacklisted"`
	CreatedAtMs              int64  `json:"createdAtMs"`
	UpdatedAtMs              int64  `json:"updatedAtMs"`
	DisplayUserID            string `json:"displayUserId"`
	DefaultDisplayUserID     string `json:"defaultDisplayUserId"`
	DisplayUserIDKind        string `json:"displayUserIdKind"`
	DisplayUserIDExpiresAtMs int64  `json:"displayUserIdExpiresAtMs"`
	PrettyID                 string `json:"prettyId,omitempty"`
	PrettyDisplayUserID      string `json:"prettyDisplayUserId,omitempty"`
	Username                 string `json:"username"`
	Gender                   string `json:"gender"`
	Country                  string `json:"country"`
	Avatar                   string `json:"avatar"`
	Birth                    string `json:"birth"`
	Language                 string `json:"language"`
	CountryID                int64  `json:"countryId"`
	CountryName              string `json:"countryName"`
	CountryDisplayName       string `json:"countryDisplayName"`
	RegionID                 int64  `json:"regionId"`
	RegionCode               string `json:"regionCode"`
	RegionName               string `json:"regionName"`
	ProfileCompleted         bool   `json:"profileCompleted"`
	ProfileCompletedAtMs     int64  `json:"profileCompletedAtMs"`
	OnboardingStatus         string `json:"onboardingStatus"`
	ISONumeric               string `json:"isoNumeric"`
	PhoneCountryCode         string `json:"phoneCountryCode"`
	CountryEnabled           bool   `json:"countryEnabled"`
	ContactInfo              string `json:"contactInfo"`
}

type GRPCClient struct {
	client          userv1.UserServiceClient
	authClient      userv1.AuthServiceClient
	identityClient  userv1.UserIdentityServiceClient
	countryClient   userv1.CountryAdminServiceClient
	regionClient    userv1.RegionAdminServiceClient
	prettyClient    userv1.UserPrettyDisplayIDAdminServiceClient
	hostClient      userv1.UserHostServiceClient
	hostAdminClient userv1.UserHostAdminServiceClient
}

func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
	return &GRPCClient{
		client:          userv1.NewUserServiceClient(conn),
		authClient:      userv1.NewAuthServiceClient(conn),
		identityClient:  userv1.NewUserIdentityServiceClient(conn),
		countryClient:   userv1.NewCountryAdminServiceClient(conn),
		regionClient:    userv1.NewRegionAdminServiceClient(conn),
		prettyClient:    userv1.NewUserPrettyDisplayIDAdminServiceClient(conn),
		hostClient:      userv1.NewUserHostServiceClient(conn),
		hostAdminClient: userv1.NewUserHostAdminServiceClient(conn),
	}
}

func (c *GRPCClient) QuickCreateAccount(ctx context.Context, req QuickCreateAccountRequest) (*QuickCreateAccountResult, error) {
	resp, err := c.authClient.QuickCreateAccount(ctx, &userv1.QuickCreateAccountRequest{
		Meta: &userv1.RequestMeta{
			RequestId: req.RequestID,
			Caller:    req.Caller,
			SentAtMs:  time.Now().UnixMilli(),
			AppCode:   appctx.FromContext(ctx),
			Platform:  req.Platform,
			Language:  req.Language,
			Timezone:  req.Timezone,
		},
		Password:       req.Password,
		Username:       req.Username,
		Avatar:         req.Avatar,
		Gender:         req.Gender,
		Country:        req.Country,
		DeviceId:       req.DeviceID,
		Source:         req.Source,
		InstallChannel: req.InstallChannel,
		Platform:       req.Platform,
		Language:       req.Language,
		Timezone:       req.Timezone,
	})
	if err != nil {
		return nil, err
	}
	token := resp.GetToken()
	return &QuickCreateAccountResult{
		UserID:        token.GetUserId(),
		DisplayUserID: token.GetDisplayUserId(),
		AccessToken:   token.GetAccessToken(),
	}, nil
}

func (c *GRPCClient) GetUser(ctx context.Context, req GetUserRequest) (*User, error) {
	resp, err := c.client.GetUser(ctx, &userv1.GetUserRequest{
		Meta: &userv1.RequestMeta{
			RequestId: req.RequestID,
			Caller:    req.Caller,
			SentAtMs:  time.Now().UnixMilli(),
			AppCode:   appctx.FromContext(ctx),
		},
		UserId: req.UserID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoUser(resp.GetUser()), nil
}

func (c *GRPCClient) GetRegisterRiskConfig(ctx context.Context, req GetRegisterRiskConfigRequest) (*RegisterRiskConfig, error) {
	resp, err := c.authClient.GetRegisterRiskConfig(ctx, &userv1.GetRegisterRiskConfigRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller),
	})
	if err != nil {
		return nil, err
	}
	return fromProtoRegisterRiskConfig(resp.GetConfig()), nil
}

func (c *GRPCClient) UpdateRegisterRiskConfig(ctx context.Context, req UpdateRegisterRiskConfigRequest) (*RegisterRiskConfig, error) {
	resp, err := c.authClient.UpdateRegisterRiskConfig(ctx, &userv1.UpdateRegisterRiskConfigRequest{
		Meta:                 requestMeta(ctx, req.RequestID, req.Caller),
		MaxAccountsPerDevice: req.MaxAccountsPerDevice,
		OperatorAdminId:      req.OperatorAdminID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoRegisterRiskConfig(resp.GetConfig()), nil
}

func (c *GRPCClient) RefreshLoginRiskWhitelistCache(ctx context.Context, req RefreshLoginRiskWhitelistCacheRequest) (*LoginRiskWhitelistCacheSnapshot, error) {
	resp, err := c.authClient.RefreshLoginRiskWhitelistCache(ctx, &userv1.RefreshLoginRiskWhitelistCacheRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller),
	})
	if err != nil {
		return nil, err
	}
	return &LoginRiskWhitelistCacheSnapshot{
		Refreshed:            resp.GetRefreshed(),
		IPWhitelistCount:     resp.GetIpWhitelistCount(),
		UserWhitelistCount:   resp.GetUserWhitelistCount(),
		UserCacheExpiresAtMs: resp.GetUserCacheExpiresAtMs(),
	}, nil
}

// BatchGetUsers 复用 user-service 主数据 RPC，让后台列表只做展示聚合，不复制用户资料到游戏库。
func (c *GRPCClient) BatchGetUsers(ctx context.Context, req BatchGetUsersRequest) (map[int64]*User, error) {
	resp, err := c.client.BatchGetUsers(ctx, &userv1.BatchGetUsersRequest{
		Meta:    requestMeta(ctx, req.RequestID, req.Caller),
		UserIds: req.UserIDs,
	})
	if err != nil {
		return nil, err
	}
	users := make(map[int64]*User, len(resp.GetUsers()))
	for userID, user := range resp.GetUsers() {
		users[userID] = fromProtoUser(user)
	}
	return users, nil
}

func (c *GRPCClient) AdminChangeUserCountry(ctx context.Context, req AdminChangeUserCountryRequest) (*ChangeUserCountryResult, error) {
	resp, err := c.client.AdminChangeUserCountry(ctx, &userv1.AdminChangeUserCountryRequest{
		Meta:         requestMeta(ctx, req.RequestID, req.Caller),
		TargetUserId: req.TargetUserID,
		Country:      req.Country,
		AdminUserId:  req.AdminUserID,
		Reason:       req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return &ChangeUserCountryResult{
		User:          fromProtoUser(resp.GetUser()),
		OldRegionID:   resp.GetOldRegionId(),
		NewRegionID:   resp.GetNewRegionId(),
		RegionChanged: resp.GetRegionChanged(),
	}, nil
}

func (c *GRPCClient) SetUserStatus(ctx context.Context, req SetUserStatusRequest) (*SetUserStatusResult, error) {
	resp, err := c.client.SetUserStatus(ctx, &userv1.SetUserStatusRequest{
		Meta:           requestMeta(ctx, req.RequestID, req.Caller),
		TargetUserId:   req.TargetUserID,
		Status:         userStatusToProto(req.Status),
		OperatorType:   req.OperatorType,
		OperatorUserId: req.OperatorUserID,
		Reason:         req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return &SetUserStatusResult{
		User:                fromProtoUser(resp.GetUser()),
		RevokedSessionCount: resp.GetRevokedSessionCount(),
		AccessTokenRevoked:  resp.GetAccessTokenRevoked(),
		AccessTokenError:    resp.GetAccessTokenRevokeError(),
		IMKicked:            resp.GetImKicked(),
		IMKickError:         resp.GetImKickError(),
		RoomEvicted:         resp.GetRoomEvicted(),
		RoomID:              resp.GetRoomId(),
		RTCKicked:           resp.GetRtcKicked(),
		RTCKickError:        resp.GetRtcKickError(),
		RoomEvictError:      resp.GetRoomEvictError(),
	}, nil
}

// BatchGetUserAdminProfiles 汇总 user-service owner 的生日、注册设备、成功登录、身份和有效封禁事实。
func (c *GRPCClient) BatchGetUserAdminProfiles(ctx context.Context, req *userv1.BatchGetUserAdminProfilesRequest) (*userv1.BatchGetUserAdminProfilesResponse, error) {
	return c.client.BatchGetUserAdminProfiles(ctx, req)
}

// AdminIssueUserAccessToken 透传后台按用户重签 access token 的 RPC；
// user-service 侧刻意不写 login_audit，审计由 admin 操作日志完成。
func (c *GRPCClient) AdminIssueUserAccessToken(ctx context.Context, req *userv1.AdminIssueUserAccessTokenRequest) (*userv1.AdminIssueUserAccessTokenResponse, error) {
	return c.client.AdminIssueUserAccessToken(ctx, req)
}

func (c *GRPCClient) AdminBanUser(ctx context.Context, req *userv1.AdminBanUserRequest) (*userv1.AdminBanUserResponse, error) {
	return c.client.AdminBanUser(ctx, req)
}

func (c *GRPCClient) AdminUnbanUser(ctx context.Context, req *userv1.AdminUnbanUserRequest) (*userv1.AdminUnbanUserResponse, error) {
	return c.client.AdminUnbanUser(ctx, req)
}

// 内容黑名单仍由 user-service 持有事实；admin-server 只透传治理命令和读模型，
// 避免后台直接读取用户库后形成第二套状态判断。
func (c *GRPCClient) ListUserContentBlacklist(ctx context.Context, req *userv1.ListUserContentBlacklistRequest) (*userv1.ListUserContentBlacklistResponse, error) {
	return c.client.ListUserContentBlacklist(ctx, req)
}

func (c *GRPCClient) AdminAddUserContentBlacklist(ctx context.Context, req *userv1.AdminAddUserContentBlacklistRequest) (*userv1.AdminAddUserContentBlacklistResponse, error) {
	return c.client.AdminAddUserContentBlacklist(ctx, req)
}

func (c *GRPCClient) AdminRemoveUserContentBlacklist(ctx context.Context, req *userv1.AdminRemoveUserContentBlacklistRequest) (*userv1.AdminRemoveUserContentBlacklistResponse, error) {
	return c.client.AdminRemoveUserContentBlacklist(ctx, req)
}

func userStatusToProto(status string) userv1.UserStatus {
	switch status {
	case "active":
		return userv1.UserStatus_USER_STATUS_ACTIVE
	case "disabled":
		return userv1.UserStatus_USER_STATUS_DISABLED
	case "banned":
		return userv1.UserStatus_USER_STATUS_BANNED
	default:
		return userv1.UserStatus_USER_STATUS_UNSPECIFIED
	}
}

func (c *GRPCClient) ResolveDisplayUserID(ctx context.Context, req ResolveDisplayUserIDRequest) (*UserIdentity, error) {
	resp, err := c.identityClient.ResolveDisplayUserID(ctx, &userv1.ResolveDisplayUserIDRequest{
		Meta:          requestMeta(ctx, req.RequestID, req.Caller),
		DisplayUserId: req.DisplayUserID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoUserIdentity(resp.GetIdentity()), nil
}

// ResolveAdminUserIdentifier 调用 user-service owner 的后台专用解析契约；admin-server 不读取用户库，
// 也不会为了搜索默认短号而改变客户端 ResolveDisplayUserID 的 active-only 语义。
func (c *GRPCClient) ResolveAdminUserIdentifier(ctx context.Context, req ResolveAdminUserIdentifierRequest) (*UserIdentity, error) {
	resp, err := c.identityClient.ResolveAdminUserIdentifier(ctx, &userv1.ResolveAdminUserIdentifierRequest{
		Meta:           requestMeta(ctx, req.RequestID, req.Caller),
		UserIdentifier: req.UserIdentifier,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoUserIdentity(resp.GetIdentity()), nil
}

func fromProtoRegisterRiskConfig(config *userv1.RegisterRiskConfig) *RegisterRiskConfig {
	if config == nil {
		return nil
	}
	return &RegisterRiskConfig{
		AppCode:              config.GetAppCode(),
		MaxAccountsPerDevice: config.GetMaxAccountsPerDevice(),
		UpdatedAtMs:          config.GetUpdatedAtMs(),
		UpdatedByAdminID:     config.GetUpdatedByAdminId(),
	}
}

func fromProtoUser(user *userv1.User) *User {
	if user == nil {
		return nil
	}
	return &User{
		UserID:                   user.GetUserId(),
		Status:                   fromProtoStatus(user.GetStatus()),
		ContentBlacklisted:       user.GetContentBlacklisted(),
		CreatedAtMs:              user.GetCreatedAtMs(),
		UpdatedAtMs:              user.GetUpdatedAtMs(),
		DisplayUserID:            user.GetDisplayUserId(),
		DefaultDisplayUserID:     user.GetDefaultDisplayUserId(),
		DisplayUserIDKind:        user.GetDisplayUserIdKind(),
		DisplayUserIDExpiresAtMs: user.GetDisplayUserIdExpiresAtMs(),
		PrettyID:                 user.GetPrettyId(),
		PrettyDisplayUserID:      user.GetPrettyDisplayUserId(),
		Username:                 user.GetUsername(),
		Gender:                   user.GetGender(),
		Country:                  user.GetCountry(),
		Avatar:                   user.GetAvatar(),
		Birth:                    user.GetBirth(),
		Language:                 user.GetLanguage(),
		CountryID:                user.GetCountryId(),
		CountryName:              user.GetCountryName(),
		CountryDisplayName:       user.GetCountryDisplayName(),
		RegionID:                 user.GetRegionId(),
		RegionCode:               user.GetRegionCode(),
		RegionName:               user.GetRegionName(),
		ProfileCompleted:         user.GetProfileCompleted(),
		ProfileCompletedAtMs:     user.GetProfileCompletedAtMs(),
		OnboardingStatus:         user.GetOnboardingStatus(),
		ISONumeric:               user.GetIsoNumeric(),
		PhoneCountryCode:         user.GetPhoneCountryCode(),
		CountryEnabled:           user.GetCountryEnabled(),
		ContactInfo:              user.GetContactInfo(),
	}
}

func fromProtoStatus(status userv1.UserStatus) string {
	switch status {
	case userv1.UserStatus_USER_STATUS_ACTIVE:
		return "active"
	case userv1.UserStatus_USER_STATUS_DISABLED:
		return "disabled"
	case userv1.UserStatus_USER_STATUS_BANNED:
		return "banned"
	default:
		return ""
	}
}

func fromProtoUserIdentity(identity *userv1.UserIdentity) *UserIdentity {
	if identity == nil {
		return nil
	}
	return &UserIdentity{
		UserID:                   identity.GetUserId(),
		DisplayUserID:            identity.GetDisplayUserId(),
		Status:                   identity.GetStatus(),
		DefaultDisplayUserID:     identity.GetDefaultDisplayUserId(),
		DisplayUserIDKind:        identity.GetDisplayUserIdKind(),
		DisplayUserIDExpiresAtMs: identity.GetDisplayUserIdExpiresAtMs(),
		PrettyID:                 identity.GetPrettyId(),
		PrettyDisplayUserID:      identity.GetPrettyDisplayUserId(),
		AppCode:                  identity.GetAppCode(),
	}
}

type CreateBDLeaderRequest struct {
	RequestID    string
	Caller       string
	CommandID    string
	AdminUserID  int64
	TargetUserID int64
	Reason       string
}

type CreateBDRequest struct {
	RequestID          string
	Caller             string
	CommandID          string
	AdminUserID        int64
	TargetUserID       int64
	ParentLeaderUserID int64
	Reason             string
}

type SetBDStatusRequest struct {
	RequestID    string
	Caller       string
	CommandID    string
	AdminUserID  int64
	TargetUserID int64
	Role         string
	Status       string
	Reason       string
}

type CreateCoinSellerRequest struct {
	RequestID               string
	Caller                  string
	CommandID               string
	AdminUserID             int64
	TargetUserID            int64
	Reason                  string
	CanManageSubCoinSellers *bool
}

type SetCoinSellerStatusRequest struct {
	RequestID               string
	Caller                  string
	CommandID               string
	AdminUserID             int64
	TargetUserID            int64
	Status                  string
	Reason                  string
	CanManageSubCoinSellers *bool
}

type ReviewCoinSellerSubApplicationRequest struct {
	RequestID     string
	Caller        string
	CommandID     string
	AdminUserID   int64
	ApplicationID int64
	Decision      string
	Reason        string
}

type GetCoinSellerProfileRequest struct {
	RequestID string
	Caller    string
	UserID    int64
}

type GetUserRoleSummaryRequest struct {
	RequestID string
	Caller    string
	UserID    int64
}

type CreateAgencyRequest struct {
	RequestID      string
	Caller         string
	CommandID      string
	AdminUserID    int64
	OwnerUserID    int64
	ParentBDUserID int64
	Name           string
	JoinEnabled    bool
	Reason         string
}

type AdminAddAgencyHostRequest struct {
	RequestID    string
	Caller       string
	CommandID    string
	AdminUserID  int64
	AgencyID     int64
	TargetUserID int64
	Reason       string
}

type CloseAgencyRequest struct {
	RequestID   string
	Caller      string
	CommandID   string
	AdminUserID int64
	AgencyID    int64
	Reason      string
}

type DeleteAgencyRequest struct {
	RequestID   string
	Caller      string
	CommandID   string
	AdminUserID int64
	AgencyID    int64
	Reason      string
}

type SetAgencyStatusRequest struct {
	RequestID   string
	Caller      string
	CommandID   string
	AdminUserID int64
	AgencyID    int64
	Status      string
	Reason      string
}

type SetAgencyJoinEnabledRequest struct {
	RequestID   string
	Caller      string
	CommandID   string
	AdminUserID int64
	AgencyID    int64
	JoinEnabled bool
	Reason      string
}

type BDProfile struct {
	UserID                 int64        `json:"userId,string"`
	Role                   string       `json:"role"`
	RegionID               int64        `json:"regionId"`
	ParentLeaderUserID     int64        `json:"parentLeaderUserId,string"`
	PositionAlias          string       `json:"positionAlias"`
	Status                 string       `json:"status"`
	CreatedByUserID        int64        `json:"createdByUserId,string"`
	CreatedAtMs            int64        `json:"createdAtMs"`
	UpdatedAtMs            int64        `json:"updatedAtMs"`
	DisplayUserID          string       `json:"displayUserId"`
	Username               string       `json:"username"`
	Avatar                 string       `json:"avatar"`
	Contact                string       `json:"contact"`
	RegionName             string       `json:"regionName"`
	ParentLeaderDisplayID  string       `json:"parentLeaderDisplayUserId"`
	ParentLeaderUsername   string       `json:"parentLeaderUsername"`
	ParentLeaderAvatar     string       `json:"parentLeaderAvatar"`
	CreatedByDisplayUserID string       `json:"createdByDisplayUserId"`
	CreatedByUsername      string       `json:"createdByUsername"`
	CreatedByAvatar        string       `json:"createdByAvatar"`
	AgencyCount            int64        `json:"agencyCount"`
	SubBDCount             int64        `json:"subBdCount"`
	SalaryWallet           SalaryWallet `json:"salaryWallet"`
}

type Agency struct {
	AgencyID              int64        `json:"agencyId,string"`
	OwnerUserID           int64        `json:"ownerUserId,string"`
	RegionID              int64        `json:"regionId"`
	ParentBDUserID        int64        `json:"parentBdUserId,string"`
	Name                  string       `json:"name"`
	Avatar                string       `json:"avatar"`
	Status                string       `json:"status"`
	JoinEnabled           bool         `json:"joinEnabled"`
	MaxHosts              int32        `json:"maxHosts"`
	CreatedByUserID       int64        `json:"createdByUserId,string"`
	CreatedAtMs           int64        `json:"createdAtMs"`
	UpdatedAtMs           int64        `json:"updatedAtMs"`
	OwnerDisplayUserID    string       `json:"ownerDisplayUserId"`
	OwnerUsername         string       `json:"ownerUsername"`
	OwnerAvatar           string       `json:"ownerAvatar"`
	Contact               string       `json:"contact"`
	ParentBDDisplayUserID string       `json:"parentBdDisplayUserId"`
	ParentBDUsername      string       `json:"parentBdUsername"`
	ParentBDAvatar        string       `json:"parentBdAvatar"`
	RegionName            string       `json:"regionName"`
	ActiveHostCount       int64        `json:"activeHostCount"`
	SalaryWallet          SalaryWallet `json:"salaryWallet"`
}

type HostProfile struct {
	UserID                          int64        `json:"userId,string"`
	Status                          string       `json:"status"`
	RegionID                        int64        `json:"regionId"`
	CurrentAgencyID                 int64        `json:"currentAgencyId,string"`
	CurrentMembershipID             int64        `json:"currentMembershipId,string"`
	Source                          string       `json:"source"`
	FirstBecameHostAtMs             int64        `json:"firstBecameHostAtMs"`
	CreatedAtMs                     int64        `json:"createdAtMs"`
	UpdatedAtMs                     int64        `json:"updatedAtMs"`
	DisplayUserID                   string       `json:"displayUserId"`
	Username                        string       `json:"username"`
	Avatar                          string       `json:"avatar"`
	Contact                         string       `json:"contact"`
	RegionName                      string       `json:"regionName"`
	CurrentAgencyName               string       `json:"currentAgencyName"`
	Diamond                         int64        `json:"diamond"`
	CurrentAgencyOwnerUserID        int64        `json:"currentAgencyOwnerUserId,string"`
	CurrentAgencyOwnerDisplayUserID string       `json:"currentAgencyOwnerDisplayUserId"`
	CurrentAgencyOwnerUsername      string       `json:"currentAgencyOwnerUsername"`
	CurrentAgencyOwnerAvatar        string       `json:"currentAgencyOwnerAvatar"`
	SalaryWallet                    SalaryWallet `json:"salaryWallet"`
}

type SalaryWallet struct {
	Role            string `json:"role"`
	UserID          int64  `json:"userId,string"`
	AssetType       string `json:"assetType"`
	AvailableAmount int64  `json:"availableAmount"`
	FrozenAmount    int64  `json:"frozenAmount"`
	UpdatedAtMs     int64  `json:"updatedAtMs"`
}

type CoinSellerProfile struct {
	UserID                  int64  `json:"userId,string"`
	Status                  string `json:"status"`
	MerchantAssetType       string `json:"merchantAssetType"`
	CanManageSubCoinSellers bool   `json:"canManageSubCoinSellers"`
	CreatedByUserID         int64  `json:"createdByUserId,string"`
	CreatedAtMs             int64  `json:"createdAtMs"`
	UpdatedAtMs             int64  `json:"updatedAtMs"`
}

type CoinSellerListItem struct {
	UserID                  int64  `json:"userId,string"`
	DisplayUserID           string `json:"displayUserId"`
	Username                string `json:"username"`
	Avatar                  string `json:"avatar"`
	RegionID                int64  `json:"regionId"`
	RegionName              string `json:"regionName"`
	Status                  string `json:"status"`
	MerchantAssetType       string `json:"merchantAssetType"`
	ContactInfo             string `json:"contactInfo"`
	CanManageSubCoinSellers bool   `json:"canManageSubCoinSellers"`
	UpdatedAtMs             int64  `json:"updatedAtMs"`
}

type CoinSellerSubApplication struct {
	ApplicationID     int64  `json:"applicationId,string"`
	CommandID         string `json:"commandId"`
	ParentUserID      int64  `json:"parentUserId,string"`
	TargetUserID      int64  `json:"targetUserId,string"`
	Status            string `json:"status"`
	RelationID        int64  `json:"relationId,string"`
	ReviewedByAdminID int64  `json:"reviewedByAdminId,string"`
	ReviewReason      string `json:"reviewReason"`
	ReviewedAtMs      int64  `json:"reviewedAtMs"`
	CreatedAtMs       int64  `json:"createdAtMs"`
	UpdatedAtMs       int64  `json:"updatedAtMs"`
}

type CoinSellerSubRelation struct {
	RelationID      int64  `json:"relationId,string"`
	ParentUserID    int64  `json:"parentUserId,string"`
	ChildUserID     int64  `json:"childUserId,string"`
	Status          string `json:"status"`
	CreatedByUserID int64  `json:"createdByUserId,string"`
	CreatedAtMs     int64  `json:"createdAtMs"`
	UpdatedAtMs     int64  `json:"updatedAtMs"`
}

type CoinSellerSubApplicationReviewResult struct {
	Application *CoinSellerSubApplication `json:"application"`
	Relation    *CoinSellerSubRelation    `json:"relation,omitempty"`
	Child       *CoinSellerListItem       `json:"child,omitempty"`
}

type UserRoleSummary struct {
	UserID                 int64  `json:"userId,string"`
	IsHost                 bool   `json:"isHost"`
	IsAgency               bool   `json:"isAgency"`
	IsBD                   bool   `json:"isBd"`
	IsBDLeader             bool   `json:"isBdLeader"`
	IsCoinSeller           bool   `json:"isCoinSeller"`
	HostStatus             string `json:"hostStatus"`
	AgencyID               int64  `json:"agencyId,string"`
	BDID                   int64  `json:"bdId,string"`
	BDStatus               string `json:"bdStatus"`
	CoinSellerStatus       string `json:"coinSellerStatus"`
	PendingRoleInvitations int64  `json:"pendingRoleInvitations"`
	IsManager              bool   `json:"isManager"`
}

type AgencyMembership struct {
	MembershipID   int64  `json:"membershipId,string"`
	AgencyID       int64  `json:"agencyId,string"`
	HostUserID     int64  `json:"hostUserId,string"`
	RegionID       int64  `json:"regionId"`
	MembershipType string `json:"membershipType"`
	Status         string `json:"status"`
	JoinedAtMs     int64  `json:"joinedAtMs"`
	EndedAtMs      int64  `json:"endedAtMs"`
	EndedByUserID  int64  `json:"endedByUserId,string"`
	EndedReason    string `json:"endedReason"`
	CreatedAtMs    int64  `json:"createdAtMs"`
	UpdatedAtMs    int64  `json:"updatedAtMs"`
}

type CreateAgencyResult struct {
	Agency      *Agency           `json:"agency"`
	HostProfile *HostProfile      `json:"hostProfile"`
	Membership  *AgencyMembership `json:"membership"`
}

type ExternalTeamUser struct {
	UserID               int64    `json:"userId,string"`
	DefaultDisplayUserID string   `json:"defaultDisplayUserId"`
	CurrentDisplayUserID string   `json:"currentDisplayUserId"`
	Username             string   `json:"username"`
	Avatar               string   `json:"avatar"`
	Status               string   `json:"status"`
	RegionID             int64    `json:"regionId"`
	Roles                []string `json:"roles"`
	ParentOwnerUserID    int64    `json:"parentOwnerUserId,string"`
	ParentBDUserID       int64    `json:"parentBdUserId,string"`
	AgencyID             int64    `json:"agencyId,string"`
}

type ExternalRoleInvitation struct {
	InvitationID       int64  `json:"invitationId,string"`
	CommandID          string `json:"commandId"`
	InvitationType     string `json:"invitationType"`
	Status             string `json:"status"`
	ExternalOperatorID int64  `json:"externalOperatorUserId,string"`
	TargetUserID       int64  `json:"targetUserId,string"`
	RegionID           int64  `json:"regionId"`
	AgencyName         string `json:"agencyName,omitempty"`
	ParentBDUserID     int64  `json:"parentBdUserId,string,omitempty"`
	ParentOwnerUserID  int64  `json:"parentOwnerUserId,string,omitempty"`
	ParentAgencyID     int64  `json:"parentAgencyId,string,omitempty"`
	CreatedAtMS        int64  `json:"createdAtMs"`
	UpdatedAtMS        int64  `json:"updatedAtMs"`
}

type ExternalInviteBDRequest struct {
	RequestID        string
	Caller           string
	CommandID        string
	OperatorUserID   int64
	TargetUserID     int64
	ExpectedRegionID int64
}

type ExternalInviteAgencyRequest struct {
	RequestID         string
	Caller            string
	CommandID         string
	OperatorUserID    int64
	ParentBDUserID    int64
	TargetUserID      int64
	AgencyName        string
	ScopeOwnerUserIDs []int64
	ExpectedRegionID  int64
}

type ExternalInviteHostRequest struct {
	RequestID         string
	Caller            string
	CommandID         string
	OperatorUserID    int64
	AgencyID          int64
	TargetUserID      int64
	ScopeOwnerUserIDs []int64
	ExpectedRegionID  int64
}

type ListExternalTeamUsersRequest struct {
	RequestID         string
	Caller            string
	ScopeOwnerUserIDs []int64
	RegionID          int64
	Role              string
	Status            string
	Keyword           string
	Page              int
	PageSize          int
}

type ListExternalTeamUsersResult struct {
	Users    []ExternalTeamUser `json:"items"`
	Total    int64              `json:"total"`
	Page     int                `json:"page"`
	PageSize int                `json:"pageSize"`
}

type SearchExternalInvitationCandidateRequest struct {
	RequestID        string
	Caller           string
	OperatorUserID   int64
	ExpectedRegionID int64
	InvitationType   string
	Keyword          string
}

func (c *GRPCClient) CreateBDLeader(ctx context.Context, req CreateBDLeaderRequest) (*BDProfile, error) {
	resp, err := c.hostAdminClient.CreateBDLeader(ctx, &userv1.CreateBDLeaderRequest{
		Meta:         requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:    req.CommandID,
		AdminUserId:  req.AdminUserID,
		TargetUserId: req.TargetUserID,
		Reason:       req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoBDProfile(resp.GetBdProfile()), nil
}

func (c *GRPCClient) GetRoleScopePolicy(ctx context.Context, req GetRoleScopePolicyRequest) (*RoleScopePolicy, error) {
	resp, err := c.hostClient.GetRoleScopePolicy(ctx, &userv1.GetRoleScopePolicyRequest{
		Meta:  requestMeta(ctx, req.RequestID, req.Caller),
		Scene: req.Scene,
	})
	if err != nil {
		return nil, err
	}
	policy := resp.GetPolicy()
	if policy == nil {
		return nil, fmt.Errorf("role scope policy missing")
	}
	return &RoleScopePolicy{
		Scene:                       policy.GetScene(),
		BaseScope:                   policy.GetBaseScope(),
		ExpandedScope:               policy.GetExpandedScope(),
		RegionExpansionConfigurable: policy.GetRegionExpansionConfigurable(),
		UpdatedAtMS:                 policy.GetUpdatedAtMs(),
	}, nil
}

func (c *GRPCClient) UpdateRoleScopePolicies(ctx context.Context, req UpdateRoleScopePoliciesRequest) ([]RoleScopePolicy, error) {
	policies := make([]*userv1.RoleScopePolicy, 0, len(req.Policies))
	for _, policy := range req.Policies {
		policies = append(policies, &userv1.RoleScopePolicy{
			Scene:                       policy.Scene,
			BaseScope:                   policy.BaseScope,
			ExpandedScope:               policy.ExpandedScope,
			RegionExpansionConfigurable: policy.RegionExpansionConfigurable,
		})
	}
	resp, err := c.hostAdminClient.UpdateRoleScopePolicies(ctx, &userv1.UpdateRoleScopePoliciesRequest{
		Meta:     requestMeta(ctx, req.RequestID, req.Caller),
		Policies: policies,
	})
	if err != nil {
		return nil, err
	}
	stored := make([]RoleScopePolicy, 0, len(resp.GetPolicies()))
	for _, policy := range resp.GetPolicies() {
		if policy == nil {
			continue
		}
		stored = append(stored, RoleScopePolicy{
			Scene:                       policy.GetScene(),
			BaseScope:                   policy.GetBaseScope(),
			ExpandedScope:               policy.GetExpandedScope(),
			RegionExpansionConfigurable: policy.GetRegionExpansionConfigurable(),
			UpdatedAtMS:                 policy.GetUpdatedAtMs(),
		})
	}
	return stored, nil
}

func (c *GRPCClient) CreateBD(ctx context.Context, req CreateBDRequest) (*BDProfile, error) {
	resp, err := c.hostAdminClient.CreateBD(ctx, &userv1.CreateBDRequest{
		Meta:               requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:          req.CommandID,
		AdminUserId:        req.AdminUserID,
		TargetUserId:       req.TargetUserID,
		ParentLeaderUserId: req.ParentLeaderUserID,
		Reason:             req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoBDProfile(resp.GetBdProfile()), nil
}

func (c *GRPCClient) SetBDStatus(ctx context.Context, req SetBDStatusRequest) (*BDProfile, error) {
	resp, err := c.hostAdminClient.SetBDStatus(ctx, &userv1.SetBDStatusRequest{
		Meta:         requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:    req.CommandID,
		AdminUserId:  req.AdminUserID,
		TargetUserId: req.TargetUserID,
		Role:         req.Role,
		Status:       req.Status,
		Reason:       req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoBDProfile(resp.GetBdProfile()), nil
}

func (c *GRPCClient) CreateCoinSeller(ctx context.Context, req CreateCoinSellerRequest) (*CoinSellerProfile, error) {
	protoReq := &userv1.CreateCoinSellerRequest{
		Meta:         requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:    req.CommandID,
		AdminUserId:  req.AdminUserID,
		TargetUserId: req.TargetUserID,
		Reason:       req.Reason,
	}
	if req.CanManageSubCoinSellers != nil {
		// optional bool 只在后台显式提交时设置；nil 表示沿用 user-service 默认值。
		protoReq.CanManageSubCoinSellers = req.CanManageSubCoinSellers
	}
	resp, err := c.hostAdminClient.CreateCoinSeller(ctx, protoReq)
	if err != nil {
		return nil, err
	}
	return fromProtoCoinSellerProfile(resp.GetCoinSellerProfile()), nil
}

func (c *GRPCClient) SetCoinSellerStatus(ctx context.Context, req SetCoinSellerStatusRequest) (*CoinSellerProfile, error) {
	protoReq := &userv1.SetCoinSellerStatusRequest{
		Meta:         requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:    req.CommandID,
		AdminUserId:  req.AdminUserID,
		TargetUserId: req.TargetUserID,
		Status:       req.Status,
		Reason:       req.Reason,
	}
	if req.CanManageSubCoinSellers != nil {
		// nil 表示“只改状态”，false 才表示关闭子币商管理权限。
		protoReq.CanManageSubCoinSellers = req.CanManageSubCoinSellers
	}
	resp, err := c.hostAdminClient.SetCoinSellerStatus(ctx, protoReq)
	if err != nil {
		return nil, err
	}
	return fromProtoCoinSellerProfile(resp.GetCoinSellerProfile()), nil
}

func (c *GRPCClient) ReviewCoinSellerSubApplication(ctx context.Context, req ReviewCoinSellerSubApplicationRequest) (*CoinSellerSubApplicationReviewResult, error) {
	resp, err := c.hostAdminClient.ReviewSubCoinSellerApplication(ctx, &userv1.ReviewSubCoinSellerApplicationRequest{
		Meta:          requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:     req.CommandID,
		AdminUserId:   req.AdminUserID,
		ApplicationId: req.ApplicationID,
		Decision:      req.Decision,
		Reason:        req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return &CoinSellerSubApplicationReviewResult{
		Application: fromProtoCoinSellerSubApplication(resp.GetApplication()),
		Relation:    fromProtoCoinSellerSubRelation(resp.GetRelation()),
		Child:       fromProtoCoinSellerListItem(resp.GetChild()),
	}, nil
}

func (c *GRPCClient) GetCoinSellerProfile(ctx context.Context, req GetCoinSellerProfileRequest) (*CoinSellerProfile, error) {
	resp, err := c.hostClient.GetCoinSellerProfile(ctx, &userv1.GetCoinSellerProfileRequest{
		Meta:   requestMeta(ctx, req.RequestID, req.Caller),
		UserId: req.UserID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoCoinSellerProfile(resp.GetCoinSellerProfile()), nil
}

func (c *GRPCClient) GetUserRoleSummary(ctx context.Context, req GetUserRoleSummaryRequest) (*UserRoleSummary, error) {
	resp, err := c.hostClient.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{
		Meta:   requestMeta(ctx, req.RequestID, req.Caller),
		UserId: req.UserID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoUserRoleSummary(resp.GetSummary()), nil
}

func (c *GRPCClient) CreateAgency(ctx context.Context, req CreateAgencyRequest) (*CreateAgencyResult, error) {
	resp, err := c.hostAdminClient.CreateAgency(ctx, &userv1.CreateAgencyRequest{
		Meta:           requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:      req.CommandID,
		AdminUserId:    req.AdminUserID,
		OwnerUserId:    req.OwnerUserID,
		ParentBdUserId: req.ParentBDUserID,
		Name:           req.Name,
		JoinEnabled:    req.JoinEnabled,
		Reason:         req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return &CreateAgencyResult{
		Agency:      fromProtoAgency(resp.GetAgency()),
		HostProfile: fromProtoHostProfile(resp.GetHostProfile()),
		Membership:  fromProtoAgencyMembership(resp.GetMembership()),
	}, nil
}

func (c *GRPCClient) AdminAddAgencyHost(ctx context.Context, req AdminAddAgencyHostRequest) (*CreateAgencyResult, error) {
	resp, err := c.hostAdminClient.AdminAddAgencyHost(ctx, &userv1.AdminAddAgencyHostRequest{
		Meta:         requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:    req.CommandID,
		AdminUserId:  req.AdminUserID,
		AgencyId:     req.AgencyID,
		TargetUserId: req.TargetUserID,
		Reason:       req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return &CreateAgencyResult{
		Agency:      fromProtoAgency(resp.GetAgency()),
		HostProfile: fromProtoHostProfile(resp.GetHostProfile()),
		Membership:  fromProtoAgencyMembership(resp.GetMembership()),
	}, nil
}

func (c *GRPCClient) CloseAgency(ctx context.Context, req CloseAgencyRequest) (*Agency, error) {
	resp, err := c.hostAdminClient.CloseAgency(ctx, &userv1.CloseAgencyRequest{
		Meta:        requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:   req.CommandID,
		AdminUserId: req.AdminUserID,
		AgencyId:    req.AgencyID,
		Reason:      req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoAgency(resp.GetAgency()), nil
}

func (c *GRPCClient) DeleteAgency(ctx context.Context, req DeleteAgencyRequest) (*Agency, error) {
	resp, err := c.hostAdminClient.DeleteAgency(ctx, &userv1.DeleteAgencyRequest{
		Meta:        requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:   req.CommandID,
		AdminUserId: req.AdminUserID,
		AgencyId:    req.AgencyID,
		Reason:      req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoAgency(resp.GetAgency()), nil
}

func (c *GRPCClient) SetAgencyStatus(ctx context.Context, req SetAgencyStatusRequest) (*Agency, error) {
	resp, err := c.hostAdminClient.SetAgencyStatus(ctx, &userv1.SetAgencyStatusRequest{
		Meta:        requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:   req.CommandID,
		AdminUserId: req.AdminUserID,
		AgencyId:    req.AgencyID,
		Status:      req.Status,
		Reason:      req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoAgency(resp.GetAgency()), nil
}

func (c *GRPCClient) SetAgencyJoinEnabled(ctx context.Context, req SetAgencyJoinEnabledRequest) (*Agency, error) {
	resp, err := c.hostAdminClient.SetAgencyJoinEnabled(ctx, &userv1.SetAgencyJoinEnabledRequest{
		Meta:        requestMeta(ctx, req.RequestID, req.Caller),
		CommandId:   req.CommandID,
		AdminUserId: req.AdminUserID,
		AgencyId:    req.AgencyID,
		JoinEnabled: req.JoinEnabled,
		Reason:      req.Reason,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoAgency(resp.GetAgency()), nil
}

func (c *GRPCClient) ExternalInviteBD(ctx context.Context, req ExternalInviteBDRequest) (*ExternalRoleInvitation, error) {
	resp, err := c.hostAdminClient.ExternalInviteBD(ctx, &userv1.ExternalInviteBDRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller), CommandId: req.CommandID,
		OperatorUserId: req.OperatorUserID, TargetUserId: req.TargetUserID, ExpectedRegionId: req.ExpectedRegionID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoExternalRoleInvitation(resp.GetInvitation()), nil
}

func (c *GRPCClient) ExternalInviteAgency(ctx context.Context, req ExternalInviteAgencyRequest) (*ExternalRoleInvitation, error) {
	resp, err := c.hostAdminClient.ExternalInviteAgency(ctx, &userv1.ExternalInviteAgencyRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller), CommandId: req.CommandID,
		OperatorUserId: req.OperatorUserID, ParentBdUserId: req.ParentBDUserID, TargetUserId: req.TargetUserID,
		AgencyName: req.AgencyName, ScopeOwnerUserIds: append([]int64(nil), req.ScopeOwnerUserIDs...), ExpectedRegionId: req.ExpectedRegionID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoExternalRoleInvitation(resp.GetInvitation()), nil
}

func (c *GRPCClient) ExternalInviteHost(ctx context.Context, req ExternalInviteHostRequest) (*ExternalRoleInvitation, error) {
	resp, err := c.hostAdminClient.ExternalInviteHost(ctx, &userv1.ExternalInviteHostRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller), CommandId: req.CommandID,
		OperatorUserId: req.OperatorUserID, AgencyId: req.AgencyID, TargetUserId: req.TargetUserID,
		ScopeOwnerUserIds: append([]int64(nil), req.ScopeOwnerUserIDs...), ExpectedRegionId: req.ExpectedRegionID,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoExternalRoleInvitation(resp.GetInvitation()), nil
}

func (c *GRPCClient) ListExternalTeamUsers(ctx context.Context, req ListExternalTeamUsersRequest) (*ListExternalTeamUsersResult, error) {
	resp, err := c.hostAdminClient.ListExternalTeamUsers(ctx, &userv1.ListExternalTeamUsersRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller), ScopeOwnerUserIds: append([]int64(nil), req.ScopeOwnerUserIDs...),
		RegionId: req.RegionID, Role: req.Role, Status: req.Status, Keyword: req.Keyword,
		Page: int32(req.Page), PageSize: int32(req.PageSize),
	})
	if err != nil {
		return nil, err
	}
	result := &ListExternalTeamUsersResult{Total: resp.GetTotal(), Page: int(resp.GetPage()), PageSize: int(resp.GetPageSize())}
	result.Users = make([]ExternalTeamUser, 0, len(resp.GetUsers()))
	for _, item := range resp.GetUsers() {
		if converted := fromProtoExternalTeamUser(item); converted != nil {
			result.Users = append(result.Users, *converted)
		}
	}
	return result, nil
}

func (c *GRPCClient) SearchExternalInvitationCandidate(ctx context.Context, req SearchExternalInvitationCandidateRequest) (*ExternalTeamUser, error) {
	resp, err := c.hostAdminClient.SearchExternalInvitationCandidate(ctx, &userv1.SearchExternalInvitationCandidateRequest{
		Meta: requestMeta(ctx, req.RequestID, req.Caller), OperatorUserId: req.OperatorUserID,
		ExpectedRegionId: req.ExpectedRegionID, InvitationType: req.InvitationType, Keyword: req.Keyword,
	})
	if err != nil {
		return nil, err
	}
	return fromProtoExternalTeamUser(resp.GetUser()), nil
}

func fromProtoExternalTeamUser(user *userv1.ExternalTeamUser) *ExternalTeamUser {
	if user == nil {
		return nil
	}
	return &ExternalTeamUser{
		UserID: user.GetUserId(), DefaultDisplayUserID: user.GetDefaultDisplayUserId(),
		CurrentDisplayUserID: user.GetCurrentDisplayUserId(), Username: user.GetUsername(), Avatar: user.GetAvatar(),
		Status: user.GetStatus(), RegionID: user.GetRegionId(), Roles: append([]string(nil), user.GetRoles()...),
		ParentOwnerUserID: user.GetParentOwnerUserId(), ParentBDUserID: user.GetParentBdUserId(), AgencyID: user.GetAgencyId(),
	}
}

func fromProtoExternalRoleInvitation(invitation *userv1.RoleInvitation) *ExternalRoleInvitation {
	if invitation == nil {
		return nil
	}
	return &ExternalRoleInvitation{
		InvitationID: invitation.GetInvitationId(), CommandID: invitation.GetCommandId(),
		InvitationType: invitation.GetInvitationType(), Status: invitation.GetStatus(),
		ExternalOperatorID: invitation.GetExternalOperatorUserId(), TargetUserID: invitation.GetTargetUserId(),
		RegionID: invitation.GetRegionId(), AgencyName: invitation.GetAgencyName(),
		ParentBDUserID: invitation.GetParentBdUserId(), ParentOwnerUserID: invitation.GetParentOwnerUserId(),
		ParentAgencyID: invitation.GetParentAgencyId(), CreatedAtMS: invitation.GetCreatedAtMs(), UpdatedAtMS: invitation.GetUpdatedAtMs(),
	}
}

func requestMeta(ctx context.Context, requestID string, caller string) *userv1.RequestMeta {
	return &userv1.RequestMeta{
		RequestId: requestID,
		Caller:    caller,
		SentAtMs:  time.Now().UnixMilli(),
		AppCode:   appctx.FromContext(ctx),
	}
}

func fromProtoBDProfile(profile *userv1.BDProfile) *BDProfile {
	if profile == nil {
		return nil
	}
	return &BDProfile{
		UserID:             profile.GetUserId(),
		Role:               profile.GetRole(),
		RegionID:           profile.GetRegionId(),
		ParentLeaderUserID: profile.GetParentLeaderUserId(),
		Status:             profile.GetStatus(),
		CreatedByUserID:    profile.GetCreatedByUserId(),
		CreatedAtMs:        profile.GetCreatedAtMs(),
		UpdatedAtMs:        profile.GetUpdatedAtMs(),
	}
}

func fromProtoAgency(agency *userv1.Agency) *Agency {
	if agency == nil {
		return nil
	}
	return &Agency{
		AgencyID:        agency.GetAgencyId(),
		OwnerUserID:     agency.GetOwnerUserId(),
		RegionID:        agency.GetRegionId(),
		ParentBDUserID:  agency.GetParentBdUserId(),
		Name:            agency.GetName(),
		Avatar:          agency.GetAvatar(),
		Status:          agency.GetStatus(),
		JoinEnabled:     agency.GetJoinEnabled(),
		MaxHosts:        agency.GetMaxHosts(),
		CreatedByUserID: agency.GetCreatedByUserId(),
		CreatedAtMs:     agency.GetCreatedAtMs(),
		UpdatedAtMs:     agency.GetUpdatedAtMs(),
		ActiveHostCount: int64(agency.GetHostCount()),
	}
}

func fromProtoHostProfile(profile *userv1.HostProfile) *HostProfile {
	if profile == nil {
		return nil
	}
	return &HostProfile{
		UserID:              profile.GetUserId(),
		Status:              profile.GetStatus(),
		RegionID:            profile.GetRegionId(),
		CurrentAgencyID:     profile.GetCurrentAgencyId(),
		CurrentMembershipID: profile.GetCurrentMembershipId(),
		Source:              profile.GetSource(),
		FirstBecameHostAtMs: profile.GetFirstBecameHostAtMs(),
		CreatedAtMs:         profile.GetCreatedAtMs(),
		UpdatedAtMs:         profile.GetUpdatedAtMs(),
	}
}

func fromProtoCoinSellerProfile(profile *userv1.CoinSellerProfile) *CoinSellerProfile {
	if profile == nil {
		return nil
	}
	return &CoinSellerProfile{
		UserID:                  profile.GetUserId(),
		Status:                  profile.GetStatus(),
		MerchantAssetType:       profile.GetMerchantAssetType(),
		CanManageSubCoinSellers: profile.GetCanManageSubCoinSellers(),
		CreatedByUserID:         profile.GetCreatedByUserId(),
		CreatedAtMs:             profile.GetCreatedAtMs(),
		UpdatedAtMs:             profile.GetUpdatedAtMs(),
	}
}

func fromProtoCoinSellerSubApplication(application *userv1.CoinSellerSubApplication) *CoinSellerSubApplication {
	if application == nil {
		return nil
	}
	return &CoinSellerSubApplication{
		ApplicationID:     application.GetApplicationId(),
		CommandID:         application.GetCommandId(),
		ParentUserID:      application.GetParentUserId(),
		TargetUserID:      application.GetTargetUserId(),
		Status:            application.GetStatus(),
		RelationID:        application.GetRelationId(),
		ReviewedByAdminID: application.GetReviewedByAdminId(),
		ReviewReason:      application.GetReviewReason(),
		ReviewedAtMs:      application.GetReviewedAtMs(),
		CreatedAtMs:       application.GetCreatedAtMs(),
		UpdatedAtMs:       application.GetUpdatedAtMs(),
	}
}

func fromProtoCoinSellerSubRelation(relation *userv1.CoinSellerSubRelation) *CoinSellerSubRelation {
	if relation == nil {
		return nil
	}
	return &CoinSellerSubRelation{
		RelationID:      relation.GetRelationId(),
		ParentUserID:    relation.GetParentUserId(),
		ChildUserID:     relation.GetChildUserId(),
		Status:          relation.GetStatus(),
		CreatedByUserID: relation.GetCreatedByUserId(),
		CreatedAtMs:     relation.GetCreatedAtMs(),
		UpdatedAtMs:     relation.GetUpdatedAtMs(),
	}
}

func fromProtoCoinSellerListItem(item *userv1.CoinSellerListItem) *CoinSellerListItem {
	if item == nil {
		return nil
	}
	return &CoinSellerListItem{
		UserID:                  item.GetUserId(),
		DisplayUserID:           item.GetDisplayUserId(),
		Username:                item.GetUsername(),
		Avatar:                  item.GetAvatar(),
		RegionID:                item.GetRegionId(),
		RegionName:              item.GetRegionName(),
		Status:                  item.GetStatus(),
		MerchantAssetType:       item.GetMerchantAssetType(),
		ContactInfo:             item.GetContactInfo(),
		CanManageSubCoinSellers: item.GetCanManageSubCoinSellers(),
		UpdatedAtMs:             item.GetUpdatedAtMs(),
	}
}

func fromProtoUserRoleSummary(summary *userv1.UserRoleSummary) *UserRoleSummary {
	if summary == nil {
		return nil
	}
	return &UserRoleSummary{
		UserID:                 summary.GetUserId(),
		IsHost:                 summary.GetIsHost(),
		IsAgency:               summary.GetIsAgency(),
		IsBD:                   summary.GetIsBd(),
		IsBDLeader:             summary.GetIsBdLeader(),
		IsCoinSeller:           summary.GetIsCoinSeller(),
		HostStatus:             summary.GetHostStatus(),
		AgencyID:               summary.GetAgencyId(),
		BDID:                   summary.GetBdId(),
		BDStatus:               summary.GetBdStatus(),
		CoinSellerStatus:       summary.GetCoinSellerStatus(),
		PendingRoleInvitations: summary.GetPendingRoleInvitations(),
		IsManager:              summary.GetIsManager(),
	}
}

func fromProtoAgencyMembership(membership *userv1.AgencyMembership) *AgencyMembership {
	if membership == nil {
		return nil
	}
	return &AgencyMembership{
		MembershipID:   membership.GetMembershipId(),
		AgencyID:       membership.GetAgencyId(),
		HostUserID:     membership.GetHostUserId(),
		RegionID:       membership.GetRegionId(),
		MembershipType: membership.GetMembershipType(),
		Status:         membership.GetStatus(),
		JoinedAtMs:     membership.GetJoinedAtMs(),
		EndedAtMs:      membership.GetEndedAtMs(),
		EndedByUserID:  membership.GetEndedByUserId(),
		EndedReason:    membership.GetEndedReason(),
		CreatedAtMs:    membership.GetCreatedAtMs(),
		UpdatedAtMs:    membership.GetUpdatedAtMs(),
	}
}
