// 本包实现 user-service 内 Host/Agency/BD 业务用例。
package host

import (
	"context"
	"strings"
	"time"

	"hyapp/pkg/idgen"
	"hyapp/pkg/xerr"
	hostdomain "hyapp/services/user-service/internal/domain/host"
)

// Repository 是 Host 领域的持久化边界。
// 写方法内部必须开启 MySQL 事务并完成行锁、幂等检查、事实写入和事件箱写入。
type Repository interface {
	SearchAgencies(ctx context.Context, command SearchAgenciesCommand) ([]hostdomain.Agency, error)
	ApplyToAgency(ctx context.Context, command ApplyToAgencyCommand) (hostdomain.AgencyApplication, error)
	ReviewAgencyApplication(ctx context.Context, command ReviewAgencyApplicationCommand) (hostdomain.ReviewAgencyApplicationResult, error)
	KickAgencyHost(ctx context.Context, command KickAgencyHostCommand) (hostdomain.KickAgencyHostResult, error)
	InviteAgency(ctx context.Context, command InviteAgencyCommand) (hostdomain.RoleInvitation, error)
	InviteBD(ctx context.Context, command InviteBDCommand) (hostdomain.RoleInvitation, error)
	InviteHost(ctx context.Context, command InviteHostCommand) (hostdomain.RoleInvitation, error)
	ExternalInviteAgency(ctx context.Context, command ExternalInviteAgencyCommand) (hostdomain.RoleInvitation, error)
	ExternalInviteBD(ctx context.Context, command ExternalInviteBDCommand) (hostdomain.RoleInvitation, error)
	ExternalInviteHost(ctx context.Context, command ExternalInviteHostCommand) (hostdomain.RoleInvitation, error)
	ListExternalTeamUsers(ctx context.Context, command ListExternalTeamUsersCommand) (hostdomain.ExternalTeamUsers, error)
	SearchExternalInvitationCandidate(ctx context.Context, command SearchExternalInvitationCandidateCommand) (hostdomain.ExternalTeamUser, error)
	ListRoleInvitations(ctx context.Context, command ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error)
	GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (hostdomain.RoleInvitation, error)
	ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error)
	ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error)
	ListBDAgencies(ctx context.Context, command ListBDAgenciesCommand) ([]hostdomain.Agency, error)
	ListManagerTeamAgencies(ctx context.Context, command ListManagerTeamAgenciesCommand) (hostdomain.ManagerTeamAgencies, error)
	ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error)
	CreateBDLeader(ctx context.Context, command CreateBDLeaderCommand) (hostdomain.BDProfile, error)
	CreateBD(ctx context.Context, command CreateBDCommand) (hostdomain.BDProfile, error)
	SetBDStatus(ctx context.Context, command SetBDStatusCommand) (hostdomain.BDProfile, error)
	CreateCoinSeller(ctx context.Context, command CreateCoinSellerCommand) (hostdomain.CoinSellerProfile, error)
	SetCoinSellerStatus(ctx context.Context, command SetCoinSellerStatusCommand) (hostdomain.CoinSellerProfile, error)
	CreateSubCoinSeller(ctx context.Context, command CreateSubCoinSellerCommand) (hostdomain.CreateSubCoinSellerResult, error)
	SubmitSubCoinSellerApplication(ctx context.Context, command SubmitSubCoinSellerApplicationCommand) (hostdomain.SubmitSubCoinSellerApplicationResult, error)
	ReviewSubCoinSellerApplication(ctx context.Context, command ReviewSubCoinSellerApplicationCommand) (hostdomain.ReviewSubCoinSellerApplicationResult, error)
	ListSubCoinSellers(ctx context.Context, command ListSubCoinSellersCommand) (hostdomain.ListSubCoinSellersResult, error)
	CheckCoinSellerSubRelation(ctx context.Context, command CheckCoinSellerSubRelationCommand) (hostdomain.CoinSellerListItem, error)
	CreateAgency(ctx context.Context, command CreateAgencyCommand) (hostdomain.CreateAgencyResult, error)
	CloseAgency(ctx context.Context, command CloseAgencyCommand) (hostdomain.Agency, error)
	DeleteAgency(ctx context.Context, command DeleteAgencyCommand) (hostdomain.Agency, error)
	SetAgencyStatus(ctx context.Context, command SetAgencyStatusCommand) (hostdomain.Agency, error)
	SetAgencyJoinEnabled(ctx context.Context, command SetAgencyJoinEnabledCommand) (hostdomain.Agency, error)
	AdminAddAgencyHost(ctx context.Context, command AdminAddAgencyHostCommand) (hostdomain.CreateAgencyResult, error)
	GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error)
	BatchGetHostProfiles(ctx context.Context, userIDs []int64) (map[int64]hostdomain.HostProfile, error)
	GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error)
	GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
	ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error)
	GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
	GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error)
	UpdateAgencyProfile(ctx context.Context, command UpdateAgencyProfileCommand) (hostdomain.Agency, error)
	HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error)
	HasActiveManagerProfile(ctx context.Context, userID int64) (bool, error)
	HasActiveManagerCapability(ctx context.Context, userID int64, capability string) (bool, error)
	ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
	ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
	GetHostEngagementStats(ctx context.Context, query hostdomain.EngagementStatsQuery) (hostdomain.EngagementStats, error)
	RecordPrivateMessageEvent(ctx context.Context, event hostdomain.PrivateMessageEvent) (bool, error)
}

// IDGenerator 生成 Host 领域内部事实 ID。
type IDGenerator interface {
	NewInt64() int64
}

// Service 承载 Host/Agency/BD 用例；它不直接访问数据库，事务语义保留在持久化层。
type Service struct {
	repository                Repository
	roleScopePolicyRepository RoleScopePolicyRepository
	// privateMessageHistory* 只用于 cron 离线同步，不进入任何 Host Center 查询或 IM 实时链路。
	privateMessageHistoryRepository PrivateMessageHistoryRepository
	privateMessageHistoryClient     PrivateMessageHistoryClient
	privateMessageHistorySDKAppID   int64
	idGenerator                     IDGenerator
	now                             func() time.Time
}

// Option 调整 Host 业务层的运行时依赖，测试可以注入固定时钟和确定性发号器。
type Option func(*Service)

// New 创建 Host 领域业务层对象。
func New(repository Repository, options ...Option) *Service {
	svc := &Service{
		repository:  repository,
		idGenerator: idgen.NewInt64Generator(0),
		now:         time.Now,
	}
	if roleScopeRepository, ok := repository.(RoleScopePolicyRepository); ok {
		// 生产 Host repository 同时拥有 App 角色范围小表；自动接入可以保证 App 入口和后台 RPC 读取同一 owner。
		svc.roleScopePolicyRepository = roleScopeRepository
	}
	for _, option := range options {
		option(svc)
	}

	return svc
}

// WithIDGenerator 注入事实 ID 发号器。
func WithIDGenerator(generator IDGenerator) Option {
	return func(s *Service) {
		if generator != nil {
			s.idGenerator = generator
		}
	}
}

// WithClock 注入业务时钟，避免测试依赖真实时间。
func WithClock(now func() time.Time) Option {
	return func(s *Service) {
		if now != nil {
			s.now = now
		}
	}
}

// SearchAgencies 返回当前用户可加入的有效 Agency；持久化层按 owner 配置决定全域或同国家范围。
func (s *Service) SearchAgencies(ctx context.Context, userID int64, keyword string, pageSize int32) ([]hostdomain.Agency, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if userID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
	}
	limit := int(pageSize)
	if limit <= 0 || limit > 100 {
		// 搜索接口默认收敛到小结果集，避免第一阶段误用成全表导出。
		limit = 50
	}

	// 业务层只负责输入归一化；App 级区域策略和用户国家事实都由持久化层统一执行。
	return s.repository.SearchAgencies(ctx, SearchAgenciesCommand{
		UserID:   userID,
		Keyword:  strings.TrimSpace(keyword),
		PageSize: limit,
	})
}

// ApplyToAgency 创建用户加入 Agency 的待处理申请。
func (s *Service) ApplyToAgency(ctx context.Context, command ApplyToAgencyInput) (hostdomain.AgencyApplication, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.AgencyApplication{}, err
	}
	// command_id 来自调用方，是幂等边界；申请 ID 由服务端生成，避免客户端猜测事实主键。
	if strings.TrimSpace(command.CommandID) == "" {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.InvalidArgument, "command_id is required")
	}
	if command.UserID <= 0 || command.AgencyID <= 0 {
		return hostdomain.AgencyApplication{}, xerr.New(xerr.InvalidArgument, "user_id and agency_id are required")
	}
	nowMs := s.now().UnixMilli()

	// 业务层不做 Agency、区域、成员关系判定，避免在无事务上下文里读到过期事实。
	return s.repository.ApplyToAgency(ctx, ApplyToAgencyCommand{
		CommandID:     strings.TrimSpace(command.CommandID),
		ApplicationID: s.idGenerator.NewInt64(),
		UserID:        command.UserID,
		AgencyID:      command.AgencyID,
		NowMs:         nowMs,
	})
}

// ReviewAgencyApplication 审核申请；通过时会在同一事务内创建 Host 身份和有效成员关系。
func (s *Service) ReviewAgencyApplication(ctx context.Context, command ReviewAgencyApplicationInput) (hostdomain.ReviewAgencyApplicationResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.ReviewAgencyApplicationResult{}, err
	}
	// 对外动作统一做空白裁剪和小写化，领域层只接收规范化后的状态机输入。
	decision := normalizeAction(command.Decision)
	if strings.TrimSpace(command.CommandID) == "" || command.ReviewerUserID <= 0 || command.ApplicationID <= 0 {
		return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.InvalidArgument, "command_id, reviewer_user_id and application_id are required")
	}
	if decision != hostdomain.ApplicationDecisionApprove && decision != hostdomain.ApplicationDecisionReject {
		return hostdomain.ReviewAgencyApplicationResult{}, xerr.New(xerr.InvalidArgument, "decision is invalid")
	}
	nowMs := s.now().UnixMilli()

	// 成员关系 ID 和事件 ID 对拒绝动作可能不会被使用；提前生成可以保持命令对象完整，
	// 真正是否落库由持久化层事务状态机决定。
	eventID := s.nextEventIDRange(2)
	return s.repository.ReviewAgencyApplication(ctx, ReviewAgencyApplicationCommand{
		CommandID:      strings.TrimSpace(command.CommandID),
		ReviewerUserID: command.ReviewerUserID,
		ApplicationID:  command.ApplicationID,
		Decision:       decision,
		Reason:         strings.TrimSpace(command.Reason),
		MembershipID:   s.idGenerator.NewInt64(),
		EventID:        eventID,
		NowMs:          nowMs,
	})
}

// KickAgencyHost 只结束普通成员关系；Host 是长期身份，不能随 Agency 归属一起停用。
func (s *Service) KickAgencyHost(ctx context.Context, command KickAgencyHostInput) (hostdomain.KickAgencyHostResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.KickAgencyHostResult{}, err
	}
	// 业务层只校验必填字段；操作者是否为拥有者、Host 是否为普通成员必须在锁内判断。
	if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.AgencyID <= 0 || command.HostUserID <= 0 {
		return hostdomain.KickAgencyHostResult{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id, agency_id and host_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.KickAgencyHost(ctx, KickAgencyHostCommand{
		CommandID:      strings.TrimSpace(command.CommandID),
		OperatorUserID: command.OperatorUserID,
		AgencyID:       command.AgencyID,
		HostUserID:     command.HostUserID,
		Reason:         strings.TrimSpace(command.Reason),
		EventID:        s.nextEventIDRange(1),
		NowMs:          nowMs,
	})
}

// GetAgency 读取 Agency 详情；调用方负责根据业务入口校验 owner 或管理员身份。
func (s *Service) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
	if s.repository == nil {
		return hostdomain.Agency{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if agencyID <= 0 {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency_id is required")
	}
	return s.repository.GetAgency(ctx, agencyID)
}

// UpdateAgencyProfile 只修改 Agency 独立组织资料，不回写 owner 用户昵称或头像。
func (s *Service) UpdateAgencyProfile(ctx context.Context, command UpdateAgencyProfileInput) (hostdomain.Agency, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.Agency{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.AgencyID <= 0 {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id and agency_id are required")
	}
	if command.Name == nil && command.Avatar == nil {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "name or avatar is required")
	}
	var name *string
	if command.Name != nil {
		trimmed := strings.TrimSpace(*command.Name)
		if trimmed == "" {
			return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency name is required")
		}
		name = &trimmed
	}
	var avatar *string
	if command.Avatar != nil {
		trimmed := strings.TrimSpace(*command.Avatar)
		if trimmed == "" {
			return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency avatar is required")
		}
		avatar = &trimmed
	}

	return s.repository.UpdateAgencyProfile(ctx, UpdateAgencyProfileCommand{
		CommandID:      strings.TrimSpace(command.CommandID),
		OperatorUserID: command.OperatorUserID,
		AgencyID:       command.AgencyID,
		Name:           name,
		Avatar:         avatar,
		RequestID:      strings.TrimSpace(command.RequestID),
		EventID:        s.nextEventIDRange(1),
		NowMs:          s.now().UnixMilli(),
	})
}

// InviteAgency 由 BD 或 BD Leader 创建 Agency owner 邀请；目标用户接受前不创建 Agency 身份。
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	// Agency 名称允许邀请方显式覆盖；为空时在目标接受并真正创建 Agency 时复制 owner 当前昵称/短 ID。
	agencyName := strings.TrimSpace(command.AgencyName)
	if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.InviteAgency(ctx, InviteAgencyCommand{
		CommandID:     strings.TrimSpace(command.CommandID),
		InvitationID:  s.idGenerator.NewInt64(),
		InviterUserID: command.InviterUserID,
		TargetUserID:  command.TargetUserID,
		AgencyName:    agencyName,
		NowMs:         nowMs,
	})
}

// InviteBD 由 BD Leader 创建 BD 邀请；目标用户接受前不创建普通 BD 身份。
func (s *Service) InviteBD(ctx context.Context, command InviteBDInput) (hostdomain.RoleInvitation, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	// 负责人身份、App 级区域策略、目标是否已有 BD 都在同一事务中校验，避免发出不可处理的待确认消息。
	if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.InviteBD(ctx, InviteBDCommand{
		CommandID:     strings.TrimSpace(command.CommandID),
		InvitationID:  s.idGenerator.NewInt64(),
		InviterUserID: command.InviterUserID,
		TargetUserID:  command.TargetUserID,
		NowMs:         nowMs,
	})
}

// InviteHost 由 active Agency owner 邀请用户加入自己的 Agency，接受前不创建 Host 或成员关系。
func (s *Service) InviteHost(ctx context.Context, command InviteHostInput) (hostdomain.RoleInvitation, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	// command_id 仍是发送动作的幂等边界；目标处理时会使用新的 command_id 推进邀请终态。
	if strings.TrimSpace(command.CommandID) == "" || command.InviterUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, inviter_user_id and target_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.InviteHost(ctx, InviteHostCommand{
		CommandID:     strings.TrimSpace(command.CommandID),
		InvitationID:  s.idGenerator.NewInt64(),
		InviterUserID: command.InviterUserID,
		TargetUserID:  command.TargetUserID,
		NowMs:         nowMs,
	})
}

// ExternalInviteBD 允许外管 Local、国家外管经理或外管SuperAdmin 直接发展 BD。
// Portal 身份权限由 admin-server 判断；user-service 只接受 App 用户 ID，并在事务内强制 operator/target 同区。
func (s *Service) ExternalInviteBD(ctx context.Context, command ExternalInviteBDInput) (hostdomain.RoleInvitation, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.TargetUserID <= 0 || command.ExpectedRegionID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id, target_user_id and expected_region_id are required")
	}
	return s.repository.ExternalInviteBD(ctx, ExternalInviteBDCommand{
		CommandID:        strings.TrimSpace(command.CommandID),
		InvitationID:     s.idGenerator.NewInt64(),
		OperatorUserID:   command.OperatorUserID,
		TargetUserID:     command.TargetUserID,
		ExpectedRegionID: command.ExpectedRegionID,
		NowMs:            s.now().UnixMilli(),
	})
}

// ExternalInviteAgency 为团队中显式选择的 BD 创建 Agency 邀请。
// scope owner 为空时直接拒绝，避免“没有下级”被错误解释为“允许全 App”。
func (s *Service) ExternalInviteAgency(ctx context.Context, command ExternalInviteAgencyInput) (hostdomain.RoleInvitation, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	scopeOwnerUserIDs, err := normalizeExternalScopeOwnerIDs(command.ScopeOwnerUserIDs)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.ParentBDUserID <= 0 || command.TargetUserID <= 0 || command.ExpectedRegionID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id, parent_bd_user_id, target_user_id and expected_region_id are required")
	}
	return s.repository.ExternalInviteAgency(ctx, ExternalInviteAgencyCommand{
		CommandID:         strings.TrimSpace(command.CommandID),
		InvitationID:      s.idGenerator.NewInt64(),
		OperatorUserID:    command.OperatorUserID,
		ParentBDUserID:    command.ParentBDUserID,
		TargetUserID:      command.TargetUserID,
		AgencyName:        strings.TrimSpace(command.AgencyName),
		ScopeOwnerUserIDs: scopeOwnerUserIDs,
		ExpectedRegionID:  command.ExpectedRegionID,
		NowMs:             s.now().UnixMilli(),
	})
}

// ExternalInviteHost 为团队中显式选择的 Agency 创建 Host 邀请。
func (s *Service) ExternalInviteHost(ctx context.Context, command ExternalInviteHostInput) (hostdomain.RoleInvitation, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	scopeOwnerUserIDs, err := normalizeExternalScopeOwnerIDs(command.ScopeOwnerUserIDs)
	if err != nil {
		return hostdomain.RoleInvitation{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.OperatorUserID <= 0 || command.AgencyID <= 0 || command.TargetUserID <= 0 || command.ExpectedRegionID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "command_id, operator_user_id, agency_id, target_user_id and expected_region_id are required")
	}
	return s.repository.ExternalInviteHost(ctx, ExternalInviteHostCommand{
		CommandID:         strings.TrimSpace(command.CommandID),
		InvitationID:      s.idGenerator.NewInt64(),
		OperatorUserID:    command.OperatorUserID,
		AgencyID:          command.AgencyID,
		TargetUserID:      command.TargetUserID,
		ScopeOwnerUserIDs: scopeOwnerUserIDs,
		ExpectedRegionID:  command.ExpectedRegionID,
		NowMs:             s.now().UnixMilli(),
	})
}

// ListExternalTeamUsers 返回数据库真实分页的团队用户；region 和 scope 缺失都 fail-closed。
func (s *Service) ListExternalTeamUsers(ctx context.Context, command ListExternalTeamUsersCommand) (hostdomain.ExternalTeamUsers, error) {
	if s.repository == nil {
		return hostdomain.ExternalTeamUsers{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	scopeOwnerUserIDs, err := normalizeExternalScopeOwnerIDs(command.ScopeOwnerUserIDs)
	if err != nil {
		return hostdomain.ExternalTeamUsers{}, err
	}
	if command.RegionID <= 0 {
		return hostdomain.ExternalTeamUsers{}, xerr.New(xerr.InvalidArgument, "region_id is required")
	}
	role := strings.ToLower(strings.TrimSpace(command.Role))
	if role != "" && role != hostdomain.BDRoleBD && role != "agency" && role != "host" {
		return hostdomain.ExternalTeamUsers{}, xerr.New(xerr.InvalidArgument, "external team role is invalid")
	}
	status := normalizeStatus(command.Status)
	if status == "" {
		status = "active"
	}
	if status != "active" && status != "disabled" && status != "banned" {
		return hostdomain.ExternalTeamUsers{}, xerr.New(xerr.InvalidArgument, "user status is invalid")
	}
	page := command.Page
	if page <= 0 {
		page = 1
	}
	pageSize := command.PageSize
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 200 {
		pageSize = 200
	}
	return s.repository.ListExternalTeamUsers(ctx, ListExternalTeamUsersCommand{
		ScopeOwnerUserIDs: scopeOwnerUserIDs,
		RegionID:          command.RegionID,
		Role:              role,
		Status:            status,
		Keyword:           strings.TrimSpace(command.Keyword),
		Page:              page,
		PageSize:          pageSize,
	})
}

// SearchExternalInvitationCandidate 精确查找当前区域候选人，禁止 username 模糊搜索扩散用户目录。
func (s *Service) SearchExternalInvitationCandidate(ctx context.Context, command SearchExternalInvitationCandidateCommand) (hostdomain.ExternalTeamUser, error) {
	if s.repository == nil {
		return hostdomain.ExternalTeamUser{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	invitationType := strings.ToLower(strings.TrimSpace(command.InvitationType))
	if command.OperatorUserID <= 0 || command.ExpectedRegionID <= 0 || strings.TrimSpace(command.Keyword) == "" {
		return hostdomain.ExternalTeamUser{}, xerr.New(xerr.InvalidArgument, "operator_user_id, expected_region_id and keyword are required")
	}
	if invitationType != hostdomain.InvitationTypeBD && invitationType != hostdomain.InvitationTypeAgency && invitationType != hostdomain.InvitationTypeHost {
		return hostdomain.ExternalTeamUser{}, xerr.New(xerr.InvalidArgument, "invitation_type is invalid")
	}
	return s.repository.SearchExternalInvitationCandidate(ctx, SearchExternalInvitationCandidateCommand{
		OperatorUserID:   command.OperatorUserID,
		ExpectedRegionID: command.ExpectedRegionID,
		InvitationType:   invitationType,
		Keyword:          strings.TrimSpace(command.Keyword),
		NowMs:            s.now().UnixMilli(),
	})
}

func normalizeExternalScopeOwnerIDs(userIDs []int64) ([]int64, error) {
	if len(userIDs) == 0 {
		return nil, xerr.New(xerr.PermissionDenied, "external team scope is empty")
	}
	if len(userIDs) > 1000 {
		return nil, xerr.New(xerr.InvalidArgument, "external team scope exceeds 1000 owners")
	}
	seen := make(map[int64]struct{}, len(userIDs))
	result := make([]int64, 0, len(userIDs))
	for _, userID := range userIDs {
		if userID <= 0 {
			return nil, xerr.New(xerr.InvalidArgument, "scope_owner_user_ids contains invalid user id")
		}
		if _, ok := seen[userID]; ok {
			continue
		}
		seen[userID] = struct{}{}
		result = append(result, userID)
	}
	if len(result) == 0 {
		return nil, xerr.New(xerr.PermissionDenied, "external team scope is empty")
	}
	return result, nil
}

// ListRoleInvitations 返回当前用户收到的角色/团队邀请收件箱。
func (s *Service) ListRoleInvitations(ctx context.Context, command ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.TargetUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
	}
	return s.repository.ListRoleInvitations(ctx, ListRoleInvitationsCommand{
		TargetUserID: command.TargetUserID,
		Status:       normalizeStatus(command.Status),
		PageSize:     normalizePageSize(command.PageSize),
	})
}

// GetRoleInvitation 返回单条邀请事实；actor 必须是邀请发起人或目标用户，避免内部接口被误用成任意枚举入口。
func (s *Service) GetRoleInvitation(ctx context.Context, actorUserID int64, invitationID int64) (hostdomain.RoleInvitation, error) {
	if s.repository == nil {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if actorUserID <= 0 || invitationID <= 0 {
		return hostdomain.RoleInvitation{}, xerr.New(xerr.InvalidArgument, "actor_user_id and invitation_id are required")
	}
	return s.repository.GetRoleInvitation(ctx, actorUserID, invitationID)
}

// ListBDLeaderBDs 返回当前 BD Leader 直属的有效 BD 列表。
func (s *Service) ListBDLeaderBDs(ctx context.Context, command ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.LeaderUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "leader_user_id is required")
	}
	return s.repository.ListBDLeaderBDs(ctx, ListBDLeaderBDsCommand{
		LeaderUserID: command.LeaderUserID,
		Status:       normalizeStatus(command.Status),
		PageSize:     normalizePageSize(command.PageSize),
	})
}

// ListBDLeaderAgencies 返回当前 BD Leader 团队下的 Agency 列表。
func (s *Service) ListBDLeaderAgencies(ctx context.Context, command ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.LeaderUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "leader_user_id is required")
	}
	return s.repository.ListBDLeaderAgencies(ctx, ListBDLeaderAgenciesCommand{
		LeaderUserID: command.LeaderUserID,
		Status:       normalizeStatus(command.Status),
		PageSize:     normalizePageSize(command.PageSize),
	})
}

// ListBDAgencies 返回当前 BD 直接拓展的 Agency 列表。
func (s *Service) ListBDAgencies(ctx context.Context, command ListBDAgenciesCommand) ([]hostdomain.Agency, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.BDUserID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "bd_user_id is required")
	}
	return s.repository.ListBDAgencies(ctx, ListBDAgenciesCommand{
		BDUserID: command.BDUserID,
		Status:   normalizeStatus(command.Status),
		PageSize: normalizePageSize(command.PageSize),
	})
}

// ListManagerTeamAgencies 返回经理团队树内全部有效 Agency，经理中心团队工资统计据此圈定主播归属。
func (s *Service) ListManagerTeamAgencies(ctx context.Context, command ListManagerTeamAgenciesCommand) (hostdomain.ManagerTeamAgencies, error) {
	if s.repository == nil {
		return hostdomain.ManagerTeamAgencies{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.ManagerUserID <= 0 {
		return hostdomain.ManagerTeamAgencies{}, xerr.New(xerr.InvalidArgument, "manager_user_id is required")
	}
	// 团队统计要求一次拿全 Agency 集合，防御上限比常规分页列表更高；超限用 Truncated 显式标记而不是静默丢数据。
	pageSize := command.PageSize
	if pageSize <= 0 {
		pageSize = 2000
	}
	if pageSize > 5000 {
		pageSize = 5000
	}
	return s.repository.ListManagerTeamAgencies(ctx, ListManagerTeamAgenciesCommand{
		ManagerUserID: command.ManagerUserID,
		PageSize:      pageSize,
	})
}

// ProcessRoleInvitation 接受、拒绝或取消角色邀请。
func (s *Service) ProcessRoleInvitation(ctx context.Context, command ProcessRoleInvitationInput) (hostdomain.ProcessRoleInvitationResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.ProcessRoleInvitationResult{}, err
	}
	// 接受、拒绝、取消共用同一入口；先归一化再进入持久化层状态机。
	action := normalizeAction(command.Action)
	if strings.TrimSpace(command.CommandID) == "" || command.ActorUserID <= 0 || command.InvitationID <= 0 {
		return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "command_id, actor_user_id and invitation_id are required")
	}
	if action != hostdomain.InvitationActionAccept && action != hostdomain.InvitationActionReject && action != hostdomain.InvitationActionCancel {
		return hostdomain.ProcessRoleInvitationResult{}, xerr.New(xerr.InvalidArgument, "action is invalid")
	}
	nowMs := s.now().UnixMilli()

	// Agency ID、成员关系 ID、事件 ID 只在接受 Agency 邀请时使用；
	// 拒绝、取消、接受 BD 邀请可能消耗不到这些预分配 ID，这是开发阶段可接受的设计。
	eventID := s.nextEventIDRange(3)
	return s.repository.ProcessRoleInvitation(ctx, ProcessRoleInvitationCommand{
		CommandID:    strings.TrimSpace(command.CommandID),
		ActorUserID:  command.ActorUserID,
		InvitationID: command.InvitationID,
		Action:       action,
		Reason:       strings.TrimSpace(command.Reason),
		AgencyID:     s.idGenerator.NewInt64(),
		MembershipID: s.idGenerator.NewInt64(),
		EventID:      eventID,
		NowMs:        nowMs,
	})
}

// CreateBDLeader 创建 BD Leader/Admin 身份。
// 后台 admin 的权限在 admin-server 校验；App 经理中心的权限由 CheckBusinessCapability 基于 manager_profiles 校验。
// 本方法只负责统一写入 BD Leader 事实，保证幂等、区域和身份冲突规则不会因入口不同而分叉。
func (s *Service) CreateBDLeader(ctx context.Context, command CreateBDLeaderInput) (hostdomain.BDProfile, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.BDProfile{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.CreateBDLeader(ctx, CreateBDLeaderCommand{
		CommandID:    strings.TrimSpace(command.CommandID),
		AdminUserID:  command.AdminUserID,
		TargetUserID: command.TargetUserID,
		Reason:       strings.TrimSpace(command.Reason),
		RequestID:    strings.TrimSpace(command.RequestID),
		EventID:      s.idGenerator.NewInt64(),
		NowMs:        nowMs,
	})
}

// CreateBD 由后台创建普通 BD；传入父级 Leader 时绑定到该 Leader，未传时创建独立 BD。
func (s *Service) CreateBD(ctx context.Context, command CreateBDInput) (hostdomain.BDProfile, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.BDProfile{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
	}
	if command.ParentLeaderUserID < 0 {
		return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "parent_leader_user_id must not be negative")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.CreateBD(ctx, CreateBDCommand{
		CommandID:          strings.TrimSpace(command.CommandID),
		AdminUserID:        command.AdminUserID,
		TargetUserID:       command.TargetUserID,
		ParentLeaderUserID: command.ParentLeaderUserID,
		Reason:             strings.TrimSpace(command.Reason),
		RequestID:          strings.TrimSpace(command.RequestID),
		EventID:            s.idGenerator.NewInt64(),
		NowMs:              nowMs,
	})
}

// SetBDStatus 调整普通 BD 或 BD Leader 状态；调用方必须传 role，避免同一用户两种身份时误改。
func (s *Service) SetBDStatus(ctx context.Context, command SetBDStatusInput) (hostdomain.BDProfile, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.BDProfile{}, err
	}
	status := normalizeStatus(command.Status)
	role := normalizeBDRole(command.Role)
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 || role == "" {
		return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, target_user_id and role are required")
	}
	if status != hostdomain.BDStatusActive && status != hostdomain.BDStatusDisabled {
		return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "bd status is invalid")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.SetBDStatus(ctx, SetBDStatusCommand{
		CommandID:    strings.TrimSpace(command.CommandID),
		AdminUserID:  command.AdminUserID,
		TargetUserID: command.TargetUserID,
		Role:         role,
		Status:       status,
		Reason:       strings.TrimSpace(command.Reason),
		RequestID:    strings.TrimSpace(command.RequestID),
		EventID:      s.idGenerator.NewInt64(),
		NowMs:        nowMs,
	})
}

// CreateCoinSeller 由后台创建币商身份；该身份可以和 Agency/BD/BD Leader 并存。
func (s *Service) CreateCoinSeller(ctx context.Context, command CreateCoinSellerInput) (hostdomain.CoinSellerProfile, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.CoinSellerProfile{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.CreateCoinSeller(ctx, CreateCoinSellerCommand{
		CommandID:               strings.TrimSpace(command.CommandID),
		AdminUserID:             command.AdminUserID,
		TargetUserID:            command.TargetUserID,
		Reason:                  strings.TrimSpace(command.Reason),
		RequestID:               strings.TrimSpace(command.RequestID),
		CanManageSubCoinSellers: command.CanManageSubCoinSellers,
		EventID:                 s.idGenerator.NewInt64(),
		NowMs:                   nowMs,
	})
}

// SetCoinSellerStatus 调整币商身份状态；停用后 gateway 不能再放行币商转账。
func (s *Service) SetCoinSellerStatus(ctx context.Context, command SetCoinSellerStatusInput) (hostdomain.CoinSellerProfile, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.CoinSellerProfile{}, err
	}
	status := normalizeStatus(command.Status)
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
	}
	if status != hostdomain.CoinSellerStatusActive && status != hostdomain.CoinSellerStatusDisabled {
		return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "coin_seller status is invalid")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.SetCoinSellerStatus(ctx, SetCoinSellerStatusCommand{
		CommandID:               strings.TrimSpace(command.CommandID),
		AdminUserID:             command.AdminUserID,
		TargetUserID:            command.TargetUserID,
		Status:                  status,
		Reason:                  strings.TrimSpace(command.Reason),
		RequestID:               strings.TrimSpace(command.RequestID),
		CanManageSubCoinSellers: command.CanManageSubCoinSellers,
		EventID:                 s.idGenerator.NewInt64(),
		NowMs:                   nowMs,
	})
}

// CreateSubCoinSeller 是历史直开 RPC。审批制上线后保留 proto 兼容，但服务端拒绝新直开请求，避免旧调用方绕过后台审核。
func (s *Service) CreateSubCoinSeller(ctx context.Context, command CreateSubCoinSellerInput) (hostdomain.CreateSubCoinSellerResult, error) {
	if strings.TrimSpace(command.CommandID) == "" || command.ParentUserID <= 0 || command.ChildUserID <= 0 {
		return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.InvalidArgument, "command_id, parent_user_id and child_user_id are required")
	}
	if command.ParentUserID == command.ChildUserID {
		return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.Conflict, "coin seller cannot add self as sub seller")
	}
	return hostdomain.CreateSubCoinSellerResult{}, xerr.New(xerr.PermissionDenied, "sub coin seller invitation requires admin review")
}

// SubmitSubCoinSellerApplication 只创建待审申请；active 直属关系必须等待后台审核通过后再落库。
func (s *Service) SubmitSubCoinSellerApplication(ctx context.Context, command SubmitSubCoinSellerApplicationInput) (hostdomain.SubmitSubCoinSellerApplicationResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.ParentUserID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.InvalidArgument, "command_id, parent_user_id and target_user_id are required")
	}
	if command.ParentUserID == command.TargetUserID {
		return hostdomain.SubmitSubCoinSellerApplicationResult{}, xerr.New(xerr.Conflict, "coin seller cannot add self as sub seller")
	}
	nowMs := s.now().UnixMilli()
	return s.repository.SubmitSubCoinSellerApplication(ctx, SubmitSubCoinSellerApplicationCommand{
		CommandID:     strings.TrimSpace(command.CommandID),
		ApplicationID: s.idGenerator.NewInt64(),
		ParentUserID:  command.ParentUserID,
		TargetUserID:  command.TargetUserID,
		RequestID:     strings.TrimSpace(command.RequestID),
		NowMs:         nowMs,
	})
}

// ReviewSubCoinSellerApplication 是后台审批边界；通过时才创建子币商身份与直属关系。
func (s *Service) ReviewSubCoinSellerApplication(ctx context.Context, command ReviewSubCoinSellerApplicationInput) (hostdomain.ReviewSubCoinSellerApplicationResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, err
	}
	decision := strings.TrimSpace(command.Decision)
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.ApplicationID <= 0 || decision == "" {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, application_id and decision are required")
	}
	if decision != hostdomain.CoinSellerSubApplicationStatusApproved && decision != hostdomain.CoinSellerSubApplicationStatusRejected {
		return hostdomain.ReviewSubCoinSellerApplicationResult{}, xerr.New(xerr.InvalidArgument, "decision must be approved or rejected")
	}
	nowMs := s.now().UnixMilli()
	return s.repository.ReviewSubCoinSellerApplication(ctx, ReviewSubCoinSellerApplicationCommand{
		CommandID:     strings.TrimSpace(command.CommandID),
		AdminUserID:   command.AdminUserID,
		ApplicationID: command.ApplicationID,
		Decision:      decision,
		Reason:        strings.TrimSpace(command.Reason),
		RequestID:     strings.TrimSpace(command.RequestID),
		RelationID:    s.idGenerator.NewInt64(),
		EventID:       s.nextEventIDRange(2),
		NowMs:         nowMs,
	})
}

// ListSubCoinSellers 返回父币商直属子币商列表；权限和 active 身份在存储层按当前事实校验。
func (s *Service) ListSubCoinSellers(ctx context.Context, command ListSubCoinSellersCommand) (hostdomain.ListSubCoinSellersResult, error) {
	if s.repository == nil {
		return hostdomain.ListSubCoinSellersResult{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.ParentUserID <= 0 {
		return hostdomain.ListSubCoinSellersResult{}, xerr.New(xerr.InvalidArgument, "parent_user_id is required")
	}
	page := command.Page
	if page <= 0 {
		page = 1
	}
	pageSize := command.PageSize
	if pageSize <= 0 || pageSize > 100 {
		pageSize = 20
	}
	return s.repository.ListSubCoinSellers(ctx, ListSubCoinSellersCommand{
		ParentUserID: command.ParentUserID,
		Page:         page,
		PageSize:     pageSize,
	})
}

// CheckCoinSellerSubRelation 是转账前的服务端授权边界，避免 H5 隐藏按钮后仍能绕过前端直调。
func (s *Service) CheckCoinSellerSubRelation(ctx context.Context, command CheckCoinSellerSubRelationCommand) (hostdomain.CoinSellerListItem, error) {
	if s.repository == nil {
		return hostdomain.CoinSellerListItem{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if command.ParentUserID <= 0 || command.ChildUserID <= 0 {
		return hostdomain.CoinSellerListItem{}, xerr.New(xerr.InvalidArgument, "parent_user_id and child_user_id are required")
	}
	if command.ParentUserID == command.ChildUserID {
		return hostdomain.CoinSellerListItem{}, xerr.New(xerr.Conflict, "coin seller sub relation is invalid")
	}
	return s.repository.CheckCoinSellerSubRelation(ctx, command)
}

// CreateAgency 由后台直接创建 Agency；父级 BD 可选，并原子创建或复用 owner 的 host_profile 和 owner membership。
func (s *Service) CreateAgency(ctx context.Context, command CreateAgencyInput) (hostdomain.CreateAgencyResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.CreateAgencyResult{}, err
	}
	name := strings.TrimSpace(command.Name)
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.OwnerUserID <= 0 {
		return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and owner_user_id are required")
	}
	if command.ParentBDUserID < 0 {
		return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "parent_bd_user_id must not be negative")
	}
	nowMs := s.now().UnixMilli()

	eventID := s.nextEventIDRange(3)
	return s.repository.CreateAgency(ctx, CreateAgencyCommand{
		CommandID:      strings.TrimSpace(command.CommandID),
		AdminUserID:    command.AdminUserID,
		OwnerUserID:    command.OwnerUserID,
		ParentBDUserID: command.ParentBDUserID,
		Name:           name,
		JoinEnabled:    command.JoinEnabled,
		Reason:         strings.TrimSpace(command.Reason),
		RequestID:      strings.TrimSpace(command.RequestID),
		AgencyID:       s.idGenerator.NewInt64(),
		MembershipID:   s.idGenerator.NewInt64(),
		EventID:        eventID,
		NowMs:          nowMs,
	})
}

// CloseAgency 关闭 Agency；当前阶段只关闭组织和入会开关，不追溯改历史成员关系。
func (s *Service) CloseAgency(ctx context.Context, command CloseAgencyInput) (hostdomain.Agency, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.Agency{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.CloseAgency(ctx, CloseAgencyCommand{
		CommandID:   strings.TrimSpace(command.CommandID),
		AdminUserID: command.AdminUserID,
		AgencyID:    command.AgencyID,
		Reason:      strings.TrimSpace(command.Reason),
		RequestID:   strings.TrimSpace(command.RequestID),
		EventID:     s.idGenerator.NewInt64(),
		NowMs:       nowMs,
	})
}

// DeleteAgency 删除 Agency 身份；它只解除当前 Agency 关系，不删除主播身份和历史记录。
func (s *Service) DeleteAgency(ctx context.Context, command DeleteAgencyInput) (hostdomain.Agency, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.Agency{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.DeleteAgency(ctx, DeleteAgencyCommand{
		CommandID:   strings.TrimSpace(command.CommandID),
		AdminUserID: command.AdminUserID,
		AgencyID:    command.AgencyID,
		Reason:      strings.TrimSpace(command.Reason),
		RequestID:   strings.TrimSpace(command.RequestID),
		EventID:     s.idGenerator.NewInt64(),
		NowMs:       nowMs,
	})
}

// SetAgencyStatus 显式切换 Agency 可用状态；closed 可恢复为 active，但 deleted 是删除事实，不能复活。
func (s *Service) SetAgencyStatus(ctx context.Context, command SetAgencyStatusInput) (hostdomain.Agency, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.Agency{}, err
	}
	status := normalizeStatus(command.Status)
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
	}
	if status != hostdomain.AgencyStatusActive && status != hostdomain.AgencyStatusClosed {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency status is invalid")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.SetAgencyStatus(ctx, SetAgencyStatusCommand{
		CommandID:   strings.TrimSpace(command.CommandID),
		AdminUserID: command.AdminUserID,
		AgencyID:    command.AgencyID,
		Status:      status,
		Reason:      strings.TrimSpace(command.Reason),
		RequestID:   strings.TrimSpace(command.RequestID),
		EventID:     s.idGenerator.NewInt64(),
		NowMs:       nowMs,
	})
}

// SetAgencyJoinEnabled 设置 Agency 是否允许 App 用户申请加入。
func (s *Service) SetAgencyJoinEnabled(ctx context.Context, command SetAgencyJoinEnabledInput) (hostdomain.Agency, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.Agency{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 {
		return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and agency_id are required")
	}
	nowMs := s.now().UnixMilli()

	return s.repository.SetAgencyJoinEnabled(ctx, SetAgencyJoinEnabledCommand{
		CommandID:   strings.TrimSpace(command.CommandID),
		AdminUserID: command.AdminUserID,
		AgencyID:    command.AgencyID,
		JoinEnabled: command.JoinEnabled,
		Reason:      strings.TrimSpace(command.Reason),
		RequestID:   strings.TrimSpace(command.RequestID),
		EventID:     s.idGenerator.NewInt64(),
		NowMs:       nowMs,
	})
}

// AdminAddAgencyHost 由后台直接把目标用户添加为 Agency 普通 Host。
func (s *Service) AdminAddAgencyHost(ctx context.Context, command AdminAddAgencyHostInput) (hostdomain.CreateAgencyResult, error) {
	if err := s.requireWriteDependencies(); err != nil {
		return hostdomain.CreateAgencyResult{}, err
	}
	if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.AgencyID <= 0 || command.TargetUserID <= 0 {
		return hostdomain.CreateAgencyResult{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, agency_id and target_user_id are required")
	}
	nowMs := s.now().UnixMilli()

	// 一个直接添加动作最多创建/恢复 Host 身份并创建 membership 两类事实，因此一次占用两个连续 outbox ID。
	return s.repository.AdminAddAgencyHost(ctx, AdminAddAgencyHostCommand{
		CommandID:    strings.TrimSpace(command.CommandID),
		AdminUserID:  command.AdminUserID,
		AgencyID:     command.AgencyID,
		TargetUserID: command.TargetUserID,
		Reason:       strings.TrimSpace(command.Reason),
		RequestID:    strings.TrimSpace(command.RequestID),
		MembershipID: s.idGenerator.NewInt64(),
		EventID:      s.nextEventIDRange(2),
		NowMs:        nowMs,
	})
}

// GetHostProfile 读取主播身份事实。
func (s *Service) GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error) {
	if s.repository == nil {
		return hostdomain.HostProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if userID <= 0 {
		return hostdomain.HostProfile{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}

	return s.repository.GetHostProfile(ctx, userID)
}

// BatchGetHostProfiles 批量读取主播身份事实，缺失用户不返回占位对象。
func (s *Service) BatchGetHostProfiles(ctx context.Context, userIDs []int64) (map[int64]hostdomain.HostProfile, error) {
	if len(userIDs) == 0 {
		// 空批量请求是合法的无操作，调用方可以直接按空 map 组装结果。
		return map[int64]hostdomain.HostProfile{}, nil
	}
	uniqueUserIDs := make([]int64, 0, len(userIDs))
	seen := make(map[int64]bool, len(userIDs))
	for _, userID := range userIDs {
		if userID <= 0 {
			// 任一非法 ID 都表示调用方组装的批量请求不可信，不能静默跳过。
			return nil, xerr.New(xerr.InvalidArgument, "user_ids contains invalid value")
		}
		if seen[userID] {
			continue
		}
		seen[userID] = true
		uniqueUserIDs = append(uniqueUserIDs, userID)
	}
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}

	return s.repository.BatchGetHostProfiles(ctx, uniqueUserIDs)
}

// GetBDProfile 按指定 role 读取普通 BD 或 BD Leader 身份事实。
func (s *Service) GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error) {
	if s.repository == nil {
		return hostdomain.BDProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	role = normalizeBDRole(role)
	if userID <= 0 || role == "" {
		return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "user_id and role are required")
	}

	return s.repository.GetBDProfile(ctx, userID, role)
}

func normalizeBDRole(role string) string {
	switch strings.ToLower(strings.TrimSpace(role)) {
	case hostdomain.BDRoleBD:
		return hostdomain.BDRoleBD
	case hostdomain.BDRoleLeader:
		return hostdomain.BDRoleLeader
	default:
		return ""
	}
}

// GetCoinSellerProfile 读取币商身份事实，供 gateway 在发起币商转账前校验。
func (s *Service) GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error) {
	if s.repository == nil {
		return hostdomain.CoinSellerProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if userID <= 0 {
		return hostdomain.CoinSellerProfile{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}

	return s.repository.GetCoinSellerProfile(ctx, userID)
}

// ListActiveCoinSellersInMyRegion 返回当前用户所在区域的所有启用币商。
func (s *Service) ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if userID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
	}

	// 当前区域属于 user-service 用户主事实，gateway 不能传入 region_id 来扩大查询范围。
	return s.repository.ListActiveCoinSellersInMyRegion(ctx, userID)
}

// GetUserRoleSummary 读取 App 我的页使用的轻量身份摘要，避免 gateway 为入口显隐发起多个角色 RPC。
func (s *Service) GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error) {
	if s.repository == nil {
		return hostdomain.UserRoleSummary{}, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if userID <= 0 {
		return hostdomain.UserRoleSummary{}, xerr.New(xerr.InvalidArgument, "user_id is required")
	}

	return s.repository.GetUserRoleSummary(ctx, userID)
}

// ListAgencyMembers 读取 Agency 成员关系列表，第一阶段直接读事实表。
func (s *Service) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if agencyID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "agency_id is required")
	}

	return s.repository.ListAgencyMembers(ctx, agencyID, normalizeStatus(status))
}

// ListAgencyApplications 读取 Agency 申请列表，第一阶段直接读事实表。
func (s *Service) ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error) {
	if s.repository == nil {
		return nil, xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if agencyID <= 0 {
		return nil, xerr.New(xerr.InvalidArgument, "agency_id is required")
	}

	return s.repository.ListAgencyApplications(ctx, agencyID, normalizeStatus(status))
}

func (s *Service) requireWriteDependencies() error {
	// 写路径必须具备持久化层、ID 发号器和时钟；缺任何一个都会破坏事务事实或事件时间。
	if s.repository == nil {
		return xerr.New(xerr.Unavailable, "host repository is not configured")
	}
	if s.idGenerator == nil {
		return xerr.New(xerr.Unavailable, "host id generator is not configured")
	}
	if s.now == nil {
		return xerr.New(xerr.Unavailable, "host clock is not configured")
	}
	return nil
}

func (s *Service) nextEventIDRange(count int) int64 {
	// 部分 host 事务会在同一命令里写多条 outbox；这里一次性占用连续事件 ID，避免下一条命令拿到 eventID+1 后和本事务内部事件冲突。
	first := s.idGenerator.NewInt64()
	for i := 1; i < count; i++ {
		s.idGenerator.NewInt64()
	}
	return first
}

func normalizeAction(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func normalizeStatus(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func normalizePageSize(value int) int {
	if value <= 0 {
		return 20
	}
	if value > 100 {
		return 100
	}
	return value
}
